package main import ( "fmt" "path/filepath" "runtime" "log" "os" "math/rand" "net/http" "strings" "lcthw.dev/go/vidcrunch/config" "github.com/modfy/fluent-ffmpeg" "strconv" "os/exec" "slices" "iter" ) func AudioOnly(codec config.CodecOpts) bool { return codec.Scale == "" && codec.VideoCodec == "none" } func VideoOnly(codec config.CodecOpts) bool { return codec.AudioCodec == "none" } func SetCodecTarget(codec *config.CodecOpts, path string, encoding config.EncodeOpts) { base := filepath.Base(path) target := filepath.Join(encoding.OutDir, base) cleaned := filepath.Clean(target) dir, file := filepath.Split(cleaned) ext := filepath.Ext(file) base, found := strings.CutSuffix(file, ext) if !found { panic("no extension found?!") } if AudioOnly(*codec) { renamed := fmt.Sprint(base, ".audio.", encoding.Format) codec.Target = filepath.Join(dir, renamed) } else { dim := strings.Replace(codec.Scale, ":", ".", 1) renamed := fmt.Sprint(base, ".", dim, ".", encoding.Format) codec.Target = filepath.Join(dir, renamed) } } func Run(encoding config.EncodeOpts, codec config.CodecOpts, pass int, pid int, input string, output string) { encode := fluentffmpeg.NewCommand("") extras := []string{ "-pix_fmt", "yuv420p", } if encoding.Passes > 1 { extras = append(extras, "-pass", fmt.Sprint(pass), "-passlogfile", fmt.Sprintf("ffmpegpass-%x.log", pid)) } if encoding.Preset != "" { extras = append(extras, "-preset", encoding.Preset) } if codec.Resize { extras = append(extras, "-vf", fmt.Sprintf("scale=%s:flags=lanczos", codec.Scale), "-aspect", codec.Scale) } if pass != encoding.Passes || VideoOnly(codec) { extras = append(extras, "-an") } else { encode.AudioCodec(codec.AudioCodec) extras = append(extras, "-b:a", fmt.Sprint(codec.AudioBitrate * 1024)) } if encoding.Test > 0 { encode.InputOptions("-ss", fmt.Sprintf("00:%d", encoding.TestStart)) extras = append(extras, "-t", fmt.Sprint(encoding.Test)) } if codec.VideoCodec == "none" { extras = append(extras, "-vn") } else { encode.VideoCodec(codec.VideoCodec). VideoBitRate(codec.VideoBitrate * 1024). FrameRate(encoding.FPS). ConstantRateFactor(encoding.CRF) } extras = append(extras, encoding.Extras...) if encoding.Dash { extras = append(extras, "-dash", "1") } encode.OutputOptions(extras...) cmd := encode.InputPath(input). OutputFormat(encoding.Format). OutputPath(output). OutputLogs(os.Stdout). Overwrite(true). Build() fmt.Println(">", cmd.String()) err := cmd.Run() if err != nil { log.Fatalf("%v", err) } } func DevNull() string { if runtime.GOOS == "windows" { return "NUL" } else { return "/dev/null" } } func RenderFile(encoding config.EncodeOpts, pid int, path string, force bool) { for i := 0; i < len(encoding.Variants); i++ { codec := &encoding.Variants[i] SetCodecTarget(codec, path, encoding) _, err := os.Stat(codec.Target) if err == nil && !force { fmt.Println("^^^ SKIP", path, "->", codec.Target) return } fmt.Println("--- PATH", path, "->", codec.Target) for i := 1; i < encoding.Passes; i++ { Run(encoding, *codec, i, pid, path, DevNull()) } Run(encoding, *codec, encoding.Passes, pid, path, codec.Target) } SaveMPD(encoding) } func StrSeq(up_to int) iter.Seq[string] { return func (yield func(x string) bool) { for i := range up_to { if !yield(strconv.Itoa(i)) { return } } } } func SaveMPD(encoding config.EncodeOpts) { args := make([]string, 0, 10) var the_audio string // force it to overwrite args = append(args, "-y") for _, codec := range encoding.Variants { if VideoOnly(codec) { args = append(args, "-f", "webm_dash_manifest", "-i", codec.Target) } else if AudioOnly(codec) { the_audio = codec.Target } } args = append(args, "-f", "webm_dash_manifest", "-i", the_audio) // create map for each args = append(args, "-c", "copy") for i := 0; i < len(encoding.Variants); i++ { args = append(args, "-map", strconv.Itoa(i)) } // generate adaptation sets, separating the audio // id=0 is videos, id=1 is audio args = append(args, "-f", "webm_dash_manifest", "-adaptation_sets") da_ints := slices.Collect(StrSeq(len(encoding.Variants) - 1)) video_set := strings.Join(da_ints, ",") adapt_set := fmt.Sprintf("id=0,streams=%s id=1,streams=%d", video_set, len(encoding.Variants) - 1) args = append(args, adapt_set) args = append(args, filepath.Join(encoding.OutDir, "manifest.mpd")) fmt.Println("\n\n\n\nARGS", args) ffmpeg := exec.Command("ffmpeg", args...) out, err := ffmpeg.CombinedOutput() os.Stdout.Write(out) log.Fatal(err) } func RenderToDir(encoding config.EncodeOpts, force bool) { matches, err := filepath.Glob(encoding.Input) if err != nil { log.Fatalf("%v", err) } for _, path := range matches { RenderFile(encoding, rand.Int(), path, force) } } func main() { settings := config.Load() if settings.Serve != "" { dir_handler := http.FileServer(http.Dir(settings.Serve)) http.HandleFunc("/", func (w http.ResponseWriter, r *http.Request) { fmt.Println(r.Method, r.URL) dir_handler.ServeHTTP(w, r) }) log.Fatal(http.ListenAndServe(settings.Port, nil)) } else { for i := 0; i < len(settings.Encodings); i++ { fmt.Println("LOOP", i) encoding := &settings.Encodings[i]; if encoding.OutDir != "" { RenderToDir(*encoding, settings.Force) } else { log.Fatal("config file needs either Output or OutDir") } } } }