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>
395 lines
18 KiB
C
395 lines
18 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 <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;
|
|
}
|
|
|
|
/* ── 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);
|
|
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 ──────────────────────────────────────── */
|
|
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);
|
|
return 1;
|
|
}
|
|
|
|
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;
|
|
}
|