diff --git a/services/worker/src/ffmpeg/executor.js b/services/worker/src/ffmpeg/executor.js index fe31b41..8b7ea03 100644 --- a/services/worker/src/ffmpeg/executor.js +++ b/services/worker/src/ffmpeg/executor.js @@ -94,20 +94,19 @@ export const transcodeVideo = async (inputPath, outputPath, options = {}) => { const { videoCodec = 'libx264', videoPreset = 'fast', - videoBitrate = '10M', - rateControl = null, // 'cbr' | 'vbr' | 'cqp' — optional + videoBitrate = '750k', // average/target for VBR + videoMinRate = null, // VBR minimum e.g. '500k' + videoMaxRate = null, // VBR maximum e.g. '1000k' + videoBufSize = null, // VBR buffer e.g. '2000k' (2× maxrate recommended) + rateControl = null, // 'cbr' | 'vbr' | 'cqp' — optional override for HW codecs audioCodec = 'aac', - audioBitrate = '192k', + audioBitrate = '128k', hasAudio = true, } = options; - // libx264 / yuv420p require even dimensions. Captured frames from SDI - // or upstream uploads sometimes arrive odd-sized (e.g. 1243x1125). + // libx264 / yuv420p require even dimensions. const vf = "scale='trunc(iw/2)*2:trunc(ih/2)*2',format=yuv420p"; - // analyzeduration/probesize must be set BEFORE -i. Some ProRes captures - // write unusual timebases (60k tbn) that ffmpeg cannot resolve with the - // default 5MB probe — bump to 100MB so we always read enough of the file. const args = [ '-analyzeduration', '100M', '-probesize', '100M', @@ -118,8 +117,14 @@ export const transcodeVideo = async (inputPath, outputPath, options = {}) => { '-b:v', videoBitrate, ]; - // NVENC takes rate control via -rc / -cq. VAAPI uses -rc_mode. libx264 - // ignores both (rate is implied by -b:v + -maxrate). + // VBR min/max/bufsize for libx264 ABR mode. + // When minrate+maxrate are set, libx264 operates in ABR with hard limits + // rather than strict CBR — quality varies per-scene within the envelope. + if (videoMinRate) args.push('-minrate', videoMinRate); + if (videoMaxRate) args.push('-maxrate', videoMaxRate); + if (videoBufSize) args.push('-bufsize', videoBufSize); + + // NVENC/VAAPI hardware rate control flags if (rateControl) { if (NVENC_CODECS.has(videoCodec)) { args.push('-rc', rateControl); diff --git a/services/worker/src/workers/proxy.js b/services/worker/src/workers/proxy.js index 1cd7270..d0d4d3f 100644 --- a/services/worker/src/workers/proxy.js +++ b/services/worker/src/workers/proxy.js @@ -21,15 +21,26 @@ async function loadProxyEncodingSettings() { const gpuEnabled = map.gpu_transcode_enabled === 'true'; const codec = map.gpu_codec || (gpuEnabled ? 'h264_nvenc' : 'libx264'); const preset = map.gpu_preset || (gpuEnabled ? 'p4' : 'fast'); - const bitrateM = parseFloat(map.gpu_bitrate_mbps || '1.5'); const rcMode = map.gpu_rc_mode || null; const audioCodec = map.gpu_audio_codec || 'aac'; const audioKbps = parseInt(map.gpu_audio_bitrate_kbps || '128', 10); + // VBR 500k–1M: target average 750k, hard cap 1M, buffer 2M. + // libx264 ABR mode — quality varies per-scene within the envelope. + // These are stored in the DB as the bitrate field; min/max derived from it. + const bitrateM = parseFloat(map.gpu_bitrate_mbps || '0.75'); + const targetBps = Math.round(bitrateM * 1000); // kbps + const minKbps = Math.round(targetBps * 0.5); // 50% of target + const maxKbps = Math.round(targetBps * 1.33); // 133% of target, capped at ~1M for 750k target + const bufKbps = maxKbps * 2; // 2× maxrate recommended + return { videoCodec: codec, videoPreset: preset, - videoBitrate: `${bitrateM}M`, + videoBitrate: `${targetBps}k`, + videoMinRate: `${minKbps}k`, + videoMaxRate: `${maxKbps}k`, + videoBufSize: `${bufKbps}k`, rateControl: rcMode, audioCodec, audioBitrate: `${audioKbps}k`, @@ -161,20 +172,21 @@ export const proxyWorker = async (job) => { const encSettings = await loadProxyEncodingSettings(); console.log( `[proxy] Transcoding asset ${assetId} via ${encSettings._gpu ? 'GPU' : 'CPU'} ` + - `(${encSettings.videoCodec} ${encSettings.videoPreset} ${encSettings.videoBitrate})` + `(${encSettings.videoCodec} ${encSettings.videoPreset} VBR ${encSettings.videoMinRate}-${encSettings.videoMaxRate} avg=${encSettings.videoBitrate})` ); try { await transcodeVideo(inputPath, outputPath, { ...encSettings, hasAudio }); } catch (err) { if (encSettings._gpu) { - // Hardware encoder failed — typically "no NVIDIA driver" or "VAAPI - // device not found". Fall back to libx264 so the job doesn't fail - // when the worker host has no GPU. console.warn(`[proxy] GPU encode failed (${err.message}); falling back to libx264`); await transcodeVideo(inputPath, outputPath, { - videoCodec: 'libx264', videoPreset: 'fast', + videoCodec: 'libx264', videoPreset: 'fast', videoBitrate: encSettings.videoBitrate, - audioCodec: encSettings.audioCodec, audioBitrate: encSettings.audioBitrate, + videoMinRate: encSettings.videoMinRate, + videoMaxRate: encSettings.videoMaxRate, + videoBufSize: encSettings.videoBufSize, + audioCodec: encSettings.audioCodec, + audioBitrate: encSettings.audioBitrate, hasAudio, }); } else { throw err; }