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) } }