/** * 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 [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 #include #include #include #include #include #include #include #include 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 [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_LAPPED) { /* Copy was torn (writer lapped us mid-read). No valid frame to * write — log and read again. */ total_dropped = fc_consumer_dropped(c); fprintf(stderr, "[fc_pipe] WARNING: frame lapped mid-read — total dropped: %llu\n", (unsigned long long)total_dropped); continue; } if (rc == FC_DROPPED) { /* Skipped one or more older frames, but THIS frame is valid — log * and write it (do NOT continue). */ 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 (ref.data is a stable consumer-owned copy) */ 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; }