diff --git a/README.md b/README.md new file mode 100644 index 0000000..c84609d --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# js-vmaf: Simple, but effective VMAF comparison tool +This is a simple wrapper around FFmpeg and VMAF to handle comparison of files. + +## Installing +``` +npm install +npm run build +``` + +## Usage +``` +node . --help +``` + +## Examples +### Compare all files in a directory +``` +node . --ffmpeg ./ffmpeg --ffprobe ./ffprobe -r /mnt/usb0/reference.mp4 /mnt/usb1/ +``` diff --git a/package-lock.json b/package-lock.json index 4d5d6a7..658520b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "js-vmaf", - "version": "0.0.0", + "version": "1.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "js-vmaf", - "version": "0.0.0", + "version": "1.0.1", "license": "BSD 3-Clause \"New\" or \"Revised\" License", "dependencies": { - "argparse": "^2.0.1" + "argparse": "^2.0.1", + "percentile": "^1.6.0" }, "devDependencies": { "@types/argparse": "^2.0.10", @@ -1266,6 +1267,11 @@ "node": ">=8" } }, + "node_modules/percentile": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/percentile/-/percentile-1.6.0.tgz", + "integrity": "sha512-8vSyjdzwxGDHHwH+cSGch3A9Uj2On3UpgOWxWXMKwUvoAbnujx6DaqmV1duWXNiH/oEWpyVd6nSQccix6DM3Ng==" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2499,6 +2505,11 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "percentile": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/percentile/-/percentile-1.6.0.tgz", + "integrity": "sha512-8vSyjdzwxGDHHwH+cSGch3A9Uj2On3UpgOWxWXMKwUvoAbnujx6DaqmV1duWXNiH/oEWpyVd6nSQccix6DM3Ng==" + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", diff --git a/package.json b/package.json index d44b76e..aebbc27 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "js-vmaf", - "version": "1.0.1", + "version": "1.1.0", "description": "A simple tool to quickly and correctly compare a reference file with a distorted file using VMAF.", "main": "generated/index.js", "scripts": { @@ -21,6 +21,7 @@ "typescript": "^4.9.4" }, "dependencies": { - "argparse": "^2.0.1" + "argparse": "^2.0.1", + "percentile": "^1.6.0" } } diff --git a/source/index.ts b/source/index.ts index f3c9128..908fc66 100644 --- a/source/index.ts +++ b/source/index.ts @@ -8,6 +8,7 @@ import CHILD_PROCESS from "node:child_process"; import PROCESS from "node:process"; import PATH from "node:path"; import FS from "node:fs"; +import PERCENTILE from "percentile"; function valueOrDefault(value : any, fallback : any) : any { if (value === undefined) { @@ -119,7 +120,7 @@ class App { this._argparse.add_argument("-m", "--model", { type: "str", action: "append", - default: [ "version=vmaf_v0.6.1" ], + default: [], help: "Enable (and configure) a model" }); @@ -224,9 +225,15 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AN } async compare(path : string) { - if (!this._args.quiet) console.log(`'${path}' Comparing...`); + if (!this._args.quiet) console.log(`Comparing '${path}'...`); - const cmp = this.FFprobe([path]); + let cmp; + try { + cmp = this.FFprobe([path]); + } catch { + console.error("Not a video file."); + return; + } if (cmp.video.length == 0) { console.error(`'${path}' Missing video track.`); return; @@ -250,6 +257,25 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AN chain.push(`fps=${valueOrDefault(this._args.fps, fref.video[0].r_framerate)}`); } + // Scale + if (this._args.width + || this._args.height) { + chain.push( + `scale=w=${valueOrDefault(this._args.width, -1)}`+ + `:h=${valueOrDefault(this._args.height, -1)}`+ + ":flags=bicubic+full_chroma_inp+full_chroma_int"+ + ":force_original_aspect_ratio=0" + ); + } else if ((this._ref.video[0].width !== fref.video[0].width) + || (this._ref.video[0].height !== fref.video[0].height)) { + chain.push( + `scale=w=${valueOrDefault(fref.video[0].width, this._ref.video[0].width)}`+ + `:h=${valueOrDefault(fref.video[0].height, this._ref.video[0].height)}`+ + ":flags=bicubic+full_chroma_inp+full_chroma_int"+ + ":force_original_aspect_ratio=0" + ); + } + // Convert format and color. if (this._args.color_space || this._args.color_primaries @@ -272,25 +298,6 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AN `:format=${valueOrDefault(this._args.format, valueOrDefault(fref.video[0].pix_fmt, "yuv420p"))}`); } - // Scale - if (this._args.width - || this._args.height) { - chain.push( - `scale=w=${valueOrDefault(this._args.width, -1)}`+ - `:h=${valueOrDefault(this._args.height, -1)}`+ - ":flags=bicubic+full_chroma_inp+full_chroma_int"+ - ":force_original_aspect_ratio=0" - ); - } else if ((this._ref.video[0].width !== fref.video[0].width) - || (this._ref.video[0].height !== fref.video[0].height)) { - chain.push( - `scale=w=${valueOrDefault(fref.video[0].width, this._ref.video[0].width)}`+ - `:h=${valueOrDefault(fref.video[0].height, this._ref.video[0].height)}`+ - ":flags=bicubic+full_chroma_inp+full_chroma_int"+ - ":force_original_aspect_ratio=0" - ); - } - filters.push(`[0:v:0]${chain.join(",")}[ref]`); } { // Adjust distorted to the expected format. @@ -365,7 +372,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AN const proc = this.FFmpeg([ "-hide_banner", - "-v", "info", + "-v", "quiet", "-stats", "-hwaccel", "auto", @@ -421,6 +428,35 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AN }); }); } + + if (PATH.extname(log) === ".json") { + // Post-process the JSON file so we get more useful metrics. + const data = JSON.parse(FS.readFileSync(log).toString("utf-8")); + + const prc = [50, 75, 90, 99, 99.9]; + + const frames : any = {}; + for (const metric in data.pooled_metrics) { + frames[metric] = []; + } + + for (const frame of data.frames) { + for (const metric in frame.metrics) { + frames[metric].push(frame.metrics[metric]); + } + } + + for (const metric in data.pooled_metrics) { + for (const p in prc) { + data.pooled_metrics[metric][`${prc[p]}th %ile`] = PERCENTILE( + 100 - prc[p], + frames[metric] + ); + } + } + + FS.writeFileSync(log, JSON.stringify(data, undefined, "\t")); + } } public async run() : Promise {