dragonflight/services/capture/deltacast-bridge/main.c
ZGaetano cad1e52c38 fix(deltacast-bridge): reset signal-wait deadline AFTER acquiring flock
The signal timeout deadline was set at process start before waiting for
the flock. Bridges queued behind earlier ports waited minutes for the
lock, then found their 30s signal deadline had already expired before
they even opened the board, causing false "no signal" failures on ports
that have live signal.

Fix: move clock_gettime deadline initialisation to AFTER flock acquired
and board opened, so the full sig_timeout is always available for signal
detection regardless of queue wait time.
2026-06-01 19:32:01 -04:00

440 lines
20 KiB
C

/* services/capture/deltacast-bridge/main.c
*
* Deltacast VideoMaster SDI capture bridge.
* Writes raw UYVY video to stdout and stereo PCM to a named FIFO.
* Emits one JSON line to stderr on signal lock before streaming starts.
*
* Usage:
* deltacast-capture --device <N> --port <N> --audio-pipe <path>
* [--signal-timeout <sec>]
*/
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <time.h>
#include <unistd.h>
#include "VideoMasterHD_Core.h"
#include "VideoMasterHD_Sdi.h"
#include "VideoMasterHD_Sdi_Audio.h"
/* ── Globals ─────────────────────────────────────────────────────────── */
static atomic_int g_stop = 0;
static void on_signal(int s) { (void)s; atomic_store(&g_stop, 1); }
/* ── Stream type by port index ───────────────────────────────────────── */
static ULONG rx_streamtype(unsigned port) {
switch (port) {
case 0: return VHD_ST_RX0;
case 1: return VHD_ST_RX1;
case 2: return VHD_ST_RX2;
case 3: return VHD_ST_RX3;
case 4: return VHD_ST_RX4;
case 5: return VHD_ST_RX5;
case 6: return VHD_ST_RX6;
case 7: return VHD_ST_RX7;
default:
fprintf(stderr, "{\"error\":\"port %u not supported (max 7)\"}\n", port);
return VHD_ST_RX0; /* caller will fail on signal lock */
}
}
/* ── Loopback board property by port index ───────────────────────────── */
static ULONG loopback_prop(unsigned port) {
switch (port) {
case 0: return VHD_CORE_BP_PASSIVE_LOOPBACK_0;
case 1: return VHD_CORE_BP_PASSIVE_LOOPBACK_1;
case 2: return VHD_CORE_BP_PASSIVE_LOOPBACK_2;
case 3: return VHD_CORE_BP_PASSIVE_LOOPBACK_3;
default: return VHD_CORE_BP_PASSIVE_LOOPBACK_0;
}
}
/* ── Video standard → width/height/fps/interlaced ───────────────────── */
typedef struct { int width, height, fps_num, fps_den; int interlaced; } VideoInfo;
static VideoInfo video_info(VHD_VIDEOSTANDARD std, VHD_CLOCKDIVISOR div) {
int ntsc = (div == VHD_CLOCKDIV_1001);
switch (std) {
case VHD_VIDEOSTD_S274M_1080p_25Hz: return (VideoInfo){1920,1080,25,1,0};
case VHD_VIDEOSTD_S274M_1080p_30Hz: return (VideoInfo){1920,1080,ntsc?30000:30,ntsc?1001:1,0};
case VHD_VIDEOSTD_S274M_1080p_24Hz: return (VideoInfo){1920,1080,ntsc?24000:24,ntsc?1001:1,0};
case VHD_VIDEOSTD_S274M_1080p_50Hz: return (VideoInfo){1920,1080,50,1,0};
case VHD_VIDEOSTD_S274M_1080p_60Hz: return (VideoInfo){1920,1080,ntsc?60000:60,ntsc?1001:1,0};
case VHD_VIDEOSTD_S274M_1080psf_24Hz: return (VideoInfo){1920,1080,ntsc?24000:24,ntsc?1001:1,0};
case VHD_VIDEOSTD_S274M_1080psf_25Hz: return (VideoInfo){1920,1080,25,1,0};
case VHD_VIDEOSTD_S274M_1080psf_30Hz: return (VideoInfo){1920,1080,ntsc?30000:30,ntsc?1001:1,0};
case VHD_VIDEOSTD_S274M_1080i_50Hz: return (VideoInfo){1920,1080,25,1,1};
case VHD_VIDEOSTD_S274M_1080i_60Hz: return (VideoInfo){1920,1080,ntsc?30000:30,ntsc?1001:1,1};
case VHD_VIDEOSTD_S296M_720p_50Hz: return (VideoInfo){1280,720,50,1,0};
case VHD_VIDEOSTD_S296M_720p_60Hz: return (VideoInfo){1280,720,ntsc?60000:60,ntsc?1001:1,0};
case VHD_VIDEOSTD_S296M_720p_25Hz: return (VideoInfo){1280,720,25,1,0};
case VHD_VIDEOSTD_S296M_720p_30Hz: return (VideoInfo){1280,720,ntsc?30000:30,ntsc?1001:1,0};
case VHD_VIDEOSTD_S296M_720p_24Hz: return (VideoInfo){1280,720,ntsc?24000:24,ntsc?1001:1,0};
case VHD_VIDEOSTD_3840x2160p_24Hz: return (VideoInfo){3840,2160,ntsc?24000:24,ntsc?1001:1,0};
case VHD_VIDEOSTD_3840x2160p_25Hz: return (VideoInfo){3840,2160,25,1,0};
case VHD_VIDEOSTD_3840x2160p_30Hz: return (VideoInfo){3840,2160,ntsc?30000:30,ntsc?1001:1,0};
case VHD_VIDEOSTD_3840x2160p_50Hz: return (VideoInfo){3840,2160,50,1,0};
case VHD_VIDEOSTD_3840x2160p_60Hz: return (VideoInfo){3840,2160,ntsc?60000:60,ntsc?1001:1,0};
case VHD_VIDEOSTD_S259M_NTSC_480: return (VideoInfo){720,480,ntsc?30000:30,ntsc?1001:1,1};
default: return (VideoInfo){1920,1080,25,1,0};
}
}
/* ── 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;
/* 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;
}
/* 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;
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);
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));
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)) {
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;
}
}
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);
if (stream) {
VHD_StopStream(stream);
VHD_CloseStreamHandle(stream);
}
free(buf);
return NULL;
}
/* ── Main ────────────────────────────────────────────────────────────── */
int main(int argc, char *argv[]) {
unsigned device_id = 0;
unsigned port_id = 0;
int sig_timeout = 30;
const char *audio_pipe = NULL;
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--device") && i+1 < argc) device_id = (unsigned)atoi(argv[++i]);
else if (!strcmp(argv[i], "--port") && i+1 < argc) port_id = (unsigned)atoi(argv[++i]);
else if (!strcmp(argv[i], "--audio-pipe") && i+1 < argc) audio_pipe = argv[++i];
else if (!strcmp(argv[i], "--signal-timeout") && i+1 < argc) sig_timeout = atoi(argv[++i]);
}
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;
if (VHD_GetApiInfo(&dll_ver, &nb_boards) != VHDERR_NOERROR) {
fprintf(stderr, "{\"error\":\"VHD_GetApiInfo failed\"}\n");
return 1;
}
if (device_id >= nb_boards) {
fprintf(stderr, "{\"error\":\"board %u not found (%lu detected)\"}\n", device_id, nb_boards);
return 1;
}
/* ── Serialize board open via flock ──────────────────────────────
* delta_x300 BufMngr.c:781 has an array-index-out-of-bounds bug that
* fires when two VHD_OpenBoardHandle calls race on the same board.
* Use a cross-container exclusive lock on a file in /dev/shm/deltacast/
* (already bind-mounted into every capture sidecar) to guarantee only
* one bridge runs OpenBoardHandle + signal-wait at a time. The lock is
* released after signal lock succeeds (plus a settle delay) or on
* failure — so the next bridge is never permanently blocked.
*
* IMPORTANT: the signal-wait deadline is set AFTER acquiring the lock so
* the full sig_timeout is available for signal detection regardless of
* how long this bridge waited in the queue. */
const char *lock_path = "/dev/shm/deltacast/bridge.lock";
int lock_fd = open(lock_path, O_CREAT | O_RDWR, 0666);
if (lock_fd >= 0) {
fprintf(stderr, "[board] waiting for board-open lock (port %u)...\n", port_id);
if (flock(lock_fd, LOCK_EX) != 0) {
fprintf(stderr, "[board] flock failed: %s — proceeding without lock\n", strerror(errno));
close(lock_fd);
lock_fd = -1;
} else {
fprintf(stderr, "[board] lock acquired (port %u)\n", port_id);
}
} else {
fprintf(stderr, "[board] could not open lock file %s: %s — proceeding without lock\n",
lock_path, strerror(errno));
}
/* ── Open board ───────────────────────────────────────────────── */
HANDLE board = NULL;
if (VHD_OpenBoardHandle(device_id, &board, NULL, 0) != VHDERR_NOERROR) {
fprintf(stderr, "{\"error\":\"VHD_OpenBoardHandle failed for board %u\"}\n", device_id);
if (lock_fd >= 0) { flock(lock_fd, LOCK_UN); close(lock_fd); }
return 1;
}
/* Disable passive (relay) loopback so RX is live.
* VHD_CORE_BP_PASSIVE_LOOPBACK_<n> only exists for ports 0-3 in SDK 6.34.1,
* and the board reports passive-loopback capability 0, so skipping ports 4-7
* is harmless. */
if (port_id < 4) {
VHD_SetBoardProperty(board, loopback_prop(port_id), FALSE);
}
/* ── Wait for signal lock ──────────────────────────────────────────
* Deadline is set HERE — after the flock is acquired and the board is
* open — so the full sig_timeout is available regardless of queue wait. */
ULONG video_std = (ULONG)NB_VHD_VIDEOSTANDARDS;
struct timespec deadline;
clock_gettime(CLOCK_MONOTONIC, &deadline);
deadline.tv_sec += sig_timeout;
while (!atomic_load(&g_stop)) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
if (now.tv_sec > deadline.tv_sec ||
(now.tv_sec == deadline.tv_sec && now.tv_nsec >= deadline.tv_nsec)) break;
VHD_GetChannelProperty(board, VHD_RX_CHANNEL, port_id,
VHD_SDI_CP_VIDEO_STANDARD, &video_std);
if (video_std != (ULONG)NB_VHD_VIDEOSTANDARDS) break;
struct timespec ts = {0, 200000000L}; /* 200ms */
nanosleep(&ts, NULL);
}
if (atomic_load(&g_stop) || video_std == (ULONG)NB_VHD_VIDEOSTANDARDS) {
fprintf(stderr,
"{\"error\":\"no signal on board %u port %u within %ds\"}\n",
device_id, port_id, sig_timeout);
VHD_CloseBoardHandle(board);
if (lock_fd >= 0) { flock(lock_fd, LOCK_UN); close(lock_fd); }
return 1;
}
/* Signal locked. Hold the board-open lock for a settle period so the
* board's RX buffer queues are fully initialised before the next bridge
* calls OpenBoardHandle. 4 seconds is enough for 1080p59.94 @ queue-depth 8. */
if (lock_fd >= 0) {
struct timespec settle = {4, 0};
nanosleep(&settle, NULL);
flock(lock_fd, LOCK_UN);
close(lock_fd);
lock_fd = -1;
fprintf(stderr, "[board] lock released (port %u) — streaming\n", port_id);
}
ULONG clock_div = VHD_CLOCKDIV_1;
VHD_GetChannelProperty(board, VHD_RX_CHANNEL, port_id,
VHD_SDI_CP_CLOCK_DIVISOR, &clock_div);
VideoInfo vi = video_info((VHD_VIDEOSTANDARD)video_std,
(VHD_CLOCKDIVISOR)clock_div);
/* ── Emit format JSON to stderr (one line, flushed) ─────────────── */
fprintf(stderr,
"{\"width\":%d,\"height\":%d,\"fps_num\":%d,\"fps_den\":%d,"
"\"interlaced\":%s,\"pix_fmt\":\"uyvy422\","
"\"audio_channels\":2,\"audio_rate\":48000,"
"\"device\":%u,\"port\":%u}\n",
vi.width, vi.height, vi.fps_num, vi.fps_den,
vi.interlaced ? "true" : "false",
device_id, port_id);
fflush(stderr);
/* ── Open video stream ───────────────────────────────────────────── */
HANDLE video_stream = NULL;
if (VHD_OpenStreamHandle(board, rx_streamtype(port_id),
VHD_SDI_STPROC_DISJOINED_VIDEO,
NULL, &video_stream, NULL) != VHDERR_NOERROR) {
fprintf(stderr, "{\"error\":\"VHD_OpenStreamHandle (video) failed\"}\n");
VHD_CloseBoardHandle(board);
return 1;
}
VHD_SetStreamProperty(video_stream, VHD_SDI_SP_VIDEO_STANDARD, video_std);
VHD_SetStreamProperty(video_stream, VHD_SDI_SP_CLOCK_SYSTEM, clock_div);
VHD_SetStreamProperty(video_stream, VHD_CORE_SP_TRANSFER_SCHEME, VHD_TRANSFER_SLAVED);
VHD_SetStreamProperty(video_stream, VHD_CORE_SP_BUFFERQUEUE_DEPTH, 8);
/* ── Launch audio thread (FIFO open blocks until FFmpeg connects) ── */
pthread_t audio_tid = 0;
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);
}
/* ── Start video stream ──────────────────────────────────────────── */
if (VHD_StartStream(video_stream) != VHDERR_NOERROR) {
atomic_store(&g_stop, 1);
if (audio_tid) pthread_join(audio_tid, NULL);
VHD_CloseStreamHandle(video_stream);
VHD_CloseBoardHandle(board);
return 1;
}
/* ── Video capture loop ──────────────────────────────────────────── */
HANDLE slot = NULL;
while (!atomic_load(&g_stop)) {
ULONG r = VHD_LockSlotHandle(video_stream, &slot);
if (r == VHDERR_NOERROR) {
BYTE *buf = NULL;
ULONG sz = 0;
if (VHD_GetSlotBuffer(slot, VHD_SDI_BT_VIDEO, &buf, &sz) == VHDERR_NOERROR) {
ULONG written = 0;
while (written < sz) {
ssize_t n = write(STDOUT_FILENO, buf + written, sz - written);
if (n <= 0) { atomic_store(&g_stop, 1); break; }
written += (ULONG)n;
}
}
VHD_UnlockSlotHandle(slot);
} else if (r != VHDERR_TIMEOUT) {
fprintf(stderr, "[video] VHD_LockSlotHandle error %lu — stopping\n", r);
break;
}
}
/* ── Cleanup ─────────────────────────────────────────────────── */
VHD_StopStream(video_stream);
VHD_CloseStreamHandle(video_stream);
if (audio_tid) pthread_join(audio_tid, NULL);
VHD_CloseBoardHandle(board);
return 0;
}