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.
161 lines
5.9 KiB
C++
161 lines
5.9 KiB
C++
#include <jni.h>
|
|
#include <mpv/client.h>
|
|
|
|
#include <cmath>
|
|
|
|
#include "globals.h"
|
|
#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, jsession, jprop, (jlong)source_id, jhas_source_id);
|
|
break;
|
|
case MPV_FORMAT_FLAG:
|
|
env->CallStaticVoidMethod(
|
|
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, 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, 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, jsession, jprop, jvalue, (jlong)source_id,
|
|
jhas_source_id);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if (jprop) env->DeleteLocalRef(jprop);
|
|
if (jvalue) env->DeleteLocalRef(jvalue);
|
|
}
|
|
|
|
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, (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) {
|
|
mpv_event_end_file* end_file = (mpv_event_end_file*)event->data;
|
|
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, (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, (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(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, (jlong)thread_session, jname, (jlong)hook->id);
|
|
if (jname) env->DeleteLocalRef(jname);
|
|
}
|
|
|
|
void* event_thread(void* arg) {
|
|
JNIEnv* env = NULL;
|
|
acquire_jni_env(g_vm, &env);
|
|
if (!env) die("failed to acquire java env");
|
|
|
|
int64_t source_id = 0;
|
|
bool has_source_id = false;
|
|
|
|
while (true) {
|
|
mpv_event* mp_event;
|
|
mpv_event_property* mp_property;
|
|
mpv_event_log_message* msg;
|
|
|
|
mp_event = mpv_wait_event(thread_mpv, -1.0);
|
|
|
|
if (g_event_thread_request_exit) break;
|
|
|
|
if (mp_event->event_id == MPV_EVENT_NONE) continue;
|
|
|
|
switch (mp_event->event_id) {
|
|
case MPV_EVENT_LOG_MESSAGE:
|
|
msg = (mpv_event_log_message*)mp_event->data;
|
|
sendLogMessageToJava(env, msg);
|
|
break;
|
|
case MPV_EVENT_PROPERTY_CHANGE:
|
|
mp_property = (mpv_event_property*)mp_event->data;
|
|
sendPropertyUpdateToJava(env, mp_property, source_id, has_source_id);
|
|
break;
|
|
case MPV_EVENT_END_FILE:
|
|
sendEndFileToJava(env, mp_event);
|
|
break;
|
|
case MPV_EVENT_START_FILE: {
|
|
mpv_event_start_file* start_file = (mpv_event_start_file*)mp_event->data;
|
|
has_source_id = start_file != NULL;
|
|
source_id = start_file ? start_file->playlist_entry_id : 0;
|
|
sendEventToJava(env, mp_event->event_id, source_id, has_source_id);
|
|
break;
|
|
}
|
|
case MPV_EVENT_FILE_LOADED:
|
|
sendEventToJava(env, mp_event->event_id, source_id, has_source_id);
|
|
break;
|
|
case MPV_EVENT_HOOK:
|
|
sendHookToJava(env, (mpv_event_hook*)mp_event->data);
|
|
break;
|
|
case MPV_EVENT_PLAYBACK_RESTART: {
|
|
double position_seconds = 0.0;
|
|
const bool has_position_seconds =
|
|
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;
|
|
}
|
|
default:
|
|
// Nothing on the Kotlin side consumes the remaining ids (MpvEvent.fromId).
|
|
break;
|
|
}
|
|
}
|
|
|
|
g_vm->DetachCurrentThread();
|
|
|
|
return NULL;
|
|
}
|