From b6f5b9b407f98b1ec2783b72888de066f90be641 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Thu, 28 May 2026 22:36:06 +0000 Subject: [PATCH] =?UTF-8?q?fix(capture):=20disable=20concurrent=20SDI=20pr?= =?UTF-8?q?oxy=20ffmpeg=20=E2=80=94=20DeckLink=20Duo=202=20rejects=20secon?= =?UTF-8?q?d=20reader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeckLink Duo 2 does not support two simultaneous ffmpeg processes on the same port. The second (proxy) process immediately gets 'Cannot Autodetect input stream or No signal', producing an empty upload that could crash the container before the hires upload completes. Fix: remove the parallel proxy spawn for SDI entirely. proxyKey is now always null for SDI recordings (same as SRT/RTMP). needsProxy=true is already set when proxyKey is null, so the BullMQ worker generates the proxy from the hires master after stop — same pattern that works for network sources. Also revert bad regex change: ffmpeg -sources decklink output on this hardware uses hex-address format ('81:76669a80:00000000 [DeckLink Duo (1)]') not bare indented names — original regex was correct. --- services/capture/src/capture-manager.js | 58 +++++++------------------ services/capture/src/routes/capture.js | 14 +++--- 2 files changed, 20 insertions(+), 52 deletions(-) diff --git a/services/capture/src/capture-manager.js b/services/capture/src/capture-manager.js index 6d858ff..98180c1 100644 --- a/services/capture/src/capture-manager.js +++ b/services/capture/src/capture-manager.js @@ -149,10 +149,9 @@ class CaptureManager { const out = execSync('ffmpeg -hide_banner -sources decklink 2>&1', { encoding: 'utf-8', timeout: 5000 }); const names = []; for (const line of out.split('\n')) { - // Device name lines are indented (start with one or more spaces). - // Header/blank lines are skipped. - const m = line.match(/^ {2,}(.+?)\s*$/); - if (m && m[1]) names.push(m[1]); + // DeckLink source lines: " 81:76669a80:00000000 [DeckLink Duo (1)] (none)" + const m = line.match(/^\s+[0-9a-f:]+\s+\[([^\]]+)\]/); + if (m) names.push(m[1]); } if (names[idx]) { deckLinkName = names[idx]; @@ -230,12 +229,16 @@ class CaptureManager { catch (err) { console.error('[capture] could not create growing dir:', err.message); } } - // Network sources cannot be opened by two FFmpeg processes simultaneously - // (one socket = one consumer). For SRT/RTMP the BullMQ worker generates - // the proxy after the recording stops. - const proxyKey = (sourceType === 'sdi' && proxyEnabled) - ? `projects/${projectId}/proxies/${clipName}.${proxyExt}` - : null; + // DeckLink hardware does NOT support concurrent capture from the same port. + // Opening a second ffmpeg process on the same DeckLink input while the first + // is already capturing causes "Cannot Autodetect input stream or No signal" + // on the second process — making the proxy empty and potentially crashing the + // container before the hires upload completes. + // + // Treat SDI the same as SRT/RTMP: set proxyKey=null here and let the BullMQ + // worker generate the proxy from the hires master after the recording stops. + // The stop handler sets needsProxy=true so the worker picks it up. + const proxyKey = null; const startedAt = new Date().toISOString(); @@ -318,39 +321,8 @@ class CaptureManager { } }); - // SDI only: spawn a second ffmpeg for the proxy. - // DeckLink cards allow concurrent reads; network sockets do not. - if (!isNetwork && proxyEnabled) { - const proxyCodecArgs = buildEncodeArgs({ - codec: proxyVideoCodec, - videoBitrate: proxyVideoBitrate, - framerate: proxyFramerate, - audioCodec: proxyAudioCodec, - audioBitrate: proxyAudioBitrate, - audioChannels: proxyAudioChannels, - container: proxyContainer, - isNetwork: false, - isProxy: true, - }); - - console.log('[capture] proxy ffmpeg args:', proxyCodecArgs.join(' ')); - - const proxyProcess = spawn('ffmpeg', [ - ...inputArgs, - ...sdiFilterArgs, - ...proxyCodecArgs, - '-movflags', '+frag_keyframe+empty_moov', - 'pipe:1', - ], { stdio: ['ignore', 'pipe', 'pipe'] }); - - const proxyUpload = createUploadStream(S3_BUCKET, proxyKey, proxyProcess.stdout); - processes.proxy = proxyProcess; - uploads.proxy = proxyUpload; - - proxyProcess.stderr.on('data', (data) => { - console.error(`[PROXY] ${data}`); - }); - } + // Proxy is generated after stop by the BullMQ worker (same as SRT/RTMP). + // DeckLink hardware does not support two concurrent readers on the same port. this.state.recording = true; this.state.sessionId = sessionId; diff --git a/services/capture/src/routes/capture.js b/services/capture/src/routes/capture.js index 394dd9c..f26f108 100644 --- a/services/capture/src/routes/capture.js +++ b/services/capture/src/routes/capture.js @@ -96,17 +96,13 @@ router.get('/devices', (req, res) => { } // Parse ffmpeg output for DeckLink device names. - // ffmpeg -sources decklink output: - // Auto-detected sources for decklink: - // DeckLink Duo 2 - // DeckLink Duo 2 (2) - // Device name lines are indented (2+ leading spaces); header/blank lines are not. + // DeckLink source lines: " 81:76669a80:00000000 [DeckLink Duo (1)] (none)" const lines = output.split('\n'); let deviceIndex = 0; for (const line of lines) { - const match = line.match(/^ {2,}(.+?)\s*$/); - if (match && match[1]) { + const match = line.match(/^\s+[0-9a-f:]+\s+\[([^\]]+)\]/); + if (match) { devices.push({ index: deviceIndex, name: match[1], @@ -144,8 +140,8 @@ router.post('/probe', async (req, res) => { const raw = execSync('ffmpeg -hide_banner -sources decklink 2>&1', { encoding: 'utf-8', timeout: 5000 }); const devices = []; for (const line of raw.split('\n')) { - const m = line.match(/^ {2,}(.+?)\s*$/); - if (m && m[1]) devices.push(m[1]); + const m = line.match(/^\s+[0-9a-f:]+\s+\[([^\]]+)\]/); + if (m) devices.push(m[1]); } return res.json({ ok: true, source_type, devices }); } catch (err) {