Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a new MediaThumbnailRetriever class that provides a lightweight API for extracting video thumbnails and embedded artwork from media files. The implementation uses FFmpeg's libraries through JNI to decode frames at specific timestamps or frame indices, with automatic rotation handling based on video metadata.
Changes:
- Added
MediaThumbnailRetrieverKotlin class providing frame extraction APIs similar to Android'sMediaMetadataRetrieverbut focused on thumbnails - Implemented C++ JNI layer using FFmpeg libraries for video decoding and frame extraction
- Added ProGuard rules to preserve native methods from obfuscation
- Updated CMakeLists.txt to include the new C++ source file
- Updated .gitignore to exclude ffmpeg build artifacts
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| MediaThumbnailRetriever.kt | New Kotlin class providing thumbnail retrieval API with multiple data source setters (path, file descriptor, URI), frame extraction at time/index, and embedded picture retrieval |
| media_thumbnail_retriever.cpp | C++ JNI implementation handling FFmpeg context management, video stream selection, frame decoding, rotation detection, and bitmap conversion |
| CMakeLists.txt | Added media_thumbnail_retriever.cpp to the build configuration |
| consumer-rules.pro | New ProGuard rules file to prevent native method obfuscation |
| .gitignore | Added ffmpeg/cmake and ffmpeg/licenses directories to ignore list |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| AVStream *videoStream = context->formatContext->streams[context->videoStreamIndex]; | ||
| int64_t targetTimestamp = av_rescale_q(timeUs, AV_TIME_BASE_Q, videoStream->time_base); | ||
| av_seek_frame(context->formatContext, context->videoStreamIndex, targetTimestamp, AVSEEK_FLAG_BACKWARD); |
There was a problem hiding this comment.
Similar to decode_frame_at_index, the return value of av_seek_frame is not checked. Consider adding error handling for seek failures.
| av_seek_frame(context->formatContext, context->videoStreamIndex, targetTimestamp, AVSEEK_FLAG_BACKWARD); | |
| int seekResult = av_seek_frame(context->formatContext, context->videoStreamIndex, targetTimestamp, AVSEEK_FLAG_BACKWARD); | |
| if (seekResult < 0) { | |
| // Failed to seek to the requested timestamp; clean up and return null. | |
| avcodec_free_context(&codecContext); | |
| return nullptr; | |
| } |
| -keepclasseswithmembernames class * { | ||
| native <methods>; | ||
| } |
There was a problem hiding this comment.
The ProGuard rule is correct, but missing an explanatory comment. In media3ext/consumer-rules.pro, the same rule includes a comment: "This prevents the names of native methods from being obfuscated." Consider adding a similar comment here for consistency and clarity.
| } | ||
|
|
||
| for (unsigned int i = 0; i < context->formatContext->nb_streams; ++i) { | ||
| AVStream *stream = context->formatContext->streams[i]; |
There was a problem hiding this comment.
Missing null check for AVStream. Similar to the issue in create_context_from_source, the stream at index i is accessed without verification. Add a null check before accessing stream->disposition.
| AVStream *stream = context->formatContext->streams[i]; | |
| AVStream *stream = context->formatContext->streams[i]; | |
| if (!stream) { | |
| continue; | |
| } |
| @Keep | ||
| private external fun nativeCreateFromPath(filePath: String): Long | ||
|
|
||
| @Keep | ||
| private external fun nativeCreateFromFD(fileDescriptor: Int): Long | ||
|
|
||
| @Keep | ||
| private external fun nativeGetEmbeddedPicture(handle: Long): ByteArray? | ||
|
|
||
| @Keep | ||
| private external fun nativeGetFrameAtTime(handle: Long, timeUs: Long): Bitmap? | ||
|
|
||
| @Keep | ||
| private external fun nativeGetFrameAtIndex(handle: Long, frameIndex: Int): Bitmap? | ||
|
|
||
| @Keep | ||
| private external fun nativeGetRotationDegrees(handle: Long): Int | ||
|
|
||
| @Keep | ||
| private external fun nativeRelease(handle: Long) | ||
|
|
||
| companion object { | ||
| init { | ||
| System.loadLibrary("mediainfo") | ||
| } |
There was a problem hiding this comment.
Native method declarations are inconsistent with the codebase convention. In FrameLoader.kt, native methods are declared in a companion object with @JvmStatic annotation. This class declares them as instance methods. For consistency and potential performance benefits (static methods have slightly lower overhead in JNI), consider moving these to a companion object with @JvmStatic.
| @Keep | |
| private external fun nativeCreateFromPath(filePath: String): Long | |
| @Keep | |
| private external fun nativeCreateFromFD(fileDescriptor: Int): Long | |
| @Keep | |
| private external fun nativeGetEmbeddedPicture(handle: Long): ByteArray? | |
| @Keep | |
| private external fun nativeGetFrameAtTime(handle: Long, timeUs: Long): Bitmap? | |
| @Keep | |
| private external fun nativeGetFrameAtIndex(handle: Long, frameIndex: Int): Bitmap? | |
| @Keep | |
| private external fun nativeGetRotationDegrees(handle: Long): Int | |
| @Keep | |
| private external fun nativeRelease(handle: Long) | |
| companion object { | |
| init { | |
| System.loadLibrary("mediainfo") | |
| } | |
| companion object { | |
| init { | |
| System.loadLibrary("mediainfo") | |
| } | |
| @Keep | |
| @JvmStatic | |
| private external fun nativeCreateFromPath(filePath: String): Long | |
| @Keep | |
| @JvmStatic | |
| private external fun nativeCreateFromFD(fileDescriptor: Int): Long | |
| @Keep | |
| @JvmStatic | |
| private external fun nativeGetEmbeddedPicture(handle: Long): ByteArray? | |
| @Keep | |
| @JvmStatic | |
| private external fun nativeGetFrameAtTime(handle: Long, timeUs: Long): Bitmap? | |
| @Keep | |
| @JvmStatic | |
| private external fun nativeGetFrameAtIndex(handle: Long, frameIndex: Int): Bitmap? | |
| @Keep | |
| @JvmStatic | |
| private external fun nativeGetRotationDegrees(handle: Long): Int | |
| @Keep | |
| @JvmStatic | |
| private external fun nativeRelease(handle: Long) |
| static jobject create_bitmap(JNIEnv *env, int width, int height) { | ||
| jclass bitmapClass = env->FindClass("android/graphics/Bitmap"); | ||
| if (!bitmapClass) { | ||
| return nullptr; | ||
| } | ||
|
|
||
| jclass bitmapConfigClass = env->FindClass("android/graphics/Bitmap$Config"); | ||
| if (!bitmapConfigClass) { | ||
| return nullptr; | ||
| } | ||
|
|
||
| jfieldID argb8888Field = env->GetStaticFieldID( | ||
| bitmapConfigClass, | ||
| "ARGB_8888", | ||
| "Landroid/graphics/Bitmap$Config;"); | ||
| if (!argb8888Field) { | ||
| return nullptr; | ||
| } | ||
|
|
||
| jobject argb8888Obj = env->GetStaticObjectField(bitmapConfigClass, argb8888Field); | ||
| if (!argb8888Obj) { | ||
| return nullptr; | ||
| } | ||
|
|
||
| jmethodID createBitmapMethod = env->GetStaticMethodID( | ||
| bitmapClass, | ||
| "createBitmap", | ||
| "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;"); | ||
| if (!createBitmapMethod) { | ||
| return nullptr; | ||
| } | ||
|
|
||
| return env->CallStaticObjectMethod(bitmapClass, createBitmapMethod, width, height, argb8888Obj); | ||
| } |
There was a problem hiding this comment.
Potential JNI local reference leak. When create_bitmap fails to find classes, get field IDs, or call methods, the function returns nullptr without deleting previously created local references (bitmapClass, bitmapConfigClass, argb8888Obj). While these will be cleaned up when the JNI frame ends, it's better practice to explicitly delete them or use DeleteLocalRef to avoid accumulating references in loops or long-running operations.
| AVStream *stream = formatContext->streams[i]; | ||
| if (stream->codecpar->codec_type != AVMEDIA_TYPE_VIDEO) { |
There was a problem hiding this comment.
Missing null check for AVStream. After retrieving the stream at line 310, the code dereferences stream->codecpar without verifying that stream is not null. While unlikely in practice, this could cause a crash if nb_streams is inconsistent with the actual streams array.
| return nullptr; | ||
| } | ||
|
|
||
| av_seek_frame(context->formatContext, context->videoStreamIndex, 0, AVSEEK_FLAG_BACKWARD); |
There was a problem hiding this comment.
The return value of av_seek_frame is not checked. While the subsequent avcodec_flush_buffers and decode operations may still work, ignoring seek failures could lead to unexpected behavior. Consider checking the return value and handling seek failures appropriately, similar to how decode_frame_at_time handles it.
| av_seek_frame(context->formatContext, context->videoStreamIndex, 0, AVSEEK_FLAG_BACKWARD); | |
| int seekResult = av_seek_frame(context->formatContext, context->videoStreamIndex, 0, AVSEEK_FLAG_BACKWARD); | |
| if (seekResult < 0) { | |
| avcodec_free_context(&codecContext); | |
| return nullptr; | |
| } |
No description provided.