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:
parent
3d3c8c48de
commit
d6e515e1a8
1 changed files with 28 additions and 10 deletions
|
|
@ -559,7 +559,7 @@ class CaptureManager {
|
||||||
* Returns { inputArgs, isNetwork }
|
* Returns { inputArgs, isNetwork }
|
||||||
* @private
|
* @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') {
|
if (sourceType === 'srt') {
|
||||||
let url;
|
let url;
|
||||||
if (listen) {
|
if (listen) {
|
||||||
|
|
@ -599,9 +599,17 @@ class CaptureManager {
|
||||||
throw new Error(`Failed to create audio FIFO ${audioFifo}: ${e.message}`);
|
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', [
|
const bridge = spawn('deltacast-capture', [
|
||||||
'--device', String(idx),
|
'--device', String(boardIdx),
|
||||||
'--port', String(idx),
|
'--port', String(portIdx),
|
||||||
'--audio-pipe', audioFifo,
|
'--audio-pipe', audioFifo,
|
||||||
'--signal-timeout', '30',
|
'--signal-timeout', '30',
|
||||||
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
|
@ -669,7 +677,7 @@ class CaptureManager {
|
||||||
*
|
*
|
||||||
* Returns the argv for spawn('bash', argv).
|
* 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 { rawFlag, frameRate, ffRate } = deriveGrowingRaster(resolution, framerate);
|
||||||
const vb = videoBitrate || GROWING_DEFAULT_BITRATE;
|
const vb = videoBitrate || GROWING_DEFAULT_BITRATE;
|
||||||
const ach = audioChannels ? Number(audioChannels) : 2;
|
const ach = audioChannels ? Number(audioChannels) : 2;
|
||||||
|
|
@ -691,7 +699,7 @@ class CaptureManager {
|
||||||
'-r', ffRate,
|
'-r', ffRate,
|
||||||
'-f', 'mpeg2video', '@VF@',
|
'-f', 'mpeg2video', '@VF@',
|
||||||
// (b) PCM s16le audio → "$AF"
|
// (b) PCM s16le audio → "$AF"
|
||||||
'-map', '0:a:0?',
|
'-map', audioMap,
|
||||||
'-c:a', 'pcm_s16le', '-ar', '48000', '-ac', String(ach),
|
'-c:a', 'pcm_s16le', '-ar', '48000', '-ac', String(ach),
|
||||||
'-f', 's16le', '@AF@',
|
'-f', 's16le', '@AF@',
|
||||||
];
|
];
|
||||||
|
|
@ -699,7 +707,7 @@ class CaptureManager {
|
||||||
if (hlsDir) {
|
if (hlsDir) {
|
||||||
ffHls = [
|
ffHls = [
|
||||||
// (c) H.264 HLS preview — GPU-gated, unchanged behaviour.
|
// (c) H.264 HLS preview — GPU-gated, unchanged behaviour.
|
||||||
'-map', '[vlo]', '-map', '0:a:0?',
|
'-map', '[vlo]', '-map', audioMap,
|
||||||
...buildHlsVideoArgs(videoCodec, framerate),
|
...buildHlsVideoArgs(videoCodec, framerate),
|
||||||
'-c:a', 'aac', '-b:a', '128k', '-ar', '44100',
|
'-c:a', 'aac', '-b:a', '128k', '-ar', '44100',
|
||||||
'-f', 'hls', '-hls_time', '2', '-hls_list_size', '15',
|
'-f', 'hls', '-hls_time', '2', '-hls_list_size', '15',
|
||||||
|
|
@ -813,6 +821,10 @@ exit "$BMXRC"
|
||||||
binId,
|
binId,
|
||||||
clipName,
|
clipName,
|
||||||
device,
|
device,
|
||||||
|
// Deltacast: one board (index 0) with 8 channels. `port` selects the
|
||||||
|
// channel; `board` selects the physical board (default 0).
|
||||||
|
port,
|
||||||
|
board,
|
||||||
sourceType = 'sdi',
|
sourceType = 'sdi',
|
||||||
// Source-backend selection for SDI capture (issue #168). Defaults to
|
// Source-backend selection for SDI capture (issue #168). Defaults to
|
||||||
// `blackmagic` (DeckLink) so existing recorders are unaffected.
|
// `blackmagic` (DeckLink) so existing recorders are unaffected.
|
||||||
|
|
@ -893,9 +905,14 @@ exit "$BMXRC"
|
||||||
|
|
||||||
this._sessionIdForBridge = sessionId;
|
this._sessionIdForBridge = sessionId;
|
||||||
const { inputArgs, isNetwork, bridgeProcess = null, audioFifo = null, interlaced = false } = await this._buildInputArgs({
|
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
|
// Non-growing master: ffmpeg muxes the finalized MOV directly. Growing
|
||||||
// master: raw2bmx muxes the OP1a from elementary FIFOs (handled below via
|
// master: raw2bmx muxes the OP1a from elementary FIFOs (handled below via
|
||||||
// the orchestrator), so we don't build ffmpeg codec args here for it.
|
// 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); }
|
catch (err) { console.error('[capture] could not create temp master dir:', err.message); }
|
||||||
}
|
}
|
||||||
const hiresOutput = localMasterPath;
|
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
|
// 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
|
// tee, so the live HLS preview is produced as a SECOND OUTPUT of the hires
|
||||||
|
|
@ -967,6 +984,7 @@ exit "$BMXRC"
|
||||||
outPath: growingPath,
|
outPath: growingPath,
|
||||||
hlsDir: (sourceType === 'sdi') ? sdiHlsDir : null,
|
hlsDir: (sourceType === 'sdi') ? sdiHlsDir : null,
|
||||||
videoCodec,
|
videoCodec,
|
||||||
|
audioMap,
|
||||||
});
|
});
|
||||||
console.log('[capture] growing master via raw2bmx; orchestrator script length=' + orchArgs[1].length);
|
console.log('[capture] growing master via raw2bmx; orchestrator script length=' + orchArgs[1].length);
|
||||||
hiresProcess = spawn('bash', orchArgs, { stdio: ['ignore', 'ignore', 'pipe'], detached: true });
|
hiresProcess = spawn('bash', orchArgs, { stdio: ['ignore', 'ignore', 'pipe'], detached: true });
|
||||||
|
|
@ -978,14 +996,14 @@ exit "$BMXRC"
|
||||||
...inputArgs,
|
...inputArgs,
|
||||||
'-filter_complex', '[0:v]yadif=mode=1:deint=1,split=2[vhi][vlo]',
|
'-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)
|
// Output 0 — ProRes/MOV master (local temp, uploaded to S3 on stop)
|
||||||
'-map', '[vhi]', '-map', '0:a:0?',
|
'-map', '[vhi]', '-map', audioMap,
|
||||||
...hiresCodecArgs,
|
...hiresCodecArgs,
|
||||||
hiresOutput,
|
hiresOutput,
|
||||||
// Output 1 — low-latency H.264 HLS preview for the UI monitor.
|
// Output 1 — low-latency H.264 HLS preview for the UI monitor.
|
||||||
// GPU-encoded (h264_nvenc) when the GPU is attached to this sidecar,
|
// GPU-encoded (h264_nvenc) when the GPU is attached to this sidecar,
|
||||||
// otherwise libx264 (issue #164). GOP is pinned to one IDR per HLS
|
// otherwise libx264 (issue #164). GOP is pinned to one IDR per HLS
|
||||||
// segment so segments start on keyframes (avoids black/flashing).
|
// segment so segments start on keyframes (avoids black/flashing).
|
||||||
'-map', '[vlo]', '-map', '0:a:0?',
|
'-map', '[vlo]', '-map', audioMap,
|
||||||
...buildHlsVideoArgs(videoCodec, framerate),
|
...buildHlsVideoArgs(videoCodec, framerate),
|
||||||
'-c:a', 'aac', '-b:a', '128k', '-ar', '44100',
|
'-c:a', 'aac', '-b:a', '128k', '-ar', '44100',
|
||||||
'-f', 'hls', '-hls_time', '2', '-hls_list_size', '15',
|
'-f', 'hls', '-hls_time', '2', '-hls_list_size', '15',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue