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 ", "720, 1080, or 2160 for example", 1080], ["--test ", "Make a test video seconds long."], ["--test-start ", "When to start the test clip."], ["--init-outdir", "if outdir doesn't exist create it", false], ["--br-video ", "video bitrate", 900], ["--br-audio ", "video bitrate", 192], ["--speed ", "ffmpeg speed preset", "veryslow"], ["--crf ", "constant rate factor", 30], ["--tune ", "special tuning setting for mp4 try film", "animation"], ["--debug ", "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 ", "specific output file name"], ["--outdir ", "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); }