Anchor video pacing to presentation acknowledgements and total backlog

This commit is contained in:
evgeny committed 2026-10-19 19:03:48 +02:00
1 parent baeb4973dd
commit 28f3b264b8
2 files changed
+62 -29

No files matched your search

+47 -22
View File
@@ -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;
}
+15 -7
View File
@@ -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 <stdint.h>
@@ -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