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