Implement ab-av1 crf-search execution for VMAF mode (v2.30)
- Added executeAbAv1CrfSearch() function with synchronous execution - Parses CRF and VMAF score from ab-av1 output - 5-minute timeout for sample encodes - Graceful fallback to configured CRF on errors - Updates FFmpeg command with found CRF value - Proper logging of ab-av1 results and errors
This commit is contained in:
@@ -6,12 +6,12 @@ const details = () => ({
|
||||
Operation: 'Transcode',
|
||||
Description: `
|
||||
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.
|
||||
**Rate Control Modes**: VBR (predictable file sizes), CRF (quality-based), or VMAF (quality-targeted with ab-av1 crf-search).
|
||||
Features resolution-aware CRF, source-relative bitrate strategies, ab-av1 auto-CRF, 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.25',
|
||||
Tags: 'video,av1,svt,quality,performance,speed-optimized,vbr,crf',
|
||||
Version: '2.30',
|
||||
Tags: 'video,av1,svt,quality,performance,speed-optimized,vbr,crf,vmaf',
|
||||
Inputs: [
|
||||
{
|
||||
name: 'crf',
|
||||
@@ -474,6 +474,77 @@ const plugin = (file, librarySettings, inputs, otherArguments) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
// Execute ab-av1 crf-search synchronously to find optimal CRF for target VMAF
|
||||
// Returns { success: boolean, crf: number|null, vmaf: number|null, error: string|null }
|
||||
const executeAbAv1CrfSearch = (abav1Path, inputFile, vmafTarget, sampleCount, preset) => {
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
try {
|
||||
// Build ab-av1 command
|
||||
// --min-vmaf is the target VMAF score to achieve
|
||||
// --samples controls how many sample segments to test
|
||||
// --encoder specifies the encoder (libsvtav1 for FFmpeg)
|
||||
const args = [
|
||||
'crf-search',
|
||||
'-i', `"${inputFile}"`,
|
||||
'--min-vmaf', vmafTarget.toString(),
|
||||
'--samples', sampleCount.toString(),
|
||||
'--encoder', 'libsvtav1',
|
||||
'--preset', preset.toString(),
|
||||
];
|
||||
|
||||
const command = `${abav1Path} ${args.join(' ')}`;
|
||||
|
||||
// Execute with timeout (5 minutes should be enough for sample encodes)
|
||||
const output = execSync(command, {
|
||||
encoding: 'utf8',
|
||||
timeout: 300000, // 5 minute timeout
|
||||
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
// Parse ab-av1 output for CRF value
|
||||
// Expected format: "crf 28, VMAF 95.2" or similar
|
||||
// Also matches: "Best crf: 28" or "crf: 28 vmaf: 95.2"
|
||||
const crfMatch = output.match(/(?:crf|CRF)[:\s]+(\d+)/i);
|
||||
const vmafMatch = output.match(/(?:vmaf|VMAF)[:\s]+([\d.]+)/i);
|
||||
|
||||
if (crfMatch) {
|
||||
return {
|
||||
success: true,
|
||||
crf: parseInt(crfMatch[1]),
|
||||
vmaf: vmafMatch ? parseFloat(vmafMatch[1]) : null,
|
||||
error: null,
|
||||
output: output.substring(0, 500) // Truncate for logging
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
crf: null,
|
||||
vmaf: null,
|
||||
error: 'Could not parse CRF from ab-av1 output',
|
||||
output: output.substring(0, 500)
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Handle execution errors
|
||||
let errorMsg = error.message;
|
||||
if (error.killed) {
|
||||
errorMsg = 'ab-av1 timed out after 5 minutes';
|
||||
} else if (error.status) {
|
||||
errorMsg = `ab-av1 exited with code ${error.status}`;
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
crf: null,
|
||||
vmaf: null,
|
||||
error: errorMsg,
|
||||
output: error.stderr ? error.stderr.substring(0, 500) : ''
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Detect actual input container format via ffprobe
|
||||
const actualFormatName = file.ffProbeData?.format?.format_name || '';
|
||||
const looksLikeAppleMp4Family = actualFormatName.includes('mov') || actualFormatName.includes('mp4');
|
||||
@@ -821,7 +892,8 @@ const plugin = (file, librarySettings, inputs, otherArguments) => {
|
||||
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
|
||||
// Fall through to standard CRF encoding - use user's configured CRF
|
||||
bitrateControlInfo = `CRF ${finalCrf} (VMAF fallback - ab-av1 not found)`;
|
||||
} else {
|
||||
response.infoLog += `Using ab-av1 for quality-targeted encoding (target VMAF ${sanitized.vmaf_target}).\n`;
|
||||
response.infoLog += `ab-av1 binary: ${abav1Path}\n`;
|
||||
@@ -829,16 +901,44 @@ const plugin = (file, librarySettings, inputs, otherArguments) => {
|
||||
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 += `Running ab-av1 crf-search to find optimal CRF for VMAF ${vmafTarget}...\n`;
|
||||
response.infoLog += `Using ${sampleCount} sample segments for quality analysis.\n`;
|
||||
|
||||
// Execute ab-av1 crf-search synchronously
|
||||
const crfResult = executeAbAv1CrfSearch(
|
||||
abav1Path,
|
||||
file.file,
|
||||
vmafTarget,
|
||||
sampleCount,
|
||||
finalPreset
|
||||
);
|
||||
|
||||
if (crfResult.success && crfResult.crf !== null) {
|
||||
// Success! Use the found CRF
|
||||
const foundCrf = crfResult.crf;
|
||||
response.infoLog += `✅ ab-av1 found optimal CRF: ${foundCrf}`;
|
||||
if (crfResult.vmaf) {
|
||||
response.infoLog += ` (predicted VMAF: ${crfResult.vmaf})`;
|
||||
}
|
||||
response.infoLog += '\n';
|
||||
|
||||
// Update qualityArgs with the ab-av1 determined CRF
|
||||
// Replace the CRF in qualityArgs (which was set earlier with user's default)
|
||||
qualityArgs = qualityArgs.replace(/-crf \d+/, `-crf ${foundCrf}`);
|
||||
bitrateControlInfo = `VMAF-targeted CRF ${foundCrf} (target VMAF: ${vmafTarget}, achieved: ${crfResult.vmaf || 'unknown'})`;
|
||||
|
||||
// Store metadata for logging/debugging
|
||||
response.abav1CrfResult = foundCrf;
|
||||
response.abav1VmafResult = crfResult.vmaf;
|
||||
} else {
|
||||
// ab-av1 failed - fall back to user's configured CRF
|
||||
response.infoLog += `⚠️ ab-av1 crf-search failed: ${crfResult.error}\n`;
|
||||
if (crfResult.output) {
|
||||
response.infoLog += `ab-av1 output: ${crfResult.output}\n`;
|
||||
}
|
||||
response.infoLog += `Falling back to configured CRF ${finalCrf}.\n`;
|
||||
bitrateControlInfo = `CRF ${finalCrf} (VMAF fallback - ab-av1 failed)`;
|
||||
}
|
||||
}
|
||||
} else if (calculatedMaxrate) {
|
||||
// CRF Mode with maxrate cap
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Tdarr Plugin Suite Documentation
|
||||
|
||||
> **Version**: 2025-12-15
|
||||
> **Plugins**: misc_fixes v2.8 | stream_organizer v4.10 | audio_standardizer v1.15 | av1_converter v2.25
|
||||
> **Plugins**: misc_fixes v2.8 | stream_organizer v4.10 | audio_standardizer v1.15 | av1_converter v2.30
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user