From 19470a3ededfec1f2d7390b6db3dc252d5080df1 Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Tue, 25 Feb 2014 09:28:46 +0100 Subject: [PATCH 1/8] Use boolean argument for constant video bitrate --- lib/fluent-ffmpeg.js | 7 +++---- test/args.test.js | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/fluent-ffmpeg.js b/lib/fluent-ffmpeg.js index ea7da776..e49eeacc 100644 --- a/lib/fluent-ffmpeg.js +++ b/lib/fluent-ffmpeg.js @@ -94,11 +94,11 @@ function FfmpegCommand(args) { this.options.audio.skip = true; return this; }; - FfmpegCommand.prototype.withVideoBitrate = function(vbitrate, type) { + FfmpegCommand.prototype.withVideoBitrate = function(vbitrate, constant) { if (typeof vbitrate === 'string' && vbitrate.indexOf('k') > 0) { vbitrate = vbitrate.replace('k', ''); } - if (type && type === exports.CONSTANT_BITRATE) { + if (constant) { this.options._useConstantVideoBitrate = true; } this.options.video.bitrate = parseInt(vbitrate, 10); @@ -342,5 +342,4 @@ exports = module.exports = function(args) { exports.Metadata = metaDataLib; exports.Calculate = require('./calculate'); -exports.CONSTANT_BITRATE = 1; -exports.VARIABLE_BITRATE = 2; +exports.CONSTANT_BITRATE = true; diff --git a/test/args.test.js b/test/args.test.js index 9f114335..aadea00c 100644 --- a/test/args.test.js +++ b/test/args.test.js @@ -100,9 +100,9 @@ describe('Command', function() { done(); }); }); - it('should apply additional bitrate arguments for CONSTANT_BITRATE', function(done) { + it('should apply additional bitrate arguments for constant bitrate', function(done) { new Ffmpeg({ source: this.testfile, nolog: true }) - .withVideoBitrate('256k', Ffmpeg.CONSTANT_BITRATE) + .withVideoBitrate('256k', true) .getArgs(function(args) { args.indexOf('-b:v').should.above(-1); args.indexOf('-maxrate').should.above(-1);; From a405312e5c5673b9eee06ecaa7d79a6ecbb02db3 Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Tue, 25 Feb 2014 09:40:37 +0100 Subject: [PATCH 2/8] Allow usingPreset to use preset functions --- lib/fluent-ffmpeg.js | 20 ++++++++++++-------- test/args.test.js | 25 ++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/lib/fluent-ffmpeg.js b/lib/fluent-ffmpeg.js index e49eeacc..1069c014 100644 --- a/lib/fluent-ffmpeg.js +++ b/lib/fluent-ffmpeg.js @@ -74,15 +74,19 @@ function FfmpegCommand(args) { // public chaining methods FfmpegCommand.prototype.usingPreset = function(preset) { - // require preset (since require() works like a singleton, multiple calls generate no overhead) - try { - var module = require('./presets/' + preset); - if (typeof module.load === 'function') { - module.load(this); + if (typeof preset === 'function') { + preset(this); + } else { + // require preset (since require() works like a singleton, multiple calls generate no overhead) + try { + var module = require('./presets/' + preset); + if (typeof module.load === 'function') { + module.load(this); + } + return this; + } catch (err) { + throw new Error('preset ' + preset + ' could not be loaded'); } - return this; - } catch (err) { - throw new Error('preset ' + preset + ' could not be loaded'); } return this; }; diff --git a/test/args.test.js b/test/args.test.js index aadea00c..1ac3f367 100644 --- a/test/args.test.js +++ b/test/args.test.js @@ -37,6 +37,29 @@ describe('Command', function() { done(); }); }); + + it('should allow using functions as presets', function(done) { + var presetArg; + + function presetFunc(command) { + presetArg = command; + command.withVideoCodec('libx264'); + command.withAudioFrequency(22050); + } + + var cmd = new Ffmpeg({ source: this.testfile }); + + cmd + .usingPreset(presetFunc) + .getArgs(function(args) { + presetArg.should.equal(cmd); + args.join(' ').indexOf('-vcodec libx264').should.not.equal(-1); + args.join(' ').indexOf('-ar 22050').should.not.equal(-1); + + done(); + }); + }); + it('should throw an exception when a preset it not found', function() { (function() { new Ffmpeg({ source: this.testfile, nolog: true }) @@ -264,7 +287,7 @@ describe('Command', function() { }); }); - describe('withVideCodec', function() { + describe('withVideoCodec', function() { it('should apply the video codec argument', function(done) { new Ffmpeg({ source: this.testfile, nolog: true }) .withVideoCodec('divx') From 23bea3f551da0c28d6f66d5b39d6cfdd33aa216b Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Tue, 25 Feb 2014 18:00:01 +0100 Subject: [PATCH 3/8] Add addInputOption and addInputOptions --- lib/fluent-ffmpeg.js | 20 +++++++++++++++----- lib/processor.js | 9 +++++++++ test/args.test.js | 21 ++++++++++++++++++++- 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/lib/fluent-ffmpeg.js b/lib/fluent-ffmpeg.js index 1069c014..8689321b 100644 --- a/lib/fluent-ffmpeg.js +++ b/lib/fluent-ffmpeg.js @@ -66,6 +66,7 @@ function FfmpegCommand(args) { video: {}, audio: {}, additional: [], + inputOptions: [], otherInputs: [], informInputAudioCodec: null, informInputVideoCodec: null, @@ -195,22 +196,31 @@ function FfmpegCommand(args) { this.options.otherInputs.push(inputFile); return this; }; - FfmpegCommand.prototype.addOptions = function(optionArray) { + FfmpegCommand.prototype.addInputOptions = function(optionsArray) { + return this.addOptions(optionsArray, true); + }; + FfmpegCommand.prototype.addOptions = function(optionArray, forInput) { + var target = forInput ? this.options.inputOptions : this.options.additional; + if (typeof optionArray.length !== undefined) { var self = this; optionArray.forEach(function(el) { if (el.indexOf(' ') > 0) { var values = el.split(' '); - self.options.additional.push(values[0], values[1]); + target.push(values[0], values[1]); } else { - self.options.additional.push(el); + target.push(el); } }); } return this; }; - FfmpegCommand.prototype.addOption = function(option, value) { - this.options.additional.push(option, value); + FfmpegCommand.prototype.addInputOption = function(option, value) { + return this.addOption(option, value, true); + }; + FfmpegCommand.prototype.addOption = function(option, value, forInput) { + var target = forInput ? this.options.inputOptions : this.options.additional; + target.push(option, value); return this; }; FfmpegCommand.prototype.mergeAdd = function(path){ diff --git a/lib/processor.js b/lib/processor.js index eac33de7..55ffc64e 100644 --- a/lib/processor.js +++ b/lib/processor.js @@ -691,6 +691,15 @@ exports = module.exports = function Processor(command) { args.push('-f', this.options.fromFormat); } + // add additional input options + if (this.options.inputOptions) { + if (this.options.inputOptions.length > 0) { + this.options.inputOptions.forEach(function(el) { + args.push(el); + }); + } + } + // add input file (if using fs mode) if (this.options.inputfile && !this.options.inputstream && !this.options.inputlive) { // add input file fps diff --git a/test/args.test.js b/test/args.test.js index 1ac3f367..48965d96 100644 --- a/test/args.test.js +++ b/test/args.test.js @@ -59,7 +59,7 @@ describe('Command', function() { done(); }); }); - + it('should throw an exception when a preset it not found', function() { (function() { new Ffmpeg({ source: this.testfile, nolog: true }) @@ -462,6 +462,25 @@ describe('Command', function() { done(); }); }); + it('should apply a single input option', function(done) { + new Ffmpeg({ source: this.testfile }) + .addInputOption('-r', '29.97') + .getArgs(function(args) { + var joined = args.join(' '); + joined.indexOf('-r 29.97').should.above(-1).and.below(joined.indexOf('-i ')); + done(); + }); + }); + it('should apply multiple input options', function(done) { + new Ffmpeg({ source: this.testfile }) + .addInputOptions(['-r 29.97', '-f ogg']) + .getArgs(function(args) { + var joined = args.join(' '); + joined.indexOf('-r 29.97').should.above(-1).and.below(joined.indexOf('-i')); + joined.indexOf('-f ogg').should.above(-1).and.below(joined.indexOf('-i')); + done(); + }); + }); }); describe('toFormat', function() { From c808b574dc30926f439b3f477f54c49bb17edbdd Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Wed, 26 Feb 2014 19:56:22 +0100 Subject: [PATCH 4/8] Document small API changes --- README.md | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a31c12b9..5530b6cc 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ new FFmpeg({ source: '/path/to/video.avi' }) .withVideoBitrate('650k') // Specify a constant video bitrate - .withVideoBitrate('650k', FFmpeg.CONSTANT_BITRATE) + .withVideoBitrate('650k', true) /** Video size **/ @@ -202,6 +202,13 @@ new FFmpeg({ source: '/path/to/video.avi' }) // Use strict experimental flag (needed for some codecs) .withStrictExperimental() + // Add custom input option (will be added before the input + // on ffmpeg command line) + .addInputOption('-f', 'avi') + + // Add several input options at once + .addInputOptions(['-f avi', '-ss 2:30']) + // Add custom option .addOption('-crf', '23') @@ -314,7 +321,26 @@ new FFmpeg({ source: '/path/to/video.avi' }) ### Using presets -Presets are located in fluent-ffmpeg `lib/presets` directory. To use a preset, call the `usingPreset` method on a command. +#### Preset functions + +You can define a preset as a function that takes an `FfmpegCommand` as an argument and calls method on it, and then pass it to `usePreset`. + +```js +function myPreset(command) { + command + .withAudioCodec('libmp3lame') + .withVideoCodec('libx264') + .withSize('320x240'); +} + +new Ffmpeg({ source: '/path/to/video.avi' }) + .usingPreset(myPreset) + .saveToFile('/path/to/converted.mp4'); +``` + +#### Preset modules + +Preset modules are located in fluent-ffmpeg `lib/presets` directory. To use a preset, call the `usingPreset` method on a command. ```js new FFmpeg({ source: '/path/to/video.avi' }) @@ -333,7 +359,7 @@ exports.load = function(command) { }; ``` -fluent-ffmpeg comes with the following presets preinstalled: +fluent-ffmpeg comes with the following preset modules preinstalled: * `divx` * `flashvideo` * `podcast` From 5bf1d4dd33334a81acd3fb91f41950588e402100 Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Thu, 27 Feb 2014 06:20:46 +0100 Subject: [PATCH 5/8] Make stream tests output more info on failure --- test/processor.test.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/processor.test.js b/test/processor.test.js index d6e3584f..38dc00f9 100644 --- a/test/processor.test.js +++ b/test/processor.test.js @@ -394,6 +394,10 @@ describe('Processor', function() { var outstream = fs.createWriteStream(testFile); new Ffmpeg({ source: this.testfile, nolog: true }) .usingPreset('flashvideo') + .on('error', function(err) { + console.log('got error ' + err.message); + assert.ok(!err); + }) .on('end', function(stdout, stderr) { fs.exists(testFile, function(exist) { if (!exist) { @@ -422,8 +426,16 @@ describe('Processor', function() { var outstream = fs.createWriteStream(testFile); new Ffmpeg({ source: instream, nolog: true }) .usingPreset('flashvideo') - .on('end', function() { + .on('error', function(err) { + console.log('got error ' + err.message); + assert.ok(!err); + }) + .on('end', function(stdout,stderr) { fs.exists(testFile, function(exist) { + if (!exist) { + console.log(stderr); + } + exist.should.true; // check filesize to make sure conversion actually worked fs.stat(testFile, function(err, stats) { From 56b85600085279aca3839b78b9ad15f47aaca7fb Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Fri, 28 Feb 2014 07:13:01 +0100 Subject: [PATCH 6/8] Use full precision timestamps, fixes #81 --- README.md | 1 + lib/extensions.js | 9 ++------- test/extensions.test.js | 4 ++-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 5530b6cc..94a224c9 100644 --- a/README.md +++ b/README.md @@ -256,6 +256,7 @@ var command = new FFmpeg({ source: '/path/to/video.avi' }) // - 'targetSize': the current size of the target file // in kilobytes // - 'timemark': the timestamp of the current frame + // in seconds // - 'percent': an estimation of the progress console.log('Processing: ' + progress.percent + '% done'); diff --git a/lib/extensions.js b/lib/extensions.js index 26c160df..00da0737 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -39,19 +39,14 @@ exports = module.exports = function Extensions(command) { }; command.prototype.ffmpegTimemarkToSeconds = function(timemark) { - // In case ffmpeg outputs the timemark as float if(timemark.indexOf(':') === -1 && timemark.indexOf('.') >= 0) - return parseInt(timemark) + return parseFloat(timemark) var parts = timemark.split(':'); - var secs = 0; - - // split sec/msec part - var secParts = parts.pop().split('.'); // add seconds - secs += parseInt(secParts[0], 10); + var secs = parseFloat(parts.pop()); if (parts.length) { // add minutes diff --git a/test/extensions.test.js b/test/extensions.test.js index bd59444c..bf9b774d 100644 --- a/test/extensions.test.js +++ b/test/extensions.test.js @@ -45,10 +45,10 @@ describe('Extensions', function() { ext.ffmpegTimemarkToSeconds('00:02:00.00').should.be.equal(120); }); it('should correctly convert a complex timestamp', function() { - ext.ffmpegTimemarkToSeconds('00:08:09.10').should.be.equal(489); + ext.ffmpegTimemarkToSeconds('00:08:09.10').should.be.equal(489.1); }); it('should correclty convert a simple float timestamp', function() { - ext.ffmpegTimemarkToSeconds('132.44').should.be.equal(132); + ext.ffmpegTimemarkToSeconds('132.44').should.be.equal(132.44); }); }); }); From 7ce0fe60bef35214376794b7e5991300cfdf66c2 Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Fri, 28 Feb 2014 07:45:45 +0100 Subject: [PATCH 7/8] Add withAudioFilter/withVideoFilter --- README.md | 9 +++++++++ lib/fluent-ffmpeg.js | 10 ++++++++++ lib/processor.js | 13 +++++++++++-- test/args.test.js | 36 +++++++++++++++++++++++++++++++----- 4 files changed, 61 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 94a224c9..f5517bf0 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,15 @@ new FFmpeg({ source: '/path/to/video.avi' }) // Set output format .toFormat('webm') + /** Custom filters **/ + + // Add custom audio filters + .withAudioFilter('equalizer=f=1000:width_type=h:width=200:g=-10') + .withAudioFilter('pan=1:c0=0.9*c0+0.1*c1') + + // Add custom video filters + .withVideoFilter('size=iw*1.5:ih/2') + .withVideoFilter('drawtext=\'fontfile=FreeSans.ttf:text=Hello\'') /** Miscellaneous options **/ diff --git a/lib/fluent-ffmpeg.js b/lib/fluent-ffmpeg.js index 8689321b..b611e29f 100644 --- a/lib/fluent-ffmpeg.js +++ b/lib/fluent-ffmpeg.js @@ -150,6 +150,11 @@ function FfmpegCommand(args) { this.options.video.codec = codec; return this; }; + FfmpegCommand.prototype.withVideoFilter = function(filter) { + this.options.video.filters = this.options.video.filters || []; + this.options.video.filters.push(filter); + return this; + }; FfmpegCommand.prototype.loop = function(duration) { this.options.video.loop = true; if (duration) { @@ -184,6 +189,11 @@ function FfmpegCommand(args) { this.options.audio.quality = parseInt(quality, 10); return this; }; + FfmpegCommand.prototype.withAudioFilter = function(filter) { + this.options.audio.filters = this.options.audio.filters || []; + this.options.audio.filters.push(filter); + return this; + }; FfmpegCommand.prototype.setStartTime = function(timestamp) { this.options.starttime = timestamp; return this; diff --git a/lib/processor.js b/lib/processor.js index 55ffc64e..a5edd345 100644 --- a/lib/processor.js +++ b/lib/processor.js @@ -809,6 +809,10 @@ exports = module.exports = function Processor(command) { } } + if (this.options.audio.filters) { + args.push('-filter:a', this.options.audio.filters.join(',')); + } + // add additional options if (this.options.additional) { if (this.options.additional.length > 0) { @@ -818,12 +822,13 @@ exports = module.exports = function Processor(command) { } } + var videoFilters = this.options.video.filters || []; + if (this.options.video.pad && !this.options.video.skip) { // we have padding arguments, push if (this.atLeastVersion(this.metaData.ffmpegversion, '0.7')) { // padding is not supported ffmpeg < 0.7 (only using legacy commands which were replaced by vfilter calls) - args.push('-vf'); - args.push('pad=' + this.options.video.pad.w + + videoFilters.push('pad=' + this.options.video.pad.w + ':' + this.options.video.pad.h + ':' + this.options.video.pad.x + ':' + this.options.video.pad.y + @@ -833,6 +838,10 @@ exports = module.exports = function Processor(command) { } } + if (videoFilters.length) { + args.push('-filter:v', videoFilters.join(',')); + } + // add size and output file if (this.options.video.size && !this.options.video.skip) { args.push('-s', this.options.video.size); diff --git a/test/args.test.js b/test/args.test.js index 48965d96..02e2928f 100644 --- a/test/args.test.js +++ b/test/args.test.js @@ -172,7 +172,7 @@ describe('Command', function() { if (err) { done(err); } else { - args.indexOf('-vf').should.above(-1); + args.indexOf('-filter:v').should.above(-1); args.indexOf('pad=1024:768:128:0:red').should.above(-1); done(); } @@ -187,7 +187,7 @@ describe('Command', function() { done(err); } else { args.indexOf('1280x540').should.above(-1); - args.indexOf('-vf').should.above(-1); + args.indexOf('-filter:v').should.above(-1); args.indexOf('pad=1280:720:0:90:black').should.above(-1); done(); } @@ -201,7 +201,7 @@ describe('Command', function() { if (err) { done(err); } else { - args.indexOf('-vf').should.above(-1); + args.indexOf('-filter:v').should.above(-1); args.indexOf('pad=640:480:0:60:black').should.above(-1); done(); } @@ -216,7 +216,7 @@ describe('Command', function() { if (err) { done(err); } else { - args.indexOf('-vf').should.above(-1); + args.indexOf('-filter:v').should.above(-1); args.indexOf('pad=640:480:0:60:black').should.above(-1); done(); } @@ -232,7 +232,7 @@ describe('Command', function() { if (err) { done(err); } else { - args.indexOf('-vf').should.above(-1); + args.indexOf('-filter:v').should.above(-1); args.indexOf('pad=640:480:0:60:black').should.above(-1); done(); } @@ -299,6 +299,19 @@ describe('Command', function() { }); }); + describe('withVideoFilter', function() { + it('should apply the video filter argument', function(done) { + new Ffmpeg({ source: this.testfile, nolog: true }) + .withVideoFilter('scale=123:456') + .withVideoFilter('pad=1230:4560:100:100:yellow') + .getArgs(function(args) { + args.indexOf('-filter:v').should.above(-1); + args.indexOf('scale=123:456,pad=1230:4560:100:100:yellow').should.above(-1); + done(); + }); + }); + }) + describe('withAudioBitrate', function() { it('should apply the audio bitrate argument', function(done) { new Ffmpeg({ source: this.testfile, nolog: true }) @@ -379,6 +392,19 @@ describe('Command', function() { }); }); + describe('withAudioFilter', function() { + it('should apply the audio filter argument', function(done) { + new Ffmpeg({ source: this.testfile, nolog: true }) + .withAudioFilter('silencedetect=n=-50dB:d=5') + .withAudioFilter('volume=0.5') + .getArgs(function(args) { + args.indexOf('-filter:a').should.above(-1); + args.indexOf('silencedetect=n=-50dB:d=5,volume=0.5').should.above(-1); + done(); + }); + }); + }) + describe('withAudioChannels', function() { it('should apply the audio channels argument', function(done) { new Ffmpeg({ source: this.testfile, nolog: true }) From 0f1c99d2e23154378f541361321fa3b886039337 Mon Sep 17 00:00:00 2001 From: Nicolas Joyard Date: Fri, 28 Feb 2014 18:26:08 +0100 Subject: [PATCH 8/8] Avoid using exec(), prefer spawn() --- lib/capabilities.js | 12 +- lib/fluent-ffmpeg.js | 4 +- lib/metadata.js | 247 +++++++++++++-------------- lib/processor.js | 366 +++++++++++++++++++++-------------------- test/processor.test.js | 4 +- 5 files changed, 311 insertions(+), 322 deletions(-) diff --git a/lib/capabilities.js b/lib/capabilities.js index b28f0707..19abaa91 100644 --- a/lib/capabilities.js +++ b/lib/capabilities.js @@ -1,5 +1,5 @@ -var exec = require('child_process').exec, - Registry = require('./registry'); +var exec = require('child_process').exec, + Registry = require('./registry'); var avCodecRegexp = /^ ([D ])([E ])([VAS])([S ])([D ])([T ]) ([^ ]+) +(.*)$/; var ffCodecRegexp = /^ ([D\.])([E\.])([VAS])([I\.])([L\.])([S\.]) ([^ ]+) +(.*)$/; @@ -17,9 +17,7 @@ exports = module.exports = function capabilities(command) { command.prototype.getAvailableCodecs = function(callback) { var codecs = Registry.instance.get('capabilityCodecs'); if (!codecs) { - var command = [this.ffmpegPath, '-codecs']; - - exec(command.join(' '), function(err, stdout, stderr) { + this._spawnFfmpeg(['-codecs'], { captureStdout: true }, function(err, stdout) { if (err) { return callback(err); } @@ -95,9 +93,7 @@ exports = module.exports = function capabilities(command) { command.prototype.getAvailableFormats = function(callback) { var formats = Registry.instance.get('capabilityFormats'); if (!formats) { - var command = [this.ffmpegPath, '-formats']; - - exec(command.join(' '), function(err, stdout, stderr) { + this._spawnFfmpeg(['-formats'], { captureStdout: true }, function (err, stdout) { if (err) { return callback(err); } diff --git a/lib/fluent-ffmpeg.js b/lib/fluent-ffmpeg.js index b611e29f..a4cc46fb 100644 --- a/lib/fluent-ffmpeg.js +++ b/lib/fluent-ffmpeg.js @@ -56,7 +56,7 @@ function FfmpegCommand(args) { _isStreamable: true, _updateFlvMetadata: false, _useConstantVideoBitrate: false, - _nice: { level: priority }, + _niceness: priority, keepPixelAspect: false, inputfile: srcfile, inputstream: srcstream, @@ -263,7 +263,7 @@ function FfmpegCommand(args) { level = 0; } - this.options._nice.level = level; + this.options._niceness = level; if (this.ffmpegProc) { this._renice(this.ffmpegProc, level); diff --git a/lib/metadata.js b/lib/metadata.js index ec368978..bacdde1c 100644 --- a/lib/metadata.js +++ b/lib/metadata.js @@ -1,28 +1,7 @@ -var exec = require('exec-queue'), - path = require('path'), +var path = require('path'), os = require('os').platform(); exports = module.exports = function Metadata(command) { - - command.prototype.escapedPath = function(path, enclose) { - if(/http/.exec(path)) { - path = path.replace(' ', '%20'); - } else { - if (os.match(/win(32|64)/)) { - // on windows, we have to fix up the filename - var parts = path.split(/\\/gi); - var fName = parts[parts.length - 1]; - parts[parts.length - 1] = fName.replace(/[\s\\:"'*?<>|\/]+/mig, '-'); - path = parts.join('\\'); - if (enclose && path[0] != '"' && path[path.length-1] != '"') - path = '"' + path + '"' - } else { - if (enclose && path[0] != '"' && path[path.length-1] != '"') - path = '"' + path + '"'; - } - } - return path; - }; // for internal use command.prototype.getMetadata = function(inputfile, callback) { this.inputfile = path.normalize(inputfile); @@ -32,7 +11,6 @@ exports = module.exports = function Metadata(command) { // for external use command.prototype.get = function(callback) { // import extensions for external call - this.inputfile = path.normalize(inputfile); this._loadDataInternal(callback); }; command.prototype.meta = function() { @@ -42,137 +20,138 @@ exports = module.exports = function Metadata(command) { else{ return {}; } - } + }; command.prototype._loadDataInternal = function(callback) { if (this.metaData){ return callback(this.metaData); } - - var inputfile = this.escapedPath(this.inputfile, true); var self = this; - exec(this.ffmpegPath + ' -i ' + inputfile, function(err, stdout, stderr) { - // parse data from stderr - - var none = [] - , aspect = /DAR ([0-9\:]+)/.exec(stderr) || none - , pixel = /[SP]AR ([0-9\:]+)/.exec(stderr) || none - , video_bitrate = /bitrate: ([0-9]+) kb\/s/.exec(stderr) || none - , fps = /(INAM|fps)\s+:\s(.+)/i.exec(stderr) || none - , container = /Input #0, ([a-zA-Z0-9]+),/.exec(stderr) || none - , title = /(INAM|title)\s+:\s(.+)/i.exec(stderr) || none - , artist = /artist\s+:\s(.+)/i.exec(stderr) || none - , album = /album\s+:\s(.+)/i.exec(stderr) || none - , track = /track\s+:\s(.+)/i.exec(stderr) || none - , date = /date\s+:\s(.+)/i.exec(stderr) || none - , video_stream = /Stream #([0-9\.]+)([a-z0-9\(\)\[\]]*)[:] Video/.exec(stderr) || none - , video_codec = /Video: ([\w]+)/.exec(stderr) || none - , duration = /Duration: (([0-9]+):([0-9]{2}):([0-9]{2}).([0-9]+))/.exec(stderr) || none - , resolution = /(([0-9]{2,5})x([0-9]{2,5}))/.exec(stderr) || none - , audio_bitrate = /Audio:(.)*, ([0-9]+) kb\/s/.exec(stderr) || none - , sample_rate = /([0-9]+) Hz/i.exec(stderr) || none - , audio_codec = /Audio: ([\w]+)/.exec(stderr) || none - , channels = /Audio: [\w]+, [0-9]+ Hz, ([a-z0-9:]+)[a-z0-9\/,]*/.exec(stderr) || none - , audio_stream = /Stream #([0-9\.]+)([a-z0-9\(\)\[\]]*)[:] Audio/.exec(stderr) || none - , is_synched = (/start: 0.000000/.exec(stderr) !== null) - , rotate = /rotate[\s]+:[\s]([\d]{2,3})/.exec(stderr) || none - , getVersion = /ffmpeg version (?:(\d+)\.)?(?:(\d+)\.)?(\*|\d+)/i.exec(stderr) - , major_brand = /major_brand\s+:\s([^\s]+)/.exec(stderr) || none - , ffmpegVersion = 0; - if (getVersion) { - ffmpegVersion = [ - getVersion[1]>=0 ? getVersion[1] : null, - getVersion[2]>=0 ? getVersion[2] : null, - getVersion[3]>=0 ? getVersion[3] : null - ].filter(function(val) { - return val !== null; - }).join('.'); - } + var process = this._spawnFfmpeg( + ['-i', this.inputfile], + { captureStderr: true }, + function(err, stdout, stderr) { + // parse data from stderr + var none = [] + , aspect = /DAR ([0-9\:]+)/.exec(stderr) || none + , pixel = /[SP]AR ([0-9\:]+)/.exec(stderr) || none + , video_bitrate = /bitrate: ([0-9]+) kb\/s/.exec(stderr) || none + , fps = /(INAM|fps)\s+:\s(.+)/i.exec(stderr) || none + , container = /Input #0, ([a-zA-Z0-9]+),/.exec(stderr) || none + , title = /(INAM|title)\s+:\s(.+)/i.exec(stderr) || none + , artist = /artist\s+:\s(.+)/i.exec(stderr) || none + , album = /album\s+:\s(.+)/i.exec(stderr) || none + , track = /track\s+:\s(.+)/i.exec(stderr) || none + , date = /date\s+:\s(.+)/i.exec(stderr) || none + , video_stream = /Stream #([0-9\.]+)([a-z0-9\(\)\[\]]*)[:] Video/.exec(stderr) || none + , video_codec = /Video: ([\w]+)/.exec(stderr) || none + , duration = /Duration: (([0-9]+):([0-9]{2}):([0-9]{2}).([0-9]+))/.exec(stderr) || none + , resolution = /(([0-9]{2,5})x([0-9]{2,5}))/.exec(stderr) || none + , audio_bitrate = /Audio:(.)*, ([0-9]+) kb\/s/.exec(stderr) || none + , sample_rate = /([0-9]+) Hz/i.exec(stderr) || none + , audio_codec = /Audio: ([\w]+)/.exec(stderr) || none + , channels = /Audio: [\w]+, [0-9]+ Hz, ([a-z0-9:]+)[a-z0-9\/,]*/.exec(stderr) || none + , audio_stream = /Stream #([0-9\.]+)([a-z0-9\(\)\[\]]*)[:] Audio/.exec(stderr) || none + , is_synched = (/start: 0.000000/.exec(stderr) !== null) + , rotate = /rotate[\s]+:[\s]([\d]{2,3})/.exec(stderr) || none + , getVersion = /ffmpeg version (?:(\d+)\.)?(?:(\d+)\.)?(\*|\d+)/i.exec(stderr) + , major_brand = /major_brand\s+:\s([^\s]+)/.exec(stderr) || none + , ffmpegVersion = 0; - // build return object - var _ref - , ret = { - ffmpegversion: ffmpegVersion - , title: title[2] || '' - , artist: artist[1] || '' - , album: album[1] || '' - , track: track[1] || '' - , date: date[1] || '' - , durationraw: duration[1] || '' - , durationsec: duration[1] ? self.ffmpegTimemarkToSeconds(duration[1]) : 0 - , synched: is_synched - , major_brand: major_brand[1] - , video: { - container: container[1] || '' - , bitrate: (video_bitrate.length > 1) ? parseInt(video_bitrate[1], 10) : 0 - , codec: video_codec[1] || '' - , resolution: { - w: resolution.length > 2 ? parseInt(resolution[2], 10) : 0 - , h: resolution.length > 3 ? parseInt(resolution[3], 10) : 0 - } - , resolutionSquare: {} - , rotate: rotate.length > 1 ? parseInt(rotate[1], 10) : 0 - , fps: fps.length > 1 ? parseFloat(fps[2]) : 0.0 - , stream: video_stream.length > 1 ? parseFloat(video_stream[1]) : 0.0 - } - , audio: { - codec: audio_codec[1] || '' - , bitrate: parseInt((_ref = audio_bitrate[audio_bitrate.length - 1]) != null ? _ref : 0, 10) - , sample_rate: sample_rate.length > 1 ? parseInt(sample_rate[1], 10) : 0 - , stream: audio_stream.length > 1 ? parseFloat(audio_stream[1]) : 0.0 + if (getVersion) { + ffmpegVersion = [ + getVersion[1]>=0 ? getVersion[1] : null, + getVersion[2]>=0 ? getVersion[2] : null, + getVersion[3]>=0 ? getVersion[3] : null + ].filter(function(val) { + return val !== null; + }).join('.'); } - }; - if (channels.length > 0) { - ret.audio.channels = {stereo:2, mono:1}[channels[1]] || 0; - } + // build return object + var _ref + , ret = { + ffmpegversion: ffmpegVersion + , title: title[2] || '' + , artist: artist[1] || '' + , album: album[1] || '' + , track: track[1] || '' + , date: date[1] || '' + , durationraw: duration[1] || '' + , durationsec: duration[1] ? self.ffmpegTimemarkToSeconds(duration[1]) : 0 + , synched: is_synched + , major_brand: major_brand[1] + , video: { + container: container[1] || '' + , bitrate: (video_bitrate.length > 1) ? parseInt(video_bitrate[1], 10) : 0 + , codec: video_codec[1] || '' + , resolution: { + w: resolution.length > 2 ? parseInt(resolution[2], 10) : 0 + , h: resolution.length > 3 ? parseInt(resolution[3], 10) : 0 + } + , resolutionSquare: {} + , rotate: rotate.length > 1 ? parseInt(rotate[1], 10) : 0 + , fps: fps.length > 1 ? parseFloat(fps[2]) : 0.0 + , stream: video_stream.length > 1 ? parseFloat(video_stream[1]) : 0.0 + } + , audio: { + codec: audio_codec[1] || '' + , bitrate: parseInt((_ref = audio_bitrate[audio_bitrate.length - 1]) != null ? _ref : 0, 10) + , sample_rate: sample_rate.length > 1 ? parseInt(sample_rate[1], 10) : 0 + , stream: audio_stream.length > 1 ? parseFloat(audio_stream[1]) : 0.0 + } + }; - // save aspect ratio for auto-padding - if (aspect.length > 0) { - ret.video.aspectString = aspect[1]; - var n = aspect[1].split(":"); - ret.video.aspect = parseFloat((parseInt(n[0], 10) / parseInt(n[1], 10))); - } else { - if(ret.video.resolution.w !== 0) { - var f = self.gcd(ret.video.resolution.w, ret.video.resolution.h); - ret.video.aspectString = ret.video.resolution.w/f + ':' + ret.video.resolution.h/f; - ret.video.aspect = parseFloat((ret.video.resolution.w / ret.video.resolution.h)); - } else { - ret.video.aspect = 0.0; + if (channels.length > 0) { + ret.audio.channels = {stereo:2, mono:1}[channels[1]] || 0; } - } - // save pixel ratio for output size calculation - if (pixel.length > 0) { - ret.video.pixelString = pixel[1]; - var n = pixel[1].split(":"); - ret.video.pixel = parseFloat((parseInt(n[0], 10) / parseInt(n[1], 10))); - } else { - if (ret.video.resolution.w !== 0) { - var f = self.gcd(ret.video.resolution.w, ret.video.resolution.h); - ret.video.pixelString = '1:1'; - ret.video.pixel = 1; + // save aspect ratio for auto-padding + if (aspect.length > 0) { + ret.video.aspectString = aspect[1]; + var n = aspect[1].split(":"); + ret.video.aspect = parseFloat((parseInt(n[0], 10) / parseInt(n[1], 10))); } else { - ret.video.pixel = 0.0; + if(ret.video.resolution.w !== 0) { + var f = self.gcd(ret.video.resolution.w, ret.video.resolution.h); + ret.video.aspectString = ret.video.resolution.w/f + ':' + ret.video.resolution.h/f; + ret.video.aspect = parseFloat((ret.video.resolution.w / ret.video.resolution.h)); + } else { + ret.video.aspect = 0.0; + } } - } - // correct video.resolution when pixel aspectratio is not 1 - if (ret.video.pixel !== 1 || ret.video.pixel !== 0) { - if( ret.video.pixel > 1 ) { - ret.video.resolutionSquare.w = parseInt(ret.video.resolution.w * ret.video.pixel, 10); - ret.video.resolutionSquare.h = ret.video.resolution.h; + // save pixel ratio for output size calculation + if (pixel.length > 0) { + ret.video.pixelString = pixel[1]; + var n = pixel[1].split(":"); + ret.video.pixel = parseFloat((parseInt(n[0], 10) / parseInt(n[1], 10))); } else { - ret.video.resolutionSquare.w = ret.video.resolution.w; - ret.video.resolutionSquare.h = parseInt(ret.video.resolution.h / ret.video.pixel, 10); + if (ret.video.resolution.w !== 0) { + var f = self.gcd(ret.video.resolution.w, ret.video.resolution.h); + ret.video.pixelString = '1:1'; + ret.video.pixel = 1; + } else { + ret.video.pixel = 0.0; + } } - } - self.metaData = ret; + // correct video.resolution when pixel aspectratio is not 1 + if (ret.video.pixel !== 1 || ret.video.pixel !== 0) { + if( ret.video.pixel > 1 ) { + ret.video.resolutionSquare.w = parseInt(ret.video.resolution.w * ret.video.pixel, 10); + ret.video.resolutionSquare.h = ret.video.resolution.h; + } else { + ret.video.resolutionSquare.w = ret.video.resolution.w; + ret.video.resolutionSquare.h = parseInt(ret.video.resolution.h / ret.video.pixel, 10); + } + } - callback(ret); - }); + self.metaData = ret; + callback(ret); + } + ); }; command.prototype.requiresMetaData = function() { diff --git a/lib/processor.js b/lib/processor.js index a5edd345..a36a7a24 100644 --- a/lib/processor.js +++ b/lib/processor.js @@ -6,6 +6,7 @@ var fs = require('fs'), spawn = require('child_process').spawn, Registry = require('./registry'), + exports = module.exports = function Processor(command) { command.prototype._codecDataAlreadySent = false; @@ -79,7 +80,35 @@ exports = module.exports = function Processor(command) { self.emit('start', 'ffmpeg ' + args.join(' ')); // Run ffmpeg - self.ffmpegProc = self._spawnProcess(args); + + var stdout = null; + var stderr = ''; + self.ffmpegProc = self._spawnFfmpeg(args, function(err) { + if (err) { + emitEnd(err, stdout, stderr); + } else { + if (options._updateFlvMetadata) { + spawn('flvtool2', ['-U', options.outputfile]) + .on('error', function(err) { + emitEnd(new Error('Error running flvtool2: ' + err.message)); + }) + .on('exit', function(code, signal) { + if (code !== 0 || signal) { + emitEnd( + new Error('flvtool2 ' + + (signal ? 'received signal ' + signal + : 'exited with code ' + code)) + ); + } else { + emitEnd(null, stdout, stderr); + } + }); + } else { + emitEnd(null, stdout, stderr); + } + } + }); + if (options.inputstream) { // Pipe input stream to ffmpeg stdin options.inputstream.on('error', function(err) { @@ -102,46 +131,6 @@ exports = module.exports = function Processor(command) { }, options.timeout * 1000); } - var stdout = null; - var stderr = ''; - - // Handle ffmpeg exit - self.ffmpegProc.on('exit', function(code, signal) { - if (processTimer) { - clearTimeout(processTimer); - } - - if (code !== 0 || signal) { - return emitEnd( - new Error('ffmpeg ' + - (signal ? 'received signal ' + signal - : 'exited with code ' + code)), - stdout, - stderr - ); - } - - if (options._updateFlvMetadata) { - spawn('flvtool2', ['-U', options.outputfile]) - .on('error', function(err) { - emitEnd(new Error('Error running flvtool2: ' + err.message)); - }) - .on('exit', function(code, signal) { - if (code !== 0 || signal) { - emitEnd( - new Error('flvtool2 ' + - (signal ? 'received signal ' + signal - : 'exited with code ' + code)) - ); - } else { - emitEnd(null, stdout, stderr); - } - }); - } else { - emitEnd(null, stdout, stderr); - } - }); - if (isStream) { // Pipe ffmpeg stdout to output stream self.ffmpegProc.stdout.pipe(target, pipeOptions); @@ -234,96 +223,71 @@ exports = module.exports = function Processor(command) { var self = this; var options = this.options; - var getExtension = function(filename) { - var filename = path.normalize(filename) || ''; - var ext = path.extname(filename).split('.'); + function getExtension(filename) { + var ext = path.extname(path.normalize(filename) || '').split('.'); return ext[ext.length - 1]; - }; + } // creates intermediate copies of each video. - var makeIntermediateFile = function(_mergeSource,_callback){ - var fname = _mergeSource+".temp.mpg"; - var command = [ - self.ffmpegPath, - [ - '-i', _mergeSource, - '-qscale:v',1, - fname - ] - ]; - - command[1] = self.options.additional.concat(command[1]).join(' '); + function makeIntermediateFile(_mergeSource,_callback) { + var fname = _mergeSource + '.temp.mpg'; + var args = self.options.additional.concat(['-i', _mergeSource, '-qscale:v', 1, fname]); - exec(command.join(' '),function(err, stdout, stderr) { + self._spawnFfmpeg(args, function(err) { _callback(err, fname); }); - }; + } // concat all created intermediate copies - var concatIntermediates = function(target,intermediatesList,_callback){ - var fname = path.normalize(target)+".temp.merged.mpg"; - // unescape paths - for(var i=0; i 0) { niceLevel = '+' + niceLevel; } - // renice the spawned process without waiting for callback + var self = this; - var command = [ - 'renice -n', niceLevel, - '-p', process.pid - ].join(' '); + var renice = spawn('renice', ['-n', niceLevel, '-p', process.pid]); + + renice.on('error', function(err) { + self.options.logger.warn('could not renice process ' + process.pid + ': ' + err.message); + }); - exec(command, function(err, stderr, stdout) { - if (!err) { + renice.on('exit', function(code, signal) { + if (code) { + self.options.logger.warn('could not renice process ' + process.pid + ': renice exited with ' + code); + } else if (signal) { + self.options.logger.warn('could not renice process ' + process.pid + ': renice was killed by signal ' + signal); + } else { self.options.logger.info('successfully reniced process ' + process.pid + ' to ' + niceLevel + ' niceness!'); } - }); + }) } }; }; diff --git a/test/processor.test.js b/test/processor.test.js index 38dc00f9..7e7550d4 100644 --- a/test/processor.test.js +++ b/test/processor.test.js @@ -33,7 +33,7 @@ describe('Processor', function() { if (!os.match(/win(32|64)/)) { it('should properly limit niceness', function() { new Ffmpeg({ source: this.testfile, nolog: true, timeout: 0.02 }) - .renice(100).options._nice.level.should.equal(0); + .renice(100).options._niceness.should.equal(0); }); it('should dynamically renice process', function(done) { @@ -242,7 +242,7 @@ describe('Processor', function() { ffmpegJob .on('error', function(err) { - err.message.indexOf('ffmpeg received signal SIGKILL').should.not.equal(-1); + err.message.indexOf('ffmpeg was killed with signal SIGKILL').should.not.equal(-1); fs.exists(testFile, function(exist) { if (exist) {