A simple little command line tool in Go to crunch my videos. This includes many settings I've found that compress "code videos" really well. YMMV.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
vidcrunch/main.go

236 lines
5.4 KiB

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(encoding config.VideoOpts) bool {
return encoding.Scale == "" && encoding.VideoCodec == "none"
}
func VideoOnly(encoding config.VideoOpts) bool {
return encoding.AudioCodec == "none"
}
func ModFile(fname string, encoding config.VideoOpts) string {
cleaned := filepath.Clean(fname)
dir, file := filepath.Split(cleaned)
ext := filepath.Ext(file)
base, found := strings.CutSuffix(file, ext)
if !found { panic("no extension found?!") }
if AudioOnly(encoding) {
renamed := fmt.Sprint(base, ".audio.", encoding.Format)
return filepath.Join(dir, renamed)
} else {
dim := strings.Replace(encoding.Scale, ":", ".", 1)
renamed := fmt.Sprint(base, ".", dim, ".", encoding.Format)
return filepath.Join(dir, renamed)
}
}
func Run(encoding config.VideoOpts, 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 encoding.Resize {
extras = append(extras,
"-vf", fmt.Sprintf("scale=%s:flags=lanczos", encoding.Scale),
"-aspect", encoding.Scale)
}
if pass != encoding.Passes || VideoOnly(encoding) {
extras = append(extras, "-an")
} else {
encode.AudioCodec(encoding.AudioCodec)
extras = append(extras,
"-b:a", fmt.Sprint(encoding.AudioBitrate * 1024))
}
if encoding.Test > 0 {
encode.InputOptions("-ss", fmt.Sprintf("00:%d", encoding.TestStart))
extras = append(extras, "-t", fmt.Sprint(encoding.Test))
}
if encoding.VideoCodec == "none" {
extras = append(extras, "-vn")
} else {
encode.VideoCodec(encoding.VideoCodec).
VideoBitRate(encoding.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.VideoOpts, pid int, path string, target string) {
for i := 1; i < encoding.Passes; i++ {
Run(encoding, i, pid, path, DevNull())
}
Run(encoding, encoding.Passes, pid, path, target)
}
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(settings config.Settings) {
args := make([]string, 0, 10)
var the_audio string
// force it to overwrite
args = append(args, "-y")
for _, encoding := range settings.Encodings {
if VideoOnly(encoding) {
args = append(args, "-f", "webm_dash_manifest", "-i", encoding.FakeResult)
} else if AudioOnly(encoding) {
the_audio = encoding.FakeResult
}
}
args = append(args, "-f", "webm_dash_manifest", "-i", the_audio)
// create map for each
args = append(args, "-c", "copy")
for i := 0; i < len(settings.Encodings); 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(settings.Encodings) - 1))
video_set := strings.Join(da_ints, ",")
adapt_set := fmt.Sprintf("id=0,streams=%s id=1,streams=%d",
video_set, len(settings.Encodings) - 1)
args = append(args, adapt_set)
args = append(args, filepath.Join(settings.Encodings[0].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.VideoOpts, force bool) string {
matches, err := filepath.Glob(encoding.Input)
if err != nil { log.Fatalf("%v", err) }
target := ""
for _, path := range matches {
base := filepath.Base(path)
target = filepath.Join(encoding.OutDir, base)
target = ModFile(target, encoding)
_, err := os.Stat(target)
if err != nil || force {
fmt.Println("--- PATH", path, "->", target)
RenderFile(encoding, rand.Int(), path, target)
} else {
fmt.Println("^^^ SKIP", path, "->", target)
}
}
if target == "" { log.Fatal("fuck!") }
return target
}
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 != "" {
encoding.FakeResult = RenderToDir(*encoding, settings.Force)
fmt.Println("ENCODING result", encoding.FakeResult, "len=", len(settings.Encodings))
} else {
log.Fatal("config file needs either Output or OutDir")
}
}
SaveMPD(settings)
}
}