From bebfe7a43d2bb03b3fea6904a71366d711714f5a Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Tue, 2 Jun 2026 21:59:34 +0000 Subject: [PATCH] fix(audio): per-port A/V start barrier so video+audio FIFOs begin at the same instant (fixes fixed A/V offset) --- services/capture/deltacast-bridge/main.c | 67 +++++++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/services/capture/deltacast-bridge/main.c b/services/capture/deltacast-bridge/main.c index 977a0ee..59c1d3f 100644 --- a/services/capture/deltacast-bridge/main.c +++ b/services/capture/deltacast-bridge/main.c @@ -50,6 +50,19 @@ static void on_signal(int s) { (void)s; atomic_store(&g_stop, 1); } * rather than stopping the port, so the thread survives ffmpeg restarts. */ static atomic_int g_port_stop[MAX_PORTS]; +/* ── Per-port A/V start barrier ────────────────────────────────────────── + * Video and audio are two independent FIFOs that ffmpeg opens at slightly + * different times (input 0 video, then input 1 audio). Whichever thread's + * reader connects first would otherwise start pumping data immediately, + * giving its stream a head start → a fixed A/V offset that never recovers + * (raw streams carry no timestamps, so ffmpeg PTS-zeroes whatever each stream + * delivers first). To start both streams from the SAME instant, each thread + * publishes "my FIFO reader is connected" and then BOTH spin until the other + * side is also connected before writing their first real sample/frame. + * Reset to 0 each time a fresh pair of readers connects. */ +static atomic_int g_video_ready[MAX_PORTS]; +static atomic_int g_audio_ready[MAX_PORTS]; + /* ── Stream type by port index (non-contiguous SDK enum) ────────────── */ static ULONG rx_streamtype(unsigned port) { switch (port) { @@ -252,6 +265,28 @@ static void *audio_thread(void *arg) { #endif fcntl(fd, F_SETPIPE_SZ, 1024 * 1024); + /* A/V start barrier: announce ready, then wait (up to ~5s) for the + * video reader so both streams begin at the same instant. While + * waiting we drain+discard hardware audio slots so the board audio + * queue doesn't overflow and the first sample we write lines up with + * the first video frame. */ + atomic_store(&g_audio_ready[ps->port], 1); + { + int waited = 0; + while (!atomic_load(&g_video_ready[ps->port]) + && !atomic_load(&g_stop) && !atomic_load(&g_port_stop[ps->port]) + && waited < 5000) { + if (have_vhd_audio) { + HANDLE ds = NULL; + if (VHD_LockSlotHandle(stream, &ds) == VHDERR_NOERROR) + VHD_UnlockSlotHandle(ds); /* discard pre-roll audio */ + } + struct timespec t = {0, 1000000L}; nanosleep(&t, NULL); waited++; + } + fprintf(stderr, "[audio:%u] A/V barrier released (video_ready=%d)\n", + ps->port, atomic_load(&g_video_ready[ps->port])); + } + /* Reset wall-clock baseline after potentially blocking on open(). * Only used for the SILENCE fallback path (no hardware audio). */ struct timespec next; @@ -343,6 +378,8 @@ static void *audio_thread(void *arg) { } close(fd); + /* Reader gone — clear ready so the next session re-syncs at the barrier. */ + atomic_store(&g_audio_ready[ps->port], 0); } if (stream) { @@ -383,6 +420,26 @@ static void *video_thread(void *arg) { } } + /* A/V start barrier: announce ready, then wait (up to ~5s) for the + * audio reader so both streams begin at the same instant. While + * waiting we drain+discard incoming video slots so the board queue + * doesn't overflow and so the FIRST frame we actually write is the one + * captured at the moment audio also starts. */ + atomic_store(&g_video_ready[ps->port], 1); + { + int waited = 0; + while (!atomic_load(&g_audio_ready[ps->port]) + && !atomic_load(&g_stop) && !atomic_load(&g_port_stop[ps->port]) + && waited < 5000) { + HANDLE ds = NULL; + if (VHD_LockSlotHandle(ps->video_stream, &ds) == VHDERR_NOERROR) + VHD_UnlockSlotHandle(ds); /* discard pre-roll frame */ + struct timespec t = {0, 1000000L}; nanosleep(&t, NULL); waited++; + } + fprintf(stderr, "[video:%u] A/V barrier released (audio_ready=%d)\n", + ps->port, atomic_load(&g_audio_ready[ps->port])); + } + HANDLE slot = NULL; int fatal = 0; while (!atomic_load(&g_stop) && !atomic_load(&g_port_stop[ps->port])) { @@ -424,6 +481,8 @@ static void *video_thread(void *arg) { } close(fd); + /* Reader gone — clear ready so the next session re-syncs at the barrier. */ + atomic_store(&g_video_ready[ps->port], 0); if (fatal) break; } @@ -610,8 +669,12 @@ int main(int argc, char *argv[]) { memset(ps, 0, sizeof(ps)); int active_count = 0; - /* Initialise per-port stop flags. */ - for (int pi = 0; pi < MAX_PORTS; pi++) atomic_store(&g_port_stop[pi], 0); + /* Initialise per-port stop + A/V barrier flags. */ + for (int pi = 0; pi < MAX_PORTS; pi++) { + atomic_store(&g_port_stop[pi], 0); + atomic_store(&g_video_ready[pi], 0); + atomic_store(&g_audio_ready[pi], 0); + } for (int pi = 0; pi < port_count; pi++) { if (!locked[pi]) continue;