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 (_) {}