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.
260 lines
6.2 KiB
260 lines
6.2 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(codec config.CodecOpts) bool {
|
|
return codec.Scale == "" && codec.VideoCodec == "none"
|
|
}
|
|
|
|
func VideoOnly(codec config.CodecOpts) bool {
|
|
return codec.AudioCodec == "none"
|
|
}
|
|
|
|
func ExtractBase(source_path string, outdir string) (string, string) {
|
|
base := filepath.Base(source_path)
|
|
target := filepath.Join(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?!") }
|
|
|
|
return dir, base
|
|
}
|
|
|
|
func SetCodecTarget(codec *config.CodecOpts, source_path string, encoding config.EncodeOpts) {
|
|
dir, base := ExtractBase(source_path, encoding.OutDir)
|
|
|
|
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)
|
|
}
|
|
|
|
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) }
|
|
}
|
|
|
|
func DevNull() string {
|
|
if runtime.GOOS == "windows" {
|
|
return "NUL"
|
|
} else {
|
|
return "/dev/null"
|
|
}
|
|
}
|
|
|
|
func FileExists(path string) bool {
|
|
_, err := os.Stat(path)
|
|
return err == nil
|
|
}
|
|
|
|
func RenderFile(encoding config.EncodeOpts, pid int, source_path string, force bool) {
|
|
for i := 0; i < len(encoding.Variants); i++ {
|
|
codec := &encoding.Variants[i]
|
|
|
|
SetCodecTarget(codec, source_path, encoding)
|
|
|
|
// 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) && !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())
|
|
}
|
|
|
|
Run(encoding, *codec, encoding.Passes, pid, source_path, codec.Target)
|
|
}
|
|
|
|
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 }
|
|
}
|
|
}
|
|
}
|
|
|
|
func SaveMPD(source_path string, 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)
|
|
|
|
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()
|
|
|
|
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 _, source_path := range matches {
|
|
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")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|