Files

510 lines
15 KiB
JavaScript
Raw Permalink Normal View History

2020-10-25 18:08:58 +01:00
// --------------------------------------------------------------------------------
// Development options (for user options, see config.json)
global.debug = false;
// --------------------------------------------------------------------------------
// Actual Code
// Import modules
const ffmpeg = require('./ffmpeg.js');
const poolqueue = require('./poolqueue.js');
const fs = require('fs');
const path = require('path');
const process = require('process');
const util = require('util');
const { config } = require('process');
// Define some Helpers
function float_eq(a, b, edge) { return (Math.abs(a - b) <= edge); }
function float_lt(a, b, edge) { return ((a + edge) < b); }
function float_le(a, b, edge) { return float_lt(a, b, edge) || float_eq(a, b, edge); }
function float_gt(a, b, edge) { return ((a - edge) > b); }
function float_ge(a, b, edge) { return float_gt(a, b, edge) || float_eq(a, b, edge); }
2020-10-25 18:32:07 +01:00
Object.size = function (obj) { var size = 0, key; for (key in obj) { if (obj.hasOwnProperty(key)) { size++; } } return size; };
2020-10-25 18:08:58 +01:00
// Actual Code
async function load_config() { // Load Configuration
console.group("Loading configuration...");
console.time("Total");
let config = require('./config.json');
// Resolve and create directories.
for (let name in config.paths) {
config.paths[name] = path.resolve(config.paths[name]);
if (!fs.existsSync(config.paths[name])) {
fs.mkdirSync(config.paths[name]);
}
}
console.timeEnd("Total");
console.groupEnd();
return config;
}
async function load_encoders(config, ff) { // Load Encoders
console.group("Loading encoders...");
console.time("Total");
let encoders = new Map();
for (let name in config.encoders) {
let encoder = config.encoders[name];
// Is the encoder disabled, or invalid?
if (!encoder || !encoder.enabled) {
if (global.debug) console.debug(`${name} is disabled or invalid.`);
continue;
}
// Does the encoder exist?
if (!fs.existsSync(path.join(".", `encoder_${name}.js`))) {
console.error(`${name} does not exist.`);
continue;
}
// Attempt to load the encoder, if possible.
console.group(name);
console.time("Subtotal");
try {
let instance = new (require(`./encoder_${name}.js`))(ff, config, encoder);
instance.load();
encoders.set(name, instance);
} catch (ex) {
console.log(ex);
}
console.timeEnd("Subtotal");
console.groupEnd();
}
console.timeEnd("Total");
console.groupEnd();
return encoders;
}
async function load_videos(config, ff) { // Load Videos
console.group("Loading videos...");
console.time("Total");
let videos = new Map();
let promises = []; // Asynchronous loading.
for (let name in config.videos) {
let video = config.videos[name];
// Is the video disabled, or invalid?
if (!video || !video.enabled) {
if (global.debug) console.debug(`${name} is disabled or invalid.`);
continue;
}
2020-10-25 18:32:07 +01:00
2020-10-25 18:08:58 +01:00
// Does the video exist?
if (!fs.existsSync(path.join(config.paths.videos, `${name}.mkv`))) {
console.error(`${name} does not exist.`);
continue;
}
// Load video information asynchronously.
promises.push(new Promise((resolve, reject) => {
console.time(name);
let file_name = path.join(config.paths.videos, `${name}.mkv`);
let probeprom = ff.probe(file_name);
probeprom.then((json) => {
let data = new Object();
2020-10-25 18:32:07 +01:00
data.name = name;
data.info = json;
data.caches = {};
2020-10-25 18:08:58 +01:00
// File Information
2020-10-25 18:32:07 +01:00
data.file_name = `${name}.mkv`;
data.file_ext = path.extname(data.file_name);
data.file_base = path.basename(data.file_name, data.file_ext);
data.file = path.join(config.paths.videos, data.file_name);
2020-10-25 18:08:58 +01:00
// Video Information
2020-10-25 18:32:07 +01:00
data.resolution = { width: data.info.streams[0].width, height: data.info.streams[0].height };
data.framerate = eval(data.info.streams[0].r_frame_rate);
data.duration = data.info.streams[0].duration;
2020-10-25 18:08:58 +01:00
// Color Information
data.color = {};
2020-10-25 18:32:07 +01:00
data.color.range = data.info.streams[0].color_range ? data.info.streams[0].color_range : 'tv';
data.color.trc = data.info.streams[0].color_transfer ? data.info.streams[0].color_transfer : 'bt709';
data.color.primaries = data.info.streams[0].color_primaries ? data.info.streams[0].color_primaries : 'bt709';
data.color.matrix = data.info.streams[0].color_space ? data.info.streams[0].color_space : 'bt709';
2020-10-25 18:08:58 +01:00
videos.set(data.name, data);
console.timeEnd(name);
resolve(data);
});
}));
}
await Promise.allSettled(promises);
// Sort the videos list again. (We don't care about what order the user wants!)
videos = new Map([...videos].sort((a, b) => a[0] > b[0] ? 1 : -1));
console.timeEnd("Total");
console.groupEnd();
return videos;
}
async function create_caches(config, ff, videos, encoders) { // Create Caches
console.group("Creating caches...");
console.time("Total");
for (let name of videos.keys()) {
let video = videos.get(name);
console.group(name);
console.time("Subtotal");
video.caches = new Map();
for (let resolution of config.options.resolutions) {
let aspect_ratio = (video.resolution.height / video.resolution.width);
for (let framerate_scaling of config.options.framerate_scalings) {
let height = (Math.round(resolution[0] * aspect_ratio * 0.5) * 2);
// Skip resolutions that are too large horizontally.
if (resolution[0] > video.resolution.width) {
if (global.debug) console.debug(`${resolution[0]}x${height}x${(video.framerate * framerate_scaling)} skipped.`);
continue;
}
// Create Cache Information
let cache = {
framerate: (video.framerate * framerate_scaling),
width: resolution[0],
height: height
};
cache.framerate_s = cache.framerate.toFixed(2);
cache.duration = Math.floor(video.duration * video.framerate * framerate_scaling) / cache.framerate;
let key = `${cache.width}x${cache.height}x${cache.framerate_s}`;
cache.file = path.join(config.paths.cache, `${video.file_base}-${key}.mkv`);
cache.queues = new Map();
video.caches.set(`${key}`, cache);
}
}
for (let key of video.caches.keys()) {
let cache = video.caches.get(key);
// Check if cache file already exists and is correct.
if (fs.existsSync(cache.file)) {
let info = ff.probeSync(cache.file);
if ((info.streams)
2020-10-25 18:32:07 +01:00
&& (info.streams.length > 0)
&& (info.streams[0].width == cache.width)
&& (info.streams[0].height == cache.height)
&& float_eq(eval(info.streams[0].r_frame_rate), cache.framerate, 0.01)
&& float_eq(info.streams[0].duration, video.duration, 0.1)) {
2020-10-25 18:08:58 +01:00
if (global.debug) console.debug(`${key} already exists.`);
continue;
}
}
console.time(`${key} created`);
let command = [
"-y",
"-hide_banner",
"-v", "error",
"-i", video.file,
2020-10-26 09:56:49 +01:00
"-filter_complex", `fps=fps=${cache.framerate_s},scale=flags=bicubic+full_chroma_inp+full_chroma_int:w=${cache.width}:h=${cache.height},colorspace=space=${config.options.transcode.color.matrix}:trc=${config.options.transcode.color.trc}:primaries=${config.options.transcode.color.primaries}:range=${config.options.transcode.color.range},format=pix_fmts=${config.options.transcode.format}`,
2020-10-25 18:08:58 +01:00
"-an",
];
if (encoders.has("h264_nvenc")) {
command.push(
"-c:v", "h264_nvenc",
2020-10-25 18:32:07 +01:00
"-profile:v", "high",
2020-10-25 18:08:58 +01:00
"-preset", "p1",
"-tune", "lossless",
"-rc", "constqp",
"-rc-lookahead", "0",
"-multipass", "0",
"-b:v", "0",
"-minrate", "0",
"-maxrate", "0",
"-bufsize", "0",
"-qp", "0",
"-init_qpI", "0",
"-init_qpP", "0",
"-init_qpB", "0",
"-bf", "0",
"-g", `15`,
);
} else {
command.push(
"-c:v", "libx264",
"-preset", "veryfast",
"-crf", "0",
"-b:v", "0",
"-minrate", "0",
"-maxrate", "0",
"-bufsize", "0",
"-g", `15`,
);
}
2020-10-25 18:32:07 +01:00
command.push(cache.file);
2020-10-25 18:08:58 +01:00
let res = ff.ffmpegSync(command);
if (res.status != 0) {
console.log(res.stderr.toString());
}
if (global.debug) console.log(res.stdout.toString(), res.stderr.toString());
console.timeEnd(`${key} created`);
}
console.timeEnd("Subtotal");
console.groupEnd(name);
}
console.timeEnd("Total");
console.groupEnd();
return videos;
}
2020-10-25 22:26:04 +01:00
async function queue(config, ff, videos, encoders) {
console.group("Queueing...");
2020-10-25 18:08:58 +01:00
console.time("Total");
let promises = [];
2020-10-25 18:08:58 +01:00
for (let video_key of videos.keys()) {
promises.push(new Promise(async (resolve, reject) => {
let video = videos.get(video_key);
let video_promises = [];
console.time(video_key);
for (let cache_key of video.caches.keys()) {
video_promises.push(new Promise(async (resolve1) => {
let cache = video.caches.get(cache_key);
let queue_commands = new poolqueue();
let queue_files = new poolqueue();
let queue_promises = new Array();
for (let encoder_key of encoders.keys()) {
let encoder = encoders.get(encoder_key);
let encoder_pool = encoder.pool();
let encoder_extra = encoder.extra();
for (let idx = 0; idx < encoder.count(); idx++) {
let command = encoder.get(idx, cache.width, cache.height, cache.framerate);
for (let bitrate of config.options.bitrates) {
for (let kfinterval of config.options.keyframeinterval) {
queue_promises.push(new Promise((resolve2) => {
let file = path.join(
config.paths.output,
video.name,
cache_key,
encoder_key,
bitrate.toFixed(0),
(cache.framerate * kfinterval).toFixed(0),
`${command.hash}.mkv`
);
let file_json = path.join(
config.paths.output,
video.name,
cache_key,
encoder_key,
bitrate.toFixed(0),
(cache.framerate * kfinterval).toFixed(0),
`${command.hash}.json`
);
// Check if this file already exists at the target location.
if (fs.existsSync(file_json)) {
// Don't need to check for a video here, since we only care about results.
if (global.debug) console.debug(`${file} already completed.`);
resolve2(false);
return;
}
let line = [
"-map", "0:v:0",
"-an",
"-c:v", encoder_key,
"-g", (cache.framerate * kfinterval).toFixed(0),
"-b:v", `${bitrate}k`,
"-minrate", "0",
"-maxrate", "0",
"-bufsize", `${2 * bitrate}k`,
].concat(command.options).concat(encoder_extra).concat([file]);
queue_commands.push(encoder_pool, line, command.cost);
2020-10-25 22:26:04 +01:00
queue_files.push(encoder_pool, [[file, file_json]], command.cost);
resolve2(true);
}));
2020-10-25 18:08:58 +01:00
}
}
2020-10-25 18:08:58 +01:00
}
}
await Promise.allSettled(queue_promises);
let data = {
commands: queue_commands.finalize(),
files: queue_files.finalize(),
};
cache.queues = data;
resolve1(true);
}));
2020-10-25 18:08:58 +01:00
}
await Promise.allSettled(video_promises);
console.timeEnd(video_key);
resolve();
}));
2020-10-25 18:08:58 +01:00
}
await Promise.allSettled(promises);
2020-10-25 22:26:04 +01:00
console.timeEnd("Total");
2020-10-25 18:08:58 +01:00
console.groupEnd();
2020-10-25 22:26:04 +01:00
}
2020-10-25 18:08:58 +01:00
2020-10-25 22:26:04 +01:00
async function work(config, ff, videos, encoders) {
2020-10-25 18:32:07 +01:00
// Process from here on out.
// LOOP
// 1. Pull out front of the command and file queue.
// 2. Encode using the given command(s).
// 3. Compare resulting files with real input (libvmaf).
// 4. Delete encoded files.
// 5. Repeat until queues empty, no more caches for video, and no more videos.
2020-10-25 18:08:58 +01:00
2020-10-25 22:26:04 +01:00
let vmaf_model = ff.consolify(path.resolve(path.join(config.paths.ffmpeg, "vmaf", config.options.vmaf.model)));
console.group("Processing...")
console.time("Total");
for (let video_key of videos.keys()) {
console.group(video_key);
console.time(video_key);
let video = videos.get(video_key);
for (let cache_key of video.caches.keys()) {
2020-10-28 18:40:22 +01:00
console.group(cache_key);
2020-10-25 22:26:04 +01:00
console.time(cache_key);
let cache = video.caches.get(cache_key);
let length = cache.queues.commands.length;
console.log(`0.0% (0 / ${length}): 0.000s`);
while (cache.queues.commands.length > 0) {
let commands = cache.queues.commands.shift();
let files = cache.queues.files.shift();
let prc1 = length - cache.queues.commands.length;
let prc2 = prc1 / length * 100.0;
let LABEL = `${prc2.toFixed(1)}% (${prc1} / ${length})`
console.time(LABEL)
console.group();
2020-10-26 09:56:49 +01:00
for (let file of files) { // Create directories.
2020-10-25 22:26:04 +01:00
fs.mkdirSync(path.dirname(file[0]), { recursive: true });
}
2020-10-26 09:56:49 +01:00
{ // Encode
2020-10-25 22:26:04 +01:00
console.time("Encoding");
let opts = [
"-y",
"-hide_banner",
"-v", "error",
"-hwaccel", "auto",
"-i", cache.file
].concat(commands);
let res = ff.ffmpegSync(opts);
if (res.status != 0) {
console.log(res.stdout.toString(), res.stderr.toString());
continue;
}
console.timeEnd("Encoding");
}
2020-10-26 09:56:49 +01:00
{ // Process
2020-10-25 22:26:04 +01:00
console.time("Processing");
let opts = [
"-hide_banner",
"-v", "warning",
"-hwaccel", "auto",
"-i", video.file,
];
let filter = "";
let references = [];
// Build Filter Graph
if (files.length > 1) {
filter = `[0:v:0]split=${files.length}`
for (let idx = 0; idx < files.length; idx++) {
filter = `${filter}[ref:${idx}]`;
references[idx] = `[ref:${idx}]`;
}
filter = `${filter}`
} else {
references[0] = "[0:v:0]";
}
// Rescale and Resample all inputs and compare them.
for (let idx = 0; idx < files.length; idx++) {
let file = files[idx];
opts.push("-i", file[0]);
if (filter != "")
filter = `${filter};` // [temp:${idx}];[temp:${idx}]
filter += `[${idx+1}:v:0]scale=flags=bicubic+full_chroma_inp+full_chroma_int:w=${video.resolution.width.toFixed(0)}:h=${video.resolution.height.toFixed(0)}`
+ `,colorspace=ispace=${config.options.transcode.color.matrix}:itrc=${config.options.transcode.color.trc}:iprimaries=${config.options.transcode.color.primaries}:irange=${config.options.transcode.color.range}:space=${video.color.matrix}:trc=${video.color.trc}:primaries=${video.color.primaries}:range=${video.color.range}`
+ `,format=pix_fmts=yuv444p,fps=fps=${video.framerate.toFixed(2)}[scaled:${idx}]`
+ `;[scaled:${idx}][${references[idx]}libvmaf=model_path=${vmaf_model}:log_fmt=json:log_path=${ff.consolify(file[1])}:enable_conf_interval=1:psnr=1:ssim=1:n_threads=${(config.options.vmaf.threads / files.length).toFixed(0)}[main:${idx}]`
2020-10-25 22:26:04 +01:00
}
opts.push("-filter_complex", filter);
for (let idx = 0; idx < files.length; idx++) {
opts.push(
"-map", `[main:${idx}]`,
"-f", "null",
"-"
)
}
let res = ff.ffmpegSync(opts);
if (res.status != 0) {
console.log(res.stdout.toString(), res.stderr.toString());
continue;
}
console.timeEnd("Processing");
}
2020-10-26 09:56:49 +01:00
for (let file of files) { // Clean up
fs.unlinkSync(file[0]);
}
2020-10-25 22:26:04 +01:00
console.groupEnd();
console.timeEnd(LABEL);
2020-10-25 18:08:58 +01:00
}
2020-10-28 18:40:22 +01:00
console.groupEnd(cache_key);
2020-10-25 22:26:04 +01:00
console.timeEnd(cache_key);
2020-10-25 18:08:58 +01:00
}
2020-10-25 22:26:04 +01:00
console.timeEnd(video_key);
2020-10-25 18:08:58 +01:00
console.groupEnd();
}
console.timeEnd("Total");
console.groupEnd();
}
async function main() {
let config;
let ff;
let encoders;
let videos;
await load_config().then((p) => { config = p; });
ff = new ffmpeg(config.paths.ffmpeg);
await load_encoders(config, ff).then((p) => { encoders = p; });
await load_videos(config, ff).then((p) => { videos = p; });
await create_caches(config, ff, videos, encoders);
2020-10-25 22:26:04 +01:00
await queue(config, ff, videos, encoders);
await work(config, ff, videos, encoders);
2020-10-25 18:08:58 +01:00
}
main();