diff --git a/services/capture/src/capture-manager.js b/services/capture/src/capture-manager.js index 0a806df..006e8c7 100644 --- a/services/capture/src/capture-manager.js +++ b/services/capture/src/capture-manager.js @@ -366,13 +366,18 @@ const CONTAINER_EXT = { // essence is a raw H.264 stream (-f h264) wrapped by raw2bmx --avci100_1080p // at -f 60000/1001, clip type op1a. Verified on-node: produces a valid // "h264 / High 4:2:2 Intra / yuv422p10le / 60000/1001" MXF. -const GROWING_VIDEO_ELEMENTARY_ARGS = [ - '-c:v', 'libx264', '-profile:v', 'high422', '-level', '4.2', - '-preset', 'ultrafast', '-tune', 'zerolatency', - '-pix_fmt', 'yuv422p10le', - '-x264-params', 'avcintra-class=100:bframes=0:keyint=1:scenecut=0', - '-aud', '1', -]; +// AVC-Intra elementary encode args per class (50 / 100 / 200). +const GROWING_AVCI_CLASS = { avci50: 50, avci100: 100, avci200: 200 }; +function growingVideoElementaryArgs(codec) { + const cls = GROWING_AVCI_CLASS[codec] || 100; + return [ + '-c:v', 'libx264', '-profile:v', 'high422', '-level', '4.2', + '-preset', 'ultrafast', '-tune', 'zerolatency', + '-pix_fmt', 'yuv422p10le', + '-x264-params', `avcintra-class=${cls}:bframes=0:keyint=1:scenecut=0`, + '-aud', '1', + ]; +} const GROWING_DEFAULT_BITRATE = '25M'; const GROWING_EXT = 'mxf'; // Video essence partition interval (frames). raw2bmx starts a new body partition @@ -386,7 +391,15 @@ const GROWING_PART_INTERVAL_FRAMES = 30; // 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'); +// Valid growing codec values: 'avci50' | 'avci100' | 'avci200' | 'hevc_nvenc' +const GROWING_AVC_CODECS = new Set(['avci50', 'avci100', 'avci200']); +const growingCodec = () => { + const v = process.env.GROWING_CODEC; + if (v === 'hevc_nvenc') return 'hevc_nvenc'; + if (v === 'avci50') return 'avci50'; + if (v === 'avci200') return 'avci200'; + return 'avci100'; // default +}; // 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). @@ -406,7 +419,7 @@ const GROWING_HEVC_DEFAULT_BITRATE = '80M'; // a sensible value from the recorder's configured resolution/framerate; if those // are absent or ambiguous it defaults to 1080i59.94. A live DeckLink confirm of // the actual SDI raster/fps is advised before production use (see report). -function deriveGrowingRaster(resolution, framerate, scanHint = null) { +function deriveGrowingRaster(resolution, framerate, scanHint = null, codec = 'avci100') { // Normalise fps. Accept '59.94', '60000/1001', '25', '50', '30', '29.97'… let fpsNum = null; const fr = (framerate == null) ? '' : String(framerate).trim(); @@ -451,12 +464,13 @@ function deriveGrowingRaster(resolution, framerate, scanHint = null) { if (scan == null) scan = scanHint || ((height >= 1080) ? 'i' : 'p'); const r = rates(fpsNum); - // AVC-Intra 100 raster flags. --avci100_1080p accepts true 1080p59.94 (verified). + // AVC-Intra raster flags — class from codec name ('avci50'/'avci100'/'avci200'). + const avciClass = GROWING_AVCI_CLASS[codec] || 100; let rawFlag; if (height >= 1080) { - rawFlag = (scan === 'i') ? '--avci100_1080i' : '--avci100_1080p'; + rawFlag = (scan === 'i') ? `--avci${avciClass}_1080i` : `--avci${avciClass}_1080p`; } else if (height >= 720) { - rawFlag = '--avci100_720p'; + rawFlag = `--avci${avciClass}_720p`; if (fpsNum == null) { r.ff = '60000/1001'; r.raw = '60000/1001'; } } else { rawFlag = '--mpeg2lg_422p_ml_576i'; @@ -903,8 +917,8 @@ class CaptureManager { 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'); + _buildGrowingOrchestrator({ inputArgs, videoBitrate, resolution, framerate, audioChannels, outPath, hlsDir, videoCodec, growingCodecName = 'avci100', audioMap = '0:a:0?', interlaced = false }) { + const { rawFlag, frameRate, ffRate } = deriveGrowingRaster(resolution, framerate, interlaced ? 'i' : 'p', growingCodecName); const vb = videoBitrate || GROWING_DEFAULT_BITRATE; const ach = audioChannels ? Number(audioChannels) : 2; @@ -927,7 +941,7 @@ class CaptureManager { '-filter_complex', filterComplex, // (a) MPEG-2 422 elementary video → "$VF" '-map', '[vhi]', - ...GROWING_VIDEO_ELEMENTARY_ARGS, + ...growingVideoElementaryArgs(growingCodecName), '-b:v', vb, '-minrate', vb, '-maxrate', vb, '-bufsize', vb, '-r', ffRate, '-f', 'h264', '@VF@', @@ -1277,6 +1291,7 @@ exit "$BMXRC" outPath: growingPath, hlsDir: (sourceType === 'sdi' || sourceType === 'deltacast') ? sdiHlsDir : null, videoCodec, + growingCodecName: gCodec, audioMap, interlaced: isInterlacedSource, }); diff --git a/services/mam-api/src/routes/recorders.js b/services/mam-api/src/routes/recorders.js index 60ea7a7..2eb0417 100644 --- a/services/mam-api/src/routes/recorders.js +++ b/services/mam-api/src/routes/recorders.js @@ -820,7 +820,7 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => { `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_CODEC=${['avci50','avci100','avci200','hevc_nvenc'].includes(recorder.growing_codec) ? recorder.growing_codec : '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 @@ -918,7 +918,7 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => { growing_smb_username: growingInfra.growing_smb_username || '', growing_smb_password: growingInfra.growing_smb_password || '', growing_smb_vers: growingInfra.growing_smb_vers || '3.0', - growing_codec: recorder.growing_codec === 'hevc_nvenc' ? 'hevc_nvenc' : 'avci100', + growing_codec: ['avci50','avci100','avci200','hevc_nvenc'].includes(recorder.growing_codec) ? recorder.growing_codec : 'avci100', }; const captureRes = await fetch(captureStartUrl, { method: 'POST', diff --git a/services/web-ui/public/screens-ingest.jsx b/services/web-ui/public/screens-ingest.jsx index 46c18e5..e102ee8 100644 --- a/services/web-ui/public/screens-ingest.jsx +++ b/services/web-ui/public/screens-ingest.jsx @@ -619,7 +619,8 @@ function RecorderConfigModal({ recorder, onClose, onSaved }) { const _seedBitrate = (recorder.recording_video_bitrate || (recorder.growing_enabled === true ? '50' : '25')).replace(/M$/i, ''); const [bitrate, setBitrate] = React.useState(_seedBitrate); const [growing, setGrowing] = React.useState(recorder.growing_enabled === true); - const [growingCodec, setGrowingCodec] = React.useState(recorder.growing_codec === 'hevc_nvenc' ? 'hevc_nvenc' : 'avci100'); + const _validGC = new Set(['avci50','avci100','avci200','hevc_nvenc']); + const [growingCodec, setGrowingCodec] = React.useState(_validGC.has(recorder.growing_codec) ? recorder.growing_codec : 'avci100'); const [projectId, setProjectId] = React.useState(recorder.project_id || PROJECTS[0]?.id || ''); const [saving, setSaving] = React.useState(false); const [err, setErr] = React.useState(null); @@ -743,13 +744,19 @@ function RecorderConfigModal({ recorder, onClose, onSaved }) {