|
|
|
'use strict'; |
|
|
|
var spawn = require('child_process').spawn; |
|
var path = require('path'); |
|
var fs = require('fs'); |
|
var async = require('async'); |
|
var utils = require('./utils'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, |
|
windowsHide: true |
|
}, |
|
|
|
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() {}); |
|
} |
|
|
|
|
|
if (self.options.timeout) { |
|
self.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) { |
|
clearTimeout(self.processTimer); |
|
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], {windowsHide: true}) |
|
.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()); |
|
} |
|
} |
|
} |
|
); |
|
}); |
|
|
|
return this; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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], {windowsHide: true}); |
|
|
|
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; |
|
}; |
|
}; |
|
|