Files
av1-go/video.go

210 lines
6.3 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
)
var SvtAv1Params = map[string]interface{}{
"speed": "slower",
"quality": "medium",
"film-grain": 6,
"color-primaries": 1,
"transfer-characteristics": 1,
"matrix-coefficients": 1,
"scd": 0,
"keyint": 0,
"lp": 2,
"auto-tiling": 1,
"tune": 1,
"progress": 2,
}
// ConvertVideo handles the video conversion pipeline
func ConvertVideo(sourceFileBase, sourceFileFull string, isVfr bool, targetCfrFps string, autocropFilter string) (string, string, error) {
LogInfo(" --- Starting Video Processing ---")
vpyFile := sourceFileBase + ".vpy"
utVideoFile := sourceFileBase + ".ut.mkv"
encodedVideoFile := "temp-" + sourceFileBase + ".mkv"
var handbrakeIntermediate string
currentInputForUt := sourceFileFull
if isVfr && targetCfrFps != "" {
LogInfo(" - Source is VFR. Converting to CFR (%s) with HandBrakeCLI...", targetCfrFps)
handbrakeIntermediate = sourceFileBase + ".cfr_temp.mkv"
hbArgs := []string{
"--input", sourceFileFull,
"--output", handbrakeIntermediate,
"--cfr",
"--rate", targetCfrFps,
"--encoder", "x264_10bit",
"--quality", "0",
"--encoder-preset", "superfast",
"--encoder-tune", "fastdecode",
"--audio", "none",
"--subtitle", "none",
"--crop-mode", "none",
}
LogInfo(" - Running HandBrakeCLI: %s", strings.Join(hbArgs, " "))
_, err := RunCmd("HandBrakeCLI", hbArgs, false)
if err != nil {
LogInfo(" - Error during HandBrakeCLI execution: %v", err)
LogInfo(" - Proceeding with original source for UTVideo.")
handbrakeIntermediate = ""
} else {
// Check if file exists and size > 0
fi, err := os.Stat(handbrakeIntermediate)
if err == nil && fi.Size() > 0 {
LogInfo(" - HandBrake VFR to CFR conversion successful: %s", handbrakeIntermediate)
currentInputForUt = handbrakeIntermediate
} else {
LogInfo(" - Warning: HandBrakeCLI VFR-to-CFR conversion failed or produced an empty file.")
handbrakeIntermediate = ""
}
}
}
LogInfo(" - Creating UTVideo intermediate file (overwriting if exists)...")
// Check source codec
ffprobeArgs := []string{
"-v", "error", "-select_streams", "v:0",
"-show_entries", "stream=codec_name", "-of", "default=noprint_wrappers=1:nokey=1",
currentInputForUt,
}
sourceCodec, _ := RunCmd("ffprobe", ffprobeArgs, true)
sourceCodec = strings.TrimSpace(sourceCodec)
videoCodecArgs := []string{"-c:v", "utvideo"}
if sourceCodec == "utvideo" && currentInputForUt == sourceFileFull {
LogInfo(" - Source is already UTVideo. Copying video stream...")
videoCodecArgs = []string{"-c:v", "copy"}
}
ffmpegArgs := []string{
"-hide_banner", "-v", "quiet", "-stats", "-y", "-i", currentInputForUt,
"-map", "0:v:0", "-map_metadata", "-1", "-map_chapters", "-1", "-an", "-sn", "-dn",
}
if autocropFilter != "" {
ffmpegArgs = append(ffmpegArgs, "-vf", autocropFilter)
}
ffmpegArgs = append(ffmpegArgs, videoCodecArgs...)
ffmpegArgs = append(ffmpegArgs, utVideoFile)
_, err := RunCmd("ffmpeg", ffmpegArgs, false)
if err != nil {
return "", "", fmt.Errorf("failed to create UTVideo: %w", err)
}
LogInfo(" - Indexing UTVideo file with ffmsindex for VapourSynth...")
_, err = RunCmd("ffmsindex", []string{"-f", utVideoFile}, false)
if err != nil {
return "", "", fmt.Errorf("ffmsindex failed: %w", err)
}
utVideoFullPath, _ := filepath.Abs(utVideoFile)
// Escape backslashes for python string
utVideoFullPath = strings.ReplaceAll(utVideoFullPath, "\\", "\\\\")
vpyContent := fmt.Sprintf(`import vapoursynth as vs
core = vs.core
core.num_threads = 4
clip = core.ffms2.Source(source=r'''%s''')
clip = core.resize.Point(clip, format=vs.YUV420P10, matrix_in_s="709") # type: ignore
clip.set_output()
`, utVideoFullPath)
err = os.WriteFile(vpyFile, []byte(vpyContent), 0666)
if err != nil {
return "", "", fmt.Errorf("failed to write .vpy file: %w", err)
}
LogInfo(" - Starting AV1 encode with av1an (this will take a long time)...")
totalCores := GetTotalCores()
if totalCores == 0 {
totalCores = 4
}
workers := (totalCores / 2) - 1
if workers < 1 {
workers = 1
}
LogInfo(" - Using %d workers for av1an (Total Cores: %d).", workers, totalCores)
var params []string
for k, v := range SvtAv1Params {
params = append(params, fmt.Sprintf("--%s %v", k, v))
}
av1anParamsStr := strings.Join(params, " ")
LogInfo(" - Using SVT-AV1 parameters: %s", av1anParamsStr)
av1anArgs := []string{
"-i", vpyFile, "-o", encodedVideoFile, "-n",
"-e", "svt-av1", "--resume", "--sc-pix-format", "yuv420p", "-c", "mkvmerge",
"--set-thread-affinity", "2", "--pix-format", "yuv420p10le", "--force", "--no-defaults",
"-w", fmt.Sprintf("%d", workers),
"-v", av1anParamsStr,
}
_, err = RunCmd("av1an", av1anArgs, false)
if err != nil {
return "", "", fmt.Errorf("av1an failed: %w", err)
}
LogInfo(" --- Finished Video Processing ---")
return encodedVideoFile, handbrakeIntermediate, nil
}
func IsFfmpegDecodable(filePath string) bool {
args := []string{
"-v", "error", "-i", filePath, "-map", "0:a:0", "-t", "1", "-f", "null", "-",
}
_, err := RunCmd("ffmpeg", args, false)
return err == nil
}
func DetectAutocropFilter(inputFile string) string {
// Probe duration
out, err := RunCmd("ffprobe", []string{"-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", inputFile}, true)
if err != nil {
return ""
}
duration, _ := strconv.Atoi(strings.Split(strings.TrimSpace(out), ".")[0])
// Probe resolution
out, err = RunCmd("ffprobe", []string{"-v", "error", "-select_streams", "v", "-show_entries", "stream=width,height,disposition", "-of", "json", inputFile}, true)
if err != nil {
return ""
}
var probe FFprobeOutput
json.Unmarshal([]byte(out), &probe)
width := 0
height := 0
for _, s := range probe.Streams {
// Check disposition if needed, but usually first video stream is fine or the one not attached pic
// Simplified check:
if s.Width > 0 && s.Height > 0 {
width = s.Width
height = s.Height
break
}
}
if width == 0 || height == 0 {
return ""
}
return DetectAutocrop(inputFile, duration, width, height)
}