diff --git a/.gitignore b/.gitignore index 1cc5826d..e89b704f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ *.project +package-lock.json node_modules lib-cov *.swp .idea *.iml +.nyc_output/* diff --git a/Makefile b/Makefile index 26e1b143..a02ed1ce 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ REPORTER = spec MOCHA = node_modules/.bin/mocha +NYC = node_modules/.bin/nyc test: @NODE_ENV=test $(MOCHA) --require should --reporter $(REPORTER) @@ -7,14 +8,8 @@ test: test-colors: @NODE_ENV=test $(MOCHA) --require should --reporter $(REPORTER) --colors -test-cov: test/coverage.html - -test/coverage.html: lib-cov - @FLUENTFFMPEG_COV=1 NODE_ENV=test $(MOCHA) --require should --reporter html-cov > test/coverage.html - -lib-cov: - @rm -fr ./$@ - @jscoverage lib $@ +test-cov: + @FLUENTFFMPEG_COV=1 NODE_ENV=test ${NYC} $(MOCHA) --require should publish: @npm version patch -m "version bump" @@ -26,4 +21,4 @@ JSDOC_CONF = tools/jsdoc-conf.json doc: $(JSDOC) --configure $(JSDOC_CONF) -.PHONY: test test-cov lib-cov test-colors publish doc \ No newline at end of file +.PHONY: test test-cov test-colors publish doc diff --git a/index.js b/index.js index fb4805dd..b1f61b3d 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -module.exports = require(`./lib${process.env.FLUENTFFMPEG_COV ? '-cov' : ''}/fluent-ffmpeg`); +module.exports = require(`./lib/fluent-ffmpeg`); diff --git a/lib/capabilities.js b/lib/capabilities.js index 3722ff16..0673304d 100644 --- a/lib/capabilities.js +++ b/lib/capabilities.js @@ -1,27 +1,30 @@ /*jshint node:true*/ -'use strict'; +"use strict"; -var fs = require('fs'); -var path = require('path'); -var async = require('async'); -var utils = require('./utils'); +var fs = require("fs"); +var path = require("path"); +var async = require("async"); +var utils = require("./utils"); /* *! Capability helpers */ var avCodecRegexp = /^\s*([D ])([E ])([VAS])([S ])([D ])([T ]) ([^ ]+) +(.*)$/; -var ffCodecRegexp = /^\s*([D\.])([E\.])([VAS])([I\.])([L\.])([S\.]) ([^ ]+) +(.*)$/; +var ffCodecRegexp = + /^\s*([D\.])([E\.])([VAS])([I\.])([L\.])([S\.]) ([^ ]+) +(.*)$/; var ffEncodersRegexp = /\(encoders:([^\)]+)\)/; var ffDecodersRegexp = /\(decoders:([^\)]+)\)/; -var encodersRegexp = /^\s*([VAS\.])([F\.])([S\.])([X\.])([B\.])([D\.]) ([^ ]+) +(.*)$/; +var encodersRegexp = + /^\s*([VAS\.])([F\.])([S\.])([X\.])([B\.])([D\.]) ([^ ]+) +(.*)$/; var formatRegexp = /^\s*([D ])([E ]) ([^ ]+) +(.*)$/; var lineBreakRegexp = /\r\n|\r|\n/; -var filterRegexp = /^(?: [T\.][S\.][C\.] )?([^ ]+) +(AA?|VV?|\|)->(AA?|VV?|\|) +(.*)$/; +var filterRegexp = + /^(?: [T\.][S\.][C\.] )?([^ ]+) +(AA?|VV?|\|)->(AA?|VV?|\|) +(.*)$/; var cache = {}; -module.exports = function(proto) { +module.exports = function (proto) { /** * Manually define the ffmpeg binary full path. * @@ -30,7 +33,7 @@ module.exports = function(proto) { * @param {String} ffmpegPath The full path to the ffmpeg binary. * @return FfmpegCommand */ - proto.setFfmpegPath = function(ffmpegPath) { + proto.setFfmpegPath = function (ffmpegPath) { cache.ffmpegPath = ffmpegPath; return this; }; @@ -43,7 +46,7 @@ module.exports = function(proto) { * @param {String} ffprobePath The full path to the ffprobe binary. * @return FfmpegCommand */ - proto.setFfprobePath = function(ffprobePath) { + proto.setFfprobePath = function (ffprobePath) { cache.ffprobePath = ffprobePath; return this; }; @@ -56,7 +59,7 @@ module.exports = function(proto) { * @param {String} flvtool The full path to the flvtool2 or flvmeta binary. * @return FfmpegCommand */ - proto.setFlvtoolPath = function(flvtool) { + proto.setFlvtoolPath = function (flvtool) { cache.flvtoolPath = flvtool; return this; }; @@ -69,7 +72,7 @@ module.exports = function(proto) { * @method FfmpegCommand#_forgetPaths * @private */ - proto._forgetPaths = function() { + proto._forgetPaths = function () { delete cache.ffmpegPath; delete cache.ffprobePath; delete cache.flvtoolPath; @@ -85,47 +88,49 @@ module.exports = function(proto) { * @param {Function} callback callback with signature (err, path) * @private */ - proto._getFfmpegPath = function(callback) { - if ('ffmpegPath' in cache) { + proto._getFfmpegPath = function (callback) { + if ("ffmpegPath" in cache) { return callback(null, cache.ffmpegPath); } - async.waterfall([ - // Try FFMPEG_PATH - function(cb) { - if (process.env.FFMPEG_PATH) { - fs.exists(process.env.FFMPEG_PATH, function(exists) { - if (exists) { - cb(null, process.env.FFMPEG_PATH); - } else { - cb(null, ''); - } + async.waterfall( + [ + // Try FFMPEG_PATH + function (cb) { + if (process.env.FFMPEG_PATH) { + fs.exists(process.env.FFMPEG_PATH, function (exists) { + if (exists) { + cb(null, process.env.FFMPEG_PATH); + } else { + cb(null, ""); + } + }); + } else { + cb(null, ""); + } + }, + + // Search in the PATH + function (ffmpeg, cb) { + if (ffmpeg.length) { + return cb(null, ffmpeg); + } + + utils.which("ffmpeg", function (err, ffmpeg) { + cb(err, ffmpeg); }); + }, + ], + function (err, ffmpeg) { + if (err) { + callback(err); } else { - cb(null, ''); - } - }, - - // Search in the PATH - function(ffmpeg, cb) { - if (ffmpeg.length) { - return cb(null, ffmpeg); + callback(null, (cache.ffmpegPath = ffmpeg || "")); } - - utils.which('ffmpeg', function(err, ffmpeg) { - cb(err, ffmpeg); - }); } - ], function(err, ffmpeg) { - if (err) { - callback(err); - } else { - callback(null, cache.ffmpegPath = (ffmpeg || '')); - } - }); + ); }; - /** * Check for ffprobe availability * @@ -137,66 +142,68 @@ module.exports = function(proto) { * @param {Function} callback callback with signature (err, path) * @private */ - proto._getFfprobePath = function(callback) { + proto._getFfprobePath = function (callback) { var self = this; - if ('ffprobePath' in cache) { + if ("ffprobePath" in cache) { return callback(null, cache.ffprobePath); } - async.waterfall([ - // Try FFPROBE_PATH - function(cb) { - if (process.env.FFPROBE_PATH) { - fs.exists(process.env.FFPROBE_PATH, function(exists) { - cb(null, exists ? process.env.FFPROBE_PATH : ''); - }); - } else { - cb(null, ''); - } - }, - - // Search in the PATH - function(ffprobe, cb) { - if (ffprobe.length) { - return cb(null, ffprobe); - } + async.waterfall( + [ + // Try FFPROBE_PATH + function (cb) { + if (process.env.FFPROBE_PATH) { + fs.exists(process.env.FFPROBE_PATH, function (exists) { + cb(null, exists ? process.env.FFPROBE_PATH : ""); + }); + } else { + cb(null, ""); + } + }, - utils.which('ffprobe', function(err, ffprobe) { - cb(err, ffprobe); - }); - }, + // Search in the PATH + function (ffprobe, cb) { + if (ffprobe.length) { + return cb(null, ffprobe); + } - // Search in the same directory as ffmpeg - function(ffprobe, cb) { - if (ffprobe.length) { - return cb(null, ffprobe); - } + utils.which("ffprobe", function (err, ffprobe) { + cb(err, ffprobe); + }); + }, - self._getFfmpegPath(function(err, ffmpeg) { - if (err) { - cb(err); - } else if (ffmpeg.length) { - var name = utils.isWindows ? 'ffprobe.exe' : 'ffprobe'; - var ffprobe = path.join(path.dirname(ffmpeg), name); - fs.exists(ffprobe, function(exists) { - cb(null, exists ? ffprobe : ''); - }); - } else { - cb(null, ''); + // Search in the same directory as ffmpeg + function (ffprobe, cb) { + if (ffprobe.length) { + return cb(null, ffprobe); } - }); - } - ], function(err, ffprobe) { - if (err) { - callback(err); - } else { - callback(null, cache.ffprobePath = (ffprobe || '')); + + self._getFfmpegPath(function (err, ffmpeg) { + if (err) { + cb(err); + } else if (ffmpeg.length) { + var name = utils.isWindows ? "ffprobe.exe" : "ffprobe"; + var ffprobe = path.join(path.dirname(ffmpeg), name); + fs.exists(ffprobe, function (exists) { + cb(null, exists ? ffprobe : ""); + }); + } else { + cb(null, ""); + } + }); + }, + ], + function (err, ffprobe) { + if (err) { + callback(err); + } else { + callback(null, (cache.ffprobePath = ffprobe || "")); + } } - }); + ); }; - /** * Check for flvtool2/flvmeta availability * @@ -207,69 +214,71 @@ module.exports = function(proto) { * @param {Function} callback callback with signature (err, path) * @private */ - proto._getFlvtoolPath = function(callback) { - if ('flvtoolPath' in cache) { + proto._getFlvtoolPath = function (callback) { + if ("flvtoolPath" in cache) { return callback(null, cache.flvtoolPath); } - async.waterfall([ - // Try FLVMETA_PATH - function(cb) { - if (process.env.FLVMETA_PATH) { - fs.exists(process.env.FLVMETA_PATH, function(exists) { - cb(null, exists ? process.env.FLVMETA_PATH : ''); - }); - } else { - cb(null, ''); - } - }, + async.waterfall( + [ + // Try FLVMETA_PATH + function (cb) { + if (process.env.FLVMETA_PATH) { + fs.exists(process.env.FLVMETA_PATH, function (exists) { + cb(null, exists ? process.env.FLVMETA_PATH : ""); + }); + } else { + cb(null, ""); + } + }, - // Try FLVTOOL2_PATH - function(flvtool, cb) { - if (flvtool.length) { - return cb(null, flvtool); - } + // Try FLVTOOL2_PATH + function (flvtool, cb) { + if (flvtool.length) { + return cb(null, flvtool); + } - if (process.env.FLVTOOL2_PATH) { - fs.exists(process.env.FLVTOOL2_PATH, function(exists) { - cb(null, exists ? process.env.FLVTOOL2_PATH : ''); - }); - } else { - cb(null, ''); - } - }, + if (process.env.FLVTOOL2_PATH) { + fs.exists(process.env.FLVTOOL2_PATH, function (exists) { + cb(null, exists ? process.env.FLVTOOL2_PATH : ""); + }); + } else { + cb(null, ""); + } + }, - // Search for flvmeta in the PATH - function(flvtool, cb) { - if (flvtool.length) { - return cb(null, flvtool); - } + // Search for flvmeta in the PATH + function (flvtool, cb) { + if (flvtool.length) { + return cb(null, flvtool); + } - utils.which('flvmeta', function(err, flvmeta) { - cb(err, flvmeta); - }); - }, + utils.which("flvmeta", function (err, flvmeta) { + cb(err, flvmeta); + }); + }, - // Search for flvtool2 in the PATH - function(flvtool, cb) { - if (flvtool.length) { - return cb(null, flvtool); - } + // Search for flvtool2 in the PATH + function (flvtool, cb) { + if (flvtool.length) { + return cb(null, flvtool); + } - utils.which('flvtool2', function(err, flvtool2) { - cb(err, flvtool2); - }); - }, - ], function(err, flvtool) { - if (err) { - callback(err); - } else { - callback(null, cache.flvtoolPath = (flvtool || '')); + utils.which("flvtool2", function (err, flvtool2) { + cb(err, flvtool2); + }); + }, + ], + function (err, flvtool) { + if (err) { + callback(err); + } else { + callback(null, (cache.flvtoolPath = flvtool || "")); + } } - }); + ); }; - /** * A callback passed to {@link FfmpegCommand#availableFilters}. * @@ -293,39 +302,41 @@ module.exports = function(proto) { * * @param {FfmpegCommand~filterCallback} callback callback function */ - proto.availableFilters = - proto.getAvailableFilters = function(callback) { - if ('filters' in cache) { + proto.availableFilters = proto.getAvailableFilters = function (callback) { + if ("filters" in cache) { return callback(null, cache.filters); } - this._spawnFfmpeg(['-filters'], { captureStdout: true, stdoutLines: 0 }, function (err, stdoutRing) { - if (err) { - return callback(err); - } - - var stdout = stdoutRing.get(); - var lines = stdout.split('\n'); - var data = {}; - var types = { A: 'audio', V: 'video', '|': 'none' }; - - lines.forEach(function(line) { - var match = line.match(filterRegexp); - if (match) { - data[match[1]] = { - description: match[4], - input: types[match[2].charAt(0)], - multipleInputs: match[2].length > 1, - output: types[match[3].charAt(0)], - multipleOutputs: match[3].length > 1 - }; + this._spawnFfmpeg( + ["-filters"], + { captureStdout: true, stdoutLines: 0 }, + function (err, stdoutRing) { + if (err) { + return callback(err); } - }); - callback(null, cache.filters = data); - }); - }; + var stdout = stdoutRing.get(); + var lines = stdout.split("\n"); + var data = {}; + var types = { A: "audio", V: "video", "|": "none" }; + + lines.forEach(function (line) { + var match = line.match(filterRegexp); + if (match) { + data[match[1]] = { + description: match[4], + input: types[match[2].charAt(0)], + multipleInputs: match[2].length > 1, + output: types[match[3].charAt(0)], + multipleOutputs: match[3].length > 1, + }; + } + }); + callback(null, (cache.filters = data)); + } + ); + }; /** * A callback passed to {@link FfmpegCommand#availableCodecs}. @@ -349,83 +360,85 @@ module.exports = function(proto) { * * @param {FfmpegCommand~codecCallback} callback callback function */ - proto.availableCodecs = - proto.getAvailableCodecs = function(callback) { - if ('codecs' in cache) { + proto.availableCodecs = proto.getAvailableCodecs = function (callback) { + if ("codecs" in cache) { return callback(null, cache.codecs); } - this._spawnFfmpeg(['-codecs'], { captureStdout: true, stdoutLines: 0 }, function(err, stdoutRing) { - if (err) { - return callback(err); - } - - var stdout = stdoutRing.get(); - var lines = stdout.split(lineBreakRegexp); - var data = {}; - - lines.forEach(function(line) { - var match = line.match(avCodecRegexp); - if (match && match[7] !== '=') { - data[match[7]] = { - type: { 'V': 'video', 'A': 'audio', 'S': 'subtitle' }[match[3]], - description: match[8], - canDecode: match[1] === 'D', - canEncode: match[2] === 'E', - drawHorizBand: match[4] === 'S', - directRendering: match[5] === 'D', - weirdFrameTruncation: match[6] === 'T' - }; + this._spawnFfmpeg( + ["-codecs"], + { captureStdout: true, stdoutLines: 0 }, + function (err, stdoutRing) { + if (err) { + return callback(err); } - match = line.match(ffCodecRegexp); - if (match && match[7] !== '=') { - var codecData = data[match[7]] = { - type: { 'V': 'video', 'A': 'audio', 'S': 'subtitle' }[match[3]], - description: match[8], - canDecode: match[1] === 'D', - canEncode: match[2] === 'E', - intraFrameOnly: match[4] === 'I', - isLossy: match[5] === 'L', - isLossless: match[6] === 'S' - }; - - var encoders = codecData.description.match(ffEncodersRegexp); - encoders = encoders ? encoders[1].trim().split(' ') : []; - - var decoders = codecData.description.match(ffDecodersRegexp); - decoders = decoders ? decoders[1].trim().split(' ') : []; - - if (encoders.length || decoders.length) { - var coderData = {}; - utils.copy(codecData, coderData); - delete coderData.canEncode; - delete coderData.canDecode; - - encoders.forEach(function(name) { - data[name] = {}; - utils.copy(coderData, data[name]); - data[name].canEncode = true; + var stdout = stdoutRing.get(); + var lines = stdout.split(lineBreakRegexp); + var data = {}; + + lines.forEach(function (line) { + var match = line.match(avCodecRegexp); + if (match && match[7] !== "=") { + data[match[7]] = { + type: { V: "video", A: "audio", S: "subtitle" }[match[3]], + description: match[8], + canDecode: match[1] === "D", + canEncode: match[2] === "E", + drawHorizBand: match[4] === "S", + directRendering: match[5] === "D", + weirdFrameTruncation: match[6] === "T", + }; + } + + match = line.match(ffCodecRegexp); + if (match && match[7] !== "=") { + var codecData = (data[match[7]] = { + type: { V: "video", A: "audio", S: "subtitle" }[match[3]], + description: match[8], + canDecode: match[1] === "D", + canEncode: match[2] === "E", + intraFrameOnly: match[4] === "I", + isLossy: match[5] === "L", + isLossless: match[6] === "S", }); - decoders.forEach(function(name) { - if (name in data) { - data[name].canDecode = true; - } else { + var encoders = codecData.description.match(ffEncodersRegexp); + encoders = encoders ? encoders[1].trim().split(" ") : []; + + var decoders = codecData.description.match(ffDecodersRegexp); + decoders = decoders ? decoders[1].trim().split(" ") : []; + + if (encoders.length || decoders.length) { + var coderData = {}; + utils.copy(codecData, coderData); + delete coderData.canEncode; + delete coderData.canDecode; + + encoders.forEach(function (name) { data[name] = {}; utils.copy(coderData, data[name]); - data[name].canDecode = true; - } - }); + data[name].canEncode = true; + }); + + decoders.forEach(function (name) { + if (name in data) { + data[name].canDecode = true; + } else { + data[name] = {}; + utils.copy(coderData, data[name]); + data[name].canDecode = true; + } + }); + } } - } - }); + }); - callback(null, cache.codecs = data); - }); + callback(null, (cache.codecs = data)); + } + ); }; - /** * A callback passed to {@link FfmpegCommand#availableEncoders}. * @@ -451,40 +464,42 @@ module.exports = function(proto) { * * @param {FfmpegCommand~encodersCallback} callback callback function */ - proto.availableEncoders = - proto.getAvailableEncoders = function(callback) { - if ('encoders' in cache) { + proto.availableEncoders = proto.getAvailableEncoders = function (callback) { + if ("encoders" in cache) { return callback(null, cache.encoders); } - this._spawnFfmpeg(['-encoders'], { captureStdout: true, stdoutLines: 0 }, function(err, stdoutRing) { - if (err) { - return callback(err); - } - - var stdout = stdoutRing.get(); - var lines = stdout.split(lineBreakRegexp); - var data = {}; - - lines.forEach(function(line) { - var match = line.match(encodersRegexp); - if (match && match[7] !== '=') { - data[match[7]] = { - type: { 'V': 'video', 'A': 'audio', 'S': 'subtitle' }[match[1]], - description: match[8], - frameMT: match[2] === 'F', - sliceMT: match[3] === 'S', - experimental: match[4] === 'X', - drawHorizBand: match[5] === 'B', - directRendering: match[6] === 'D' - }; + this._spawnFfmpeg( + ["-encoders"], + { captureStdout: true, stdoutLines: 0 }, + function (err, stdoutRing) { + if (err) { + return callback(err); } - }); - callback(null, cache.encoders = data); - }); - }; + var stdout = stdoutRing.get(); + var lines = stdout.split(lineBreakRegexp); + var data = {}; + + lines.forEach(function (line) { + var match = line.match(encodersRegexp); + if (match && match[7] !== "=") { + data[match[7]] = { + type: { V: "video", A: "audio", S: "subtitle" }[match[1]], + description: match[8], + frameMT: match[2] === "F", + sliceMT: match[3] === "S", + experimental: match[4] === "X", + drawHorizBand: match[5] === "B", + directRendering: match[6] === "D", + }; + } + }); + callback(null, (cache.encoders = data)); + } + ); + }; /** * A callback passed to {@link FfmpegCommand#availableFormats}. @@ -507,50 +522,52 @@ module.exports = function(proto) { * * @param {FfmpegCommand~formatCallback} callback callback function */ - proto.availableFormats = - proto.getAvailableFormats = function(callback) { - if ('formats' in cache) { + proto.availableFormats = proto.getAvailableFormats = function (callback) { + if ("formats" in cache) { return callback(null, cache.formats); } // Run ffmpeg -formats - this._spawnFfmpeg(['-formats'], { captureStdout: true, stdoutLines: 0 }, function (err, stdoutRing) { - if (err) { - return callback(err); - } + this._spawnFfmpeg( + ["-formats"], + { captureStdout: true, stdoutLines: 0 }, + function (err, stdoutRing) { + if (err) { + return callback(err); + } - // Parse output - var stdout = stdoutRing.get(); - var lines = stdout.split(lineBreakRegexp); - var data = {}; - - lines.forEach(function(line) { - var match = line.match(formatRegexp); - if (match) { - match[3].split(',').forEach(function(format) { - if (!(format in data)) { - data[format] = { - description: match[4], - canDemux: false, - canMux: false - }; - } + // Parse output + var stdout = stdoutRing.get(); + var lines = stdout.split(lineBreakRegexp); + var data = {}; + + lines.forEach(function (line) { + var match = line.match(formatRegexp); + if (match) { + match[3].split(",").forEach(function (format) { + if (!(format in data)) { + data[format] = { + description: match[4], + canDemux: false, + canMux: false, + }; + } - if (match[1] === 'D') { - data[format].canDemux = true; - } - if (match[2] === 'E') { - data[format].canMux = true; - } - }); - } - }); + if (match[1] === "D") { + data[format].canDemux = true; + } + if (match[2] === "E") { + data[format].canMux = true; + } + }); + } + }); - callback(null, cache.formats = data); - }); + callback(null, (cache.formats = data)); + } + ); }; - /** * Check capabilities before executing a command * @@ -560,24 +577,24 @@ module.exports = function(proto) { * @param {Function} callback callback with signature (err) * @private */ - proto._checkCapabilities = function(callback) { + proto._checkCapabilities = function (callback) { var self = this; - async.waterfall([ - // Get available formats - function(cb) { - self.availableFormats(cb); - }, - - // Check whether specified formats are available - function(formats, cb) { - var unavailable; - - // Output format(s) - unavailable = self._outputs - .reduce(function(fmts, output) { - var format = output.options.find('-f', 1); + async.waterfall( + [ + // Get available formats + function (cb) { + self.availableFormats(cb); + }, + + // Check whether specified formats are available + function (formats, cb) { + var unavailable; + + // Output format(s) + unavailable = self._outputs.reduce(function (fmts, output) { + var format = output.options.find("-f", 1); if (format) { - if (!(format[0] in formats) || !(formats[format[0]].canMux)) { + if (!(format[0] in formats) || !formats[format[0]].canMux) { fmts.push(format); } } @@ -585,18 +602,25 @@ module.exports = function(proto) { return fmts; }, []); - if (unavailable.length === 1) { - return cb(new Error('Output format ' + unavailable[0] + ' is not available')); - } else if (unavailable.length > 1) { - return cb(new Error('Output formats ' + unavailable.join(', ') + ' are not available')); - } + if (unavailable.length === 1) { + return cb( + new Error("Output format " + unavailable[0] + " is not available") + ); + } else if (unavailable.length > 1) { + return cb( + new Error( + "Output formats " + + unavailable.join(", ") + + " are not available" + ) + ); + } - // Input format(s) - unavailable = self._inputs - .reduce(function(fmts, input) { - var format = input.options.find('-f', 1); + // Input format(s) + unavailable = self._inputs.reduce(function (fmts, input) { + var format = input.options.find("-f", 1); if (format) { - if (!(format[0] in formats) || !(formats[format[0]].canDemux)) { + if (!(format[0] in formats) || !formats[format[0]].canDemux) { fmts.push(format[0]); } } @@ -604,62 +628,88 @@ module.exports = function(proto) { return fmts; }, []); - if (unavailable.length === 1) { - return cb(new Error('Input format ' + unavailable[0] + ' is not available')); - } else if (unavailable.length > 1) { - return cb(new Error('Input formats ' + unavailable.join(', ') + ' are not available')); - } - - cb(); - }, - - // Get available codecs - function(cb) { - self.availableEncoders(cb); - }, - - // Check whether specified codecs are available and add strict experimental options if needed - function(encoders, cb) { - var unavailable; + if (unavailable.length === 1) { + return cb( + new Error("Input format " + unavailable[0] + " is not available") + ); + } else if (unavailable.length > 1) { + return cb( + new Error( + "Input formats " + unavailable.join(", ") + " are not available" + ) + ); + } - // Audio codec(s) - unavailable = self._outputs.reduce(function(cdcs, output) { - var acodec = output.audio.find('-acodec', 1); - if (acodec && acodec[0] !== 'copy') { - if (!(acodec[0] in encoders) || encoders[acodec[0]].type !== 'audio') { - cdcs.push(acodec[0]); + cb(); + }, + + // Get available codecs + function (cb) { + self.availableEncoders(cb); + }, + + // Check whether specified codecs are available and add strict experimental options if needed + function (encoders, cb) { + var unavailable; + + // Audio codec(s) + unavailable = self._outputs.reduce(function (cdcs, output) { + var acodec = output.audio.find("-acodec", 1); + if (acodec && acodec[0] !== "copy") { + if ( + !(acodec[0] in encoders) || + encoders[acodec[0]].type !== "audio" + ) { + cdcs.push(acodec[0]); + } } - } - return cdcs; - }, []); + return cdcs; + }, []); - if (unavailable.length === 1) { - return cb(new Error('Audio codec ' + unavailable[0] + ' is not available')); - } else if (unavailable.length > 1) { - return cb(new Error('Audio codecs ' + unavailable.join(', ') + ' are not available')); - } + if (unavailable.length === 1) { + return cb( + new Error("Audio codec " + unavailable[0] + " is not available") + ); + } else if (unavailable.length > 1) { + return cb( + new Error( + "Audio codecs " + unavailable.join(", ") + " are not available" + ) + ); + } - // Video codec(s) - unavailable = self._outputs.reduce(function(cdcs, output) { - var vcodec = output.video.find('-vcodec', 1); - if (vcodec && vcodec[0] !== 'copy') { - if (!(vcodec[0] in encoders) || encoders[vcodec[0]].type !== 'video') { - cdcs.push(vcodec[0]); + // Video codec(s) + unavailable = self._outputs.reduce(function (cdcs, output) { + var vcodec = output.video.find("-vcodec", 1); + if (vcodec && vcodec[0] !== "copy") { + if ( + !(vcodec[0] in encoders) || + encoders[vcodec[0]].type !== "video" + ) { + cdcs.push(vcodec[0]); + } } - } - return cdcs; - }, []); + return cdcs; + }, []); - if (unavailable.length === 1) { - return cb(new Error('Video codec ' + unavailable[0] + ' is not available')); - } else if (unavailable.length > 1) { - return cb(new Error('Video codecs ' + unavailable.join(', ') + ' are not available')); - } + if (unavailable.length === 1) { + return cb( + new Error("Video codec " + unavailable[0] + " is not available") + ); + } else if (unavailable.length > 1) { + return cb( + new Error( + "Video codecs " + unavailable.join(", ") + " are not available" + ) + ); + } - cb(); - } - ], callback); + cb(); + }, + ], + callback + ); }; }; diff --git a/package.json b/package.json index f889d9be..5de9b7e6 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,10 @@ }, "repository": "git://github.com/fluent-ffmpeg/node-fluent-ffmpeg.git", "devDependencies": { + "jsdoc": "latest", "mocha": "latest", - "should": "latest", - "jsdoc": "latest" + "nyc": "^15.1.0", + "should": "latest" }, "dependencies": { "async": ">=0.2.9", diff --git a/test/capabilities.test.js b/test/capabilities.test.js index b74427a9..55c83274 100644 --- a/test/capabilities.test.js +++ b/test/capabilities.test.js @@ -1,125 +1,130 @@ /*jshint node:true*/ /*global describe,it,beforeEach,afterEach,after*/ -'use strict'; +"use strict"; -var Ffmpeg = require('../index'), - path = require('path'), - assert = require('assert'), - testhelper = require('./helpers'), - async = require('async'); +var Ffmpeg = require("../index"), + path = require("path"), + assert = require("assert"), + testhelper = require("./helpers"), + async = require("async"); // delimiter fallback for node 0.8 -var PATH_DELIMITER = path.delimiter || (require('os').platform().match(/win(32|64)/) ? ';' : ':'); - - -describe('Capabilities', function() { - describe('ffmpeg capabilities', function() { - it('should enable querying for available codecs', function(done) { - new Ffmpeg({ source: '' }).getAvailableCodecs(function(err, codecs) { +var PATH_DELIMITER = + path.delimiter || + (require("os") + .platform() + .match(/win(32|64)/) + ? ";" + : ":"); + +describe("Capabilities", function () { + describe("ffmpeg capabilities", function () { + it("should enable querying for available codecs", function (done) { + new Ffmpeg({ source: "" }).getAvailableCodecs(function (err, codecs) { testhelper.logError(err); assert.ok(!err); - (typeof codecs).should.equal('object'); + (typeof codecs).should.equal("object"); Object.keys(codecs).length.should.not.equal(0); - ('pcm_s16le' in codecs).should.equal(true); - ('type' in codecs.pcm_s16le).should.equal(true); - (typeof codecs.pcm_s16le.type).should.equal('string'); - ('description' in codecs.pcm_s16le).should.equal(true); - (typeof codecs.pcm_s16le.description).should.equal('string'); - ('canEncode' in codecs.pcm_s16le).should.equal(true); - (typeof codecs.pcm_s16le.canEncode).should.equal('boolean'); - ('canDecode' in codecs.pcm_s16le).should.equal(true); - (typeof codecs.pcm_s16le.canDecode).should.equal('boolean'); + ("pcm_s16le" in codecs).should.equal(true); + ("type" in codecs.pcm_s16le).should.equal(true); + (typeof codecs.pcm_s16le.type).should.equal("string"); + ("description" in codecs.pcm_s16le).should.equal(true); + (typeof codecs.pcm_s16le.description).should.equal("string"); + ("canEncode" in codecs.pcm_s16le).should.equal(true); + (typeof codecs.pcm_s16le.canEncode).should.equal("boolean"); + ("canDecode" in codecs.pcm_s16le).should.equal(true); + (typeof codecs.pcm_s16le.canDecode).should.equal("boolean"); done(); }); }); - it('should enable querying for available encoders', function(done) { - new Ffmpeg({ source: '' }).getAvailableEncoders(function(err, encoders) { + it("should enable querying for available encoders", function (done) { + new Ffmpeg({ source: "" }).getAvailableEncoders(function (err, encoders) { testhelper.logError(err); assert.ok(!err); - (typeof encoders).should.equal('object'); + (typeof encoders).should.equal("object"); Object.keys(encoders).length.should.not.equal(0); - ('pcm_s16le' in encoders).should.equal(true); - ('type' in encoders.pcm_s16le).should.equal(true); - (typeof encoders.pcm_s16le.type).should.equal('string'); - ('description' in encoders.pcm_s16le).should.equal(true); - (typeof encoders.pcm_s16le.description).should.equal('string'); - ('experimental' in encoders.pcm_s16le).should.equal(true); - (typeof encoders.pcm_s16le.experimental).should.equal('boolean'); + ("pcm_s16le" in encoders).should.equal(true); + ("type" in encoders.pcm_s16le).should.equal(true); + (typeof encoders.pcm_s16le.type).should.equal("string"); + ("description" in encoders.pcm_s16le).should.equal(true); + (typeof encoders.pcm_s16le.description).should.equal("string"); + ("experimental" in encoders.pcm_s16le).should.equal(true); + (typeof encoders.pcm_s16le.experimental).should.equal("boolean"); done(); }); }); - it('should enable querying for available formats', function(done) { - new Ffmpeg({ source: '' }).getAvailableFormats(function(err, formats) { + it("should enable querying for available formats", function (done) { + new Ffmpeg({ source: "" }).getAvailableFormats(function (err, formats) { testhelper.logError(err); assert.ok(!err); - (typeof formats).should.equal('object'); + (typeof formats).should.equal("object"); Object.keys(formats).length.should.not.equal(0); - ('wav' in formats).should.equal(true); - ('description' in formats.wav).should.equal(true); - (typeof formats.wav.description).should.equal('string'); - ('canMux' in formats.wav).should.equal(true); - (typeof formats.wav.canMux).should.equal('boolean'); - ('canDemux' in formats.wav).should.equal(true); - (typeof formats.wav.canDemux).should.equal('boolean'); + ("wav" in formats).should.equal(true); + ("description" in formats.wav).should.equal(true); + (typeof formats.wav.description).should.equal("string"); + ("canMux" in formats.wav).should.equal(true); + (typeof formats.wav.canMux).should.equal("boolean"); + ("canDemux" in formats.wav).should.equal(true); + (typeof formats.wav.canDemux).should.equal("boolean"); done(); }); }); - it('should enable querying for available filters', function(done) { - new Ffmpeg({ source: '' }).getAvailableFilters(function(err, filters) { + it("should enable querying for available filters", function (done) { + new Ffmpeg({ source: "" }).getAvailableFilters(function (err, filters) { testhelper.logError(err); assert.ok(!err); - (typeof filters).should.equal('object'); + (typeof filters).should.equal("object"); Object.keys(filters).length.should.not.equal(0); - ('anull' in filters).should.equal(true); - ('description' in filters.anull).should.equal(true); - (typeof filters.anull.description).should.equal('string'); - ('input' in filters.anull).should.equal(true); - (typeof filters.anull.input).should.equal('string'); - ('output' in filters.anull).should.equal(true); - (typeof filters.anull.output).should.equal('string'); - ('multipleInputs' in filters.anull).should.equal(true); - (typeof filters.anull.multipleInputs).should.equal('boolean'); - ('multipleOutputs' in filters.anull).should.equal(true); - (typeof filters.anull.multipleOutputs).should.equal('boolean'); + ("anull" in filters).should.equal(true); + ("description" in filters.anull).should.equal(true); + (typeof filters.anull.description).should.equal("string"); + ("input" in filters.anull).should.equal(true); + (typeof filters.anull.input).should.equal("string"); + ("output" in filters.anull).should.equal(true); + (typeof filters.anull.output).should.equal("string"); + ("multipleInputs" in filters.anull).should.equal(true); + (typeof filters.anull.multipleInputs).should.equal("boolean"); + ("multipleOutputs" in filters.anull).should.equal(true); + (typeof filters.anull.multipleOutputs).should.equal("boolean"); done(); }); }); - it('should enable querying capabilities without instanciating a command', function(done) { - Ffmpeg.getAvailableCodecs(function(err, codecs) { + it("should enable querying capabilities without instanciating a command", function (done) { + Ffmpeg.getAvailableCodecs(function (err, codecs) { testhelper.logError(err); assert.ok(!err); - (typeof codecs).should.equal('object'); + (typeof codecs).should.equal("object"); Object.keys(codecs).length.should.not.equal(0); - Ffmpeg.getAvailableFilters(function(err, filters) { + Ffmpeg.getAvailableFilters(function (err, filters) { testhelper.logError(err); assert.ok(!err); - (typeof filters).should.equal('object'); + (typeof filters).should.equal("object"); Object.keys(filters).length.should.not.equal(0); - Ffmpeg.getAvailableFormats(function(err, formats) { + Ffmpeg.getAvailableFormats(function (err, formats) { testhelper.logError(err); assert.ok(!err); - (typeof formats).should.equal('object'); + (typeof formats).should.equal("object"); Object.keys(formats).length.should.not.equal(0); done(); @@ -128,129 +133,144 @@ describe('Capabilities', function() { }); }); - it('should enable checking command arguments for available codecs, formats and encoders', function(done) { - async.waterfall([ - // Check with everything available - function(cb) { - new Ffmpeg('/path/to/file.avi') - .fromFormat('avi') - .audioCodec('pcm_u16le') - .videoCodec('png') - .toFormat('mp4') - ._checkCapabilities(cb); - }, - - // Invalid input format - function(cb) { - new Ffmpeg('/path/to/file.avi') - .fromFormat('invalid-input-format') - .audioCodec('pcm_u16le') - .videoCodec('png') - .toFormat('mp4') - ._checkCapabilities(function(err) { - assert.ok(!!err); - err.message.should.match(/Input format invalid-input-format is not available/); - - cb(); - }); - }, - - // Invalid output format - function(cb) { - new Ffmpeg('/path/to/file.avi') - .fromFormat('avi') - .audioCodec('pcm_u16le') - .videoCodec('png') - .toFormat('invalid-output-format') - ._checkCapabilities(function(err) { - assert.ok(!!err); - err.message.should.match(/Output format invalid-output-format is not available/); - - cb(); - }); - }, - - // Invalid audio codec - function(cb) { - new Ffmpeg('/path/to/file.avi') - .fromFormat('avi') - .audioCodec('invalid-audio-codec') - .videoCodec('png') - .toFormat('mp4') - ._checkCapabilities(function(err) { - assert.ok(!!err); - err.message.should.match(/Audio codec invalid-audio-codec is not available/); - - cb(); - }); - }, - - // Invalid video codec - function(cb) { - new Ffmpeg('/path/to/file.avi') - .fromFormat('avi') - .audioCodec('pcm_u16le') - .videoCodec('invalid-video-codec') - .toFormat('mp4') - ._checkCapabilities(function(err) { - assert.ok(!!err); - err.message.should.match(/Video codec invalid-video-codec is not available/); - - cb(); - }); - }, - - // Invalid audio encoder - function(cb) { - new Ffmpeg('/path/to/file.avi') - .fromFormat('avi') - // Valid codec, but not a valid encoder for audio - .audioCodec('png') - .videoCodec('png') - .toFormat('mp4') - ._checkCapabilities(function(err) { - assert.ok(!!err); - err.message.should.match(/Audio codec png is not available/); - - cb(); - }); - }, - - // Invalid video encoder - function(cb) { - new Ffmpeg('/path/to/file.avi') - .fromFormat('avi') - .audioCodec('pcm_u16le') - // Valid codec, but not a valid encoder for video - .videoCodec('pcm_u16le') - .toFormat('mp4') - ._checkCapabilities(function(err) { - assert.ok(!!err); - err.message.should.match(/Video codec pcm_u16le is not available/); - - cb(); - }); - } - ], function(err) { - testhelper.logError(err); - assert.ok(!err); + it("should enable checking command arguments for available codecs, formats and encoders", function (done) { + async.waterfall( + [ + // Check with everything available + function (cb) { + new Ffmpeg("/path/to/file.avi") + .fromFormat("avi") + .audioCodec("pcm_u16le") + .videoCodec("png") + .toFormat("mp4") + ._checkCapabilities(cb); + }, + + // Invalid input format + function (cb) { + new Ffmpeg("/path/to/file.avi") + .fromFormat("invalid-input-format") + .audioCodec("pcm_u16le") + .videoCodec("png") + .toFormat("mp4") + ._checkCapabilities(function (err) { + assert.ok(!!err); + err.message.should.match( + /Input format invalid-input-format is not available/ + ); + + cb(); + }); + }, + + // Invalid output format + function (cb) { + new Ffmpeg("/path/to/file.avi") + .fromFormat("avi") + .audioCodec("pcm_u16le") + .videoCodec("png") + .toFormat("invalid-output-format") + ._checkCapabilities(function (err) { + assert.ok(!!err); + err.message.should.match( + /Output format invalid-output-format is not available/ + ); + + cb(); + }); + }, + + // Invalid audio codec + function (cb) { + new Ffmpeg("/path/to/file.avi") + .fromFormat("avi") + .audioCodec("invalid-audio-codec") + .videoCodec("png") + .toFormat("mp4") + ._checkCapabilities(function (err) { + assert.ok(!!err); + err.message.should.match( + /Audio codec invalid-audio-codec is not available/ + ); + + cb(); + }); + }, + + // Invalid video codec + function (cb) { + new Ffmpeg("/path/to/file.avi") + .fromFormat("avi") + .audioCodec("pcm_u16le") + .videoCodec("invalid-video-codec") + .toFormat("mp4") + ._checkCapabilities(function (err) { + assert.ok(!!err); + err.message.should.match( + /Video codec invalid-video-codec is not available/ + ); + + cb(); + }); + }, + + // Invalid audio encoder + function (cb) { + new Ffmpeg("/path/to/file.avi") + .fromFormat("avi") + // Valid codec, but not a valid encoder for audio + .audioCodec("png") + .videoCodec("png") + .toFormat("mp4") + ._checkCapabilities(function (err) { + assert.ok(!!err); + err.message.should.match(/Audio codec png is not available/); + + cb(); + }); + }, + + // Invalid video encoder + function (cb) { + new Ffmpeg("/path/to/file.avi") + .fromFormat("avi") + .audioCodec("pcm_u16le") + // Valid codec, but not a valid encoder for video + .videoCodec("pcm_u16le") + .toFormat("mp4") + ._checkCapabilities(function (err) { + assert.ok(!!err); + err.message.should.match( + /Video codec pcm_u16le is not available/ + ); + + cb(); + }); + }, + ], + function (err) { + testhelper.logError(err); + assert.ok(!err); - done(); - }); + done(); + } + ); }); - it('should check capabilities before running a command', function(done) { - new Ffmpeg('/path/to/file.avi') - .on('error', function(err) { - err.message.should.match(/Output format invalid-output-format is not available/); + it("should check capabilities before running a command", function (done) { + new Ffmpeg("/path/to/file.avi") + .on("error", function (err) { + err.message.should.match( + /Output format invalid-output-format is not available/ + ); done(); }) - .toFormat('invalid-output-format') - .saveToFile('/tmp/will-not-be-created.mp4'); + .toFormat("invalid-output-format") + .saveToFile("/tmp/will-not-be-created.mp4"); }); }); - describe('ffmpeg path', function() { + describe("ffmpeg path", function () { var FFMPEG_PATH; var ALT_FFMPEG_PATH; var skipAltTest = false; @@ -262,54 +282,54 @@ describe('Capabilities', function() { skipAltTest = true; } - beforeEach(function() { + beforeEach(function () { // Save environment before each test FFMPEG_PATH = process.env.FFMPEG_PATH; }); - afterEach(function() { + afterEach(function () { // Restore environment after each test process.env.FFMPEG_PATH = FFMPEG_PATH; }); - after(function() { + after(function () { // Forget paths after all tests - (new Ffmpeg())._forgetPaths(); + new Ffmpeg()._forgetPaths(); }); - it('should allow manual definition of ffmpeg binary path', function(done) { + it("should allow manual definition of ffmpeg binary path", function (done) { var ff = new Ffmpeg(); - ff.setFfmpegPath('/doom/di/dom'); - ff._getFfmpegPath(function(err, ffmpeg) { + ff.setFfmpegPath("/doom/di/dom"); + ff._getFfmpegPath(function (err, ffmpeg) { testhelper.logError(err); assert.ok(!err); - ffmpeg.should.equal('/doom/di/dom'); + ffmpeg.should.equal("/doom/di/dom"); done(); }); }); - it('should allow static manual definition of ffmpeg binary path', function(done) { + it("should allow static manual definition of ffmpeg binary path", function (done) { var ff = new Ffmpeg(); - Ffmpeg.setFfmpegPath('/doom/di/dom2'); - ff._getFfmpegPath(function(err, ffmpeg) { + Ffmpeg.setFfmpegPath("/doom/di/dom2"); + ff._getFfmpegPath(function (err, ffmpeg) { testhelper.logError(err); assert.ok(!err); - ffmpeg.should.equal('/doom/di/dom2'); + ffmpeg.should.equal("/doom/di/dom2"); done(); }); }); - it('should look for ffmpeg in the PATH if FFMPEG_PATH is not defined', function(done) { + it("should look for ffmpeg in the PATH if FFMPEG_PATH is not defined", function (done) { var ff = new Ffmpeg(); delete process.env.FFMPEG_PATH; ff._forgetPaths(); - ff._getFfmpegPath(function(err, ffmpeg) { + ff._getFfmpegPath(function (err, ffmpeg) { testhelper.logError(err); assert.ok(!err); @@ -322,28 +342,31 @@ describe('Capabilities', function() { }); }); - (skipAltTest ? it.skip : it)('should use FFMPEG_PATH if defined and valid', function(done) { - var ff = new Ffmpeg(); + (skipAltTest ? it.skip : it)( + "should use FFMPEG_PATH if defined and valid", + function (done) { + var ff = new Ffmpeg(); - process.env.FFMPEG_PATH = ALT_FFMPEG_PATH; + process.env.FFMPEG_PATH = ALT_FFMPEG_PATH; - ff._forgetPaths(); - ff._getFfmpegPath(function(err, ffmpeg) { - testhelper.logError(err); - assert.ok(!err); + ff._forgetPaths(); + ff._getFfmpegPath(function (err, ffmpeg) { + testhelper.logError(err); + assert.ok(!err); - ffmpeg.should.equal(ALT_FFMPEG_PATH); - done(); - }); - }); + ffmpeg.should.equal(ALT_FFMPEG_PATH); + done(); + }); + } + ); - it('should fall back to searching in the PATH if FFMPEG_PATH is invalid', function(done) { + it("should fall back to searching in the PATH if FFMPEG_PATH is invalid", function (done) { var ff = new Ffmpeg(); - process.env.FFMPEG_PATH = '/nope/not-here/nothing-to-see-here'; + process.env.FFMPEG_PATH = "/nope/not-here/nothing-to-see-here"; ff._forgetPaths(); - ff._getFfmpegPath(function(err, ffmpeg) { + ff._getFfmpegPath(function (err, ffmpeg) { testhelper.logError(err); assert.ok(!err); @@ -356,13 +379,13 @@ describe('Capabilities', function() { }); }); - it('should remember ffmpeg path', function(done) { + it("should remember ffmpeg path", function (done) { var ff = new Ffmpeg(); delete process.env.FFMPEG_PATH; ff._forgetPaths(); - ff._getFfmpegPath(function(err, ffmpeg) { + ff._getFfmpegPath(function (err, ffmpeg) { testhelper.logError(err); assert.ok(!err); @@ -372,7 +395,7 @@ describe('Capabilities', function() { // Just check that the callback is actually called synchronously // (which indicates no which call was made) var after = 0; - ff._getFfmpegPath(function(err, ffmpeg) { + ff._getFfmpegPath(function (err, ffmpeg) { testhelper.logError(err); assert.ok(!err); @@ -388,7 +411,7 @@ describe('Capabilities', function() { }); }); - describe('ffprobe path', function() { + describe("ffprobe path", function () { var FFPROBE_PATH; var ALT_FFPROBE_PATH; var skipAltTest = false; @@ -400,54 +423,54 @@ describe('Capabilities', function() { skipAltTest = true; } - beforeEach(function() { + beforeEach(function () { // Save environment before each test FFPROBE_PATH = process.env.FFPROBE_PATH; }); - afterEach(function() { + afterEach(function () { // Restore environment after each test process.env.FFPROBE_PATH = FFPROBE_PATH; }); - after(function() { + after(function () { // Forget paths after all tests - (new Ffmpeg())._forgetPaths(); + new Ffmpeg()._forgetPaths(); }); - it('should allow manual definition of ffprobe binary path', function(done) { + it("should allow manual definition of ffprobe binary path", function (done) { var ff = new Ffmpeg(); - ff.setFfprobePath('/doom/di/dom'); - ff._getFfprobePath(function(err, ffprobe) { + ff.setFfprobePath("/doom/di/dom"); + ff._getFfprobePath(function (err, ffprobe) { testhelper.logError(err); assert.ok(!err); - ffprobe.should.equal('/doom/di/dom'); + ffprobe.should.equal("/doom/di/dom"); done(); }); }); - it('should allow static manual definition of ffprobe binary path', function(done) { + it("should allow static manual definition of ffprobe binary path", function (done) { var ff = new Ffmpeg(); - Ffmpeg.setFfprobePath('/doom/di/dom2'); - ff._getFfprobePath(function(err, ffprobe) { + Ffmpeg.setFfprobePath("/doom/di/dom2"); + ff._getFfprobePath(function (err, ffprobe) { testhelper.logError(err); assert.ok(!err); - ffprobe.should.equal('/doom/di/dom2'); + ffprobe.should.equal("/doom/di/dom2"); done(); }); }); - it('should look for ffprobe in the PATH if FFPROBE_PATH is not defined', function(done) { + it("should look for ffprobe in the PATH if FFPROBE_PATH is not defined", function (done) { var ff = new Ffmpeg(); delete process.env.FFPROBE_PATH; ff._forgetPaths(); - ff._getFfprobePath(function(err, ffprobe) { + ff._getFfprobePath(function (err, ffprobe) { testhelper.logError(err); assert.ok(!err); @@ -460,28 +483,31 @@ describe('Capabilities', function() { }); }); - (skipAltTest ? it.skip : it)('should use FFPROBE_PATH if defined and valid', function(done) { - var ff = new Ffmpeg(); + (skipAltTest ? it.skip : it)( + "should use FFPROBE_PATH if defined and valid", + function (done) { + var ff = new Ffmpeg(); - process.env.FFPROBE_PATH = ALT_FFPROBE_PATH; + process.env.FFPROBE_PATH = ALT_FFPROBE_PATH; - ff._forgetPaths(); - ff._getFfprobePath(function(err, ffprobe) { - testhelper.logError(err); - assert.ok(!err); + ff._forgetPaths(); + ff._getFfprobePath(function (err, ffprobe) { + testhelper.logError(err); + assert.ok(!err); - ffprobe.should.equal(ALT_FFPROBE_PATH); - done(); - }); - }); + ffprobe.should.equal(ALT_FFPROBE_PATH); + done(); + }); + } + ); - it('should fall back to searching in the PATH if FFPROBE_PATH is invalid', function(done) { + it("should fall back to searching in the PATH if FFPROBE_PATH is invalid", function (done) { var ff = new Ffmpeg(); - process.env.FFPROBE_PATH = '/nope/not-here/nothing-to-see-here'; + process.env.FFPROBE_PATH = "/nope/not-here/nothing-to-see-here"; ff._forgetPaths(); - ff._getFfprobePath(function(err, ffprobe) { + ff._getFfprobePath(function (err, ffprobe) { testhelper.logError(err); assert.ok(!err); @@ -494,13 +520,13 @@ describe('Capabilities', function() { }); }); - it('should remember ffprobe path', function(done) { + it("should remember ffprobe path", function (done) { var ff = new Ffmpeg(); delete process.env.FFPROBE_PATH; ff._forgetPaths(); - ff._getFfprobePath(function(err, ffprobe) { + ff._getFfprobePath(function (err, ffprobe) { testhelper.logError(err); assert.ok(!err); @@ -510,7 +536,7 @@ describe('Capabilities', function() { // Just check that the callback is actually called synchronously // (which indicates no which call was made) var after = 0; - ff._getFfprobePath(function(err, ffprobe) { + ff._getFfprobePath(function (err, ffprobe) { testhelper.logError(err); assert.ok(!err); @@ -526,14 +552,14 @@ describe('Capabilities', function() { }); }); - describe('flvtool path', function() { + describe("flvtool path", function () { var FLVTOOL2_PATH; var ALT_FLVTOOL_PATH; var skipAltTest = false; var skipTest = false; // Skip test if we know travis failed to instal flvtool2 - if (process.env.FLVTOOL2_PRESENT === 'no') { + if (process.env.FLVTOOL2_PRESENT === "no") { skipTest = true; } @@ -544,107 +570,122 @@ describe('Capabilities', function() { skipAltTest = true; } - beforeEach(function() { + beforeEach(function () { // Save environment before each test FLVTOOL2_PATH = process.env.FLVTOOL2_PATH; }); - afterEach(function() { + afterEach(function () { // Restore environment after each test process.env.FLVTOOL2_PATH = FLVTOOL2_PATH; }); - after(function() { + after(function () { // Forget paths after all tests - (new Ffmpeg())._forgetPaths(); + new Ffmpeg()._forgetPaths(); }); - (skipTest ? it.skip : it)('should allow manual definition of fflvtool binary path', function(done) { - var ff = new Ffmpeg(); + (skipTest ? it.skip : it)( + "should allow manual definition of fflvtool binary path", + function (done) { + var ff = new Ffmpeg(); - ff.setFlvtoolPath('/doom/di/dom'); - ff._getFlvtoolPath(function(err, fflvtool) { - testhelper.logError(err); - assert.ok(!err); + ff.setFlvtoolPath("/doom/di/dom"); + ff._getFlvtoolPath(function (err, fflvtool) { + testhelper.logError(err); + assert.ok(!err); - fflvtool.should.equal('/doom/di/dom'); - done(); - }); - }); + fflvtool.should.equal("/doom/di/dom"); + done(); + }); + } + ); - (skipTest ? it.skip : it)('should allow static manual definition of fflvtool binary path', function(done) { - var ff = new Ffmpeg(); + (skipTest ? it.skip : it)( + "should allow static manual definition of fflvtool binary path", + function (done) { + var ff = new Ffmpeg(); - Ffmpeg.setFlvtoolPath('/doom/di/dom2'); - ff._getFlvtoolPath(function(err, fflvtool) { - testhelper.logError(err); - assert.ok(!err); + Ffmpeg.setFlvtoolPath("/doom/di/dom2"); + ff._getFlvtoolPath(function (err, fflvtool) { + testhelper.logError(err); + assert.ok(!err); - fflvtool.should.equal('/doom/di/dom2'); - done(); - }); - }); + fflvtool.should.equal("/doom/di/dom2"); + done(); + }); + } + ); - (skipTest ? it.skip : it)('should look for fflvtool in the PATH if FLVTOOL2_PATH is not defined', function(done) { - var ff = new Ffmpeg(); + (skipTest ? it.skip : it)( + "should look for fflvtool in the PATH if FLVTOOL2_PATH is not defined", + function (done) { + var ff = new Ffmpeg(); - delete process.env.FLVTOOL2_PATH; + delete process.env.FLVTOOL2_PATH; - ff._forgetPaths(); - ff._getFlvtoolPath(function(err, fflvtool) { - testhelper.logError(err); - assert.ok(!err); + ff._forgetPaths(); + ff._getFlvtoolPath(function (err, fflvtool) { + testhelper.logError(err); + assert.ok(!err); - fflvtool.should.instanceOf(String); - fflvtool.length.should.above(0); + fflvtool.should.instanceOf(String); + fflvtool.length.should.above(0); - var paths = process.env.PATH.split(PATH_DELIMITER); - paths.indexOf(path.dirname(fflvtool)).should.above(-1); - done(); - }); - }); + var paths = process.env.PATH.split(PATH_DELIMITER); + paths.indexOf(path.dirname(fflvtool)).should.above(-1); + done(); + }); + } + ); - (skipTest || skipAltTest ? it.skip : it)('should use FLVTOOL2_PATH if defined and valid', function(done) { - var ff = new Ffmpeg(); + (skipTest || skipAltTest ? it.skip : it)( + "should use FLVTOOL2_PATH if defined and valid", + function (done) { + var ff = new Ffmpeg(); - process.env.FLVTOOL2_PATH = ALT_FLVTOOL_PATH; + process.env.FLVTOOL2_PATH = ALT_FLVTOOL_PATH; - ff._forgetPaths(); - ff._getFlvtoolPath(function(err, fflvtool) { - testhelper.logError(err); - assert.ok(!err); + ff._forgetPaths(); + ff._getFlvtoolPath(function (err, fflvtool) { + testhelper.logError(err); + assert.ok(!err); - fflvtool.should.equal(ALT_FLVTOOL_PATH); - done(); - }); - }); + fflvtool.should.equal(ALT_FLVTOOL_PATH); + done(); + }); + } + ); - (skipTest ? it.skip : it)('should fall back to searching in the PATH if FLVTOOL2_PATH is invalid', function(done) { - var ff = new Ffmpeg(); + (skipTest ? it.skip : it)( + "should fall back to searching in the PATH if FLVTOOL2_PATH is invalid", + function (done) { + var ff = new Ffmpeg(); - process.env.FLVTOOL2_PATH = '/nope/not-here/nothing-to-see-here'; + process.env.FLVTOOL2_PATH = "/nope/not-here/nothing-to-see-here"; - ff._forgetPaths(); - ff._getFlvtoolPath(function(err, fflvtool) { - testhelper.logError(err); - assert.ok(!err); + ff._forgetPaths(); + ff._getFlvtoolPath(function (err, fflvtool) { + testhelper.logError(err); + assert.ok(!err); - fflvtool.should.instanceOf(String); - fflvtool.length.should.above(0); + fflvtool.should.instanceOf(String); + fflvtool.length.should.above(0); - var paths = process.env.PATH.split(PATH_DELIMITER); - paths.indexOf(path.dirname(fflvtool)).should.above(-1); - done(); - }); - }); + var paths = process.env.PATH.split(PATH_DELIMITER); + paths.indexOf(path.dirname(fflvtool)).should.above(-1); + done(); + }); + } + ); - (skipTest ? it.skip : it)('should remember fflvtool path', function(done) { + (skipTest ? it.skip : it)("should remember fflvtool path", function (done) { var ff = new Ffmpeg(); delete process.env.FLVTOOL2_PATH; ff._forgetPaths(); - ff._getFlvtoolPath(function(err, fflvtool) { + ff._getFlvtoolPath(function (err, fflvtool) { testhelper.logError(err); assert.ok(!err); @@ -654,7 +695,7 @@ describe('Capabilities', function() { // Just check that the callback is actually called synchronously // (which indicates no which call was made) var after = 0; - ff._getFlvtoolPath(function(err, fflvtool) { + ff._getFlvtoolPath(function (err, fflvtool) { testhelper.logError(err); assert.ok(!err); @@ -669,5 +710,4 @@ describe('Capabilities', function() { }); }); }); - }); diff --git a/test/coverage.html b/test/coverage.html index 58996049..e69de29b 100644 --- a/test/coverage.html +++ b/test/coverage.html @@ -1,355 +0,0 @@ -make[1]: entrant dans le répertoire « /home/niko/dev/forks/node-fluent-ffmpeg » -Coverage -

Coverage

89%
945
848
97

lib/capabilities.js

86%
179
154
25
LineHitsSource
1/*jshint node:true*/
2'use strict';
3
41var fs = require('fs');
51var path = require('path');
61var async = require('async');
71var utils = require('./utils');
8
9/*
10 *! Capability helpers
11 */
12
131var avCodecRegexp = /^\s*([D ])([E ])([VAS])([S ])([D ])([T ]) ([^ ]+) +(.*)$/;
141var ffCodecRegexp = /^\s*([D\.])([E\.])([VAS])([I\.])([L\.])([S\.]) ([^ ]+) +(.*)$/;
151var ffEncodersRegexp = /\(encoders:([^\)]+)\)/;
161var ffDecodersRegexp = /\(decoders:([^\)]+)\)/;
171var formatRegexp = /^\s*([D ])([E ]) ([^ ]+) +(.*)$/;
181var lineBreakRegexp = /\r\n|\r|\n/;
191var filterRegexp = /^(?: [T\.][S\.][C\.] )?([^ ]+) +(AA?|VV?|\|)->(AA?|VV?|\|) +(.*)$/;
20
211var cache = {};
22
23function copy(src, dest) {
24125 Object.keys(src).forEach(function(k) {
25741 dest[k] = src[k];
26 });
27}
28
291module.exports = function(proto) {
30 /**
31 * Forget executable paths
32 *
33 * (only used for testing purposes)
34 *
35 * @method FfmpegCommand#_forgetPaths
36 * @private
37 */
381 proto._forgetPaths = function() {
398 delete cache.ffmpegPath;
408 delete cache.ffprobePath;
418 delete cache.flvtoolPath;
42 };
43
44
45 /**
46 * Check for ffmpeg availability
47 *
48 * If the FFMPEG_PATH environment variable is set, try to use it.
49 * If it is unset or incorrect, try to find ffmpeg in the PATH instead.
50 *
51 * @method FfmpegCommand#_getFfmpegPath
52 * @param {Function} callback callback with signature (err, path)
53 * @private
54 */
551 proto._getFfmpegPath = function(callback) {
5635 if ('ffmpegPath' in cache) {
5729 return callback(null, cache.ffmpegPath);
58 }
59
606 async.waterfall([
61 // Try FFMPEG_PATH
62 function(cb) {
636 if (process.env.FFMPEG_PATH) {
643 fs.exists(process.env.FFMPEG_PATH, function(exists) {
653 if (exists) {
661 cb(null, process.env.FFMPEG_PATH);
67 } else {
682 cb(null, '');
69 }
70 });
71 } else {
723 cb(null, '');
73 }
74 },
75
76 // Search in the PATH
77 function(ffmpeg, cb) {
786 if (ffmpeg.length) {
791 return cb(null, ffmpeg);
80 }
81
825 utils.which('ffmpeg', function(err, ffmpeg) {
835 cb(err, ffmpeg);
84 });
85 }
86 ], function(err, ffmpeg) {
876 if (err) {
880 callback(err);
89 } else {
906 callback(null, cache.ffmpegPath = (ffmpeg || ''));
91 }
92 });
93 };
94
95
96 /**
97 * Check for ffprobe availability
98 *
99 * If the FFPROBE_PATH environment variable is set, try to use it.
100 * If it is unset or incorrect, try to find ffprobe in the PATH instead.
101 * If this still fails, try to find ffprobe in the same directory as ffmpeg.
102 *
103 * @method FfmpegCommand#_getFfprobePath
104 * @param {Function} callback callback with signature (err, path)
105 * @private
106 */
1071 proto._getFfprobePath = function(callback) {
10813 if ('ffprobePath' in cache) {
1099 return callback(null, cache.ffprobePath);
110 }
111
1124 var self = this;
1134 async.waterfall([
114 // Try FFPROBE_PATH
115 function(cb) {
1164 if (process.env.FFPROBE_PATH) {
1172 fs.exists(process.env.FFPROBE_PATH, function(exists) {
1182 cb(null, exists ? process.env.FFPROBE_PATH : '');
119 });
120 } else {
1212 cb(null, '');
122 }
123 },
124
125 // Search in the PATH
126 function(ffprobe, cb) {
1274 if (ffprobe.length) {
1281 return cb(null, ffprobe);
129 }
130
1313 utils.which('ffprobe', function(err, ffprobe) {
1323 cb(err, ffprobe);
133 });
134 },
135
136 // Search in the same directory as ffmpeg
137 function(ffprobe, cb) {
1384 if (ffprobe.length) {
1394 return cb(null, ffprobe);
140 }
141
1420 self._getFfmpegPath(function(err, ffmpeg) {
1430 if (err) {
1440 cb(err);
1450 } else if (ffmpeg.length) {
1460 var name = utils.isWindows ? 'ffprobe.exe' : 'ffprobe';
1470 var ffprobe = path.join(path.dirname(ffmpeg), name);
1480 fs.exists(ffprobe, function(exists) {
1490 cb(null, exists ? ffprobe : '');
150 });
151 } else {
1520 cb(null, '');
153 }
154 });
155 }
156 ], function(err, ffprobe) {
1574 if (err) {
1580 callback(err);
159 } else {
1604 callback(null, cache.ffprobePath = (ffprobe || ''));
161 }
162 });
163 };
164
165
166 /**
167 * Check for flvtool2/flvmeta availability
168 *
169 * If the FLVTOOL2_PATH or FLVMETA_PATH environment variable are set, try to use them.
170 * If both are either unset or incorrect, try to find flvtool2 or flvmeta in the PATH instead.
171 *
172 * @method FfmpegCommand#_getFlvtoolPath
173 * @param {Function} callback callback with signature (err, path)
174 * @private
175 */
1761 proto._getFlvtoolPath = function(callback) {
17729 if ('flvtoolPath' in cache) {
17828 return callback(null, cache.flvtoolPath);
179 }
180
1811 async.waterfall([
182 // Try FLVMETA_PATH
183 function(cb) {
1841 if (process.env.FLVMETA_PATH) {
1850 fs.exists(process.env.FLVMETA_PATH, function(exists) {
1860 cb(null, exists ? process.env.FLVMETA_PATH : '');
187 });
188 } else {
1891 cb(null, '');
190 }
191 },
192
193 // Try FLVTOOL2_PATH
194 function(flvtool, cb) {
1951 if (flvtool.length) {
1960 return cb(null, flvtool);
197 }
198
1991 if (process.env.FLVTOOL2_PATH) {
2000 fs.exists(process.env.FLVTOOL2_PATH, function(exists) {
2010 cb(null, exists ? process.env.FLVTOOL2_PATH : '');
202 });
203 } else {
2041 cb(null, '');
205 }
206 },
207
208 // Search for flvmeta in the PATH
209 function(flvtool, cb) {
2101 if (flvtool.length) {
2110 return cb(null, flvtool);
212 }
213
2141 utils.which('flvmeta', function(err, flvmeta) {
2151 cb(err, flvmeta);
216 });
217 },
218
219 // Search for flvtool2 in the PATH
220 function(flvtool, cb) {
2211 if (flvtool.length) {
2221 return cb(null, flvtool);
223 }
224
2250 utils.which('flvtool2', function(err, flvtool2) {
2260 cb(err, flvtool2);
227 });
228 },
229 ], function(err, flvtool) {
2301 if (err) {
2310 callback(err);
232 } else {
2331 callback(null, cache.flvtoolPath = (flvtool || ''));
234 }
235 });
236 };
237
238
239 /**
240 * Query ffmpeg for available filters
241 *
242 * Calls 'callback' with a filters object as its second argument. This
243 * object has keys for every available filter, and values are object
244 * with filter data:
245 * - 'description': filter description
246 * - 'input': input type ('audio', 'video' or 'none')
247 * - 'multipleInputs': bool, whether the filter supports multiple inputs
248 * - 'output': output type ('audio', 'video' or 'none')
249 * - 'multipleOutputs': bool, whether the filter supports multiple outputs
250 *
251 * @method FfmpegCommand#availableFilters
252 * @category Capabilities
253 * @aliases getAvailableFilters
254 *
255 * @param {Function} callback callback with signature (err, filters)
256 */
2571 proto.availableFilters =
258 proto.getAvailableFilters = function(callback) {
2592 if ('filters' in cache) {
2601 return callback(null, cache.filters);
261 }
262
2631 this._spawnFfmpeg(['-filters'], { captureStdout: true }, function (err, stdout) {
2641 if (err) {
2650 return callback(err);
266 }
267
2681 var lines = stdout.split('\n');
2691 var data = {};
2701 var types = { A: 'audio', V: 'video', '|': 'none' };
271
2721 lines.forEach(function(line) {
273137 var match = line.match(filterRegexp);
274137 if (match) {
275135 data[match[1]] = {
276 description: match[4],
277 input: types[match[2].charAt(0)],
278 multipleInputs: match[2].length > 1,
279 output: types[match[3].charAt(0)],
280 multipleOutputs: match[3].length > 1
281 };
282 }
283 });
284
2851 callback(null, cache.filters = data);
286 });
287 };
288
289
290 /**
291 * Query ffmpeg for available codecs
292 *
293 * Calls 'callback' with a codecs object as its second argument. This
294 * object has keys for every available codec, and values are object
295 * with codec data:
296 * - 'description': codec description
297 * - 'canEncode': bool, whether the codec can encode streams
298 * - 'canDecode': bool, whether the codec can decode streams
299 *
300 * Depending on the ffmpeg version, more keys can be available.
301 *
302 * @method FfmpegCommand#availableCodecs
303 * @category Capabilities
304 * @aliases getAvailableCodecs
305 *
306 * @param {Function} callback callback with signature (err, codecs)
307 */
3081 proto.availableCodecs =
309 proto.getAvailableCodecs = function(callback) {
31025 if ('codecs' in cache) {
31124 return callback(null, cache.codecs);
312 }
313
3141 this._spawnFfmpeg(['-codecs'], { captureStdout: true }, function(err, stdout) {
3151 if (err) {
3160 return callback(err);
317 }
318
3191 var lines = stdout.split(lineBreakRegexp);
3201 var data = {};
321
3221 lines.forEach(function(line) {
323369 var match = line.match(avCodecRegexp);
324369 if (match && match[7] !== '=') {
3250 data[match[7]] = {
326 type: { 'V': 'video', 'A': 'audio', 'S': 'subtitle' }[match[3]],
327 description: match[8],
328 canDecode: match[1] === 'D',
329 canEncode: match[2] === 'E',
330 drawHorizBand: match[4] === 'S',
331 directRendering: match[5] === 'D',
332 weirdFrameTruncation: match[6] === 'T'
333 };
334 }
335
336369 match = line.match(ffCodecRegexp);
337369 if (match && match[7] !== '=') {
338357 var codecData = data[match[7]] = {
339 type: { 'V': 'video', 'A': 'audio', 'S': 'subtitle' }[match[3]],
340 description: match[8],
341 canDecode: match[1] === 'D',
342 canEncode: match[2] === 'E',
343 intraFrameOnly: match[4] === 'I',
344 isLossy: match[5] === 'L',
345 isLossless: match[6] === 'S'
346 };
347
348357 var encoders = codecData.description.match(ffEncodersRegexp);
349357 encoders = encoders ? encoders[1].trim().split(' ') : [];
350
351357 var decoders = codecData.description.match(ffDecodersRegexp);
352357 decoders = decoders ? decoders[1].trim().split(' ') : [];
353
354357 if (encoders.length || decoders.length) {
35558 var coderData = {};
35658 copy(codecData, coderData);
35758 delete coderData.canEncode;
35858 delete coderData.canDecode;
359
36058 encoders.forEach(function(name) {
36132 data[name] = {};
36232 copy(coderData, data[name]);
36332 data[name].canEncode = true;
364 });
365
36658 decoders.forEach(function(name) {
36773 if (name in data) {
36838 data[name].canDecode = true;
369 } else {
37035 data[name] = {};
37135 copy(coderData, data[name]);
37235 data[name].canDecode = true;
373 }
374 });
375 }
376 }
377 });
378
3791 callback(null, cache.codecs = data);
380 });
381 };
382
383
384 /**
385 * Query ffmpeg for available formats
386 *
387 * Calls 'callback' with a formats object as its second argument. This
388 * object has keys for every available format, and values are object
389 * with format data:
390 * - 'description': format description
391 * - 'canMux': bool, whether the format can mux streams into an output file
392 * - 'canDemux': bool, whether the format can demux streams from an input file
393 *
394 * @method FfmpegCommand#availableFormats
395 * @category Capabilities
396 * @aliases getAvailableFormats
397 *
398 * @param {Function} callback callback with signature (err, formats)
399 */
4001 proto.availableFormats =
401 proto.getAvailableFormats = function(callback) {
40228 if ('formats' in cache) {
40327 return callback(null, cache.formats);
404 }
405
406 // Run ffmpeg -formats
4071 this._spawnFfmpeg(['-formats'], { captureStdout: true }, function (err, stdout) {
4081 if (err) {
4090 return callback(err);
410 }
411
412 // Parse output
4131 var lines = stdout.split(lineBreakRegexp);
4141 var data = {};
415
4161 lines.forEach(function(line) {
417252 var match = line.match(formatRegexp);
418252 if (match) {
419247 data[match[3]] = {
420 description: match[4],
421 canDemux: match[1] === 'D',
422 canMux: match[2] === 'E'
423 };
424 }
425 });
426
4271 callback(null, cache.formats = data);
428 });
429 };
430
431
432 /**
433 * Check capabilities before executing a command
434 *
435 * Checks whether all used codecs and formats are indeed available
436 *
437 * @method FfmpegCommand#_checkCapabilities
438 * @param {Function} callback callback with signature (err)
439 * @private
440 */
4411 proto._checkCapabilities = function(callback) {
44226 var self = this;
44326 async.waterfall([
444 // Get available formats
445 function(cb) {
44626 self.availableFormats(cb);
447 },
448
449 // Check whether specified formats are available
450 function(formats, cb) {
451 // Output format
45226 var format = self._output.find('-f', 1);
453
45426 if (format) {
45524 if (!(format[0] in formats) || !(formats[format[0]].canMux)) {
4562 return cb(new Error('Output format ' + format[0] + ' is not available'));
457 }
458 }
459
460 // Input format(s)
46124 var unavailable = self._inputs.reduce(function(fmts, input) {
46224 var format = input.before.find('-f', 1);
46324 if (format) {
4644 if (!(format[0] in formats) || !(formats[format[0]].canDemux)) {
4651 fmts.push(format[0]);
466 }
467 }
468
46924 return fmts;
470 }, []);
471
47224 if (unavailable.length === 1) {
4731 cb(new Error('Input format ' + unavailable[0] + ' is not available'));
47423 } else if (unavailable.length > 1) {
4750 cb(new Error('Input formats ' + unavailable.join(', ') + ' are not available'));
476 } else {
47723 cb();
478 }
479 },
480
481 // Get available codecs
482 function(cb) {
48323 self.availableCodecs(cb);
484 },
485
486 // Check whether specified codecs are available
487 function(codecs, cb) {
488 // Audio codec
48923 var acodec = self._audio.find('-acodec', 1);
49023 if (acodec && acodec[0] !== 'copy') {
49121 if (!(acodec[0] in codecs) || codecs[acodec[0]].type !== 'audio' || !(codecs[acodec[0]].canEncode)) {
4921 return cb(new Error('Audio codec ' + acodec[0] + ' is not available'));
493 }
494 }
495
496 // Video codec
49722 var vcodec = self._video.find('-vcodec', 1);
49822 if (vcodec && vcodec[0] !== 'copy') {
49920 if (!(vcodec[0] in codecs) || codecs[vcodec[0]].type !== 'video' || !(codecs[vcodec[0]].canEncode)) {
5001 return cb(new Error('Video codec ' + vcodec[0] + ' is not available'));
501 }
502 }
503
50421 cb();
505 }
506 ], callback);
507 };
508};
509

lib/ffprobe.js

80%
66
53
13
LineHitsSource
1/*jshint node:true, laxcomma:true*/
2'use strict';
3
41var spawn = require('child_process').spawn;
5
6
7263function legacyTag(key) { return key.match(/^TAG:/); }
8263function legacyDisposition(key) { return key.match(/^DISPOSITION:/); }
9
10
111module.exports = function(proto) {
12 /**
13 * Run ffprobe on last specified input
14 *
15 * Callback will receive an object as its second argument. This object
16 * has the same format as what the following command returns:
17 *
18 * ffprobe -print_format json -show_streams -show_format INPUTFILE
19 *
20 * @method FfmpegCommand#ffprobe
21 * @category Metadata
22 *
23 * @param {Function} callback callback with signature (err, ffprobeData)
24 *
25 */
261 proto.ffprobe = function(callback) {
2710 if (!this._currentInput) {
281 return callback(new Error('No input specified'));
29 }
30
319 if (typeof this._currentInput.source !== 'string') {
321 return callback(new Error('Cannot run ffprobe on non-file input'));
33 }
34
35 // Find ffprobe
368 var self = this;
378 this._getFfprobePath(function(err, path) {
388 if (err) {
390 return callback(err);
408 } else if (!path) {
410 return callback(new Error('Cannot find ffprobe'));
42 }
43
448 var stdout = '';
458 var stdoutClosed = false;
468 var stderr = '';
478 var stderrClosed = false;
48
49 // Spawn ffprobe
508 var ffprobe = spawn(path, [
51 '-print_format', 'json',
52 '-show_streams',
53 '-show_format',
54 self._currentInput.source
55 ]);
56
578 ffprobe.on('error', function(err) {
580 callback(err);
59 });
60
61 // Ensure we wait for captured streams to end before calling callback
628 var exitError = null;
63 function handleExit(err) {
6424 if (err) {
651 exitError = err;
66 }
67
6824 if (processExited && stdoutClosed && stderrClosed) {
698 if (exitError) {
701 if (stderr) {
711 exitError.message += '\n' + stderr;
72 }
73
741 return callback(exitError);
75 }
76
77 // Process output
787 var data;
79
807 try {
817 data = JSON.parse(stdout);
82 } catch(e) {
830 return callback(e);
84 }
85
86 // Handle legacy output with "TAG:x" and "DISPOSITION:x" keys
877 [data.format].concat(data.streams).forEach(function(target) {
8815 var legacyTagKeys = Object.keys(target).filter(legacyTag);
89
9015 if (legacyTagKeys.length) {
910 target.tags = target.tags || {};
92
930 legacyTagKeys.forEach(function(tagKey) {
940 target.tags[tagKey.substr(4)] = target[tagKey];
950 delete target[tagKey];
96 });
97 }
98
9915 var legacyDispositionKeys = Object.keys(target).filter(legacyDisposition);
100
10115 if (legacyDispositionKeys.length) {
1020 target.disposition = target.disposition || {};
103
1040 legacyDispositionKeys.forEach(function(dispositionKey) {
1050 target.disposition[dispositionKey.substr(12)] = target[dispositionKey];
1060 delete target[dispositionKey];
107 });
108 }
109 });
110
1117 callback(null, data);
112 }
113 }
114
115 // Handle ffprobe exit
1168 var processExited = false;
1178 ffprobe.on('exit', function(code, signal) {
1188 processExited = true;
119
1208 if (code) {
1211 handleExit(new Error('ffprobe exited with code ' + code));
1227 } else if (signal) {
1230 handleExit(new Error('ffprobe was killed with signal ' + signal));
124 } else {
1257 handleExit();
126 }
127 });
128
129 // Handle stdout/stderr streams
1308 ffprobe.stdout.on('data', function(data) {
13118 stdout += data;
132 });
133
1348 ffprobe.stdout.on('close', function() {
1358 stdoutClosed = true;
1368 handleExit();
137 });
138
1398 ffprobe.stderr.on('data', function(data) {
14033 stderr += data;
141 });
142
1438 ffprobe.stderr.on('close', function() {
1448 stderrClosed = true;
1458 handleExit();
146 });
147 });
148 };
149};
150
151

lib/fluent-ffmpeg.js

100%
44
44
0
LineHitsSource
1/*jshint node:true*/
2'use strict';
3
41var path = require('path');
51var util = require('util');
61var EventEmitter = require('events').EventEmitter;
7
81var utils = require('./utils');
9
10
11/**
12 * Create an ffmpeg command
13 *
14 * Can be called with or without the 'new' operator, and the 'input' parameter
15 * may be specified as 'options.source' instead (or passed later with the
16 * addInput method).
17 *
18 * @constructor
19 * @param {String|ReadableStream} [input] input file path or readable stream
20 * @param {Object} [options] command options
21 * @param {Object} [options.logger=<no logging>] logger object with 'error', 'warning', 'info' and 'debug' methods
22 * @param {Number} [options.niceness=0] ffmpeg process niceness, ignored on Windows
23 * @param {Number} [options.priority=0] alias for `niceness`
24 * @param {String} [options.presets="fluent-ffmpeg/lib/presets"] directory to load presets from
25 * @param {String} [options.preset="fluent-ffmpeg/lib/presets"] alias for `presets`
26 * @param {Number} [options.timeout=<no timeout>] ffmpeg processing timeout in seconds
27 * @param {String|ReadableStream} [options.source=<no input>] alias for the `input` parameter
28 */
29function FfmpegCommand(input, options) {
30 // Make 'new' optional
31207 if (!(this instanceof FfmpegCommand)) {
321 return new FfmpegCommand(input, options);
33 }
34
35206 EventEmitter.call(this);
36
37206 if (typeof input === 'object' && !('readable' in input)) {
38 // Options object passed directly
3989 options = input;
40 } else {
41 // Input passed first
42117 options = options || {};
43117 options.source = input;
44 }
45
46 // Add input if present
47206 this._inputs = [];
48206 if (options.source) {
4994 this.addInput(options.source);
50 }
51
52 // Create argument lists
53206 this._audio = utils.args();
54206 this._audioFilters = utils.args();
55206 this._video = utils.args();
56206 this._videoFilters = utils.args();
57206 this._sizeFilters = utils.args();
58206 this._output = utils.args();
59
60 // Set default option values
61206 options.presets = options.presets || options.preset || path.join(__dirname, 'presets');
62206 options.niceness = options.niceness || options.priority || 0;
63
64 // Save options
65206 this.options = options;
66
67 // Setup logger
68206 this.logger = options.logger || {
69 debug: function() {},
70 info: function() {},
71 warn: function() {},
72 error: function() {}
73 };
74}
751util.inherits(FfmpegCommand, EventEmitter);
761module.exports = FfmpegCommand;
77
78
79/* Add methods from options submodules */
80
811require('./options/inputs')(FfmpegCommand.prototype);
821require('./options/audio')(FfmpegCommand.prototype);
831require('./options/video')(FfmpegCommand.prototype);
841require('./options/videosize')(FfmpegCommand.prototype);
851require('./options/output')(FfmpegCommand.prototype);
861require('./options/custom')(FfmpegCommand.prototype);
871require('./options/misc')(FfmpegCommand.prototype);
88
89
90/* Add processor methods */
91
921require('./processor')(FfmpegCommand.prototype);
93
94
95/* Add capabilities methods */
96
971require('./capabilities')(FfmpegCommand.prototype);
98
991FfmpegCommand.availableFilters =
100FfmpegCommand.getAvailableFilters = function(callback) {
1011 (new FfmpegCommand()).availableFilters(callback);
102};
103
1041FfmpegCommand.availableCodecs =
105FfmpegCommand.getAvailableCodecs = function(callback) {
1061 (new FfmpegCommand()).availableCodecs(callback);
107};
108
1091FfmpegCommand.availableFormats =
110FfmpegCommand.getAvailableFormats = function(callback) {
1111 (new FfmpegCommand()).availableFormats(callback);
112};
113
114
115/* Add ffprobe methods */
116
1171require('./ffprobe')(FfmpegCommand.prototype);
118
1191FfmpegCommand.ffprobe = function(file, callback) {
1204 (new FfmpegCommand(file)).ffprobe(callback);
121};
122
123

lib/options/audio.js

100%
25
25
0
LineHitsSource
1/*jshint node:true*/
2'use strict';
3
4/*
5 *! Audio-related methods
6 */
7
81module.exports = function(proto) {
9 /**
10 * Disable audio in the output
11 *
12 * @method FfmpegCommand#noAudio
13 * @category Audio
14 * @aliases withNoAudio
15 * @return FfmpegCommand
16 */
171 proto.withNoAudio =
18 proto.noAudio = function() {
192 this._audio.clear();
202 this._audio('-an');
21
222 return this;
23 };
24
25
26 /**
27 * Specify audio codec
28 *
29 * @method FfmpegCommand#audioCodec
30 * @category Audio
31 * @aliases withAudioCodec
32 *
33 * @param {String} codec audio codec name
34 * @return FfmpegCommand
35 */
361 proto.withAudioCodec =
37 proto.audioCodec = function(codec) {
3826 this._audio('-acodec', codec);
3926 return this;
40 };
41
42
43 /**
44 * Specify audio bitrate
45 *
46 * @method FfmpegCommand#audioBitrate
47 * @category Audio
48 * @aliases withAudioBitrate
49 *
50 * @param {String|Number} bitrate audio bitrate in kbps (with an optional 'k' suffix)
51 * @return FfmpegCommand
52 */
531 proto.withAudioBitrate =
54 proto.audioBitrate = function(bitrate) {
5522 this._audio('-b:a', ('' + bitrate).replace(/k?$/, 'k'));
5622 return this;
57 };
58
59
60 /**
61 * Specify audio channel count
62 *
63 * @method FfmpegCommand#audioChannels
64 * @category Audio
65 * @aliases withAudioChannels
66 *
67 * @param {Number} channels channel count
68 * @return FfmpegCommand
69 */
701 proto.withAudioChannels =
71 proto.audioChannels = function(channels) {
7222 this._audio('-ac', channels);
7322 return this;
74 };
75
76
77 /**
78 * Specify audio frequency
79 *
80 * @method FfmpegCommand#audioFrequency
81 * @category Audio
82 * @aliases withAudioFrequency
83 *
84 * @param {Number} freq audio frequency in Hz
85 * @return FfmpegCommand
86 */
871 proto.withAudioFrequency =
88 proto.audioFrequency = function(freq) {
8920 this._audio('-ar', freq);
9020 return this;
91 };
92
93
94 /**
95 * Specify audio quality
96 *
97 * @method FfmpegCommand#audioQuality
98 * @category Audio
99 * @aliases withAudioQuality
100 *
101 * @param {Number} quality audio quality factor
102 * @return FfmpegCommand
103 */
1041 proto.withAudioQuality =
105 proto.audioQuality = function(quality) {
1061 this._audio('-aq', quality);
1071 return this;
108 };
109
110
111 /**
112 * Specify custom audio filter(s)
113 *
114 * Can be called both with one or many filters, or a filter array.
115 *
116 * @example
117 * command.audioFilters('filter1');
118 *
119 * @example
120 * command.audioFilters('filter1', 'filter2');
121 *
122 * @example
123 * command.audioFilters(['filter1', 'filter2']);
124 *
125 * @method FfmpegCommand#audioFilters
126 * @aliases withAudioFilter,withAudioFilters,audioFilter
127 * @category Audio
128 *
129 * @param {String|Array} filters... audio filter strings or string array
130 * @return FfmpegCommand
131 */
1321 proto.withAudioFilter =
133 proto.withAudioFilters =
134 proto.audioFilter =
135 proto.audioFilters = function(filters) {
1363 if (arguments.length > 1) {
1371 filters = [].slice.call(arguments);
138 }
139
1403 this._audioFilters(filters);
1413 return this;
142 };
143};
144

lib/options/custom.js

100%
31
31
0
LineHitsSource
1/*jshint node:true*/
2'use strict';
3
4/*
5 *! Custom options methods
6 */
7
81module.exports = function(proto) {
9 /**
10 * Add custom input option(s)
11 *
12 * When passing a single string or an array, each string containing two
13 * words is split (eg. inputOptions('-option value') is supported) for
14 * compatibility reasons. This is not the case when passing more than
15 * one argument.
16 *
17 * @example
18 * command.inputOptions('option1');
19 *
20 * @example
21 * command.inputOptions('option1', 'option2');
22 *
23 * @example
24 * command.inputOptions(['option1', 'option2']);
25 *
26 * @method FfmpegCommand#inputOptions
27 * @category Custom options
28 * @aliases addInputOption,addInputOptions,withInputOption,withInputOptions,inputOption
29 *
30 * @param {...String} options option string(s) or string array
31 * @return FfmpegCommand
32 */
331 proto.addInputOption =
34 proto.addInputOptions =
35 proto.withInputOption =
36 proto.withInputOptions =
37 proto.inputOption =
38 proto.inputOptions = function(options) {
395 if (!this._currentInput) {
401 throw new Error('No input specified');
41 }
42
434 var doSplit = true;
44
454 if (arguments.length > 1) {
462 options = [].slice.call(arguments);
472 doSplit = false;
48 }
49
504 if (!Array.isArray(options)) {
511 options = [options];
52 }
53
544 this._currentInput.before(options.reduce(function(options, option) {
557 var split = option.split(' ');
56
577 if (doSplit && split.length === 2) {
583 options.push(split[0], split[1]);
59 } else {
604 options.push(option);
61 }
62
637 return options;
64 }, []));
654 return this;
66 };
67
68
69 /**
70 * Add custom output option(s)
71 *
72 * @example
73 * command.outputOptions('option1');
74 *
75 * @example
76 * command.outputOptions('option1', 'option2');
77 *
78 * @example
79 * command.outputOptions(['option1', 'option2']);
80 *
81 * @method FfmpegCommand#outputOptions
82 * @category Custom options
83 * @aliases addOutputOption,addOutputOptions,addOption,addOptions,withOutputOption,withOutputOptions,withOption,withOptions,outputOption
84 *
85 * @param {...String} options option string(s) or string array
86 * @return FfmpegCommand
87 */
881 proto.addOutputOption =
89 proto.addOutputOptions =
90 proto.addOption =
91 proto.addOptions =
92 proto.withOutputOption =
93 proto.withOutputOptions =
94 proto.withOption =
95 proto.withOptions =
96 proto.outputOption =
97 proto.outputOptions = function(options) {
986 var doSplit = true;
99
1006 if (arguments.length > 1) {
1012 options = [].slice.call(arguments);
1022 doSplit = false;
103 }
104
1056 if (!Array.isArray(options)) {
1061 options = [options];
107 }
108
1096 this._output(options.reduce(function(options, option) {
11045 var split = option.split(' ');
111
11245 if (doSplit && split.length === 2) {
11319 options.push(split[0], split[1]);
114 } else {
11526 options.push(option);
116 }
117
11845 return options;
119 }, []));
1206 return this;
121 };
122};
123

lib/options/inputs.js

100%
39
39
0
LineHitsSource
1/*jshint node:true*/
2'use strict';
3
41var utils = require('../utils');
5
6/*
7 *! Input-related methods
8 */
9
101module.exports = function(proto) {
11 /**
12 * Add an input to command
13 *
14 * Also switches "current input", that is the input that will be affected
15 * by subsequent input-related methods.
16 *
17 * Note: only one stream input is supported for now.
18 *
19 * @method FfmpegCommand#input
20 * @category Input
21 * @aliases mergeAdd,addInput
22 *
23 * @param {String|Readable} source input file path or readable stream
24 * @return FfmpegCommand
25 */
261 proto.mergeAdd =
27 proto.addInput =
28 proto.input = function(source) {
29101 if (typeof source !== 'string') {
306 if (!('readable' in source)) {
311 throw new Error('Invalid input');
32 }
33
345 var hasInputStream = this._inputs.some(function(input) {
351 return typeof input.source !== 'string';
36 });
37
385 if (hasInputStream) {
391 throw new Error('Only one input stream is supported');
40 }
41
424 source.pause();
43 }
44
4599 this._inputs.push(this._currentInput = {
46 source: source,
47 before: utils.args(),
48 after: utils.args(),
49 });
50
5199 return this;
52 };
53
54
55 /**
56 * Specify input format for the last specified input
57 *
58 * @method FfmpegCommand#inputFormat
59 * @category Input
60 * @aliases withInputFormat,fromFormat
61 *
62 * @param {String} format input format
63 * @return FfmpegCommand
64 */
651 proto.withInputFormat =
66 proto.inputFormat =
67 proto.fromFormat = function(format) {
686 if (!this._currentInput) {
691 throw new Error('No input specified');
70 }
71
725 this._currentInput.before('-f', format);
735 return this;
74 };
75
76
77 /**
78 * Specify input FPS for the last specified input
79 * (only valid for raw video formats)
80 *
81 * @method FfmpegCommand#inputFps
82 * @category Input
83 * @aliases withInputFps,withInputFPS,withFpsInput,withFPSInput,inputFPS,inputFps,fpsInput
84 *
85 * @param {Number} fps input FPS
86 * @return FfmpegCommand
87 */
881 proto.withInputFps =
89 proto.withInputFPS =
90 proto.withFpsInput =
91 proto.withFPSInput =
92 proto.inputFPS =
93 proto.inputFps =
94 proto.fpsInput =
95 proto.FPSInput = function(fps) {
962 if (!this._currentInput) {
971 throw new Error('No input specified');
98 }
99
1001 this._currentInput.before('-r', fps);
1011 return this;
102 };
103
104
105 /**
106 * Specify input seek time for the last specified input
107 *
108 * @method FfmpegCommand#seek
109 * @category Input
110 * @aliases setStartTime,seekTo
111 *
112 * @param {String|Number} seek seek time in seconds or as a '[hh:[mm:]]ss[.xxx]' string
113 * @param {Boolean} [fast=false] use fast (but inexact) seek
114 * @return FfmpegCommand
115 */
1161 proto.setStartTime =
117 proto.seekTo =
118 proto.seek = function(seek, fast) {
1194 if (!this._currentInput) {
1202 throw new Error('No input specified');
121 }
122
1232 if (fast) {
1241 this._currentInput.before('-ss', seek);
125 } else {
1261 this._currentInput.after('-ss', seek);
127 }
128
1292 return this;
130 };
131
132
133 /**
134 * Specify input fast-seek time for the last specified input
135 *
136 * @method FfmpegCommand#fastSeek
137 * @category Input
138 * @aliases fastSeekTo
139 *
140 * @param {String|Number} seek fast-seek time in seconds or as a '[[hh:]mm:]ss[.xxx]' string
141 * @return FfmpegCommand
142 */
1431 proto.fastSeek =
144 proto.fastSeekTo = function(seek) {
1451 return this.seek(seek, true);
146 };
147
148
149 /**
150 * Loop over the last specified input
151 *
152 * @method FfmpegCommand#loop
153 * @category Input
154 *
155 * @param {String|Number} [duration] loop duration in seconds or as a '[[hh:]mm:]ss[.xxx]' string
156 * @return FfmpegCommand
157 */
1581 proto.loop = function(duration) {
1594 if (!this._currentInput) {
1601 throw new Error('No input specified');
161 }
162
1633 this._currentInput.before('-loop', '1');
164
1653 if (typeof duration !== 'undefined') {
1662 this.duration(duration);
167 }
168
1693 return this;
170 };
171};
172

lib/options/misc.js

100%
19
19
0
LineHitsSource
1/*jshint node:true*/
2'use strict';
3
41var path = require('path');
5
6/*
7 *! Miscellaneous methods
8 */
9
101module.exports = function(proto) {
11 /**
12 * Use preset
13 *
14 * @method FfmpegCommand#preset
15 * @category Miscellaneous
16 * @aliases usingPreset
17 *
18 * @param {String|Function} preset preset name or preset function
19 */
201 proto.usingPreset =
21 proto.preset = function(preset) {
2223 if (typeof preset === 'function') {
231 preset(this);
24 } else {
2522 try {
2622 var modulePath = path.join(this.options.presets, preset);
2722 var module = require(modulePath);
28
2921 if (typeof module.load === 'function') {
3020 module.load(this);
31 } else {
321 throw new Error('preset ' + modulePath + ' has no load() function');
33 }
34 } catch (err) {
352 throw new Error('preset ' + modulePath + ' could not be loaded: ' + err.message);
36 }
37 }
38
3921 return this;
40 };
41
42
43 /**
44 * Enable experimental codecs
45 *
46 * @method FfmpegCommand#strict
47 * @category Miscellaneous
48 * @aliases withStrictExperimental
49 *
50 * @return FfmpegCommand
51 */
521 proto.withStrictExperimental =
53 proto.strict = function() {
5420 this._output('-strict', 'experimental');
5520 return this;
56 };
57
58
59 /**
60 * Run flvtool2/flvmeta on output
61 *
62 * @method FfmpegCommand#flvmeta
63 * @category Miscellaneous
64 * @aliases updateFlvMetadata
65 *
66 * @return FfmpegCommand
67 */
681 proto.updateFlvMetadata =
69 proto.flvmeta = function() {
7018 this.options.flvmeta = true;
7118 return this;
72 };
73};
74

lib/options/output.js

100%
7
7
0
LineHitsSource
1/*jshint node:true*/
2'use strict';
3
4/*
5 *! Output-related methods
6 */
7
81module.exports = function(proto) {
9 /**
10 * Set output duration
11 *
12 * @method FfmpegCommand#duration
13 * @category Output
14 * @aliases withDuration,setDuration
15 *
16 * @param {String|Number} duration duration in seconds or as a '[[hh:]mm:]ss[.xxx]' string
17 * @return FfmpegCommand
18 */
191 proto.withDuration =
20 proto.setDuration =
21 proto.duration = function(duration) {
223 this._output('-t', duration);
233 return this;
24 };
25
26
27 /**
28 * Set output format
29 *
30 * @method FfmpegCommand#format
31 * @category Output
32 * @aliases toFormat,withOutputFormat,outputFormat
33 *
34 * @param {String} format output format name
35 * @return FfmpegCommand
36 */
371 proto.toFormat =
38 proto.withOutputFormat =
39 proto.outputFormat =
40 proto.format = function(format) {
4127 this._output('-f', format);
4227 return this;
43 };
44};
45

lib/options/video.js

100%
27
27
0
LineHitsSource
1/*jshint node:true*/
2'use strict';
3
4/*
5 *! Video-related methods
6 */
7
81module.exports = function(proto) {
9 /**
10 * Disable video in the output
11 *
12 * @method FfmpegCommand#noVideo
13 * @category Video
14 * @aliases withNoVideo
15 *
16 * @return FfmpegCommand
17 */
181 proto.withNoVideo =
19 proto.noVideo = function() {
202 this._video.clear();
212 this._video('-vn');
22
232 return this;
24 };
25
26
27 /**
28 * Specify video codec
29 *
30 * @method FfmpegCommand#videoCodec
31 * @category Video
32 * @aliases withVideoCodec
33 *
34 * @param {String} codec video codec name
35 * @return FfmpegCommand
36 */
371 proto.withVideoCodec =
38 proto.videoCodec = function(codec) {
3927 this._video('-vcodec', codec);
4027 return this;
41 };
42
43
44 /**
45 * Specify video bitrate
46 *
47 * @method FfmpegCommand#videoBitrate
48 * @category Video
49 * @aliases withVideoBitrate
50 *
51 * @param {String|Number} bitrate video bitrate in kbps (with an optional 'k' suffix)
52 * @param {Boolean} [constant=false] enforce constant bitrate
53 * @return FfmpegCommand
54 */
551 proto.withVideoBitrate =
56 proto.videoBitrate = function(bitrate, constant) {
5722 bitrate = ('' + bitrate).replace(/k?$/, 'k');
58
5922 this._video('-b:v', bitrate);
6022 if (constant) {
611 this._video(
62 '-maxrate', bitrate,
63 '-minrate', bitrate,
64 '-bufsize', '3M'
65 );
66 }
67
6822 return this;
69 };
70
71
72 /**
73 * Specify custom video filter(s)
74 *
75 * Can be called both with one or many filters, or a filter array.
76 *
77 * @example
78 * command.videoFilters('filter1');
79 *
80 * @example
81 * command.videoFilters('filter1', 'filter2');
82 *
83 * @example
84 * command.videoFilters(['filter1', 'filter2']);
85 *
86 * @method FfmpegCommand#videoFilters
87 * @category Video
88 * @aliases withVideoFilter,withVideoFilters,videoFilter
89 *
90 * @param {String|Array} filters... video filter strings or string array
91 * @return FfmpegCommand
92 */
931 proto.withVideoFilter =
94 proto.withVideoFilters =
95 proto.videoFilter =
96 proto.videoFilters = function(filters) {
974 if (arguments.length > 1) {
982 filters = [].slice.call(arguments);
99 }
100
1014 if (Array.isArray(filters)) {
1022 this._videoFilters.apply(null, filters);
103 } else {
1042 this._videoFilters(filters);
105 }
106
1074 return this;
108 };
109
110
111 /**
112 * Specify output FPS
113 *
114 * @method FfmpegCommand#fps
115 * @category Video
116 * @aliases withOutputFps,withOutputFPS,withFpsOutput,withFPSOutput,withFps,withFPS,outputFPS,outputFps,fpsOutput,FPSOutput,FPS
117 *
118 * @param {Number} fps output FPS
119 * @return FfmpegCommand
120 */
1211 proto.withOutputFps =
122 proto.withOutputFPS =
123 proto.withFpsOutput =
124 proto.withFPSOutput =
125 proto.withFps =
126 proto.withFPS =
127 proto.outputFPS =
128 proto.outputFps =
129 proto.fpsOutput =
130 proto.FPSOutput =
131 proto.fps =
132 proto.FPS = function(fps) {
13319 this._video('-r', fps);
13419 return this;
135 };
136
137
138 /**
139 * Only transcode a certain number of frames
140 *
141 * @method FfmpegCommand#frames
142 * @category Video
143 * @aliases takeFrames,withFrames
144 *
145 * @param {Number} frames frame count
146 * @return FfmpegCommand
147 */
1481 proto.takeFrames =
149 proto.withFrames =
150 proto.frames = function(frames) {
1515 this._video('-vframes', frames);
1525 return this;
153 };
154};
155

lib/options/videosize.js

100%
62
62
0
LineHitsSource
1/*jshint node:true*/
2'use strict';
3
4/*
5 *! Size helpers
6 */
7
8
9/**
10 * Return filters to pad video to width*height,
11 *
12 * @param {Number} width output width
13 * @param {Number} height output height
14 * @param {Number} aspect video aspect ratio (without padding)
15 * @param {Number} color padding color
16 * @return scale/pad filters
17 * @private
18 */
19function getScalePadFilters(width, height, aspect, color) {
20 /*
21 let a be the input aspect ratio, A be the requested aspect ratio
22
23 if a > A, padding is done on top and bottom
24 if a < A, padding is done on left and right
25 */
26
2710 return [
28 /*
29 In both cases, we first have to scale the input to match the requested size.
30 When using computed width/height, we truncate them to multiples of 2
31
32 scale=
33 w=if(gt(a, A), width, trunc(height*a/2)*2):
34 h=if(lt(a, A), height, trunc(width/a/2)*2)
35 */
36
37 'scale=\'' +
38 'w=if(gt(a,' + aspect + '),' + width + ',trunc(' + height + '*a/2)*2):' +
39 'h=if(lt(a,' + aspect + '),' + height + ',trunc(' + width + '/a/2)*2)\'',
40
41 /*
42 Then we pad the scaled input to match the target size
43
44 pad=
45 w=width:
46 h=height:
47 x=if(gt(a, A), 0, (width - iw)/2):
48 y=if(lt(a, A), 0, (height - ih)/2)
49
50 (here iw and ih refer to the padding input, i.e the scaled output)
51 */
52
53 'pad=\'' +
54 'w=' + width + ':' +
55 'h=' + height + ':' +
56 'x=if(gt(a,' + aspect + '),0,(' + width + '-iw)/2):' +
57 'y=if(lt(a,' + aspect + '),0,(' + height + '-ih)/2):' +
58 'color=' + color + '\''
59 ];
60}
61
62
63/**
64 * Recompute size filters
65 *
66 * @param {FfmpegCommand} command
67 * @param {String} key newly-added parameter name ('size', 'aspect' or 'pad')
68 * @param {String} value newly-added parameter value
69 * @return filter string array
70 * @private
71 */
72function createSizeFilters(command, key, value) {
73 // Store parameters
7480 var data = command._sizeData = command._sizeData || {};
7580 data[key] = value;
76
7780 if (!('size' in data)) {
78 // No size requested, keep original size
792 return [];
80 }
81
82 // Try to match the different size string formats
8378 var fixedSize = data.size.match(/([0-9]+)x([0-9]+)/);
8478 var fixedWidth = data.size.match(/([0-9]+)x\?/);
8578 var fixedHeight = data.size.match(/\?x([0-9]+)/);
8678 var percentRatio = data.size.match(/\b([0-9]{1,3})%/);
8778 var width, height, aspect;
88
8978 if (percentRatio) {
905 var ratio = Number(percentRatio[1]) / 100;
915 return ['scale=trunc(iw*' + ratio + '/2)*2:trunc(ih*' + ratio + '/2)*2'];
9273 } else if (fixedSize) {
93 // Round target size to multiples of 2
9421 width = Math.round(Number(fixedSize[1]) / 2) * 2;
9521 height = Math.round(Number(fixedSize[2]) / 2) * 2;
96
9721 aspect = width / height;
98
9921 if (data.pad) {
1005 return getScalePadFilters(width, height, aspect, data.pad);
101 } else {
102 // No autopad requested, rescale to target size
10316 return ['scale=' + width + ':' + height];
104 }
10552 } else if (fixedWidth || fixedHeight) {
10651 if ('aspect' in data) {
107 // Specified aspect ratio
10814 width = fixedWidth ? fixedWidth[1] : Math.round(Number(fixedHeight[1]) * data.aspect);
10914 height = fixedHeight ? fixedHeight[1] : Math.round(Number(fixedWidth[1]) / data.aspect);
110
111 // Round to multiples of 2
11214 width = Math.round(width / 2) * 2;
11314 height = Math.round(height / 2) * 2;
114
11514 if (data.pad) {
1165 return getScalePadFilters(width, height, data.aspect, data.pad);
117 } else {
118 // No autopad requested, rescale to target size
1199 return ['scale=' + width + ':' + height];
120 }
121 } else {
122 // Keep input aspect ratio
123
12437 if (fixedWidth) {
12531 return ['scale=' + (Math.round(Number(fixedWidth[1]) / 2) * 2) + ':trunc(ow/a/2)*2'];
126 } else {
1276 return ['scale=trunc(oh*a/2)*2:' + (Math.round(Number(fixedHeight[1]) / 2) * 2)];
128 }
129 }
130 } else {
1311 throw new Error('Invalid size specified: ' + data.size);
132 }
133}
134
135
136/*
137 *! Video size-related methods
138 */
139
1401module.exports = function(proto) {
141 /**
142 * Keep display aspect ratio
143 *
144 * This method is useful when converting an input with non-square pixels to an output format
145 * that does not support non-square pixels. It rescales the input so that the display aspect
146 * ratio is the same.
147 *
148 * @method FfmpegCommand#keepDAR
149 * @category Video size
150 * @aliases keepPixelAspect,keepDisplayAspect,keepDisplayAspectRatio
151 *
152 * @return FfmpegCommand
153 */
1541 proto.keepPixelAspect = // Only for compatibility, this is not about keeping _pixel_ aspect ratio
155 proto.keepDisplayAspect =
156 proto.keepDisplayAspectRatio =
157 proto.keepDAR = function() {
1581 return this.videoFilters(
159 'scale=\'w=if(gt(sar,1),iw*sar,iw):h=if(lt(sar,1),ih/sar,ih)\'',
160 'setsar=1'
161 );
162 };
163
164
165 /**
166 * Set output size
167 *
168 * The 'size' parameter can have one of 4 forms:
169 * - 'X%': rescale to xx % of the original size
170 * - 'WxH': specify width and height
171 * - 'Wx?': specify width and compute height from input aspect ratio
172 * - '?xH': specify height and compute width from input aspect ratio
173 *
174 * Note: both dimensions will be truncated to multiples of 2.
175 *
176 * @method FfmpegCommand#size
177 * @category Video size
178 * @aliases withSize,setSize
179 *
180 * @param {String} size size string, eg. '33%', '320x240', '320x?', '?x240'
181 * @return FfmpegCommand
182 */
1831 proto.withSize =
184 proto.setSize =
185 proto.size = function(size) {
18652 var filters = createSizeFilters(this, 'size', size);
187
18851 this._sizeFilters.clear();
18951 this._sizeFilters(filters);
190
19151 return this;
192 };
193
194
195 /**
196 * Set output aspect ratio
197 *
198 * @method FfmpegCommand#aspect
199 * @category Video size
200 * @aliases withAspect,withAspectRatio,setAspect,setAspectRatio,aspectRatio
201 *
202 * @param {String|Number} aspect aspect ratio (number or 'X:Y' string)
203 * @return FfmpegCommand
204 */
2051 proto.withAspect =
206 proto.withAspectRatio =
207 proto.setAspect =
208 proto.setAspectRatio =
209 proto.aspect =
210 proto.aspectRatio = function(aspect) {
21115 var a = Number(aspect);
21215 if (isNaN(a)) {
2133 var match = aspect.match(/^(\d+):(\d+)$/);
2143 if (match) {
2152 a = Number(match[1]) / Number(match[2]);
216 } else {
2171 throw new Error('Invalid aspect ratio: ' + aspect);
218 }
219 }
220
22114 var filters = createSizeFilters(this, 'aspect', a);
222
22314 this._sizeFilters.clear();
22414 this._sizeFilters(filters);
225
22614 return this;
227 };
228
229
230 /**
231 * Enable auto-padding the output
232 *
233 * @method FfmpegCommand#autopad
234 * @category Video size
235 * @aliases applyAutopadding,applyAutoPadding,applyAutopad,applyAutoPad,withAutopadding,withAutoPadding,withAutopad,withAutoPad,autoPad
236 *
237 * @param {Boolean} [pad=true] enable/disable auto-padding
238 * @param {String} [color='black'] pad color
239 */
2401 proto.applyAutopadding =
241 proto.applyAutoPadding =
242 proto.applyAutopad =
243 proto.applyAutoPad =
244 proto.withAutopadding =
245 proto.withAutoPadding =
246 proto.withAutopad =
247 proto.withAutoPad =
248 proto.autoPad =
249 proto.autopad = function(pad, color) {
250 // Allow autopad(color)
25114 if (typeof pad === 'string') {
2521 color = pad;
2531 pad = true;
254 }
255
256 // Allow autopad() and autopad(undefined, color)
25714 if (typeof pad === 'undefined') {
2581 pad = true;
259 }
260
26114 var filters = createSizeFilters(this, 'pad', pad ? color || 'black' : false);
262
26314 this._sizeFilters.clear();
26414 this._sizeFilters(filters);
265
26614 return this;
267 };
268};
269

lib/presets/flashvideo.js

100%
2
2
0
LineHitsSource
1/*jshint node:true */
2'use strict';
3
41exports.load = function(ffmpeg) {
518 ffmpeg
6 .format('flv')
7 .flvmeta()
8 .size('320x?')
9 .videoBitrate('512k')
10 .videoCodec('libx264')
11 .fps(24)
12 .audioBitrate('96k')
13 .audioCodec('aac')
14 .strict()
15 .audioFrequency(22050)
16 .audioChannels(2);
17};
18

lib/presets/podcast.js

100%
2
2
0
LineHitsSource
1/*jshint node:true */
2'use strict';
3
41exports.load = function(ffmpeg) {
51 ffmpeg
6 .format('m4v')
7 .videoBitrate('512k')
8 .videoCodec('libx264')
9 .size('320x176')
10 .audioBitrate('128k')
11 .audioCodec('aac')
12 .strict()
13 .audioChannels(1)
14 .outputOptions(['-flags', '+loop', '-cmp', '+chroma', '-partitions','+parti4x4+partp8x8+partb8x8', '-flags2',
15 '+mixed_refs', '-me_method umh', '-subq 5', '-bufsize 2M', '-rc_eq \'blurCplx^(1-qComp)\'',
16 '-qcomp 0.6', '-qmin 10', '-qmax 51', '-qdiff 4', '-level 13' ]);
17};
18

lib/processor.js

85%
360
307
53
LineHitsSource
1/*jshint node:true*/
2'use strict';
3
41var spawn = require('child_process').spawn;
51var PassThrough = require('stream').PassThrough;
61var path = require('path');
71var fs = require('fs');
81var async = require('async');
91var utils = require('./utils');
10
11
12/*
13 *! Processor methods
14 */
15
16
17/**
18 * @param {FfmpegCommand} command
19 * @param {String|Writable} target
20 * @param {Object} [pipeOptions]
21 * @private
22 */
23function _process(command, target, pipeOptions) {
2419 var isStream;
25
2619 if (typeof target === 'string') {
2716 isStream = false;
28 } else {
293 isStream = true;
303 pipeOptions = pipeOptions || {};
31 }
32
33 // Ensure we send 'end' or 'error' only once
3419 var ended = false;
35 function emitEnd(err, stdout, stderr) {
3624 if (!ended) {
3719 ended = true;
38
3919 if (err) {
405 command.emit('error', err, stdout, stderr);
41 } else {
4214 command.emit('end', stdout, stderr);
43 }
44 }
45 }
46
4719 command._prepare(function(err, args) {
4819 if (err) {
491 return emitEnd(err);
50 }
51
5218 if (isStream) {
533 args.push('pipe:1');
54
553 if (command.options.flvmeta) {
563 command.logger.warn('Updating flv metadata is not supported for streams');
573 command.options.flvmeta = false;
58 }
59 } else {
6015 args.push('-y', target);
61 }
62
63 // Get input stream if any
6418 var inputStream = command._inputs.filter(function(input) {
6518 return typeof input.source !== 'string';
66 })[0];
67
68 // Run ffmpeg
6918 var stdout = null;
7018 var stderr = '';
7118 command._spawnFfmpeg(
72 args,
73
74 { niceness: command.options.niceness },
75
76 function processCB(ffmpegProc) {
7718 command.ffmpegProc = ffmpegProc;
7818 command.emit('start', 'ffmpeg ' + args.join(' '));
79
80 // Pipe input stream if any
8118 if (inputStream) {
822 inputStream.source.on('error', function(err) {
830 emitEnd(new Error('Input stream error: ' + err.message));
840 ffmpegProc.kill();
85 });
86
872 inputStream.source.resume();
882 inputStream.source.pipe(ffmpegProc.stdin);
89 }
90
91 // Setup timeout if requested
9218 var processTimer;
9318 if (command.options.timeout) {
944 processTimer = setTimeout(function() {
953 var msg = 'process ran into a timeout (' + command.options.timeout + 's)';
96
973 emitEnd(new Error(msg), stdout, stderr);
983 ffmpegProc.kill();
99 }, command.options.timeout * 1000);
100 }
101
10218 if (isStream) {
103 // Pipe ffmpeg stdout to output stream
1043 ffmpegProc.stdout.pipe(target, pipeOptions);
105
106 // Handle output stream events
1073 target.on('close', function() {
1082 command.logger.debug('Output stream closed, scheduling kill for ffmpgeg process');
109
110 // Don't kill process yet, to give a chance to ffmpeg to
111 // terminate successfully first This is necessary because
112 // under load, the process 'exit' event sometimes happens
113 // after the output stream 'close' event.
1142 setTimeout(function() {
1152 emitEnd(new Error('Output stream closed'));
1162 ffmpegProc.kill();
117 }, 20);
118 });
119
1203 target.on('error', function(err) {
1210 command.logger.debug('Output stream error, killing ffmpgeg process');
1220 emitEnd(new Error('Output stream error: ' + err.message));
1230 ffmpegProc.kill();
124 });
125 } else {
126 // Gather ffmpeg stdout
12715 stdout = '';
12815 ffmpegProc.stdout.on('data', function (data) {
1290 stdout += data;
130 });
131 }
132
133 // Process ffmpeg stderr data
13418 command._codecDataSent = false;
13518 ffmpegProc.stderr.on('data', function (data) {
136324 stderr += data;
137
138324 if (!command._codecDataSent && command.listeners('codecData').length) {
13911 utils.extractCodecData(command, stderr);
140 }
141
142324 if (command.listeners('progress').length) {
14326 var duration = 0;
144
14526 if (command._ffprobeData && command._ffprobeData.format && command._ffprobeData.format.duration) {
14621 duration = Number(command._ffprobeData.format.duration);
147 }
148
14926 utils.extractProgress(command, stderr, duration);
150 }
151 });
152 },
153
154 function endCB(err) {
15518 delete command.ffmpegProc;
156
15718 if (err) {
1584 emitEnd(err, stdout, stderr);
159 } else {
16014 if (command.options.flvmeta) {
16111 command._getFlvtoolPath(function(err, flvtool) {
162 // No error possible here, _getFlvtoolPath was called by _prepare
163
16411 spawn(flvtool, ['-U', target])
165 .on('error', function(err) {
1660 emitEnd(new Error('Error running ' + flvtool + ': ' + err.message));
167 })
168 .on('exit', function(code, signal) {
16911 if (code !== 0 || signal) {
1700 emitEnd(
171 new Error(flvtool + ' ' +
172 (signal ? 'received signal ' + signal
173 : 'exited with code ' + code))
174 );
175 } else {
17611 emitEnd(null, stdout, stderr);
177 }
178 });
179 });
180 } else {
1813 emitEnd(null, stdout, stderr);
182 }
183 }
184 }
185 );
186 });
187}
188
189
190/**
191 * Run ffprobe asynchronously and store data in command
192 *
193 * @param {FfmpegCommand} command
194 * @private
195 */
196function runFfprobe(command) {
1971 command.ffprobe(function(err, data) {
1981 command._ffprobeData = data;
199 });
200}
201
202
2031module.exports = function(proto) {
204 /**
205 * Emitted just after ffmpeg has been spawned.
206 *
207 * @event FfmpegCommand#start
208 * @param {String} command ffmpeg command line
209 */
210
211 /**
212 * Emitted when ffmpeg reports progress information
213 *
214 * @event FfmpegCommand#progress
215 * @param {Object} progress progress object
216 */
217
218 /**
219 * Emitted when ffmpeg reports input codec data
220 *
221 * @event FfmpegCommand#codecData
222 * @param {Object} codecData codec data object
223 */
224
225 /**
226 * Emitted when an error happens when preparing or running a command
227 *
228 * @event FfmpegCommand#error
229 * @param {Error} error error
230 * @param {String|null} stdout ffmpeg stdout, unless outputting to a stream
231 * @param {String|null} stderr ffmpeg stderr
232 */
233
234 /**
235 * Emitted when a command finishes processing
236 *
237 * @event FfmpegCommand#end
238 * @param {Array|null} [filenames] generated filenames when taking screenshots, null otherwise
239 */
240
241
242 /**
243 * Spawn an ffmpeg process
244 *
245 * The 'options' argument may contain the following keys:
246 * - 'niceness': specify process niceness, ignored on Windows (default: 0)
247 * - 'captureStdout': capture stdout and pass it to 'endCB' as its 2nd argument (default: false)
248 * - 'captureStderr': capture stderr and pass it to 'endCB' as its 3rd argument (default: false)
249 *
250 * The 'processCB' callback, if present, is called as soon as the process is created and
251 * receives a nodejs ChildProcess object. It may not be called at all if an error happens
252 * before spawning the process.
253 *
254 * The 'endCB' callback is called either when an error occurs or when the ffmpeg process finishes.
255 *
256 * @method FfmpegCommand#_spawnFfmpeg
257 * @param {Array} args ffmpeg command line argument list
258 * @param {Object} [options] spawn options (see above)
259 * @param {Function} [processCB] callback called with process object when it has been created
260 * @param {Function} endCB callback with signature (err, stdout, stderr)
261 * @private
262 */
2631 proto._spawnFfmpeg = function(args, options, processCB, endCB) {
264 // Enable omitting options
26530 if (typeof options === 'function') {
2668 endCB = processCB;
2678 processCB = options;
2688 options = {};
269 }
270
271 // Enable omitting processCB
27230 if (typeof endCB === 'undefined') {
27312 endCB = processCB;
27412 processCB = function() {};
275 }
276
277 // Find ffmpeg
27830 this._getFfmpegPath(function(err, command) {
27930 if (err) {
2800 return endCB(err);
28130 } else if (!command || command.length === 0) {
2820 return endCB(new Error('Cannot find ffmpeg'));
283 }
284
285 // Apply niceness
28630 if (options.niceness && options.niceness !== 0 && !utils.isWindows) {
2870 args.unshift('-n', options.niceness, command);
2880 command = 'nice';
289 }
290
29130 var stdout = null;
29230 var stdoutClosed = false;
293
29430 var stderr = null;
29530 var stderrClosed = false;
296
297 // Spawn process
29830 var ffmpegProc = spawn(command, args, options);
299
30030 if (ffmpegProc.stderr && options.captureStderr) {
3011 ffmpegProc.stderr.setEncoding('utf8');
302 }
303
30430 ffmpegProc.on('error', function(err) {
3050 endCB(err);
306 });
307
308 // Ensure we wait for captured streams to end before calling endCB
30930 var exitError = null;
310 function handleExit(err) {
31135 if (err) {
3124 exitError = err;
313 }
314
31535 if (processExited &&
316 (stdoutClosed || !options.captureStdout) &&
317 (stderrClosed || !options.captureStderr)) {
31830 endCB(exitError, stdout, stderr);
319 }
320 }
321
322 // Handle process exit
32330 var processExited = false;
32430 ffmpegProc.on('exit', function(code, signal) {
32530 processExited = true;
326
32730 if (code) {
3283 handleExit(new Error('ffmpeg exited with code ' + code));
32927 } else if (signal) {
3301 handleExit(new Error('ffmpeg was killed with signal ' + signal));
331 } else {
33226 handleExit();
333 }
334 });
335
336 // Capture stdout if specified
33730 if (options.captureStdout) {
3384 stdout = '';
339
3404 ffmpegProc.stdout.on('data', function(data) {
34111 stdout += data;
342 });
343
3444 ffmpegProc.stdout.on('close', function() {
3454 stdoutClosed = true;
3464 handleExit();
347 });
348 }
349
350 // Capture stderr if specified
35130 if (options.captureStderr) {
3521 stderr = '';
353
3541 ffmpegProc.stderr.on('data', function(data) {
3550 stderr += data;
356 });
357
3581 ffmpegProc.stderr.on('close', function() {
3591 stderrClosed = true;
3601 handleExit();
361 });
362 }
363
364 // Call process callback
36530 processCB(ffmpegProc);
366 });
367 };
368
369
370 /**
371 * Build the argument list for an ffmpeg command
372 *
373 * @method FfmpegCommand#_getArguments
374 * @return argument list
375 * @private
376 */
3771 proto._getArguments = function() {
37853 var audioFilters = this._audioFilters.get();
37953 var videoFilters = this._videoFilters.get().concat(this._sizeFilters.get());
380
38153 return this._inputs.reduce(function(args, input) {
38254 var source = (typeof input.source === 'string') ? input.source : '-';
383
38454 return args.concat(
385 input.before.get(),
386 ['-i', source],
387 input.after.get()
388 );
389 }, [])
390 .concat(
391 this._audio.get(),
392 audioFilters.length ? ['-filter:a', audioFilters.join(',')] : [],
393 this._video.get(),
394 videoFilters.length ? ['-filter:v', videoFilters.join(',')] : [],
395 this._output.get()
396 );
397 };
398
399
400 /**
401 * Prepare execution of an ffmpeg command
402 *
403 * Checks prerequisites for the execution of the command (codec/format availability, flvtool...),
404 * then builds the argument list for ffmpeg and pass them to 'callback'.
405 *
406 * @method FfmpegCommand#_prepare
407 * @param {Function} callback callback with signature (err, args)
408 * @param {Boolean} [readMetadata=false] read metadata before processing
409 * @private
410 */
4111 proto._prepare = function(callback, readMetadata) {
41221 var self = this;
413
41421 async.waterfall([
415 // Check codecs and formats
416 function(cb) {
41721 self._checkCapabilities(cb);
418 },
419
420 // Read metadata if required
421 function(cb) {
42220 if (!readMetadata) {
42318 return cb();
424 }
425
4262 self.ffprobe(function(err, data) {
4272 if (!err) {
4282 self._ffprobeData = data;
429 }
430
4312 cb();
432 });
433 },
434
435 // Check for flvtool2/flvmeta if necessary
436 function(cb) {
43720 if (self.options.flvmeta) {
43818 self._getFlvtoolPath(function(err) {
43918 cb(err);
440 });
441 } else {
4422 cb();
443 }
444 },
445
446 // Build argument list
447 function(cb) {
44820 var args;
44920 try {
45020 args = self._getArguments();
451 } catch(e) {
4520 return cb(e);
453 }
454
45520 cb(null, args);
456 }
457 ], callback);
458
45921 if (!readMetadata) {
460 // Read metadata as soon as 'progress' listeners are added
461
46219 if (this.listeners('progress').length > 0) {
463 // Read metadata in parallel
4641 runFfprobe(this);
465 } else {
466 // Read metadata as soon as the first 'progress' listener is added
46718 this.once('newListener', function(event) {
4680 if (event === 'progress') {
4690 runFfprobe(this);
470 }
471 });
472 }
473 }
474 };
475
476
477 /**
478 * Execute ffmpeg command and save output to a file
479 *
480 * @method FfmpegCommand#save
481 * @category Processing
482 * @aliases saveToFile
483 *
484 * @param {String} output file path
485 * @return FfmpegCommand
486 */
4871 proto.saveToFile =
488 proto.save = function(output) {
48916 _process(this, output);
490 };
491
492
493 /**
494 * Execute ffmpeg command and save output to a stream
495 *
496 * If 'stream' is not specified, a PassThrough stream is created and returned.
497 * 'options' will be used when piping ffmpeg output to the output stream
498 * (@see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options)
499 *
500 * @method FfmpegCommand#pipe
501 * @category Processing
502 * @aliases stream,writeToStream
503 *
504 * @param {stream.Writable} [stream] output stream
505 * @param {Object} [options={}] pipe options
506 * @return Output stream
507 */
5081 proto.writeToStream =
509 proto.pipe =
510 proto.stream = function(stream, options) {
5113 if (stream && !('writable' in stream)) {
5121 options = stream;
5131 stream = undefined;
514 }
515
5163 if (!stream) {
5171 if (process.version.match(/v0\.8\./)) {
5180 throw new Error('PassThrough stream is not supported on node v0.8');
519 }
520
5211 stream = new PassThrough();
522 }
523
5243 _process(this, stream, options);
5253 return stream;
526 };
527
528
529 /**
530 * Merge (concatenate) inputs to a single file
531 *
532 * Warning: soon to be deprecated
533 *
534 * @method FfmpegCommand#mergeToFile
535 * @category Processing
536 *
537 * @param {String} targetfile output file path
538 */
5391 proto.mergeToFile = function(targetfile) {
5401 var outputfile = path.normalize(targetfile);
5411 if(fs.existsSync(outputfile)){
5420 return this.emit('error', new Error('Output file already exists, merge aborted'));
543 }
544
5451 var self = this;
546
547 // creates intermediate copies of each video.
548 function makeIntermediateFile(_mergeSource,_callback) {
5493 var fname = _mergeSource + '.temp.mpg';
5503 var args = self._output.get().concat(['-i', _mergeSource, '-qscale:v', 1, fname]);
551
5523 self._spawnFfmpeg(args, function(err) {
5533 _callback(err, fname);
554 });
555 }
556
557 // concat all created intermediate copies
558 function concatIntermediates(target, intermediatesList, _callback) {
5591 var fname = path.normalize(target) + '.temp.merged.mpg';
560
5611 var args = [
562 // avoid too many log messages from ffmpeg
563 '-loglevel', 'panic',
564 '-i', 'concat:' + intermediatesList.join('|'),
565 '-c', 'copy',
566 fname
567 ];
568
5691 self._spawnFfmpeg(args, {captureStdout:true,captureStderr:true}, function(err) {
5701 _callback(err, fname);
571 });
572 }
573
574 function quantizeConcat(concatResult, numFiles, _callback) {
5751 var args = [
576 '-i', concatResult,
577 '-qscale:v',numFiles,
578 targetfile
579 ];
580
5811 self._spawnFfmpeg(args, function(err) {
5821 _callback(err);
583 });
584 }
585
586 function deleteIntermediateFiles(intermediates, callback) {
5872 async.each(intermediates, function(item,cb){
5888 fs.exists(item,function(exists){
5898 if(exists){
5904 fs.unlink(item ,cb);
591 }
592 else{
5934 cb();
594 }
595
596 });
597 }, callback);
598 }
599
600 function makeProgress() {
6015 progress.createdFiles = progress.createdFiles + 1;
6025 progress.percent = progress.createdFiles / progress.totalFiles * 100;
6035 self.emit('progress', progress);
604 }
605
6061 if (this._inputs.length < 2) {
6070 return this.emit('error', new Error('No file added to be merged'));
608 }
609
6104 var mergeList = this._inputs.map(function(input) { return input.source; });
611
6121 var progress = {frames : 0,
613 currentFps: 0,
614 currentKbps: 0,
615 targetSize: 0,
616 timemark: 0,
617 percent: 0,
618 totalFiles: mergeList.length + 2,
619 createdFiles: 0};
620
6214 var toDelete = mergeList.map(function(name) { return name + '.temp.mpg'; });
6221 toDelete.push(outputfile + '.temp.merged.mpg');
6231 deleteIntermediateFiles(toDelete);
624
6251 var intermediateFiles = [];
626
6271 async.whilst(
628 function(){
6294 return (mergeList.length !== 0);
630 },
631 function (callback){
6323 makeIntermediateFile(mergeList.shift(), function(err, createdIntermediateFile) {
6333 if(err) {
6340 return callback(err);
635 }
636
6373 if(!createdIntermediateFile) {
6380 return callback(new Error('Invalid intermediate file'));
639 }
640
6413 intermediateFiles.push(createdIntermediateFile);
6423 makeProgress();
6433 callback();
644 });
645 },
646 function(err) {
6471 if (err) {
6480 return self.emit('error', err);
649 }
650
6511 concatIntermediates(targetfile, intermediateFiles, function(err, concatResult) {
6521 if(err) {
6530 return self.emit('error', err);
654 }
655
6561 if(!concatResult) {
6570 return self.emit('error', new Error('Invalid concat result file'));
658 }
659
6601 makeProgress();
6611 quantizeConcat(concatResult, intermediateFiles.length, function() {
6621 makeProgress();
663 // add concatResult to intermediates list so it can be deleted too.
6641 intermediateFiles.push(concatResult);
6651 deleteIntermediateFiles(intermediateFiles, function(err) {
6661 if (err) {
6670 self.emit('error', err);
668 } else {
6691 self.emit('end');
670 }
671 });
672 });
673 });
674 }
675 );
676 };
677
678
679 /**
680 * Take screenshots
681 *
682 * The 'config' parameter may either be the number of screenshots to take or an object
683 * with the following keys:
684 * - 'count': screenshot count
685 * - 'timemarks': array of screenshot timestamps in seconds (defaults to taking screenshots at regular intervals)
686 * - 'filename': screenshot filename pattern (defaults to 'tn_%ss' or 'tn_%ss_%i' for multiple screenshots)
687 *
688 * The 'filename' option may contain tokens that will be replaced for each screenshot taken:
689 * - '%s': offset in seconds
690 * - '%w': screenshot width
691 * - '%h': screenshot height
692 * - '%r': screenshot resolution (eg. '320x240')
693 * - '%f': input filename
694 * - '%b': input basename (filename w/o extension)
695 * - '%i': index of screenshot in timemark array (can be zero-padded by using it like `%000i`)
696 *
697 * @method FfmpegCommand#takeScreenshots
698 * @category Processing
699 *
700 * @param {Number|Object} config screenshot count or configuration object (see above)
701 * @param {String} [folder='.'] output directory
702 */
7031 proto.takeScreenshots = function(config, folder) {
7042 var width, height;
7052 var self = this;
706
707 function _computeSize(size) {
708 // Select video stream with biggest resolution
7092 var vstream = self._ffprobeData.streams.reduce(function(max, stream) {
7102 if (stream.codec_type !== 'video') return max;
7112 return max.width * max.height < stream.width * stream.height ? stream : max;
712 }, { width: 0, height: 0 });
713
7142 var w = vstream.width;
7152 var h = vstream.height;
7162 var a = w / h;
717
7182 var fixedSize = size.match(/([0-9]+)x([0-9]+)/);
7192 var fixedWidth = size.match(/([0-9]+)x\?/);
7202 var fixedHeight = size.match(/\?x([0-9]+)/);
7212 var percentRatio = size.match(/\b([0-9]{1,3})%/);
722
7232 if (fixedSize) {
7240 width = Number(fixedSize[1]);
7250 height = Number(fixedSize[2]);
7262 } else if (fixedWidth) {
7272 width = Number(fixedWidth[1]);
7282 height = width / a;
7290 } else if (fixedHeight) {
7300 height = Number(fixedHeight[1]);
7310 width = height * a;
732 } else {
7330 var pc = Number(percentRatio[0]) / 100;
7340 width = w * pc;
7350 height = h * pc;
736 }
737 }
738
739 function _zeroPad(number, len) {
7404 len = len-String(number).length+2;
7414 return new Array(len<0?0:len).join('0')+number;
742 }
743
744 function _renderOutputName(j, offset) {
7454 var result = filename;
7464 if(/%0*i/.test(result)) {
7474 var numlen = String(result.match(/%(0*)i/)[1]).length;
7484 result = result.replace(/%0*i/, _zeroPad(j, numlen));
749 }
7504 result = result.replace('%s', offset);
7514 result = result.replace('%w', width);
7524 result = result.replace('%h', height);
7534 result = result.replace('%r', width+'x'+height);
7544 result = result.replace('%f', path.basename(inputfile));
7554 result = result.replace('%b', path.basename(inputfile, path.extname(inputfile)));
7564 return result;
757 }
758
759 function _screenShotInternal() {
7602 self._prepare(function(err, args) {
7612 if(err) {
7620 return self.emit('error', err);
763 }
764
7652 _computeSize(self._sizeData.size);
766
7672 var duration = 0;
7682 if (self._ffprobeData && self._ffprobeData.format && self._ffprobeData.format.duration) {
7692 duration = Number(self._ffprobeData.format.duration);
770 }
771
7722 if (!duration) {
7730 var errString = 'meta data contains no duration, aborting screenshot creation';
7740 return self.emit('error', new Error(errString));
775 }
776
777 // check if all timemarks are inside duration
7782 if (Array.isArray(timemarks)) {
7792 for (var i = 0; i < timemarks.length; i++) {
780 /* convert percentage to seconds */
7814 if( timemarks[i].indexOf('%') > 0 ) {
7820 timemarks[i] = (parseInt(timemarks[i], 10) / 100) * duration;
783 }
7844 if (parseInt(timemarks[i], 10) > duration) {
785 // remove timemark from array
7860 timemarks.splice(i, 1);
7870 --i;
788 }
789 }
790 // if there are no more timemarks around, add one at end of the file
7912 if (timemarks.length === 0) {
7920 timemarks[0] = (duration * 0.9);
793 }
794 }
795 // get positions for screenshots (using duration of file minus 10% to remove fade-in/fade-out)
7962 var secondOffset = (duration * 0.9) / screenshotcount;
797
798 // reset iterator
7992 var j = 1;
800
8012 var filenames = [];
802
803 // use async helper function to generate all screenshots and
804 // fire callback just once after work is done
8052 async.until(
806 function() {
8076 return j > screenshotcount;
808 },
809 function(taskcallback) {
8104 var offset;
8114 if (Array.isArray(timemarks)) {
812 // get timemark for current iteration
8134 offset = timemarks[(j - 1)];
814 } else {
8150 offset = secondOffset * j;
816 }
817
8184 var fname = _renderOutputName(j, offset) + (fileextension ? fileextension : '.jpg');
8194 var target = path.join(folder, fname);
820
821 // build screenshot command
8224 var allArgs = [
823 '-ss', Math.floor(offset * 100) / 100
824 ]
825 .concat(args)
826 .concat([
827 '-vframes', '1',
828 '-an',
829 '-vcodec', 'mjpeg',
830 '-f', 'rawvideo',
831 '-y', target
832 ]);
833
8344 j++;
835
8364 self._spawnFfmpeg(allArgs, taskcallback);
8374 filenames.push(fname);
838 },
839 function(err) {
8402 if (err) {
8410 self.emit('error', err);
842 } else {
8432 self.emit('end', filenames);
844 }
845 }
846 );
847 }, true);
848 }
849
8502 var timemarks, screenshotcount, filename, fileextension;
8512 if (typeof config === 'object') {
852 // use json object as config
8532 if (config.count) {
8542 screenshotcount = config.count;
855 }
8562 if (config.timemarks) {
8572 timemarks = config.timemarks;
858 }
8592 if (config.fileextension){
8600 fileextension = config.fileextension;
861 }
862 } else {
863 // assume screenshot count as parameter
8640 screenshotcount = config;
8650 timemarks = null;
866 }
867
8682 if (!this._sizeData || !this._sizeData.size) {
8690 throw new Error('Size must be specified');
870 }
871
8722 var inputfile = this._currentInput.source;
873
8742 filename = config.filename || 'tn_%ss';
8752 if(!/%0*i/.test(filename) && Array.isArray(timemarks) && timemarks.length > 1 ) {
876 // if there are multiple timemarks but no %i in filename add one
877 // so we won't overwrite the same thumbnail with each timemark
8781 filename += '_%i';
879 }
8802 folder = folder || '.';
881
882 // check target folder
8832 fs.exists(folder, function(exists) {
8842 if (!exists) {
8852 fs.mkdir(folder, '0755', function(err) {
8862 if (err !== null) {
8870 self.emit('error', err);
888 } else {
8892 _screenShotInternal();
890 }
891 });
892 } else {
8930 _screenShotInternal();
894 }
895 });
896 };
897
898
899 /**
900 * Renice current and/or future ffmpeg processes
901 *
902 * Ignored on Windows platforms.
903 *
904 * @method FfmpegCommand#renice
905 * @category Processing
906 *
907 * @param {Number} [niceness=0] niceness value between -20 (highest priority) and 20 (lowest priority)
908 * @return FfmpegCommand
909 */
9101 proto.renice = function(niceness) {
9112 if (!utils.isWindows) {
9122 niceness = niceness || 0;
913
9142 if (niceness < -20 || niceness > 20) {
9151 this.logger.warn('Invalid niceness value: ' + niceness + ', must be between -20 and 20');
916 }
917
9182 niceness = Math.min(20, Math.max(-20, niceness));
9192 this.options.niceness = niceness;
920
9212 if (this.ffmpegProc) {
9221 var logger = this.logger;
9231 var pid = this.ffmpegProc.pid;
9241 var renice = spawn('renice', [niceness, '-p', pid]);
925
9261 renice.on('error', function(err) {
9270 logger.warn('could not renice process ' + pid + ': ' + err.message);
928 });
929
9301 renice.on('exit', function(code, signal) {
9311 if (code) {
9320 logger.warn('could not renice process ' + pid + ': renice exited with ' + code);
9331 } else if (signal) {
9340 logger.warn('could not renice process ' + pid + ': renice was killed by signal ' + signal);
935 } else {
9361 logger.info('successfully reniced process ' + pid + ' to ' + niceness + ' niceness');
937 }
938 });
939 }
940 }
941
9422 return this;
943 };
944
945
946 /**
947 * Kill current ffmpeg process, if any
948 *
949 * @method FfmpegCommand#kill
950 * @category Processing
951 *
952 * @param {String} [signal=SIGKILL] signal name
953 * @return FfmpegCommand
954 */
9551 proto.kill = function(signal) {
9563 if (!this.ffmpegProc) {
9570 this.options.logger.warn('No running ffmpeg process, cannot send signal');
958 } else {
9593 this.ffmpegProc.kill(signal || 'SIGKILL');
960 }
961
9623 return this;
963 };
964};
965

lib/utils.js

92%
82
76
6
LineHitsSource
1/*jshint node:true*/
2'use strict';
3
41var exec = require('child_process').exec;
51var isWindows = require('os').platform().match(/win(32|64)/);
6
71var whichCache = {};
8
9/**
10 * Parse progress line from ffmpeg stderr
11 *
12 * @param {String} line progress line
13 * @return progress object
14 * @private
15 */
16function parseProgressLine(line) {
1726 var progress = {};
18
19 // Remove all spaces after = and trim
2026 line = line.replace(/=\s+/g, '=').trim();
2126 var progressParts = line.split(' ');
22
23 // Split every progress part by "=" to get key and value
2426 for(var i = 0; i < progressParts.length; i++) {
25110 var progressSplit = progressParts[i].split('=', 2);
26110 var key = progressSplit[0];
27110 var value = progressSplit[1];
28
29 // This is not a progress line
30110 if(typeof value === 'undefined')
3114 return null;
32
3396 progress[key] = value;
34 }
35
3612 return progress;
37}
38
39
401var utils = module.exports = {
41 isWindows: isWindows,
42
43 /**
44 * Create an argument list
45 *
46 * Returns a function that adds new arguments to the list.
47 * It also has the following methods:
48 * - clear() empties the argument list
49 * - get() returns the argument list
50 * - find(arg, count) finds 'arg' in the list and return the following 'count' items, or undefined if not found
51 * - remove(arg, count) remove 'arg' in the list as well as the following 'count' items
52 *
53 * @private
54 */
55 args: function() {
561434 var list = [];
571434 var argfunc = function() {
58326 if (arguments.length === 1 && Array.isArray(arguments[0])) {
5990 list = list.concat(arguments[0]);
60 } else {
61236 list = list.concat([].slice.call(arguments));
62 }
63 };
64
651434 argfunc.clear = function() {
6683 list = [];
67 };
68
691434 argfunc.get = function() {
70485 return list;
71 };
72
731434 argfunc.find = function(arg, count) {
7495 var index = list.indexOf(arg);
7595 if (index !== -1) {
7669 return list.slice(index + 1, index + 1 + (count || 0));
77 }
78 };
79
801434 argfunc.remove = function(arg, count) {
810 var index = list.indexOf(arg);
820 if (index !== -1) {
830 list.splice(index, (count || 0) + 1);
84 }
85 };
86
871434 return argfunc;
88 },
89
90
91 /**
92 * Search for an executable
93 *
94 * Uses 'which' or 'where' depending on platform
95 *
96 * @param {String} name executable name
97 * @param {Function} callback callback with signature (err, path)
98 * @private
99 */
100 which: function(name, callback) {
1019 if (name in whichCache) {
1026 return callback(null, whichCache[name]);
103 }
104
1053 var cmd = 'which ' + name;
1063 if (isWindows) {
1070 cmd = 'where ' + name + '.exe';
108 }
109
1103 exec(cmd, function(err, stdout) {
1113 if (err) {
112 // Treat errors as not found
1130 callback(null, whichCache[name] = '');
114 } else {
1153 callback(null, whichCache[name] = stdout.replace(/\n$/, ''));
116 }
117 });
118 },
119
120
121 /**
122 * Convert a [[hh:]mm:]ss[.xxx] timemark into seconds
123 *
124 * @param {String} timemark timemark string
125 * @return Number
126 * @private
127 */
128 timemarkToSeconds: function(timemark) {
12912 if(timemark.indexOf(':') === -1 && timemark.indexOf('.') >= 0)
1300 return Number(timemark);
131
13212 var parts = timemark.split(':');
133
134 // add seconds
13512 var secs = Number(parts.pop());
136
13712 if (parts.length) {
138 // add minutes
13912 secs += Number(parts.pop()) * 60;
140 }
141
14212 if (parts.length) {
143 // add hours
14412 secs += Number(parts.pop()) * 3600;
145 }
146
14712 return secs;
148 },
149
150
151 /**
152 * Extract codec data from ffmpeg stderr and emit 'codecData' event if appropriate
153 *
154 * @param {FfmpegCommand} command event emitter
155 * @param {String} stderr ffmpeg stderr output
156 * @private
157 */
158 extractCodecData: function(command, stderr) {
15911 var format= /Input #[0-9]+, ([^ ]+),/.exec(stderr);
16011 var dur = /Duration\: ([^,]+)/.exec(stderr);
16111 var audio = /Audio\: (.*)/.exec(stderr);
16211 var video = /Video\: (.*)/.exec(stderr);
16311 var codecObject = { format: '', audio: '', video: '', duration: '' };
164
16511 if (format && format.length > 1) {
1668 codecObject.format = format[1];
167 }
168
16911 if (dur && dur.length > 1) {
1708 codecObject.duration = dur[1];
171 }
172
17311 if (audio && audio.length > 1) {
1747 audio = audio[1].split(', ');
1757 codecObject.audio = audio[0];
1767 codecObject.audio_details = audio;
177 }
17811 if (video && video.length > 1) {
1797 video = video[1].split(', ');
1807 codecObject.video = video[0];
1817 codecObject.video_details = video;
182 }
183
18411 var codecInfoPassed = /Press (\[q\]|ctrl-c) to stop/.test(stderr);
18511 if (codecInfoPassed) {
1861 command.emit('codecData', codecObject);
1871 command._codecDataSent = true;
188 }
189 },
190
191
192 /**
193 * Extract progress data from ffmpeg stderr and emit 'progress' event if appropriate
194 *
195 * @param {FfmpegCommand} command event emitter
196 * @param {Number} [duration=0] expected output duration in seconds
197 */
198 extractProgress: function(command, stderr, duration) {
19926 var lines = stderr.split(/\r\n|\r|\n/g);
20026 var lastline = lines[lines.length - 2];
20126 var progress;
202
20326 if (lastline) {
20426 progress = parseProgressLine(lastline);
205 }
206
20726 if (progress) {
208 // build progress report object
20912 var ret = {
210 frames: parseInt(progress.frame, 10),
211 currentFps: parseInt(progress.fps, 10),
212 currentKbps: parseFloat(progress.bitrate.replace('kbits/s', '')),
213 targetSize: parseInt(progress.size, 10),
214 timemark: progress.time
215 };
216
217 // calculate percent progress using duration
21812 if (duration && duration > 0) {
21912 ret.percent = (utils.timemarkToSeconds(ret.timemark) / duration) * 100;
220 }
221
22212 command.emit('progress', ret);
223 }
224 }
225};
226
make[1]: quittant le répertoire « /home/niko/dev/forks/node-fluent-ffmpeg »