dragonflight/services/capture/deltacast-bridge/main.c
Wild Dragon Dev 0d479d043d feat(framecache): phase 2 — deltacast-bridge writes to shm ring
- fc_writer.h/fc_writer.c: new framecache slot writer module
  - Registers slot via POST /slots to framecache HTTP API on signal lock
  - Opens shm file returned by API (O_RDWR + mmap MAP_SHARED)
  - fc_writer_write(): atomic write_cursor advance + sem_post per frame
  - fc_writer_close(): DELETE /slots/:id + munmap + sem_close
  - HTTP calls via raw POSIX sockets (no libcurl dependency)
  - Parses host:port from FC_URL env var or --fc-url arg

- main.c changes:
  - PortState gains slot_id, fc_url, fc_writer fields
  - --fc-url CLI arg + FC_URL env var (default http://localhost:7435)
  - On signal lock: fc_writer_open() before thread launch;
    falls back to FIFO if framecache unreachable (fc_writer == NULL)
  - video_thread: shm path primary (fc_writer != NULL),
    FIFO path fallback (fc_writer == NULL or LEGACY_FIFO=1)
  - Format JSON now includes slot_id field for node-agent consumption
  - Cleanup: fc_writer_close() before VHD_CloseBoardHandle

- CMakeLists.txt:
  - Add fc_writer.c to build
  - Link rt (shm_open, sem_open)
  - LEGACY_FIFO option (OFF by default) for nodes without framecache

Audio thread unchanged — audio stays in FIFO (shm audio is roadmap).
2026-06-03 15:13:20 +00:00

811 lines
37 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* services/capture/deltacast-bridge/main.c
*
* Deltacast VideoMaster SDI shared multi-port bridge daemon.
*
* Opens the board ONCE, opens RX streams for all requested ports, and
* writes each port's video frames into a shared-memory framecache slot
* (and audio to a named FIFO — audio-in-shm is a future roadmap item).
*
* Signal fan-out architecture:
* Board → video_thread → fc_writer → /dev/shm/framecache/<slot>
* └→ N consumers (recording, proxy,
* HLS preview) each read with
* their own cursor — zero-copy,
* no bandwidth splitting.
*
* Usage:
* deltacast-bridge --device <N> --ports <csv>
* [--video-pipe-dir /dev/shm/deltacast]
* [--audio-pipe-dir /dev/shm/deltacast]
* [--fc-url http://framecache:7435]
* [--signal-timeout <sec>]
*
* Compat alias: --port <N> treated as --ports <N> (single port).
*
* For each port that acquires signal, emits one JSON line to stderr:
* {"port":N,"width":W,"height":H,"fps_num":N,"fps_den":D,
* "pix_fmt":"uyvy422","audio_rate":48000,"audio_channels":2,
* "slot_id":"deltacast-<device>-<port>"}
*
* Compile with -DLEGACY_FIFO=1 to disable shm writes and fall back to
* the original named-FIFO path (for nodes without framecache running).
*
* Runs until SIGTERM/SIGINT, then closes all streams and the board.
*/
#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/stat.h>
#include <time.h>
#include <unistd.h>
#include "VideoMasterHD_Core.h"
#include "VideoMasterHD_Sdi.h"
#include "VideoMasterHD_Sdi_Audio.h"
#ifndef LEGACY_FIFO
# include "fc_writer.h"
#endif
#ifndef F_SETPIPE_SZ
#define F_SETPIPE_SZ 1031
#endif
/* Default framecache URL — overridden by FC_URL env var or --fc-url arg */
#define FC_URL_DEFAULT "http://localhost:7435"
/* ── Constants ────────────────────────────────────────────────────────── */
#define MAX_PORTS 8
/* ── Globals ──────────────────────────────────────────────────────────── */
static atomic_int g_stop = 0; /* global shutdown (SIGTERM/SIGINT only) */
static void on_signal(int s) { (void)s; atomic_store(&g_stop, 1); }
/* Per-port stop flag — set only when a fatal error occurs on that specific
* port (e.g. video lock lost). Audio EPIPE is handled by reopening the FIFO
* rather than stopping the port, so the thread survives ffmpeg restarts. */
static atomic_int g_port_stop[MAX_PORTS];
/* ── Stream type by port index (non-contiguous SDK enum) ────────────── */
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;
}
}
/* ── 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 -1; /* ports 4-7 have no passive loopback property; call site guards p < 4 */
}
}
/* ── 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};
}
}
/* ── Write-all helper ─────────────────────────────────────────────────── */
/* Writes all bytes to fd. Uses non-blocking I/O so the bridge never stalls
* waiting for a slow reader. Returns 0 on success, -1 on fatal error (EPIPE
* = reader closed the FIFO). EAGAIN / EWOULDBLOCK on a full pipe is NOT fatal
* — the caller (video_thread) will retry on the next slot lock. */
static int write_all(int fd, const unsigned char *p, size_t len) {
/* Make the fd non-blocking for the duration of this write */
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) return -1;
if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) return -1;
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;
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
/* Pipe full — brief yield then retry */
struct timespec ts = {0, 1000000L}; /* 1ms */
nanosleep(&ts, NULL);
continue;
}
/* EPIPE or other fatal error — restore flags and return */
fcntl(fd, F_SETFL, flags);
return -1;
}
/* Restore blocking mode */
fcntl(fd, F_SETFL, flags);
return 0;
}
/* ── Per-port state ───────────────────────────────────────────────────── */
typedef struct {
HANDLE board;
unsigned port;
unsigned device;
ULONG video_std;
ULONG clock_div;
VideoInfo vi;
char video_fifo[256];
char audio_fifo[256];
char slot_id[128]; /* framecache slot id: "deltacast-<dev>-<port>" */
char fc_url[256]; /* framecache HTTP base URL */
/* threads */
pthread_t video_tid;
pthread_t audio_tid;
/* streams (owned by threads, set before thread launch) */
HANDLE video_stream;
#ifndef LEGACY_FIFO
fc_writer_t *fc_writer; /* shm ring buffer writer (NULL = use FIFO fallback) */
#endif
} PortState;
/* ── Audio thread ──────────────────────────────────────────────────────
*
* - Opens FIFO writer (blocks until a reader connects — correct behaviour).
* - Feeds continuous wall-clock-paced s16le stereo (real or silence).
* - Best-effort VHD audio stream; silence fallback on any failure.
* - On EPIPE (ffmpeg reader died): closes and REOPENS the FIFO so the
* thread survives an ffmpeg restart without bringing down other ports.
* EPIPE never sets g_stop — only SIGTERM/SIGINT does that.
*/
static void *audio_thread(void *arg) {
PortState *ps = (PortState *)arg;
const int AUDIO_RATE = 48000;
const int CHANNELS = 2;
const size_t FRAME_BYTES = (size_t)CHANNELS * 2; /* s16le stereo */
int fps_num = ps->vi.fps_num > 0 ? ps->vi.fps_num : 25;
int fps_den = ps->vi.fps_den > 0 ? ps->vi.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)ps->video_std,
(VHD_CLOCKDIVISOR)ps->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);
if (!buf) return NULL;
/* Open the VHD audio stream once for the lifetime of the bridge.
* The stream stays open across reader reconnects — no need to reopen it. */
HANDLE stream = NULL;
int have_vhd_audio = 0;
VHD_AUDIOINFO ai;
memset(&ai, 0, sizeof(ai));
ULONG r = VHD_OpenStreamHandle(ps->board, rx_streamtype(ps->port),
VHD_SDI_STPROC_DISJOINED_ANC,
NULL, &stream, NULL);
if (r == VHDERR_NOERROR) {
/* Per Deltacast SDK Sample_RXAudio.cpp: VHD_SDI_SP_INTERFACE must be
* propagated to the audio stream, otherwise VHD_SlotExtractAudio
* returns 0 samples (silent capture). */
ULONG iface = 0;
VHD_GetStreamProperty(stream, VHD_SDI_SP_INTERFACE, &iface);
VHD_SetStreamProperty(stream, VHD_SDI_SP_VIDEO_STANDARD, ps->video_std);
VHD_SetStreamProperty(stream, VHD_SDI_SP_CLOCK_SYSTEM, ps->clock_div);
VHD_SetStreamProperty(stream, VHD_CORE_SP_TRANSFER_SCHEME, VHD_TRANSFER_SLAVED);
VHD_SetStreamProperty(stream, VHD_SDI_SP_INTERFACE, iface);
/* Configure BOTH channels of the stereo pair (group 0). The actual PCM
* samples land in pAudioChannels[0].pData (packed L/R s16le). Channel
* [1] must declare Mode+BufferFormat so the SDK recognizes the pair. */
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;
ai.pAudioGroups[0].pAudioChannels[1].Mode = VHD_AM_STEREO;
ai.pAudioGroups[0].pAudioChannels[1].BufferFormat = VHD_AF_16;
if (VHD_StartStream(stream) == VHDERR_NOERROR) {
have_vhd_audio = 1;
} else {
fprintf(stderr, "[audio:%u] VHD_StartStream failed — feeding silence\n", ps->port);
VHD_CloseStreamHandle(stream);
stream = NULL;
}
} else {
fprintf(stderr, "[audio:%u] VHD_OpenStreamHandle failed (%lu) — feeding silence\n",
ps->port, r);
}
long frame_ns = (long)(1000000000.0 * (double)fps_den / (double)fps_num);
HANDLE slot = NULL;
/* Outer loop: reopen the FIFO writer each time a reader connects.
* This allows the bridge to survive ffmpeg session stop/restart on a port
* without affecting any other port's threads. */
while (!atomic_load(&g_stop) && !atomic_load(&g_port_stop[ps->port])) {
int fd = open(ps->audio_fifo, O_WRONLY);
if (fd < 0) {
/* Open failed (rare — FIFO was deleted?). Brief pause then retry. */
fprintf(stderr, "[audio:%u] open FIFO failed: %s\n", ps->port, strerror(errno));
struct timespec ts = {0, 200000000L};
nanosleep(&ts, NULL);
continue;
}
fcntl(fd, F_SETPIPE_SZ, 1024 * 1024);
/* Reset wall-clock baseline after potentially blocking on open().
* Only used for the SILENCE fallback path (no hardware audio). */
struct timespec next;
clock_gettime(CLOCK_MONOTONIC, &next);
/* Inner loop: feed audio into the open FIFO until reader exits (EPIPE). */
while (!atomic_load(&g_stop) && !atomic_load(&g_port_stop[ps->port])) {
size_t out_bytes = 0;
if (have_vhd_audio) {
/* HARDWARE-PACED PATH (the normal case).
* VHD_LockSlotHandle blocks until the board has the next audio
* slot ready — this slot is generated from the SAME SDI signal
* as the video, so blocking here paces audio in lockstep with
* video at the TRUE hardware rate. We write ONLY the real
* samples the board gives us (no silence padding, no wall-clock
* sleep) so the audio timeline length exactly tracks video.
* This is the fix for progressive A/V drift: mixing wall-clock
* paced silence with variable-length real reads made the audio
* stream length diverge from the video stream length. */
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);
if (out_bytes > 0) {
if (write_all(fd, buf, out_bytes) < 0) {
fprintf(stderr, "[audio:%u] EPIPE — waiting for next reader\n", ps->port);
break;
}
}
/* No wall-clock sleep — the board's slot cadence is the clock. */
continue;
} else if (r == VHDERR_TIMEOUT) {
/* No slot yet — loop and try again (do NOT inject silence,
* that would add extra samples and cause drift). */
continue;
} else {
fprintf(stderr, "[audio:%u] lock error %lu — degrading to silence\n",
ps->port, r);
VHD_StopStream(stream);
VHD_CloseStreamHandle(stream);
stream = NULL;
have_vhd_audio = 0;
clock_gettime(CLOCK_MONOTONIC, &next); /* rebase silence clock */
}
}
/* SILENCE FALLBACK PATH (no hardware audio available).
* Wall-clock paced one-frame-of-silence per video-frame interval so
* ffmpeg's input 1 never starves and audio length still tracks
* real time. */
memset(buf, 0, tick_bytes);
out_bytes = tick_bytes;
if (write_all(fd, buf, out_bytes) < 0) {
fprintf(stderr, "[audio:%u] EPIPE — waiting for next reader\n", ps->port);
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;
}
}
close(fd);
}
if (stream) {
VHD_StopStream(stream);
VHD_CloseStreamHandle(stream);
}
free(buf);
return NULL;
}
/* ── Video thread ─────────────────────────────────────────────────────── */
static void *video_thread(void *arg) {
PortState *ps = (PortState *)arg;
#ifndef LEGACY_FIFO
/* ── Framecache shm path (primary) ──────────────────────────────────
* Write frames directly into the shared memory ring buffer.
* Multiple consumers (growing recorder, proxy encoder, HLS preview)
* each hold their own read cursor and read independently — no FIFO
* splitting, no bandwidth halving.
*
* The fc_writer was opened by main() after signal lock. If it is
* NULL the framecache service was unavailable and we fall through to
* the legacy FIFO path automatically.
*/
if (ps->fc_writer) {
uint64_t frame_seq = 0;
while (!atomic_load(&g_stop) && !atomic_load(&g_port_stop[ps->port])) {
HANDLE slot = NULL;
ULONG r = VHD_LockSlotHandle(ps->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 expected = (ULONG)ps->vi.width * (ULONG)ps->vi.height * 2;
if (sz != expected) {
fprintf(stderr,
"[video:%u] WARN: sz=%lu != expected %lu — packing mismatch, skipping\n",
ps->port, (unsigned long)sz, (unsigned long)expected);
VHD_UnlockSlotHandle(slot);
continue;
}
/* pts: frame index × frame duration in µs */
uint64_t pts_us = 0;
if (ps->vi.fps_num > 0) {
pts_us = frame_seq * 1000000ULL
* (uint64_t)ps->vi.fps_den
/ (uint64_t)ps->vi.fps_num;
}
fc_writer_write(ps->fc_writer, buf, (uint32_t)sz, pts_us);
frame_seq++;
}
VHD_UnlockSlotHandle(slot);
} else if (r != VHDERR_TIMEOUT) {
fprintf(stderr, "[video:%u] VHD_LockSlotHandle error %lu — stopping port\n",
ps->port, (unsigned long)r);
atomic_store(&g_port_stop[ps->port], 1);
break;
}
}
return NULL;
}
/* fc_writer == NULL → fall through to FIFO path */
fprintf(stderr, "[video:%u] fc_writer unavailable — falling back to FIFO\n", ps->port);
#endif /* !LEGACY_FIFO */
/* ── Legacy FIFO path ────────────────────────────────────────────────
* Kept as compile-time fallback (-DLEGACY_FIFO=1) or when the
* framecache service is not reachable at startup.
*
* Outer loop: reopen the FIFO writer each time a reader connects.
* EPIPE means the ffmpeg sidecar for this port died (session
* stop/restart), NOT a hardware fault. Reopen and block until the
* next recorder start; other ports are unaffected.
*/
while (!atomic_load(&g_stop) && !atomic_load(&g_port_stop[ps->port])) {
int fd = open(ps->video_fifo, O_WRONLY);
if (fd < 0) {
fprintf(stderr, "[video:%u] open FIFO failed: %s\n", ps->port, strerror(errno));
struct timespec ts = {0, 200000000L};
nanosleep(&ts, NULL);
continue;
}
{
int pipe_sz = 64 * 1024 * 1024; /* 64 MB — ~16 frames of 1080p UYVY */
if (fcntl(fd, F_SETPIPE_SZ, pipe_sz) < 0) {
fprintf(stderr, "[video:%u] fcntl F_SETPIPE_SZ failed: %s\n",
ps->port, strerror(errno));
}
}
HANDLE slot = NULL;
int fatal = 0;
while (!atomic_load(&g_stop) && !atomic_load(&g_port_stop[ps->port])) {
ULONG r = VHD_LockSlotHandle(ps->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 expected = (ULONG)ps->vi.width * (ULONG)ps->vi.height * 2;
if (sz != expected) {
fprintf(stderr,
"[video:%u] WARN: slot sz=%lu != expected %lu (w=%d h=%d) -- packing mismatch; skipping frame\n",
ps->port, (unsigned long)sz, (unsigned long)expected,
ps->vi.width, ps->vi.height);
VHD_UnlockSlotHandle(slot);
continue;
}
if (write_all(fd, buf, sz) < 0) {
fprintf(stderr, "[video:%u] EPIPE — waiting for next reader\n", ps->port);
VHD_UnlockSlotHandle(slot);
break;
}
}
VHD_UnlockSlotHandle(slot);
} else if (r != VHDERR_TIMEOUT) {
fprintf(stderr, "[video:%u] VHD_LockSlotHandle error %lu — stopping port\n",
ps->port, (unsigned long)r);
atomic_store(&g_port_stop[ps->port], 1);
fatal = 1;
break;
}
}
close(fd);
if (fatal) break;
}
return NULL;
}
/* ── Parse comma-separated port list ─────────────────────────────────── */
static int parse_ports(const char *csv, unsigned *ports, int max) {
int count = 0;
char buf[256];
strncpy(buf, csv, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
char *tok = strtok(buf, ",");
while (tok && count < max) {
ports[count++] = (unsigned)atoi(tok);
tok = strtok(NULL, ",");
}
return count;
}
/* ── Main ─────────────────────────────────────────────────────────────── */
int main(int argc, char *argv[]) {
unsigned device_id = 0;
unsigned ports[MAX_PORTS] = {0};
int port_count = 0;
int sig_timeout = 30;
const char *video_pipe_dir = "/dev/shm/deltacast";
const char *audio_pipe_dir = "/dev/shm/deltacast";
/* Framecache URL: CLI arg > FC_URL env var > default */
const char *fc_url_env = getenv("FC_URL");
const char *fc_url = fc_url_env ? fc_url_env : FC_URL_DEFAULT;
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], "--ports") && i+1 < argc) {
port_count = parse_ports(argv[++i], ports, MAX_PORTS);
} else if (!strcmp(argv[i], "--port") && i+1 < argc) {
/* single-port compat alias */
ports[0] = (unsigned)atoi(argv[++i]);
port_count = 1;
} else if (!strcmp(argv[i], "--video-pipe-dir") && i+1 < argc) {
video_pipe_dir = argv[++i];
} else if (!strcmp(argv[i], "--audio-pipe-dir") && i+1 < argc) {
audio_pipe_dir = argv[++i];
} else if (!strcmp(argv[i], "--signal-timeout") && i+1 < argc) {
sig_timeout = atoi(argv[++i]);
} else if (!strcmp(argv[i], "--fc-url") && i+1 < argc) {
fc_url = argv[++i];
}
}
if (port_count == 0) {
fprintf(stderr, "{\"error\":\"no ports specified — use --ports 0,1,2,...\"}\n");
return 1;
}
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
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;
}
/* ── Configure bi-directional channel mode before opening board ─────
*
* The DELTA-12G-e-h 8c is a bidirectional card. Unless we explicitly
* call VHD_SetBiDirCfg(BrdId, VHD_BIDIR_80) the board may default to
* a mixed RX/TX configuration (e.g. 4RX/4TX), which causes random RX
* stream opens to fail with VHDERR_RESOURCEUNAVAILABLE and produces the
* "connecting…" hang operators see when starting certain recorders.
*
* Per SDK sample Tools.cpp SetNbChannels(): open a temporary handle,
* check IS_BIDIR and channel counts, call VHD_SetBiDirCfg if needed,
* then close. The subsequent real board open will see all 8 as RX.
*/
{
HANDLE tmp = NULL;
if (VHD_OpenBoardHandle(device_id, &tmp, NULL, 0) == VHDERR_NOERROR) {
ULONG nb_rx = 0, nb_tx = 0, is_bidir = 0;
VHD_GetBoardProperty(tmp, VHD_CORE_BP_NB_RXCHANNELS, &nb_rx);
VHD_GetBoardProperty(tmp, VHD_CORE_BP_NB_TXCHANNELS, &nb_tx);
VHD_GetBoardProperty(tmp, VHD_CORE_BP_IS_BIDIR, &is_bidir);
VHD_CloseBoardHandle(tmp);
if (is_bidir) {
/* Set all channels to RX. For 8-channel bidir: VHD_BIDIR_80.
* VHD_SetBiDirCfg takes the board INDEX, not a handle. */
ULONG cfg = (nb_rx + nb_tx == 8) ? VHD_BIDIR_80 : VHD_BIDIR_40;
ULONG r = VHD_SetBiDirCfg(device_id, cfg);
if (r == VHDERR_NOERROR)
fprintf(stderr, "[board] SetBiDirCfg(%lu) OK — %lu+%lu ch bidir configured all-RX\n",
cfg, nb_rx, nb_tx);
else
fprintf(stderr, "[board] SetBiDirCfg warn rc=%lu (non-fatal)\n", r);
}
}
}
/* ── Open board ONCE ─────────────────────────────────────────────── */
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;
}
fprintf(stderr, "[board] opened board %u with %d port(s)\n", device_id, port_count);
/* Per SDK samples: for 12G-ASI or 3G-ASI channel types the channel must be
* explicitly switched to SDI mode. Without this, VHD_SDI_CP_VIDEO_STANDARD
* polls return NB_VHD_VIDEOSTANDARDS (no signal) even when signal present.
* Also disable passive loopback for ports 0-3 so RX doesn't loop to TX. */
for (int pi = 0; pi < port_count; pi++) {
unsigned p = ports[pi];
ULONG chn_type = 0;
if (VHD_GetChannelProperty(board, VHD_RX_CHANNEL, p, VHD_CORE_CP_TYPE, &chn_type) == VHDERR_NOERROR) {
if (chn_type == VHD_CHNTYPE_3GSDI_ASI || chn_type == VHD_CHNTYPE_12GSDI_ASI)
VHD_SetChannelProperty(board, VHD_RX_CHANNEL, p, VHD_CORE_CP_MODE, VHD_CHANNEL_MODE_SDI);
}
if (p < 4) VHD_SetBoardProperty(board, loopback_prop(p), FALSE);
}
/* ── Wait for signal on all ports ───────────────────────────────── */
ULONG video_stds[MAX_PORTS] = {0};
ULONG clock_divs[MAX_PORTS] = {0};
int locked[MAX_PORTS] = {0};
for (int pi = 0; pi < port_count; pi++) {
video_stds[pi] = (ULONG)NB_VHD_VIDEOSTANDARDS;
clock_divs[pi] = VHD_CLOCKDIV_1;
}
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;
int all_locked = 1;
for (int pi = 0; pi < port_count; pi++) {
if (locked[pi]) continue;
VHD_GetChannelProperty(board, VHD_RX_CHANNEL, ports[pi],
VHD_SDI_CP_VIDEO_STANDARD, &video_stds[pi]);
if (video_stds[pi] != (ULONG)NB_VHD_VIDEOSTANDARDS) {
VHD_GetChannelProperty(board, VHD_RX_CHANNEL, ports[pi],
VHD_SDI_CP_CLOCK_DIVISOR, &clock_divs[pi]);
locked[pi] = 1;
fprintf(stderr, "[board] port %u signal locked (std=%lu)\n",
ports[pi], video_stds[pi]);
} else {
all_locked = 0;
}
}
if (all_locked) break;
struct timespec ts = {0, 200000000L}; /* 200ms poll */
nanosleep(&ts, NULL);
}
/* Report results — continue with whatever locked, abort only if NONE locked. */
int any_locked = 0;
for (int pi = 0; pi < port_count; pi++) {
if (locked[pi]) { any_locked = 1; }
else {
fprintf(stderr,
"{\"error\":\"no signal on board %u port %u within %ds\"}\n",
device_id, ports[pi], sig_timeout);
}
}
if (!any_locked || atomic_load(&g_stop)) {
VHD_CloseBoardHandle(board);
return 1;
}
/* ── Create FIFOs and open streams for each locked port ─────────── */
PortState ps[MAX_PORTS];
memset(ps, 0, sizeof(ps));
int active_count = 0;
/* Initialise per-port stop flags. */
for (int pi = 0; pi < MAX_PORTS; pi++) atomic_store(&g_port_stop[pi], 0);
for (int pi = 0; pi < port_count; pi++) {
if (!locked[pi]) continue;
PortState *p = &ps[active_count];
p->board = board;
p->port = ports[pi];
p->device = device_id;
p->video_std = video_stds[pi];
p->clock_div = clock_divs[pi];
p->vi = video_info((VHD_VIDEOSTANDARD)video_stds[pi],
(VHD_CLOCKDIVISOR)clock_divs[pi]);
snprintf(p->video_fifo, sizeof(p->video_fifo),
"%s/video-%u.fifo", video_pipe_dir, ports[pi]);
snprintf(p->audio_fifo, sizeof(p->audio_fifo),
"%s/audio-%u.fifo", audio_pipe_dir, ports[pi]);
snprintf(p->slot_id, sizeof(p->slot_id),
"deltacast-%u-%u", device_id, ports[pi]);
strncpy(p->fc_url, fc_url, sizeof(p->fc_url) - 1);
/* Create audio FIFO (always needed — audio stays in FIFO for now). */
if (mkfifo(p->audio_fifo, 0666) != 0 && errno != EEXIST) {
fprintf(stderr, "[port:%u] mkfifo audio failed: %s\n", ports[pi], strerror(errno));
continue;
}
#ifndef LEGACY_FIFO
/* Open framecache slot for video frames.
* Fall back to FIFO if framecache is unreachable. */
p->fc_writer = fc_writer_open(p->fc_url, p->slot_id,
(uint32_t)p->vi.width, (uint32_t)p->vi.height,
(uint32_t)p->vi.fps_num, (uint32_t)p->vi.fps_den);
if (!p->fc_writer) {
fprintf(stderr, "[port:%u] framecache unavailable — creating video FIFO fallback\n",
ports[pi]);
if (mkfifo(p->video_fifo, 0666) != 0 && errno != EEXIST) {
fprintf(stderr, "[port:%u] mkfifo video failed: %s\n", ports[pi], strerror(errno));
continue;
}
}
#else
/* Legacy: always use video FIFO */
if (mkfifo(p->video_fifo, 0666) != 0 && errno != EEXIST) {
fprintf(stderr, "[port:%u] mkfifo video failed: %s\n", ports[pi], strerror(errno));
continue;
}
#endif
/* Open video stream. */
HANDLE vs = NULL;
ULONG r = VHD_OpenStreamHandle(board, rx_streamtype(ports[pi]),
VHD_SDI_STPROC_DISJOINED_VIDEO,
NULL, &vs, NULL);
if (r != VHDERR_NOERROR) {
fprintf(stderr, "{\"error\":\"VHD_OpenStreamHandle video failed port %u rc=%lu\"}\n",
ports[pi], r);
continue;
}
VHD_SetStreamProperty(vs, VHD_SDI_SP_VIDEO_STANDARD, p->video_std);
VHD_SetStreamProperty(vs, VHD_SDI_SP_CLOCK_SYSTEM, p->clock_div);
VHD_SetStreamProperty(vs, VHD_CORE_SP_TRANSFER_SCHEME, VHD_TRANSFER_SLAVED);
VHD_SetStreamProperty(vs, VHD_CORE_SP_BUFFERQUEUE_DEPTH, 8);
ULONG iface = 0;
if (VHD_GetStreamProperty(vs, VHD_SDI_SP_INTERFACE, &iface) == VHDERR_NOERROR) {
VHD_SetStreamProperty(vs, VHD_SDI_SP_INTERFACE, iface);
fprintf(stderr, "[board] port %u explicitly set SDI Interface to %lu\n", ports[pi], iface);
}
/* Pin to tightly-packed 8-bit UYVY. Relying on SDK default caused
* the board to deliver frames whose size != width*height*2,
* producing rolled/sheared ("bouncing and bending") video. */
VHD_SetStreamProperty(vs, VHD_CORE_SP_BUFFER_PACKING, VHD_BUFPACK_VIDEO_YUV422_8);
p->video_stream = vs;
if (VHD_StartStream(vs) != VHDERR_NOERROR) {
fprintf(stderr, "{\"error\":\"VHD_StartStream video failed port %u\"}\n", ports[pi]);
VHD_CloseStreamHandle(vs);
p->video_stream = NULL;
continue;
}
/* Emit format JSON to stderr (one line per port on signal lock).
* Includes slot_id so node-agent / capture-manager can identify
* the framecache slot for this port. */
fprintf(stderr,
"{\"port\":%u,\"width\":%d,\"height\":%d,"
"\"fps_num\":%d,\"fps_den\":%d,"
"\"interlaced\":%s,"
"\"pix_fmt\":\"uyvy422\","
"\"audio_channels\":2,\"audio_rate\":48000,"
"\"device\":%u,"
"\"slot_id\":\"%s\"}\n",
ports[pi],
p->vi.width, p->vi.height,
p->vi.fps_num, p->vi.fps_den,
p->vi.interlaced ? "true" : "false",
device_id,
p->slot_id);
fflush(stderr);
/* Launch audio thread (blocks until reader connects to audio FIFO). */
pthread_create(&p->audio_tid, NULL, audio_thread, p);
/* Launch video thread (blocks until reader connects to video FIFO). */
pthread_create(&p->video_tid, NULL, video_thread, p);
active_count++;
}
if (active_count == 0) {
fprintf(stderr, "{\"error\":\"no ports successfully started\"}\n");
VHD_CloseBoardHandle(board);
return 1;
}
/* ── Wait for all threads to finish ─────────────────────────────── */
for (int i = 0; i < active_count; i++) {
if (ps[i].video_tid) pthread_join(ps[i].video_tid, NULL);
if (ps[i].audio_tid) pthread_join(ps[i].audio_tid, NULL);
}
/* ── Cleanup ─────────────────────────────────────────────────────── */
for (int i = 0; i < active_count; i++) {
if (ps[i].video_stream) {
VHD_StopStream(ps[i].video_stream);
VHD_CloseStreamHandle(ps[i].video_stream);
}
#ifndef LEGACY_FIFO
if (ps[i].fc_writer) {
fc_writer_close(ps[i].fc_writer);
ps[i].fc_writer = NULL;
}
#endif
}
VHD_CloseBoardHandle(board);
return 0;
}