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).
This commit is contained in:
parent
2f37119379
commit
97267aa857
5 changed files with 360 additions and 174 deletions
|
|
@ -757,59 +757,32 @@ class CaptureManager {
|
|||
const fcPipeBin = process.env.FC_PIPE_BIN || 'fc_pipe';
|
||||
const WAIT_MS = 30_000;
|
||||
|
||||
// Determine audio FIFO path based on source type
|
||||
const idx = (typeof device === 'number' || /^\d+$/.test(String(device)))
|
||||
? parseInt(device, 10) : 0;
|
||||
const portIdx = (sourceType === 'deltacast')
|
||||
? ((typeof port === 'number' || /^\d+$/.test(String(port)))
|
||||
? parseInt(port, 10) : idx)
|
||||
: idx;
|
||||
|
||||
let audioFifoDir, audioFifoPath;
|
||||
if (sourceType === 'deltacast') {
|
||||
audioFifoDir = process.env.DELTACAST_PIPE_DIR || '/dev/shm/deltacast';
|
||||
} else {
|
||||
audioFifoDir = process.env.DECKLINK_AUDIO_DIR || '/dev/shm/decklink';
|
||||
}
|
||||
audioFifoPath = `${audioFifoDir}/audio-${portIdx}.fifo`;
|
||||
|
||||
// ── Frame-coupled audio (FC_VERSION 2) ────────────────────────────────
|
||||
// The bridge no longer owns the audio FIFO: audio rides in the framecache
|
||||
// ring entry with its video frame. capture-manager CREATES the audio FIFO
|
||||
// and fc_pipe WRITES it, sourced from the SAME ring entry as the video, so
|
||||
// audio is frame-locked (no second transport, no drift). We mkfifo here
|
||||
// (idempotent) so fc_pipe and ffmpeg have a stable rendezvous path.
|
||||
const { existsSync: _exists, mkdirSync: _mkdir } = await import('node:fs');
|
||||
try { _mkdir(audioFifoDir, { recursive: true }); } catch { /* exists */ }
|
||||
if (!_exists(audioFifoPath)) {
|
||||
try {
|
||||
execFileSync('mkfifo', ['-m', '0666', audioFifoPath]);
|
||||
} catch (e) {
|
||||
if (!_exists(audioFifoPath)) {
|
||||
throw new Error(`failed to create audio FIFO ${audioFifoPath}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Single-input AVI: fc_pipe muxes video+audio into ONE streaming AVI
|
||||
// container on stdout. ffmpeg reads it as a SINGLE input (-f avi -i pipe:0),
|
||||
// which eliminates the confirmed two-live-pipe deadlock (ffmpeg given a raw
|
||||
// video pipe AND a separate live audio FIFO stalled forever probing input 0).
|
||||
// No audio FIFO is created or used on this path anymore: audio rides inside
|
||||
// the AVI as interleaved 01wb chunks, frame-coupled to each 00dc video chunk
|
||||
// (both come from the SAME framecache ring entry in fc_pipe's read loop).
|
||||
|
||||
// Video dimensions and fps come from env vars injected by node-agent
|
||||
// (populated from the bridge's format JSON on signal lock).
|
||||
// (populated from the bridge's format JSON on signal lock). fc_pipe also
|
||||
// reads them from the slot header for the AVI header; these stay for logging.
|
||||
const fcSize = process.env.DELTACAST_VIDEO_SIZE || '1920x1080';
|
||||
const fcFps = process.env.DELTACAST_FRAMERATE || '60000/1001';
|
||||
const fcInterlaced = process.env.DELTACAST_INTERLACED === '1';
|
||||
|
||||
console.log(`[framecache] slot=${slotId} size=${fcSize} fps=${fcFps} audio=${audioFifoPath} (frame-coupled)`);
|
||||
console.log(`[framecache] slot=${slotId} size=${fcSize} fps=${fcFps} mode=avi (single-input video+audio, frame-coupled)`);
|
||||
|
||||
// Spawn fc_pipe: opens the framecache slot with its own read cursor and,
|
||||
// for each ring entry, writes the VIDEO bytes to stdout (ffmpeg rawvideo
|
||||
// input 0) AND that frame's AUDIO bytes to the audio FIFO (ffmpeg s16le
|
||||
// input 1) IN LOCKSTEP from one cursor read. Because both come from the
|
||||
// SAME ring entry in the same iteration, audio can never drift from video
|
||||
// — the "audio ahead of video" offset is eliminated at the root.
|
||||
// argv: <slot_id> <wait_ms> <audio_fifo_path>
|
||||
const fcPipeProcess = spawn(fcPipeBin, [slotId, String(WAIT_MS), audioFifoPath], {
|
||||
// Spawn fc_pipe in AVI mode: for each ring entry it emits a 00dc video chunk
|
||||
// followed by a 01wb audio chunk into one AVI byte stream on stdout. ffmpeg
|
||||
// reads that single stream and maps 0:v / 0:a. Because video and its audio
|
||||
// are interleaved from the same ring entry, audio can never drift from video.
|
||||
// argv: <slot_id> <wait_ms> --avi
|
||||
const fcPipeProcess = spawn(fcPipeBin, [slotId, String(WAIT_MS), '--avi'], {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
// Pause until piped to ffmpeg (avoids OS pipe-buffer fill stall — see
|
||||
// Pause until piped to ffmpeg (avoids OS pipe-buffer fill stall - see
|
||||
// the network path above for the full rationale).
|
||||
fcPipeProcess.stdout.pause();
|
||||
fcPipeProcess.stderr.on('data', chunk => {
|
||||
|
|
@ -821,42 +794,23 @@ class CaptureManager {
|
|||
|
||||
return {
|
||||
inputArgs: [
|
||||
// fc_pipe stdout → ffmpeg rawvideo input 0 (video).
|
||||
// fc_pipe stdout -> ffmpeg AVI input 0. ONE input carries both streams:
|
||||
// 0:v = UYVY422 video (00dc chunks), 0:a = pcm_s16le audio (01wb chunks).
|
||||
// The AVI demuxer reads the strf headers + the chunk stream with no index
|
||||
// and no seeking, so streaming over a pipe is fine (RIFF/movi sizes are
|
||||
// left as the streaming sentinel by fc_pipe).
|
||||
'-thread_queue_size', '512',
|
||||
'-f', 'rawvideo',
|
||||
'-pix_fmt', 'uyvy422',
|
||||
'-video_size', fcSize,
|
||||
'-framerate', fcFps,
|
||||
'-i', 'pipe:0',
|
||||
// Audio FIFO → ffmpeg input 1.
|
||||
//
|
||||
// fc_pipe writes this FIFO from the SAME framecache ring entry as the
|
||||
// video it sends to input 0, one entry per loop iteration — so the
|
||||
// audio is exactly each video frame's SDI-embedded audio, delivered
|
||||
// frame-locked. There is no independent audio buffer to race ahead.
|
||||
//
|
||||
// Do NOT use -use_wallclock_as_timestamps here. fc_pipe feeds raw
|
||||
// s16le at a steady 48000 samples/s off the SAME SDI clock as video,
|
||||
// so letting ffmpeg derive audio PTS from the sample count keeps audio
|
||||
// and video in one clock domain (no drift). Wallclock stamps audio by
|
||||
// arrival wall-time instead — when the HEVC encoder dips under realtime
|
||||
// the audio ends up 3–18% LONGER than the frame-count video, and the
|
||||
// master aresample=async=1 then pads seconds of LEADING SILENCE to
|
||||
// "align" them → the silent-head + start-stutter + apparent "no audio"
|
||||
// regression (reverts commit d6b0b3a; restores 8e5405c/55a72af).
|
||||
'-thread_queue_size', '512',
|
||||
'-f', 's16le',
|
||||
'-ar', '48000',
|
||||
'-ac', '2',
|
||||
'-f', 'avi',
|
||||
// Optional fixed A/V trim (env AUDIO_OFFSET_MS); default empty = no shift.
|
||||
// Applied as an input option so it shifts the AVI's audio relative to video.
|
||||
...audioOffsetArgs(),
|
||||
'-i', audioFifoPath,
|
||||
'-i', 'pipe:0',
|
||||
],
|
||||
isNetwork: false,
|
||||
bridgeProcess: fcPipeProcess, /* capture-manager pipes this to ffmpeg stdin */
|
||||
audioFifo: audioFifoPath, /* flushed just before ffmpeg opens it (A/V align) */
|
||||
audioFifo: null, /* no separate audio FIFO on the AVI path */
|
||||
interlaced: fcInterlaced,
|
||||
audioInputIndex: 1, /* audio FIFO is ffmpeg input 1 */
|
||||
audioInputIndex: 0, /* audio is inside the single AVI input (0:a) */
|
||||
_fcPipeProcess: fcPipeProcess, /* stored for clean stop */
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ install(TARGETS net_ingest DESTINATION bin)
|
|||
add_executable(fc_pipe
|
||||
client/fc_pipe.c
|
||||
)
|
||||
target_link_libraries(fc_pipe fc_client)
|
||||
target_link_libraries(fc_pipe fc_client pthread)
|
||||
target_include_directories(fc_pipe PRIVATE src client)
|
||||
|
||||
# ── test consumer (dev utility) ──────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -229,3 +229,19 @@ uint64_t fc_consumer_dropped(fc_consumer_t *c)
|
|||
{
|
||||
return c->local_dropped;
|
||||
}
|
||||
|
||||
int fc_consumer_info(fc_consumer_t *c, fc_stream_info_t *info)
|
||||
{
|
||||
if (!c || !info) return -1;
|
||||
fc_header_t *hdr = (fc_header_t *)c->base;
|
||||
info->width = hdr->width;
|
||||
info->height = hdr->height;
|
||||
info->fps_num = hdr->fps_num;
|
||||
info->fps_den = hdr->fps_den;
|
||||
info->pixel_format = hdr->pixel_format;
|
||||
info->frame_size = hdr->frame_size;
|
||||
info->audio_rate = hdr->audio_rate ? hdr->audio_rate : FC_AUDIO_RATE;
|
||||
info->audio_channels = hdr->audio_channels ? hdr->audio_channels : FC_AUDIO_CHANNELS;
|
||||
info->audio_sample_bytes = FC_AUDIO_SAMPLE_BYTES; /* s16le */
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,6 +81,23 @@ uint64_t fc_consumer_write_cursor(fc_consumer_t *c);
|
|||
/** Frames dropped by this consumer since open. */
|
||||
uint64_t fc_consumer_dropped(fc_consumer_t *c);
|
||||
|
||||
/* Stream format info read from the slot header (set at slot creation by the
|
||||
* bridge). Used by fc_pipe to emit a correct AVI/container header. */
|
||||
typedef struct {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t fps_num;
|
||||
uint32_t fps_den;
|
||||
uint32_t pixel_format; /* FC_PIX_UYVY422 */
|
||||
uint32_t frame_size; /* video bytes per frame (width*height*2 for UYVY422) */
|
||||
uint32_t audio_rate; /* 48000 */
|
||||
uint32_t audio_channels; /* 2 */
|
||||
uint32_t audio_sample_bytes; /* 2 (s16le) */
|
||||
} fc_stream_info_t;
|
||||
|
||||
/** Fill *info from the slot header. Returns 0 on success, -1 on error. */
|
||||
int fc_consumer_info(fc_consumer_t *c, fc_stream_info_t *info);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,54 +1,35 @@
|
|||
/**
|
||||
* fc_pipe.c — Framecache slot → stdout(video) + FIFO(audio) pipe adapter.
|
||||
* 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. fc_pipe reads ONE entry per loop iteration
|
||||
* and writes:
|
||||
* - the video bytes → stdout (ffmpeg rawvideo input 0 / pipe:0)
|
||||
* - the audio bytes → audio FIFO (ffmpeg s16le input 1)
|
||||
* IN LOCKSTEP from the SAME cursor read. Because both come out of the same
|
||||
* ring entry in the same iteration, audio can never drift ahead of (or
|
||||
* behind) its video frame — there is no second independent buffer/transport
|
||||
* to race. This eliminates the constant "audio ahead of video" offset at the
|
||||
* root.
|
||||
* SDI-embedded AUDIO together (written by the JOINED bridge from one slot).
|
||||
* fc_pipe reads ONE entry per loop iteration.
|
||||
*
|
||||
* capture-manager.js spawns this process, pipes its stdout to ffmpeg input 0,
|
||||
* and passes the audio FIFO path it created as argv[3]; ffmpeg reads that FIFO
|
||||
* as input 1.
|
||||
* TWO OUTPUT MODES:
|
||||
*
|
||||
* Each consumer instance has its own independent read cursor, so multiple
|
||||
* fc_pipe processes reading from the same slot never interfere with each other
|
||||
* (growing + proxy + HLS all read the same SDI signal simultaneously).
|
||||
* 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).
|
||||
*
|
||||
* Usage:
|
||||
* fc_pipe <slot_id> [wait_ms] [audio_fifo_path]
|
||||
* 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.
|
||||
*
|
||||
* audio_fifo_path optional: if omitted (or "-"), audio is NOT emitted and
|
||||
* fc_pipe behaves video-only (legacy / network video-only sources).
|
||||
* The old split video-stdout / audio-FIFO design is REMOVED — it was the
|
||||
* source of the ffmpeg deadlock.
|
||||
*
|
||||
* Audio FIFO lifecycle:
|
||||
* - Opened O_WRONLY|O_NONBLOCK with retry; until a reader (ffmpeg input 1)
|
||||
* attaches the open returns ENXIO. We keep delivering VIDEO meanwhile so
|
||||
* ffmpeg makes progress and opens the FIFO. Audio for those very first
|
||||
* pre-roll frames is dropped (sub-frame startup gap inside pre-roll).
|
||||
* - Once open we switch the fd to blocking and write each frame's audio with
|
||||
* the same write_all() as video, keeping them coupled. If the signal has
|
||||
* no embedded audio on a frame (audio_size 0) we synthesize exactly that
|
||||
* frame's worth of silence so ffmpeg input 1 never starves and the audio
|
||||
* timeline length always equals the video timeline length (no drift).
|
||||
* - On audio FIFO EPIPE (ffmpeg input 1 reader died, e.g. session restart),
|
||||
* we close and re-open the FIFO; VIDEO delivery is unaffected.
|
||||
* 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 (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)
|
||||
* Terminates on: SIGTERM/SIGINT, stdout EPIPE (ffmpeg exited), slot gone.
|
||||
*/
|
||||
|
||||
#include "../src/slot.h"
|
||||
|
|
@ -63,6 +44,7 @@
|
|||
#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; }
|
||||
|
|
@ -75,31 +57,213 @@ static int write_all_fd(int fd, const void *buf, size_t 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 -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] [audio_fifo_path]\n", argv[0]);
|
||||
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;
|
||||
const char *audio_fifo = (argc >= 4 && strcmp(argv[3], "-") != 0) ? argv[3] : NULL;
|
||||
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); /* detect EPIPE via write() return value */
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
/* Set stdout to binary/blocking 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) audio_fifo=%s\n",
|
||||
slot_id, (unsigned long long)wait_ms,
|
||||
audio_fifo ? audio_fifo : "(none)");
|
||||
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) {
|
||||
|
|
@ -108,80 +272,115 @@ int main(int argc, char *argv[]) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "[fc_pipe] slot open, streaming video→stdout%s\n",
|
||||
audio_fifo ? " + audio→FIFO (frame-coupled)" : "");
|
||||
/* 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;
|
||||
|
||||
int afd = -1; /* audio FIFO fd, -1 until a reader attaches */
|
||||
int logged_aud = 0;
|
||||
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; /* frames where embedded audio was absent (silence filled) */
|
||||
uint64_t audio_gaps = 0;
|
||||
|
||||
while (!g_stop) {
|
||||
fc_frame_ref_t ref;
|
||||
int rc = fc_consumer_read(c, &ref, 2000 /* 2s timeout */);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/* ── Try to (re)attach the audio FIFO without stalling video ──────────
|
||||
* O_WRONLY|O_NONBLOCK returns ENXIO until ffmpeg input 1 opens the read
|
||||
* end. We keep delivering video so ffmpeg progresses and opens it. */
|
||||
if (audio_fifo && afd < 0) {
|
||||
int fd = open(audio_fifo, O_WRONLY | O_NONBLOCK);
|
||||
if (fd >= 0) {
|
||||
/* Switch to blocking for coupled writes. */
|
||||
int fl = fcntl(fd, F_GETFL, 0);
|
||||
if (fl >= 0) fcntl(fd, F_SETFL, fl & ~O_NONBLOCK);
|
||||
afd = fd;
|
||||
if (!logged_aud) {
|
||||
fprintf(stderr, "[fc_pipe] audio FIFO reader attached — coupled audio live\n");
|
||||
logged_aud = 1;
|
||||
}
|
||||
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;
|
||||
}
|
||||
/* else: not ready yet (ENXIO) — deliver video, retry next frame. */
|
||||
}
|
||||
|
||||
/* ── Write VIDEO 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;
|
||||
}
|
||||
|
||||
/* ── Write THIS frame's AUDIO to the FIFO, in lockstep ────────────────
|
||||
* Same ring entry, same iteration ⇒ frame-coupled. The bridge writer
|
||||
* guarantees every entry carries one frame-interval of audio (real
|
||||
* embedded PCM, or silence when the signal has none), so ref.audio_size
|
||||
* is non-zero in steady state and the audio timeline length always
|
||||
* tracks the video timeline length (no drift). A 0-size entry (only at
|
||||
* the very first frame, or a video-only net source) contributes nothing
|
||||
* and is harmless because ffmpeg derives audio PTS from sample count. */
|
||||
if (afd >= 0) {
|
||||
if (ref.audio_size > 0 && ref.audio) {
|
||||
if (write_all_fd(afd, ref.audio, ref.audio_size) < 0) {
|
||||
fprintf(stderr, "[fc_pipe] audio FIFO EPIPE — will reattach\n");
|
||||
close(afd); afd = -1;
|
||||
} else {
|
||||
audio_bytes += ref.audio_size;
|
||||
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 {
|
||||
audio_gaps++; /* diagnostics: frame without embedded audio */
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -195,7 +394,7 @@ int main(int argc, char *argv[]) {
|
|||
}
|
||||
}
|
||||
|
||||
if (afd >= 0) close(afd);
|
||||
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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue