diff --git a/services/node-agent/index.js b/services/node-agent/index.js index 5f48f51..722f8e3 100644 --- a/services/node-agent/index.js +++ b/services/node-agent/index.js @@ -29,6 +29,21 @@ const VERSION = '1.4.0'; // interpolated into a shell string. const DRIVER_VENDORS = ['blackmagic', 'aja', 'deltacast', 'ndi']; +// Deltacast board-open stagger: serialize sidecar starts so only one +// VHD_OpenBoardHandle is in flight at a time. Simultaneous opens trip a +// kernel BufMngr OOB bug (delta_x300 v6.34.1, BufMngr.c:781). A promise +// chain acts as a FIFO mutex; the settle delay lets the driver stabilize. +const DELTACAST_STAGGER_MS = parseInt(process.env.DELTACAST_START_STAGGER_MS || '3500', 10); +let _dcMutex = Promise.resolve(); + +function acquireDeltacastSlot() { + let release; + const ticket = new Promise(r => { release = r; }); + const prev = _dcMutex; + _dcMutex = prev.then(() => ticket); + return prev.then(() => release); +} + // Pick the host's LAN IP. Inside a bridge-mode container, // os.networkInterfaces() returns the container's docker-bridge IP (172.x), // not the host's LAN address. Two strategies: @@ -166,6 +181,41 @@ async function handleSidecarStart(body, res) { HostConfig: hostConfig, }; + // Deltacast: serialize board opens to prevent simultaneous VHD_OpenBoardHandle + // calls from wedging the delta_x300 BufMngr (OOB bug in v6.34.1). Acquire the + // slot (waits for any prior deltacast start to clear its settle delay), then + // start the container, then hold for DELTACAST_STAGGER_MS before releasing. + // Fail-open: if launch throws, the slot is still released so the next start + // isn't blocked forever. + if (sourceType === 'deltacast') { + const release = await acquireDeltacastSlot(); + try { + const createRes = await dockerApi('POST', '/containers/create', spec); + if (createRes.status !== 201) { + return jsonResponse(res, 502, { error: 'Failed to create container', details: createRes.data }); + } + + const containerId = createRes.data.Id; + const _u = (env.find(e => e.startsWith('MAM_API_URL=')) || '').slice(12); + const _tok = env.some(e => e.startsWith('MAM_API_TOKEN=') && e.length > 14); + console.log(`[sidecar-start] ${containerId} image=${image} src=${sourceType} MAM_API_URL=${_u} token=${_tok} stagger=${DELTACAST_STAGGER_MS}ms`); + const startRes = await dockerApi('POST', `/containers/${containerId}/start`); + if (startRes.status !== 204) { + await dockerApi('DELETE', `/containers/${containerId}?force=true`).catch(() => {}); + return jsonResponse(res, 502, { error: 'Failed to start container', details: startRes.data }); + } + + jsonResponse(res, 201, { containerId, capturePort }); + // Hold slot for settle delay AFTER responding — client can proceed, + // but the next deltacast start won't begin until after the delay. + await new Promise(r => setTimeout(r, DELTACAST_STAGGER_MS)); + } finally { + release(); + } + return; + } + + // Non-deltacast path: no stagger needed. const createRes = await dockerApi('POST', '/containers/create', spec); if (createRes.status !== 201) { return jsonResponse(res, 502, { error: 'Failed to create container', details: createRes.data });