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) }