From 28f3b264b8943f4a5742118924bc83b3e490fa57 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 19 Oct 2026 19:03:48 +0200 Subject: [PATCH] Anchor video pacing to presentation acknowledgements and total backlog --- src/call/call_video_playout.c | 69 ++++++++++++++++++++++++----------- src/call/call_video_playout.h | 22 +++++++---- 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/src/call/call_video_playout.c b/src/call/call_video_playout.c index fbd1a871..a7fdf47a 100644 --- a/src/call/call_video_playout.c +++ b/src/call/call_video_playout.c @@ -19,10 +19,10 @@ int64_t call_video_playout_now_us(void) { } void call_video_playout_init(struct call_video_playout *p, uint64_t call_id) { - memset(p, 0, sizeof(*p)); p->call_id = call_id; p->last_pts = -1; + memset(p, 0, sizeof(*p)); p->call_id = call_id; p->last_pts = p->head_pts = -1; p->interval_min = p->av_min = INT64_MAX; p->av_max = INT64_MIN; p->speed_ppm = 1000000; - DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video playout init call=%llu sequential=1 max_speed_ppm=%d ahead_us=%d", + DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video playout init call=%llu presentation_clock=1 max_speed_ppm=%d ahead_us=%d", (unsigned long long)call_id, VIDEO_MAX_SPEED_PPM, VIDEO_AHEAD_US); } @@ -33,13 +33,14 @@ static void report(struct call_video_playout *p, int64_t now) { DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video playout call=%llu interval_us=%lld shown=%u speed_ppm=%u drop_order=%u " "cadence_us=%lld..%lld gap_pts_us=%lld gap_rx_us=%lld gap_dispatch_us=%lld " - "av_us=%lld..%lld dispatch_max_us=%lld data_shift_us=%lld audio_wait=%u clock_wait=%u source_gaps=%u source_gap_max_us=%lld", + "av_us=%lld..%lld dispatch_max_us=%lld data_shift_us=%lld audio_wait=%u clock_wait=%u source_gaps=%u source_gap_max_us=%lld " + "queued=%u reserve=%u awaiting=%d lead_us=%lld", (unsigned long long)p->call_id, (long long)(now - p->report_us), p->shown, p->speed_ppm, p->order, (long long)(p->interval_min == INT64_MAX ? 0 : p->interval_min), (long long)p->interval_max, (long long)p->gap_pts, (long long)p->gap_received, (long long)p->gap_dispatch, (long long)(p->shown ? p->av_min : 0), (long long)(p->shown ? p->av_max : 0), (long long)p->dispatch_max, (long long)p->data_shift, p->audio_wait, p->clock_wait, - p->source_gaps, (long long)p->source_gap_max); + p->source_gaps, (long long)p->source_gap_max, p->queued, p->reserve, p->awaiting, (long long)p->lead_us); p->report_us = now; p->shown = p->order = p->audio_wait = p->clock_wait = 0; p->interval_min = p->av_min = INT64_MAX; p->av_max = INT64_MIN; p->interval_max = p->dispatch_max = p->data_shift = 0; @@ -48,20 +49,20 @@ static void report(struct call_video_playout *p, int64_t now) { } enum call_video_playout_action call_video_playout_poll(struct call_video_playout *p, - const struct call_video_playout_frame *f, const struct call_video_playout_frame *next, - int64_t now, const struct call_clock_snapshot *clock) { - (void)next; + const struct call_video_playout_frame *f, int64_t now, const struct call_clock_snapshot *clock, + unsigned queued, unsigned reserve) { int audio_valid = clock && clock->valid; int64_t audio = clock ? clock->pts_us : -1; + p->queued = queued; p->reserve = reserve; report(p, now); p->deadline_us = now + 5000; if (p->audio_valid != !!audio_valid) { DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video playout clock call=%llu valid=%d pts=%lld", (unsigned long long)p->call_id, !!audio_valid, (long long)audio); p->audio_valid = !!audio_valid; } - if (!audio_valid) { p->anchored = 0; p->clock_wait++; return CALL_VIDEO_PLAYOUT_WAIT; } - // Ожидание новых данных уже прошло в реальном времени: повторно добавлять этот интервал нельзя. - if (!f) { p->anchored = 0; return CALL_VIDEO_PLAYOUT_WAIT; } + if (!audio_valid) { p->head_pts = -1; p->clock_wait++; return CALL_VIDEO_PLAYOUT_WAIT; } + // Пустой FIFO не означает разрыв часов; срок сохраняется до появления следующей головы. + if (!f || p->awaiting) return CALL_VIDEO_PLAYOUT_WAIT; if (f->pts_us <= p->last_pts || f->received_us < 0 || f->received_us > now || f->interval_us < 16666 || f->interval_us > 1000000) { DEBUG_WARN(DEBUG_CATEGORY_CALL, "video playout invalid/order call=%llu pts=%lld last=%lld received=%lld now=%lld interval_us=%u", @@ -69,17 +70,28 @@ enum call_video_playout_action call_video_playout_poll(struct call_video_playout (long long)f->received_us, (long long)now, f->interval_us); p->order++; return CALL_VIDEO_PLAYOUT_DROP_ORDER; } - // Аудио задаёт базовый темп; долг видео снимается с постоянной 0.5с, максимум 1.6x. + // Небольшой рабочий запас сохраняем; ускорение снимает избыток всего конвейера, а не только decoded FIFO. int64_t av = f->pts_us - audio; uint32_t base = clock->rate_ppm > 1000000 ? clock->rate_ppm : 1000000; - int64_t correction = av < -300000 ? 600000 : av < 0 ? -av * 2 : 0; - int64_t speed = base + correction; - p->speed_ppm = speed < VIDEO_MAX_SPEED_PPM ? (uint32_t)speed : VIDEO_MAX_SPEED_PPM; + unsigned target_frames = (reserve < 8 ? reserve : 8) + 1; + int64_t target = base; + if (queued > target_frames) target += (int64_t)(queued - target_frames) * 100000; + else if (queued < target_frames) target -= 20000; // До одного будущего кадра набираем запас без стартовой паузы. + // Слабая A/V-коррекция видит скрытый сетевой долг, но не съедает рабочий запас. + int64_t lag = -av - (int64_t)(target_frames + 1) * f->interval_us - 100000; + if (lag > 0) target += lag > 1000000 ? 50000 : lag / 20; + if (target > VIDEO_MAX_SPEED_PPM) target = VIDEO_MAX_SPEED_PPM; + int64_t elapsed = p->rate_us ? now - p->rate_us : 0; + if (elapsed > 100000) elapsed = 100000; + if (elapsed > 0) p->speed_ppm += (target - p->speed_ppm) * elapsed / (200000 + elapsed); + p->rate_us = now; int64_t delta = p->last_pts >= 0 ? f->pts_us - p->last_pts : 0; - // PTS-разрыв захвата уже прошёл; готовую голову нельзя снова удерживать delta/speed. - int64_t cadence = delta < f->interval_us ? delta : f->interval_us; - int64_t step = cadence * 1000000 / p->speed_ppm; - int64_t deadline = p->anchored ? p->last_deadline + step : now; + int64_t step = (int64_t)f->interval_us * 1000000 / p->speed_ppm; + if (p->head_pts != f->pts_us) { + p->head_pts = f->pts_us; + p->head_due_us = p->anchored ? p->last_show_us + step - p->lead_us : now; + } + int64_t deadline = p->head_due_us; p->deadline_us = deadline; if (av > VIDEO_AHEAD_US) { p->deadline_us = now + 5000; p->audio_wait++; @@ -93,9 +105,23 @@ enum call_video_playout_action call_video_playout_poll(struct call_video_playout p->source_gaps++; if (delta > p->source_gap_max) p->source_gap_max = delta; } - p->last_pts = f->pts_us; p->last_deadline = now; p->anchored = 1; + p->last_pts = f->pts_us; p->pending = *f; p->awaiting = 1; p->selected_us = now; p->pending_av = av; + if (now - deadline > p->dispatch_max) p->dispatch_max = now - deadline; + return CALL_VIDEO_PLAYOUT_SHOW; +} + +void call_video_playout_presented(struct call_video_playout *p, int64_t pts, int64_t now) { + if (!p->awaiting || p->pending.pts_us != pts || now < p->selected_us) { + DEBUG_WARN(DEBUG_CATEGORY_CALL, "video presentation ACK rejected call=%llu pts=%lld expected=%lld awaiting=%d now=%lld selected=%lld", + (unsigned long long)p->call_id, (long long)pts, (long long)p->pending.pts_us, p->awaiting, + (long long)now, (long long)p->selected_us); return; + } + const struct call_video_playout_frame *f = &p->pending; + int64_t av = p->pending_av, lead = now - p->selected_us; + // GUI freeze не становится постоянным опережением dispatch для последующих кадров. + if (lead <= f->interval_us / 2) p->lead_us = (p->lead_us * 7 + lead) / 8; + p->awaiting = 0; p->anchored = 1; p->last_deadline = now; p->head_pts = -1; { - if (now - deadline > p->dispatch_max) p->dispatch_max = now - deadline; if (av < p->av_min) p->av_min = av; if (av > p->av_max) p->av_max = av; if (p->last_show_us) { @@ -103,10 +129,9 @@ enum call_video_playout_action call_video_playout_poll(struct call_video_playout if (gap < p->interval_min) p->interval_min = gap; if (gap > p->interval_max) { p->interval_max = gap; p->gap_pts = f->pts_us - p->last_show_pts; - p->gap_received = f->received_us - p->last_received; p->gap_dispatch = now - deadline; + p->gap_received = f->received_us - p->last_received; p->gap_dispatch = lead; } } p->last_show_us = now; p->last_show_pts = f->pts_us; p->last_received = f->received_us; p->shown++; } - return CALL_VIDEO_PLAYOUT_SHOW; } diff --git a/src/call/call_video_playout.h b/src/call/call_video_playout.h index eb11f960..20f9b891 100644 --- a/src/call/call_video_playout.h +++ b/src/call/call_video_playout.h @@ -1,9 +1,10 @@ /* Общий таймер показа видео, один владелец — RX worker. Изображениями не владеет. - * poll вызывается для первого готового decoded кадра; next — следующий готовый кадр или NULL. + * poll вызывается для первого готового decoded кадра. queued — все ещё не показанные полные кадры. * SHOW/DROP_ORDER потребляют первый кадр, WAIT сохраняет его. Decoder работает по свободным output-буферам. * received_us ставится на входе RX port, до ожидания worker; все локальные времена — MONOTONIC, не BOOTTIME. - * После паузы расписание продолжается от фактического показа; долг снимается ускорением без пропусков. - * Невалидные аудиочасы снимают привязку, но не очищают decoder/очередь и не меняют порядок PTS. */ + * SHOW резервирует один кадр до presented(pts, фактическое время); только ACK продвигает часы. + * Короткое опустошение сохраняет расписание. Темп плавно зависит от суммарного запаса кадров. + * Невалидные аудиочасы запрещают новый показ, сохраняя decoder/очередь и порядок PTS. */ #ifndef CALL_VIDEO_PLAYOUT_H #define CALL_VIDEO_PLAYOUT_H #include @@ -14,7 +15,7 @@ extern "C" { struct call_video_playout_frame { int64_t pts_us, received_us; - uint32_t interval_us; // 1000000/format.fps: предел интервала показа, пауза capture не растягивает его. + uint32_t interval_us; // 1000000/format.fps; разрыв capture PTS не создаёт повторную паузу. }; enum call_video_playout_action { CALL_VIDEO_PLAYOUT_WAIT, CALL_VIDEO_PLAYOUT_SHOW, CALL_VIDEO_PLAYOUT_DROP_ORDER @@ -29,16 +30,23 @@ struct call_video_playout { unsigned shown, order, audio_wait, clock_wait; unsigned source_gaps; int64_t source_gap_max; // Большие разрывы capture PTS, уже прошедшие до показа. + struct call_video_playout_frame pending; // Единственный выбранный кадр до подтверждения потребителем. + int awaiting; + int64_t selected_us, pending_av, head_pts, head_due_us, lead_us, rate_us; + unsigned queued, reserve; // reserve — будущие кадры сверх головы, включая необходимый pipeline backend. }; int64_t call_video_playout_now_us(void); void call_video_playout_init(struct call_video_playout *p, uint64_t call_id); -/* frame==NULL позволяет обновлять состояние аудиочасов/сводку даже без готовых кадров. +/* frame==NULL обновляет состояние без снятия временной привязки. * deadline_us — абсолютный срок при WAIT, проверять аудиочасы не реже чем раз в 5мс. + * queued учитывает encoded, decoder, decoded и GUI каждый ровно один раз; reserve обычно 1. * source_epoch не участвует: коррекция аудио не означает разрыв видеокодека. */ enum call_video_playout_action call_video_playout_poll(struct call_video_playout *p, - const struct call_video_playout_frame *frame, const struct call_video_playout_frame *next, - int64_t now_us, const struct call_clock_snapshot *audio); + const struct call_video_playout_frame *frame, int64_t now_us, const struct call_clock_snapshot *audio, + unsigned queued, unsigned reserve); +/* Подтверждение выбранного PTS в MONOTONIC. Повторный/чужой ACK не меняет расписание. */ +void call_video_playout_presented(struct call_video_playout *p, int64_t pts_us, int64_t now_us); #ifdef __cplusplus } #endif