fix(growing): AVC-Intra 100 1080p59.94 via libx264 (verified raw2bmx accepts)

Verified on-node: libx264 high422 10-bit + x264 avcintra-class=100 -> raw2bmx -t op1a --avci100_1080p -f 60000/1001 produces a valid MXF (ffprobe: h264 High 4:2:2 Intra yuv422p10le 60000/1001). True 1080p59.94, openable in Premiere. CPU encode for now per demo deadline; NVENC h264 cannot do 4:2:2.
This commit is contained in:
Zac Gaetano 2026-06-04 18:35:42 +00:00
parent e6b7856bb2
commit 4609517756

View file

@ -359,9 +359,15 @@ const CONTAINER_EXT = {
// bitrate (operator target, default 50 Mbps) match XDCAM HD422 essence. `-g 15`
// keeps a short GOP. Muxed to a raw `mpeg2video` elementary stream (no
// container) so raw2bmx ingests it via --mpeg2lg_*.
// CPU AVC-Intra 100 via libx264 — the proven-working growing essence for
// 1080p59.94 (NVENC h264 can't do 4:2:2; mpeg2 422 can't do 1080p59.94).
// raw2bmx wraps this via --avci100_1080p at 60000/1001.
const GROWING_VIDEO_ELEMENTARY_ARGS = [
'-c:v', 'mpeg2video', '-pix_fmt', 'yuv422p',
'-dc', '10', '-g', '15', '-bf', '2',
'-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',
];
const GROWING_DEFAULT_BITRATE = '25M';
const GROWING_EXT = 'mxf';
@ -411,36 +417,32 @@ function deriveGrowingRaster(resolution, framerate, scanHint = null) {
}
if (height == null) height = 1080; // default raster
// ffmpeg rate + raw2bmx rate strings for the common broadcast rates.
// CRITICAL: XDCAM HD422 (MPEG-2 422 Long GOP) at 1080 lines does NOT support
// 1080p59.94 — raw2bmx rejects MPEG-2 422 @ 60000/1001 outright. A 1080p59.94
// SDI feed is therefore wrapped as 1080i59.94 (= 30000/1001 frame rate), which
// is exactly what every previously-working growing file on the share used and
// what Premiere's edit-while-ingest path reads. So 59.94 maps to 30000/1001.
// ffmpeg rate + raw2bmx rate strings. AVC-Intra 100 DOES support true
// 1080p59.94 (unlike XDCAM HD422 / MPEG-2 422 which raw2bmx rejects at 60000/1001),
// so a 1080p59.94 SDI feed is wrapped progressively at its native 60000/1001.
function rates(fps) {
if (fps == null) return { ff: '30000/1001', raw: '30000/1001' }; // 1080i59.94 default
if (Math.abs(fps - 59.94) < 0.2 || Math.abs(fps - 29.97) < 0.05)
return { ff: '30000/1001', raw: '30000/1001' };
if (fps == null) return { ff: '60000/1001', raw: '60000/1001' };
if (Math.abs(fps - 59.94) < 0.2) return { ff: '60000/1001', raw: '60000/1001' };
if (Math.abs(fps - 29.97) < 0.05) return { ff: '30000/1001', raw: '30000/1001' };
if (Math.abs(fps - 60) < 0.05) return { ff: '60', raw: '60' };
if (Math.abs(fps - 50) < 0.05) return { ff: '25', raw: '25' }; // 1080i50 → 25 fps frames
if (Math.abs(fps - 50) < 0.05) return { ff: '50', raw: '50' };
if (Math.abs(fps - 25) < 0.05) return { ff: '25', raw: '25' };
if (Math.abs(fps - 24) < 0.2) return { ff: '24000/1001', raw: '24000/1001' };
if (Math.abs(fps - 30) < 0.05) return { ff: '30', raw: '30' };
return { ff: String(fps), raw: String(fps) };
}
// Scan for the MPEG-2 422 essence. raw2bmx CANNOT wrap MPEG-2 422 as 1080p at
// 59.94, so a 1080-line raster is always wrapped as INTERLACED (1080i59.94),
// regardless of the progressive scanHint. 720 and below stay progressive.
if (height >= 1080) scan = 'i';
else if (scan == null) scan = scanHint || 'p';
// AVC-Intra wraps progressive natively, so respect the scanHint (Deltacast
// reports progressive). Default 1080 to interlaced only when no hint is given.
if (scan == null) scan = scanHint || ((height >= 1080) ? 'i' : 'p');
const r = rates(fpsNum);
// AVC-Intra 100 raster flags. raw2bmx accepts 1080p59.94 here (verified).
let rawFlag;
if (height >= 1080) {
rawFlag = '--mpeg2lg_422p_hl_1080i';
rawFlag = (scan === 'i') ? '--avci100_1080i' : '--avci100_1080p';
} else if (height >= 720) {
rawFlag = '--mpeg2lg_422p_hl_720p';
rawFlag = '--avci100_720p';
if (fpsNum == null) { r.ff = '60000/1001'; r.raw = '60000/1001'; }
} else {
rawFlag = '--mpeg2lg_422p_ml_576i';
@ -863,7 +865,7 @@ class CaptureManager {
...GROWING_VIDEO_ELEMENTARY_ARGS,
'-b:v', vb, '-minrate', vb, '-maxrate', vb, '-bufsize', vb,
'-r', ffRate,
'-f', 'mpeg2video', '@VF@',
'-f', 'h264', '@VF@',
// (b) PCM s16le audio → "$AF"
'-map', audioMap,
'-c:a', 'pcm_s16le', '-ar', '48000', '-ac', String(ach),
@ -912,7 +914,7 @@ class CaptureManager {
// fields with the live frame count every 3s (Premiere reads the header
// Duration on each refresh; without the patch it sees duration=N/A).
const bmx = [
'raw2bmx', '-t', 'rdd9', '-o', '"$OUT"', '-f', frameRate,
'raw2bmx', '-t', 'op1a', '-o', '"$OUT"', '-f', frameRate,
'--part', String(GROWING_PART_INTERVAL_FRAMES),
'--index-follows',
rawFlag, '"$VF"',