From ca4612e2e5497c11e07623320380a41d9856694f Mon Sep 17 00:00:00 2001 From: Riteshp2001 Date: Wed, 14 Jan 2026 13:53:41 +0530 Subject: [PATCH] playback video issues and Thumbnail generation fix --- app/src/main/java/is/xyz/mpv/BaseMPVView.kt | 48 ++++++++++ app/src/main/java/is/xyz/mpv/Utils.kt | 15 ++-- app/src/main/jni/thumbnail.cpp | 99 ++++++++++++++++++--- 3 files changed, 145 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/is/xyz/mpv/BaseMPVView.kt b/app/src/main/java/is/xyz/mpv/BaseMPVView.kt index 0289f0e..3df0b51 100644 --- a/app/src/main/java/is/xyz/mpv/BaseMPVView.kt +++ b/app/src/main/java/is/xyz/mpv/BaseMPVView.kt @@ -32,9 +32,49 @@ abstract class BaseMPVView(context: Context, attrs: AttributeSet) : SurfaceView( MPVLib.setOptionString(opt, cacheDir) initOptions() + // Optimized video rendering (MX Player-like quality on all devices) + MPVLib.setOptionString("vo", "gpu") + MPVLib.setOptionString("gpu-context", "android") + MPVLib.setOptionString("opengl-es", "yes") + + // Balanced scaling (sharp quality, low GPU cost) + MPVLib.setOptionString("scale", "lanczos") + MPVLib.setOptionString("cscale", "lanczos") + MPVLib.setOptionString("dscale", "lanczos") + MPVLib.setOptionString("scale-radius", "2") // Fast lanczos + + // Color and quality + MPVLib.setOptionString("dither-depth", "auto") + MPVLib.setOptionString("deband", "yes") + MPVLib.setOptionString("deband-iterations", "1") + MPVLib.setOptionString("deband-threshold", "48") + MPVLib.setOptionString("deband-range", "16") + MPVLib.setOptionString("deband-grain", "24") + MPVLib.setOptionString("temporal-dither", "yes") + + // Hardware decoding (with software fallback) + MPVLib.setOptionString("hwdec", "mediacodec-copy") // Best compatibility + MPVLib.setOptionString("hwdec-codecs", "h264,hevc,vp8,vp9,av1") + + // Smooth playback without dropped frames + MPVLib.setOptionString("video-sync", "audio") + MPVLib.setOptionString("framedrop", "decoder+vo") // Smart frame drop + MPVLib.setOptionString("video-latency-hacks", "yes") + + // Optimized cache + MPVLib.setOptionString("demuxer-max-bytes", "64MiB") + MPVLib.setOptionString("demuxer-max-back-bytes", "32MiB") + MPVLib.setOptionString("demuxer-readahead-secs", "5") + MPVLib.setOptionString("cache", "yes") + + // Audio + MPVLib.setOptionString("audio-pitch-correction", "yes") + MPVLib.setOptionString("ao", "audiotrack") + MPVLib.init() postInitOptions() + MPVLib.setOptionString("keep-open", "yes") MPVLib.setOptionString("force-window", "no") MPVLib.setOptionString("idle", "once") @@ -83,6 +123,14 @@ abstract class BaseMPVView(context: Context, attrs: AttributeSet) : SurfaceView( override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { MPVLib.setPropertyString("android-surface-size", "${width}x$height") + // Force redraw when paused to fix broken image after orientation change + val paused = MPVLib.getPropertyBoolean("pause") + if (paused == true) { + val pos = MPVLib.getPropertyDouble("time-pos") + if (pos != null) { + MPVLib.command("seek", pos.toString(), "absolute", "exact") + } + } } override fun surfaceCreated(holder: SurfaceHolder) { diff --git a/app/src/main/java/is/xyz/mpv/Utils.kt b/app/src/main/java/is/xyz/mpv/Utils.kt index 5d3f9e8..56522fc 100644 --- a/app/src/main/java/is/xyz/mpv/Utils.kt +++ b/app/src/main/java/is/xyz/mpv/Utils.kt @@ -107,7 +107,7 @@ object Utils { val candidates = mutableListOf() // check all media dirs, there's usually one on each storage volume - // Using safe call to handle potential null values in the array + @Suppress("DEPRECATION") context.externalMediaDirs?.filterNotNull()?.forEach { candidates.add(it.absolutePath) } @@ -130,7 +130,7 @@ object Utils { continue // find the actual root path of that volume - while (root.parentFile != null && storageManager.getStorageVolume(root.parentFile) == vol) { + while (root.parentFile != null && storageManager.getStorageVolume(root.parentFile!!) == vol) { root = root.parentFile!! } @@ -296,9 +296,10 @@ object Utils { ) val VERSIONS = Versions( - mpv = "%MPV_VERSION%", - buildDate = "%DATE%", - libPlacebo = "%LIBPLACEBO_VERSION%", - ffmpeg = "%FFMPEG_VERSION%", - ) + mpv = "%MPV_VERSION%", + buildDate = "%DATE%", + libPlacebo = "%LIBPLACEBO_VERSION%", + ffmpeg = "%FFMPEG_VERSION%", + ) + } diff --git a/app/src/main/jni/thumbnail.cpp b/app/src/main/jni/thumbnail.cpp index 8e2334f..7cd5bf0 100644 --- a/app/src/main/jni/thumbnail.cpp +++ b/app/src/main/jni/thumbnail.cpp @@ -299,9 +299,32 @@ jni_func(void, clearThumbnailCache) { static jobject frame_to_bitmap(JNIEnv *env, AVFrame *frame, int target_dimension) { init_methods_cache(env); + // Handle hardware frames - transfer to software + AVFrame *sw_frame = nullptr; + AVFrame *use_frame = frame; + + if (frame->format == AV_PIX_FMT_MEDIACODEC || + frame->hw_frames_ctx != nullptr) { + sw_frame = av_frame_alloc(); + if (!sw_frame) { + ALOGE("Thumbnail | Failed to allocate sw_frame"); + return NULL; + } + + if (av_hwframe_transfer_data(sw_frame, frame, 0) < 0) { + ALOGE("Thumbnail | Failed to transfer hw frame to sw"); + av_frame_free(&sw_frame); + return NULL; + } + + sw_frame->pts = frame->pts; + use_frame = sw_frame; + ALOGV("Thumbnail | Transferred HW frame to SW"); + } + // Calculate scaled dimensions while preserving aspect ratio - int width = frame->width; - int height = frame->height; + int width = use_frame->width; + int height = use_frame->height; if (width > 0 && height > 0) { float scale = 1.0f; @@ -328,13 +351,14 @@ static jobject frame_to_bitmap(JNIEnv *env, AVFrame *frame, int target_dimension // Create SwsContext for scaling and format conversion // Android Bitmap.Config.ARGB_8888 expects BGRA byte order (little-endian) struct SwsContext *sws_ctx = sws_getContext( - frame->width, frame->height, (AVPixelFormat)frame->format, + use_frame->width, use_frame->height, (AVPixelFormat)use_frame->format, width, height, AV_PIX_FMT_BGRA, sws_algorithm, NULL, NULL, NULL ); if (!sws_ctx) { - ALOGE("Thumbnail | Failed to create scaler"); + ALOGE("Thumbnail | Failed to create scaler for format %d", use_frame->format); + if (sw_frame) av_frame_free(&sw_frame); return NULL; } @@ -342,6 +366,7 @@ static jobject frame_to_bitmap(JNIEnv *env, AVFrame *frame, int target_dimension if (!arr) { ALOGE("Thumbnail | Failed to allocate array"); sws_freeContext(sws_ctx); + if (sw_frame) av_frame_free(&sw_frame); return NULL; } @@ -350,13 +375,15 @@ static jobject frame_to_bitmap(JNIEnv *env, AVFrame *frame, int target_dimension ALOGE("Thumbnail | Failed to get array elements"); env->DeleteLocalRef(arr); sws_freeContext(sws_ctx); + if (sw_frame) av_frame_free(&sw_frame); return NULL; } uint8_t *dst_data[4] = { (uint8_t*)pixels }; int dst_linesize[4] = { width * 4 }; - sws_scale(sws_ctx, frame->data, frame->linesize, 0, frame->height, dst_data, dst_linesize); + sws_scale(sws_ctx, use_frame->data, use_frame->linesize, 0, use_frame->height, dst_data, dst_linesize); sws_freeContext(sws_ctx); + if (sw_frame) av_frame_free(&sw_frame); env->ReleaseIntArrayElements(arr, pixels, 0); jobject bitmap_config = env->GetStaticObjectField( @@ -434,16 +461,68 @@ jni_func(jobject, grabThumbnailFast, jstring jpath, jdouble position, jint dimen return NULL; } - // Find video stream + // Find video stream and check for embedded thumbnails int video_stream_idx = -1; + int embedded_thumb_idx = -1; AVCodecParameters *codec_params = NULL; for (unsigned int i = 0; i < format_ctx->nb_streams; i++) { - if (format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { - video_stream_idx = i; - codec_params = format_ctx->streams[i]->codecpar; - break; + AVStream *stream = format_ctx->streams[i]; + + // Check for attached picture (embedded thumbnail/cover art) + if (stream->disposition & AV_DISPOSITION_ATTACHED_PIC) { + embedded_thumb_idx = i; + ALOGV("Thumbnail | Found embedded thumbnail in stream %d", i); + } + + if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO && video_stream_idx == -1) { + // Skip attached pictures as main video stream + if (!(stream->disposition & AV_DISPOSITION_ATTACHED_PIC)) { + video_stream_idx = i; + codec_params = stream->codecpar; + } + } + } + + // Try to use embedded thumbnail first (much faster) + if (embedded_thumb_idx >= 0 && position == 0.0) { + AVStream *thumb_stream = format_ctx->streams[embedded_thumb_idx]; + AVPacket *pkt = &thumb_stream->attached_pic; + + if (pkt->data && pkt->size > 0) { + // Decode the embedded image + const AVCodec *img_codec = avcodec_find_decoder(thumb_stream->codecpar->codec_id); + if (img_codec) { + AVCodecContext *img_ctx = avcodec_alloc_context3(img_codec); + if (img_ctx) { + if (avcodec_parameters_to_context(img_ctx, thumb_stream->codecpar) >= 0 && + avcodec_open2(img_ctx, img_codec, NULL) >= 0) { + + AVFrame *img_frame = av_frame_alloc(); + if (img_frame) { + if (avcodec_send_packet(img_ctx, pkt) >= 0 && + avcodec_receive_frame(img_ctx, img_frame) >= 0) { + + jobject bitmap = frame_to_bitmap(env, img_frame, dimension); + av_frame_free(&img_frame); + avcodec_free_context(&img_ctx); + avformat_close_input(&format_ctx); + + if (bitmap) { + auto total_end = std::chrono::high_resolution_clock::now(); + auto total_duration = std::chrono::duration_cast(total_end - total_start); + ALOGI("Thumbnail (embedded) | %lldms", (long long)total_duration.count()); + return bitmap; + } + } + av_frame_free(&img_frame); + } + } + avcodec_free_context(&img_ctx); + } + } } + ALOGV("Thumbnail | Embedded thumbnail decoding failed, falling back to video decode"); } if (video_stream_idx == -1) {