diff --git a/services/capture/src/capture-manager.js b/services/capture/src/capture-manager.js index 7db5a6e..45e4bf2 100644 --- a/services/capture/src/capture-manager.js +++ b/services/capture/src/capture-manager.js @@ -181,8 +181,24 @@ function parseFps(framerate, fallback = 60) { return Number.isFinite(f) && f > 0 ? f : fallback; } +// Which physical GPU this sidecar's NVENC encodes should use. node-agent +// round-robins capture ports across the host's GPUs and passes the index here. +// We MUST select it explicitly with ffmpeg's `-gpu N` because the capture +// sidecars run Privileged (so they see every /dev/nvidiaN regardless of +// NVIDIA_VISIBLE_DEVICES) — without -gpu, nvenc defaults every session to GPU 0 +// and all 8 ports pile onto one card → it falls below realtime → video freezes. +const CAPTURE_GPU_INDEX = (() => { + const v = process.env.CAPTURE_GPU_INDEX; + if (v == null || v === '' || v === 'all') return null; + const n = parseInt(v, 10); + return Number.isInteger(n) && n >= 0 ? n : null; +})(); +// `-gpu N` must come BEFORE the input/encoder is initialized; ffmpeg accepts it +// as an encoder option right after -c:v. Returns [] when no pin is configured. +const nvencGpuSel = () => (CAPTURE_GPU_INDEX != null ? ['-gpu', String(CAPTURE_GPU_INDEX)] : []); + function hevcNvencArgs(framerate, growing) { - const base = ['-c:v', 'hevc_nvenc', '-preset', 'p4', '-rc', 'vbr', '-profile:v', 'main10']; + const base = ['-c:v', 'hevc_nvenc', ...nvencGpuSel(), '-preset', 'p4', '-rc', 'vbr', '-profile:v', 'main10']; if (growing) { return [...base, '-bf', '0', '-forced-idr', '1', '-g', '600', '-force_key_frames', 'expr:1']; } @@ -232,7 +248,7 @@ function buildHlsVideoArgs(masterCodec, framerate) { // Low-latency NVENC preset (p1 + ll tune). forced-idr + a keyframe every GOP // frames keeps segment boundaries on IDR frames so hls.js can sync cleanly. return [ - '-c:v', 'h264_nvenc', '-preset', 'p1', '-tune', 'll', + '-c:v', 'h264_nvenc', ...nvencGpuSel(), '-preset', 'p1', '-tune', 'll', '-pix_fmt', 'yuv420p', '-b:v', '2M', '-g', String(gop), '-forced-idr', '1', '-sc_threshold', '0', ]; diff --git a/services/node-agent/index.js b/services/node-agent/index.js index 40ff530..51ff3ad 100644 --- a/services/node-agent/index.js +++ b/services/node-agent/index.js @@ -527,6 +527,10 @@ async function handleSidecarStart(body, res) { var startVisibleDevices = pickVisibleDevices(gpuUuid, capturePort); sidecarEnv.push(`NVIDIA_VISIBLE_DEVICES=${startVisibleDevices}`); sidecarEnv.push('NVIDIA_DRIVER_CAPABILITIES=video,compute,utility'); + // Privileged sidecars see every /dev/nvidiaN regardless of + // NVIDIA_VISIBLE_DEVICES, so also tell ffmpeg explicitly which GPU to + // encode on via CAPTURE_GPU_INDEX (capture-manager adds `-gpu N`). + if (startVisibleDevices !== 'all') sidecarEnv.push(`CAPTURE_GPU_INDEX=${startVisibleDevices}`); console.log(`[gpu] sidecar port ${capturePort} → NVIDIA_VISIBLE_DEVICES=${startVisibleDevices}`); } @@ -841,6 +845,7 @@ async function handleSidecarStandby(body, res) { standbyVisibleDevices = pickVisibleDevices(gpuUuid, capturePort); sidecarEnv.push(`NVIDIA_VISIBLE_DEVICES=${standbyVisibleDevices}`); sidecarEnv.push('NVIDIA_DRIVER_CAPABILITIES=video,compute,utility'); + if (standbyVisibleDevices !== 'all') sidecarEnv.push(`CAPTURE_GPU_INDEX=${standbyVisibleDevices}`); console.log(`[gpu] standby sidecar port ${capturePort} → NVIDIA_VISIBLE_DEVICES=${standbyVisibleDevices}`); } sidecarEnv.push(`FC_URL=${FC_URL}`);