- services/framecache/client/fc_pipe.c: new slot→stdout pipe adapter
- Opens framecache slot as consumer (independent cursor per instance)
- Streams raw UYVY422 frames to stdout continuously
- SIGPIPE detection via write() return — exits cleanly on ffmpeg exit
- SIGTERM/SIGINT clean stop from capture-manager
- Periodic stats to stderr (every 300 frames)
- Exit codes: 0=clean, 1=slot not found, 2=EPIPE
- services/framecache/CMakeLists.txt: add fc_pipe target + install
- services/framecache/Dockerfile: copy fc_pipe to runtime image
- services/capture/Dockerfile:
- New fc-pipe-builder stage (builds fc_pipe from framecache sources)
- Copies fc_pipe binary to /usr/local/bin/fc_pipe in runtime image
- services/capture/src/capture-manager.js:
- _buildInputArgs: new framecache path for deltacast + sdi/blackmagic
when FC_SLOT_ID env is set (injected by node-agent from bridge fmt JSON)
- Spawns fc_pipe <slot_id> as child process
- Uses pipe:0 as ffmpeg rawvideo input 0
- Audio FIFO (unchanged) as ffmpeg input 1
- Falls back to legacy FIFO path when FC_SLOT_ID unset
- audioMap: covers blackmagic via framecache (input 1 for audio FIFO)
- isInterlacedSource: covers blackmagic interlaced signals
- hiresStdio: pipe stdin when bridgeProcess set (fc_pipe stdout→ffmpeg)
- Non-growing spawn: pipes fc_pipe.stdout → ffmpeg.stdin
- Growing orchestrator spawn: pipes fc_pipe.stdout → bash.stdin
- sdiHlsDir: covers blackmagic source type
- Session state stores _fcPipeProcess for clean stop
- stop(): sends SIGTERM to fc_pipe after ffmpeg SIGINT
122 lines
3.9 KiB
C
122 lines
3.9 KiB
C
/**
|
|
* fc_pipe.c — Framecache slot → stdout pipe adapter.
|
|
*
|
|
* Opens a framecache slot as a consumer and writes raw video frames to
|
|
* stdout in a continuous stream. capture-manager.js spawns this process
|
|
* and feeds its stdout to ffmpeg as a rawvideo pipe input — identical to
|
|
* the way DeckLink bridges currently pipe raw frames.
|
|
*
|
|
* Each consumer instance has its own independent read cursor, so multiple
|
|
* fc_pipe processes reading from the same slot never interfere with each
|
|
* other. This is how growing + proxy + HLS all read the same SDI signal
|
|
* simultaneously.
|
|
*
|
|
* Usage:
|
|
* fc_pipe <slot_id> [wait_ms]
|
|
*
|
|
* Writes raw UYVY422 frame data to stdout. Terminates on:
|
|
* - SIGTERM / SIGINT (clean stop from capture-manager)
|
|
* - stdout EPIPE (ffmpeg exited)
|
|
* - Slot disappears (bridge stopped)
|
|
*
|
|
* Exit codes:
|
|
* 0 clean stop (SIGTERM)
|
|
* 1 slot not found within wait_ms
|
|
* 2 stdout write error (EPIPE)
|
|
*/
|
|
|
|
#include "../src/slot.h"
|
|
#include "fc_client.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <signal.h>
|
|
#include <stdint.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
#include <time.h>
|
|
#include <fcntl.h>
|
|
|
|
static volatile int g_stop = 0;
|
|
static void on_signal(int s) { (void)s; g_stop = 1; }
|
|
|
|
/* Write all bytes to fd. Returns 0 on success, -1 on EPIPE/error. */
|
|
static int write_all_fd(int fd, const void *buf, size_t len) {
|
|
const uint8_t *p = (const uint8_t *)buf;
|
|
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; /* EPIPE or other fatal error */
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int main(int argc, char *argv[]) {
|
|
if (argc < 2) {
|
|
fprintf(stderr, "Usage: %s <slot_id> [wait_ms]\n", argv[0]);
|
|
return 1;
|
|
}
|
|
const char *slot_id = argv[1];
|
|
uint64_t wait_ms = argc >= 3 ? (uint64_t)atoll(argv[2]) : 30000;
|
|
|
|
signal(SIGTERM, on_signal);
|
|
signal(SIGINT, on_signal);
|
|
signal(SIGPIPE, SIG_IGN); /* detect EPIPE via write() return value */
|
|
|
|
/* Set stdout to binary mode — no newline translation */
|
|
fcntl(STDOUT_FILENO, F_SETFL,
|
|
fcntl(STDOUT_FILENO, F_GETFL, 0) & ~O_NONBLOCK);
|
|
|
|
fprintf(stderr, "[fc_pipe] opening slot '%s' (wait %llums)\n",
|
|
slot_id, (unsigned long long)wait_ms);
|
|
|
|
fc_consumer_t *c = fc_consumer_open(slot_id, wait_ms);
|
|
if (!c) {
|
|
fprintf(stderr, "[fc_pipe] slot '%s' not found within %llums\n",
|
|
slot_id, (unsigned long long)wait_ms);
|
|
return 1;
|
|
}
|
|
|
|
fprintf(stderr, "[fc_pipe] slot open, streaming to stdout\n");
|
|
|
|
uint64_t frames_out = 0;
|
|
uint64_t total_dropped = 0;
|
|
|
|
while (!g_stop) {
|
|
fc_frame_ref_t ref;
|
|
int rc = fc_consumer_read(c, &ref, 2000 /* 2s timeout */);
|
|
|
|
if (rc == FC_TIMEOUT) continue;
|
|
if (rc == FC_ERROR) break;
|
|
|
|
if (rc == FC_DROPPED) {
|
|
total_dropped = fc_consumer_dropped(c);
|
|
fprintf(stderr, "[fc_pipe] WARNING: consumer fell behind — total dropped: %llu\n",
|
|
(unsigned long long)total_dropped);
|
|
}
|
|
|
|
/* Write frame data to stdout */
|
|
if (write_all_fd(STDOUT_FILENO, ref.data, ref.size) < 0) {
|
|
if (!g_stop)
|
|
fprintf(stderr, "[fc_pipe] stdout EPIPE — ffmpeg exited\n");
|
|
break;
|
|
}
|
|
frames_out++;
|
|
|
|
/* Periodic stats to stderr (every 300 frames ≈ 5s at 60fps) */
|
|
if (frames_out % 300 == 0) {
|
|
fprintf(stderr, "[fc_pipe] frames=%llu dropped=%llu\n",
|
|
(unsigned long long)frames_out,
|
|
(unsigned long long)total_dropped);
|
|
}
|
|
}
|
|
|
|
fc_consumer_close(c);
|
|
fprintf(stderr, "[fc_pipe] done frames=%llu dropped=%llu\n",
|
|
(unsigned long long)frames_out,
|
|
(unsigned long long)total_dropped);
|
|
return 0;
|
|
}
|