210 lines
6.3 KiB
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)
|
|
}
|