From a00a280689695818e80dc66f52751141b43eb992 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Thu, 4 Jun 2026 18:59:53 +0000 Subject: [PATCH] =?UTF-8?q?fix(growing):=20ffmpeg=20reads=20video=20from?= =?UTF-8?q?=20saved=20FD=209=20=E2=80=94=20fixes=20empty-output=20growing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROOT CAUSE FOUND + verified. When growing video comes from fc_pipe, node pipes it to the bash orchestrator's stdin. ffmpeg ran as a backgrounded subshell merely inheriting fd 0 (0<&0). With a PIPE source (not the working file/FIFO case), that subshell was starved of the raw video -> filtergraph 'No filtered frames' -> empty mpeg2video -> raw2bmx broken pipe -> sidecar crash (write EPIPE). Reproduced exactly with 'fc_pipe | bash -c orchestrator'. Fix: save original stdin to FD 9 BEFORE the FIFO-priming fd games (exec 9<&0), point ffmpeg's fd0 at fd9 (0<&9), close 9 in raw2bmx + parent. Verified the live-equivalent path now produces a valid mpeg2video 4:2:2 yuv422p progressive 30000/1001 MXF matching the working Delta7 file. Also added EPIPE handlers so a broken pipe never crashes the sidecar. --- services/capture/src/capture-manager.js | 45 +++++++++++++------------ 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/services/capture/src/capture-manager.js b/services/capture/src/capture-manager.js index b0732ae..9fce90a 100644 --- a/services/capture/src/capture-manager.js +++ b/services/capture/src/capture-manager.js @@ -937,6 +937,12 @@ const bmx = [ // overwrites them in-place. It is killed by the cleanup trap on exit. const script = ` set -u +# Save the ORIGINAL stdin (the fc_pipe video stream Node pipes in) to FD 9 +# BEFORE any FIFO priming touches fd 0. ffmpeg later reads from FD 9 explicitly, +# guaranteeing it is the sole reader of the raw video — running ffmpeg as a +# backgrounded subshell that merely inherited fd 0 starved it of bytes when the +# input was a pipe (the "No filtered frames / empty output" growing failure). +exec 9<&0 VF=$(mktemp -u /tmp/grow_v.XXXXXX); AF=$(mktemp -u /tmp/grow_a.XXXXXX) OUT=${sh(outPath)} mkfifo "$VF" "$AF" @@ -945,24 +951,16 @@ cleanup() { rm -f "$VF" "$AF"; [ -n "$PATCHPID" ] && kill "$PATCHPID" 2>/dev/nul trap cleanup EXIT # Prime both FIFOs read-write (non-blocking) to break the open-order deadlock. exec 7<>"$VF" 8<>"$AF" -# raw2bmx: close priming FDs (no stray writer) before exec so it sees real EOF. -# Capture raw2bmx stderr to /tmp/raw2bmx.log for debugging. -( exec 7>&- 8>&- 0/tmp/raw2bmx.log 2>&1 ) & +# raw2bmx: close priming FDs (no stray writer) + read stdin from /dev/null before +# exec so it sees real EOF and never competes for the video pipe. +( exec 7>&- 8>&- 9>&- 0/tmp/raw2bmx.log 2>&1 ) & BMXPID=$! -# ffmpeg: closes priming FDs and EXPLICITLY inherits bash stdin (fd 0) so that -# 'pipe:0' reads the fc_pipe video stream Node piped into this orchestrator's -# stdin. For non-fc_pipe sources (FIFO/device input) fd 0 is unused and this is -# harmless. -( exec 7>&- 8>&- 0<&0; exec ${ffLine} ) & +# ffmpeg reads the raw video from FD 9 (the saved original stdin). pipe:0 in its +# arg list maps to fd 0, so we point fd 0 at FD 9 for the child. +( exec 7>&- 8>&- 0<&9 9<&-; exec ${ffLine} ) & FFPID=$! -# CRITICAL: the parent shell must DROP its own copy of stdin (fd 0) now that -# ffmpeg has inherited it. When video comes from fc_pipe (node pipes it to this -# orchestrator's stdin), leaving fd 0 open in the parent means BOTH the parent -# and the ffmpeg subshell hold the read end of that pipe — the kernel delivers -# bytes to whichever reads first, so ffmpeg is starved of the raw video and its -# filtergraph gets "No filtered frames" / empty output. Closing fd 0 here makes -# ffmpeg the SOLE reader so all fc_pipe video reaches pipe:0. -exec 0/dev/null; } trap stop INT TERM @@ -975,12 +973,6 @@ for i in $(seq 1 200); do sleep 0.1 done exec 7>&- 8>&- -# No header-duration patcher is needed. In this bmx v1.6 build, raw2bmx's rdd9 -# writer with --part maintains a live, correct header Duration as the file grows -# (verified on-node: ffprobe reads a growing duration mid-write, e.g. 2.04s of a -# 10s clip while still recording). A patcher (the earlier dur-patch.py) was a -# no-op here — it searched for Duration=-1, which rdd9 never writes — and opening -# the file r+b while raw2bmx appends over CIFS only adds concurrency risk. PATCHPID= # Wait for ffmpeg (source end), then for raw2bmx to finalize the footer. wait "$FFPID"; FFRC=$? @@ -1228,6 +1220,15 @@ exit "$BMXRC" // When video comes from fc_pipe, pipe its stdout to the bash orchestrator // stdin (which the orchestrator forwards to the ffmpeg rawvideo input). if (bridgeProcess && bridgeProcess.stdout && hiresProcess.stdin) { + // Swallow EPIPE/stream errors so a broken video pipe (e.g. the + // orchestrator exiting) can never crash the whole capture sidecar with + // an unhandled 'error' event ("Error: write EPIPE"). + hiresProcess.stdin.on('error', (e) => { + if (e && e.code !== 'EPIPE') console.warn(`[capture] orchestrator stdin error: ${e.message}`); + }); + bridgeProcess.stdout.on('error', (e) => { + console.warn(`[capture] fc_pipe stdout error: ${e && e.message}`); + }); bridgeProcess.stdout.pipe(hiresProcess.stdin); bridgeProcess.on('exit', () => { try { if (hiresProcess.stdin) hiresProcess.stdin.end(); } catch (_) {}