/* 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 --port --audio-pipe * [--signal-timeout ] */ #include #include #include #include #include #include #include #include #include #include #include #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. */ 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_ 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); 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; }