fix(growing): ffmpeg reads video from saved FD 9 — fixes empty-output growing

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.
This commit is contained in:
Zac Gaetano 2026-06-04 18:59:53 +00:00
parent 690f27218d
commit a00a280689

View file

@ -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</dev/null; exec ${bmxLine} >/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</dev/null; exec ${bmxLine} >/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
# Parent no longer needs the saved stdin drop it so ffmpeg is the sole reader.
exec 9<&-
# Forward a clean stop to ffmpeg; raw2bmx then gets EOF and finalizes the footer.
stop() { kill -INT "$FFPID" 2>/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 (_) {}