diff --git a/config.json b/config.json index cd75f5d..30a47ef 100644 --- a/config.json +++ b/config.json @@ -31,8 +31,8 @@ ], "CRF": 30, "FPS": 30, - "Input": "test*.mp4", - "OutDir": "dash_test", + "Input": "LGoTHW/*.mp4", + "OutDir": "LGoTHW_DASH", "Passes": 1, "Dash": true, "Extras": [ diff --git a/config/settings.go b/config/settings.go index fbc4515..ea69422 100644 --- a/config/settings.go +++ b/config/settings.go @@ -26,6 +26,7 @@ type EncodeOpts struct { Dash bool CRF int FPS int + HardwareAccel string Tune string Input string OutDir string diff --git a/main.go b/main.go index 17c872c..7255ecc 100644 --- a/main.go +++ b/main.go @@ -1,133 +1,140 @@ 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" + "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" + return codec.Scale == "" && codec.VideoCodec == "none" } func VideoOnly(codec config.CodecOpts) bool { - return codec.AudioCodec == "none" + return codec.AudioCodec == "none" } func ExtractBase(source_path string, outdir string) (string, string) { - base := filepath.Base(source_path) - target := filepath.Join(outdir, base) + base := filepath.Base(source_path) + target := filepath.Join(outdir, base) - cleaned := filepath.Clean(target) - dir, file := filepath.Split(cleaned) - ext := filepath.Ext(file) + 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?!") } + base, found := strings.CutSuffix(file, ext) + if !found { panic("no extension found?!") } - return dir, base + return dir, base } func SetCodecTarget(codec *config.CodecOpts, source_path string, encoding config.EncodeOpts) { - dir, base := ExtractBase(source_path, encoding.OutDir) + dir, base := ExtractBase(source_path, encoding.OutDir) - dot_or_dash := "."; if encoding.Dash { dot_or_dash = "/" } + dot_or_dash := "."; if encoding.Dash { dot_or_dash = "/" } - if AudioOnly(*codec) { - renamed := fmt.Sprint(base, dot_or_dash, "audio.", encoding.Format) - codec.Target = filepath.Join(dir, renamed) - } else { - dim := strings.Replace(codec.Scale, ":", ".", 1) - renamed := fmt.Sprint(base, dot_or_dash, dim, ".", encoding.Format) - codec.Target = filepath.Join(dir, renamed) - } + if AudioOnly(*codec) { + renamed := fmt.Sprint(base, dot_or_dash, "audio.", encoding.Format) + codec.Target = filepath.Join(dir, renamed) + } else { + dim := strings.Replace(codec.Scale, ":", ".", 1) + renamed := fmt.Sprint(base, dot_or_dash, dim, ".", encoding.Format) + codec.Target = filepath.Join(dir, renamed) + } - codec.Target = filepath.ToSlash(codec.Target) + codec.Target = filepath.ToSlash(codec.Target) } 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("====== FFMPEG ======\n", cmd.String(), "\n============") - - err := cmd.Run() - if err != nil { log.Fatalf("%v", err) } + encode := fluentffmpeg.NewCommand("") + + extras := []string{ + "-pix_fmt", "yuv420p", + } + + in_extras := []string{} + + 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 { + in_extras = append(in_extras, "-ss", fmt.Sprintf("00:%d", encoding.TestStart)) + extras = append(extras, "-t", fmt.Sprint(encoding.Test)) + } + + if encoding.HardwareAccel != "" { + in_extras = append(in_extras, "-hwaccel", encoding.HardwareAccel) + } + + 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.InputOptions(in_extras...) + encode.OutputOptions(extras...) + + cmd := encode.InputPath(input). + OutputFormat(encoding.Format). + OutputPath(output). + OutputLogs(os.Stdout). + Overwrite(true). + Build() + + fmt.Println("====== FFMPEG ======\n", cmd.String(), "\n============") + + err := cmd.Run() + if err != nil { log.Fatalf("%v", err) } } func DevNull() string { - if runtime.GOOS == "windows" { - return "NUL" - } else { - return "/dev/null" - } + if runtime.GOOS == "windows" { + return "NUL" + } else { + return "/dev/null" + } } func FileExists(path string) bool { @@ -136,125 +143,131 @@ func FileExists(path string) bool { } func RenderFile(encoding config.EncodeOpts, pid int, source_path string, force bool) { - for i := 0; i < len(encoding.Variants); i++ { - codec := &encoding.Variants[i] + for i := 0; i < len(encoding.Variants); i++ { + codec := &encoding.Variants[i] - SetCodecTarget(codec, source_path, encoding) + SetCodecTarget(codec, source_path, encoding) - // ensure the target directories exist - target_dir := filepath.Dir(codec.Target) + // ensure the target directories exist + target_dir := filepath.Dir(codec.Target) - if !FileExists(codec.Target) { - err := os.MkdirAll(target_dir, 0750) - if err != nil { - log.Fatal("failed to make target path: %s: %v", target_dir, err) - } - } + if !FileExists(codec.Target) { + err := os.MkdirAll(target_dir, 0750) + if err != nil { + log.Fatal("failed to make target path: %s: %v", target_dir, err) + } + } - if FileExists(codec.Target) && !force { - fmt.Println("^^^ SKIP", source_path, "->", codec.Target) - return - } + if FileExists(codec.Target) && !force { + fmt.Println("^^^ SKIP", source_path, "->", codec.Target) + return + } - fmt.Println("--- PATH", source_path, "->", codec.Target) - for i := 1; i < encoding.Passes; i++ { - Run(encoding, *codec, i, pid, source_path, DevNull()) - } + fmt.Println("--- PATH", source_path, "->", codec.Target) + for i := 1; i < encoding.Passes; i++ { + Run(encoding, *codec, i, pid, source_path, DevNull()) + } - Run(encoding, *codec, encoding.Passes, pid, source_path, codec.Target) - } + Run(encoding, *codec, encoding.Passes, pid, source_path, codec.Target) + } - SaveMPD(source_path, encoding) + if encoding.Dash { + SaveMPD(source_path, 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 } - } - } + return func (yield func(x string) bool) { + for i := range up_to { + if !yield(strconv.Itoa(i)) { return } + } + } } func SaveMPD(source_path string, encoding config.EncodeOpts) { - args := make([]string, 0, 10) - var the_audio string + args := make([]string, 0, 10) + var the_audio string - // force it to overwrite - args = append(args, "-y") + // 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 - } - } + 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) + args = append(args, "-f", "webm_dash_manifest", "-i", the_audio) - // create map for each - args = append(args, "-c", "copy") + // create map for each + args = append(args, "-c", "copy") - for i := 0; i < len(encoding.Variants); i++ { - args = append(args, "-map", strconv.Itoa(i)) - } + 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") + // 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)) + da_ints := slices.Collect(StrSeq(len(encoding.Variants) - 1)) - video_set := strings.Join(da_ints, ",") + video_set := strings.Join(da_ints, ",") - adapt_set := fmt.Sprintf("id=0,streams=%s id=1,streams=%d", - video_set, len(encoding.Variants) - 1) + 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, adapt_set) - dir, base := ExtractBase(source_path, encoding.OutDir) - mpd_path := filepath.ToSlash(filepath.Join(dir, base, "manifest.mpd")) - args = append(args, mpd_path) + dir, base := ExtractBase(source_path, encoding.OutDir) + mpd_path := filepath.ToSlash(filepath.Join(dir, base, "manifest.mpd")) + args = append(args, mpd_path) - ffmpeg := exec.Command("ffmpeg", args...) - fmt.Println("==== MPD COMMAND ====\n", ffmpeg.String(), "\n==============") - out, err := ffmpeg.CombinedOutput() + ffmpeg := exec.Command("ffmpeg", args...) + fmt.Println("==== MPD COMMAND ====\n", ffmpeg.String(), "\n==============") + out, err := ffmpeg.CombinedOutput() - os.Stdout.Write(out) - log.Fatal(err) + os.Stdout.Write(out) + + if err != nil { log.Fatal(err) } } func RenderToDir(encoding config.EncodeOpts, force bool) { - matches, err := filepath.Glob(encoding.Input) + matches, err := filepath.Glob(encoding.Input) - if err != nil { log.Fatalf("%v", err) } + if err != nil { log.Fatal(err) } - for _, source_path := range matches { - RenderFile(encoding, rand.Int(), source_path, force) - } + for _, source_path := range matches { + // BUG: glob will return these even if it's not in the regex + if source_path != "." || source_path != ".." { + RenderFile(encoding, rand.Int(), source_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++ { - encoding := &settings.Encodings[i]; - - if encoding.OutDir != "" { - RenderToDir(*encoding, settings.Force) - } else { - log.Fatal("config file needs either Output or OutDir") - } - } - } + 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++ { + encoding := &settings.Encodings[i]; + + if encoding.OutDir != "" { + RenderToDir(*encoding, settings.Force) + } else { + log.Fatal("config file needs either Output or OutDir") + } + } + } }