package encoding import ( "fmt" "os/exec" "strconv" "strings" ) // AV1AdvancedParams contains advanced SVT-AV1 encoding parameters type AV1AdvancedParams struct { CRF int // Default: 29 Preset int // Default: 10 Tune int // Default: 0 (VQ) SCD bool // Default: true (Scene Change Detection) AQMode int // Default: 2 (DeltaQ) Lookahead int // Default: -1 (auto) EnableTF bool // Default: true (Temporal Filtering) Threads int // Default: 0 (auto) Keyint int // Default: -2 (~5 seconds) HierarchicalLevels int // Default: 4 FilmGrain int // Default: 0 InputDepth int // Default: 8 FastDecode bool // Default: true ResolutionCRFAdjust bool // Default: true MaxrateCap int // Default: 0 (unlimited) SkipHEVC bool // Default: true ForceTranscode bool // Default: false } // DefaultAV1AdvancedParams returns default AV1 advanced parameters // Updated to align with Tdarr AV1 defaults (CRF 29, Preset 10) func DefaultAV1AdvancedParams() AV1AdvancedParams { return AV1AdvancedParams{ CRF: 29, Preset: 10, Tune: 0, SCD: true, AQMode: 2, Lookahead: -1, EnableTF: true, Threads: 0, Keyint: -2, HierarchicalLevels: 4, FilmGrain: 0, InputDepth: 8, FastDecode: true, ResolutionCRFAdjust: true, MaxrateCap: 0, SkipHEVC: false, // Default: transcode HEVC files ForceTranscode: false, // Default: skip AV1 files (use --force to transcode) } } // GetVideoResolution returns the video height for resolution-based CRF adjustment func GetVideoResolution(file string) (int, error) { cmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", "v:0", "-show_entries", "stream=height", "-of", "csv=p=0", file) output, err := cmd.Output() if err != nil { return 0, err } heightStr := strings.TrimSpace(string(output)) height, err := strconv.Atoi(heightStr) if err != nil { return 0, err } return height, nil } // AdjustCRFForResolution adjusts CRF based on video resolution func AdjustCRFForResolution(baseCRF int, height int) int { if height >= 2160 { // 4K return min(63, baseCRF+2) } else if height <= 720 { // 720p or lower return max(1, baseCRF-2) } // 1080p - use base CRF return baseCRF } // GetVideoWidth returns the video width func GetVideoWidth(file string) (int, error) { cmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", "v:0", "-show_entries", "stream=width", "-of", "csv=p=0", file) output, err := cmd.Output() if err != nil { return 0, err } widthStr := strings.TrimSpace(string(output)) width, err := strconv.Atoi(widthStr) if err != nil { return 0, err } return width, nil } // GetResolutionPresetHeight converts a resolution preset to pixel height // Returns 0 for "original" or empty string func GetResolutionPresetHeight(preset string) int { preset = strings.ToLower(strings.TrimSpace(preset)) switch preset { case "480p": return 480 case "720p": return 720 case "1080p": return 1080 case "original", "": return 0 default: return 0 } } // BuildScaleFilter builds an FFmpeg scale filter for resolution scaling // Returns empty string if no scaling is needed or preset is "original" // Only scales down, never upscales func BuildScaleFilter(file string, preset string) (string, error) { targetHeight := GetResolutionPresetHeight(preset) // No scaling if preset is "original" or empty if targetHeight == 0 { return "", nil } // Get current video height currentHeight, err := GetVideoResolution(file) if err != nil { return "", err } // Don't upscale - only scale down if currentHeight <= targetHeight { return "", nil } // Build scale filter: scale to target height, width auto-calculated maintaining aspect ratio // -2 ensures width is divisible by 2 (required for most codecs) scaleFilter := fmt.Sprintf("-vf scale=-2:%d", targetHeight) return scaleFilter, nil } // GetVideoCodec returns the video codec name from a file func GetVideoCodec(file string) (string, error) { cmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", "v:0", "-show_entries", "stream=codec_name", "-of", "csv=p=0", file) output, err := cmd.Output() if err != nil { return "", err } codec := strings.TrimSpace(string(output)) return codec, nil } // IsAV1Codec checks if the codec is AV1 func IsAV1Codec(codec string) bool { codec = strings.ToLower(codec) return codec == "av01" || codec == "av1" || codec == "libsvtav1" } // IsHEVCCodec checks if the codec is HEVC/H.265 func IsHEVCCodec(codec string) bool { codec = strings.ToLower(codec) return codec == "hevc" || codec == "h265" || codec == "libx265" } // BuildSVTParams builds the SVT-AV1 parameter string for FFmpeg func BuildSVTParams(params AV1AdvancedParams) string { svtParams := []string{ fmt.Sprintf("preset=%d", params.Preset), fmt.Sprintf("tune=%d", params.Tune), fmt.Sprintf("scd=%d", boolToInt(params.SCD)), fmt.Sprintf("aq-mode=%d", params.AQMode), fmt.Sprintf("lp=%d", params.Threads), fmt.Sprintf("keyint=%d", params.Keyint), fmt.Sprintf("hierarchical-levels=%d", params.HierarchicalLevels), fmt.Sprintf("film-grain=%d", params.FilmGrain), fmt.Sprintf("input-depth=%d", params.InputDepth), fmt.Sprintf("fast-decode=%d", boolToInt(params.FastDecode)), fmt.Sprintf("lookahead=%d", params.Lookahead), fmt.Sprintf("enable-tf=%d", boolToInt(params.EnableTF)), } return strings.Join(svtParams, ":") } // BuildAV1QualityArgs builds FFmpeg quality arguments for AV1 encoding func BuildAV1QualityArgs(params AV1AdvancedParams, finalCRF int) (string, string) { // Base CRF with fixed qmin/qmax qualityArgs := fmt.Sprintf("-crf %d -qmin 10 -qmax 50", finalCRF) bitrateControlInfo := fmt.Sprintf("Using CRF mode with value %d", finalCRF) // Add maxrate cap if specified if params.MaxrateCap > 0 { bufsize := int(float64(params.MaxrateCap) * 1.5) // Buffer size = 1.5x maxrate qualityArgs += fmt.Sprintf(" -maxrate %dk -bufsize %dk", params.MaxrateCap, bufsize) bitrateControlInfo += fmt.Sprintf(" with capped bitrate at %dk (bufsize: %dk)", params.MaxrateCap, bufsize) } return qualityArgs, bitrateControlInfo } // ShouldSkipFile checks if a file should be skipped based on codec and params func ShouldSkipFile(file string, params AV1AdvancedParams) (bool, string) { codec, err := GetVideoCodec(file) if err != nil { return false, "" } // Check if already AV1 and force_transcode is disabled if IsAV1Codec(codec) && !params.ForceTranscode { return true, "File is already AV1 encoded and force_transcode is disabled" } // Check if HEVC and skip_hevc is enabled (default: false, so HEVC is transcoded) if IsHEVCCodec(codec) && params.SkipHEVC { return true, "File is HEVC/H.265 encoded and skip_hevc is enabled" } // All other codecs (VP8, VP9, H.264, etc.) are transcoded by default return false, "" } // Helper functions func boolToInt(b bool) int { if b { return 1 } return 0 } func min(a, b int) int { if a < b { return a } return b } func max(a, b int) int { if a > b { return a } return b }