fix(worker): conform — preserve audio + map ProRes/DNxHR codecs

Three cooperating bugs left the rendered output silent and in the
wrong codec:

1. executor.js trimSegment used `-frames:v` with no audio mapping.
   ffmpeg dropped the audio track on each segment before they reached
   the concat step. Add `-c:a copy -shortest` so each segment carries
   its original audio.

2. conform.js audioFlag was `audio === 'include' ? aac : -an`. The
   panel's v2.2.1 defaults send `audio: 'broadcast'`, which didn't
   match 'include' → `-an` explicitly stripped audio at the encode
   step. Switch to the opposite default: only an explicit 'none' or
   'off' disables audio; everything else gets AAC 320k @ 48kHz.

3. conform.js video codec map only matched `codec === 'prores'`. The
   panel sends `'prores_hq'` (and the conform slide panel can send
   `'prores_4444'` / `'dnxhr_hq'`). All of those fell through to
   libx264 and silently rendered H.264 instead of the requested codec.
   Add a real codec map with the right prores_ks profiles (3=HQ,
   4=4444) and DNxHR. Skip -crf for ProRes since the profile encodes
   quality.

The asset-row metadata's `codec` column is normalised the same way so
the new asset record matches what was actually written.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Claude 2026-05-28 14:32:49 -04:00
parent 56d7479a35
commit 6412b5c252
2 changed files with 49 additions and 6 deletions

View file

@ -185,10 +185,16 @@ export const trimSegment = async (inputPath, outputPath, inPoint, outPoint) => {
const inSeconds = inPoint / fps;
const frameCount = outPoint - inPoint;
// `-frames:v` bounds the video output but says nothing about audio. Without
// -c:a copy + -shortest the audio track is either dropped or runs past
// the trimmed video. We need audio in the final concat output, so passthrough
// the audio codec and stop on the shortest stream.
const args = [
'-ss', inSeconds.toString(),
'-i', inputPath,
'-frames:v', frameCount.toString(),
'-c:a', 'copy',
'-shortest',
'-start_number', '0',
'-y',
outputPath,

View file

@ -233,15 +233,47 @@ export const conformWorker = async (job) => {
await job.updateProgress(70);
console.log(`[conform] Concatenating segments for job ${jobId}`);
// Use re-encode instead of stream copy for consistent output
const audioFlag = audio === 'include' ? ['-c:a', 'aac'] : ['-an'];
// Audio: be permissive. Anything that isn't an explicit 'none' should
// get encoded — the panel sends 'broadcast' (default), 'include' is the
// legacy value, and there's no reason to silently drop audio for any
// other label. 320k AAC is a safe broadcast-quality default in mp4.
const audioFlag = (audio === 'none' || audio === 'off')
? ['-an']
: ['-c:a', 'aac', '-b:a', '320k', '-ar', '48000'];
// 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 = [];
if (codec === 'prores_hq' || codec === 'prores') {
videoCodec = 'prores_ks'; profileFlag = ['-profile:v', '3'];
} else if (codec === 'prores_4444') {
videoCodec = 'prores_ks'; profileFlag = ['-profile:v', '4'];
} else if (codec === 'h265' || codec === 'hevc') {
videoCodec = 'libx265';
} else if (codec === 'dnxhr_hq') {
videoCodec = 'dnxhd'; profileFlag = ['-profile:v', 'dnxhr_hq'];
} 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
: [
'-preset', quality === 'high' ? 'slow' : quality === 'broadcast' ? 'veryslow' : 'fast',
'-crf', quality === 'broadcast' ? '18' : quality === 'high' ? '23' : '28',
];
await runFFmpeg([
'-f', 'concat',
'-safe', '0',
'-i', segmentListPath,
'-c:v', codec === 'prores' ? 'prores_ks' : codec === 'h265' ? 'libx265' : 'libx264',
'-preset', quality === 'high' ? 'slow' : quality === 'broadcast' ? 'veryslow' : 'fast',
'-crf', quality === 'broadcast' ? '18' : quality === 'high' ? '23' : '28',
'-c:v', videoCodec,
...profileFlag,
...qualityArgs,
...audioFlag,
'-y', outputPath,
]);
@ -260,7 +292,12 @@ export const conformWorker = async (job) => {
`conformed-${seqName.replace(/[^a-z0-9]/gi, '_')}.mp4`,
`Conformed: ${seqName}`,
outputKey,
codec === 'prores' ? 'prores' : codec === 'h265' ? 'hevc' : 'h264',
// Normalise the panel's codec id into the canonical name we store on
// the asset row. Keep aligned with the encode branch above.
(codec === 'prores_hq' || codec === 'prores_4444' || codec === 'prores') ? 'prores'
: (codec === 'h265' || codec === 'hevc') ? 'hevc'
: (codec === 'dnxhr_hq') ? 'dnxhd'
: 'h264',
resolution !== 'match' ? resolution : '1920x1080',
seqFps,
null,