diff --git a/Local/Tdarr_Plugin_av1_svt_converter.js b/Local/Tdarr_Plugin_av1_svt_converter.js index 716b369..401123d 100644 --- a/Local/Tdarr_Plugin_av1_svt_converter.js +++ b/Local/Tdarr_Plugin_av1_svt_converter.js @@ -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 diff --git a/PLUGIN_DOCUMENTATION.md b/PLUGIN_DOCUMENTATION.md index a538a8d..c7bf48b 100644 --- a/PLUGIN_DOCUMENTATION.md +++ b/PLUGIN_DOCUMENTATION.md @@ -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 ---