Files
tdarr-plugs/Local/Tdarr_Plugin_av1_svt_converter.js
Tdarr Plugin Developer aa71eb96d7 Initial commit: Tdarr plugin stack
Plugins:
- misc_fixes v2.8: Pre-processing, container remux, stream conforming
- stream_organizer v4.8: English priority, subtitle extraction, SRT conversion
- combined_audio_standardizer v1.13: AAC/Opus encoding, downmix creation
- av1_svt_converter v2.22: AV1 video encoding via SVT-AV1

Structure:
- Local/ - Plugin .js files (mount in Tdarr)
- agent_notes/ - Development documentation
- Latest-Reports/ - Error logs for analysis
2025-12-15 11:33:36 -08:00

758 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 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 35 and/or lower CRF for higher quality when speed is less important.
`,
Version: '2.22',
Tags: 'video,av1,svt,quality,performance,speed-optimized,capped-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) 2428 = high quality, 30+ = faster/transcoding. 1020 = 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 maxrate relative to detected source bitrate.',
},
{
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), 89 = very fast, 34 = 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, ~510% 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, 1020% 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, ~1525% 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), 150 = 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),
skip_hevc: stripStar(inputs.skip_hevc),
force_transcode: stripStar(inputs.force_transcode),
};
// 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`;
}
}
// Calculate target maxrate using precedence logic
let calculatedMaxrate = null;
let maxrateSource = '';
// Priority 1: target_bitrate_strategy (if not static)
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;
}
calculatedMaxrate = Math.round(sourceBitrateKbps * multiplier);
maxrateSource = `target_bitrate_strategy '${sanitized.target_bitrate_strategy}': Source ${sourceBitrateKbps}k → Maxrate ${calculatedMaxrate}k`;
response.infoLog += `Using ${maxrateSource}.\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) {
calculatedMaxrate = customValue;
maxrateSource = `custom_maxrate: ${calculatedMaxrate}k`;
response.infoLog += `Using ${maxrateSource}.\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);
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) {
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 {
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 = `<io>${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;