Live-TV clock generations were matched to mpv sources first-in-first-out: every start-file popped the oldest registered open, on the assumption of exactly one start-file per open in dispatch order. Opens without a generation on the same player, an Android loadfile rejected silently by nativeCommand, and the independent delivery of the command ack and the start-file event all broke that, so a seek that reopened the stream could calibrate against the wrong source. The loadfile reply now carries mpv's playlist_entry_id on Android, Apple, Linux and Windows, PlayerNative.open resolves with it, and the live session binds each generation to that id explicitly. Source events that land before the reply are buffered per id and replayed on binding; a rejected or unreachable load fails its generation instead of leaving a phantom; opens with no generation are invisible to clock binding. Android now reports a rejected mpv command as COMMAND_FAILED like the other cores.
195 lines
5.4 KiB
C++
195 lines
5.4 KiB
C++
#include <jni.h>
|
|
#include <mpv/client.h>
|
|
#include <pthread.h>
|
|
|
|
#include <atomic>
|
|
#include <clocale>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
extern "C" {
|
|
#include <libavcodec/jni.h>
|
|
}
|
|
|
|
#include "event.h"
|
|
#include "jni_utils.h"
|
|
#include "log.h"
|
|
|
|
#define ARRAYLEN(a) (sizeof(a) / sizeof(a[0]))
|
|
|
|
void render_cleanup(JNIEnv* env);
|
|
|
|
extern "C" {
|
|
jni_func(void, nativeCreate, jobject appctx);
|
|
jni_func(void, nativeInit);
|
|
jni_func(void, nativeDestroy);
|
|
|
|
jni_func(jint, nativeSetLogLevel, jstring level);
|
|
|
|
jni_func(jlong, nativeCommand, jobjectArray jarray);
|
|
jni_func(void, nativeHookContinue, jlong id);
|
|
};
|
|
|
|
JavaVM* g_vm;
|
|
mpv_handle* g_mpv;
|
|
std::atomic<bool> g_event_thread_request_exit(false);
|
|
|
|
static pthread_t event_thread_id;
|
|
static bool event_thread_started = false;
|
|
static std::mutex g_lifecycle_mutex;
|
|
|
|
static void prepare_environment(JNIEnv* env, jobject appctx) {
|
|
setlocale(LC_NUMERIC, "C");
|
|
|
|
if (!env->GetJavaVM(&g_vm) && g_vm) av_jni_set_java_vm(g_vm, NULL);
|
|
|
|
jobject global_appctx = env->NewGlobalRef(appctx);
|
|
if (global_appctx) av_jni_set_android_app_ctx(global_appctx, NULL);
|
|
|
|
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;
|
|
}
|
|
mpv_handle* local_mpv = g_mpv;
|
|
|
|
// Configuration (including an invalid initial log level) can fail before
|
|
// nativeInit starts the event thread.
|
|
if (event_thread_started) {
|
|
g_event_thread_request_exit = true;
|
|
mpv_wakeup(local_mpv);
|
|
pthread_join(event_thread_id, NULL);
|
|
event_thread_started = false;
|
|
}
|
|
|
|
g_mpv = NULL;
|
|
|
|
// The MediaCodec VO can retain the Surface until final decoder teardown.
|
|
// Keep its JNI refs alive for the entire blocking termination.
|
|
mpv_terminate_destroy(local_mpv);
|
|
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;
|
|
|
|
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());
|
|
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);
|
|
|
|
const char* arguments[128] = {0};
|
|
int len = env->GetArrayLength(jarray);
|
|
if (len >= (int)ARRAYLEN(arguments)) {
|
|
die("too many command arguments");
|
|
return MPV_ERROR_INVALID_PARAMETER;
|
|
}
|
|
|
|
std::vector<std::string> storage;
|
|
storage.reserve(len);
|
|
for (int i = 0; i < len; ++i) {
|
|
jstring jarg = (jstring)env->GetObjectArrayElement(jarray, i);
|
|
storage.push_back(java_string_to_utf8(env, jarg));
|
|
arguments[i] = storage.back().c_str();
|
|
env->DeleteLocalRef(jarg);
|
|
}
|
|
|
|
mpv_node result{};
|
|
const int status = mpv_command_ret(g_mpv, arguments, &result);
|
|
if (status < 0) {
|
|
ALOGE("mpv_command(%s) returned error %s", len > 0 ? arguments[0] : "", mpv_error_string(status));
|
|
return status;
|
|
}
|
|
|
|
jlong playlist_entry_id = 0;
|
|
const mpv_node_list* map = result.format == MPV_FORMAT_NODE_MAP ? result.u.list : nullptr;
|
|
if (map && map->keys && map->values) {
|
|
for (int i = 0; i < map->num; ++i) {
|
|
if (map->keys[i] && strcmp(map->keys[i], "playlist_entry_id") == 0 && map->values[i].format == MPV_FORMAT_INT64) {
|
|
playlist_entry_id = (jlong)map->values[i].u.int64;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
mpv_free_node_contents(&result);
|
|
return playlist_entry_id;
|
|
}
|
|
|
|
jni_func(void, nativeHookContinue, jlong id) {
|
|
if (!g_mpv) return;
|
|
mpv_hook_continue(g_mpv, (uint64_t)id);
|
|
}
|