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🅰️0? for deltacast, 0🅰️0? for
  DeckLink SDI / network) instead of an unconditional 0🅰️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 <noreply@anthropic.com>
This commit is contained in:
Zac Gaetano 2026-06-01 14:57:53 -04:00
parent 3d3c8c48de
commit d6e515e1a8

View file

@ -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',