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 {
|
const {
|
||||||
videoCodec = 'libx264',
|
videoCodec = 'libx264',
|
||||||
videoPreset = 'fast',
|
videoPreset = 'fast',
|
||||||
videoBitrate = '10M',
|
videoBitrate = '750k', // average/target for VBR
|
||||||
rateControl = null, // 'cbr' | 'vbr' | 'cqp' — optional
|
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',
|
audioCodec = 'aac',
|
||||||
audioBitrate = '192k',
|
audioBitrate = '128k',
|
||||||
hasAudio = true,
|
hasAudio = true,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// libx264 / yuv420p require even dimensions. Captured frames from SDI
|
// libx264 / yuv420p require even dimensions.
|
||||||
// or upstream uploads sometimes arrive odd-sized (e.g. 1243x1125).
|
|
||||||
const vf = "scale='trunc(iw/2)*2:trunc(ih/2)*2',format=yuv420p";
|
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 = [
|
const args = [
|
||||||
'-analyzeduration', '100M',
|
'-analyzeduration', '100M',
|
||||||
'-probesize', '100M',
|
'-probesize', '100M',
|
||||||
|
|
@ -118,8 +117,14 @@ export const transcodeVideo = async (inputPath, outputPath, options = {}) => {
|
||||||
'-b:v', videoBitrate,
|
'-b:v', videoBitrate,
|
||||||
];
|
];
|
||||||
|
|
||||||
// NVENC takes rate control via -rc / -cq. VAAPI uses -rc_mode. libx264
|
// VBR min/max/bufsize for libx264 ABR mode.
|
||||||
// ignores both (rate is implied by -b:v + -maxrate).
|
// 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 (rateControl) {
|
||||||
if (NVENC_CODECS.has(videoCodec)) {
|
if (NVENC_CODECS.has(videoCodec)) {
|
||||||
args.push('-rc', rateControl);
|
args.push('-rc', rateControl);
|
||||||
|
|
|
||||||
|
|
@ -21,15 +21,26 @@ async function loadProxyEncodingSettings() {
|
||||||
const gpuEnabled = map.gpu_transcode_enabled === 'true';
|
const gpuEnabled = map.gpu_transcode_enabled === 'true';
|
||||||
const codec = map.gpu_codec || (gpuEnabled ? 'h264_nvenc' : 'libx264');
|
const codec = map.gpu_codec || (gpuEnabled ? 'h264_nvenc' : 'libx264');
|
||||||
const preset = map.gpu_preset || (gpuEnabled ? 'p4' : 'fast');
|
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 rcMode = map.gpu_rc_mode || null;
|
||||||
const audioCodec = map.gpu_audio_codec || 'aac';
|
const audioCodec = map.gpu_audio_codec || 'aac';
|
||||||
const audioKbps = parseInt(map.gpu_audio_bitrate_kbps || '128', 10);
|
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 {
|
return {
|
||||||
videoCodec: codec,
|
videoCodec: codec,
|
||||||
videoPreset: preset,
|
videoPreset: preset,
|
||||||
videoBitrate: `${bitrateM}M`,
|
videoBitrate: `${targetBps}k`,
|
||||||
|
videoMinRate: `${minKbps}k`,
|
||||||
|
videoMaxRate: `${maxKbps}k`,
|
||||||
|
videoBufSize: `${bufKbps}k`,
|
||||||
rateControl: rcMode,
|
rateControl: rcMode,
|
||||||
audioCodec,
|
audioCodec,
|
||||||
audioBitrate: `${audioKbps}k`,
|
audioBitrate: `${audioKbps}k`,
|
||||||
|
|
@ -161,20 +172,21 @@ export const proxyWorker = async (job) => {
|
||||||
const encSettings = await loadProxyEncodingSettings();
|
const encSettings = await loadProxyEncodingSettings();
|
||||||
console.log(
|
console.log(
|
||||||
`[proxy] Transcoding asset ${assetId} via ${encSettings._gpu ? 'GPU' : 'CPU'} ` +
|
`[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 {
|
try {
|
||||||
await transcodeVideo(inputPath, outputPath, { ...encSettings, hasAudio });
|
await transcodeVideo(inputPath, outputPath, { ...encSettings, hasAudio });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (encSettings._gpu) {
|
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`);
|
console.warn(`[proxy] GPU encode failed (${err.message}); falling back to libx264`);
|
||||||
await transcodeVideo(inputPath, outputPath, {
|
await transcodeVideo(inputPath, outputPath, {
|
||||||
videoCodec: 'libx264', videoPreset: 'fast',
|
videoCodec: 'libx264', videoPreset: 'fast',
|
||||||
videoBitrate: encSettings.videoBitrate,
|
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,
|
hasAudio,
|
||||||
});
|
});
|
||||||
} else { throw err; }
|
} else { throw err; }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue