123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- /*jshint node:true*/
- 'use strict';
- var spawn = require('child_process').spawn;
- var path = require('path');
- var fs = require('fs');
- var async = require('async');
- var utils = require('./utils');
- var nlRegexp = /\r\n|\r|\n/g;
- /*
- *! Processor methods
- */
- /**
- * Run ffprobe asynchronously and store data in command
- *
- * @param {FfmpegCommand} command
- * @private
- */
- function runFfprobe(command) {
- const inputProbeIndex = 0;
- if (command._inputs[inputProbeIndex].isStream) {
- // Don't probe input streams as this will consume them
- return;
- }
- command.ffprobe(inputProbeIndex, function(err, data) {
- command._ffprobeData = data;
- });
- }
- module.exports = function(proto) {
- /**
- * Emitted just after ffmpeg has been spawned.
- *
- * @event FfmpegCommand#start
- * @param {String} command ffmpeg command line
- */
- /**
- * Emitted when ffmpeg reports progress information
- *
- * @event FfmpegCommand#progress
- * @param {Object} progress progress object
- * @param {Number} progress.frames number of frames transcoded
- * @param {Number} progress.currentFps current processing speed in frames per second
- * @param {Number} progress.currentKbps current output generation speed in kilobytes per second
- * @param {Number} progress.targetSize current output file size
- * @param {String} progress.timemark current video timemark
- * @param {Number} [progress.percent] processing progress (may not be available depending on input)
- */
- /**
- * Emitted when ffmpeg outputs to stderr
- *
- * @event FfmpegCommand#stderr
- * @param {String} line stderr output line
- */
- /**
- * Emitted when ffmpeg reports input codec data
- *
- * @event FfmpegCommand#codecData
- * @param {Object} codecData codec data object
- * @param {String} codecData.format input format name
- * @param {String} codecData.audio input audio codec name
- * @param {String} codecData.audio_details input audio codec parameters
- * @param {String} codecData.video input video codec name
- * @param {String} codecData.video_details input video codec parameters
- */
- /**
- * Emitted when an error happens when preparing or running a command
- *
- * @event FfmpegCommand#error
- * @param {Error} error error object, with optional properties 'inputStreamError' / 'outputStreamError' for errors on their respective streams
- * @param {String|null} stdout ffmpeg stdout, unless outputting to a stream
- * @param {String|null} stderr ffmpeg stderr
- */
- /**
- * Emitted when a command finishes processing
- *
- * @event FfmpegCommand#end
- * @param {Array|String|null} [filenames|stdout] generated filenames when taking screenshots, ffmpeg stdout when not outputting to a stream, null otherwise
- * @param {String|null} stderr ffmpeg stderr
- */
- /**
- * Spawn an ffmpeg process
- *
- * The 'options' argument may contain the following keys:
- * - 'niceness': specify process niceness, ignored on Windows (default: 0)
- * - `cwd`: change working directory
- * - 'captureStdout': capture stdout and pass it to 'endCB' as its 2nd argument (default: false)
- * - 'stdoutLines': override command limit (default: use command limit)
- *
- * The 'processCB' callback, if present, is called as soon as the process is created and
- * receives a nodejs ChildProcess object. It may not be called at all if an error happens
- * before spawning the process.
- *
- * The 'endCB' callback is called either when an error occurs or when the ffmpeg process finishes.
- *
- * @method FfmpegCommand#_spawnFfmpeg
- * @param {Array} args ffmpeg command line argument list
- * @param {Object} [options] spawn options (see above)
- * @param {Function} [processCB] callback called with process object and stdout/stderr ring buffers when process has been created
- * @param {Function} endCB callback called with error (if applicable) and stdout/stderr ring buffers when process finished
- * @private
- */
- proto._spawnFfmpeg = function(args, options, processCB, endCB) {
- // Enable omitting options
- if (typeof options === 'function') {
- endCB = processCB;
- processCB = options;
- options = {};
- }
- // Enable omitting processCB
- if (typeof endCB === 'undefined') {
- endCB = processCB;
- processCB = function() {};
- }
- var maxLines = 'stdoutLines' in options ? options.stdoutLines : this.options.stdoutLines;
- // Find ffmpeg
- this._getFfmpegPath(function(err, command) {
- if (err) {
- return endCB(err);
- } else if (!command || command.length === 0) {
- return endCB(new Error('Cannot find ffmpeg'));
- }
- // Apply niceness
- if (options.niceness && options.niceness !== 0 && !utils.isWindows) {
- args.unshift('-n', options.niceness, command);
- command = 'nice';
- }
- var stdoutRing = utils.linesRing(maxLines);
- var stdoutClosed = false;
- var stderrRing = utils.linesRing(maxLines);
- var stderrClosed = false;
- // Spawn process
- var ffmpegProc = spawn(command, args, options);
- if (ffmpegProc.stderr) {
- ffmpegProc.stderr.setEncoding('utf8');
- }
- ffmpegProc.on('error', function(err) {
- endCB(err);
- });
- // Ensure we wait for captured streams to end before calling endCB
- var exitError = null;
- function handleExit(err) {
- if (err) {
- exitError = err;
- }
- if (processExited && (stdoutClosed || !options.captureStdout) && stderrClosed) {
- endCB(exitError, stdoutRing, stderrRing);
- }
- }
- // Handle process exit
- var processExited = false;
- ffmpegProc.on('exit', function(code, signal) {
- processExited = true;
- if (signal) {
- handleExit(new Error('ffmpeg was killed with signal ' + signal));
- } else if (code) {
- handleExit(new Error('ffmpeg exited with code ' + code));
- } else {
- handleExit();
- }
- });
- // Capture stdout if specified
- if (options.captureStdout) {
- ffmpegProc.stdout.on('data', function(data) {
- stdoutRing.append(data);
- });
- ffmpegProc.stdout.on('close', function() {
- stdoutRing.close();
- stdoutClosed = true;
- handleExit();
- });
- }
- // Capture stderr if specified
- ffmpegProc.stderr.on('data', function(data) {
- stderrRing.append(data);
- });
- ffmpegProc.stderr.on('close', function() {
- stderrRing.close();
- stderrClosed = true;
- handleExit();
- });
- // Call process callback
- processCB(ffmpegProc, stdoutRing, stderrRing);
- });
- };
- /**
- * Build the argument list for an ffmpeg command
- *
- * @method FfmpegCommand#_getArguments
- * @return argument list
- * @private
- */
- proto._getArguments = function() {
- var complexFilters = this._complexFilters.get();
- var fileOutput = this._outputs.some(function(output) {
- return output.isFile;
- });
- return [].concat(
- // Inputs and input options
- this._inputs.reduce(function(args, input) {
- var source = (typeof input.source === 'string') ? input.source : 'pipe:0';
- // For each input, add input options, then '-i <source>'
- return args.concat(
- input.options.get(),
- ['-i', source]
- );
- }, []),
- // Global options
- this._global.get(),
- // Overwrite if we have file outputs
- fileOutput ? ['-y'] : [],
- // Complex filters
- complexFilters,
- // Outputs, filters and output options
- this._outputs.reduce(function(args, output) {
- var sizeFilters = utils.makeFilterStrings(output.sizeFilters.get());
- var audioFilters = output.audioFilters.get();
- var videoFilters = output.videoFilters.get().concat(sizeFilters);
- var outputArg;
- if (!output.target) {
- outputArg = [];
- } else if (typeof output.target === 'string') {
- outputArg = [output.target];
- } else {
- outputArg = ['pipe:1'];
- }
- return args.concat(
- output.audio.get(),
- audioFilters.length ? ['-filter:a', audioFilters.join(',')] : [],
- output.video.get(),
- videoFilters.length ? ['-filter:v', videoFilters.join(',')] : [],
- output.options.get(),
- outputArg
- );
- }, [])
- );
- };
- /**
- * Prepare execution of an ffmpeg command
- *
- * Checks prerequisites for the execution of the command (codec/format availability, flvtool...),
- * then builds the argument list for ffmpeg and pass them to 'callback'.
- *
- * @method FfmpegCommand#_prepare
- * @param {Function} callback callback with signature (err, args)
- * @param {Boolean} [readMetadata=false] read metadata before processing
- * @private
- */
- proto._prepare = function(callback, readMetadata) {
- var self = this;
- async.waterfall([
- // Check codecs and formats
- function(cb) {
- self._checkCapabilities(cb);
- },
- // Read metadata if required
- function(cb) {
- if (!readMetadata) {
- return cb();
- }
- self.ffprobe(0, function(err, data) {
- if (!err) {
- self._ffprobeData = data;
- }
- cb();
- });
- },
- // Check for flvtool2/flvmeta if necessary
- function(cb) {
- var flvmeta = self._outputs.some(function(output) {
- // Remove flvmeta flag on non-file output
- if (output.flags.flvmeta && !output.isFile) {
- self.logger.warn('Updating flv metadata is only supported for files');
- output.flags.flvmeta = false;
- }
- return output.flags.flvmeta;
- });
- if (flvmeta) {
- self._getFlvtoolPath(function(err) {
- cb(err);
- });
- } else {
- cb();
- }
- },
- // Build argument list
- function(cb) {
- var args;
- try {
- args = self._getArguments();
- } catch(e) {
- return cb(e);
- }
- cb(null, args);
- },
- // Add "-strict experimental" option where needed
- function(args, cb) {
- self.availableEncoders(function(err, encoders) {
- for (var i = 0; i < args.length; i++) {
- if (args[i] === '-acodec' || args[i] === '-vcodec') {
- i++;
- if ((args[i] in encoders) && encoders[args[i]].experimental) {
- args.splice(i + 1, 0, '-strict', 'experimental');
- i += 2;
- }
- }
- }
- cb(null, args);
- });
- }
- ], callback);
- if (!readMetadata) {
- // Read metadata as soon as 'progress' listeners are added
- if (this.listeners('progress').length > 0) {
- // Read metadata in parallel
- runFfprobe(this);
- } else {
- // Read metadata as soon as the first 'progress' listener is added
- this.once('newListener', function(event) {
- if (event === 'progress') {
- runFfprobe(this);
- }
- });
- }
- }
- };
- /**
- * Run ffmpeg command
- *
- * @method FfmpegCommand#run
- * @category Processing
- * @aliases exec,execute
- */
- proto.exec =
- proto.execute =
- proto.run = function() {
- var self = this;
- // Check if at least one output is present
- var outputPresent = this._outputs.some(function(output) {
- return 'target' in output;
- });
- if (!outputPresent) {
- throw new Error('No output specified');
- }
- // Get output stream if any
- var outputStream = this._outputs.filter(function(output) {
- return typeof output.target !== 'string';
- })[0];
- // Get input stream if any
- var inputStream = this._inputs.filter(function(input) {
- return typeof input.source !== 'string';
- })[0];
- // Ensure we send 'end' or 'error' only once
- var ended = false;
- function emitEnd(err, stdout, stderr) {
- if (!ended) {
- ended = true;
- if (err) {
- self.emit('error', err, stdout, stderr);
- } else {
- self.emit('end', stdout, stderr);
- }
- }
- }
- self._prepare(function(err, args) {
- if (err) {
- return emitEnd(err);
- }
- // Run ffmpeg
- self._spawnFfmpeg(
- args,
- {
- captureStdout: !outputStream,
- niceness: self.options.niceness,
- cwd: self.options.cwd
- },
- function processCB(ffmpegProc, stdoutRing, stderrRing) {
- self.ffmpegProc = ffmpegProc;
- self.emit('start', 'ffmpeg ' + args.join(' '));
- // Pipe input stream if any
- if (inputStream) {
- inputStream.source.on('error', function(err) {
- var reportingErr = new Error('Input stream error: ' + err.message);
- reportingErr.inputStreamError = err;
- emitEnd(reportingErr);
- ffmpegProc.kill();
- });
- inputStream.source.resume();
- inputStream.source.pipe(ffmpegProc.stdin);
- // Set stdin error handler on ffmpeg (prevents nodejs catching the error, but
- // ffmpeg will fail anyway, so no need to actually handle anything)
- ffmpegProc.stdin.on('error', function() {});
- }
- // Setup timeout if requested
- var processTimer;
- if (self.options.timeout) {
- processTimer = setTimeout(function() {
- var msg = 'process ran into a timeout (' + self.options.timeout + 's)';
- emitEnd(new Error(msg), stdoutRing.get(), stderrRing.get());
- ffmpegProc.kill();
- }, self.options.timeout * 1000);
- }
- if (outputStream) {
- // Pipe ffmpeg stdout to output stream
- ffmpegProc.stdout.pipe(outputStream.target, outputStream.pipeopts);
- // Handle output stream events
- outputStream.target.on('close', function() {
- self.logger.debug('Output stream closed, scheduling kill for ffmpeg process');
- // Don't kill process yet, to give a chance to ffmpeg to
- // terminate successfully first This is necessary because
- // under load, the process 'exit' event sometimes happens
- // after the output stream 'close' event.
- setTimeout(function() {
- emitEnd(new Error('Output stream closed'));
- ffmpegProc.kill();
- }, 20);
- });
- outputStream.target.on('error', function(err) {
- self.logger.debug('Output stream error, killing ffmpeg process');
- var reportingErr = new Error('Output stream error: ' + err.message);
- reportingErr.outputStreamError = err;
- emitEnd(reportingErr, stdoutRing.get(), stderrRing.get());
- ffmpegProc.kill('SIGKILL');
- });
- }
- // Setup stderr handling
- if (stderrRing) {
- // 'stderr' event
- if (self.listeners('stderr').length) {
- stderrRing.callback(function(line) {
- self.emit('stderr', line);
- });
- }
- // 'codecData' event
- if (self.listeners('codecData').length) {
- var codecDataSent = false;
- var codecObject = {};
- stderrRing.callback(function(line) {
- if (!codecDataSent)
- codecDataSent = utils.extractCodecData(self, line, codecObject);
- });
- }
- // 'progress' event
- if (self.listeners('progress').length) {
- stderrRing.callback(function(line) {
- utils.extractProgress(self, line);
- });
- }
- }
- },
- function endCB(err, stdoutRing, stderrRing) {
- delete self.ffmpegProc;
- if (err) {
- if (err.message.match(/ffmpeg exited with code/)) {
- // Add ffmpeg error message
- err.message += ': ' + utils.extractError(stderrRing.get());
- }
- emitEnd(err, stdoutRing.get(), stderrRing.get());
- } else {
- // Find out which outputs need flv metadata
- var flvmeta = self._outputs.filter(function(output) {
- return output.flags.flvmeta;
- });
- if (flvmeta.length) {
- self._getFlvtoolPath(function(err, flvtool) {
- if (err) {
- return emitEnd(err);
- }
- async.each(
- flvmeta,
- function(output, cb) {
- spawn(flvtool, ['-U', output.target])
- .on('error', function(err) {
- cb(new Error('Error running ' + flvtool + ' on ' + output.target + ': ' + err.message));
- })
- .on('exit', function(code, signal) {
- if (code !== 0 || signal) {
- cb(
- new Error(flvtool + ' ' +
- (signal ? 'received signal ' + signal
- : 'exited with code ' + code)) +
- ' when running on ' + output.target
- );
- } else {
- cb();
- }
- });
- },
- function(err) {
- if (err) {
- emitEnd(err);
- } else {
- emitEnd(null, stdoutRing.get(), stderrRing.get());
- }
- }
- );
- });
- } else {
- emitEnd(null, stdoutRing.get(), stderrRing.get());
- }
- }
- }
- );
- });
- };
- /**
- * Renice current and/or future ffmpeg processes
- *
- * Ignored on Windows platforms.
- *
- * @method FfmpegCommand#renice
- * @category Processing
- *
- * @param {Number} [niceness=0] niceness value between -20 (highest priority) and 20 (lowest priority)
- * @return FfmpegCommand
- */
- proto.renice = function(niceness) {
- if (!utils.isWindows) {
- niceness = niceness || 0;
- if (niceness < -20 || niceness > 20) {
- this.logger.warn('Invalid niceness value: ' + niceness + ', must be between -20 and 20');
- }
- niceness = Math.min(20, Math.max(-20, niceness));
- this.options.niceness = niceness;
- if (this.ffmpegProc) {
- var logger = this.logger;
- var pid = this.ffmpegProc.pid;
- var renice = spawn('renice', [niceness, '-p', pid]);
- renice.on('error', function(err) {
- logger.warn('could not renice process ' + pid + ': ' + err.message);
- });
- renice.on('exit', function(code, signal) {
- if (signal) {
- logger.warn('could not renice process ' + pid + ': renice was killed by signal ' + signal);
- } else if (code) {
- logger.warn('could not renice process ' + pid + ': renice exited with ' + code);
- } else {
- logger.info('successfully reniced process ' + pid + ' to ' + niceness + ' niceness');
- }
- });
- }
- }
- return this;
- };
- /**
- * Kill current ffmpeg process, if any
- *
- * @method FfmpegCommand#kill
- * @category Processing
- *
- * @param {String} [signal=SIGKILL] signal name
- * @return FfmpegCommand
- */
- proto.kill = function(signal) {
- if (!this.ffmpegProc) {
- this.logger.warn('No running ffmpeg process, cannot send signal');
- } else {
- this.ffmpegProc.kill(signal || 'SIGKILL');
- }
- return this;
- };
- };
|