123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- '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;
- function runFfprobe(command) {
- const inputProbeIndex = 0;
- if (command._inputs[inputProbeIndex].isStream) {
-
- return;
- }
- command.ffprobe(inputProbeIndex, function(err, data) {
- command._ffprobeData = data;
- });
- }
- module.exports = function(proto) {
-
-
-
-
-
-
-
- proto._spawnFfmpeg = function(args, options, processCB, endCB) {
-
- if (typeof options === 'function') {
- endCB = processCB;
- processCB = options;
- options = {};
- }
-
- if (typeof endCB === 'undefined') {
- endCB = processCB;
- processCB = function() {};
- }
- var maxLines = 'stdoutLines' in options ? options.stdoutLines : this.options.stdoutLines;
-
- this._getFfmpegPath(function(err, command) {
- if (err) {
- return endCB(err);
- } else if (!command || command.length === 0) {
- return endCB(new Error('Cannot find ffmpeg'));
- }
-
- 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;
-
- var ffmpegProc = spawn(command, args, options);
- if (ffmpegProc.stderr) {
- ffmpegProc.stderr.setEncoding('utf8');
- }
- ffmpegProc.on('error', function(err) {
- endCB(err);
- });
-
- var exitError = null;
- function handleExit(err) {
- if (err) {
- exitError = err;
- }
- if (processExited && (stdoutClosed || !options.captureStdout) && stderrClosed) {
- endCB(exitError, stdoutRing, stderrRing);
- }
- }
-
- 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();
- }
- });
-
- if (options.captureStdout) {
- ffmpegProc.stdout.on('data', function(data) {
- stdoutRing.append(data);
- });
- ffmpegProc.stdout.on('close', function() {
- stdoutRing.close();
- stdoutClosed = true;
- handleExit();
- });
- }
-
- ffmpegProc.stderr.on('data', function(data) {
- stderrRing.append(data);
- });
- ffmpegProc.stderr.on('close', function() {
- stderrRing.close();
- stderrClosed = true;
- handleExit();
- });
-
- processCB(ffmpegProc, stdoutRing, stderrRing);
- });
- };
-
- proto._getArguments = function() {
- var complexFilters = this._complexFilters.get();
- var fileOutput = this._outputs.some(function(output) {
- return output.isFile;
- });
- return [].concat(
-
- this._inputs.reduce(function(args, input) {
- var source = (typeof input.source === 'string') ? input.source : 'pipe:0';
-
- return args.concat(
- input.options.get(),
- ['-i', source]
- );
- }, []),
-
- this._global.get(),
-
- fileOutput ? ['-y'] : [],
-
- complexFilters,
-
- 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
- );
- }, [])
- );
- };
-
- proto._prepare = function(callback, readMetadata) {
- var self = this;
- async.waterfall([
-
- function(cb) {
- self._checkCapabilities(cb);
- },
-
- function(cb) {
- if (!readMetadata) {
- return cb();
- }
- self.ffprobe(0, function(err, data) {
- if (!err) {
- self._ffprobeData = data;
- }
- cb();
- });
- },
-
- function(cb) {
- var flvmeta = self._outputs.some(function(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();
- }
- },
-
- function(cb) {
- var args;
- try {
- args = self._getArguments();
- } catch(e) {
- return cb(e);
- }
- cb(null, args);
- },
-
- 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) {
-
- if (this.listeners('progress').length > 0) {
-
- runFfprobe(this);
- } else {
-
- this.once('newListener', function(event) {
- if (event === 'progress') {
- runFfprobe(this);
- }
- });
- }
- }
- };
-
- proto.exec =
- proto.execute =
- proto.run = function() {
- var self = this;
-
- var outputPresent = this._outputs.some(function(output) {
- return 'target' in output;
- });
- if (!outputPresent) {
- throw new Error('No output specified');
- }
-
- var outputStream = this._outputs.filter(function(output) {
- return typeof output.target !== 'string';
- })[0];
-
- var inputStream = this._inputs.filter(function(input) {
- return typeof input.source !== 'string';
- })[0];
-
- 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);
- }
-
- 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(' '));
-
- 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);
-
-
- ffmpegProc.stdin.on('error', function() {});
- }
-
- 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) {
-
- ffmpegProc.stdout.pipe(outputStream.target, outputStream.pipeopts);
-
- outputStream.target.on('close', function() {
- self.logger.debug('Output stream closed, scheduling kill for ffmpeg process');
-
-
-
-
- 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');
- });
- }
-
- if (stderrRing) {
-
- if (self.listeners('stderr').length) {
- stderrRing.callback(function(line) {
- self.emit('stderr', line);
- });
- }
-
- if (self.listeners('codecData').length) {
- var codecDataSent = false;
- var codecObject = {};
- stderrRing.callback(function(line) {
- if (!codecDataSent)
- codecDataSent = utils.extractCodecData(self, line, codecObject);
- });
- }
-
- 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/)) {
-
- err.message += ': ' + utils.extractError(stderrRing.get());
- }
- emitEnd(err, stdoutRing.get(), stderrRing.get());
- } else {
-
- 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());
- }
- }
- }
- );
- });
- };
-
- 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;
- };
-
- 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;
- };
- };
|