Started the translation from my old convert.js to the new thing.

master
Zed A. Shaw 2 days ago
parent 7b2aea964a
commit 836f070df1
  1. 30
      .gitignore
  2. 8
      Makefile
  3. 10
      config.toml
  4. 56
      config/settings.go
  5. 190
      convert.js
  6. 13
      go.mod
  7. 8
      go.sum
  8. 21
      main.go

30
.gitignore vendored

@ -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…
Cancel
Save