feat: AVC-Intra 50/100/200 growing codec selector
This commit is contained in:
parent
071e1f6461
commit
c8cddc19b2
3 changed files with 49 additions and 24 deletions
|
|
@ -366,13 +366,18 @@ const CONTAINER_EXT = {
|
||||||
// essence is a raw H.264 stream (-f h264) wrapped by raw2bmx --avci100_1080p
|
// 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
|
// at -f 60000/1001, clip type op1a. Verified on-node: produces a valid
|
||||||
// "h264 / High 4:2:2 Intra / yuv422p10le / 60000/1001" MXF.
|
// "h264 / High 4:2:2 Intra / yuv422p10le / 60000/1001" MXF.
|
||||||
const GROWING_VIDEO_ELEMENTARY_ARGS = [
|
// AVC-Intra elementary encode args per class (50 / 100 / 200).
|
||||||
'-c:v', 'libx264', '-profile:v', 'high422', '-level', '4.2',
|
const GROWING_AVCI_CLASS = { avci50: 50, avci100: 100, avci200: 200 };
|
||||||
'-preset', 'ultrafast', '-tune', 'zerolatency',
|
function growingVideoElementaryArgs(codec) {
|
||||||
'-pix_fmt', 'yuv422p10le',
|
const cls = GROWING_AVCI_CLASS[codec] || 100;
|
||||||
'-x264-params', 'avcintra-class=100:bframes=0:keyint=1:scenecut=0',
|
return [
|
||||||
'-aud', '1',
|
'-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_DEFAULT_BITRATE = '25M';
|
||||||
const GROWING_EXT = 'mxf';
|
const GROWING_EXT = 'mxf';
|
||||||
// Video essence partition interval (frames). raw2bmx starts a new body partition
|
// 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).
|
// raw2bmx. True-1080p59.94 mastering codec (default).
|
||||||
// 'hevc_nvenc' -> all-intra HEVC (NVENC GPU, 4:2:0 10-bit) in fragmented MOV.
|
// 'hevc_nvenc' -> all-intra HEVC (NVENC GPU, 4:2:0 10-bit) in fragmented MOV.
|
||||||
// GPU-offloaded; frees CPU. Lower chroma, .mov not .mxf.
|
// 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.
|
// File extension per growing codec.
|
||||||
const growingExtFor = (codec) => (codec === 'hevc_nvenc' ? 'mov' : 'mxf');
|
const growingExtFor = (codec) => (codec === 'hevc_nvenc' ? 'mov' : 'mxf');
|
||||||
// Default bitrate for the HEVC-NVENC growing master (all-intra 10-bit is heavy).
|
// 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
|
// 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
|
// 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).
|
// 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'…
|
// Normalise fps. Accept '59.94', '60000/1001', '25', '50', '30', '29.97'…
|
||||||
let fpsNum = null;
|
let fpsNum = null;
|
||||||
const fr = (framerate == null) ? '' : String(framerate).trim();
|
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');
|
if (scan == null) scan = scanHint || ((height >= 1080) ? 'i' : 'p');
|
||||||
const r = rates(fpsNum);
|
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;
|
let rawFlag;
|
||||||
if (height >= 1080) {
|
if (height >= 1080) {
|
||||||
rawFlag = (scan === 'i') ? '--avci100_1080i' : '--avci100_1080p';
|
rawFlag = (scan === 'i') ? `--avci${avciClass}_1080i` : `--avci${avciClass}_1080p`;
|
||||||
} else if (height >= 720) {
|
} else if (height >= 720) {
|
||||||
rawFlag = '--avci100_720p';
|
rawFlag = `--avci${avciClass}_720p`;
|
||||||
if (fpsNum == null) { r.ff = '60000/1001'; r.raw = '60000/1001'; }
|
if (fpsNum == null) { r.ff = '60000/1001'; r.raw = '60000/1001'; }
|
||||||
} else {
|
} else {
|
||||||
rawFlag = '--mpeg2lg_422p_ml_576i';
|
rawFlag = '--mpeg2lg_422p_ml_576i';
|
||||||
|
|
@ -903,8 +917,8 @@ class CaptureManager {
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildGrowingOrchestrator({ inputArgs, videoBitrate, resolution, framerate, audioChannels, outPath, hlsDir, videoCodec, audioMap = '0:a:0?', interlaced = false }) {
|
_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');
|
const { rawFlag, frameRate, ffRate } = deriveGrowingRaster(resolution, framerate, interlaced ? 'i' : 'p', growingCodecName);
|
||||||
const vb = videoBitrate || GROWING_DEFAULT_BITRATE;
|
const vb = videoBitrate || GROWING_DEFAULT_BITRATE;
|
||||||
const ach = audioChannels ? Number(audioChannels) : 2;
|
const ach = audioChannels ? Number(audioChannels) : 2;
|
||||||
|
|
||||||
|
|
@ -927,7 +941,7 @@ class CaptureManager {
|
||||||
'-filter_complex', filterComplex,
|
'-filter_complex', filterComplex,
|
||||||
// (a) MPEG-2 422 elementary video → "$VF"
|
// (a) MPEG-2 422 elementary video → "$VF"
|
||||||
'-map', '[vhi]',
|
'-map', '[vhi]',
|
||||||
...GROWING_VIDEO_ELEMENTARY_ARGS,
|
...growingVideoElementaryArgs(growingCodecName),
|
||||||
'-b:v', vb, '-minrate', vb, '-maxrate', vb, '-bufsize', vb,
|
'-b:v', vb, '-minrate', vb, '-maxrate', vb, '-bufsize', vb,
|
||||||
'-r', ffRate,
|
'-r', ffRate,
|
||||||
'-f', 'h264', '@VF@',
|
'-f', 'h264', '@VF@',
|
||||||
|
|
@ -1277,6 +1291,7 @@ exit "$BMXRC"
|
||||||
outPath: growingPath,
|
outPath: growingPath,
|
||||||
hlsDir: (sourceType === 'sdi' || sourceType === 'deltacast') ? sdiHlsDir : null,
|
hlsDir: (sourceType === 'sdi' || sourceType === 'deltacast') ? sdiHlsDir : null,
|
||||||
videoCodec,
|
videoCodec,
|
||||||
|
growingCodecName: gCodec,
|
||||||
audioMap,
|
audioMap,
|
||||||
interlaced: isInterlacedSource,
|
interlaced: isInterlacedSource,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -820,7 +820,7 @@ router.post('/:id/start', requireRecorderEdit, async (req, res, next) => {
|
||||||
`GROWING_ENABLED=${growingEnabled ? 'true' : 'false'}`,
|
`GROWING_ENABLED=${growingEnabled ? 'true' : 'false'}`,
|
||||||
// Growing codec: 'avci100' (CPU AVC-Intra 100 MXF, default) or
|
// Growing codec: 'avci100' (CPU AVC-Intra 100 MXF, default) or
|
||||||
// 'hevc_nvenc' (GPU all-intra HEVC frag-MOV). capture-manager reads this.
|
// '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`,
|
`GROWING_PATH=/growing`,
|
||||||
// SMB mount details for the in-container CIFS mount (Approach A). Empty
|
// SMB mount details for the in-container CIFS mount (Approach A). Empty
|
||||||
// GROWING_SMB_MOUNT → capture falls back to the host-bound /growing volume
|
// 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_username: growingInfra.growing_smb_username || '',
|
||||||
growing_smb_password: growingInfra.growing_smb_password || '',
|
growing_smb_password: growingInfra.growing_smb_password || '',
|
||||||
growing_smb_vers: growingInfra.growing_smb_vers || '3.0',
|
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, {
|
const captureRes = await fetch(captureStartUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
|
||||||
|
|
@ -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 _seedBitrate = (recorder.recording_video_bitrate || (recorder.growing_enabled === true ? '50' : '25')).replace(/M$/i, '');
|
||||||
const [bitrate, setBitrate] = React.useState(_seedBitrate);
|
const [bitrate, setBitrate] = React.useState(_seedBitrate);
|
||||||
const [growing, setGrowing] = React.useState(recorder.growing_enabled === true);
|
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 [projectId, setProjectId] = React.useState(recorder.project_id || PROJECTS[0]?.id || '');
|
||||||
const [saving, setSaving] = React.useState(false);
|
const [saving, setSaving] = React.useState(false);
|
||||||
const [err, setErr] = React.useState(null);
|
const [err, setErr] = React.useState(null);
|
||||||
|
|
@ -743,13 +744,19 @@ function RecorderConfigModal({ recorder, onClose, onSaved }) {
|
||||||
<select className="field-input" value={growingCodec}
|
<select className="field-input" value={growingCodec}
|
||||||
onChange={e => setGrowingCodec(e.target.value)} disabled={isRec}
|
onChange={e => setGrowingCodec(e.target.value)} disabled={isRec}
|
||||||
style={{ appearance: 'auto' }}>
|
style={{ appearance: 'auto' }}>
|
||||||
<option value="avci100">AVC-Intra 100 (CPU, MXF · Premiere-native)</option>
|
<option value="avci50">AVC-Intra 50 (CPU, MXF · ~100 Mbps @ 59.94)</option>
|
||||||
<option value="hevc_nvenc">HEVC all-intra (GPU/NVENC, frag-MOV)</option>
|
<option value="avci100">AVC-Intra 100 (CPU, MXF · ~200 Mbps @ 59.94)</option>
|
||||||
|
<option value="avci200">AVC-Intra 200 (CPU, MXF · ~400 Mbps @ 59.94)</option>
|
||||||
|
<option value="hevc_nvenc">HEVC all-intra (GPU/NVENC, frag-MOV · experimental)</option>
|
||||||
</select>
|
</select>
|
||||||
<div className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 4 }}>
|
<div className="mono" style={{ fontSize: 10.5, color: 'var(--text-3)', marginTop: 4 }}>
|
||||||
{growingCodec === 'hevc_nvenc'
|
{growingCodec === 'hevc_nvenc'
|
||||||
? 'GPU-offloaded HEVC 10-bit 4:2:0 in fragmented MOV. Frees CPU. NOTE: not all editors import frag-MOV growing files.'
|
? 'GPU-offloaded HEVC 10-bit 4:2:0 in fragmented MOV. NOTE: Premiere does not import frag-MOV growing files.'
|
||||||
: 'CPU AVC-Intra 100, 4:2:2 10-bit, true 1080p59.94 in MXF OP1a. Premiere-native growing.'}
|
: growingCodec === 'avci50'
|
||||||
|
? 'AVC-Intra Class 50 — ~100 Mbps @ 1080p59.94. Lowest storage, broadcast-grade 4:2:2 10-bit. Premiere-native.'
|
||||||
|
: growingCodec === 'avci200'
|
||||||
|
? 'AVC-Intra Class 200 — ~400 Mbps @ 1080p59.94. Highest quality, 4:2:2 10-bit. Premiere-native.'
|
||||||
|
: 'AVC-Intra Class 100 — ~200 Mbps @ 1080p59.94. Balanced quality and storage. Premiere-native.'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="field">
|
<div className="field">
|
||||||
|
|
@ -817,7 +824,7 @@ function _normRecorder(r) {
|
||||||
res: r.recording_resolution || '·',
|
res: r.recording_resolution || '·',
|
||||||
framerate: r.recording_framerate || 'native',
|
framerate: r.recording_framerate || 'native',
|
||||||
growing: r.growing_enabled === true,
|
growing: r.growing_enabled === true,
|
||||||
growingCodec: r.growing_codec === 'hevc_nvenc' ? 'hevc_nvenc' : 'avci100',
|
growingCodec: (['avci50','avci100','avci200','hevc_nvenc'].includes(r.growing_codec) ? r.growing_codec : 'avci100'),
|
||||||
nodeId: r.node_id || null,
|
nodeId: r.node_id || null,
|
||||||
node: r.node_id ? r.node_id.slice(0, 8) : 'primary',
|
node: r.node_id ? r.node_id.slice(0, 8) : 'primary',
|
||||||
deviceIndex: portIdx ?? null,
|
deviceIndex: portIdx ?? null,
|
||||||
|
|
@ -1130,7 +1137,10 @@ function RecorderRow({ recorder: initialRecorder, onRefresh, onConfigure, nodeOn
|
||||||
<span className="badge accent" title={recorder.growingCodec === 'hevc_nvenc'
|
<span className="badge accent" title={recorder.growingCodec === 'hevc_nvenc'
|
||||||
? 'Growing-file · HEVC all-intra (NVENC GPU, fragmented MOV)'
|
? 'Growing-file · HEVC all-intra (NVENC GPU, fragmented MOV)'
|
||||||
: 'Growing-file · AVC-Intra 100 (CPU, MXF OP1a)'}>
|
: 'Growing-file · AVC-Intra 100 (CPU, MXF OP1a)'}>
|
||||||
GROWING · {recorder.growingCodec === 'hevc_nvenc' ? 'GPU/HEVC' : 'CPU/AVCI'}
|
GROWING · {recorder.growingCodec === 'hevc_nvenc' ? 'GPU/HEVC'
|
||||||
|
: recorder.growingCodec === 'avci50' ? 'CPU/AVCI-50'
|
||||||
|
: recorder.growingCodec === 'avci200' ? 'CPU/AVCI-200'
|
||||||
|
: 'CPU/AVCI-100'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue