parent
7b2aea964a
commit
836f070df1
@ -0,0 +1,30 @@ |
||||
# ---> Vim |
||||
# Swap |
||||
[._]*.s[a-v][a-z] |
||||
!*.svg # comment out if you don't need vector files |
||||
[._]*.sw[a-p] |
||||
[._]s[a-rt-v][a-z] |
||||
[._]ss[a-gi-z] |
||||
[._]sw[a-p] |
||||
|
||||
# Session |
||||
Session.vim |
||||
Sessionx.vim |
||||
|
||||
# Temporary |
||||
.netrwhist |
||||
*~ |
||||
# Auto-generated tag files |
||||
tags |
||||
# Persistent undo |
||||
[._]*.un~ |
||||
|
||||
backup |
||||
*.exe |
||||
*.dll |
||||
coverage |
||||
coverage/* |
||||
.venv |
||||
*.gz |
||||
public |
||||
*.mp4 |
@ -0,0 +1,8 @@ |
||||
GO_IS_STUPID_EXE=
|
||||
|
||||
ifeq '$(OS)' 'Windows_NT' |
||||
GO_IS_STUPID_EXE=.exe
|
||||
endif |
||||
|
||||
build: |
||||
go build .
|
@ -0,0 +1,10 @@ |
||||
Scale = "1280:720" |
||||
VideoBitrate = 900 |
||||
AudioBitrate = 192 |
||||
Speed = "veryslow" |
||||
CleanFilename = false |
||||
CRF = 20 |
||||
FPS = 30 |
||||
Tune = "animation" |
||||
Input = "test.mp4" |
||||
Output = "test_render.mp4" |
@ -0,0 +1,56 @@ |
||||
package config |
||||
|
||||
import ( |
||||
"flag" |
||||
"log" |
||||
"github.com/BurntSushi/toml" |
||||
) |
||||
|
||||
type config struct { |
||||
Test int |
||||
TestStart int |
||||
InitOutdir bool |
||||
Debug int |
||||
Progress bool |
||||
ConfigPath string |
||||
|
||||
Scale string |
||||
VideoBitrate int |
||||
AudioBitrate int |
||||
Speed string |
||||
CleanFilename bool |
||||
CRF int |
||||
FPS int |
||||
Tune string |
||||
Input string |
||||
Output string |
||||
} |
||||
|
||||
var Settings config |
||||
|
||||
func parseFlags(c *config) { |
||||
flag.IntVar(&c.Test, "test", 10, "Make a test video <int> seconds long.") |
||||
flag.IntVar(&c.TestStart, "test-start", 60, "When to start the test clip.") |
||||
flag.BoolVar(&c.InitOutdir, "init-outdir", false, "if outdir doesn't exist create it") |
||||
flag.IntVar(&c.Debug, "debug", 0, "1=print the ffmpeg command, 2=and its stderr output") |
||||
flag.BoolVar(&c.Progress, "progress", false, "Show percent progress. Not accurate (thanks ffmpeg)") |
||||
flag.StringVar(&c.ConfigPath, "config", "config.toml", "config.toml to load") |
||||
|
||||
flag.Parse() |
||||
} |
||||
|
||||
func Load() { |
||||
parseFlags(&Settings) |
||||
|
||||
metadata, err := toml.DecodeFile(Settings.ConfigPath, &Settings) |
||||
|
||||
if err != nil { |
||||
log.Fatalf("error loading config.toml: %v", err) |
||||
} |
||||
|
||||
bad_keys := metadata.Undecoded() |
||||
|
||||
if len(bad_keys) > 0 { |
||||
log.Fatalf("unknown configuration keys: %v", bad_keys); |
||||
} |
||||
} |
@ -0,0 +1,190 @@ |
||||
import path from "path"; |
||||
import ffmpeg from "fluent-ffmpeg"; |
||||
import { glob, mkdir } from "../lib/builderator.js"; |
||||
import { defer } from "../lib/api.js"; |
||||
import { existsSync as exists } from "fs"; |
||||
import assert from "assert"; |
||||
|
||||
import logging from "../lib/logging.js"; |
||||
const log = logging.create("commands/convert.js"); |
||||
|
||||
const dev_null = process.platform == "win32" ? "NUL" : "/dev/null"; |
||||
|
||||
export const description = "Converts videos from anything to .mp4."; |
||||
|
||||
export const options = [ |
||||
["--scale <int>", "720, 1080, or 2160 for example. A : will be a literal ffmpeg scale.", "1920:1080"], |
||||
["--test <int>", "Make a test video <int> seconds long."], |
||||
["--test-start <int>", "When to start the test clip."], |
||||
["--init-outdir", "if outdir doesn't exist create it", false], |
||||
["--br-video <int>", "video bitrate", 900], |
||||
["--br-audio <int>", "video bitrate", 192], |
||||
["--speed <str>", "ffmpeg speed preset", "veryslow"], |
||||
["--crf <int>", "constant rate factor", 30], |
||||
["--fps <int>", "target fps", 30], |
||||
["--tune <str>", "special tuning setting for mp4 try film", "animation"], |
||||
["--debug <level>", "1=print the ffmpeg command, 2=and its stderr output"], |
||||
["--clean-filename", "don't modify the output file with scale info", false], |
||||
["--progress", "Show percent progress. Not accurate (thanks ffmpeg)", false], |
||||
["--output <string>", "specific output file name"], |
||||
["--outdir <string>", "write the file to this dir (can't combine with output)"], |
||||
["--vp9", "encode with vp9"], |
||||
]; |
||||
|
||||
export const required = [ |
||||
["--input [string]", "input file name or glob/regex"], |
||||
]; |
||||
|
||||
const generate_output_file = (opts) => { |
||||
if(opts.output) { |
||||
return opts.output; |
||||
} else { |
||||
const { dir, name } = path.parse(opts.input); |
||||
const ext = opts.vp9 ? "webm" : "mp4"; |
||||
const target_dir = opts.outdir ? opts.outdir : dir; |
||||
|
||||
if(opts.initOutdir) mkdir(target_dir); |
||||
|
||||
assert(exists(target_dir), `Target directory ${target_dir} does not exist.`); |
||||
|
||||
if(opts.cleanFilename) { |
||||
return path.join(target_dir, `${name}.${ext}`); |
||||
} else { |
||||
return path.join(target_dir, `${name}.${opts.scale.replace(':','_')}.${ext}`); |
||||
} |
||||
} |
||||
} |
||||
|
||||
const run = async (pass, output, opts) => { |
||||
const encode_defer = defer(); |
||||
// adapt the scale so a literal one can be used in weird situations
|
||||
const scale = opts.scale.includes(":") ? opts.scale : `-1:${opts.scale}`; |
||||
|
||||
// taken from https://developers.google.com/media/vp9/settings/vod
|
||||
|
||||
|
||||
// 22m without these, 13m with these
|
||||
const vp9_opts = [ |
||||
["-pass", pass], |
||||
["-passlogfile", `ffmpeg2pass-${ process.pid }`], |
||||
["-minrate", `${opts.brVideo / 2 }k`], |
||||
["-maxrate", `${opts.brVideo * 1.45 }k`], |
||||
["-quality", "good"], |
||||
["-speed", pass == 1 ? 4 : 2], |
||||
["-threads", "8"], |
||||
// this is different for 1080 videos vs 720
|
||||
["-tile-columns", pass + 1], |
||||
// key frames at 240
|
||||
["-g", "240"], |
||||
]; |
||||
|
||||
const mp4_opts = [ |
||||
["-vf", `scale=${scale}:flags=lanczos`], |
||||
["-aspect", `${scale}`], |
||||
["-pix_fmt","yuv420p"], |
||||
["-tune", opts.tune], |
||||
["-movflags", "faststart"], |
||||
["-pass", pass], |
||||
["-passlogfile", `ffmpeg2pass-${ process.pid }.log`], |
||||
["-preset", opts.speed], |
||||
["-filter:v", `fps=${opts.fps}`], |
||||
]; |
||||
|
||||
if(opts.crf) { |
||||
vp9_opts.push(["-crf", opts.crf]); |
||||
mp4_opts.push(["-crf", opts.crf]); |
||||
} |
||||
|
||||
let encode = ffmpeg(opts.input, {logger: log}) |
||||
.inputOptions([]); |
||||
|
||||
if(opts.testStart || opts.test) { |
||||
encode.seek(opts.testStart || opts.test); |
||||
} |
||||
|
||||
// this logic is too convoluted
|
||||
if((opts.vp9 && pass < 2) || (!opts.vp9 && pass < 3)) { |
||||
encode.noAudio(); |
||||
} else { |
||||
encode.audioBitrate(opts.brAudio); |
||||
} |
||||
|
||||
// we have to use flat here because weirdly ffmpeg-fluent expects them in a big list
|
||||
encode.videoBitrate(opts.brVideo) |
||||
.outputOptions(opts.vp9 ? vp9_opts.flat() : mp4_opts.flat()) |
||||
.output(output) |
||||
|
||||
if(opts.vp9) { |
||||
encode.audioCodec("libopus") |
||||
.videoCodec("libvpx-vp9") |
||||
.format("webm"); |
||||
} else { |
||||
encode.audioCodec("aac") |
||||
.videoCodec("libx264") |
||||
.format("mp4"); |
||||
} |
||||
|
||||
if(opts.test) { |
||||
encode.duration(opts.test); |
||||
} |
||||
|
||||
if(opts.progress) { |
||||
encode.on("progress", (progress) => { |
||||
process.stdout.write(`${path.basename(opts.input)} -> ${ output } ${Math.round(progress.percent)}% \r`) |
||||
}); |
||||
} |
||||
|
||||
if(opts.debug) { |
||||
encode.on("start", line => console.log("FFMPEG: ", line)); |
||||
|
||||
if(opts.debug == 2) { |
||||
encode.on("stderr", line => console.log(line)); |
||||
} |
||||
} |
||||
|
||||
encode.on("end", () => encode_defer.resolve()); |
||||
|
||||
console.log("------ PASS", pass, opts.input); |
||||
encode.run(); |
||||
|
||||
return encode_defer; |
||||
} |
||||
|
||||
export const process_file = async (opts) => { |
||||
const output = generate_output_file(opts); |
||||
|
||||
if(exists(output)) { |
||||
console.log("Skipping output", output); |
||||
} else { |
||||
if(opts.vp9) { |
||||
await run(1, dev_null, opts); |
||||
await run(2, output, opts); |
||||
} else { |
||||
await run(1, dev_null, opts); |
||||
await run(2, dev_null, opts); |
||||
await run(3, output, opts); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export const main = async (opts) => { |
||||
const inputs = glob(opts.input); |
||||
|
||||
if(inputs.length === 0) { |
||||
console.error(`ERROR: Glob --input ${opts.input} didn't match any files.`); |
||||
process.exit(1); |
||||
} |
||||
|
||||
for(let fname of inputs) { |
||||
opts.input = fname; |
||||
try { |
||||
await process_file(opts); |
||||
} catch(e) { |
||||
console.log("--------------------------------------------------------------------") |
||||
console.error(e, `Processing file ${fname}`); |
||||
console.log("--------------------------------------------------------------------") |
||||
} |
||||
} |
||||
|
||||
process.exit(0); |
||||
} |
@ -0,0 +1,13 @@ |
||||
module lcthw.dev/vidcrunch |
||||
|
||||
go 1.24.2 |
||||
|
||||
require ( |
||||
github.com/BurntSushi/toml v1.5.0 |
||||
github.com/modfy/fluent-ffmpeg v0.1.0 |
||||
) |
||||
|
||||
require ( |
||||
github.com/fatih/structs v1.1.0 // indirect |
||||
github.com/pkg/errors v0.9.1 // indirect |
||||
) |
@ -0,0 +1,8 @@ |
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= |
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= |
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= |
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= |
||||
github.com/modfy/fluent-ffmpeg v0.1.0 h1:9T191rhSK6KfoDo9Y/+0Tph3khrudvLQEEi05O+ijHA= |
||||
github.com/modfy/fluent-ffmpeg v0.1.0/go.mod h1:GauXGqGYAmYFupCWG8n1eyuLZMKmLxGTGvszYkJ0Oyo= |
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= |
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= |
@ -0,0 +1,21 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"lcthw.dev/vidcrunch/config" |
||||
"github.com/modfy/fluent-ffmpeg" |
||||
) |
||||
|
||||
|
||||
func main() { |
||||
config.Load() |
||||
fmt.Println(config.Settings) |
||||
|
||||
err := fluentffmpeg.NewCommand(""). |
||||
InputPath(config.Settings.Input). |
||||
OutputFormat("mp4"). |
||||
OutputPath(config.Settings.Output). |
||||
Run() |
||||
|
||||
if err != nil { panic("fail") } |
||||
} |
Loading…
Reference in new issue