fix(capture): pin NVENC to a GPU with ffmpeg -gpu N (privileged bypasses env)

NVIDIA_VISIBLE_DEVICES=1 was set but the sidecar still SAW /dev/nvidia0,1,2 and nvenc used GPU 0 — because capture sidecars run Privileged, which exposes every GPU device node regardless of NVIDIA_VISIBLE_DEVICES/DeviceRequests. Real fix: node-agent passes CAPTURE_GPU_INDEX to the sidecar and capture-manager adds ffmpeg '-gpu N' to the hevc_nvenc + h264_nvenc encoders, so each port's master+HLS encode is explicitly bound to its assigned L4. Spreads 8 ports across 3 cards.
This commit is contained in:
Zac Gaetano 2026-06-04 16:07:59 +00:00
parent 15fab99d55
commit 80f157968f
2 changed files with 23 additions and 2 deletions

View file

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

View file

@ -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}`);