Update plugins: VMAF mode, documentation fixes, version sync
- Added VMAF quality-targeted mode to av1_svt_converter (v2.25) - Fixed documentation version mismatch (misc_fixes v2.8, stream_organizer v4.10, audio_standardizer v1.15) - Updated rate control documentation with VMAF mode details - Added vmaf_target and vmaf_samples input options - Added ab-av1 binary detection with ABAV1_PATH env var support
This commit is contained in:
@@ -5,13 +5,13 @@ const details = () => ({
|
||||
Type: 'Video',
|
||||
Operation: 'Transcode',
|
||||
Description: `
|
||||
AV1 conversion plugin with advanced quality control and performance optimizations for SVT-AV1 v3.0+ (2025).
|
||||
Features resolution-aware CRF, improved threading, and flexible bitrate control (custom maxrate or source-relative strategies).
|
||||
**Balanced high-quality defaults**: Preset 6, CRF 26, tune 0 (VQ), 10-bit, SCD 1, AQ 2, lookahead -1, TF on, keyint -2, fast-decode 0.
|
||||
Use presets 3–5 and/or lower CRF for higher quality when speed is less important.
|
||||
AV1 conversion plugin with advanced quality control for SVT-AV1 v3.0+ (2025).
|
||||
**Rate Control Modes**: VBR (predictable file sizes with target average bitrate), CRF (quality-based, unpredictable sizes), or VMAF (quality-targeted with ab-av1).
|
||||
Features resolution-aware CRF, source-relative bitrate strategies, and performance optimizations.
|
||||
**Balanced defaults**: Preset 6, CRF 26, tune 0 (VQ), 10-bit, SCD 1, AQ 2, lookahead -1, TF on, keyint -2, fast-decode 0.
|
||||
`,
|
||||
Version: '2.22',
|
||||
Tags: 'video,av1,svt,quality,performance,speed-optimized,capped-crf',
|
||||
Version: '2.25',
|
||||
Tags: 'video,av1,svt,quality,performance,speed-optimized,vbr,crf',
|
||||
Inputs: [
|
||||
{
|
||||
name: 'crf',
|
||||
@@ -59,7 +59,52 @@ const details = () => ({
|
||||
'25%_source'
|
||||
],
|
||||
},
|
||||
tooltip: 'Target bitrate strategy. \'static\' uses custom_maxrate. Other options set maxrate relative to detected source bitrate.',
|
||||
tooltip: 'Target bitrate strategy. \'static\' uses custom_maxrate. Other options set target/maxrate relative to detected source bitrate.',
|
||||
},
|
||||
{
|
||||
name: 'rate_control_mode',
|
||||
type: 'string',
|
||||
defaultValue: 'crf*',
|
||||
inputUI: {
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
'crf*',
|
||||
'vbr',
|
||||
'vmaf'
|
||||
],
|
||||
},
|
||||
tooltip: 'Rate control mode. \'crf\' = Quality-based (CRF + maxrate cap), \'vbr\' = Bitrate-based (target average + maxrate peaks), \'vmaf\' = Quality-targeted (ab-av1 auto CRF selection, requires ab-av1 binary).',
|
||||
},
|
||||
{
|
||||
name: 'vmaf_target',
|
||||
type: 'string',
|
||||
defaultValue: '95*',
|
||||
inputUI: {
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
'85',
|
||||
'90',
|
||||
'95*',
|
||||
'97',
|
||||
'99'
|
||||
],
|
||||
},
|
||||
tooltip: 'Target VMAF quality score (vmaf mode only). Higher = better quality but larger files. 95 = visually transparent (recommended), 90 = good quality, 85 = acceptable quality.',
|
||||
},
|
||||
{
|
||||
name: 'vmaf_samples',
|
||||
type: 'string',
|
||||
defaultValue: '4*',
|
||||
inputUI: {
|
||||
type: 'dropdown',
|
||||
options: [
|
||||
'2',
|
||||
'4*',
|
||||
'6',
|
||||
'8'
|
||||
],
|
||||
},
|
||||
tooltip: 'Number of sample segments for ab-av1 quality analysis (vmaf mode only). More samples = more accurate CRF selection but slower analysis. 4 samples is a good balance.',
|
||||
},
|
||||
{
|
||||
name: 'max_resolution',
|
||||
@@ -386,8 +431,47 @@ const plugin = (file, librarySettings, inputs, otherArguments) => {
|
||||
resolution_crf_adjust: stripStar(inputs.resolution_crf_adjust),
|
||||
custom_maxrate: stripStar(inputs.custom_maxrate),
|
||||
target_bitrate_strategy: stripStar(inputs.target_bitrate_strategy),
|
||||
rate_control_mode: stripStar(inputs.rate_control_mode),
|
||||
skip_hevc: stripStar(inputs.skip_hevc),
|
||||
force_transcode: stripStar(inputs.force_transcode),
|
||||
vmaf_target: stripStar(inputs.vmaf_target),
|
||||
vmaf_samples: stripStar(inputs.vmaf_samples),
|
||||
};
|
||||
|
||||
// Detect ab-av1 binary path with multi-level fallback
|
||||
const getAbAv1Path = () => {
|
||||
// Try environment variable first
|
||||
const envPath = (process.env.ABAV1_PATH || '').trim();
|
||||
if (envPath) {
|
||||
try {
|
||||
if (require('fs').existsSync(envPath)) {
|
||||
require('fs').accessSync(envPath, require('fs').constants.X_OK);
|
||||
return envPath;
|
||||
}
|
||||
} catch (e) {
|
||||
// Continue to next detection method
|
||||
}
|
||||
}
|
||||
|
||||
// Try common installation paths
|
||||
const commonPaths = [
|
||||
'/usr/local/bin/ab-av1',
|
||||
'/usr/bin/ab-av1',
|
||||
];
|
||||
|
||||
for (const path of commonPaths) {
|
||||
try {
|
||||
if (require('fs').existsSync(path)) {
|
||||
require('fs').accessSync(path, require('fs').constants.X_OK);
|
||||
return path;
|
||||
}
|
||||
} catch (e) {
|
||||
// Continue to next path
|
||||
}
|
||||
}
|
||||
|
||||
// Not found in any known location
|
||||
return null;
|
||||
};
|
||||
|
||||
// Detect actual input container format via ffprobe
|
||||
@@ -583,14 +667,43 @@ const plugin = (file, librarySettings, inputs, otherArguments) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate target maxrate using precedence logic
|
||||
let calculatedMaxrate = null;
|
||||
let maxrateSource = '';
|
||||
// Estimate expected average bitrate for a given CRF and resolution
|
||||
// Based on SVT-AV1 CRF 30, preset ~6, average movie content (VMAF ~95)
|
||||
// Lower CRF = higher bitrate (roughly 10-15% increase per CRF step down)
|
||||
const estimateCrfBitrate = (crf, height) => {
|
||||
// Baseline bitrates for CRF 30
|
||||
let baselineCrf30 = 3000; // Default to 1080p
|
||||
|
||||
// Priority 1: target_bitrate_strategy (if not static)
|
||||
if (height >= 2160) {
|
||||
baselineCrf30 = 12000; // 4K average
|
||||
} else if (height >= 1440) {
|
||||
baselineCrf30 = 6000; // 1440p estimate (between 1080p and 4K)
|
||||
} else if (height >= 1080) {
|
||||
baselineCrf30 = 3000; // 1080p average
|
||||
} else if (height >= 720) {
|
||||
baselineCrf30 = 2000; // 720p average
|
||||
} else {
|
||||
baselineCrf30 = 1200; // 480p average
|
||||
}
|
||||
|
||||
// Adjust for CRF difference from baseline (CRF 30)
|
||||
// Each CRF step down increases bitrate by ~12%
|
||||
const crfDiff = 30 - parseInt(crf);
|
||||
const bitrateFactor = Math.pow(1.12, crfDiff);
|
||||
|
||||
return Math.round(baselineCrf30 * bitrateFactor);
|
||||
};
|
||||
|
||||
// Calculate target bitrate and maxrate based on rate control mode
|
||||
let calculatedTargetBitrate = null; // For VBR mode
|
||||
let calculatedMaxrate = null; // For both modes
|
||||
let bitrateSource = '';
|
||||
|
||||
// Step 1: Calculate base bitrate from strategy
|
||||
if (sanitized.target_bitrate_strategy !== 'static') {
|
||||
if (sourceBitrateKbps) {
|
||||
let multiplier = 1.0;
|
||||
|
||||
switch (sanitized.target_bitrate_strategy) {
|
||||
case 'match_source':
|
||||
multiplier = 1.0;
|
||||
@@ -608,9 +721,35 @@ const plugin = (file, librarySettings, inputs, otherArguments) => {
|
||||
multiplier = 0.25;
|
||||
break;
|
||||
}
|
||||
calculatedMaxrate = Math.round(sourceBitrateKbps * multiplier);
|
||||
maxrateSource = `target_bitrate_strategy '${sanitized.target_bitrate_strategy}': Source ${sourceBitrateKbps}k → Maxrate ${calculatedMaxrate}k`;
|
||||
response.infoLog += `Using ${maxrateSource}.\n`;
|
||||
|
||||
const baseBitrate = Math.round(sourceBitrateKbps * multiplier);
|
||||
|
||||
// Step 2: Apply mode-specific logic
|
||||
if (sanitized.rate_control_mode === 'vbr') {
|
||||
// VBR Mode: Target average = base, Maxrate = base * 2.0 (headroom for peaks)
|
||||
calculatedTargetBitrate = baseBitrate;
|
||||
calculatedMaxrate = Math.round(baseBitrate * 2.0);
|
||||
bitrateSource = `VBR mode with target_bitrate_strategy '${sanitized.target_bitrate_strategy}': Source ${sourceBitrateKbps}k * ${multiplier} = Target ${calculatedTargetBitrate}k, Maxrate ${calculatedMaxrate}k (2.0x headroom)`;
|
||||
response.infoLog += `Using ${bitrateSource}.\n`;
|
||||
} else {
|
||||
// CRF Mode: Ensure maxrate is higher than what CRF would naturally produce
|
||||
// Estimate what the CRF will average based on resolution
|
||||
const estimatedCrfAvg = estimateCrfBitrate(finalCrf, outputHeight || 1080);
|
||||
|
||||
// Set maxrate to the higher of: user's calculated value OR 1.8x estimated CRF average
|
||||
// The 1.8x ensures headroom for peaks above the CRF average
|
||||
const minSafeMaxrate = Math.round(estimatedCrfAvg * 1.8);
|
||||
|
||||
if (baseBitrate < minSafeMaxrate) {
|
||||
calculatedMaxrate = minSafeMaxrate;
|
||||
bitrateSource = `CRF mode: Calculated ${baseBitrate}k from strategy, but CRF ${finalCrf} @ ${outputHeight || 1080}p averages ~${estimatedCrfAvg}k. Using Maxrate ${calculatedMaxrate}k (1.8x avg) for headroom`;
|
||||
response.infoLog += `${bitrateSource}.\n`;
|
||||
} else {
|
||||
calculatedMaxrate = baseBitrate;
|
||||
bitrateSource = `CRF mode with target_bitrate_strategy '${sanitized.target_bitrate_strategy}': Source ${sourceBitrateKbps}k * ${multiplier} = Maxrate ${calculatedMaxrate}k (above CRF estimate)`;
|
||||
response.infoLog += `Using ${bitrateSource}.\n`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.infoLog += `Warning: target_bitrate_strategy '${sanitized.target_bitrate_strategy}' selected but source bitrate unavailable. Falling back to static mode.\n`;
|
||||
}
|
||||
@@ -620,9 +759,27 @@ const plugin = (file, librarySettings, inputs, otherArguments) => {
|
||||
if (!calculatedMaxrate && sanitized.custom_maxrate && sanitized.custom_maxrate !== '' && sanitized.custom_maxrate !== '0') {
|
||||
const customValue = parseInt(sanitized.custom_maxrate);
|
||||
if (!isNaN(customValue) && customValue > 0) {
|
||||
calculatedMaxrate = customValue;
|
||||
maxrateSource = `custom_maxrate: ${calculatedMaxrate}k`;
|
||||
response.infoLog += `Using ${maxrateSource}.\n`;
|
||||
if (sanitized.rate_control_mode === 'vbr') {
|
||||
// VBR mode: Custom value is the target, maxrate = target * 2.0
|
||||
calculatedTargetBitrate = customValue;
|
||||
calculatedMaxrate = Math.round(customValue * 2.0);
|
||||
bitrateSource = `VBR mode with custom_maxrate: Target ${calculatedTargetBitrate}k, Maxrate ${calculatedMaxrate}k (2.0x headroom)`;
|
||||
response.infoLog += `Using ${bitrateSource}.\n`;
|
||||
} else {
|
||||
// CRF mode: Ensure custom maxrate is reasonable for the CRF
|
||||
const estimatedCrfAvg = estimateCrfBitrate(finalCrf, outputHeight || 1080);
|
||||
const minSafeMaxrate = Math.round(estimatedCrfAvg * 1.8);
|
||||
|
||||
if (customValue < minSafeMaxrate) {
|
||||
calculatedMaxrate = minSafeMaxrate;
|
||||
bitrateSource = `CRF mode: Custom ${customValue}k is below safe minimum for CRF ${finalCrf} @ ${outputHeight || 1080}p (est. ~${estimatedCrfAvg}k avg). Using ${calculatedMaxrate}k (1.8x) for headroom`;
|
||||
response.infoLog += `${bitrateSource}.\n`;
|
||||
} else {
|
||||
calculatedMaxrate = customValue;
|
||||
bitrateSource = `CRF mode with custom_maxrate: Maxrate ${calculatedMaxrate}k`;
|
||||
response.infoLog += `Using ${bitrateSource}.\n`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
response.infoLog += `Warning: Invalid custom_maxrate value '${sanitized.custom_maxrate}'. Using uncapped CRF.\n`;
|
||||
}
|
||||
@@ -639,20 +796,62 @@ const plugin = (file, librarySettings, inputs, otherArguments) => {
|
||||
};
|
||||
|
||||
const minBitrate = getMinBitrate(outputHeight || 1080);
|
||||
if (calculatedMaxrate && calculatedMaxrate < minBitrate) {
|
||||
|
||||
// Adjust target and maxrate if below minimum
|
||||
if (calculatedTargetBitrate && calculatedTargetBitrate < minBitrate) {
|
||||
response.infoLog += `Warning: Calculated target bitrate ${calculatedTargetBitrate}k is below minimum for ${outputHeight || 1080}p. Raising to ${minBitrate}k.\n`;
|
||||
calculatedTargetBitrate = minBitrate;
|
||||
calculatedMaxrate = Math.round(minBitrate * 2.0); // Adjust maxrate proportionally
|
||||
} else if (calculatedMaxrate && calculatedMaxrate < minBitrate) {
|
||||
response.infoLog += `Warning: Calculated maxrate ${calculatedMaxrate}k is below minimum for ${outputHeight || 1080}p. Raising to ${minBitrate}k.\n`;
|
||||
calculatedMaxrate = minBitrate;
|
||||
}
|
||||
|
||||
if (calculatedMaxrate) {
|
||||
// Step 3: Build quality/bitrate arguments based on mode
|
||||
if (sanitized.rate_control_mode === 'vbr' && calculatedTargetBitrate) {
|
||||
// VBR Mode: Use target bitrate + maxrate
|
||||
const bufsize = calculatedMaxrate; // Buffer size = maxrate for VBR
|
||||
qualityArgs += ` -b:v ${calculatedTargetBitrate}k -maxrate ${calculatedMaxrate}k -bufsize ${bufsize}k`;
|
||||
bitrateControlInfo += ` with VBR target ${calculatedTargetBitrate}k, maxrate ${calculatedMaxrate}k (bufsize: ${bufsize}k)`;
|
||||
response.infoLog += `VBR encoding: Target average ${calculatedTargetBitrate}k, peak ${calculatedMaxrate}k, buffer ${bufsize}k.\n`;
|
||||
} else if (sanitized.rate_control_mode === 'vmaf') {
|
||||
// VMAF Mode: Use ab-av1 for automatic CRF calculation
|
||||
const abav1Path = getAbAv1Path();
|
||||
|
||||
if (!abav1Path) {
|
||||
response.infoLog += 'VMAF mode selected but ab-av1 binary not found. Falling back to CRF mode.\n';
|
||||
response.infoLog += 'To use VMAF mode, ensure ab-av1 is installed and accessible (check ABAV1_PATH env var or /usr/local/bin/ab-av1).\n';
|
||||
// Fall through to standard CRF encoding - no maxrate cap in fallback
|
||||
} else {
|
||||
response.infoLog += `Using ab-av1 for quality-targeted encoding (target VMAF ${sanitized.vmaf_target}).\n`;
|
||||
response.infoLog += `ab-av1 binary: ${abav1Path}\n`;
|
||||
|
||||
const vmafTarget = parseInt(sanitized.vmaf_target);
|
||||
const sampleCount = parseInt(sanitized.vmaf_samples);
|
||||
|
||||
// Store ab-av1 metadata for worker to execute crf-search before encoding
|
||||
response.useAbAv1 = true;
|
||||
response.abav1Path = abav1Path;
|
||||
response.vmafTarget = vmafTarget;
|
||||
response.vmafSampleCount = sampleCount;
|
||||
response.sourceFile = file.file;
|
||||
|
||||
bitrateControlInfo = `VMAF-targeted encoding (target VMAF: ${vmafTarget}, samples: ${sampleCount})`;
|
||||
response.infoLog += `ab-av1 will automatically determine optimal CRF for VMAF ${vmafTarget}.\n`;
|
||||
response.infoLog += `Using ${sampleCount} sample segments for quality analysis.\n`;
|
||||
}
|
||||
} else if (calculatedMaxrate) {
|
||||
// CRF Mode with maxrate cap
|
||||
const bufsize = Math.round(calculatedMaxrate * 2.0); // Buffer size = 2.0x maxrate for stability
|
||||
qualityArgs += ` -maxrate ${calculatedMaxrate}k -bufsize ${bufsize}k`;
|
||||
bitrateControlInfo += ` with capped bitrate at ${calculatedMaxrate}k (bufsize: ${bufsize}k)`;
|
||||
response.infoLog += `Capped CRF enabled: Max bitrate ${calculatedMaxrate}k, buffer size ${bufsize}k for optimal bandwidth management.\n`;
|
||||
} else {
|
||||
// Uncapped CRF
|
||||
response.infoLog += `Using uncapped CRF for maximum quality efficiency.\n`;
|
||||
}
|
||||
|
||||
|
||||
// Add tile options for 4K content (improves parallel encoding/decoding)
|
||||
let tileArgs = '';
|
||||
if (outputHeight && outputHeight >= 2160) {
|
||||
|
||||
Reference in New Issue
Block a user