diff --git a/services/worker/src/ffmpeg/executor.js b/services/worker/src/ffmpeg/executor.js index ca2dbd1..c0e7308 100644 --- a/services/worker/src/ffmpeg/executor.js +++ b/services/worker/src/ffmpeg/executor.js @@ -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, diff --git a/services/worker/src/workers/conform.js b/services/worker/src/workers/conform.js index 566f2ff..8748973 100644 --- a/services/worker/src/workers/conform.js +++ b/services/worker/src/workers/conform.js @@ -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,