ffmpeg-progress-wrapper
Version:
A simple wrapper that helps with determinng the progress of the ffmpeg conversion
233 lines • 8.91 kB
JavaScript
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
const events_1 = require("events");
const child_process_1 = require("child_process");
const error_1 = require("./error");
const helper_1 = require("./helper");
const ReadLine = require("readline");
__export(require("./error"));
class FFMpegProgress extends events_1.EventEmitter {
constructor(args, options = {}) {
super();
this._details = {};
this._metadataDuration = null;
this._stderr = '';
this._isKilledByUser = false;
this._outOfMemory = false;
this.processOutput = (buffer) => {
const text = buffer.toString();
this.emit('raw', text);
// parsing duration from metadata
const isMetadataDuration = text.toLowerCase().match(/duration\s*:\s*((\d+:?){1,3}.\d+)/);
if (isMetadataDuration) {
this.processMetadataDuration(isMetadataDuration[1]);
}
// await for duration details
if (!this._details.file &&
~this._stderr.toLowerCase().search(/duration.*\n/i) &&
~this._stderr.toLowerCase().search(/(\d+\.?\d*?) fps/i)) {
this.processInitialOutput(this._stderr);
}
};
this.options = {
cmd: options.cmd || 'ffmpeg',
cwd: options.cwd || process.cwd(),
env: options.env || process.env,
hideFFConfig: options.hideFFConfig || false,
maxMemory: Math.max(0, options.maxMemory) || Infinity,
duration: options.duration
};
const extra_args = ['-progress', 'pipe:3'];
if (this.options.hideFFConfig) {
extra_args.push(`-hide_banner`);
}
this._args = args.slice();
this._process = child_process_1.spawn(this.options.cmd, extra_args.concat(args), {
cwd: this.options.cwd,
env: this.options.env,
stdio: [null, null, null, "pipe"]
});
this._process.stderr.on('data', this.processOutput);
this._process.stderr.on('data', (d) => this._stderr += d.toString());
this._progressCheck(this._process.stdio[3]);
this._process.once('close', (code, signal) => {
this.emit('end', code, signal);
clearInterval(this._vitalsTimer);
});
this._vitalsTimer = setInterval(this._checkVitals.bind(this), 500);
}
_progressCheck(stream) {
const lineReader = ReadLine.createInterface({ input: stream });
let lines = [];
lineReader.on('line', line => {
line = line.trim();
if (!line)
return;
lines.push(line.trim());
if (line.indexOf('progress=') === 0) {
this.processProgress(lines);
lines = [];
}
});
}
async _checkVitals() {
try {
const vitals = await helper_1.pidToResourceUsage(this._process.pid);
this._vitalsMemory = vitals.memory;
if (vitals.memory > this.options.maxMemory) {
this._outOfMemory = true;
this.kill();
}
}
catch (e) {
if (!e.stack) {
Error.captureStackTrace(e);
}
console.error(`Vitals check for PID:${this._process.pid} resulted in: ${e.stack}`);
}
}
kill(signal = 'SIGKILL') {
this._isKilledByUser = signal;
this._process.kill(signal);
}
stop() {
return this.kill('SIGINT');
}
get details() {
return this._details.file;
}
get stderrOutput() {
return this._stderr;
}
get stdout() {
return this.process.stdout;
}
get process() {
return this._process;
}
get args() {
return this._args.slice();
}
async onDone() {
const stack = (new Error()).stack.split('\n').slice(1).join('\n');
const { code, signal } = await new Promise((res) => {
this.once('end', (code, signal) => {
return res({ code, signal });
});
});
if (code || signal) {
let FFmpegErrClass = error_1.FFMpegError;
if (this._outOfMemory) {
FFmpegErrClass = error_1.FFMpegOutOfMemoryError;
}
const err = new FFmpegErrClass(this._stderr);
err.code = code;
err.signal = signal;
err.args = this._args.slice();
err.killedByUser = signal === this._isKilledByUser;
err.stack += '\n' + stack;
if (this._outOfMemory) {
err.allocated = this.options.maxMemory;
err.wasUsing = this._vitalsMemory;
}
throw err;
}
return this._stderr;
}
async onDetails() {
if (this._details.file) {
return Promise.resolve(this._details.file);
}
return new Promise(_ => this.once('details', _));
}
processMetadataDuration(humanDuration) {
this._metadataDuration = Math.max(this._metadataDuration, helper_1.humanTimeToMS(humanDuration));
}
processInitialOutput(text) {
Object.assign(this._details, {
file: {
duration: helper_1.Parse.getDuration(text),
bitrate: helper_1.Parse.getBitrate(text),
start: helper_1.Parse.getStart(text),
resolution: helper_1.Parse.getRes(text),
fps: helper_1.Parse.getFPS(text)
}
});
this.emit('details', Object.assign({}, this._details.file));
}
processProgress(lines) {
const duration = this.options.duration || (this._details.file && this._details.file.duration) || this._metadataDuration || null;
const data = lines
.map(keyVal => {
const split = keyVal.split('=');
return [
split[0].trim(),
split[1].trim()
];
})
.reduce((obj, kv) => {
obj[kv[0]] = !isNaN(Number(kv[1])) ? parseFloat(kv[1]) : kv[1];
return obj;
}, {});
const out = {
drop: data.drop_frames,
dup: data.dup_frames,
frame: data.frame === undefined ? null : data.frame,
time: data.out_time_us === 'N/A' ? null : data.out_time_us / 1e6,
speed: data.speed === 'N/A' ? null : parseFloat(data.speed.toString().replace('x', '')),
fps: data.fps === undefined ? null : data.fps,
eta: null,
progress: null,
quality: [],
psnr: [],
size: data.total_size === 'N/A' ? null : data.total_size,
bitrate: data.bitrate === 'N/A' ? null : parseFloat(data.bitrate.replace('kbits/s', '')) * 1024
};
if (duration !== null) {
// compute progress
out.progress = out.time / duration;
// compute ETA
out.eta = Math.max((duration - out.time) / out.speed, 0);
}
Object.keys(data)
.filter(x => x.indexOf('stream_') === 0)
.forEach(key => {
const quality_data = /^stream_(\d+)_(\d+)_q$/.exec(key);
const psnr_data = /^stream_(\d+)_(\d+)_psnr_(y|u|v|all)$/.exec(key);
if (quality_data) {
const [_, file_index_s, stream_index_s] = quality_data;
const file_index = parseInt(file_index_s);
const stream_index = parseInt(stream_index_s);
if (!out.quality[file_index])
out.quality[file_index] = [];
out.quality[file_index][stream_index] = parseFloat(data[key].toString());
}
if (psnr_data) {
const [_, file_index_s, stream_index_s, channel] = psnr_data;
const file_index = parseInt(file_index_s);
const stream_index = parseInt(stream_index_s);
if (!out.psnr[file_index])
out.psnr[file_index] = [];
if (!out.psnr[file_index][stream_index])
out.psnr[file_index][stream_index] = {
y: null,
u: null,
v: null,
all: null
};
out.psnr[file_index][stream_index][channel] =
data[key] === 'inf' ?
Infinity :
data[key] === 'nan' ?
NaN :
parseFloat(data[key].toString());
}
});
this.emit('progress', out);
}
}
exports.FFMpegProgress = FFMpegProgress;
//# sourceMappingURL=index.js.map