252 lines
7.2 KiB
Go
Executable File
252 lines
7.2 KiB
Go
Executable File
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
|
|
}
|