Skip to content

Media thumbnail retriever#168

Open
anilbeesetti wants to merge 3 commits intomainfrom
media-thumbnail-retriever
Open

Media thumbnail retriever#168
anilbeesetti wants to merge 3 commits intomainfrom
media-thumbnail-retriever

Conversation

@anilbeesetti
Copy link
Owner

No description provided.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 MediaThumbnailRetriever Kotlin class providing frame extraction APIs similar to Android's MediaMetadataRetriever but 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);
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to decode_frame_at_index, the return value of av_seek_frame is not checked. Consider adding error handling for seek failures.

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +3
-keepclasseswithmembernames class * {
native <methods>;
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
}

for (unsigned int i = 0; i < context->formatContext->nb_streams; ++i) {
AVStream *stream = context->formatContext->streams[i];
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
AVStream *stream = context->formatContext->streams[i];
AVStream *stream = context->formatContext->streams[i];
if (!stream) {
continue;
}

Copilot uses AI. Check for mistakes.
Comment on lines +98 to +122
@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")
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
@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)

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +89
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);
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +310 to +311
AVStream *stream = formatContext->streams[i];
if (stream->codecpar->codec_type != AVMEDIA_TYPE_VIDEO) {
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
return nullptr;
}

av_seek_frame(context->formatContext, context->videoStreamIndex, 0, AVSEEK_FLAG_BACKWARD);
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants