diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoNative.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoNative.kt index 361cd11e..6d9d1cbe 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoNative.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoNative.kt @@ -16,8 +16,9 @@ internal object CallVideoNative { external fun playoutDestroy(handle: Long) external fun playoutReset(handle: Long) /** >0: ждать микросекунды; 0: показать; <0: пропустить только показ. */ - external fun playoutStep(handle: Long, pts: Long, received: Long, nextPts: Long, nextReceived: Long, - now: Long, intervalUs: Int): Long + external fun playoutStep(handle: Long, pts: Long, received: Long, now: Long, intervalUs: Int, + localQueued: Int, reserve: Int): Long + external fun playoutPresented(handle: Long, pts: Long, now: Long) external fun requestKey(id: Long) external fun range(id: Long, minimum: Int, maximum: Int): Boolean external fun inspect(codec: Int, bytes: ByteArray): LongArray? diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoPlayer.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoPlayer.kt index 600173a5..d8e7dd35 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoPlayer.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/data/CallVideoPlayer.kt @@ -12,14 +12,14 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin @Volatile private var running = true private data class Output(val surface: Surface?, val release: () -> Unit, val detach: Surface? = null) private val outputs = ConcurrentLinkedQueue() - private data class Presentation(val surface: Surface, val pts: Long) + private data class Presentation(val surface: Surface, val pts: Long, val at: Long = 0) private val presentations = ConcurrentLinkedQueue() @Volatile private var awaiting: Presentation? = null fun presented(surface: Surface, pts: Long): Boolean { val expected = awaiting ?: return true if (!running || expected.surface !== surface) return true if (expected.pts != pts) return false - presentations.add(expected); return true + presentations.add(expected.copy(at = System.nanoTime() / 1000)); return true } private val displayed = MutableStateFlow(null) val display: StateFlow = displayed @@ -71,11 +71,12 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin while (true) { val consumed = presentations.poll() ?: break if (consumed.surface === surface && consumed.pts == awaitingPts) { + CallVideoNative.playoutPresented(playout, consumed.pts, consumed.at) awaitingPts = -1; awaiting = null; worked = true } } // Малый output lookahead; основной долг остаётся в encoded FIFO. - while (decoder != null && pictures.size < 2 && !endSeen) { + while (decoder != null && pictures.size + (if (awaitingPts >= 0) 1 else 0) < 3 && !endSeen) { val out = decoder!!.dequeueOutputBuffer(info, 0) if (out >= 0) { worked = true; lastProgress = System.nanoTime() @@ -98,9 +99,9 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin val now = System.nanoTime() val valid = NativeLib.callAudioClock(id, now / 1000, clock) && clock[5] != 0L val first = if (awaitingPts < 0) pictures.peekFirst() else null - val nextPicture = if (first != null && pictures.size > 1) pictures.peekLast() else null + val localQueued = inputs.size + pictures.size + (if (pending != null) 1 else 0) + (if (awaitingPts >= 0) 1 else 0) val decision = CallVideoNative.playoutStep(playout, first?.timing?.pts ?: -1, first?.timing?.received ?: 0, - nextPicture?.timing?.pts ?: -1, nextPicture?.timing?.received ?: 0, now / 1000, first?.timing?.interval ?: 0) + now / 1000, first?.timing?.interval ?: 0, localQueued, 1) if (decision > 0) wakeUs = now / 1000 + decision else if (first != null) { val render = decision == 0L @@ -120,11 +121,12 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin close() } // Ожидание срока output не останавливает подачу input; очередь остаётся ограниченной. - if (pending == null && inputs.size + pictures.size < 8) { + val decodedSpace = pictures.size + (if (awaitingPts >= 0) 1 else 0) < 3 + if (pending == null && inputs.size < 8 && decodedSpace) { pending = CallVideoNative.pull(id, meta); if (pending != null) pulled++ } val data = pending - if (data != null && surface?.isValid == true) { + if (data != null && surface?.isValid == true && decodedSpace) { val next = CallVideoFormat.from(meta) if (next.generation == rejectedGeneration) { pending = null; continue } val key = meta[11] != 0L @@ -167,7 +169,7 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin if (now - reportAt >= 1_000_000_000) { LogManager.addLog("DEBUG", "call", "video playback call=$id intervalMs=${(now - reportAt) / 1_000_000} " + "pulled=$pulled queued=$queued decoded=$decoded rendered=$rendered skipped=$late inflight=${inputs.size} " + - "pending=${pending != null} ready=${pictures.size} awaitingSurfacePts=$awaitingPts " + + "pending=${pending != null} ready=${pictures.size} totalLocal=$localQueued awaitingSurfacePts=$awaitingPts " + "surfaceWaitMs=${if (awaitingPts >= 0) (now - lastRendered) / 1_000_000 else 0} audioValid=$valid audioPts=${clock[4]}") reportAt = now; pulled = 0; queued = 0; decoded = 0; rendered = 0; late = 0 } diff --git a/tools/chatgui-android/jni_bridge/android_call_video_jni.c b/tools/chatgui-android/jni_bridge/android_call_video_jni.c index 4338abe3..cea09b6c 100644 --- a/tools/chatgui-android/jni_bridge/android_call_video_jni.c +++ b/tools/chatgui-android/jni_bridge/android_call_video_jni.c @@ -110,17 +110,25 @@ JNIEXPORT void JNICALL Java_com_utun_chat_data_CallVideoNative_playoutReset(JNIE call_video_playout_init(p, p->call_id); } JNIEXPORT jlong JNICALL Java_com_utun_chat_data_CallVideoNative_playoutStep(JNIEnv *e, jobject o, jlong handle, - jlong pts, jlong received, jlong next_pts, jlong next_received, jlong now, jint interval_us) { + jlong pts, jlong received, jlong now, jint interval_us, jint local_queued, jint reserve) { (void)e; (void)o; struct call_video_playout *p = (void *)(uintptr_t)handle; if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video playout poll without handle"); return 5000; } - struct call_video_playout_frame f = {pts, received, interval_us}, next = {next_pts, next_received, interval_us}; + struct call_video_playout_frame f = {pts, received, interval_us}; struct call_clock_snapshot clock = {0}; call_audio_get_clock(p->call_id, now, &clock); - int action = call_video_playout_poll(p, pts >= 0 ? &f : NULL, next_pts >= 0 ? &next : NULL, now, &clock); + struct call_video_port_status status = {0}; call_video_port_status(p->call_id, &status); + int action = call_video_playout_poll(p, pts >= 0 ? &f : NULL, now, &clock, + status.rx_frames + (local_queued > 0 ? (unsigned)local_queued : 0), reserve > 0 ? (unsigned)reserve : 1); if (action == CALL_VIDEO_PLAYOUT_SHOW) return 0; if (action != CALL_VIDEO_PLAYOUT_WAIT) return -action; int64_t wait = p->deadline_us - now; return wait < 100 ? 100 : wait > 5000 ? 5000 : wait; } +JNIEXPORT void JNICALL Java_com_utun_chat_data_CallVideoNative_playoutPresented(JNIEnv *e, jobject o, + jlong handle, jlong pts, jlong now) { + (void)e; (void)o; struct call_video_playout *p = (void *)(uintptr_t)handle; + if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video presented without playout handle"); return; } + call_video_playout_presented(p, pts, now); +} JNIEXPORT void JNICALL Java_com_utun_chat_data_CallVideoNative_requestKey(JNIEnv *e, jobject o, jlong id) { (void)e; (void)o; call_video_port_request_key(id); }