From 9210b415894d1e76abd54ba7499370c6c152ed93 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Fri, 29 May 2026 13:23:44 -0400 Subject: [PATCH] fix(capture): working All-Intra HEVC NVENC config (validated on L4) NVENC rejects -g 1 ("GopLength > numBFrames+1" at InitializeEncoder), so true all-intra is achieved with -bf 0 -forced-idr 1 -g 600 plus -force_key_frames expr:1 (forces an IDR on every frame). ffprobe confirms all frames are pict_type=I. Container is fragmented MOV; MXF muxing of HEVC fails on this ffmpeg build ("Operation not permitted"). Validated end-to-end via direct ffmpeg on zampp2/L4: hevc, Main 10, 1920x1080, yuv420p10le, ptypes=[IIIIII...] Co-Authored-By: Claude Opus 4.8 --- services/capture/src/capture-manager.js | 29 ++++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/services/capture/src/capture-manager.js b/services/capture/src/capture-manager.js index f1bc2f1..5f1315f 100644 --- a/services/capture/src/capture-manager.js +++ b/services/capture/src/capture-manager.js @@ -29,15 +29,28 @@ const VIDEO_CODECS = { h264_nvenc: { args: ['-c:v', 'h264_nvenc', '-preset', 'p5'], bitrateControl: true, pixFmt: 'yuv420p' }, h265: { args: ['-c:v', 'libx265', '-preset', 'medium'], bitrateControl: true, pixFmt: 'yuv420p' }, // All-Intra HEVC on NVENC — the growing-file master codec. - // -g 1 -bf 0: every frame is an IDR (all-intra), no B-frames. - // Required for growing-file edit-while-record (partial file must be - // decodable to the last complete frame without a full GOP). - // -rc vbr: variable bitrate; pair with a high target bitrate (100–160 Mbps - // for 1080i) to rival ProRes HQ quality. - // -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 needed, use prores_hq. + // Goal: every frame an IDR (all-intra), so a still-growing file is decodable + // to its last complete frame — the prerequisite for edit-while-record. + // + // NVENC will NOT accept `-g 1`: InitializeEncoder enforces + // "GopLength > numBFrames + 1", so with -bf 0 the minimum GOP is 2 and -g 1 + // is rejected with EINVAL (validated on the L4, driver 595). The working + // recipe for true all-intra is therefore: + // -bf 0 no B-frames + // -g 600 large GOP just to satisfy the init check + // -forced-idr 1 forced keyframes are emitted as IDR + // -force_key_frames expr:1 force a keyframe on EVERY frame + // → ffprobe confirms pict_type = I for all frames. + // + // Container: fragmented MOV (+frag_keyframe+empty_moov+default_base_moof), + // NOT MXF — this ffmpeg's MXF muxer rejects HEVC ("Operation not permitted"). + // The frag-MOV index is not deferred to EOF, so the file stays readable while + // growing. (See docs/design/2026-05-29-all-intra-hevc-ingest.md §8.) + // + // -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). hevc_nvenc: { - args: ['-c:v', 'hevc_nvenc', '-preset', 'p4', '-rc', 'vbr', '-g', '1', '-bf', '0', '-profile:v', 'main10'], + 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, pixFmt: 'p010le', },