diff --git a/tests/test_call_video_playout.c b/tests/test_call_video_playout.c index e200c79f..7c0fb7f9 100644 --- a/tests/test_call_video_playout.c +++ b/tests/test_call_video_playout.c @@ -1,127 +1,69 @@ -/* Управляемое время: сеть, decoder и поздний запуск worker — независимые события. */ +/* Управляемое время: весь поток сохраняется при паузах сети/CPU и ускорении аудио. */ #include #include "../src/call/call_video_playout.h" #include "../lib/debug_config.h" #define CHECK(x) do { if (!(x)) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "playout test line=%d: %s", __LINE__, #x); exit(1); } } while (0) -#define SHOW CALL_VIDEO_PLAYOUT_SHOW -#define WAIT CALL_VIDEO_PLAYOUT_WAIT static const int64_t origin = 1000000; -static void start(struct call_video_playout *p) { - call_video_playout_init(p, 7); - struct call_video_playout_frame f = {origin, origin}; - CHECK(call_video_playout_poll(p, &f, NULL, origin, 1, origin) == SHOW); -} - -static void test_absolute_cpu(void) { - struct call_video_playout p; start(&p); - struct call_video_playout_frame f = {origin + 33333, origin + 20000}; - CHECK(call_video_playout_poll(&p, &f, NULL, origin + 32049, 1, origin + 32049) == WAIT); - CHECK(p.deadline_us == origin + 32050); - CHECK(call_video_playout_poll(&p, &f, NULL, origin + 41050, 1, origin + 41050) == SHOW); - CHECK(p.last_deadline == origin + 32050 && p.dispatch_max == 9000 && p.data_shift == 0); - f = (struct call_video_playout_frame){origin + 66666, origin + 50000}; - CHECK(call_video_playout_poll(&p, &f, NULL, origin + 64100, 1, origin + 64100) == SHOW); - CHECK(p.last_deadline == origin + 64100); - DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS playout: CPU +9ms never shifts the next absolute deadline"); -} - -static void test_data_stall(void) { - struct call_video_playout p; start(&p); - struct call_video_playout_frame f = {origin + 33333, origin + 52050}; - CHECK(call_video_playout_poll(&p, &f, NULL, origin + 58050, 1, origin + 58050) == SHOW); - CHECK(p.last_deadline == origin + 52050 && p.data_shift == 20000 && p.dispatch_max == 6000); - int64_t last_lag = 52050 - 33333; - for (int i = 2; i <= 40; i++) { - f = (struct call_video_playout_frame){origin + i * 33333, origin + i * 33333 - 20000}; - int64_t due = origin + 20000 + i * 32050; - if (due < f.received_us) due = f.received_us; - CHECK(call_video_playout_poll(&p, &f, NULL, due, 1, due) == SHOW); - int64_t lag = due - f.pts_us; - CHECK(lag <= last_lag && lag >= -20000); last_lag = lag; +static void simulate(unsigned pause_ms, int cpu, int64_t quantum, int accelerated_audio) { + struct call_video_playout p; call_video_playout_init(&p, 7); + enum { COUNT = 240 }; + struct call_video_playout_frame frames[COUNT]; + for (int i = 0; i < COUNT; ++i) { + int64_t arrival = origin + i * 33333ll; + if (!cpu && arrival >= origin + 1000000 && arrival < origin + 1000000 + pause_ms * 1000ll) + arrival = origin + 1000000 + pause_ms * 1000ll; + frames[i] = (struct call_video_playout_frame){origin + i * 33333ll, arrival}; } - CHECK(last_lag == -20000); - DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS playout: confirmed data stall drains at 1.04x; decode delay is excluded"); -} - -static void test_backlog(void) { - struct call_video_playout p; start(&p); - struct call_video_playout_frame frames[4]; - for (int i = 0; i < 4; i++) frames[i] = (struct call_video_playout_frame){origin + (i + 1) * 33333, origin}; - int64_t now = origin + 102000; - CHECK(call_video_playout_poll(&p, frames, frames + 1, now, 1, now) == CALL_VIDEO_PLAYOUT_DROP_LATE); - CHECK(call_video_playout_poll(&p, frames + 1, frames + 2, now, 1, now) == CALL_VIDEO_PLAYOUT_DROP_SCHEDULE); - CHECK(call_video_playout_poll(&p, frames + 2, frames + 3, now, 1, now) == SHOW); - CHECK(call_video_playout_poll(&p, frames + 3, NULL, now, 1, now) == WAIT); - CHECK(p.deadline_us == origin + 128200); - DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS playout: CPU pause drops obsolete presentation, never bursts through ready frames"); -} - -static void test_audio_bounds(void) { - struct call_video_playout p; start(&p); - int64_t previous = origin; - for (int i = 1; i < 300; i++) { - struct call_video_playout_frame f = {origin + i * 33333, origin}; - int64_t now = previous; - CHECK(call_video_playout_poll(&p, &f, NULL, now, 1, now) == WAIT); - now = p.deadline_us; - CHECK(call_video_playout_poll(&p, &f, NULL, now, 1, now) == SHOW); - CHECK(f.pts_us - now >= -50000 && f.pts_us - now <= 40000); - CHECK(now - previous >= 32050 && now - previous <= 33333); previous = now; + unsigned shown = 0, accelerated = 0; + int64_t last_show = 0, recovered_lag = INT64_MAX, audio_pts = origin; + for (int64_t now = origin; now < origin + 12000000 && shown < COUNT; now += quantum) { + uint32_t rate = accelerated_audio && now >= origin + 1500000 && now < origin + 2500000 ? 1100000 : 1000000; + if (now > origin) audio_pts += quantum * rate / 1000000; + if (cpu && now >= origin + 1000000 && now < origin + 1000000 + pause_ms * 1000ll) continue; + struct call_clock_snapshot audio = {.pts_us = audio_pts, .rate_ppm = rate, + .valid = !(now >= origin + 4000000 && now < origin + 4100000)}; + struct call_video_playout_frame *f = frames + shown; + int action = call_video_playout_poll(&p, f->received_us <= now ? f : NULL, NULL, now, &audio); + if (action == CALL_VIDEO_PLAYOUT_WAIT) continue; + CHECK(action == CALL_VIDEO_PLAYOUT_SHOW && audio.valid); + CHECK(!last_show || now > last_show); + CHECK(f->pts_us - audio.pts_us <= 40000); + if (p.speed_ppm > 1050000) accelerated++; + if (now >= origin + 6000000) recovered_lag = audio.pts_us - f->pts_us; + last_show = now; shown++; + // Даже готовая очередь после фриза не выдаётся целиком за один вызов worker. + if (shown < COUNT && frames[shown].received_us <= now) + CHECK(call_video_playout_poll(&p, frames + shown, NULL, now, &audio) == CALL_VIDEO_PLAYOUT_WAIT); } - CHECK(p.last_pts - previous == 40000); - CHECK(call_video_playout_poll(&p, NULL, NULL, previous + 10000, 0, 0) == WAIT && !p.anchored); - struct call_video_playout_frame f = {p.last_pts + 33333, previous + 20000}; - CHECK(call_video_playout_poll(&p, &f, NULL, previous + 100000, 1, f.pts_us) == SHOW); - CHECK(p.last_deadline == previous + 60000); // Аудио восстановилось: фаза ограничена, долг CPU не перенесён. - CHECK(call_video_playout_poll(&p, &f, NULL, previous + 100001, 1, f.pts_us) == CALL_VIDEO_PLAYOUT_DROP_ORDER); - DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS playout: audio upper boundary, clock outage, recovery and PTS order"); + CHECK(shown == COUNT && accelerated && recovered_lag < 50000); + DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS sequential recovery pause_ms=%u cpu=%d quantum_us=%lld audio_fast=%d shown=%u/%u lag_us=%lld", + pause_ms, cpu, (long long)quantum, accelerated_audio, shown, COUNT, (long long)recovered_lag); } -/* На 10с: burst доставки, смена FPS, 1.6x аудио, недостоверные часы и задержка worker. - * Показ разрешён только в текущем окне A/V; время приёма не подменяется временем poll. */ -static void test_combined(void) { +static void order_and_clock(void) { struct call_video_playout p; call_video_playout_init(&p, 9); - struct call_video_playout_frame frames[400]; - int64_t pts = origin; - for (int i = 0; i < 400; i++) { - int64_t arrival = pts - 15000 + (i % 5) * 1000; - if (i >= 30 && i < 34) arrival = origin + 1100000; - if (i && arrival < frames[i - 1].received_us) arrival = frames[i - 1].received_us; - frames[i] = (struct call_video_playout_frame){pts, arrival}; - pts += i < 180 ? 33333 : 16667; - } - int head = 0, shown = 0, dropped = 0; - int64_t last = -1, audio = origin - 1000; - for (int64_t now = origin; now < origin + 10000000 && head < 400; now += 1000) { - if (now < origin + 4200000 || now >= origin + 4320000) - audio += now >= origin + 4000000 && now < origin + 4200000 ? 1600 : 1000; - if (now >= origin + 2000000 && now < origin + 2080000) continue; - int valid = now < origin + 3000000 || now >= origin + 3100000; - for (;;) { - if (head == 400) break; - struct call_video_playout_frame *f = frames + head, *next = head + 1 < 400 ? frames + head + 1 : NULL; - if (f->received_us > now) f = NULL; - if (next && next->received_us > now) next = NULL; - int action = call_video_playout_poll(&p, f, next, now, valid, audio); - if (action == WAIT) break; - CHECK(action != CALL_VIDEO_PLAYOUT_DROP_ORDER && f); - if (action == SHOW) { - CHECK(valid && f->pts_us > last && f->pts_us - audio >= -50000 && f->pts_us - audio <= 40000); - last = f->pts_us; shown++; - } else dropped++; - head++; - if (action == SHOW) break; - } - } - DEBUG_INFO(DEBUG_CATEGORY_CALL, "playout combined simulation consumed=%d shown=%d dropped=%d", head, shown, dropped); - CHECK(head == 400 && shown > 300 && dropped > 0); - DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS playout: combined jitter/CPU/FPS/audio tempo simulation"); + struct call_video_playout_frame f = {origin, origin}; + struct call_clock_snapshot audio = {.pts_us = origin, .rate_ppm = 1000000}; + CHECK(call_video_playout_poll(&p, &f, NULL, origin, &audio) == CALL_VIDEO_PLAYOUT_WAIT); + audio.valid = 1; + CHECK(call_video_playout_poll(&p, &f, NULL, origin, &audio) == CALL_VIDEO_PLAYOUT_SHOW); + CHECK(call_video_playout_poll(&p, &f, NULL, origin + 1000, &audio) == CALL_VIDEO_PLAYOUT_DROP_ORDER); + f.pts_us += 33333; + audio.valid = 0; + CHECK(call_video_playout_poll(&p, &f, NULL, origin + 200000, &audio) == CALL_VIDEO_PLAYOUT_WAIT); + audio.valid = 1; audio.pts_us += 200000; + CHECK(call_video_playout_poll(&p, &f, NULL, origin + 200000, &audio) == CALL_VIDEO_PLAYOUT_SHOW); } int main(void) { debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); - test_absolute_cpu(); test_data_stall(); test_backlog(); test_audio_bounds(); test_combined(); - return 0; + const unsigned pauses[] = {50, 100, 200, 500}; + for (unsigned i = 0; i < sizeof(pauses) / sizeof(pauses[0]); ++i) + for (int cpu = 0; cpu <= 1; ++cpu) { + simulate(pauses[i], cpu, 1000, 0); + simulate(pauses[i], cpu, 15625, 1); + } + order_and_clock(); return 0; } 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 80cf5763..b5dd3170 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 @@ -17,7 +17,7 @@ internal object CallVideoNative { external fun playoutReset(handle: Long) /** >0: ждать микросекунды; 0: показать; <0: пропустить только показ. */ external fun playoutStep(handle: Long, pts: Long, received: Long, nextPts: Long, nextReceived: Long, - now: Long, valid: Boolean, audio: Long): Long + now: Long): 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 48445646..6d94dd39 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 @@ -57,7 +57,7 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin pending = null; displayed.value = null; CallVideoNative.requestKey(id) LogManager.addLog("INFO", "call", "video output attached=${surface != null} call=$id") } - // Два готовых output нужны для пропуска старого показа после паузы CPU, не для дополнительного запаса. + // Малый output lookahead; основной долг остаётся в encoded FIFO. while (decoder != null && pictures.size < 2 && !endSeen) { val out = decoder!!.dequeueOutputBuffer(info, 0) if (out >= 0) { @@ -82,7 +82,7 @@ internal class CallVideoPlayer(private val id: Long, private val failure: (Strin val valid = NativeLib.callAudioClock(id, now / 1000, clock) && clock[5] != 0L val first = pictures.peekFirst(); val nextPicture = if (pictures.size > 1) pictures.peekLast() else null val decision = CallVideoNative.playoutStep(playout, first?.timing?.pts ?: -1, first?.timing?.received ?: 0, - nextPicture?.timing?.pts ?: -1, nextPicture?.timing?.received ?: 0, now / 1000, valid, clock[4]) + nextPicture?.timing?.pts ?: -1, nextPicture?.timing?.received ?: 0, now / 1000) if (decision > 0) wakeUs = now / 1000 + decision else if (first != null) { val render = decision == 0L 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 e4ef3ca3..2efac24b 100644 --- a/tools/chatgui-android/jni_bridge/android_call_video_jni.c +++ b/tools/chatgui-android/jni_bridge/android_call_video_jni.c @@ -3,6 +3,7 @@ #include #include "../../../src/call/call_video_port.h" #include "../../../src/call/call_video_playout.h" +#include "../../../src/call/call_audio.h" #include "../../../src/call/call_video_bitstream.h" #include "../../../lib/debug_config.h" #include "../../../lib/mem.h" @@ -48,8 +49,8 @@ JNIEXPORT jlongArray JNICALL Java_com_utun_chat_data_CallVideoNative_status(JNIE (void)o; struct call_video_port_status s; if (call_video_port_status(id, &s)) return NULL; jlong values[] = {s.key_serial, s.rate_generation, s.rate_request, s.rate_bps, s.configured_generation, - s.error, s.minimum_bps, s.maximum_bps, s.decoded_generation, s.decoded_accepted}; - jlongArray out = (*e)->NewLongArray(e, 10); if (out) (*e)->SetLongArrayRegion(e, out, 0, 10, values); return out; + s.error, s.minimum_bps, s.maximum_bps, s.decoded_generation, s.decoded_accepted, s.can_encode}; + jlongArray out = (*e)->NewLongArray(e, 11); if (out) (*e)->SetLongArrayRegion(e, out, 0, 11, values); return out; } JNIEXPORT jlongArray JNICALL Java_com_utun_chat_data_CallVideoNative_choose( JNIEnv *e, jobject o, jbyteArray local, jbyteArray remote, jlongArray desired) { @@ -109,11 +110,12 @@ 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, jboolean valid, jlong audio) { + jlong pts, jlong received, jlong next_pts, jlong next_received, jlong now) { (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}, next = {next_pts, next_received}; - int action = call_video_playout_poll(p, pts >= 0 ? &f : NULL, next_pts >= 0 ? &next : NULL, now, valid, audio); + 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); if (action == CALL_VIDEO_PLAYOUT_SHOW) return 0; if (action != CALL_VIDEO_PLAYOUT_WAIT) return -action; int64_t wait = p->deadline_us - now;