diff --git a/services/capture/src/capture-manager.js b/services/capture/src/capture-manager.js index 67cee91..92220a6 100644 --- a/services/capture/src/capture-manager.js +++ b/services/capture/src/capture-manager.js @@ -134,6 +134,9 @@ const VIDEO_CODECS = { // // -profile:v main10 / p010le: 10-bit 4:2:0 — the closest NVENC HEVC can get // to ProRes 4:2:2 10-bit. If strict 4:2:2 is required, use prores_hq (CPU). + // GROWING-file variant: every frame an IDR (all-intra) so a still-growing + // file is decodable to its last complete frame. This is HEAVY — only used when + // growing-files is on (see hevcNvencArgs()). hevc_nvenc: { args: ['-c:v', 'hevc_nvenc', '-preset', 'p4', '-rc', 'vbr', '-bf', '0', '-forced-idr', '1', '-g', '600', '-force_key_frames', 'expr:1', '-profile:v', 'main10'], bitrateControl: true, @@ -141,6 +144,27 @@ const VIDEO_CODECS = { }, }; +// HEVC/NVENC encode args, GOP structure chosen by mode. +// growing=false (normal record): efficient long-GOP (2s @ fps) HEVC. NVENC +// easily sustains 1080p59.94 10-bit here, so no frame drops → audio/video +// lengths stay locked. This is the DEFAULT for recorders. +// growing=true (edit-while-record): ALL-INTRA (every frame an IDR) so the +// growing file is decodable to its last written frame — the requirement for +// Premiere's growing-file refresh. Much heavier, only used when needed. +// `force_key_frames expr:1` (all-intra) is the ~4× compute path that was +// crippling realtime when applied to every recording; gating it on `growing` +// is the fix for the dropped-frame A/V drift. +function hevcNvencArgs(framerate, growing) { + const base = ['-c:v', 'hevc_nvenc', '-preset', 'p4', '-rc', 'vbr', '-profile:v', 'main10']; + if (growing) { + return [...base, '-bf', '0', '-forced-idr', '1', '-g', '600', '-force_key_frames', 'expr:1']; + } + // Normal long-GOP: ~2s keyframe interval, 2 B-frames. Realtime-friendly. + const fps = Number.parseFloat(framerate) || 60; + const gop = Math.max(2, Math.round(fps * 2)); + return [...base, '-bf', '2', '-g', String(gop)]; +} + // nvenc codecs available in the capture image. Used both to validate the master // codec and (issue #164) as the GPU-availability signal for the HLS preview. const NVENC_CODECS = new Set(['h264_nvenc', 'hevc_nvenc']); @@ -479,7 +503,14 @@ function buildEncodeArgs({ const args = []; if (isNetwork) args.push('-map', '0:v:0?', '-map', '0:a:0?'); - args.push(...v.args); + // hevc_nvenc GOP structure is mode-dependent: all-intra only for growing + // files, efficient long-GOP for normal record (so NVENC stays realtime and + // doesn't drop frames). All other codecs use their static arg set. + if (codec === 'hevc_nvenc') { + args.push(...hevcNvencArgs(framerate, growing)); + } else { + args.push(...v.args); + } if (v.pixFmt) args.push('-pix_fmt', v.pixFmt); if (v.bitrateControl && videoBitrate) args.push('-b:v', videoBitrate); if (framerate && framerate !== 'native') args.push('-r', framerate);