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.
 
 
 
 

169 lines
4.7 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 GrassValley HQX to .mp4.";
export const options = [
["--scale <int>", "720, 1080, or 2160 for example", 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],
["--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}.${ext}`);
}
}
}
const run = async (pass, output, opts) => {
const encode_defer = defer();
// 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=-1:${opts.scale}:flags=lanczos`],
["-pix_fmt","yuv420p"],
["-tune", opts.tune],
["-movflags", "faststart"],
["-pass", pass],
["-passlogfile", `ffmpeg2pass-${ process.pid }.log`],
["-preset", opts.speed],
];
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(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);
for(let fname of inputs) {
opts.input = fname;
await process_file(opts);
}
process.exit(0);
}