fix(player): bind live TV clock generations to the source the load reports

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.
This commit is contained in:
edde746
2026-09-06 13:29:37 +02:00
parent 01750ad92b
commit baa31742cb
24 changed files with 760 additions and 105 deletions
+28 -5
View File
@@ -6,6 +6,7 @@
#include <clocale>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <mutex>
#include <string>
@@ -30,7 +31,7 @@ jni_func(void, nativeDestroy);
jni_func(jint, nativeSetLogLevel, jstring level);
jni_func(void, nativeCommand, jobjectArray jarray);
jni_func(jlong, nativeCommand, jobjectArray jarray);
jni_func(void, nativeHookContinue, jlong id);
};
@@ -143,14 +144,18 @@ jni_func(jint, nativeSetLogLevel, jstring jlevel) {
return result;
}
jni_func(void, nativeCommand, jobjectArray jarray) {
CHECK_MPV_INIT();
// 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;
return MPV_ERROR_INVALID_PARAMETER;
}
std::vector<std::string> storage;
@@ -162,7 +167,25 @@ jni_func(void, nativeCommand, jobjectArray jarray) {
env->DeleteLocalRef(jarg);
}
mpv_command(g_mpv, arguments);
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) {