fix(capture): growing file is MXF OP1a (DNxHR HQ) so Premiere can open it
The growing edit-while-record file was a fragmented MOV (empty moov), which
Premiere can't open ("Unable to open file on disk"). Write the growing master
as MXF OP1a / DNxHR HQ (Premiere-native, growable on disk); finalized master
keeps today's non-fragmented +faststart MOV.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
1d642bd437
commit
794b9d9929
1 changed files with 54 additions and 13 deletions
|
|
@ -155,12 +155,54 @@ const CONTAINER_EXT = {
|
||||||
mov: 'mov', mp4: 'mp4', mkv: 'mkv', mxf: 'mxf', ts: 'ts',
|
mov: 'mov', mp4: 'mp4', mkv: 'mkv', mxf: 'mxf', ts: 'ts',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Growing-file (edit-while-record) master format.
|
||||||
|
//
|
||||||
|
// Premiere's "open capture in progress" / grow-on-disk support is FORMAT-
|
||||||
|
// SPECIFIC. A fragmented MP4/MOV (`+frag_keyframe+empty_moov+default_base_moof`)
|
||||||
|
// is NOT openable by Premiere as a growing file — its QuickTime importer needs
|
||||||
|
// the classic stco/stsz/stts sample tables in a single top-level moov, which a
|
||||||
|
// fragmented MOV never has while growing (samples live in moof/trun fragments).
|
||||||
|
// Symptom: "Unable to open file on disk." (Confirmed via ffprobe on zampp2: the
|
||||||
|
// growing .mov is ftyp + empty moov + repeating moof/mdat pairs, no sample
|
||||||
|
// tables.)
|
||||||
|
//
|
||||||
|
// The robust, broadcast-standard growing format Premiere DOES ingest is
|
||||||
|
// MXF OP1a (`-f mxf`) carrying a Premiere-native intra codec. We use DNxHR HQ
|
||||||
|
// (4:2:2 8-bit) which ffmpeg's MXF muxer accepts (HEVC/ProRes-in-MXF are
|
||||||
|
// rejected by this build), every frame is intra so a partially-written file is
|
||||||
|
// decodable to its last complete frame, and MXF writes header + body partitions
|
||||||
|
// incrementally so readers see valid essence mid-write. The same finalized .mxf
|
||||||
|
// is also a clean, Premiere-native asset, so the promotion/finalized path stays
|
||||||
|
// valid.
|
||||||
|
//
|
||||||
|
// Trade-off: DNxHR HQ is large (~22 GB/min at 1080p). Switch the profile to
|
||||||
|
// dnxhr_sq below (~half the bitrate) if disk is the constraint.
|
||||||
|
const GROWING_VIDEO_ARGS = [
|
||||||
|
'-c:v', 'dnxhd', '-profile:v', 'dnxhr_hq', '-pix_fmt', 'yuv422p',
|
||||||
|
];
|
||||||
|
const GROWING_EXT = 'mxf';
|
||||||
|
|
||||||
function buildEncodeArgs({
|
function buildEncodeArgs({
|
||||||
codec, videoBitrate, framerate,
|
codec, videoBitrate, framerate,
|
||||||
audioCodec, audioBitrate, audioChannels,
|
audioCodec, audioBitrate, audioChannels,
|
||||||
container, isNetwork, isProxy = false,
|
container, isNetwork, isProxy = false,
|
||||||
growing = false,
|
growing = false,
|
||||||
}) {
|
}) {
|
||||||
|
// ── Growing master: force MXF OP1a + DNxHR, ignoring the configured MOV/
|
||||||
|
// ProRes container/codec. This is the only combination Premiere opens as a
|
||||||
|
// growing file (see GROWING_VIDEO_ARGS above). Audio is forced to PCM,
|
||||||
|
// which MXF carries natively and Premiere ingests.
|
||||||
|
if (growing) {
|
||||||
|
const args = [];
|
||||||
|
if (isNetwork) args.push('-map', '0:v:0?', '-map', '0:a:0?');
|
||||||
|
args.push(...GROWING_VIDEO_ARGS);
|
||||||
|
if (framerate && framerate !== 'native') args.push('-r', framerate);
|
||||||
|
args.push('-c:a', 'pcm_s24le');
|
||||||
|
if (audioChannels) args.push('-ac', String(audioChannels));
|
||||||
|
args.push('-f', 'mxf');
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
const v = VIDEO_CODECS[codec] || (isProxy ? VIDEO_CODECS.h264 : VIDEO_CODECS.prores_hq);
|
const v = VIDEO_CODECS[codec] || (isProxy ? VIDEO_CODECS.h264 : VIDEO_CODECS.prores_hq);
|
||||||
const a = AUDIO_CODECS[audioCodec] || (isProxy ? AUDIO_CODECS.aac : AUDIO_CODECS.pcm_s24le);
|
const a = AUDIO_CODECS[audioCodec] || (isProxy ? AUDIO_CODECS.aac : AUDIO_CODECS.pcm_s24le);
|
||||||
const fmt = CONTAINER_FMT[container] || (isProxy ? 'mp4' : 'mov');
|
const fmt = CONTAINER_FMT[container] || (isProxy ? 'mp4' : 'mov');
|
||||||
|
|
@ -180,19 +222,15 @@ function buildEncodeArgs({
|
||||||
// moov-atom placement is the difference between a Premiere-openable master and
|
// moov-atom placement is the difference between a Premiere-openable master and
|
||||||
// a "file cannot be opened" error.
|
// a "file cannot be opened" error.
|
||||||
//
|
//
|
||||||
// - Growing-file masters (edit-while-record on the SMB share) MUST be
|
// Finalized masters (the S3-piped recording that stops cleanly) must NOT be
|
||||||
// fragmented so a moov/mvex is present from the first frame and the file is
|
// fragmented. Adobe Premiere's QuickTime/MOV importer reads the classic
|
||||||
// decodable while still being written. The samples live in moof/trun boxes.
|
// stco/stsz/stts sample tables in a single top-level moov; a fragmented MOV
|
||||||
//
|
// (moof/trun, empty sample tables) makes Premiere report "file cannot be
|
||||||
// - Finalized masters (the S3-piped recording that stops cleanly) must NOT be
|
// opened." We write a clean, non-fragmented MOV instead. `+faststart` puts the
|
||||||
// fragmented. Adobe Premiere's QuickTime/MOV importer reads the classic
|
// moov before mdat on the second pass so the file is instantly
|
||||||
// stco/stsz/stts sample tables in a single top-level moov; a fragmented MOV
|
// seekable/streamable too.
|
||||||
// (moof/trun, empty sample tables) makes Premiere report "file cannot be
|
|
||||||
// opened." We write a clean, non-fragmented MOV instead.
|
|
||||||
// `+faststart` puts the moov before mdat on the second pass so the file is
|
|
||||||
// instantly seekable/streamable too.
|
|
||||||
if (fmt === 'mov' || fmt === 'mp4') {
|
if (fmt === 'mov' || fmt === 'mp4') {
|
||||||
args.push('-movflags', growing ? '+frag_keyframe+empty_moov+default_base_moof' : '+faststart');
|
args.push('-movflags', '+faststart');
|
||||||
}
|
}
|
||||||
// ProRes-in-MOV must carry a QuickTime brand or some importers reject the tag.
|
// ProRes-in-MOV must carry a QuickTime brand or some importers reject the tag.
|
||||||
args.push('-f', fmt);
|
args.push('-f', fmt);
|
||||||
|
|
@ -384,8 +422,11 @@ class CaptureManager {
|
||||||
if (growingActive && GROWING_SMB_MOUNT) {
|
if (growingActive && GROWING_SMB_MOUNT) {
|
||||||
if (!mountGrowingShare()) growingActive = false; // fall back to S3
|
if (!mountGrowingShare()) growingActive = false; // fall back to S3
|
||||||
}
|
}
|
||||||
|
// Growing master is always MXF OP1a (the only Premiere-growable format here),
|
||||||
|
// regardless of the recorder's configured container — so it gets a .mxf
|
||||||
|
// extension, not hiresExt.
|
||||||
const growingPath = growingActive
|
const growingPath = growingActive
|
||||||
? `${GROWING_PATH}/${projectId}/${clipName}.${hiresExt}`
|
? `${GROWING_PATH}/${projectId}/${clipName}.${GROWING_EXT}`
|
||||||
: null;
|
: null;
|
||||||
if (growingPath) {
|
if (growingPath) {
|
||||||
try { mkdirSync(dirname(growingPath), { recursive: true }); }
|
try { mkdirSync(dirname(growingPath), { recursive: true }); }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue