dragonflight/services/framecache/client/fc_pipe.c
Zac Gaetano 97267aa857 fix(framecache): single-input streaming AVI muxer in fc_pipe (kills 2-pipe ffmpeg deadlock)
fc_pipe now muxes video + that frame's embedded audio into ONE streaming AVI
container on stdout, so ffmpeg reads a SINGLE input (-f avi -i pipe:0) instead
of a raw video pipe + a separate live audio FIFO. The two-live-pipe design
deadlocked ffmpeg (it stalled forever probing input 0); a single interleaved
stream removes that failure mode entirely.

fc_pipe.c:
  - New AVI mode (argv[3] == "--avi"/"avi"). Writes RIFF('AVI ') + LIST(hdrl)
    { avih + strl(vids: strh+BITMAPINFOHEADER UYVY 16bpp) + strl(auds:
    strh+WAVEFORMATEX PCM s16le 48k 2ch) } + LIST(movi) once, then per ring
    entry a '00dc' video chunk followed by a '01wb' audio chunk (LE sizes,
    even-pad). RIFF/movi sizes use the 0x7FFFFFFF streaming sentinel (pipe is
    unseekable); dwFlags has NO index bits. Frame-coupled by construction: both
    chunks come from the SAME ring entry in one read-loop iteration.
  - dwScale/dwRate = fps_den/fps_num (video) and nBlockAlign/nAvgBytesPerSec
    (audio). If a frame has audio_size 0, emits one frame-interval of silence
    (round(48000*fps_den/fps_num) samples) so the audio timeline tracks video
    and ffmpeg never starves on the audio demuxer.
  - Legacy raw video-only mode retained when no avi flag is given. The old
    split-stdout/audio-FIFO threaded path is removed (it was the deadlock).

fc_client.{h,c}:
  - Add fc_consumer_info() / fc_stream_info_t to expose the slot header's
    width/height/fps/audio params to fc_pipe for the AVI header.

capture-manager.js (_buildInputArgs deltacast/sdi framecache branch):
  - Spawn fc_pipe with "--avi" (no audio FIFO). Remove the mkfifo + audio-FIFO
    creation for this path.
  - inputArgs: ONE input  -thread_queue_size 512 -f avi [AUDIO_OFFSET_MS] -i pipe:0
    (was: -f rawvideo -i pipe:0  AND  -f s16le -ar 48000 -ac 2 -i <fifo>).
  - audioInputIndex 0, audioFifo null. Growing VC-3/HEVC builders already map
    [0:v] and audioMap 0🅰️0?; with one AVI input that resolves to 0:v / 0:a.

Validated on zampp3 against the LIVE deltacast-0-0 slot: fc_pipe --avi | ffmpeg
-f avi -i pipe:0 -> dnxhd/pcm_s24le MXF gives 360 video / 360 audio packets in
6.006s (no stall at 2 frames). A synthetic 1 kHz sine slot through the same
path yields mean_volume -9 dB / max -6 dB, proving the muxer carries real audio
end-to-end (the live SDI input currently carries no embedded audio, so the
bridge's silence fallback reads -91 dB — upstream of the muxer).
2026-06-05 14:06:35 +00:00

405 lines
17 KiB
C

/**
* fc_pipe.c — Framecache slot → stdout pipe adapter.
*
* FRAME-COUPLED AUDIO (FC_VERSION 2):
* Each framecache ring entry carries the VIDEO frame AND that frame's
* SDI-embedded AUDIO together (written by the JOINED bridge from one slot).
* fc_pipe reads ONE entry per loop iteration.
*
* TWO OUTPUT MODES:
*
* 1) AVI MODE (default when audio is wanted; selected with --avi or by giving
* an arg of "avi"): fc_pipe writes a SINGLE streaming AVI container to
* stdout — video and audio INTERLEAVED in one byte stream. ffmpeg reads it
* as ONE input:
* ffmpeg -f avi -i pipe:0 -map 0:v ... -map 0:a ...
* This eliminates the two-live-pipe deadlock: when ffmpeg was given a raw
* video pipe AND a separate audio FIFO it stalled forever probing input 0.
* The AVI muxer writes its header once, then for each ring entry emits a
* '00dc' video chunk followed by a '01wb' audio chunk — frame-coupled by
* construction (both come from the same ring entry in the same iteration).
*
* 2) RAW MODE (legacy, video-only): if no audio FIFO / avi flag is given,
* fc_pipe writes raw UYVY422 video bytes to stdout as before.
*
* The old split video-stdout / audio-FIFO design is REMOVED — it was the
* source of the ffmpeg deadlock.
*
* Usage: fc_pipe <slot_id> [wait_ms] [mode]
* mode: "--avi" | "avi" → single streaming AVI (video+audio) on stdout.
* omitted | "-" → raw UYVY422 video-only on stdout.
*
* Terminates on: SIGTERM/SIGINT, stdout EPIPE (ffmpeg exited), slot gone.
*/
#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>
#include <math.h>
static volatile int g_stop = 0;
static void on_signal(int s) { (void)s; g_stop = 1; }
/* Write all bytes to fd (blocking). 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;
}
return 0;
}
/* ── Little-endian byte emitters into a caller buffer ────────────────────────── */
static inline void put_u16(uint8_t **pp, uint16_t v) {
uint8_t *p = *pp; p[0] = (uint8_t)(v & 0xff); p[1] = (uint8_t)((v >> 8) & 0xff); *pp = p + 2;
}
static inline void put_u32(uint8_t **pp, uint32_t v) {
uint8_t *p = *pp;
p[0] = (uint8_t)(v & 0xff); p[1] = (uint8_t)((v >> 8) & 0xff);
p[2] = (uint8_t)((v >> 16) & 0xff); p[3] = (uint8_t)((v >> 24) & 0xff);
*pp = p + 4;
}
static inline void put_fourcc(uint8_t **pp, const char *cc) {
uint8_t *p = *pp; p[0] = (uint8_t)cc[0]; p[1] = (uint8_t)cc[1];
p[2] = (uint8_t)cc[2]; p[3] = (uint8_t)cc[3]; *pp = p + 4;
}
/* ── Streaming AVI header ─────────────────────────────────────────────────────
* Builds RIFF('AVI ') + LIST('hdrl'){ avih + strl(vids) + strl(auds) } +
* LIST('movi'). For a streaming AVI over a pipe we cannot seek back to patch
* the RIFF and movi sizes, so we set them to 0x7FFFFFFF; ffmpeg's AVI demuxer
* reads the strf headers and the 00dc/01wb chunk stream regardless. The hdrl
* LIST size IS fixed/known, so it is written correctly. dwFlags is 0 — we do
* NOT set AVIF_HASINDEX / AVIF_MUSTUSEINDEX (there is no index in a stream).
*
* Writes the header to *out and returns its length. Buffer must be >= 512. */
static size_t build_avi_header(uint8_t *out,
uint32_t width, uint32_t height,
uint32_t fps_num, uint32_t fps_den,
uint32_t video_bytes,
uint32_t audio_rate, uint32_t audio_channels,
uint32_t audio_sample_bytes) {
const uint32_t STREAMING = 0x7FFFFFFFu;
const uint16_t bits_per_sample = (uint16_t)(audio_sample_bytes * 8u);
const uint16_t block_align = (uint16_t)(audio_channels * audio_sample_bytes);
const uint32_t avg_bytes_sec = audio_rate * block_align;
/* dwMicroSecPerFrame = 1e6 * fps_den / fps_num */
const uint32_t usec_per_frame =
(uint32_t)((1000000.0 * (double)fps_den / (double)fps_num) + 0.5);
/* Fixed sub-sizes (data bytes only, excluding the 8-byte ckID+ckSize). */
const uint32_t AVIH_DATA = 56; /* MainAVIHeader */
const uint32_t STRH_DATA = 56; /* AVISTREAMHEADER */
const uint32_t BIH_DATA = 40; /* BITMAPINFOHEADER */
const uint32_t WFX_DATA = 18; /* WAVEFORMATEX (cbSize=0) */
/* LIST('strl') sizes = 4 (the 'strl' fourcc) + contained chunks. */
const uint32_t vstrl_size = 4 + (8 + STRH_DATA) + (8 + BIH_DATA); /* 4+64+48 = 116 */
const uint32_t astrl_size = 4 + (8 + STRH_DATA) + (8 + WFX_DATA); /* 4+64+26 = 94 */
/* LIST('hdrl') size = 4 (the 'hdrl' fourcc) + avih chunk + both strl LISTs. */
const uint32_t hdrl_size = 4 + (8 + AVIH_DATA) + (8 + vstrl_size) + (8 + astrl_size);
uint8_t *p = out;
/* RIFF 'AVI ' (size unseekable → streaming sentinel) */
put_fourcc(&p, "RIFF");
put_u32(&p, STREAMING);
put_fourcc(&p, "AVI ");
/* LIST 'hdrl' */
put_fourcc(&p, "LIST");
put_u32(&p, hdrl_size);
put_fourcc(&p, "hdrl");
/* avih — MainAVIHeader (56 bytes) */
put_fourcc(&p, "avih");
put_u32(&p, AVIH_DATA);
put_u32(&p, usec_per_frame); /* dwMicroSecPerFrame */
put_u32(&p, 0); /* dwMaxBytesPerSec */
put_u32(&p, 0); /* dwPaddingGranularity */
put_u32(&p, 0); /* dwFlags — NO index flags */
put_u32(&p, 0); /* dwTotalFrames (unknown in stream) */
put_u32(&p, 0); /* dwInitialFrames */
put_u32(&p, 2); /* dwStreams (video + audio) */
put_u32(&p, 0); /* dwSuggestedBufferSize */
put_u32(&p, width); /* dwWidth */
put_u32(&p, height); /* dwHeight */
put_u32(&p, 0); put_u32(&p, 0); put_u32(&p, 0); put_u32(&p, 0); /* dwReserved[4] */
/* LIST 'strl' — VIDEO */
put_fourcc(&p, "LIST");
put_u32(&p, vstrl_size);
put_fourcc(&p, "strl");
/* strh — AVISTREAMHEADER 'vids' (56 bytes) */
put_fourcc(&p, "strh");
put_u32(&p, STRH_DATA);
put_fourcc(&p, "vids"); /* fccType */
put_fourcc(&p, "UYVY"); /* fccHandler */
put_u32(&p, 0); /* dwFlags */
put_u16(&p, 0); /* wPriority */
put_u16(&p, 0); /* wLanguage */
put_u32(&p, 0); /* dwInitialFrames */
put_u32(&p, fps_den); /* dwScale = 1001 */
put_u32(&p, fps_num); /* dwRate = 60000 */
put_u32(&p, 0); /* dwStart */
put_u32(&p, 0); /* dwLength (unknown) */
put_u32(&p, video_bytes); /* dwSuggestedBufferSize */
put_u32(&p, 0xFFFFFFFFu); /* dwQuality (-1 default) */
put_u32(&p, video_bytes); /* dwSampleSize (fixed for uncompressed) */
put_u16(&p, 0); put_u16(&p, 0); /* rcFrame.left, top */
put_u16(&p, (uint16_t)width); /* rcFrame.right */
put_u16(&p, (uint16_t)height); /* rcFrame.bottom */
/* strf — BITMAPINFOHEADER (40 bytes) */
put_fourcc(&p, "strf");
put_u32(&p, BIH_DATA);
put_u32(&p, 40); /* biSize */
put_u32(&p, width); /* biWidth */
put_u32(&p, height); /* biHeight */
put_u16(&p, 1); /* biPlanes */
put_u16(&p, 16); /* biBitCount (UYVY422 = 16bpp) */
put_fourcc(&p, "UYVY"); /* biCompression fourcc */
put_u32(&p, video_bytes); /* biSizeImage = W*H*2 */
put_u32(&p, 0); /* biXPelsPerMeter */
put_u32(&p, 0); /* biYPelsPerMeter */
put_u32(&p, 0); /* biClrUsed */
put_u32(&p, 0); /* biClrImportant */
/* LIST 'strl' — AUDIO */
put_fourcc(&p, "LIST");
put_u32(&p, astrl_size);
put_fourcc(&p, "strl");
/* strh — AVISTREAMHEADER 'auds' (56 bytes) */
put_fourcc(&p, "strh");
put_u32(&p, STRH_DATA);
put_fourcc(&p, "auds"); /* fccType */
put_u32(&p, 0); /* fccHandler (none for PCM) */
put_u32(&p, 0); /* dwFlags */
put_u16(&p, 0); /* wPriority */
put_u16(&p, 0); /* wLanguage */
put_u32(&p, 0); /* dwInitialFrames */
put_u32(&p, block_align); /* dwScale = nBlockAlign */
put_u32(&p, avg_bytes_sec); /* dwRate = nAvgBytesPerSec */
put_u32(&p, 0); /* dwStart */
put_u32(&p, 0); /* dwLength (unknown) */
put_u32(&p, avg_bytes_sec); /* dwSuggestedBufferSize (~1s) */
put_u32(&p, 0xFFFFFFFFu); /* dwQuality */
put_u32(&p, block_align); /* dwSampleSize = nBlockAlign */
put_u16(&p, 0); put_u16(&p, 0); put_u16(&p, 0); put_u16(&p, 0); /* rcFrame */
/* strf — WAVEFORMATEX (18 bytes) */
put_fourcc(&p, "strf");
put_u32(&p, WFX_DATA);
put_u16(&p, 1); /* wFormatTag = WAVE_FORMAT_PCM */
put_u16(&p, (uint16_t)audio_channels); /* nChannels */
put_u32(&p, audio_rate); /* nSamplesPerSec */
put_u32(&p, avg_bytes_sec); /* nAvgBytesPerSec */
put_u16(&p, block_align); /* nBlockAlign */
put_u16(&p, bits_per_sample); /* wBitsPerSample */
put_u16(&p, 0); /* cbSize */
/* LIST 'movi' — frames follow. Size unseekable → streaming sentinel. */
put_fourcc(&p, "LIST");
put_u32(&p, STREAMING);
put_fourcc(&p, "movi");
return (size_t)(p - out);
}
/* Write a single AVI chunk: 4-byte fourcc + u32 LE size + data (+ pad byte if
* the size is odd, per the RIFF even-alignment rule). Returns 0 / -1. */
static int write_avi_chunk(int fd, const char *cc,
const uint8_t *data, uint32_t size) {
uint8_t hdr[8];
uint8_t *p = hdr;
put_fourcc(&p, cc);
put_u32(&p, size);
if (write_all_fd(fd, hdr, 8) < 0) return -1;
if (size && write_all_fd(fd, data, size) < 0) return -1;
if (size & 1u) {
uint8_t pad = 0;
if (write_all_fd(fd, &pad, 1) < 0) return -1;
}
return 0;
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <slot_id> [wait_ms] [--avi|-]\n", argv[0]);
return 1;
}
const char *slot_id = argv[1];
uint64_t wait_ms = argc >= 3 ? (uint64_t)atoll(argv[2]) : 30000;
/* AVI mode is selected by an explicit flag in argv[3]. Anything that is not
* "--avi"/"avi" (including "-" or omitted) → legacy raw video-only mode. */
int avi_mode = 0;
if (argc >= 4) {
const char *m = argv[3];
if (strcmp(m, "--avi") == 0 || strcmp(m, "avi") == 0) avi_mode = 1;
}
signal(SIGTERM, on_signal);
signal(SIGINT, on_signal);
signal(SIGPIPE, SIG_IGN);
fcntl(STDOUT_FILENO, F_SETFL,
fcntl(STDOUT_FILENO, F_GETFL, 0) & ~O_NONBLOCK);
fprintf(stderr, "[fc_pipe] opening slot '%s' (wait %llums) mode=%s\n",
slot_id, (unsigned long long)wait_ms, avi_mode ? "avi" : "rawvideo");
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;
}
/* Pull stream format from the slot header for the AVI header. */
fc_stream_info_t si;
if (fc_consumer_info(c, &si) != 0 || si.width == 0 || si.height == 0) {
fprintf(stderr, "[fc_pipe] failed to read slot stream info\n");
fc_consumer_close(c);
return 1;
}
if (si.fps_num == 0) { si.fps_num = 60000; si.fps_den = 1001; }
if (si.fps_den == 0) si.fps_den = 1;
if (si.audio_rate == 0) si.audio_rate = 48000;
if (si.audio_channels == 0) si.audio_channels = 2;
if (si.audio_sample_bytes == 0) si.audio_sample_bytes = 2;
const uint32_t video_bytes = si.frame_size ? si.frame_size
: si.width * si.height * 2u;
const uint32_t a_blockalign = si.audio_channels * si.audio_sample_bytes;
/* Samples per video frame for synthesized silence when a frame has no audio:
* round(audio_rate * fps_den / fps_num). Bytes = samples * blockalign. */
uint32_t silence_bytes = 0;
{
double spf = (double)si.audio_rate * (double)si.fps_den / (double)si.fps_num;
uint32_t samples = (uint32_t)(spf + 0.5);
silence_bytes = samples * a_blockalign;
}
uint8_t *silence = NULL;
if (avi_mode && silence_bytes) {
silence = (uint8_t *)calloc(1, silence_bytes);
if (!silence) silence_bytes = 0;
}
if (avi_mode) {
uint8_t hdr[512];
size_t hlen = build_avi_header(hdr, si.width, si.height,
si.fps_num, si.fps_den, video_bytes,
si.audio_rate, si.audio_channels,
si.audio_sample_bytes);
if (write_all_fd(STDOUT_FILENO, hdr, hlen) < 0) {
fprintf(stderr, "[fc_pipe] stdout EPIPE writing AVI header\n");
fc_consumer_close(c); free(silence);
return 1;
}
fprintf(stderr,
"[fc_pipe] slot open, streaming AVI(video+audio) → stdout "
"(%ux%u %u/%u, %ub/frame, audio %uHz %uch s%ule, silence=%uB/frame)\n",
si.width, si.height, si.fps_num, si.fps_den, video_bytes,
si.audio_rate, si.audio_channels, si.audio_sample_bytes * 8u,
silence_bytes);
} else {
fprintf(stderr, "[fc_pipe] slot open, streaming raw video → stdout (%ux%u)\n",
si.width, si.height);
}
uint64_t frames_out = 0;
uint64_t total_dropped = 0;
uint64_t audio_bytes = 0;
uint64_t audio_gaps = 0;
while (!g_stop) {
fc_frame_ref_t ref;
int rc = fc_consumer_read(c, &ref, 2000);
if (rc == FC_TIMEOUT) continue;
if (rc == FC_ERROR) break;
if (rc == FC_LAPPED) {
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) {
total_dropped = fc_consumer_dropped(c);
fprintf(stderr, "[fc_pipe] WARNING: consumer fell behind — total dropped: %llu\n",
(unsigned long long)total_dropped);
}
if (avi_mode) {
/* Interleave THIS frame's video + audio in one stream. Both are
* sourced from the SAME ring entry ⇒ frame-coupled by construction.
* Video first (00dc), then audio (01wb). */
if (write_avi_chunk(STDOUT_FILENO, "00dc", ref.data, ref.size) < 0) {
if (!g_stop)
fprintf(stderr, "[fc_pipe] stdout EPIPE (video) — ffmpeg exited\n");
break;
}
if (ref.audio_size > 0 && ref.audio) {
if (write_avi_chunk(STDOUT_FILENO, "01wb", ref.audio, ref.audio_size) < 0) {
if (!g_stop)
fprintf(stderr, "[fc_pipe] stdout EPIPE (audio) — ffmpeg exited\n");
break;
}
audio_bytes += ref.audio_size;
} else {
/* No embedded audio this frame: emit one frame-interval of
* silence so the audio stream length tracks the video and
* ffmpeg never starves on the audio demuxer. */
if (silence_bytes &&
write_avi_chunk(STDOUT_FILENO, "01wb", silence, silence_bytes) < 0) {
if (!g_stop)
fprintf(stderr, "[fc_pipe] stdout EPIPE (silence) — ffmpeg exited\n");
break;
}
audio_bytes += silence_bytes;
audio_gaps++;
}
} else {
/* Legacy raw video-only: write the UYVY422 bytes straight 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++;
if (frames_out % 300 == 0) {
fprintf(stderr, "[fc_pipe] frames=%llu dropped=%llu audio_bytes=%llu gaps=%llu\n",
(unsigned long long)frames_out,
(unsigned long long)total_dropped,
(unsigned long long)audio_bytes,
(unsigned long long)audio_gaps);
}
}
free(silence);
fc_consumer_close(c);
fprintf(stderr, "[fc_pipe] done frames=%llu dropped=%llu audio_bytes=%llu gaps=%llu\n",
(unsigned long long)frames_out,
(unsigned long long)total_dropped,
(unsigned long long)audio_bytes,
(unsigned long long)audio_gaps);
return 0;
}