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:
parent
690f27218d
commit
a00a280689
1 changed files with 23 additions and 22 deletions
|
|
@ -937,6 +937,12 @@ const bmx = [
|
||||||
// overwrites them in-place. It is killed by the cleanup trap on exit.
|
// overwrites them in-place. It is killed by the cleanup trap on exit.
|
||||||
const script = `
|
const script = `
|
||||||
set -u
|
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)
|
VF=$(mktemp -u /tmp/grow_v.XXXXXX); AF=$(mktemp -u /tmp/grow_a.XXXXXX)
|
||||||
OUT=${sh(outPath)}
|
OUT=${sh(outPath)}
|
||||||
mkfifo "$VF" "$AF"
|
mkfifo "$VF" "$AF"
|
||||||
|
|
@ -945,24 +951,16 @@ cleanup() { rm -f "$VF" "$AF"; [ -n "$PATCHPID" ] && kill "$PATCHPID" 2>/dev/nul
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
# Prime both FIFOs read-write (non-blocking) to break the open-order deadlock.
|
# Prime both FIFOs read-write (non-blocking) to break the open-order deadlock.
|
||||||
exec 7<>"$VF" 8<>"$AF"
|
exec 7<>"$VF" 8<>"$AF"
|
||||||
# raw2bmx: close priming FDs (no stray writer) before exec so it sees real EOF.
|
# raw2bmx: close priming FDs (no stray writer) + read stdin from /dev/null before
|
||||||
# Capture raw2bmx stderr to /tmp/raw2bmx.log for debugging.
|
# exec so it sees real EOF and never competes for the video pipe.
|
||||||
( exec 7>&- 8>&- 0</dev/null; exec ${bmxLine} >/tmp/raw2bmx.log 2>&1 ) &
|
( exec 7>&- 8>&- 9>&- 0</dev/null; exec ${bmxLine} >/tmp/raw2bmx.log 2>&1 ) &
|
||||||
BMXPID=$!
|
BMXPID=$!
|
||||||
# ffmpeg: closes priming FDs and EXPLICITLY inherits bash stdin (fd 0) so that
|
# ffmpeg reads the raw video from FD 9 (the saved original stdin). pipe:0 in its
|
||||||
# 'pipe:0' reads the fc_pipe video stream Node piped into this orchestrator's
|
# arg list maps to fd 0, so we point fd 0 at FD 9 for the child.
|
||||||
# stdin. For non-fc_pipe sources (FIFO/device input) fd 0 is unused and this is
|
( exec 7>&- 8>&- 0<&9 9<&-; exec ${ffLine} ) &
|
||||||
# harmless.
|
|
||||||
( exec 7>&- 8>&- 0<&0; exec ${ffLine} ) &
|
|
||||||
FFPID=$!
|
FFPID=$!
|
||||||
# CRITICAL: the parent shell must DROP its own copy of stdin (fd 0) now that
|
# Parent no longer needs the saved stdin — drop it so ffmpeg is the sole reader.
|
||||||
# ffmpeg has inherited it. When video comes from fc_pipe (node pipes it to this
|
exec 9<&-
|
||||||
# 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
|
|
||||||
# Forward a clean stop to ffmpeg; raw2bmx then gets EOF and finalizes the footer.
|
# Forward a clean stop to ffmpeg; raw2bmx then gets EOF and finalizes the footer.
|
||||||
stop() { kill -INT "$FFPID" 2>/dev/null; }
|
stop() { kill -INT "$FFPID" 2>/dev/null; }
|
||||||
trap stop INT TERM
|
trap stop INT TERM
|
||||||
|
|
@ -975,12 +973,6 @@ for i in $(seq 1 200); do
|
||||||
sleep 0.1
|
sleep 0.1
|
||||||
done
|
done
|
||||||
exec 7>&- 8>&-
|
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=
|
PATCHPID=
|
||||||
# Wait for ffmpeg (source end), then for raw2bmx to finalize the footer.
|
# Wait for ffmpeg (source end), then for raw2bmx to finalize the footer.
|
||||||
wait "$FFPID"; FFRC=$?
|
wait "$FFPID"; FFRC=$?
|
||||||
|
|
@ -1228,6 +1220,15 @@ exit "$BMXRC"
|
||||||
// When video comes from fc_pipe, pipe its stdout to the bash orchestrator
|
// When video comes from fc_pipe, pipe its stdout to the bash orchestrator
|
||||||
// stdin (which the orchestrator forwards to the ffmpeg rawvideo input).
|
// stdin (which the orchestrator forwards to the ffmpeg rawvideo input).
|
||||||
if (bridgeProcess && bridgeProcess.stdout && hiresProcess.stdin) {
|
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.stdout.pipe(hiresProcess.stdin);
|
||||||
bridgeProcess.on('exit', () => {
|
bridgeProcess.on('exit', () => {
|
||||||
try { if (hiresProcess.stdin) hiresProcess.stdin.end(); } catch (_) {}
|
try { if (hiresProcess.stdin) hiresProcess.stdin.end(); } catch (_) {}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue