181 lines
11 KiB
C
181 lines
11 KiB
C
/* Управляемые arrival/presentation clocks: FIFO сохраняется при паузах сети/CPU/GUI. */
|
|
#include <stdlib.h>
|
|
#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)
|
|
static const int64_t origin = 1000000;
|
|
|
|
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 = 480 };
|
|
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, 33333};
|
|
}
|
|
unsigned selected = 0, shown = 0, skipped = 0, available = 0, accelerated = 0;
|
|
int64_t last_show = 0, ack_at = 0, recovered_lag = INT64_MAX, audio_pts = origin;
|
|
for (int64_t now = origin; now < origin + 22000000 && shown + skipped < 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;
|
|
while (available < COUNT && frames[available].received_us <= now) ++available;
|
|
if (ack_at && now >= ack_at) {
|
|
call_video_playout_presented(&p, frames[selected - 1].pts_us, now);
|
|
CHECK(!last_show || now > last_show); last_show = now; ++shown; ack_at = 0;
|
|
}
|
|
struct call_clock_snapshot audio = {.pts_us = audio_pts, .rate_ppm = rate,
|
|
.valid = !(now >= origin + 4000000 && now < origin + 4100000)};
|
|
const struct call_video_playout_frame *f = selected < available ? frames + selected : NULL;
|
|
int action = call_video_playout_poll(&p, f, now, &audio, available - shown - skipped, 1);
|
|
if (now >= origin + 6000000 && shown) recovered_lag = audio.pts_us - p.last_show_pts;
|
|
if (action == CALL_VIDEO_PLAYOUT_WAIT) continue;
|
|
if (action == CALL_VIDEO_PLAYOUT_DROP_LATE) { ++selected; ++skipped; continue; }
|
|
CHECK(action == CALL_VIDEO_PLAYOUT_SHOW && audio.valid && !ack_at);
|
|
CHECK(f->pts_us - audio.pts_us <= 40000);
|
|
if (p.speed_ppm > 1050000) ++accelerated;
|
|
++selected; ack_at = now + 3000; // Не мгновенный GUI: кадр остаётся в общем запасе до ACK.
|
|
CHECK(call_video_playout_poll(&p, selected < available ? frames + selected : NULL, now, &audio,
|
|
available - shown - skipped, 1) == CALL_VIDEO_PLAYOUT_WAIT);
|
|
}
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "presentation recovery pause_ms=%u cpu=%d quantum_us=%lld audio_fast=%d shown=%u skipped=%u total=%u lag_us=%lld",
|
|
pause_ms, cpu, (long long)quantum, accelerated_audio, shown, skipped, COUNT, (long long)recovered_lag);
|
|
CHECK(shown + skipped == COUNT && accelerated);
|
|
if (quantum < 2000) CHECK(skipped == 0); // При достаточной частоте показа ускорение сохраняет все кадры.
|
|
CHECK(recovered_lag < (quantum < 2000 ? 100000 : 220000));
|
|
}
|
|
|
|
static void presentation_deadline(void) {
|
|
struct call_video_playout p; call_video_playout_init(&p, 9);
|
|
struct call_video_playout_frame f = {origin, origin, 33333};
|
|
struct call_clock_snapshot audio = {.pts_us = origin, .rate_ppm = 1000000, .valid = 1};
|
|
CHECK(call_video_playout_poll(&p, &f, origin, &audio, 2, 1) == CALL_VIDEO_PLAYOUT_SHOW);
|
|
CHECK(p.shown == 0 && p.awaiting); // Выбор кадра ещё не показ.
|
|
call_video_playout_presented(&p, f.pts_us, origin);
|
|
CHECK(p.shown == 1 && !p.awaiting);
|
|
CHECK(call_video_playout_poll(&p, NULL, origin + 5000, &audio, 0, 1) == CALL_VIDEO_PLAYOUT_WAIT);
|
|
f.pts_us += 33333; f.received_us = origin + 12000; audio.pts_us += 12000;
|
|
CHECK(call_video_playout_poll(&p, &f, origin + 12000, &audio, 1, 1) == CALL_VIDEO_PLAYOUT_WAIT);
|
|
int64_t deadline = p.deadline_us;
|
|
audio.pts_us += 12000;
|
|
CHECK(call_video_playout_poll(&p, &f, origin + 24000, &audio, 8, 1) == CALL_VIDEO_PLAYOUT_WAIT);
|
|
CHECK(p.deadline_us == deadline); // Приход пачки и движение audio clock не двигают уже назначенный срок.
|
|
CHECK(call_video_playout_poll(&p, &f, origin + 35000, &audio, 8, 1) == CALL_VIDEO_PLAYOUT_SHOW);
|
|
call_video_playout_presented(&p, f.pts_us, origin + 40000);
|
|
CHECK(p.last_show_us == origin + 40000);
|
|
CHECK(call_video_playout_poll(&p, &f, origin + 41000, &audio, 1, 1) == CALL_VIDEO_PLAYOUT_DROP_ORDER);
|
|
f.pts_us += 33333; audio.valid = 0;
|
|
CHECK(call_video_playout_poll(&p, &f, origin + 200000, &audio, 1, 1) == CALL_VIDEO_PLAYOUT_WAIT);
|
|
audio.valid = 1; audio.pts_us = origin + 200000;
|
|
CHECK(call_video_playout_poll(&p, &f, origin + 200000, &audio, 1, 1) == CALL_VIDEO_PLAYOUT_SHOW);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS presentation clock: 12ms starvation does not collapse cadence; ACK alone advances time");
|
|
}
|
|
|
|
static void queued_capture_gap(void) {
|
|
const int64_t gaps[] = {500000, 2264933, 10000000};
|
|
const unsigned fps[] = {15, 30, 60};
|
|
for (unsigned i = 0; i < 3; ++i) for (unsigned j = 0; j < 3; ++j) {
|
|
struct call_video_playout p; call_video_playout_init(&p, 11);
|
|
unsigned interval = 1000000 / fps[j];
|
|
struct call_video_playout_frame first = {origin, origin, interval}, next = {origin + gaps[i], origin, interval};
|
|
struct call_clock_snapshot audio = {.pts_us = next.pts_us + 1000000, .valid = 1, .rate_ppm = 1000000};
|
|
CHECK(call_video_playout_poll(&p, &first, origin, &audio, 2, 1) == CALL_VIDEO_PLAYOUT_SHOW);
|
|
call_video_playout_presented(&p, first.pts_us, origin);
|
|
CHECK(call_video_playout_poll(&p, &next, origin, &audio, 1, 1) == CALL_VIDEO_PLAYOUT_WAIT);
|
|
CHECK(call_video_playout_poll(&p, &next, origin + interval + 2000, &audio, 1, 1) == CALL_VIDEO_PLAYOUT_SHOW);
|
|
CHECK(p.last_pts == next.pts_us && p.source_gaps == 1);
|
|
}
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS queued capture gaps: 0.5/2.265/10s at 15/30/60fps, no repeated wait");
|
|
}
|
|
|
|
static void slowing_with_queue(void) {
|
|
struct call_video_playout p; call_video_playout_init(&p, 12);
|
|
struct call_clock_snapshot audio = {.pts_us = origin, .valid = 1, .rate_ppm = 1000000};
|
|
struct call_video_playout_frame f = {origin, origin, 33333};
|
|
uint32_t fast = 0; unsigned shows = 0;
|
|
for (int64_t now = origin; now < origin + 2000000; now += 1000) {
|
|
audio.pts_us = now;
|
|
unsigned queue = now < origin + 1000000 ? 8 : 2;
|
|
int action = call_video_playout_poll(&p, &f, now, &audio, queue, 1);
|
|
if (action == CALL_VIDEO_PLAYOUT_SHOW) {
|
|
call_video_playout_presented(&p, f.pts_us, now);
|
|
f.pts_us += 33333; ++shows;
|
|
}
|
|
if (now == origin + 999000) fast = p.speed_ppm;
|
|
}
|
|
CHECK(shows && fast > 1500000 && p.speed_ppm < 1050000);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS queue governor: speed_ppm=%u -> %u as total frames fall 8 -> 2", fast, p.speed_ppm);
|
|
}
|
|
|
|
static void render_latency_change(void) {
|
|
struct call_video_playout p; call_video_playout_init(&p, 13);
|
|
struct call_video_playout_frame f = {origin, origin, 33333};
|
|
struct call_clock_snapshot audio = {.valid = 1, .rate_ppm = 1000000};
|
|
int64_t now = origin;
|
|
for (unsigned i = 0; i < 240; ++i) {
|
|
f.pts_us = origin + i * 33333ll; audio.pts_us = f.pts_us + 100000;
|
|
int action = call_video_playout_poll(&p, &f, now, &audio, 2, 1);
|
|
if (action == CALL_VIDEO_PLAYOUT_WAIT) {
|
|
now = p.deadline_us;
|
|
CHECK(call_video_playout_poll(&p, &f, now, &audio, 2, 1) == CALL_VIDEO_PLAYOUT_SHOW);
|
|
} else CHECK(action == CALL_VIDEO_PLAYOUT_SHOW);
|
|
now += i < 120 ? 3000 : 9000;
|
|
call_video_playout_presented(&p, f.pts_us, now);
|
|
++now;
|
|
}
|
|
int64_t phase = p.last_show_us - (origin + 239 * 33333ll);
|
|
CHECK(phase >= 0 && phase < 15000);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "PASS render latency 3->9ms: phase_us=%lld; no accumulated per-frame GUI delay", (long long)phase);
|
|
}
|
|
|
|
/* Вход 60 fps, показ ограничен 60 Hz: после паузы ускориться физически нельзя.
|
|
* Каждый вход считается decoded; теряется лишь показ, ACK остаётся единственным владельцем часов. */
|
|
static void limited_display(unsigned pause_ms) {
|
|
enum { COUNT = 600 };
|
|
struct call_video_playout p; call_video_playout_init(&p, 14);
|
|
unsigned consumed = 0, shown = 0, skipped = 0;
|
|
int64_t ack_at = 0, pending_pts = 0, recovered = INT64_MAX;
|
|
for (int64_t now = origin; now < origin + 12000000 && consumed < COUNT; now += 1000) {
|
|
unsigned available = (unsigned)((now - origin) * 60 / 1000000) + 1;
|
|
if (available > COUNT) available = COUNT;
|
|
if (ack_at && now >= ack_at) {
|
|
call_video_playout_presented(&p, pending_pts, now); ++shown; ack_at = 0;
|
|
}
|
|
if (now >= origin + 1000000 && now < origin + 1000000 + pause_ms * 1000ll) continue;
|
|
if (consumed >= available) continue;
|
|
struct call_video_playout_frame f = {origin + consumed * 1000000ll / 60, origin, 16666};
|
|
struct call_clock_snapshot audio = {.pts_us = now, .rate_ppm = 1000000, .valid = 1};
|
|
const int64_t before = p.last_show_us, deadline = p.last_deadline;
|
|
int action = call_video_playout_poll(&p, &f, now, &audio, available - consumed + (ack_at != 0), 1);
|
|
if (action == CALL_VIDEO_PLAYOUT_SHOW) {
|
|
++consumed; pending_pts = f.pts_us;
|
|
ack_at = origin + ((now - origin) * 60 / 1000000 + 1) * 1000000 / 60;
|
|
} else if (action == CALL_VIDEO_PLAYOUT_DROP_LATE) {
|
|
++consumed; ++skipped;
|
|
CHECK(p.last_show_us == before && p.last_deadline == deadline && !p.awaiting);
|
|
} else CHECK(action == CALL_VIDEO_PLAYOUT_WAIT);
|
|
if (now > origin + 6000000) recovered = now - f.pts_us;
|
|
}
|
|
if (ack_at) { call_video_playout_presented(&p, pending_pts, ack_at); ++shown; }
|
|
DEBUG_INFO(DEBUG_CATEGORY_CALL, "60Hz recovery pause_ms=%u decoded=%u shown=%u skipped=%u lag_us=%lld",
|
|
pause_ms, consumed, shown, skipped, (long long)recovered);
|
|
CHECK(consumed == COUNT && shown + skipped == COUNT);
|
|
if (!pause_ms) CHECK(skipped == 0);
|
|
else CHECK(skipped > 0 && recovered < 130000);
|
|
}
|
|
|
|
int main(void) {
|
|
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO);
|
|
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);
|
|
}
|
|
presentation_deadline(); queued_capture_gap(); slowing_with_queue(); render_latency_change();
|
|
limited_display(0); limited_display(50); limited_display(100); limited_display(500); return 0;
|
|
}
|