feat(proxy): VBR 500k-1M encoding for proxy generation
executor.js: - transcodeVideo() now accepts videoMinRate, videoMaxRate, videoBufSize - When set, passes -minrate/-maxrate/-bufsize to FFmpeg for ABR/VBR mode - libx264 operates with per-scene quality variation within the envelope proxy.js: - Target average: 750k (gpu_bitrate_mbps=0.75) - Min: 375k (50% of target), Max: 998k (~133%), Buffer: 2× max - Gives effective range of ~500k-1M depending on scene complexity - Log now shows VBR min-max-avg - GPU fallback also passes VBR params - Default videoBitrate changed from 10M to 750k in executor.js
This commit is contained in:
parent
03aa7a0673
commit
e4d4c00f52
2 changed files with 35 additions and 18 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
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; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue