From d6e515e1a839e93b73f5dc409ee98fae154733ff Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Mon, 1 Jun 2026 14:57:53 -0400 Subject: [PATCH] fix(capture): map deltacast audio from input 1; per-recorder channel/port; fix bridge stdin pipe - Audio map: the deltacast bridge delivers audio on a separate FIFO wired as ffmpeg input 1, so the finalized master + HLS preview (and the growing orchestrator) now map audio via `audioMap` (1:a:0? for deltacast, 0:a:0? for DeckLink SDI / network) instead of an unconditional 0:a:0?. Without this the deltacast master/preview carried no audio. - Channel/port: spawn the bridge with --device = board index (default 0) and --port = source_config.port (falling back to the device index), so a recorder can capture from any of the board's 8 channels. Adds `port`/`board` params to start() and _buildInputArgs(). - Bridge stdin: the finalized-master ffmpeg reads the bridge's raw video from pipe:0, so its stdin must be 'pipe' when a bridge is present (was 'ignore', which made hiresProcess.stdin null and threw "Cannot read properties of null (reading 'on')" at bridgeProcess.stdout.pipe(...)). Co-Authored-By: Claude Opus 4.8 --- services/capture/src/capture-manager.js | 38 ++++++++++++++++++------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/services/capture/src/capture-manager.js b/services/capture/src/capture-manager.js index e2a38af..60cf556 100644 --- a/services/capture/src/capture-manager.js +++ b/services/capture/src/capture-manager.js @@ -559,7 +559,7 @@ class CaptureManager { * Returns { inputArgs, isNetwork } * @private */ - async _buildInputArgs({ sourceType, sourceBackend = 'blackmagic', device, sourceUrl, listen, listenPort, streamKey }) { + async _buildInputArgs({ sourceType, sourceBackend = 'blackmagic', device, port, board, sourceUrl, listen, listenPort, streamKey }) { if (sourceType === 'srt') { let url; if (listen) { @@ -599,9 +599,17 @@ class CaptureManager { throw new Error(`Failed to create audio FIFO ${audioFifo}: ${e.message}`); } + // ONE board (index 0) carries 8 channels (ports 0-7). --device is the + // board index, --port is the selected channel. board defaults to 0; the + // capture channel comes from source_config.port, falling back to the + // legacy device index so existing single-value recorders keep working. + const boardIdx = (typeof board === 'number' || /^\d+$/.test(String(board))) + ? parseInt(board, 10) : 0; + const portIdx = (typeof port === 'number' || /^\d+$/.test(String(port))) + ? parseInt(port, 10) : idx; const bridge = spawn('deltacast-capture', [ - '--device', String(idx), - '--port', String(idx), + '--device', String(boardIdx), + '--port', String(portIdx), '--audio-pipe', audioFifo, '--signal-timeout', '30', ], { stdio: ['ignore', 'pipe', 'pipe'] }); @@ -669,7 +677,7 @@ class CaptureManager { * * Returns the argv for spawn('bash', argv). */ - _buildGrowingOrchestrator({ inputArgs, videoBitrate, resolution, framerate, audioChannels, outPath, hlsDir, videoCodec }) { + _buildGrowingOrchestrator({ inputArgs, videoBitrate, resolution, framerate, audioChannels, outPath, hlsDir, videoCodec, audioMap = '0:a:0?' }) { const { rawFlag, frameRate, ffRate } = deriveGrowingRaster(resolution, framerate); const vb = videoBitrate || GROWING_DEFAULT_BITRATE; const ach = audioChannels ? Number(audioChannels) : 2; @@ -691,7 +699,7 @@ class CaptureManager { '-r', ffRate, '-f', 'mpeg2video', '@VF@', // (b) PCM s16le audio → "$AF" - '-map', '0:a:0?', + '-map', audioMap, '-c:a', 'pcm_s16le', '-ar', '48000', '-ac', String(ach), '-f', 's16le', '@AF@', ]; @@ -699,7 +707,7 @@ class CaptureManager { if (hlsDir) { ffHls = [ // (c) H.264 HLS preview — GPU-gated, unchanged behaviour. - '-map', '[vlo]', '-map', '0:a:0?', + '-map', '[vlo]', '-map', audioMap, ...buildHlsVideoArgs(videoCodec, framerate), '-c:a', 'aac', '-b:a', '128k', '-ar', '44100', '-f', 'hls', '-hls_time', '2', '-hls_list_size', '15', @@ -813,6 +821,10 @@ exit "$BMXRC" binId, clipName, device, + // Deltacast: one board (index 0) with 8 channels. `port` selects the + // channel; `board` selects the physical board (default 0). + port, + board, sourceType = 'sdi', // Source-backend selection for SDI capture (issue #168). Defaults to // `blackmagic` (DeckLink) so existing recorders are unaffected. @@ -893,9 +905,14 @@ exit "$BMXRC" this._sessionIdForBridge = sessionId; const { inputArgs, isNetwork, bridgeProcess = null, audioFifo = null, interlaced = false } = await this._buildInputArgs({ - sourceType, sourceBackend, device, sourceUrl, listen, listenPort, streamKey, + sourceType, sourceBackend, device, port, board, sourceUrl, listen, listenPort, streamKey, }); + // Audio input index: the deltacast bridge delivers audio on a separate + // FIFO wired as ffmpeg input 1, whereas DeckLink SDI and network sources + // carry audio inside input 0. (bridgeProcess is set only for deltacast.) + const audioMap = bridgeProcess ? '1:a:0?' : '0:a:0?'; + // Non-growing master: ffmpeg muxes the finalized MOV directly. Growing // master: raw2bmx muxes the OP1a from elementary FIFOs (handled below via // the orchestrator), so we don't build ffmpeg codec args here for it. @@ -935,7 +952,7 @@ exit "$BMXRC" catch (err) { console.error('[capture] could not create temp master dir:', err.message); } } const hiresOutput = localMasterPath; - const hiresStdio = ['ignore', 'ignore', 'pipe']; + const hiresStdio = [bridgeProcess ? 'pipe' : 'ignore', 'ignore', 'pipe']; // For SDI we cannot open the DeckLink device a second time for a preview // tee, so the live HLS preview is produced as a SECOND OUTPUT of the hires @@ -967,6 +984,7 @@ exit "$BMXRC" outPath: growingPath, hlsDir: (sourceType === 'sdi') ? sdiHlsDir : null, videoCodec, + audioMap, }); console.log('[capture] growing master via raw2bmx; orchestrator script length=' + orchArgs[1].length); hiresProcess = spawn('bash', orchArgs, { stdio: ['ignore', 'ignore', 'pipe'], detached: true }); @@ -978,14 +996,14 @@ exit "$BMXRC" ...inputArgs, '-filter_complex', '[0:v]yadif=mode=1:deint=1,split=2[vhi][vlo]', // Output 0 — ProRes/MOV master (local temp, uploaded to S3 on stop) - '-map', '[vhi]', '-map', '0:a:0?', + '-map', '[vhi]', '-map', audioMap, ...hiresCodecArgs, hiresOutput, // Output 1 — low-latency H.264 HLS preview for the UI monitor. // GPU-encoded (h264_nvenc) when the GPU is attached to this sidecar, // otherwise libx264 (issue #164). GOP is pinned to one IDR per HLS // segment so segments start on keyframes (avoids black/flashing). - '-map', '[vlo]', '-map', '0:a:0?', + '-map', '[vlo]', '-map', audioMap, ...buildHlsVideoArgs(videoCodec, framerate), '-c:a', 'aac', '-b:a', '128k', '-ar', '44100', '-f', 'hls', '-hls_time', '2', '-hls_list_size', '15',