From 3d3c8c48de161a35130075b76c5863a9526d2c00 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Mon, 1 Jun 2026 14:52:24 -0400 Subject: [PATCH] fix(deltacast): always open audio FIFO writer (silence fallback) to stop ffmpeg input deadlock ffmpeg opens all inputs before processing; input 1 is the audio FIFO. The bridge previously opened the FIFO writer only after VHD_OpenStreamHandle + VHD_StartStream succeeded, returning early on failure / no embedded audio and never opening the FIFO -> ffmpeg blocked forever on input 1 -> 0 fps and an empty HLS preview. Now the FIFO writer is opened unconditionally and first, and the audio thread feeds a continuous, wall-clock-paced s16le stereo stream (real samples when available, otherwise silence). SIGPIPE is ignored so a dying ffmpeg returns EPIPE instead of killing the bridge. Co-Authored-By: Claude Opus 4.8 --- services/capture/deltacast-bridge/main.c | 164 +++++++++++++++++------ 1 file changed, 120 insertions(+), 44 deletions(-) diff --git a/services/capture/deltacast-bridge/main.c b/services/capture/deltacast-bridge/main.c index 2725357..980af40 100644 --- a/services/capture/deltacast-bridge/main.c +++ b/services/capture/deltacast-bridge/main.c @@ -88,83 +88,155 @@ static VideoInfo video_info(VHD_VIDEOSTANDARD std, VHD_CLOCKDIVISOR div) { } } -/* ── Audio thread ────────────────────────────────────────────────────── */ +/* ── Audio thread ──────────────────────────────────────────── + * + * CRITICAL: ffmpeg opens ALL of its inputs before it starts processing any of + * them, and input 1 is this audio FIFO. Opening the read end of a FIFO blocks + * until a writer connects, so if this thread fails to open the FIFO writer + * ffmpeg hangs forever on input 1 -> no video frames are ever read from + * pipe:0 -> 0 fps and an empty HLS preview. Therefore the FIFO writer is + * opened UNCONDITIONALLY and FIRST, independent of any VideoMaster audio open, + * and the thread then feeds the FIFO a CONTINUOUS, wall-clock-paced s16le + * stereo stream (real samples when available, otherwise silence) so ffmpeg's + * A/V demux stays alive and video keeps flowing. */ typedef struct { HANDLE board; unsigned port; ULONG video_std; ULONG clock_div; + int fps_num; + int fps_den; const char *fifo_path; } AudioArgs; +/* Write exactly `len` bytes; returns 0 on success, -1 if writing should stop + * (EPIPE when ffmpeg is gone, or any other error). */ +static int write_all(int fd, const unsigned char *p, size_t len) { + size_t off = 0; + while (off < len) { + ssize_t n = write(fd, p + off, len - off); + if (n > 0) { off += (size_t)n; continue; } + if (n < 0 && errno == EINTR) continue; + return -1; + } + return 0; +} + static void *audio_thread(void *arg) { AudioArgs *a = (AudioArgs *)arg; - HANDLE stream = NULL; - ULONG r = VHD_OpenStreamHandle(a->board, rx_streamtype(a->port), - VHD_SDI_STPROC_DISJOINED_ANC, - NULL, &stream, NULL); - if (r != VHDERR_NOERROR) { - fprintf(stderr, "[audio] VHD_OpenStreamHandle failed: %lu\n", r); + /* 1. Open the FIFO writer FIRST, unconditionally. This is what unblocks + * ffmpeg's input 1; we must reach it even if the VHD audio open fails. */ + int fd = open(a->fifo_path, O_WRONLY); + if (fd < 0) { + fprintf(stderr, "[audio] open FIFO failed: %s\n", strerror(errno)); return NULL; } - VHD_SetStreamProperty(stream, VHD_SDI_SP_VIDEO_STANDARD, a->video_std); - VHD_SetStreamProperty(stream, VHD_SDI_SP_CLOCK_SYSTEM, a->clock_div); - VHD_SetStreamProperty(stream, VHD_CORE_SP_TRANSFER_SCHEME, VHD_TRANSFER_SLAVED); + /* 2. Pacing + silence buffer sized to one video frame of 48kHz stereo + * s16le. samples_per_frame = 48000 * fps_den / fps_num (rounded). */ + const int AUDIO_RATE = 48000; + const int CHANNELS = 2; + const size_t FRAME_BYTES = (size_t)CHANNELS * 2; /* s16le stereo */ + int fps_num = a->fps_num > 0 ? a->fps_num : 25; + int fps_den = a->fps_den > 0 ? a->fps_den : 1; + long samples_per_frame = ((long)AUDIO_RATE * fps_den + fps_num / 2) / fps_num; + if (samples_per_frame < 1) samples_per_frame = 1; + size_t tick_bytes = (size_t)samples_per_frame * FRAME_BYTES; - /* Stereo pair, 16-bit, 48kHz on group 0 channel 0 */ ULONG max_samples = VHD_GetNbSamples((VHD_VIDEOSTANDARD)a->video_std, (VHD_CLOCKDIVISOR)a->clock_div, VHD_ASR_48000, 0); ULONG block_size = VHD_GetBlockSize(VHD_AF_16, VHD_AM_STEREO); - ULONG buf_sz = (max_samples + 4) * block_size; /* +4 for 29.97 variation */ - unsigned char *buf = calloc(1, buf_sz); - if (!buf) { VHD_CloseStreamHandle(stream); return NULL; } + size_t vhd_buf_sz = ((size_t)max_samples + 64) * (block_size ? block_size : FRAME_BYTES); + size_t buf_sz = vhd_buf_sz > tick_bytes ? vhd_buf_sz : tick_bytes; + unsigned char *buf = calloc(1, buf_sz); /* zeroed -> doubles as silence */ + if (!buf) { close(fd); return NULL; } + /* 3. Try to open the VideoMaster audio stream (best effort, NON-FATAL). */ + HANDLE stream = NULL; + int have_vhd_audio = 0; VHD_AUDIOINFO ai; memset(&ai, 0, sizeof(ai)); - ai.pAudioGroups[0].pAudioChannels[0].Mode = VHD_AM_STEREO; - ai.pAudioGroups[0].pAudioChannels[0].BufferFormat = VHD_AF_16; - ai.pAudioGroups[0].pAudioChannels[0].pData = buf; - if (VHD_StartStream(stream) != VHDERR_NOERROR) { - free(buf); VHD_CloseStreamHandle(stream); return NULL; - } - - /* Open FIFO for writing — blocks until FFmpeg opens the read end */ - int fd = open(a->fifo_path, O_WRONLY); - if (fd < 0) { - fprintf(stderr, "[audio] open FIFO failed: %s\n", strerror(errno)); - VHD_StopStream(stream); VHD_CloseStreamHandle(stream); free(buf); - return NULL; + ULONG r = VHD_OpenStreamHandle(a->board, rx_streamtype(a->port), + VHD_SDI_STPROC_DISJOINED_ANC, + NULL, &stream, NULL); + if (r == VHDERR_NOERROR) { + VHD_SetStreamProperty(stream, VHD_SDI_SP_VIDEO_STANDARD, a->video_std); + VHD_SetStreamProperty(stream, VHD_SDI_SP_CLOCK_SYSTEM, a->clock_div); + VHD_SetStreamProperty(stream, VHD_CORE_SP_TRANSFER_SCHEME, VHD_TRANSFER_SLAVED); + + ai.pAudioGroups[0].pAudioChannels[0].Mode = VHD_AM_STEREO; + ai.pAudioGroups[0].pAudioChannels[0].BufferFormat = VHD_AF_16; + ai.pAudioGroups[0].pAudioChannels[0].pData = buf; + + if (VHD_StartStream(stream) == VHDERR_NOERROR) { + have_vhd_audio = 1; + } else { + fprintf(stderr, "[audio] VHD_StartStream failed - feeding silence\n"); + VHD_CloseStreamHandle(stream); + stream = NULL; + } + } else { + fprintf(stderr, "[audio] VHD_OpenStreamHandle failed (%lu) - feeding silence\n", r); } + /* 4. Continuous, wall-clock-paced feed loop: real audio when available, + * otherwise silence, so ffmpeg's input 1 never starves. */ + struct timespec next; + clock_gettime(CLOCK_MONOTONIC, &next); + long frame_ns = (long)(1000000000.0 * (double)fps_den / (double)fps_num); HANDLE slot = NULL; + while (!atomic_load(&g_stop)) { - r = VHD_LockSlotHandle(stream, &slot); - if (r == VHDERR_NOERROR) { - ai.pAudioGroups[0].pAudioChannels[0].DataSize = buf_sz; - if (VHD_SlotExtractAudio(slot, &ai) == VHDERR_NOERROR) { - ULONG sz = ai.pAudioGroups[0].pAudioChannels[0].DataSize; - if (sz > 0) { - ULONG aw = 0; - while (aw < sz) { - ssize_t n = write(fd, buf + aw, sz - aw); - if (n <= 0) { atomic_store(&g_stop, 1); break; } - aw += (ULONG)n; - } + size_t out_bytes = 0; + + if (have_vhd_audio) { + r = VHD_LockSlotHandle(stream, &slot); + if (r == VHDERR_NOERROR) { + ai.pAudioGroups[0].pAudioChannels[0].DataSize = (ULONG)buf_sz; + if (VHD_SlotExtractAudio(slot, &ai) == VHDERR_NOERROR) { + ULONG sz = ai.pAudioGroups[0].pAudioChannels[0].DataSize; + if (sz > 0 && (size_t)sz <= buf_sz) out_bytes = (size_t)sz; } + VHD_UnlockSlotHandle(slot); + } else if (r != VHDERR_TIMEOUT) { + fprintf(stderr, "[audio] lock error %lu - degrading to silence\n", r); + VHD_StopStream(stream); + VHD_CloseStreamHandle(stream); + stream = NULL; + have_vhd_audio = 0; } - VHD_UnlockSlotHandle(slot); - } else if (r != VHDERR_TIMEOUT) { + } + + if (out_bytes == 0) { + memset(buf, 0, tick_bytes); /* one frame of silence */ + out_bytes = tick_bytes; + } + + if (write_all(fd, buf, out_bytes) < 0) { + atomic_store(&g_stop, 1); /* ffmpeg closed the FIFO (EPIPE) */ break; } + + next.tv_nsec += frame_ns; + while (next.tv_nsec >= 1000000000L) { next.tv_nsec -= 1000000000L; next.tv_sec += 1; } + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + if (next.tv_sec > now.tv_sec || + (next.tv_sec == now.tv_sec && next.tv_nsec > now.tv_nsec)) { + clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL); + } else { + next = now; /* fell behind (real-audio burst) - resync */ + } } close(fd); - VHD_StopStream(stream); - VHD_CloseStreamHandle(stream); + if (stream) { + VHD_StopStream(stream); + VHD_CloseStreamHandle(stream); + } free(buf); return NULL; } @@ -185,6 +257,9 @@ int main(int argc, char *argv[]) { signal(SIGINT, on_signal); signal(SIGTERM, on_signal); + /* Don't let a dying ffmpeg kill us with SIGPIPE - writes return EPIPE + * and the FIFO/stdout write loops handle that by stopping cleanly. */ + signal(SIGPIPE, SIG_IGN); /* ── Init API ─────────────────────────────────────────────────── */ ULONG dll_ver, nb_boards; @@ -274,7 +349,8 @@ int main(int argc, char *argv[]) { /* ── Launch audio thread (FIFO open blocks until FFmpeg connects) ── */ pthread_t audio_tid = 0; - AudioArgs audio_args = { board, port_id, video_std, clock_div, audio_pipe }; + AudioArgs audio_args = { board, port_id, video_std, clock_div, + vi.fps_num, vi.fps_den, audio_pipe }; if (audio_pipe) { pthread_create(&audio_tid, NULL, audio_thread, &audio_args); }