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 <noreply@anthropic.com>
This commit is contained in:
Zac Gaetano 2026-06-01 14:52:24 -04:00
parent b65ce5b0b7
commit 3d3c8c48de

View file

@ -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);
}