fix(audio): per-port A/V start barrier so video+audio FIFOs begin at the same instant (fixes fixed A/V offset)

This commit is contained in:
Zac Gaetano 2026-06-02 21:59:34 +00:00
parent 20d913fbad
commit bebfe7a43d

View file

@ -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. */ * rather than stopping the port, so the thread survives ffmpeg restarts. */
static atomic_int g_port_stop[MAX_PORTS]; 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) ────────────── */ /* ── Stream type by port index (non-contiguous SDK enum) ────────────── */
static ULONG rx_streamtype(unsigned port) { static ULONG rx_streamtype(unsigned port) {
switch (port) { switch (port) {
@ -252,6 +265,28 @@ static void *audio_thread(void *arg) {
#endif #endif
fcntl(fd, F_SETPIPE_SZ, 1024 * 1024); 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(). /* Reset wall-clock baseline after potentially blocking on open().
* Only used for the SILENCE fallback path (no hardware audio). */ * Only used for the SILENCE fallback path (no hardware audio). */
struct timespec next; struct timespec next;
@ -343,6 +378,8 @@ static void *audio_thread(void *arg) {
} }
close(fd); 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) { 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; HANDLE slot = NULL;
int fatal = 0; int fatal = 0;
while (!atomic_load(&g_stop) && !atomic_load(&g_port_stop[ps->port])) { while (!atomic_load(&g_stop) && !atomic_load(&g_port_stop[ps->port])) {
@ -424,6 +481,8 @@ static void *video_thread(void *arg) {
} }
close(fd); 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; if (fatal) break;
} }
@ -610,8 +669,12 @@ int main(int argc, char *argv[]) {
memset(ps, 0, sizeof(ps)); memset(ps, 0, sizeof(ps));
int active_count = 0; int active_count = 0;
/* Initialise per-port stop flags. */ /* Initialise per-port stop + A/V barrier flags. */
for (int pi = 0; pi < MAX_PORTS; pi++) atomic_store(&g_port_stop[pi], 0); 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++) { for (int pi = 0; pi < port_count; pi++) {
if (!locked[pi]) continue; if (!locked[pi]) continue;