From 1d5b175cc82ccaf0085c0997fd62f40be537cd4c Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 6 Sep 2026 14:50:51 +0200 Subject: [PATCH] fix(android): bind every libmpv JNI call and callback to its native session Tearing down the mpv player while a hook handler or property write was still in flight could reach the replacement player: the native side kept one process-global handle, the Kotlin wrapper was published before the native session existed, callbacks were routed to whichever wrapper was current, and hook continuations were validated only against a `closed` flag read before the native call. A retiring player's on_preloaded handler could therefore reconfigure the successor's tracks, its late `mpv_hook_continue` could land on the successor's core, and the successor could ingest the predecessor's end-file and property tail. Every native session now has an immutable monotonic id minted by nativeCreate. Create/init/destroy hold a write lock; every other JNI entry takes a read lock and is refused when it names a retired session, so a call admitted under a live session keeps that handle alive until it returns and retirement waits for it. The event thread is bound to its session before it starts and stamps every callback with it; Kotlin dispatches a callback only to the wrapper published for that session and passes its session on every native call. create() publishes the wrapper only after the native session exists, and a superseded create fails instead of publishing a dead session. --- android/libmpv/src/main/cpp/event.cpp | 51 +++-- android/libmpv/src/main/cpp/event.h | 10 + android/libmpv/src/main/cpp/globals.h | 33 +++ android/libmpv/src/main/cpp/jni_utils.cpp | 19 +- android/libmpv/src/main/cpp/main.cpp | 178 ++++++++------- android/libmpv/src/main/cpp/property.cpp | 83 +++---- android/libmpv/src/main/cpp/render.cpp | 39 ++-- .../com/edde746/plezy/libmpv/MpvPlayer.kt | 205 ++++++++++-------- .../edde746/plezy/libmpv/MpvPlayerConfig.kt | 7 +- 9 files changed, 372 insertions(+), 253 deletions(-) diff --git a/android/libmpv/src/main/cpp/event.cpp b/android/libmpv/src/main/cpp/event.cpp index 203194f1b..f90bcf381 100644 --- a/android/libmpv/src/main/cpp/event.cpp +++ b/android/libmpv/src/main/cpp/event.cpp @@ -7,34 +7,49 @@ #include "jni_utils.h" #include "log.h" +// The session every callback below names, bound once before the thread +// starts (event_thread_bind). Kotlin drops a callback whose session is not +// the wrapper it published for, so a retiring core's tail — end-file, +// property changes, a hook raised just before teardown — can never be read +// as the successor's. +static mpv_handle* thread_mpv; +static uint64_t thread_session; + +void event_thread_bind(mpv_handle* mpv, uint64_t session) { + thread_mpv = mpv; + thread_session = session; +} + static void sendPropertyUpdateToJava(JNIEnv* env, mpv_event_property* prop, int64_t source_id, bool has_source_id) { jstring jprop = new_java_string(env, prop->name); jstring jvalue = NULL; const jboolean jhas_source_id = has_source_id ? JNI_TRUE : JNI_FALSE; + const jlong jsession = (jlong)thread_session; switch (prop->format) { case MPV_FORMAT_NONE: env->CallStaticVoidMethod( - mpv_MpvPlayer, mpv_MpvPlayer_onPropertyChanged_SJZ, jprop, (jlong)source_id, jhas_source_id); + mpv_MpvPlayer, mpv_MpvPlayer_onPropertyChanged_SJZ, jsession, jprop, (jlong)source_id, jhas_source_id); break; case MPV_FORMAT_FLAG: env->CallStaticVoidMethod( - mpv_MpvPlayer, mpv_MpvPlayer_onPropertyChanged_SZJZ, jprop, (jboolean) * (int*)prop->data, (jlong)source_id, - jhas_source_id); + mpv_MpvPlayer, mpv_MpvPlayer_onPropertyChanged_SZJZ, jsession, jprop, (jboolean) * (int*)prop->data, + (jlong)source_id, jhas_source_id); break; case MPV_FORMAT_INT64: env->CallStaticVoidMethod( - mpv_MpvPlayer, mpv_MpvPlayer_onPropertyChanged_SJJZ, jprop, (jlong) * (int64_t*)prop->data, (jlong)source_id, - jhas_source_id); + mpv_MpvPlayer, mpv_MpvPlayer_onPropertyChanged_SJJZ, jsession, jprop, (jlong) * (int64_t*)prop->data, + (jlong)source_id, jhas_source_id); break; case MPV_FORMAT_DOUBLE: env->CallStaticVoidMethod( - mpv_MpvPlayer, mpv_MpvPlayer_onPropertyChanged_SDJZ, jprop, (jdouble) * (double*)prop->data, (jlong)source_id, - jhas_source_id); + mpv_MpvPlayer, mpv_MpvPlayer_onPropertyChanged_SDJZ, jsession, jprop, (jdouble) * (double*)prop->data, + (jlong)source_id, jhas_source_id); break; case MPV_FORMAT_STRING: jvalue = new_java_string(env, *(const char**)prop->data); env->CallStaticVoidMethod( - mpv_MpvPlayer, mpv_MpvPlayer_onPropertyChanged_SSJZ, jprop, jvalue, (jlong)source_id, jhas_source_id); + mpv_MpvPlayer, mpv_MpvPlayer_onPropertyChanged_SSJZ, jsession, jprop, jvalue, (jlong)source_id, + jhas_source_id); break; default: break; @@ -47,8 +62,8 @@ static void sendEventToJava( JNIEnv* env, int event, int64_t source_id, bool has_source_id, double position_seconds = 0.0, bool has_position_seconds = false) { env->CallStaticVoidMethod( - mpv_MpvPlayer, mpv_MpvPlayer_onEvent, (jint)event, (jlong)source_id, has_source_id ? JNI_TRUE : JNI_FALSE, - (jdouble)position_seconds, has_position_seconds ? JNI_TRUE : JNI_FALSE); + mpv_MpvPlayer, mpv_MpvPlayer_onEvent, (jlong)thread_session, (jint)event, (jlong)source_id, + has_source_id ? JNI_TRUE : JNI_FALSE, (jdouble)position_seconds, has_position_seconds ? JNI_TRUE : JNI_FALSE); } static void sendEndFileToJava(JNIEnv* env, mpv_event* event) { @@ -56,24 +71,28 @@ static void sendEndFileToJava(JNIEnv* env, mpv_event* event) { const int reason = end_file ? end_file->reason : -1; const int64_t source_id = end_file ? end_file->playlist_entry_id : 0; env->CallStaticVoidMethod( - mpv_MpvPlayer, mpv_MpvPlayer_onEndFile, (jint)reason, (jlong)source_id, end_file ? JNI_TRUE : JNI_FALSE); + mpv_MpvPlayer, mpv_MpvPlayer_onEndFile, (jlong)thread_session, (jint)reason, (jlong)source_id, + end_file ? JNI_TRUE : JNI_FALSE); } static void sendLogMessageToJava(JNIEnv* env, mpv_event_log_message* msg) { jstring jprefix = new_java_string(env, msg->prefix); jstring jtext = new_java_string(env, msg->text); - env->CallStaticVoidMethod(mpv_MpvPlayer, mpv_MpvPlayer_onLogMessage, jprefix, (jint)msg->log_level, jtext); + env->CallStaticVoidMethod( + mpv_MpvPlayer, mpv_MpvPlayer_onLogMessage, (jlong)thread_session, jprefix, (jint)msg->log_level, jtext); if (jprefix) env->DeleteLocalRef(jprefix); if (jtext) env->DeleteLocalRef(jtext); } // A hook holds mpv (playback does not proceed) until Kotlin answers with -// nativeHookContinue(id); MpvPlayer guarantees that answer for every hook. +// nativeHookContinue(session, id); MpvPlayer guarantees that answer for every +// hook it is handed, and mpv itself continues any hook still held when the +// handle is destroyed. static void sendHookToJava(JNIEnv* env, mpv_event_hook* hook) { jstring jname = new_java_string(env, hook->name); - env->CallStaticVoidMethod(mpv_MpvPlayer, mpv_MpvPlayer_onHook, jname, (jlong)hook->id); + env->CallStaticVoidMethod(mpv_MpvPlayer, mpv_MpvPlayer_onHook, (jlong)thread_session, jname, (jlong)hook->id); if (jname) env->DeleteLocalRef(jname); } @@ -90,7 +109,7 @@ void* event_thread(void* arg) { mpv_event_property* mp_property; mpv_event_log_message* msg; - mp_event = mpv_wait_event(g_mpv, -1.0); + mp_event = mpv_wait_event(thread_mpv, -1.0); if (g_event_thread_request_exit) break; @@ -124,7 +143,7 @@ void* event_thread(void* arg) { case MPV_EVENT_PLAYBACK_RESTART: { double position_seconds = 0.0; const bool has_position_seconds = - mpv_get_property(g_mpv, "time-pos", MPV_FORMAT_DOUBLE, &position_seconds) >= 0 && + mpv_get_property(thread_mpv, "time-pos", MPV_FORMAT_DOUBLE, &position_seconds) >= 0 && std::isfinite(position_seconds); sendEventToJava(env, mp_event->event_id, source_id, has_source_id, position_seconds, has_position_seconds); break; diff --git a/android/libmpv/src/main/cpp/event.h b/android/libmpv/src/main/cpp/event.h index d9699006c..54e568a96 100644 --- a/android/libmpv/src/main/cpp/event.h +++ b/android/libmpv/src/main/cpp/event.h @@ -1,3 +1,13 @@ #pragma once +#include + +#include + +// Names the session the next event_thread serves. Called under the lifecycle +// write lock before pthread_create, which publishes the values to the thread; +// the thread then never reads the globals, so a successor session replacing +// them cannot redirect it. +void event_thread_bind(mpv_handle* mpv, uint64_t session); + void* event_thread(void* arg); diff --git a/android/libmpv/src/main/cpp/globals.h b/android/libmpv/src/main/cpp/globals.h index 483573d67..f97cb870c 100644 --- a/android/libmpv/src/main/cpp/globals.h +++ b/android/libmpv/src/main/cpp/globals.h @@ -2,9 +2,42 @@ #include #include +#include #include +#include extern JavaVM* g_vm; + +// The process-global native session. One mpv_handle lives at a time; each is +// identified by a monotonic session id handed to Kotlin by nativeCreate. Every +// JNI entry names the session it was issued for, every callback carries the +// session it originated from, so a retired wrapper can neither touch nor be +// fed by its successor. +// +// g_session_lock: write-held across create/init/destroy, read-held for every +// other entry. A reader admitted under a matching session keeps the handle +// alive until it returns; retirement waits for it, then flips g_session so +// later callers with the old id are refused before they reach mpv. extern mpv_handle* g_mpv; +extern uint64_t g_session; +extern pthread_rwlock_t g_session_lock; extern std::atomic g_event_thread_request_exit; + +// Read-locked admission of one JNI entry to the live session. `mpv` is NULL +// when `session` is not the live one (retired, superseded, or never created); +// the caller then reports MPV_ERROR_UNINITIALIZED / null and touches nothing. +// A refused entry is an expected outcome of teardown racing in-flight work, +// not a programming error, so it never throws into Java. +class SessionGuard { + public: + explicit SessionGuard(jlong session) { + pthread_rwlock_rdlock(&g_session_lock); + mpv = (g_mpv && g_session == (uint64_t)session) ? g_mpv : NULL; + } + ~SessionGuard() { pthread_rwlock_unlock(&g_session_lock); } + SessionGuard(const SessionGuard&) = delete; + SessionGuard& operator=(const SessionGuard&) = delete; + + mpv_handle* mpv; +}; diff --git a/android/libmpv/src/main/cpp/jni_utils.cpp b/android/libmpv/src/main/cpp/jni_utils.cpp index 3058a9ac9..3bf828767 100644 --- a/android/libmpv/src/main/cpp/jni_utils.cpp +++ b/android/libmpv/src/main/cpp/jni_utils.cpp @@ -50,21 +50,22 @@ void init_methods_cache(JNIEnv* env) { mpv_MpvPlayer = env->FindClass("com/edde746/plezy/libmpv/MpvPlayer"); mpv_MpvPlayer = reinterpret_cast(env->NewGlobalRef(mpv_MpvPlayer)); + // Every callback leads with the origin session (J); see event.cpp. mpv_MpvPlayer_onPropertyChanged_SJZ = - env->GetStaticMethodID(mpv_MpvPlayer, "onPropertyChanged", "(Ljava/lang/String;JZ)V"); + env->GetStaticMethodID(mpv_MpvPlayer, "onPropertyChanged", "(JLjava/lang/String;JZ)V"); mpv_MpvPlayer_onPropertyChanged_SZJZ = - env->GetStaticMethodID(mpv_MpvPlayer, "onPropertyChanged", "(Ljava/lang/String;ZJZ)V"); + env->GetStaticMethodID(mpv_MpvPlayer, "onPropertyChanged", "(JLjava/lang/String;ZJZ)V"); mpv_MpvPlayer_onPropertyChanged_SJJZ = - env->GetStaticMethodID(mpv_MpvPlayer, "onPropertyChanged", "(Ljava/lang/String;JJZ)V"); + env->GetStaticMethodID(mpv_MpvPlayer, "onPropertyChanged", "(JLjava/lang/String;JJZ)V"); mpv_MpvPlayer_onPropertyChanged_SDJZ = - env->GetStaticMethodID(mpv_MpvPlayer, "onPropertyChanged", "(Ljava/lang/String;DJZ)V"); + env->GetStaticMethodID(mpv_MpvPlayer, "onPropertyChanged", "(JLjava/lang/String;DJZ)V"); mpv_MpvPlayer_onPropertyChanged_SSJZ = - env->GetStaticMethodID(mpv_MpvPlayer, "onPropertyChanged", "(Ljava/lang/String;Ljava/lang/String;JZ)V"); - mpv_MpvPlayer_onEvent = env->GetStaticMethodID(mpv_MpvPlayer, "onEvent", "(IJZDZ)V"); - mpv_MpvPlayer_onEndFile = env->GetStaticMethodID(mpv_MpvPlayer, "onEndFile", "(IJZ)V"); + env->GetStaticMethodID(mpv_MpvPlayer, "onPropertyChanged", "(JLjava/lang/String;Ljava/lang/String;JZ)V"); + mpv_MpvPlayer_onEvent = env->GetStaticMethodID(mpv_MpvPlayer, "onEvent", "(JIJZDZ)V"); + mpv_MpvPlayer_onEndFile = env->GetStaticMethodID(mpv_MpvPlayer, "onEndFile", "(JIJZ)V"); mpv_MpvPlayer_onLogMessage = - env->GetStaticMethodID(mpv_MpvPlayer, "onLogMessage", "(Ljava/lang/String;ILjava/lang/String;)V"); - mpv_MpvPlayer_onHook = env->GetStaticMethodID(mpv_MpvPlayer, "onHook", "(Ljava/lang/String;J)V"); + env->GetStaticMethodID(mpv_MpvPlayer, "onLogMessage", "(JLjava/lang/String;ILjava/lang/String;)V"); + mpv_MpvPlayer_onHook = env->GetStaticMethodID(mpv_MpvPlayer, "onHook", "(JLjava/lang/String;J)V"); methods_initialized = true; } diff --git a/android/libmpv/src/main/cpp/main.cpp b/android/libmpv/src/main/cpp/main.cpp index cccf731f8..82a560b6d 100644 --- a/android/libmpv/src/main/cpp/main.cpp +++ b/android/libmpv/src/main/cpp/main.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include @@ -17,6 +16,7 @@ extern "C" { } #include "event.h" +#include "globals.h" #include "jni_utils.h" #include "log.h" @@ -25,23 +25,35 @@ extern "C" { void render_cleanup(JNIEnv* env); extern "C" { -jni_func(void, nativeCreate, jobject appctx); -jni_func(void, nativeInit); -jni_func(void, nativeDestroy); +jni_func(jlong, nativeCreate, jobject appctx); +jni_func(jint, nativeInit, jlong session); +jni_func(void, nativeDestroy, jlong session); -jni_func(jint, nativeSetLogLevel, jstring level); +jni_func(jint, nativeSetLogLevel, jlong session, jstring level); -jni_func(jlong, nativeCommand, jobjectArray jarray); -jni_func(void, nativeHookContinue, jlong id); +jni_func(jlong, nativeCommand, jlong session, jobjectArray jarray); +jni_func(void, nativeHookContinue, jlong session, jlong id); }; JavaVM* g_vm; mpv_handle* g_mpv; +uint64_t g_session; +pthread_rwlock_t g_session_lock = PTHREAD_RWLOCK_INITIALIZER; std::atomic g_event_thread_request_exit(false); +static uint64_t next_session = 0; static pthread_t event_thread_id; static bool event_thread_started = false; -static std::mutex g_lifecycle_mutex; + +// Write-locked scope for the lifecycle entries; waits for every admitted +// reader to return before the handle can change underneath it. +class LifecycleGuard { + public: + LifecycleGuard() { pthread_rwlock_wrlock(&g_session_lock); } + ~LifecycleGuard() { pthread_rwlock_unlock(&g_session_lock); } + LifecycleGuard(const LifecycleGuard&) = delete; + LifecycleGuard& operator=(const LifecycleGuard&) = delete; +}; static void prepare_environment(JNIEnv* env, jobject appctx) { setlocale(LC_NUMERIC, "C"); @@ -54,66 +66,11 @@ static void prepare_environment(JNIEnv* env, jobject appctx) { init_methods_cache(env); } -jni_func(void, nativeCreate, jobject appctx) { - std::lock_guard lock(g_lifecycle_mutex); - prepare_environment(env, appctx); - - mpv_handle* leaked_mpv = NULL; - if (g_mpv) { - ALOGE("destroying leaked mpv instance"); - leaked_mpv = g_mpv; - if (event_thread_started) { - g_event_thread_request_exit = true; - mpv_wakeup(leaked_mpv); - pthread_join(event_thread_id, NULL); - event_thread_started = false; - } - g_mpv = NULL; - mpv_terminate_destroy(leaked_mpv); - render_cleanup(env); - } - - g_mpv = mpv_create(); - if (!g_mpv) { - die("context init failed"); - return; - } - - mpv_request_log_messages(g_mpv, "warn"); -} - -jni_func(void, nativeInit) { - std::lock_guard lock(g_lifecycle_mutex); - if (!g_mpv) { - die("mpv is not created"); - return; - } - - if (mpv_initialize(g_mpv) < 0) { - die("mpv init failed"); - return; - } - - // Per-file decode routing (Dolby Vision P5, H.264 High 10) has to land - // before mpv creates the decoder; file-loaded is already too late for the - // MediaCodec path. on_preloaded runs after the demuxer opened the file and - // holds playback until Kotlin continues it (MpvPlayer.onHook). - mpv_hook_add(g_mpv, 0, "on_preloaded", 0); - - g_event_thread_request_exit = false; - if (pthread_create(&event_thread_id, NULL, event_thread, NULL) != 0) { - die("thread create failed"); - return; - } - event_thread_started = true; - pthread_setname_np(event_thread_id, "event_thread"); -} - -jni_func(void, nativeDestroy) { - std::lock_guard lock(g_lifecycle_mutex); - if (!g_mpv) { - return; - } +// Caller holds the write lock. Stops the event thread, retires the session +// id (so a late nativeHookContinue / property call for it is refused, not +// delivered to whatever comes next), and tears the core down. mpv itself +// continues any hook the destroyed handle was still holding. +static void destroy_locked(JNIEnv* env) { mpv_handle* local_mpv = g_mpv; // Configuration (including an invalid initial log level) can fail before @@ -126,6 +83,7 @@ jni_func(void, nativeDestroy) { } g_mpv = NULL; + g_session = 0; // The MediaCodec VO can retain the Surface until final decoder teardown. // Keep its JNI refs alive for the entire blocking termination. @@ -133,23 +91,79 @@ jni_func(void, nativeDestroy) { render_cleanup(env); } -jni_func(jint, nativeSetLogLevel, jstring jlevel) { - std::lock_guard lock(g_lifecycle_mutex); - if (!g_mpv) return MPV_ERROR_UNINITIALIZED; +jni_func(jlong, nativeCreate, jobject appctx) { + LifecycleGuard lock; + prepare_environment(env, appctx); + + if (g_mpv) { + ALOGE("destroying leaked mpv instance"); + destroy_locked(env); + } + + g_mpv = mpv_create(); + if (!g_mpv) { + die("context init failed"); + return 0; + } + g_session = ++next_session; + + mpv_request_log_messages(g_mpv, "warn"); + return (jlong)g_session; +} + +jni_func(jint, nativeInit, jlong session) { + LifecycleGuard lock; + if (!g_mpv || g_session != (uint64_t)session) return MPV_ERROR_UNINITIALIZED; + + const int result = mpv_initialize(g_mpv); + if (result < 0) { + ALOGE("mpv_initialize returned error %s", mpv_error_string(result)); + return result; + } + + // Per-file decode routing (Dolby Vision P5, H.264 High 10) has to land + // before mpv creates the decoder; file-loaded is already too late for the + // MediaCodec path. on_preloaded runs after the demuxer opened the file and + // holds playback until Kotlin continues it (MpvPlayer.onHook). + mpv_hook_add(g_mpv, 0, "on_preloaded", 0); + + g_event_thread_request_exit = false; + event_thread_bind(g_mpv, g_session); + if (pthread_create(&event_thread_id, NULL, event_thread, NULL) != 0) { + die("thread create failed"); + return MPV_ERROR_GENERIC; + } + event_thread_started = true; + pthread_setname_np(event_thread_id, "event_thread"); + return 0; +} + +jni_func(void, nativeDestroy, jlong session) { + LifecycleGuard lock; + // A wrapper whose session a later nativeCreate already retired has nothing + // left to destroy; the successor is not its to touch. + if (!g_mpv || g_session != (uint64_t)session) return; + destroy_locked(env); +} + +jni_func(jint, nativeSetLogLevel, jlong session, jstring jlevel) { + SessionGuard guard(session); + if (!guard.mpv) return MPV_ERROR_UNINITIALIZED; const std::string level = java_string_to_utf8(env, jlevel); if (env->ExceptionCheck()) return MPV_ERROR_NOMEM; - const int result = mpv_request_log_messages(g_mpv, level.c_str()); + const int result = mpv_request_log_messages(guard.mpv, level.c_str()); if (result < 0) ALOGE("mpv_request_log_messages returned error %s", mpv_error_string(result)); return result; } // Runs a command synchronously. Returns the negative mpv error on failure, // the `playlist_entry_id` a `loadfile` created (always > 0), or 0 for a -// command that succeeded without one. `die` (a Java RuntimeException) is -// reserved for a missing core, matching the other entry points. -jni_func(jlong, nativeCommand, jobjectArray jarray) { - CHECK_MPV_INIT_RET(MPV_ERROR_UNINITIALIZED); +// command that succeeded without one. A retired session reports +// MPV_ERROR_UNINITIALIZED like any other rejected command. +jni_func(jlong, nativeCommand, jlong session, jobjectArray jarray) { + SessionGuard guard(session); + if (!guard.mpv) return MPV_ERROR_UNINITIALIZED; const char* arguments[128] = {0}; int len = env->GetArrayLength(jarray); @@ -168,7 +182,7 @@ jni_func(jlong, nativeCommand, jobjectArray jarray) { } mpv_node result{}; - const int status = mpv_command_ret(g_mpv, arguments, &result); + const int status = mpv_command_ret(guard.mpv, arguments, &result); if (status < 0) { ALOGE("mpv_command(%s) returned error %s", len > 0 ? arguments[0] : "", mpv_error_string(status)); return status; @@ -188,7 +202,11 @@ jni_func(jlong, nativeCommand, jobjectArray jarray) { return playlist_entry_id; } -jni_func(void, nativeHookContinue, jlong id) { - if (!g_mpv) return; - mpv_hook_continue(g_mpv, (uint64_t)id); +// A continuation for a retired session is dropped: the hook id belongs to a +// handle that no longer exists, and destroying that handle already continued +// it. The successor must never receive it. +jni_func(void, nativeHookContinue, jlong session, jlong id) { + SessionGuard guard(session); + if (!guard.mpv) return; + mpv_hook_continue(guard.mpv, (uint64_t)id); } diff --git a/android/libmpv/src/main/cpp/property.cpp b/android/libmpv/src/main/cpp/property.cpp index 2336514cf..50cd3e9fb 100644 --- a/android/libmpv/src/main/cpp/property.cpp +++ b/android/libmpv/src/main/cpp/property.cpp @@ -9,49 +9,55 @@ #include "log.h" extern "C" { -jni_func(jint, nativeSetOptionString, jstring option, jstring value); +jni_func(jint, nativeSetOptionString, jlong session, jstring option, jstring value); -jni_func(jobject, nativeGetPropertyInt, jstring property); -jni_func(void, nativeSetPropertyInt, jstring property, jint value); -jni_func(jobject, nativeGetPropertyDouble, jstring property); -jni_func(void, nativeSetPropertyDouble, jstring property, jdouble value); -jni_func(jobject, nativeGetPropertyBoolean, jstring property); -jni_func(void, nativeSetPropertyBoolean, jstring property, jboolean value); -jni_func(jstring, nativeGetPropertyString, jstring jproperty); -jni_func(void, nativeSetPropertyString, jstring jproperty, jstring jvalue); +jni_func(jobject, nativeGetPropertyInt, jlong session, jstring property); +jni_func(void, nativeSetPropertyInt, jlong session, jstring property, jint value); +jni_func(jobject, nativeGetPropertyDouble, jlong session, jstring property); +jni_func(void, nativeSetPropertyDouble, jlong session, jstring property, jdouble value); +jni_func(jobject, nativeGetPropertyBoolean, jlong session, jstring property); +jni_func(void, nativeSetPropertyBoolean, jlong session, jstring property, jboolean value); +jni_func(jstring, nativeGetPropertyString, jlong session, jstring jproperty); +jni_func(void, nativeSetPropertyString, jlong session, jstring jproperty, jstring jvalue); -jni_func(void, nativeObserveProperty, jstring property, jint format); +jni_func(void, nativeObserveProperty, jlong session, jstring property, jint format); } -jni_func(jint, nativeSetOptionString, jstring joption, jstring jvalue) { - CHECK_MPV_INIT_RET(0); +jni_func(jint, nativeSetOptionString, jlong session, jstring joption, jstring jvalue) { + SessionGuard guard(session); + if (!guard.mpv) return MPV_ERROR_UNINITIALIZED; const char* option = env->GetStringUTFChars(joption, NULL); const std::string value = java_string_to_utf8(env, jvalue); - int result = mpv_set_option_string(g_mpv, option, value.c_str()); + int result = mpv_set_option_string(guard.mpv, option, value.c_str()); env->ReleaseStringUTFChars(joption, option); return result; } -static int common_get_property(JNIEnv* env, jstring jproperty, mpv_format format, void* output) { - CHECK_MPV_INIT_RET(-1); +// A retired session reads as "no value" and writes nowhere: the in-flight +// hook handler of a torn-down player must not learn about, or reconfigure, +// its successor. +static int common_get_property(JNIEnv* env, jlong session, jstring jproperty, mpv_format format, void* output) { + SessionGuard guard(session); + if (!guard.mpv) return MPV_ERROR_UNINITIALIZED; const char* prop = env->GetStringUTFChars(jproperty, NULL); - int result = mpv_get_property(g_mpv, prop, format, output); + int result = mpv_get_property(guard.mpv, prop, format, output); if (result < 0) ALOGE("mpv_get_property(%s) format %d returned error %s", prop, format, mpv_error_string(result)); env->ReleaseStringUTFChars(jproperty, prop); return result; } -static int common_set_property(JNIEnv* env, jstring jproperty, mpv_format format, void* value) { - CHECK_MPV_INIT_RET(-1); +static int common_set_property(JNIEnv* env, jlong session, jstring jproperty, mpv_format format, void* value) { + SessionGuard guard(session); + if (!guard.mpv) return MPV_ERROR_UNINITIALIZED; const char* prop = env->GetStringUTFChars(jproperty, NULL); - int result = mpv_set_property(g_mpv, prop, format, value); + int result = mpv_set_property(guard.mpv, prop, format, value); if (result < 0) ALOGE("mpv_set_property(%s, %p) format %d returned error %s", prop, value, format, mpv_error_string(result)); env->ReleaseStringUTFChars(jproperty, prop); @@ -59,57 +65,58 @@ static int common_set_property(JNIEnv* env, jstring jproperty, mpv_format format return result; } -jni_func(jobject, nativeGetPropertyInt, jstring jproperty) { +jni_func(jobject, nativeGetPropertyInt, jlong session, jstring jproperty) { int64_t value = 0; - if (common_get_property(env, jproperty, MPV_FORMAT_INT64, &value) < 0) return NULL; + if (common_get_property(env, session, jproperty, MPV_FORMAT_INT64, &value) < 0) return NULL; return env->NewObject(java_Integer, java_Integer_init, (jint)value); } -jni_func(jobject, nativeGetPropertyDouble, jstring jproperty) { +jni_func(jobject, nativeGetPropertyDouble, jlong session, jstring jproperty) { double value = 0; - if (common_get_property(env, jproperty, MPV_FORMAT_DOUBLE, &value) < 0) return NULL; + if (common_get_property(env, session, jproperty, MPV_FORMAT_DOUBLE, &value) < 0) return NULL; return env->NewObject(java_Double, java_Double_init, (jdouble)value); } -jni_func(jobject, nativeGetPropertyBoolean, jstring jproperty) { +jni_func(jobject, nativeGetPropertyBoolean, jlong session, jstring jproperty) { int value = 0; - if (common_get_property(env, jproperty, MPV_FORMAT_FLAG, &value) < 0) return NULL; + if (common_get_property(env, session, jproperty, MPV_FORMAT_FLAG, &value) < 0) return NULL; return env->NewObject(java_Boolean, java_Boolean_init, (jboolean)value); } -jni_func(jstring, nativeGetPropertyString, jstring jproperty) { +jni_func(jstring, nativeGetPropertyString, jlong session, jstring jproperty) { char* value; - if (common_get_property(env, jproperty, MPV_FORMAT_STRING, &value) < 0) return NULL; + if (common_get_property(env, session, jproperty, MPV_FORMAT_STRING, &value) < 0) return NULL; jstring jvalue = new_java_string(env, value); mpv_free(value); return jvalue; } -jni_func(void, nativeSetPropertyInt, jstring jproperty, jint jvalue) { +jni_func(void, nativeSetPropertyInt, jlong session, jstring jproperty, jint jvalue) { int64_t value = static_cast(jvalue); - common_set_property(env, jproperty, MPV_FORMAT_INT64, &value); + common_set_property(env, session, jproperty, MPV_FORMAT_INT64, &value); } -jni_func(void, nativeSetPropertyDouble, jstring jproperty, jdouble jvalue) { +jni_func(void, nativeSetPropertyDouble, jlong session, jstring jproperty, jdouble jvalue) { double value = static_cast(jvalue); - common_set_property(env, jproperty, MPV_FORMAT_DOUBLE, &value); + common_set_property(env, session, jproperty, MPV_FORMAT_DOUBLE, &value); } -jni_func(void, nativeSetPropertyBoolean, jstring jproperty, jboolean jvalue) { +jni_func(void, nativeSetPropertyBoolean, jlong session, jstring jproperty, jboolean jvalue) { int value = jvalue == JNI_TRUE ? 1 : 0; - common_set_property(env, jproperty, MPV_FORMAT_FLAG, &value); + common_set_property(env, session, jproperty, MPV_FORMAT_FLAG, &value); } -jni_func(void, nativeSetPropertyString, jstring jproperty, jstring jvalue) { +jni_func(void, nativeSetPropertyString, jlong session, jstring jproperty, jstring jvalue) { const std::string value = java_string_to_utf8(env, jvalue); const char* value_ptr = value.c_str(); - common_set_property(env, jproperty, MPV_FORMAT_STRING, &value_ptr); + common_set_property(env, session, jproperty, MPV_FORMAT_STRING, &value_ptr); } -jni_func(void, nativeObserveProperty, jstring property, jint format) { - CHECK_MPV_INIT(); +jni_func(void, nativeObserveProperty, jlong session, jstring property, jint format) { + SessionGuard guard(session); + if (!guard.mpv) return; const char* prop = env->GetStringUTFChars(property, NULL); - int result = mpv_observe_property(g_mpv, 0, prop, (mpv_format)format); + int result = mpv_observe_property(guard.mpv, 0, prop, (mpv_format)format); if (result < 0) ALOGE("mpv_observe_property(%s) format %d returned error %s", prop, format, mpv_error_string(result)); env->ReleaseStringUTFChars(property, prop); } diff --git a/android/libmpv/src/main/cpp/render.cpp b/android/libmpv/src/main/cpp/render.cpp index 2e579e358..2e1c6aebc 100644 --- a/android/libmpv/src/main/cpp/render.cpp +++ b/android/libmpv/src/main/cpp/render.cpp @@ -6,16 +6,17 @@ #include "log.h" extern "C" { -jni_func(void, nativeAttachSurface, jobject surface_); -jni_func(void, nativeDetachSurface); -jni_func(void, nativeAttachOsdSurface, jobject surface_); -jni_func(void, nativeDetachOsdSurface); +jni_func(void, nativeAttachSurface, jlong session, jobject surface_); +jni_func(void, nativeDetachSurface, jlong session); +jni_func(void, nativeAttachOsdSurface, jlong session, jobject surface_); +jni_func(void, nativeDetachOsdSurface, jlong session); }; static jobject surface; -jni_func(void, nativeAttachSurface, jobject surface_) { - CHECK_MPV_INIT(); +jni_func(void, nativeAttachSurface, jlong session, jobject surface_) { + SessionGuard guard(session); + if (!guard.mpv) return; surface = env->NewGlobalRef(surface_); if (!surface) { @@ -23,15 +24,16 @@ jni_func(void, nativeAttachSurface, jobject surface_) { return; } int64_t wid = reinterpret_cast(surface); - int result = mpv_set_option(g_mpv, "wid", MPV_FORMAT_INT64, &wid); + int result = mpv_set_option(guard.mpv, "wid", MPV_FORMAT_INT64, &wid); if (result < 0) ALOGE("mpv_set_option(wid) returned error %s", mpv_error_string(result)); } -jni_func(void, nativeDetachSurface) { - CHECK_MPV_INIT(); +jni_func(void, nativeDetachSurface, jlong session) { + SessionGuard guard(session); + if (!guard.mpv) return; int64_t wid = 0; - int result = mpv_set_option(g_mpv, "wid", MPV_FORMAT_INT64, &wid); + int result = mpv_set_option(guard.mpv, "wid", MPV_FORMAT_INT64, &wid); if (result < 0) ALOGE("mpv_set_option(wid) returned error %s", mpv_error_string(result)); env->DeleteGlobalRef(surface); @@ -42,8 +44,9 @@ static jobject osd_surface; // The OSD plane of vo=mediacodec. Same lifetime rules as the video surface: // the global ref must outlive the VO, so detach only after vo has been unset. -jni_func(void, nativeAttachOsdSurface, jobject surface_) { - CHECK_MPV_INIT(); +jni_func(void, nativeAttachOsdSurface, jlong session, jobject surface_) { + SessionGuard guard(session); + if (!guard.mpv) return; osd_surface = env->NewGlobalRef(surface_); if (!osd_surface) { @@ -51,22 +54,24 @@ jni_func(void, nativeAttachOsdSurface, jobject surface_) { return; } int64_t wid = reinterpret_cast(osd_surface); - int result = mpv_set_option(g_mpv, "vo-mediacodec-osd-surface", MPV_FORMAT_INT64, &wid); + int result = mpv_set_option(guard.mpv, "vo-mediacodec-osd-surface", MPV_FORMAT_INT64, &wid); if (result < 0) ALOGE("mpv_set_option(vo-mediacodec-osd-surface) returned error %s", mpv_error_string(result)); } -jni_func(void, nativeDetachOsdSurface) { - CHECK_MPV_INIT(); - if (!osd_surface) return; +jni_func(void, nativeDetachOsdSurface, jlong session) { + SessionGuard guard(session); + if (!guard.mpv || !osd_surface) return; int64_t wid = 0; - int result = mpv_set_option(g_mpv, "vo-mediacodec-osd-surface", MPV_FORMAT_INT64, &wid); + int result = mpv_set_option(guard.mpv, "vo-mediacodec-osd-surface", MPV_FORMAT_INT64, &wid); if (result < 0) ALOGE("mpv_set_option(vo-mediacodec-osd-surface) returned error %s", mpv_error_string(result)); env->DeleteGlobalRef(osd_surface); osd_surface = NULL; } +// Caller holds the lifecycle write lock; the surfaces belong to the session +// being retired and are released with it. void render_cleanup(JNIEnv* env) { if (surface) { env->DeleteGlobalRef(surface); diff --git a/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayer.kt b/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayer.kt index d89e9f715..261a2030a 100644 --- a/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayer.kt +++ b/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayer.kt @@ -20,7 +20,19 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull -class MpvPlayer private constructor() : AutoCloseable { +/** + * Kotlin face of the process-global native player. Each instance is bound to + * one immutable native [session]; the JNI layer refuses any call that names + * a session it has retired and Kotlin drops any callback stamped with a + * session other than the published wrapper's. Together they make a retired + * wrapper's in-flight work (a hook handler still reshaping tracks, a queued + * property write, a late hook continuation) inert against its successor, + * and keep a retiring core's tail events out of the successor's flows. + */ +class MpvPlayer private constructor( + /** Identity of the native session this wrapper owns; see nativeCreate. */ + val session: Long +) : AutoCloseable { companion object { /** Upper bound for a hook handler; longer stalls playback start. */ @@ -43,66 +55,76 @@ class MpvPlayer private constructor() : AutoCloseable { configure: MpvPlayerConfig.() -> Unit = {} ): MpvPlayer = withContext(Dispatchers.IO) { checkNotMainThread("MPV initialization") - val player = MpvPlayer() - // Atomically replace; mark old as closed so its background close() skips nativeDestroy + // Retires any leaked predecessor natively and mints the new session. + val player = MpvPlayer(nativeCreate(context.applicationContext)) synchronized(instance) { - instance.getAndSet(player)?.also { it.closed = true } + val current = instance.get() + // Sessions are monotonic: a concurrent create that already published a + // newer one has retired this native session underneath us. + if (current != null && current.session > player.session) { + player.closed = true + throw MpvException("MPV session ${player.session} was superseded before it initialized") + } + instance.set(player) + // The predecessor's native session is gone; its close() finds nothing to destroy. + current?.closed = true } - // nativeCreate's safety net handles any leaked native session try { - nativeCreate(context.applicationContext) - MpvPlayerConfig().apply(configure) - nativeInit() + MpvPlayerConfig(player.session).apply(configure) + val result = nativeInit(player.session) + if (result < 0) throw MpvException("Failed to initialize mpv: error $result") ensureActive() player } catch (e: Throwable) { - instance.compareAndSet(player, null) - try { - nativeDestroy() - } catch (_: Throwable) {} + player.close() throw e } } - // JNI callbacks — called from native event thread + // JNI callbacks — called from the native event thread. Each names the + // session it originated from; only the wrapper published for that + // session may receive it. + + private fun target(session: Long): MpvPlayer? = instance.get()?.takeIf { it.session == session } @JvmStatic - fun onPropertyChanged(name: String, sourceId: Long, hasSourceId: Boolean) { - instance.get()?.rawPropertyChanges?.trySend( + fun onPropertyChanged(session: Long, name: String, sourceId: Long, hasSourceId: Boolean) { + target(session)?.rawPropertyChanges?.trySend( PropertyChange.None(name, sourceId.takeIf { hasSourceId }) ) } @JvmStatic - fun onPropertyChanged(name: String, value: Boolean, sourceId: Long, hasSourceId: Boolean) { - instance.get()?.rawPropertyChanges?.trySend( + fun onPropertyChanged(session: Long, name: String, value: Boolean, sourceId: Long, hasSourceId: Boolean) { + target(session)?.rawPropertyChanges?.trySend( PropertyChange.Flag(name, value, sourceId.takeIf { hasSourceId }) ) } @JvmStatic - fun onPropertyChanged(name: String, value: Long, sourceId: Long, hasSourceId: Boolean) { - instance.get()?.rawPropertyChanges?.trySend( + fun onPropertyChanged(session: Long, name: String, value: Long, sourceId: Long, hasSourceId: Boolean) { + target(session)?.rawPropertyChanges?.trySend( PropertyChange.Int64(name, value, sourceId.takeIf { hasSourceId }) ) } @JvmStatic - fun onPropertyChanged(name: String, value: Double, sourceId: Long, hasSourceId: Boolean) { - instance.get()?.rawPropertyChanges?.trySend( + fun onPropertyChanged(session: Long, name: String, value: Double, sourceId: Long, hasSourceId: Boolean) { + target(session)?.rawPropertyChanges?.trySend( PropertyChange.Double(name, value, sourceId.takeIf { hasSourceId }) ) } @JvmStatic - fun onPropertyChanged(name: String, value: String, sourceId: Long, hasSourceId: Boolean) { - instance.get()?.rawPropertyChanges?.trySend( + fun onPropertyChanged(session: Long, name: String, value: String, sourceId: Long, hasSourceId: Boolean) { + target(session)?.rawPropertyChanges?.trySend( PropertyChange.Str(name, value, sourceId.takeIf { hasSourceId }) ) } @JvmStatic fun onEvent( + session: Long, eventId: Int, sourceId: Long, hasSourceId: Boolean, @@ -114,12 +136,12 @@ class MpvPlayer private constructor() : AutoCloseable { sourceId.takeIf { hasSourceId }, positionSeconds.takeIf { hasPositionSeconds && it.isFinite() } ) ?: return - instance.get()?.rawEvents?.trySend(event) + target(session)?.rawEvents?.trySend(event) } @JvmStatic - fun onEndFile(reason: Int, sourceId: Long, hasSourceId: Boolean) { - instance.get()?.rawEvents?.trySend( + fun onEndFile(session: Long, reason: Int, sourceId: Long, hasSourceId: Boolean) { + target(session)?.rawEvents?.trySend( MpvEvent.EndFile( EndFileReason.fromId(reason), sourceId.takeIf { hasSourceId } @@ -128,17 +150,18 @@ class MpvPlayer private constructor() : AutoCloseable { } @JvmStatic - fun onLogMessage(prefix: String, level: Int, text: String) { + fun onLogMessage(session: Long, prefix: String, level: Int, text: String) { val logLevel = LogLevel.fromNative(level) ?: return - instance.get()?.rawLogMessages?.trySend(LogMessage(prefix, logLevel, text.trimEnd())) + target(session)?.rawLogMessages?.trySend(LogMessage(prefix, logLevel, text.trimEnd())) } @JvmStatic - fun onHook(name: String, id: Long) { - val player = instance.get() + fun onHook(session: Long, name: String, id: Long) { + val player = target(session) if (player == null || player.closed || !player.rawHooks.trySend(Hook(name, id)).isSuccess) { - // Nobody will answer: release mpv rather than leave it waiting. - nativeHookContinue(id) + // Nobody will answer: release mpv rather than leave it waiting. The + // native side drops this if the session has since been retired. + nativeHookContinue(session, id) } } @@ -148,54 +171,58 @@ class MpvPlayer private constructor() : AutoCloseable { } } - // JNI native declarations — private to avoid internal name mangling + // JNI native declarations — private to avoid internal name mangling. + // Every entry after nativeCreate names the session it acts for; the + // native side refuses a retired one. - @JvmStatic private external fun nativeCreate(appctx: Context) + /** Retires any leaked native session and returns the new session's identity. */ + @JvmStatic private external fun nativeCreate(appctx: Context): Long - @JvmStatic private external fun nativeInit() + /** 0 on success, otherwise a negative mpv error. */ + @JvmStatic private external fun nativeInit(session: Long): Int - @JvmStatic private external fun nativeDestroy() + @JvmStatic private external fun nativeDestroy(session: Long) /** Negative mpv error, the playlist entry id a `loadfile` created, or 0 when the command returned none. */ - @JvmStatic private external fun nativeCommand(cmd: Array): Long + @JvmStatic private external fun nativeCommand(session: Long, cmd: Array): Long - @JvmStatic private external fun nativeSetLogLevel(level: String): Int + @JvmStatic private external fun nativeSetLogLevel(session: Long, level: String): Int - @JvmStatic private external fun nativeHookContinue(id: Long) + @JvmStatic private external fun nativeHookContinue(session: Long, id: Long) - @JvmStatic private external fun nativeSetOptionString(name: String, value: String): Int + @JvmStatic private external fun nativeSetOptionString(session: Long, name: String, value: String): Int - @JvmStatic private external fun nativeAttachSurface(surface: Surface) + @JvmStatic private external fun nativeAttachSurface(session: Long, surface: Surface) - @JvmStatic private external fun nativeDetachSurface() + @JvmStatic private external fun nativeDetachSurface(session: Long) - @JvmStatic private external fun nativeAttachOsdSurface(surface: Surface) + @JvmStatic private external fun nativeAttachOsdSurface(session: Long, surface: Surface) - @JvmStatic private external fun nativeDetachOsdSurface() + @JvmStatic private external fun nativeDetachOsdSurface(session: Long) - @JvmStatic private external fun nativeGetPropertyInt(name: String): Int? + @JvmStatic private external fun nativeGetPropertyInt(session: Long, name: String): Int? - @JvmStatic private external fun nativeGetPropertyDouble(name: String): Double? + @JvmStatic private external fun nativeGetPropertyDouble(session: Long, name: String): Double? - @JvmStatic private external fun nativeGetPropertyBoolean(name: String): Boolean? + @JvmStatic private external fun nativeGetPropertyBoolean(session: Long, name: String): Boolean? - @JvmStatic private external fun nativeGetPropertyString(name: String): String? + @JvmStatic private external fun nativeGetPropertyString(session: Long, name: String): String? - @JvmStatic private external fun nativeSetPropertyInt(name: String, value: Int) + @JvmStatic private external fun nativeSetPropertyInt(session: Long, name: String, value: Int) - @JvmStatic private external fun nativeSetPropertyDouble(name: String, value: Double) + @JvmStatic private external fun nativeSetPropertyDouble(session: Long, name: String, value: Double) - @JvmStatic private external fun nativeSetPropertyBoolean(name: String, value: Boolean) + @JvmStatic private external fun nativeSetPropertyBoolean(session: Long, name: String, value: Boolean) - @JvmStatic private external fun nativeSetPropertyString(name: String, value: String) + @JvmStatic private external fun nativeSetPropertyString(session: Long, name: String, value: String) - @JvmStatic private external fun nativeObserveProperty(name: String, format: Int) + @JvmStatic private external fun nativeObserveProperty(session: Long, name: String, format: Int) - internal fun setOptionString(name: String, value: String): Int = nativeSetOptionString(name, value) + internal fun setOptionString(session: Long, name: String, value: String): Int = nativeSetOptionString(session, name, value) - internal fun requestLogMessages(level: String) { + internal fun requestLogMessages(session: Long, level: String) { checkNotMainThread("MPV log level change") - val result = nativeSetLogLevel(level) + val result = nativeSetLogLevel(session, level) if (result < 0) { throw MpvException("Failed to set log level: error $result") } @@ -235,7 +262,7 @@ class MpvPlayer private constructor() : AutoCloseable { pumpScope.launch { for (hook in rawHooks) { try { - val handler = hookHandler + val handler = if (closed) null else hookHandler if (handler != null) { withTimeoutOrNull(HOOK_TIMEOUT_MS) { handler(hook.name) } ?: android.util.Log.w("MpvPlayer", "Hook ${hook.name} handler overran; continuing playback") @@ -243,7 +270,10 @@ class MpvPlayer private constructor() : AutoCloseable { } catch (e: Exception) { android.util.Log.w("MpvPlayer", "Hook ${hook.name} handler failed; continuing playback", e) } finally { - if (!closed) nativeHookContinue(hook.id) + // Validated natively against this session, atomically with its + // retirement: a continuation that lost the race is dropped, never + // delivered to a successor's core. + nativeHookContinue(session, hook.id) } } } @@ -265,100 +295,95 @@ class MpvPlayer private constructor() : AutoCloseable { */ suspend fun command(vararg args: String): Long? { checkNotClosed() - val status = withContext(Dispatchers.IO) { nativeCommand(args) } + val status = withContext(Dispatchers.IO) { nativeCommand(session, args) } if (status < 0) throw MpvException("Command '${args.firstOrNull() ?: ""}' failed: error $status") return if (status > 0) status else null } /** Called on the core's ordered IO writer, without suspending between writes. */ fun setLogLevel(level: String) { - // Hold ownership through the native call: a retiring player's queued write - // must never change the subscription of the process-global successor. - synchronized(instance) { - checkNotClosed() - check(instance.get() === this) { "MpvPlayer is no longer active" } - requestLogMessages(level) - } + checkNotClosed() + requestLogMessages(session, level) } // Surface — not suspend, called from SurfaceHolder.Callback fun attachSurface(surface: Surface) { checkNotClosed() - nativeAttachSurface(surface) + nativeAttachSurface(session, surface) } fun detachSurface() { checkNotClosed() - nativeDetachSurface() + nativeDetachSurface(session) } /** OSD/subtitle plane for `vo=mediacodec`; attach before selecting the VO. */ fun attachOsdSurface(surface: Surface) { checkNotClosed() - nativeAttachOsdSurface(surface) + nativeAttachOsdSurface(session, surface) } fun detachOsdSurface() { checkNotClosed() - nativeDetachOsdSurface() + nativeDetachOsdSurface(session) } // Property getters suspend fun getInt(name: String): Int? { checkNotClosed() - return withContext(Dispatchers.IO) { nativeGetPropertyInt(name) } + return withContext(Dispatchers.IO) { nativeGetPropertyInt(session, name) } } suspend fun getDouble(name: String): Double? { checkNotClosed() - return withContext(Dispatchers.IO) { nativeGetPropertyDouble(name) } + return withContext(Dispatchers.IO) { nativeGetPropertyDouble(session, name) } } suspend fun getFlag(name: String): Boolean? { checkNotClosed() - return withContext(Dispatchers.IO) { nativeGetPropertyBoolean(name) } + return withContext(Dispatchers.IO) { nativeGetPropertyBoolean(session, name) } } suspend fun getString(name: String): String? { checkNotClosed() - return withContext(Dispatchers.IO) { nativeGetPropertyString(name) } + return withContext(Dispatchers.IO) { nativeGetPropertyString(session, name) } } // Property setters suspend fun setProperty(name: String, value: Int) { checkNotClosed() - withContext(Dispatchers.IO) { nativeSetPropertyInt(name, value) } + withContext(Dispatchers.IO) { nativeSetPropertyInt(session, name, value) } } suspend fun setProperty(name: String, value: Double) { checkNotClosed() - withContext(Dispatchers.IO) { nativeSetPropertyDouble(name, value) } + withContext(Dispatchers.IO) { nativeSetPropertyDouble(session, name, value) } } suspend fun setProperty(name: String, value: Boolean) { checkNotClosed() - withContext(Dispatchers.IO) { nativeSetPropertyBoolean(name, value) } + withContext(Dispatchers.IO) { nativeSetPropertyBoolean(session, name, value) } } suspend fun setProperty(name: String, value: String) { checkNotClosed() - withContext(Dispatchers.IO) { nativeSetPropertyString(name, value) } + withContext(Dispatchers.IO) { nativeSetPropertyString(session, name, value) } } // Property observation fun observeProperty(name: String, format: PropertyFormat): Flow { checkNotClosed() - nativeObserveProperty(name, format.nativeValue) + nativeObserveProperty(session, name, format.nativeValue) return propertyFlow.filter { it.name == name } } fun observeFlag(name: String): Flow { checkNotClosed() - nativeObserveProperty(name, PropertyFormat.Flag.nativeValue) + nativeObserveProperty(session, name, PropertyFormat.Flag.nativeValue) return propertyFlow .filterIsInstance() .filter { it.name == name } @@ -367,7 +392,7 @@ class MpvPlayer private constructor() : AutoCloseable { fun observeInt(name: String): Flow { checkNotClosed() - nativeObserveProperty(name, PropertyFormat.Int64.nativeValue) + nativeObserveProperty(session, name, PropertyFormat.Int64.nativeValue) return propertyFlow .filterIsInstance() .filter { it.name == name } @@ -376,7 +401,7 @@ class MpvPlayer private constructor() : AutoCloseable { fun observeDouble(name: String): Flow { checkNotClosed() - nativeObserveProperty(name, PropertyFormat.Double.nativeValue) + nativeObserveProperty(session, name, PropertyFormat.Double.nativeValue) return propertyFlow .filterIsInstance() .filter { it.name == name } @@ -385,7 +410,7 @@ class MpvPlayer private constructor() : AutoCloseable { fun observeString(name: String): Flow { checkNotClosed() - nativeObserveProperty(name, PropertyFormat.String.nativeValue) + nativeObserveProperty(session, name, PropertyFormat.String.nativeValue) return propertyFlow .filterIsInstance() .filter { it.name == name } @@ -402,18 +427,18 @@ class MpvPlayer private constructor() : AutoCloseable { * Android main thread so a slow vendor decoder cannot stall the UI. */ override fun close() { - val ownsNative = synchronized(instance) { + synchronized(instance) { if (closed) return checkNotMainThread("MPV destruction") closed = true - // If create() replaced us, nativeCreate handles the leaked native session. + hookHandler = null instance.compareAndSet(this, null) } - if (ownsNative) { - nativeDestroy() - } - // After nativeDestroy no callback can produce: closing the channels - // lets each pump drain what is already queued and then complete. + // A no-op natively when a later create() already retired this session; + // the successor is never ours to destroy. + nativeDestroy(session) + // After nativeDestroy no callback can produce for this session: closing + // the channels lets each pump drain what is already queued and complete. rawEvents.close() rawHooks.close() rawPropertyChanges.close() diff --git a/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayerConfig.kt b/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayerConfig.kt index 9e35e3147..20bae5dd0 100644 --- a/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayerConfig.kt +++ b/android/libmpv/src/main/java/com/edde746/plezy/libmpv/MpvPlayerConfig.kt @@ -1,14 +1,15 @@ package com.edde746.plezy.libmpv -class MpvPlayerConfig internal constructor() { +/** Pre-initialize configuration of one native session; every write names it. */ +class MpvPlayerConfig internal constructor(private val session: Long) { fun setOption(name: String, value: String) { - val result = MpvPlayer.setOptionString(name, value) + val result = MpvPlayer.setOptionString(session, name, value) if (result < 0) { throw MpvException("Failed to set option '$name' to '$value': error $result") } } fun setLogLevel(level: String) { - MpvPlayer.requestLogMessages(level) + MpvPlayer.requestLogMessages(session, level) } }