fix(worker): proxy + conform handle VC-3/DNxHD MXF and ProRes correctly
proxy.js: the empty-source guard tripped on growing VC-3/DNxHD MXF masters, which carry a valid decodable video stream but report format.duration=N/A (durationMs=null). Only bail when there is ALSO no resolution (the true aborted-capture signature), so MXF masters transcode instead of erroring. Verified: burn-test growing masters now migrate SMB->S3 AND generate proxies (assets dc0/dc2/dc6 ready with proxy, tags s3). conform.js: pin codec-correct pixel format per preset (ProRes HQ yuv422p10le, ProRes 4444 yuva444p10le, DNxHR HQ yuv422p); only emit -preset/-crf for libx264/libx265 (dnxhd/prores drive quality via profile); and use the codec-correct output extension in the error handler (was hard-coded .mp4, so a failed ProRes/DNxHR conform never flipped to error and spun in processing forever - the broadcast-vs-web asymmetry). All presets verified rc=0.
This commit is contained in:
parent
5c07b4e8b1
commit
e64281c9fd
2 changed files with 38 additions and 10 deletions
|
|
@ -295,28 +295,39 @@ export const conformWorker = async (job) => {
|
|||
// Codec map. The panel sends 'prores_hq' / 'prores_4444' / 'h264' / 'h265'
|
||||
// / 'dnxhr_hq'; old EDL callers send 'prores' / 'h265' / 'h264'. Match
|
||||
// both. prores_ks profiles: 0=proxy 1=lt 2=std 3=hq 4=4444.
|
||||
let videoCodec, profileFlag = [];
|
||||
// pixFmtFlag pins a codec-correct pixel format on the final encode so the
|
||||
// broadcast master is deterministic regardless of the (yuv420p) normalised
|
||||
// segments. prores_ks defaults are profile-driven but we make HQ explicitly
|
||||
// 10-bit 4:2:2 (broadcast spec) and 4444 explicitly 4:4:4+alpha. dnxhd
|
||||
// *requires* a pixel format it supports (yuv422p for DNxHR HQ) or it can
|
||||
// error on certain inputs.
|
||||
let videoCodec, profileFlag = [], pixFmtFlag = [];
|
||||
if (codec === 'prores_hq' || codec === 'prores') {
|
||||
videoCodec = 'prores_ks'; profileFlag = ['-profile:v', '3'];
|
||||
pixFmtFlag = ['-pix_fmt', 'yuv422p10le'];
|
||||
} else if (codec === 'prores_4444') {
|
||||
videoCodec = 'prores_ks'; profileFlag = ['-profile:v', '4'];
|
||||
pixFmtFlag = ['-pix_fmt', 'yuva444p10le'];
|
||||
} else if (codec === 'h265' || codec === 'hevc') {
|
||||
videoCodec = 'libx265';
|
||||
} else if (codec === 'dnxhr_hq') {
|
||||
videoCodec = 'dnxhd'; profileFlag = ['-profile:v', 'dnxhr_hq'];
|
||||
pixFmtFlag = ['-pix_fmt', 'yuv422p'];
|
||||
} else {
|
||||
videoCodec = 'libx264';
|
||||
}
|
||||
|
||||
// prores_ks ignores -crf and uses -preset differently; libx264/x265 use
|
||||
// crf-based quality. Branch the encode args.
|
||||
const isProRes = videoCodec === 'prores_ks';
|
||||
const qualityArgs = isProRes
|
||||
? [] // ProRes profile already encodes the quality target
|
||||
: [
|
||||
// Quality args are libx264/libx265-specific (-preset/-crf). ProRes encodes
|
||||
// quality via its profile; dnxhd (DNxHR) drives quality via the profile too
|
||||
// and rejects/ignores x264's -preset/-crf. Only emit these for the x26x
|
||||
// encoders so we never feed an encoder flags it doesn't understand.
|
||||
const isCrfCodec = videoCodec === 'libx264' || videoCodec === 'libx265';
|
||||
const qualityArgs = isCrfCodec
|
||||
? [
|
||||
'-preset', quality === 'high' ? 'slow' : quality === 'broadcast' ? 'veryslow' : 'fast',
|
||||
'-crf', quality === 'broadcast' ? '18' : quality === 'high' ? '23' : '28',
|
||||
];
|
||||
]
|
||||
: []; // ProRes / DNxHR: quality target is encoded by the profile
|
||||
|
||||
// Concat: every segment was normalised at trim time (uniform fps,
|
||||
// resolution, pixel format, sample rate, stereo). The demuxer can
|
||||
|
|
@ -335,6 +346,7 @@ export const conformWorker = async (job) => {
|
|||
'-i', segmentListPath,
|
||||
'-c:v', videoCodec,
|
||||
...profileFlag,
|
||||
...pixFmtFlag,
|
||||
...qualityArgs,
|
||||
...encodeAudio,
|
||||
'-y', outputPath,
|
||||
|
|
@ -397,11 +409,19 @@ export const conformWorker = async (job) => {
|
|||
// BUG FIX #1: Mark the output asset (if any) as 'error' so the UI doesn't
|
||||
// show a perpetually-spinning 'processing' state when the conform fails.
|
||||
// We don't have an assetId until the INSERT succeeds, so target by job key.
|
||||
//
|
||||
// BUG FIX #2: the output key extension is codec-dependent (ProRes / DNxHR
|
||||
// land in .mov, everything else in .mp4 — see `outputExt` above). The error
|
||||
// mark previously hard-coded `.mp4`, so a *failed* ProRes/DNxHR conform was
|
||||
// never matched and the asset spun in 'processing' forever — which is the
|
||||
// exact asymmetry behind "Broadcast (ProRes) fails but H.264/web works":
|
||||
// the H.264 row flips to 'error' and surfaces cleanly, the ProRes row does
|
||||
// not. Use the same outputExt the success path used.
|
||||
await query(
|
||||
`UPDATE assets
|
||||
SET status = 'error', updated_at = NOW()
|
||||
WHERE original_s3_key = $1`,
|
||||
[`jobs/${jobId}/conformed.mp4`]
|
||||
[`jobs/${jobId}/conformed.${outputExt}`]
|
||||
).catch(e => console.error('[conform] Failed to mark asset error:', e.message));
|
||||
throw error;
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -164,7 +164,15 @@ export const proxyWorker = async (job) => {
|
|||
// Empty/truncated capture: probe returned a video stream but ffmpeg can't
|
||||
// read any frames. Bail with a clear message instead of dumping ~3KB of
|
||||
// ffmpeg stderr into the failed-jobs list.
|
||||
if (mediaInfo.durationMs === null && mediaInfo.codec) {
|
||||
//
|
||||
// NOTE: a null container duration alone is NOT proof of emptiness. Growing
|
||||
// VC-3/DNxHD MXF (OP1a) masters carry a valid, decodable video stream but
|
||||
// report format.duration = N/A, so durationMs comes back null even though
|
||||
// ffmpeg transcodes them fine. Only bail when there is ALSO no decodable
|
||||
// video stream (no resolution) — that is the true aborted-capture signature
|
||||
// (ftyp-only / 0-frame objects). This preserves the empty-capture guard for
|
||||
// SRT/RTMP drops while letting MXF masters through to the transcoder.
|
||||
if (mediaInfo.durationMs === null && mediaInfo.codec && !mediaInfo.resolution) {
|
||||
throw new Error(
|
||||
`Empty or truncated source: codec=${mediaInfo.codec}, ` +
|
||||
`resolution=${mediaInfo.resolution || 'unknown'}, no readable frames.`
|
||||
|
|
|
|||
Loading…
Reference in a new issue