feat(growing): add GPU-offload HEVC-NVENC frag-MOV growing codec
Second selectable growing path alongside AVC-Intra 100. GROWING_CODEC env (per-recorder growing_codec field) picks: avci100 (CPU 4:2:2 MXF, default) or hevc_nvenc (GPU all-intra HEVC 10-bit 4:2:0 fragmented MOV). The hevc path runs a single ffmpeg (no raw2bmx/FIFO) writing frag-MOV directly; +empty_moov makes it grow with readable duration mid-write. Proven live on zampp3: size+duration advance monotonically, finalized file decodes RC=0 (hevc Main10 1080p59.94).
This commit is contained in:
parent
967547ae97
commit
a031ff1c9e
2 changed files with 99 additions and 3 deletions
|
|
@ -380,6 +380,18 @@ const GROWING_EXT = 'mxf';
|
|||
// which the growing file's recorded duration advances. ~1s at 25/29.97 fps.
|
||||
const GROWING_PART_INTERVAL_FRAMES = 30;
|
||||
|
||||
// Growing-file codec selector. Read FRESH from env at record time (standby
|
||||
// sidecars boot with it unset and receive it per-session via /capture/start).
|
||||
// 'avci100' -> AVC-Intra 100 (CPU libx264, 4:2:2 10-bit) in MXF OP1a via
|
||||
// raw2bmx. True-1080p59.94 mastering codec (default).
|
||||
// 'hevc_nvenc' -> all-intra HEVC (NVENC GPU, 4:2:0 10-bit) in fragmented MOV.
|
||||
// GPU-offloaded; frees CPU. Lower chroma, .mov not .mxf.
|
||||
const growingCodec = () => (process.env.GROWING_CODEC === 'hevc_nvenc' ? 'hevc_nvenc' : 'avci100');
|
||||
// File extension per growing codec.
|
||||
const growingExtFor = (codec) => (codec === 'hevc_nvenc' ? 'mov' : 'mxf');
|
||||
// Default bitrate for the HEVC-NVENC growing master (all-intra 10-bit is heavy).
|
||||
const GROWING_HEVC_DEFAULT_BITRATE = '80M';
|
||||
|
||||
// Map the recorder's resolution/fps to (1) the raw2bmx MPEG-2 Long GOP essence
|
||||
// input flag and (2) the ffmpeg edit-rate (`-r`). raw2bmx needs the correct
|
||||
// raster flag so the essence is wrapped as the right XDCAM HD422 variant; an
|
||||
|
|
@ -840,6 +852,57 @@ class CaptureManager {
|
|||
*
|
||||
* Returns the argv for spawn('bash', argv).
|
||||
*/
|
||||
|
||||
/**
|
||||
* Build the single-ffmpeg argv for a GPU-OFFLOADED growing master:
|
||||
* all-intra HEVC (NVENC, 10-bit 4:2:0) in a fragmented MOV.
|
||||
*
|
||||
* Unlike the AVC-Intra/raw2bmx path, this needs NO FIFO orchestrator and NO
|
||||
* raw2bmx: ffmpeg writes the growing fragmented-MOV directly to the share.
|
||||
* +empty_moov writes a valid moov up-front and +frag_keyframe flushes a moof
|
||||
* fragment per keyframe, so the file is readable (and its duration advances)
|
||||
* while still growing. force_key_frames expr:1 makes every frame an IDR
|
||||
* (all-intra) so the growing head is always decodable to the last COMPLETE
|
||||
* fragment. PROVEN live on zampp3: size + ffprobe duration grow monotonically
|
||||
* mid-write; finalized file decodes RC=0 (hevc Main10 yuv420p10le 1080p59.94).
|
||||
*
|
||||
* GPU pinning via nvencGpuSel() (privileged sidecars see every /dev/nvidiaN,
|
||||
* so -gpu N is the only reliable NVENC pin). Returns ffmpeg argv (no bash).
|
||||
*/
|
||||
_buildGrowingHevcMov({ inputArgs, videoBitrate, framerate, audioChannels, outPath, audioMap = '0:a:0?', hlsDir = null, videoCodec = 'hevc_nvenc', interlaced = false }) {
|
||||
const vb = videoBitrate || GROWING_HEVC_DEFAULT_BITRATE;
|
||||
const ach = audioChannels ? Number(audioChannels) : 2;
|
||||
const args = ['-y', '-hide_banner', '-loglevel', 'warning', '-stats', ...inputArgs];
|
||||
|
||||
// Deinterlace (SDI) then split: master HEVC + optional HLS preview tap.
|
||||
const filterComplex = hlsDir
|
||||
? (interlaced ? '[0:v]yadif=mode=1:deint=1,split=2[vhi][vlo]' : '[0:v]split=2[vhi][vlo]')
|
||||
: (interlaced ? '[0:v]yadif=mode=1:deint=1,split=1[vhi]' : '[0:v]split=1[vhi]');
|
||||
args.push('-filter_complex', filterComplex);
|
||||
|
||||
// (a) GPU all-intra HEVC 10-bit master -> fragmented MOV at outPath.
|
||||
args.push('-map', '[vhi]',
|
||||
'-c:v', 'hevc_nvenc', ...nvencGpuSel(),
|
||||
'-preset', 'p4', '-rc', 'vbr', '-profile:v', 'main10', '-pix_fmt', 'p010le',
|
||||
'-bf', '0', '-forced-idr', '1', '-g', '600', '-force_key_frames', 'expr:1',
|
||||
'-b:v', vb,
|
||||
'-map', audioMap, '-c:a', 'aac', '-b:a', '256k', '-ar', '48000', '-ac', String(ach),
|
||||
'-movflags', '+frag_keyframe+empty_moov+default_base_moof',
|
||||
'-f', 'mov', outPath);
|
||||
|
||||
// (b) optional H.264 HLS preview (unchanged behaviour) -> second output.
|
||||
if (hlsDir) {
|
||||
args.push('-map', '[vlo]', '-map', audioMap,
|
||||
...buildHlsVideoArgs(videoCodec, framerate),
|
||||
'-c:a', 'aac', '-b:a', '128k', '-ar', '44100',
|
||||
'-f', 'hls', '-hls_time', '2', '-hls_list_size', '15',
|
||||
'-hls_flags', 'delete_segments+append_list+omit_endlist',
|
||||
'-hls_segment_filename', `${hlsDir}/seg-%05d.ts`,
|
||||
`${hlsDir}/index.m3u8`);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
_buildGrowingOrchestrator({ inputArgs, videoBitrate, resolution, framerate, audioChannels, outPath, hlsDir, videoCodec, audioMap = '0:a:0?', interlaced = false }) {
|
||||
const { rawFlag, frameRate, ffRate } = deriveGrowingRaster(resolution, framerate, interlaced ? 'i' : 'p');
|
||||
const vb = videoBitrate || GROWING_DEFAULT_BITRATE;
|
||||
|
|
@ -1034,8 +1097,10 @@ exit "$BMXRC"
|
|||
// format Premiere reads while growing — see GROWING_VIDEO_ELEMENTARY_ARGS /
|
||||
// _buildGrowingOrchestrator), regardless of the recorder's configured
|
||||
// container — so it gets a .mxf extension, not the container's.
|
||||
const _growCodec = growingActive ? growingCodec() : null;
|
||||
const _growExt = _growCodec ? growingExtFor(_growCodec) : GROWING_EXT;
|
||||
const growingPath = growingActive
|
||||
? `${GROWING_PATH}/${projectId}/${clipName}.${GROWING_EXT}`
|
||||
? `${GROWING_PATH}/${projectId}/${clipName}.${_growExt}`
|
||||
: null;
|
||||
|
||||
// hiresKey MUST match the actual master format/destination:
|
||||
|
|
@ -1044,7 +1109,7 @@ exit "$BMXRC"
|
|||
// (A stale .mov key here would make the proxy job download a nonexistent
|
||||
// object → "unable to open the file on disk".)
|
||||
// - growing fell back to S3 → the normal container extension.
|
||||
const hiresExt = growingPath ? GROWING_EXT : (CONTAINER_EXT[container] || 'mov');
|
||||
const hiresExt = growingPath ? _growExt : (CONTAINER_EXT[container] || 'mov');
|
||||
const hiresKey = `projects/${projectId}/masters/${clipName}.${hiresExt}`;
|
||||
if (growingPath) {
|
||||
try { mkdirSync(dirname(growingPath), { recursive: true }); }
|
||||
|
|
@ -1165,7 +1230,35 @@ exit "$BMXRC"
|
|||
}
|
||||
|
||||
let hiresProcess;
|
||||
if (growingPath) {
|
||||
if (growingPath && _growCodec === 'hevc_nvenc') {
|
||||
// ── GPU-OFFLOAD GROWING master: HEVC NVENC -> fragmented MOV ──
|
||||
// Single ffmpeg, NO raw2bmx / NO FIFO orchestrator. Video from fc_pipe
|
||||
// stdin (pipe:0) like the non-growing master; frag-MOV grows on disk and
|
||||
// stays decodable to the last complete fragment. Proven live on zampp3.
|
||||
const hevcArgs = this._buildGrowingHevcMov({
|
||||
inputArgs, videoBitrate, framerate, audioChannels,
|
||||
outPath: growingPath, audioMap,
|
||||
hlsDir: (sourceType === 'sdi' || sourceType === 'deltacast') ? sdiHlsDir : null,
|
||||
videoCodec, interlaced: isInterlacedSource,
|
||||
});
|
||||
console.log('[capture] growing master via HEVC-NVENC frag-MOV; args=' + hevcArgs.length);
|
||||
hiresProcess = spawn('ffmpeg', hevcArgs, {
|
||||
stdio: bridgeProcess ? ['pipe', 'ignore', 'pipe'] : ['ignore', 'ignore', 'pipe'],
|
||||
detached: true,
|
||||
});
|
||||
if (bridgeProcess && bridgeProcess.stdout && hiresProcess.stdin) {
|
||||
hiresProcess.stdin.on('error', (e) => {
|
||||
if (e && e.code !== 'EPIPE') console.warn(`[capture] hevc growing stdin error: ${e.message}`);
|
||||
});
|
||||
bridgeProcess.stdout.on('error', (e) => {
|
||||
console.warn(`[capture] fc_pipe stdout error: ${e && e.message}`);
|
||||
});
|
||||
bridgeProcess.stdout.pipe(hiresProcess.stdin);
|
||||
bridgeProcess.on('exit', () => {
|
||||
try { if (hiresProcess.stdin) hiresProcess.stdin.end(); } catch (_) {}
|
||||
});
|
||||
}
|
||||
} else if (growingPath) {
|
||||
// ── GROWING master: raw2bmx orchestrator ──────────────────────────
|
||||
// One ffmpeg (single SDI read) → MPEG-2 422 elementary + PCM to FIFOs +
|
||||
// the H.264 HLS preview; raw2bmx muxes the growing OP1a MXF from the FIFOs.
|
||||
|
|
|
|||
|
|
@ -818,6 +818,9 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => {
|
|||
`ASSET_ID=${assetIdLive}`,
|
||||
`MAM_API_TOKEN=${process.env.CAPTURE_TOKEN || ''}`,
|
||||
`GROWING_ENABLED=${growingEnabled ? 'true' : 'false'}`,
|
||||
// Growing codec: 'avci100' (CPU AVC-Intra 100 MXF, default) or
|
||||
// 'hevc_nvenc' (GPU all-intra HEVC frag-MOV). capture-manager reads this.
|
||||
`GROWING_CODEC=${recorder.growing_codec === 'hevc_nvenc' ? 'hevc_nvenc' : 'avci100'}`,
|
||||
`GROWING_PATH=/growing`,
|
||||
// SMB mount details for the in-container CIFS mount (Approach A). Empty
|
||||
// GROWING_SMB_MOUNT → capture falls back to the host-bound /growing volume
|
||||
|
|
|
|||
Loading…
Reference in a new issue