From 6ba160954a1a6dbab01db5956df97f3285363da3 Mon Sep 17 00:00:00 2001 From: xufulong Date: Thu, 24 Feb 2022 16:27:30 +0800 Subject: [PATCH] Add: java of FFmpegMediaRetriever --- app/src/main/cpp/metadata/media_retriever.cpp | 14 +- .../main/cpp/metadata/media_retriever_jni.cpp | 37 +- .../ffmpeg/metadata/FFmpegMediaRetriever.java | 322 ++++++++++++++++++ 3 files changed, 347 insertions(+), 26 deletions(-) create mode 100755 app/src/main/java/com/frank/ffmpeg/metadata/FFmpegMediaRetriever.java diff --git a/app/src/main/cpp/metadata/media_retriever.cpp b/app/src/main/cpp/metadata/media_retriever.cpp index d37ba46..92b6d93 100755 --- a/app/src/main/cpp/metadata/media_retriever.cpp +++ b/app/src/main/cpp/metadata/media_retriever.cpp @@ -13,42 +13,42 @@ MediaRetriever::MediaRetriever() MediaRetriever::~MediaRetriever() { - Mutex::Autolock _l(mLock); + Mutex::Autolock lock(mLock); ::release(&state); } int MediaRetriever::setDataSource(const char *srcUrl) { - Mutex::Autolock _l(mLock); + Mutex::Autolock lock(mLock); return ::set_data_source(&state, srcUrl); } int MediaRetriever::setDataSource(int fd, int64_t offset, int64_t length) { - Mutex::Autolock _l(mLock); + Mutex::Autolock lock(mLock); return ::set_data_source_fd(&state, fd, offset, length); } const char* MediaRetriever::extractMetadata(const char *key) { - Mutex::Autolock _l(mLock); + Mutex::Autolock lock(mLock); return ::extract_metadata(&state, key); } int MediaRetriever::getFrameAtTime(int64_t timeUs, int option, AVPacket *pkt) { - Mutex::Autolock _l(mLock); + Mutex::Autolock lock(mLock); return ::get_frame_at_time(&state, timeUs, option, pkt); } int MediaRetriever::getScaledFrameAtTime(int64_t timeUs, int option, AVPacket *pkt, int width, int height) { - Mutex::Autolock _l(mLock); + Mutex::Autolock lock(mLock); return ::get_scaled_frame_at_time(&state, timeUs, option, pkt, width, height); } int MediaRetriever::setNativeWindow(ANativeWindow* native_window) { - Mutex::Autolock _l(mLock); + Mutex::Autolock lock(mLock); return ::set_native_window(&state, native_window); } \ No newline at end of file diff --git a/app/src/main/cpp/metadata/media_retriever_jni.cpp b/app/src/main/cpp/metadata/media_retriever_jni.cpp index 6c89ba5..8966503 100755 --- a/app/src/main/cpp/metadata/media_retriever_jni.cpp +++ b/app/src/main/cpp/metadata/media_retriever_jni.cpp @@ -19,6 +19,9 @@ struct fields_t { static fields_t fields; static const char* mClassName = "com/frank/ffmpeg/metadata/FFmpegMediaRetriever"; +static const char* mNoAvailableMsg = "No retriever available"; +static const char* mIllegalStateException = "java/lang/IllegalStateException"; +static const char* mIllegalArgException = "java/lang/IllegalArgumentException"; static jstring NewStringUTF(JNIEnv* env, const char* data) { jstring str = nullptr; @@ -54,7 +57,7 @@ void jniThrowException(JNIEnv* env, const char* className, static void process_retriever_call(JNIEnv *env, int status, const char* exception, const char *message) { if (status == -2) { - jniThrowException(env, "java/lang/IllegalStateException", nullptr); + jniThrowException(env, mIllegalStateException, nullptr); } else if (status == -1) { if (strlen(message) > 520) { jniThrowException( env, exception, message); @@ -90,7 +93,7 @@ RETRIEVER_FUNC(void, native_1init) return; } - fields.context = env->GetFieldID(clazz, "mNativeContext", "J"); + fields.context = env->GetFieldID(clazz, "mNativeRetriever", "J"); if (fields.context == nullptr) { return; } @@ -102,11 +105,11 @@ RETRIEVER_FUNC(void, native_1init) RETRIEVER_FUNC(void, native_1setDataSource, jstring path) { MediaRetriever* retriever = getRetriever(env, thiz); if (retriever == nullptr) { - jniThrowException(env, "java/lang/IllegalStateException", "No retriever available"); + jniThrowException(env, mIllegalStateException, mNoAvailableMsg); return; } if (!path) { - jniThrowException(env, "java/lang/IllegalArgumentException", "Null pointer"); + jniThrowException(env, mIllegalArgException, "Null of path"); return; } const char *tmp = env->GetStringUTFChars(path, nullptr); @@ -114,12 +117,8 @@ RETRIEVER_FUNC(void, native_1setDataSource, jstring path) { return; } - process_retriever_call( - env, - retriever->setDataSource(tmp), - "java/lang/IllegalArgumentException", - "setDataSource failed"); - + process_retriever_call(env, retriever->setDataSource(tmp), + mIllegalArgException,"setDataSource failed"); env->ReleaseStringUTFChars(path, tmp); } @@ -144,28 +143,28 @@ RETRIEVER_FUNC(void, native_1setDataSourceFD, jobject fileDescriptor, jlong offs } MediaRetriever* retriever = getRetriever(env, thiz); if (retriever == nullptr) { - jniThrowException(env, "java/lang/IllegalStateException", "No retriever available"); + jniThrowException(env, mIllegalStateException, mNoAvailableMsg); return; } if (!fileDescriptor) { - jniThrowException(env, "java/lang/IllegalArgumentException", nullptr); + jniThrowException(env, mIllegalArgException, nullptr); return; } int fd = getFileDescriptor(env, fileDescriptor); if (fd < 0) { LOGE(LOG_TAG, "invalid file descriptor!"); - jniThrowException(env, "java/lang/IllegalArgumentException", nullptr); + jniThrowException(env, mIllegalArgException, nullptr); return; } process_retriever_call(env, retriever->setDataSource(fd, offset, length), - "java/lang/RuntimeException", "setDataSource failed"); + "java/lang/IOException", "setDataSource failed"); } RETRIEVER_FUNC(void, native_1setSurface, jobject surface) { MediaRetriever* retriever = getRetriever(env, thiz); if (retriever == nullptr) { - jniThrowException(env, "java/lang/IllegalStateException", "No retriever available"); + jniThrowException(env, mIllegalStateException, mNoAvailableMsg); return; } ANativeWindow *mNativeWindow = ANativeWindow_fromSurface(env, surface); @@ -178,11 +177,11 @@ RETRIEVER_FUNC(jobject, native_1extractMetadata, jstring jkey) { MediaRetriever* retriever = getRetriever(env, thiz); if (retriever == nullptr) { - jniThrowException(env, "java/lang/IllegalStateException", "No retriever available"); + jniThrowException(env, mIllegalStateException, mNoAvailableMsg); return nullptr; } if (!jkey) { - jniThrowException(env, "java/lang/IllegalArgumentException", "Null pointer"); + jniThrowException(env, mIllegalArgException, "Null of key"); return nullptr; } const char *key = env->GetStringUTFChars(jkey, nullptr); @@ -201,7 +200,7 @@ RETRIEVER_FUNC(jbyteArray, native_1getFrameAtTime, jlong timeUs, jint option) { MediaRetriever* retriever = getRetriever(env, thiz); if (retriever == nullptr) { - jniThrowException(env, "java/lang/IllegalStateException", "No retriever available"); + jniThrowException(env, mIllegalStateException, mNoAvailableMsg); return nullptr; } @@ -233,7 +232,7 @@ RETRIEVER_FUNC(jbyteArray, native_1getScaleFrameAtTime, jlong timeUs, jint optio { MediaRetriever* retriever = getRetriever(env, thiz); if (retriever == nullptr) { - jniThrowException(env, "java/lang/IllegalStateException", "No retriever available"); + jniThrowException(env, mIllegalStateException, mNoAvailableMsg); return nullptr; } diff --git a/app/src/main/java/com/frank/ffmpeg/metadata/FFmpegMediaRetriever.java b/app/src/main/java/com/frank/ffmpeg/metadata/FFmpegMediaRetriever.java new file mode 100755 index 0000000..fa90325 --- /dev/null +++ b/app/src/main/java/com/frank/ffmpeg/metadata/FFmpegMediaRetriever.java @@ -0,0 +1,322 @@ + +package com.frank.ffmpeg.metadata; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.IOException; + +/** + * Retrieve frame and extract metadata from an input media file. + */ +public class FFmpegMediaRetriever { + + static { + try { + System.loadLibrary("media-handle"); + } catch (UnsatisfiedLinkError e) { + Log.e("FFmpegMediaRetriever", "loadLibrary error=" + e.toString()); + } + } + + // The field is accessed by native method + private long mNativeRetriever; + + public FFmpegMediaRetriever() { + native_init(); + native_setup(); + } + + public void setDataSource(String path) { + native_setDataSource(path); + } + + /** + * Sets the data source (FileDescriptor) to use. It is the caller's + * responsibility to close the file descriptor. It is safe to do so as soon + * as this call returns. Call this method before the rest of the methods in + * this class. This method may be time-consuming. + * + * @param fd the FileDescriptor for the file you want to play + */ + public void setDataSource(FileDescriptor fd) + throws IllegalArgumentException { + if (fd == null) { + return; + } + native_setDataSourceFD(fd, 0, 0x7ffffffffffffffL); + } + + /** + * Sets the data source as a content Uri. Call this method before + * the rest of the methods in this class. This method may be time-consuming. + * + * @param context the Context to use when resolving the Uri + * @param uri the Content URI of the data you want to play + * + */ + public void setDataSource(Context context, Uri uri) throws IllegalArgumentException, SecurityException { + if (uri == null) { + throw new IllegalArgumentException(); + } + + String scheme = uri.getScheme(); + if(scheme == null || scheme.equals("file")) { + setDataSource(uri.getPath()); + return; + } + + AssetFileDescriptor fd = null; + try { + ContentResolver resolver = context.getContentResolver(); + try { + fd = resolver.openAssetFileDescriptor(uri, "r"); + } catch(FileNotFoundException e) { + throw new IllegalArgumentException(); + } + if (fd == null) { + throw new IllegalArgumentException(); + } + FileDescriptor descriptor = fd.getFileDescriptor(); + if (!descriptor.valid()) { + throw new IllegalArgumentException(); + } + if (fd.getDeclaredLength() < 0) { + setDataSource(descriptor); + } else { + native_setDataSourceFD(descriptor, fd.getStartOffset(), fd.getDeclaredLength()); + } + return; + } catch (SecurityException ex) { + ex.printStackTrace(); + } finally { + try { + if (fd != null) { + fd.close(); + } + } catch(IOException ioEx) { + ioEx.printStackTrace(); + } + } + setDataSource(uri.toString()); + } + + public String extractMetadata(String key) { + if (key == null || key.isEmpty()) { + return null; + } + return native_extractMetadata(key); + } + + /** + * Call this method after setDataSource(). This method finds a + * representative frame close to the given time position by considering + * the given option if possible, and returns it as a bitmap. This is + * useful for generating a thumbnail for an input data source or just + * obtain and display a frame at the given time position. + * + * @param timeUs The time position where the frame will be retrieved. + * + * @param option a hint on how the frame is found. Use + * {@link #OPTION_PREVIOUS_SYNC} if one wants to retrieve a sync frame + * that has a timestamp earlier than or the same as timeUs. Use + * {@link #OPTION_NEXT_SYNC} if one wants to retrieve a sync frame + * that has a timestamp later than or the same as timeUs. Use + * {@link #OPTION_CLOSEST_SYNC} if one wants to retrieve a sync frame + * that has a timestamp closest to or the same as timeUs. Use + * {@link #OPTION_CLOSEST} if one wants to retrieve a frame that may + * or may not be a sync frame but is closest to or the same as timeUs. + * {@link #OPTION_CLOSEST} often has larger performance overhead compared + * to the other options if there is no sync frame located at timeUs. + * + * @return A Bitmap containing a representative video frame, which + * can be null, if such a frame cannot be retrieved. + */ + public Bitmap getFrameAtTime(long timeUs, int option) { + if (option < OPTION_PREVIOUS_SYNC || + option > OPTION_CLOSEST) { + throw new IllegalArgumentException("Unsupported option: " + option); + } + + BitmapFactory.Options bitmapOptions= new BitmapFactory.Options(); + bitmapOptions.inScaled = true; + byte [] picture = native_getFrameAtTime(timeUs, option); + if (picture != null) { + return BitmapFactory.decodeByteArray(picture, 0, picture.length, bitmapOptions); + } + + return null; + } + + public Bitmap getFrameAtTime(long timeUs) { + return getFrameAtTime(timeUs, OPTION_CLOSEST_SYNC); + } + + /** + * Call this method after setDataSource(). This method finds a + * representative frame close to the given time position by considering + * the given option if possible, and returns it as a bitmap. This is + * useful for generating a thumbnail for an input data source or just + * obtain and display a frame at the given time position. + * + * @param timeUs The time position where the frame will be retrieved. + * + * @param option a hint on how the frame is found. Use + * {@link #OPTION_PREVIOUS_SYNC} if one wants to retrieve a sync frame + * that has a timestamp earlier than or the same as timeUs. Use + * {@link #OPTION_NEXT_SYNC} if one wants to retrieve a sync frame + * that has a timestamp later than or the same as timeUs. Use + * {@link #OPTION_CLOSEST_SYNC} if one wants to retrieve a sync frame + * that has a timestamp closest to or the same as timeUs. Use + * {@link #OPTION_CLOSEST} if one wants to retrieve a frame that may + * or may not be a sync frame but is closest to or the same as timeUs. + * {@link #OPTION_CLOSEST} often has larger performance overhead compared + * to the other options if there is no sync frame located at timeUs. + * + * @return A Bitmap containing a representative video frame, which + * can be null, if such a frame cannot be retrieved. + */ + public Bitmap getScaledFrameAtTime(long timeUs, int option, int width, int height) { + if (option < OPTION_PREVIOUS_SYNC || + option > OPTION_CLOSEST) { + throw new IllegalArgumentException("Unsupported option: " + option); + } + + BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); + bitmapOptions.inScaled = true; + byte [] picture = native_getScaleFrameAtTime(timeUs, option, width, height); + if (picture != null) { + return BitmapFactory.decodeByteArray(picture, 0, picture.length, bitmapOptions); + } + + return null; + } + + public Bitmap getScaledFrameAtTime(long timeUs, int width, int height) { + return getScaledFrameAtTime(timeUs, OPTION_CLOSEST_SYNC, width, height); + } + + public void release() { + native_release(); + } + + private native void native_setup(); + + private native void native_init(); + + private native void native_setDataSource(String path) throws IllegalArgumentException; + + private native void native_setDataSourceFD(FileDescriptor fd, long offset, long length) throws IllegalArgumentException; + + private native String native_extractMetadata(String key); + + private native void native_setSurface(Object surface); + + private native byte[] native_getFrameAtTime(long timeUs, int option); + + private native byte[] native_getScaleFrameAtTime(long timeUs, int option, int width, int height); + + private native void native_release(); + + /** + * This option is used with {@link #getFrameAtTime(long, int)} to retrieve + * a sync (or key) frame associated with a data source that is located + * right before or at the given time. + * + * @see #getFrameAtTime(long, int) + */ + int OPTION_PREVIOUS_SYNC = 0x00; + /** + * This option is used with {@link #getFrameAtTime(long, int)} to retrieve + * a sync (or key) frame associated with a data source that is located + * right after or at the given time. + * + * @see #getFrameAtTime(long, int) + */ + int OPTION_NEXT_SYNC = 0x01; + /** + * This option is used with {@link #getFrameAtTime(long, int)} to retrieve + * a sync (or key) frame associated with a data source that is located + * closest to (in time) or at the given time. + * + * @see #getFrameAtTime(long, int) + */ + int OPTION_CLOSEST_SYNC = 0x02; + /** + * This option is used with {@link #getFrameAtTime(long, int)} to retrieve + * a frame (not necessarily a key frame) associated with a data source that + * is located closest to or at the given time. + * + * @see #getFrameAtTime(long, int) + */ + int OPTION_CLOSEST = 0x03; + + + /** + * The metadata key to retrieve the original name of the file. + */ + public static String METADATA_KEY_FILENAME = "filename"; + /** + * The metadata key to retrieve the main language in which the work is performed, preferably + * in ISO 639-2 format. Multiple languages can be specified by separating them with commas. + */ + public static String METADATA_KEY_LANGUAGE = "language"; + /** + * The metadata key to retrieve the name of the work. + */ + public static String METADATA_KEY_TITLE = "title"; + /** + * The metadata key to retrieve the number of this work in the set, can be in form current/total. + */ + public static String METADATA_KEY_TRACK = "track"; + /** + * The metadata key to retrieve the total bitrate of the bitrate variant that the current stream + * is part of. + */ + public static String METADATA_KEY_VARIANT_BITRATE = "bitrate"; + /** + * The metadata key to retrieve the duration of the work in milliseconds. + */ + public static String METADATA_KEY_DURATION = "duration"; + /** + * The metadata key to retrieve the audio codec of the work. + */ + public static String METADATA_KEY_AUDIO_CODEC = "audio_codec"; + /** + * The metadata key to retrieve the video codec of the work. + */ + public static String METADATA_KEY_VIDEO_CODEC = "video_codec"; + /** + * This key retrieves the video rotation angle in degrees, if available. + * The video rotation angle may be 0, 90, 180, or 270 degrees. + */ + public static String METADATA_KEY_VIDEO_ROTATION = "rotate"; + /** + * This metadata key retrieves the average framerate (in frames/sec). + */ + public static String METADATA_KEY_FRAMERATE = "framerate"; + /** + * The metadata key to retrieve the file size in bytes. + */ + public static String METADATA_KEY_FILESIZE = "filesize"; + /** + * The metadata key to retrieve the video width. + */ + public static String METADATA_KEY_VIDEO_WIDTH = "video_width"; + /** + * The metadata key to retrieve the video height. + */ + public static String METADATA_KEY_VIDEO_HEIGHT = "video_height"; + /** + * The metadata key to retrieve the mime type. + */ + public static String METADATA_KEY_MIME_TYPE = "mime_type"; + +}