Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions app/src/main/java/is/xyz/mpv/BaseMPVView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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) {
Expand Down
15 changes: 8 additions & 7 deletions app/src/main/java/is/xyz/mpv/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ object Utils {

val candidates = mutableListOf<String>()
// 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)
}
Expand All @@ -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!!
}

Expand Down Expand Up @@ -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%",
)

}
99 changes: 89 additions & 10 deletions app/src/main/jni/thumbnail.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -328,20 +351,22 @@ 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;
}

jintArray arr = env->NewIntArray(width * height);
if (!arr) {
ALOGE("Thumbnail | Failed to allocate array");
sws_freeContext(sws_ctx);
if (sw_frame) av_frame_free(&sw_frame);
return NULL;
}

Expand All @@ -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(
Expand Down Expand Up @@ -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<std::chrono::milliseconds>(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) {
Expand Down