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.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <mpv/client.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -2,9 +2,42 @@
|
||||
|
||||
#include <jni.h>
|
||||
#include <mpv/client.h>
|
||||
#include <pthread.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
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<bool> 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;
|
||||
};
|
||||
|
||||
@@ -50,21 +50,22 @@ void init_methods_cache(JNIEnv* env) {
|
||||
|
||||
mpv_MpvPlayer = env->FindClass("com/edde746/plezy/libmpv/MpvPlayer");
|
||||
mpv_MpvPlayer = reinterpret_cast<jclass>(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;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -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<bool> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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);
|
||||
}
|
||||
|
||||
@@ -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<int64_t>(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<double>(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);
|
||||
}
|
||||
|
||||
@@ -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<intptr_t>(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<intptr_t>(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);
|
||||
|
||||
Reference in New Issue
Block a user