UNPKG

ffmpeg-progress-wrapper

Version:

A simple wrapper that helps with determinng the progress of the ffmpeg conversion

233 lines 8.91 kB
"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