const details = () => ({ id: 'Tdarr_Plugin_av1_svt_converter', Stage: 'Pre-processing', Name: 'Convert to AV1 SVT-AV1', Type: 'Video', 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. **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', Inputs: [ { name: 'crf', type: 'string', defaultValue: '26*', inputUI: { type: 'dropdown', options: [ '22', '24', '26*', '28', '30', '32', '34', '36', '38', '40', '42' ], }, tooltip: 'Quality setting (CRF). Higher = faster encoding, lower quality. (default: 26 for 1080p) 24–28 = high quality, 30+ = faster/transcoding. 10–20 = archival. For 4K, add +2; for 720p, subtract 2. [SVT-AV1 v3.0+]', }, { name: 'custom_maxrate', type: 'string', defaultValue: '0', inputUI: { type: 'text', }, tooltip: 'Maximum bitrate in kbps (0 or empty = unlimited). Used when target_bitrate_strategy is \'static\'. Capped CRF saves bandwidth on easy scenes while preserving quality on complex ones.', }, { name: 'target_bitrate_strategy', type: 'string', defaultValue: 'static*', inputUI: { type: 'dropdown', options: [ 'static*', 'match_source', '75%_source', '50%_source', '33%_source', '25%_source' ], }, 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', type: 'string', defaultValue: 'none*', inputUI: { type: 'dropdown', options: [ 'none*', '480p', '720p', '1080p', '1440p', '2160p' ], }, tooltip: 'Maximum output resolution. Videos exceeding this will be downscaled while maintaining aspect ratio. CRF adjustment (if enabled) applies to output resolution.', }, { name: 'resolution_crf_adjust', type: 'string', defaultValue: 'enabled*', inputUI: { type: 'dropdown', options: [ 'disabled', 'enabled*' ], }, tooltip: 'Auto-adjust CRF based on resolution: 4K gets +2 CRF, 1080p baseline, 720p gets -2 CRF. Improves efficiency with minimal quality impact.', }, { name: 'preset', type: 'string', defaultValue: '6*', inputUI: { type: 'dropdown', options: [ '-1', '0', '1', '2', '3', '4', '5', '6*', '7', '8', '9', '10', '11', '12' ], }, tooltip: 'SVT-AV1 preset. (default: 6) 6 = balanced speed/quality, 10 = fastest (real-time), 8–9 = very fast, 3–4 = best quality but slow. Higher = faster, lower = better quality. [v3.0+]', }, { name: 'tune', type: 'string', defaultValue: '0*', inputUI: { type: 'dropdown', options: [ '0*', '1', '2' ], }, tooltip: 'Tuning mode. (default: 0 VQ) 0 = VQ (best visual quality), 1 = PSNR (faster), 2 = SSIM (slowest). [v3.0+]', }, { name: 'scd', type: 'string', defaultValue: '1*', inputUI: { type: 'dropdown', options: [ '0', '1*' ], }, tooltip: 'Scene Change Detection. (default: 1) 0 = Off (fastest), 1 = On (better keyframe placement, ~5–10% slower).', }, { name: 'aq_mode', type: 'string', defaultValue: '2*', inputUI: { type: 'dropdown', options: [ '0', '1', '2*' ], }, tooltip: 'Adaptive Quantization. (default: 2) 0 = Off (fastest), 1 = Variance AQ (better quality, minor speed loss), 2 = DeltaQ AQ (best quality, 10–20% slower).', }, { name: 'lookahead', type: 'string', defaultValue: '-1*', inputUI: { type: 'dropdown', options: [ '-1*', '0', '60', '90', '120' ], }, tooltip: 'Lookahead frames. (default: -1) 0 = Off (fastest), -1 = Auto (good compromise), higher = better quality, slower encoding.', }, { name: 'enable_tf', type: 'string', defaultValue: '1*', inputUI: { type: 'dropdown', options: [ '0', '1*' ], }, tooltip: 'Temporal Filtering. (default: 1) 0 = Off (fastest), 1 = On (better noise reduction/quality, ~15–25% slower).', }, { name: 'threads', type: 'string', defaultValue: '0*', inputUI: { type: 'dropdown', options: [ '0*', '1', '2', '3', '4', '5', '6', '7', '8', '12', '16', '24', '32' ], }, tooltip: 'Number of encoding threads. 0 = Auto (use all cores, recommended). SVT-AV1 scales well with more threads.', }, { name: 'keyint', type: 'string', defaultValue: '-2*', inputUI: { type: 'dropdown', options: [ '-2*', '-1', '120', '240', '360', '480', '600', '720', '900', '1200' ], }, tooltip: 'Keyframe interval. (default: -2 ≈5s) -2=~5 seconds, -1=infinite (CRF only), higher = smaller files but worse seeking; lower = better quality/seeking, larger files.', }, { name: 'hierarchical_levels', type: 'string', defaultValue: '4*', inputUI: { type: 'dropdown', options: [ '2', '3', '4*', '5' ], }, tooltip: 'Hierarchical levels: 2=3 temporal layers, 3=4 temporal layers, 4=5 temporal layers (recommended), 5=6 temporal layers. Controls GOP structure complexity.', }, { name: 'film_grain', type: 'string', defaultValue: '0*', inputUI: { type: 'dropdown', options: [ '0*', '1', '5', '10', '15', '20', '25', '30', '35', '40', '45', '50' ], }, tooltip: 'Film grain synthesis: 0 = Off (fastest), 1–50 = denoising level (slower, more natural grain).', }, { name: 'input_depth', type: 'string', defaultValue: '10*', inputUI: { type: 'dropdown', options: [ '8', '10*' ], }, tooltip: 'Output bit depth: 8 = faster encoding, 10 = better quality (prevents banding), ~10-20% slower. Recommended: 10-bit for high-quality sources.', }, { name: 'fast_decode', type: 'string', defaultValue: '0*', inputUI: { type: 'dropdown', options: [ '0*', '1' ], }, tooltip: 'Fast decode optimization. (default: 0) 1 = moderate decode speed improvement, 0 = off (best compression). [v3.0+]', }, { name: 'container', type: 'string', defaultValue: 'mp4*', inputUI: { type: 'dropdown', options: [ 'mp4*', 'mkv', 'webm', 'original' ], }, tooltip: 'Output container format. "mp4" = best compatibility. "original" keeps input container.', }, { name: 'skip_hevc', type: 'string', defaultValue: 'enabled*', inputUI: { type: 'dropdown', options: [ 'disabled', 'enabled*' ], }, tooltip: 'Skip HEVC/H.265 files without converting. Useful if you want to handle HEVC files separately or they are already efficient.', }, { name: 'force_transcode', type: 'string', defaultValue: 'disabled*', inputUI: { type: 'dropdown', options: [ 'disabled*', 'enabled' ], }, tooltip: 'Force transcoding even if the file is already AV1. Useful for changing quality or preset.', } ], }); // eslint-disable-next-line @typescript-eslint/no-unused-vars const plugin = (file, librarySettings, inputs, otherArguments) => { const lib = require('../methods/lib')(); // Initialize response first for error handling const response = { processFile: false, preset: '', container: '', handbrakeMode: false, ffmpegMode: false, reQueueAfter: false, infoLog: '', }; try { // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-param-reassign inputs = lib.loadDefaultValues(inputs, details); // Strip UI star markers from input values (Tdarr uses '*' to indicate default values in UI) const stripStar = (value) => { if (typeof value === 'string') { return value.replace(/\*/g, ''); } return value; }; // Sanitize UI-starred defaults const sanitized = { crf: stripStar(inputs.crf), preset: stripStar(inputs.preset), tune: stripStar(inputs.tune), scd: stripStar(inputs.scd), aq_mode: stripStar(inputs.aq_mode), threads: stripStar(inputs.threads), keyint: stripStar(inputs.keyint), hierarchical_levels: stripStar(inputs.hierarchical_levels), film_grain: stripStar(inputs.film_grain), input_depth: stripStar(inputs.input_depth), fast_decode: stripStar(inputs.fast_decode), lookahead: stripStar(inputs.lookahead), enable_tf: stripStar(inputs.enable_tf), container: stripStar(inputs.container), max_resolution: stripStar(inputs.max_resolution), 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 const actualFormatName = file.ffProbeData?.format?.format_name || ''; const looksLikeAppleMp4Family = actualFormatName.includes('mov') || actualFormatName.includes('mp4'); // Detect Apple/broadcast streams that are problematic in MKV or missing codec name const unsupportedSubtitleIdx = []; const unsupportedDataIdx = []; try { file.ffProbeData.streams.forEach((s, idx) => { if (s.codec_type === 'subtitle') { const name = (s.codec_name || '').toLowerCase(); const tag = (s.codec_tag_string || '').toLowerCase(); if (!name) { // skip subs missing codec_name (e.g., WEBVTT detection failures) unsupportedSubtitleIdx.push(idx); } else if (name === 'eia_608' || name === 'cc_dec') { unsupportedSubtitleIdx.push(idx); } else if (name === 'tx3g' || tag === 'tx3g') { // tx3g sometimes shows as timed text in MP4; in mkv it may appear as bin_data unsupportedSubtitleIdx.push(idx); } } else if (s.codec_type === 'data') { const name = (s.codec_name || '').toLowerCase(); const tag = (s.codec_tag_string || '').toLowerCase(); if (name === 'bin_data' || tag === 'tx3g') { unsupportedDataIdx.push(idx); } } }); } catch (e) { // ignore detection errors, continue safely } // Check if file is already AV1 and skip if not forcing transcode const isAV1 = file.ffProbeData.streams.some(stream => stream.codec_type === 'video' && (stream.codec_name === 'av01' || stream.codec_name === 'av1' || stream.codec_name === 'libsvtav1') ); if (isAV1 && sanitized.force_transcode !== 'enabled') { response.processFile = false; response.infoLog += 'File is already AV1 encoded and force_transcode is disabled. Skipping.\n'; return response; } // Check if file is HEVC and skip if skip_hevc is enabled const isHEVC = file.ffProbeData.streams.some(stream => stream.codec_type === 'video' && (stream.codec_name === 'hevc' || stream.codec_name === 'h265' || stream.codec_name === 'libx265') ); if (isHEVC && sanitized.skip_hevc === 'enabled') { response.processFile = false; response.infoLog += 'File is HEVC/H.265 encoded and skip_hevc is enabled. Skipping for separate processing.\n'; return response; } // Validate video stream exists const videoStream = file.ffProbeData.streams.find(s => s.codec_type === 'video'); if (!videoStream) { response.processFile = false; response.infoLog += 'Error: No video stream found in file. Skipping.\n'; return response; } // Use specified preset const finalPreset = sanitized.preset; response.infoLog += `Using preset ${finalPreset} (speed-optimized default).\n`; // Use specified thread count const threadCount = sanitized.threads; response.infoLog += `Using ${threadCount} encoding threads.\n`; // Resolution mapping and downscaling logic const resolutionMap = { '480p': 480, '720p': 720, '1080p': 1080, '1440p': 1440, '2160p': 2160 }; // videoStream was validated and assigned earlier (after HEVC skip check) let scaleFilter = ''; let outputHeight = null; // Detect HDR metadata for color preservation let hdrArgs = ''; const colorTransfer = videoStream.color_transfer || ''; const colorPrimaries = videoStream.color_primaries || ''; const colorSpace = videoStream.color_space || ''; // Check for HDR10, HLG, or PQ transfer characteristics const isHDR10 = colorTransfer === 'smpte2084'; // PQ const isHLG = colorTransfer === 'arib-std-b67'; // HLG const isHDR = (isHDR10 || isHLG) && ( colorPrimaries === 'bt2020' || colorSpace === 'bt2020nc' || colorSpace === 'bt2020c' ); if (isHDR) { // Preserve HDR color metadata hdrArgs = ` -colorspace ${colorSpace || 'bt2020nc'} -color_trc ${colorTransfer || 'smpte2084'} -color_primaries ${colorPrimaries || 'bt2020'}`; response.infoLog += `HDR content detected (${colorTransfer}/${colorPrimaries}), preserving color metadata.\n`; } if (videoStream && videoStream.height && sanitized.max_resolution !== 'none') { const inputHeight = videoStream.height; const maxHeight = resolutionMap[sanitized.max_resolution]; if (maxHeight && inputHeight > maxHeight) { // Downscale needed - use scale filter with -2 to maintain aspect ratio and ensure even dimensions outputHeight = maxHeight; scaleFilter = `-vf "scale=-2:${maxHeight}"`; response.infoLog += `Downscaling from ${inputHeight}p to ${maxHeight}p while maintaining aspect ratio.\n`; } else if (maxHeight) { // Input is already at or below max resolution outputHeight = inputHeight; response.infoLog += `Input resolution ${inputHeight}p is within max limit of ${maxHeight}p, no downscaling needed.\n`; } else { // No max resolution set outputHeight = inputHeight; } } else if (videoStream && videoStream.height) { // No max resolution constraint outputHeight = videoStream.height; } // Resolution-based CRF adjustment (applies to OUTPUT resolution after any downscaling) let finalCrf = sanitized.crf; if (sanitized.resolution_crf_adjust === 'enabled' && outputHeight) { const baseCrf = parseInt(sanitized.crf); // Validate CRF is a valid number if (isNaN(baseCrf) || baseCrf < 0 || baseCrf > 63) { response.infoLog += `Warning: Invalid CRF value "${sanitized.crf}", using default.\n`; finalCrf = '26'; } else { if (outputHeight >= 2160) { // 4K finalCrf = Math.min(63, baseCrf + 2).toString(); response.infoLog += `4K output resolution detected, CRF adjusted from ${sanitized.crf} to ${finalCrf}.\n`; } else if (outputHeight <= 720) { // 720p or lower finalCrf = Math.max(1, baseCrf - 2).toString(); response.infoLog += `720p or lower output resolution detected, CRF adjusted from ${sanitized.crf} to ${finalCrf}.\n`; } else { response.infoLog += `1080p output resolution detected, using base CRF ${finalCrf}.\n`; } } } else if (sanitized.resolution_crf_adjust === 'enabled') { response.infoLog += `Resolution-based CRF adjustment enabled but video height not detected, using base CRF ${finalCrf}.\n`; } // Build SVT-AV1 parameters string const svtParams = [ `preset=${finalPreset}`, `tune=${sanitized.tune}`, `scd=${sanitized.scd}`, `aq-mode=${sanitized.aq_mode}`, `lp=${threadCount}`, `keyint=${sanitized.keyint}`, `hierarchical-levels=${sanitized.hierarchical_levels}`, `film-grain=${sanitized.film_grain}`, `input-depth=${sanitized.input_depth}`, `fast-decode=${sanitized.fast_decode}`, `lookahead=${sanitized.lookahead}`, `enable-tf=${sanitized.enable_tf}` ].join(':'); // Set up FFmpeg arguments for CRF quality control with fixed qmin/qmax let qualityArgs = `-crf ${finalCrf} -qmin 10 -qmax 50`; let bitrateControlInfo = `Using CRF mode with value ${finalCrf}`; // Explicitly set pixel format for 10-bit to ensure correct encoding if (sanitized.input_depth === '10') { qualityArgs += ' -pix_fmt yuv420p10le'; response.infoLog += `10-bit encoding enabled with yuv420p10le pixel format.\n`; } // Source bitrate detection for target_bitrate_strategy let sourceBitrateKbps = null; if (videoStream) { // Try to get bitrate from video stream first if (videoStream.bit_rate) { sourceBitrateKbps = Math.round(parseInt(videoStream.bit_rate) / 1000); response.infoLog += `Detected video stream bitrate: ${sourceBitrateKbps}k.\n`; } else if (file.ffProbeData?.format?.bit_rate) { // Fall back to overall file bitrate sourceBitrateKbps = Math.round(parseInt(file.ffProbeData.format.bit_rate) / 1000); response.infoLog += `Detected file bitrate (video stream bitrate unavailable): ${sourceBitrateKbps}k.\n`; } } // 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 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; break; case '75%_source': multiplier = 0.75; break; case '50%_source': multiplier = 0.50; break; case '33%_source': multiplier = 0.33; break; case '25%_source': multiplier = 0.25; break; } 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`; } } // Priority 2: custom_maxrate (if strategy is static or failed) if (!calculatedMaxrate && sanitized.custom_maxrate && sanitized.custom_maxrate !== '' && sanitized.custom_maxrate !== '0') { const customValue = parseInt(sanitized.custom_maxrate); if (!isNaN(customValue) && customValue > 0) { 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`; } } // Apply calculated maxrate if any method succeeded // Enforce minimum bitrate threshold to prevent unusable output (resolution-aware) const getMinBitrate = (height) => { if (height >= 2160) return 2000; // 4K if (height >= 1440) return 1500; // 1440p if (height >= 1080) return 800; // 1080p if (height >= 720) return 500; // 720p return 250; // 480p and below }; const minBitrate = getMinBitrate(outputHeight || 1080); // 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; } // 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) { // 4K: 2x1 tiles = 3 tiles total (better parallelism for 4K encoding) tileArgs = ':tile-columns=2:tile-rows=1'; response.infoLog += '4K content: Adding tile-columns=2, tile-rows=1 for improved parallelism.\n'; } else if (outputHeight && outputHeight >= 1440) { // 1440p: 1x0 tiles = 2 tiles total (balanced for 1440p) tileArgs = ':tile-columns=1:tile-rows=0'; response.infoLog += '1440p content: Adding tile-columns=1 for balanced parallelism.\n'; } // 1080p and below: No tiles (overhead not worth it) // Build mapping with per-stream exclusions if needed let mapArgs = '-map 0'; const hasUnsupportedStreams = unsupportedSubtitleIdx.length > 0 || unsupportedDataIdx.length > 0; if (hasUnsupportedStreams) { [...unsupportedSubtitleIdx, ...unsupportedDataIdx].forEach((idx) => { mapArgs += ` -map -0:${idx}`; }); response.infoLog += `Excluding unsupported streams from mapping: subtitles[${unsupportedSubtitleIdx.join(', ')}] data[${unsupportedDataIdx.join(', ')}].\n`; } // Set up FFmpeg arguments for AV1 SVT conversion // Use explicit stream mapping instead of -dn to handle data streams precisely const svtParamsWithTiles = svtParams + tileArgs; response.preset = `${scaleFilter ? ' ' + scaleFilter : ''} -c:v libsvtav1 ${qualityArgs}${hdrArgs} -svtav1-params "${svtParamsWithTiles}" -c:a copy -c:s copy ${mapArgs}`; // Set container with Apple-specific handling // If user asked for MKV but input is MP4/MOV family and has unsupported streams, prefer MP4 to avoid mux errors if (sanitized.container === 'original') { response.container = `.${file.container}`; if (looksLikeAppleMp4Family && response.container === '.mkv' && hasUnsupportedStreams) { response.infoLog += 'Detected MP4/MOV input with Apple/broadcast streams; overriding output container to .mp4 to preserve compatibility.\n'; response.container = '.mp4'; } } else { response.container = `.${sanitized.container}`; // WebM container validation - warn about potential compatibility if (sanitized.container === 'webm') { response.infoLog += 'Note: WebM container selected. Ensure audio is Opus/Vorbis for full compatibility.\n'; if (hasUnsupportedStreams) { response.infoLog += 'Warning: WebM does not support all subtitle formats. Subtitles may be dropped.\n'; } } // MKV container handling with user warning if (sanitized.container === 'mkv' && (looksLikeAppleMp4Family || hasUnsupportedStreams)) { response.infoLog += 'Warning: MKV requested but file has Apple/broadcast streams that may cause issues. Consider using MP4 container.\n'; // Don't force override - let user decide, just warn } } response.ffmpegMode = true; response.handbrakeMode = false; response.reQueueAfter = true; response.processFile = true; if (isAV1) { response.infoLog += `File is AV1 but force transcoding is enabled. ${bitrateControlInfo}.\n`; } else if (isHEVC) { response.infoLog += `Converting HEVC to AV1. ${bitrateControlInfo}.\n`; } else { response.infoLog += `Converting ${file.ffProbeData.streams.find(s => s.codec_type === 'video')?.codec_name || 'unknown'} to AV1. ${bitrateControlInfo}.\n`; } response.infoLog += `Using SVT-AV1 preset: ${finalPreset}, tune: ${sanitized.tune} (VQ-optimized when 0), threads: ${threadCount}\n`; response.infoLog += `Encoding params - SCD: ${sanitized.scd}, AQ: ${sanitized.aq_mode}, Lookahead: ${sanitized.lookahead}, TF: ${sanitized.enable_tf}\n`; response.infoLog += `Quality control - CRF: ${finalCrf}, Fixed QMin: 10, Fixed QMax: 50, Film grain: ${sanitized.film_grain}\n`; response.infoLog += `Output container: ${response.container}\n`; return response; } catch (error) { // Comprehensive error handling response.processFile = false; response.preset = ''; response.container = `.${file.container || 'mkv'}`; response.reQueueAfter = false; // Provide detailed error information response.infoLog = `💥 Plugin error: ${error.message}\n`; // Add stack trace for debugging (first 5 lines) if (error.stack) { const stackLines = error.stack.split('\n').slice(0, 5).join('\n'); response.infoLog += `Stack trace:\n${stackLines}\n`; } // Log additional context response.infoLog += `File: ${file.file}\n`; response.infoLog += `Container: ${file.container}\n`; return response; } }; module.exports.details = details; module.exports.plugin = plugin;