Files
gwutilz/gwencoder/pkg/encoding/av1.go
2026-03-23 15:48:34 -07:00

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
}