diff --git a/services/capture/deltacast-bridge/main.c b/services/capture/deltacast-bridge/main.c new file mode 100644 index 0000000..91635d0 --- /dev/null +++ b/services/capture/deltacast-bridge/main.c @@ -0,0 +1,300 @@ +/* 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 "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; + default: 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 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 ────────────────────────────────────────────────────── */ +typedef struct { + HANDLE board; + unsigned port; + ULONG video_std; + ULONG clock_div; + const char *fifo_path; +} AudioArgs; + +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); + 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); + + /* 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; } + + 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; + } + + 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) write(fd, buf, sz); + } + VHD_UnlockSlotHandle(slot); + } else if (r != VHDERR_TIMEOUT) { + break; + } + } + + close(fd); + 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); + + /* ── 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_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_SLOTS_QUEUE_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, 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) { + break; + } + } + + /* ── Cleanup ─────────────────────────────────────────────────────── */ + VHD_StopStream(video_stream); + VHD_CloseStreamHandle(video_stream); + if (audio_tid) pthread_join(audio_tid, NULL); + VHD_CloseBoardHandle(board); + return 0; +} \ No newline at end of file