Enable hardware accel but nothing improves the performance over libx264, and in some cases--like vulkan--it was slower. Tryed some of the amdgpu-install and h264_amf support but it relies on Ubuntu 24.

master
Zed A. Shaw 1 month ago
parent be44bcec22
commit e86695d903
  1. 4
      config.json
  2. 1
      config/settings.go
  3. 393
      main.go

@ -31,8 +31,8 @@
], ],
"CRF": 30, "CRF": 30,
"FPS": 30, "FPS": 30,
"Input": "test*.mp4", "Input": "LGoTHW/*.mp4",
"OutDir": "dash_test", "OutDir": "LGoTHW_DASH",
"Passes": 1, "Passes": 1,
"Dash": true, "Dash": true,
"Extras": [ "Extras": [

@ -26,6 +26,7 @@ type EncodeOpts struct {
Dash bool Dash bool
CRF int CRF int
FPS int FPS int
HardwareAccel string
Tune string Tune string
Input string Input string
OutDir string OutDir string

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

Loading…
Cancel
Save