@ -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" )
}
}
}
}
}
}
}
}