A simple little command line tool in Go to crunch my videos. This includes many settings I've found that compress "code videos" really well. YMMV.
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.
vidcrunch/convert.js

191 lines
5.5 KiB

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);
}