C-Bug 1 (Torn read): fc_client.c zero-copy pointer replaced with consumer-owned copy buffer + post-copy cursor revalidation to prevent reading torn frames when the writer laps a slow consumer. New FC_LAPPED return code. C-Bug 3 (Semaphore busy-spin): fc_client.c drains the semaphore (sem_trywait) so the count never accumulates, relying entirely on write_cursor diff for availability. Prevents 100% CPU loops + EOVERFLOW. C-Bug 4 (GET /slots stack overflow): framecache.c uses heap allocation with explicit bounds checking for JSON serialization instead of a 64KB stack buffer. C-Bug 6 (DeckLink race): decklink-bridge uses pthread_mutex_t around fc_writer calls and reopen_slot to prevent UAF/double-free from concurrent SDK callbacks. C-Bug 2-net (Resolution resync): net_ingest explicitly scales to target W:H so ffmpeg always outputs exactly frame_size bytes, ignoring source resolution changes. C-Bug 8 (strdup leak): net_ingest uses static caller-owned buffers for ffmpeg args instead of strdup across listener reconnects. C-Bug 9 (PROT_READ segfault): removed atomic write to hdr->dropped_frames from the consumer read loop (which maps shm read-only).
133 lines
4.4 KiB
C
133 lines
4.4 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_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;
|
|
}
|