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
lib/capabilities.js
| Line | Hits | Source |
|---|
| 1 | | /*jshint node:true*/ |
| 2 | | 'use strict'; |
| 3 | | |
| 4 | 1 | var fs = require('fs'); |
| 5 | 1 | var path = require('path'); |
| 6 | 1 | var async = require('async'); |
| 7 | 1 | var utils = require('./utils'); |
| 8 | | |
| 9 | | /* |
| 10 | | *! Capability helpers |
| 11 | | */ |
| 12 | | |
| 13 | 1 | var avCodecRegexp = /^\s*([D ])([E ])([VAS])([S ])([D ])([T ]) ([^ ]+) +(.*)$/; |
| 14 | 1 | var ffCodecRegexp = /^\s*([D\.])([E\.])([VAS])([I\.])([L\.])([S\.]) ([^ ]+) +(.*)$/; |
| 15 | 1 | var ffEncodersRegexp = /\(encoders:([^\)]+)\)/; |
| 16 | 1 | var ffDecodersRegexp = /\(decoders:([^\)]+)\)/; |
| 17 | 1 | var formatRegexp = /^\s*([D ])([E ]) ([^ ]+) +(.*)$/; |
| 18 | 1 | var lineBreakRegexp = /\r\n|\r|\n/; |
| 19 | 1 | var filterRegexp = /^(?: [T\.][S\.][C\.] )?([^ ]+) +(AA?|VV?|\|)->(AA?|VV?|\|) +(.*)$/; |
| 20 | | |
| 21 | 1 | var cache = {}; |
| 22 | | |
| 23 | | function copy(src, dest) { |
| 24 | 125 | Object.keys(src).forEach(function(k) { |
| 25 | 741 | dest[k] = src[k]; |
| 26 | | }); |
| 27 | | } |
| 28 | | |
| 29 | 1 | module.exports = function(proto) { |
| 30 | | /** |
| 31 | | * Forget executable paths |
| 32 | | * |
| 33 | | * (only used for testing purposes) |
| 34 | | * |
| 35 | | * @method FfmpegCommand#_forgetPaths |
| 36 | | * @private |
| 37 | | */ |
| 38 | 1 | proto._forgetPaths = function() { |
| 39 | 8 | delete cache.ffmpegPath; |
| 40 | 8 | delete cache.ffprobePath; |
| 41 | 8 | 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 | | */ |
| 55 | 1 | proto._getFfmpegPath = function(callback) { |
| 56 | 35 | if ('ffmpegPath' in cache) { |
| 57 | 29 | return callback(null, cache.ffmpegPath); |
| 58 | | } |
| 59 | | |
| 60 | 6 | async.waterfall([ |
| 61 | | // Try FFMPEG_PATH |
| 62 | | function(cb) { |
| 63 | 6 | if (process.env.FFMPEG_PATH) { |
| 64 | 3 | fs.exists(process.env.FFMPEG_PATH, function(exists) { |
| 65 | 3 | if (exists) { |
| 66 | 1 | cb(null, process.env.FFMPEG_PATH); |
| 67 | | } else { |
| 68 | 2 | cb(null, ''); |
| 69 | | } |
| 70 | | }); |
| 71 | | } else { |
| 72 | 3 | cb(null, ''); |
| 73 | | } |
| 74 | | }, |
| 75 | | |
| 76 | | // Search in the PATH |
| 77 | | function(ffmpeg, cb) { |
| 78 | 6 | if (ffmpeg.length) { |
| 79 | 1 | return cb(null, ffmpeg); |
| 80 | | } |
| 81 | | |
| 82 | 5 | utils.which('ffmpeg', function(err, ffmpeg) { |
| 83 | 5 | cb(err, ffmpeg); |
| 84 | | }); |
| 85 | | } |
| 86 | | ], function(err, ffmpeg) { |
| 87 | 6 | if (err) { |
| 88 | 0 | callback(err); |
| 89 | | } else { |
| 90 | 6 | 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 | | */ |
| 107 | 1 | proto._getFfprobePath = function(callback) { |
| 108 | 13 | if ('ffprobePath' in cache) { |
| 109 | 9 | return callback(null, cache.ffprobePath); |
| 110 | | } |
| 111 | | |
| 112 | 4 | var self = this; |
| 113 | 4 | async.waterfall([ |
| 114 | | // Try FFPROBE_PATH |
| 115 | | function(cb) { |
| 116 | 4 | if (process.env.FFPROBE_PATH) { |
| 117 | 2 | fs.exists(process.env.FFPROBE_PATH, function(exists) { |
| 118 | 2 | cb(null, exists ? process.env.FFPROBE_PATH : ''); |
| 119 | | }); |
| 120 | | } else { |
| 121 | 2 | cb(null, ''); |
| 122 | | } |
| 123 | | }, |
| 124 | | |
| 125 | | // Search in the PATH |
| 126 | | function(ffprobe, cb) { |
| 127 | 4 | if (ffprobe.length) { |
| 128 | 1 | return cb(null, ffprobe); |
| 129 | | } |
| 130 | | |
| 131 | 3 | utils.which('ffprobe', function(err, ffprobe) { |
| 132 | 3 | cb(err, ffprobe); |
| 133 | | }); |
| 134 | | }, |
| 135 | | |
| 136 | | // Search in the same directory as ffmpeg |
| 137 | | function(ffprobe, cb) { |
| 138 | 4 | if (ffprobe.length) { |
| 139 | 4 | return cb(null, ffprobe); |
| 140 | | } |
| 141 | | |
| 142 | 0 | self._getFfmpegPath(function(err, ffmpeg) { |
| 143 | 0 | if (err) { |
| 144 | 0 | cb(err); |
| 145 | 0 | } else if (ffmpeg.length) { |
| 146 | 0 | var name = utils.isWindows ? 'ffprobe.exe' : 'ffprobe'; |
| 147 | 0 | var ffprobe = path.join(path.dirname(ffmpeg), name); |
| 148 | 0 | fs.exists(ffprobe, function(exists) { |
| 149 | 0 | cb(null, exists ? ffprobe : ''); |
| 150 | | }); |
| 151 | | } else { |
| 152 | 0 | cb(null, ''); |
| 153 | | } |
| 154 | | }); |
| 155 | | } |
| 156 | | ], function(err, ffprobe) { |
| 157 | 4 | if (err) { |
| 158 | 0 | callback(err); |
| 159 | | } else { |
| 160 | 4 | 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 | | */ |
| 176 | 1 | proto._getFlvtoolPath = function(callback) { |
| 177 | 29 | if ('flvtoolPath' in cache) { |
| 178 | 28 | return callback(null, cache.flvtoolPath); |
| 179 | | } |
| 180 | | |
| 181 | 1 | async.waterfall([ |
| 182 | | // Try FLVMETA_PATH |
| 183 | | function(cb) { |
| 184 | 1 | if (process.env.FLVMETA_PATH) { |
| 185 | 0 | fs.exists(process.env.FLVMETA_PATH, function(exists) { |
| 186 | 0 | cb(null, exists ? process.env.FLVMETA_PATH : ''); |
| 187 | | }); |
| 188 | | } else { |
| 189 | 1 | cb(null, ''); |
| 190 | | } |
| 191 | | }, |
| 192 | | |
| 193 | | // Try FLVTOOL2_PATH |
| 194 | | function(flvtool, cb) { |
| 195 | 1 | if (flvtool.length) { |
| 196 | 0 | return cb(null, flvtool); |
| 197 | | } |
| 198 | | |
| 199 | 1 | if (process.env.FLVTOOL2_PATH) { |
| 200 | 0 | fs.exists(process.env.FLVTOOL2_PATH, function(exists) { |
| 201 | 0 | cb(null, exists ? process.env.FLVTOOL2_PATH : ''); |
| 202 | | }); |
| 203 | | } else { |
| 204 | 1 | cb(null, ''); |
| 205 | | } |
| 206 | | }, |
| 207 | | |
| 208 | | // Search for flvmeta in the PATH |
| 209 | | function(flvtool, cb) { |
| 210 | 1 | if (flvtool.length) { |
| 211 | 0 | return cb(null, flvtool); |
| 212 | | } |
| 213 | | |
| 214 | 1 | utils.which('flvmeta', function(err, flvmeta) { |
| 215 | 1 | cb(err, flvmeta); |
| 216 | | }); |
| 217 | | }, |
| 218 | | |
| 219 | | // Search for flvtool2 in the PATH |
| 220 | | function(flvtool, cb) { |
| 221 | 1 | if (flvtool.length) { |
| 222 | 1 | return cb(null, flvtool); |
| 223 | | } |
| 224 | | |
| 225 | 0 | utils.which('flvtool2', function(err, flvtool2) { |
| 226 | 0 | cb(err, flvtool2); |
| 227 | | }); |
| 228 | | }, |
| 229 | | ], function(err, flvtool) { |
| 230 | 1 | if (err) { |
| 231 | 0 | callback(err); |
| 232 | | } else { |
| 233 | 1 | 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 | | */ |
| 257 | 1 | proto.availableFilters = |
| 258 | | proto.getAvailableFilters = function(callback) { |
| 259 | 2 | if ('filters' in cache) { |
| 260 | 1 | return callback(null, cache.filters); |
| 261 | | } |
| 262 | | |
| 263 | 1 | this._spawnFfmpeg(['-filters'], { captureStdout: true }, function (err, stdout) { |
| 264 | 1 | if (err) { |
| 265 | 0 | return callback(err); |
| 266 | | } |
| 267 | | |
| 268 | 1 | var lines = stdout.split('\n'); |
| 269 | 1 | var data = {}; |
| 270 | 1 | var types = { A: 'audio', V: 'video', '|': 'none' }; |
| 271 | | |
| 272 | 1 | lines.forEach(function(line) { |
| 273 | 137 | var match = line.match(filterRegexp); |
| 274 | 137 | if (match) { |
| 275 | 135 | 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 | | |
| 285 | 1 | 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 | | */ |
| 308 | 1 | proto.availableCodecs = |
| 309 | | proto.getAvailableCodecs = function(callback) { |
| 310 | 25 | if ('codecs' in cache) { |
| 311 | 24 | return callback(null, cache.codecs); |
| 312 | | } |
| 313 | | |
| 314 | 1 | this._spawnFfmpeg(['-codecs'], { captureStdout: true }, function(err, stdout) { |
| 315 | 1 | if (err) { |
| 316 | 0 | return callback(err); |
| 317 | | } |
| 318 | | |
| 319 | 1 | var lines = stdout.split(lineBreakRegexp); |
| 320 | 1 | var data = {}; |
| 321 | | |
| 322 | 1 | lines.forEach(function(line) { |
| 323 | 369 | var match = line.match(avCodecRegexp); |
| 324 | 369 | if (match && match[7] !== '=') { |
| 325 | 0 | 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 | | |
| 336 | 369 | match = line.match(ffCodecRegexp); |
| 337 | 369 | if (match && match[7] !== '=') { |
| 338 | 357 | 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 | | |
| 348 | 357 | var encoders = codecData.description.match(ffEncodersRegexp); |
| 349 | 357 | encoders = encoders ? encoders[1].trim().split(' ') : []; |
| 350 | | |
| 351 | 357 | var decoders = codecData.description.match(ffDecodersRegexp); |
| 352 | 357 | decoders = decoders ? decoders[1].trim().split(' ') : []; |
| 353 | | |
| 354 | 357 | if (encoders.length || decoders.length) { |
| 355 | 58 | var coderData = {}; |
| 356 | 58 | copy(codecData, coderData); |
| 357 | 58 | delete coderData.canEncode; |
| 358 | 58 | delete coderData.canDecode; |
| 359 | | |
| 360 | 58 | encoders.forEach(function(name) { |
| 361 | 32 | data[name] = {}; |
| 362 | 32 | copy(coderData, data[name]); |
| 363 | 32 | data[name].canEncode = true; |
| 364 | | }); |
| 365 | | |
| 366 | 58 | decoders.forEach(function(name) { |
| 367 | 73 | if (name in data) { |
| 368 | 38 | data[name].canDecode = true; |
| 369 | | } else { |
| 370 | 35 | data[name] = {}; |
| 371 | 35 | copy(coderData, data[name]); |
| 372 | 35 | data[name].canDecode = true; |
| 373 | | } |
| 374 | | }); |
| 375 | | } |
| 376 | | } |
| 377 | | }); |
| 378 | | |
| 379 | 1 | 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 | | */ |
| 400 | 1 | proto.availableFormats = |
| 401 | | proto.getAvailableFormats = function(callback) { |
| 402 | 28 | if ('formats' in cache) { |
| 403 | 27 | return callback(null, cache.formats); |
| 404 | | } |
| 405 | | |
| 406 | | // Run ffmpeg -formats |
| 407 | 1 | this._spawnFfmpeg(['-formats'], { captureStdout: true }, function (err, stdout) { |
| 408 | 1 | if (err) { |
| 409 | 0 | return callback(err); |
| 410 | | } |
| 411 | | |
| 412 | | // Parse output |
| 413 | 1 | var lines = stdout.split(lineBreakRegexp); |
| 414 | 1 | var data = {}; |
| 415 | | |
| 416 | 1 | lines.forEach(function(line) { |
| 417 | 252 | var match = line.match(formatRegexp); |
| 418 | 252 | if (match) { |
| 419 | 247 | data[match[3]] = { |
| 420 | | description: match[4], |
| 421 | | canDemux: match[1] === 'D', |
| 422 | | canMux: match[2] === 'E' |
| 423 | | }; |
| 424 | | } |
| 425 | | }); |
| 426 | | |
| 427 | 1 | 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 | | */ |
| 441 | 1 | proto._checkCapabilities = function(callback) { |
| 442 | 26 | var self = this; |
| 443 | 26 | async.waterfall([ |
| 444 | | // Get available formats |
| 445 | | function(cb) { |
| 446 | 26 | self.availableFormats(cb); |
| 447 | | }, |
| 448 | | |
| 449 | | // Check whether specified formats are available |
| 450 | | function(formats, cb) { |
| 451 | | // Output format |
| 452 | 26 | var format = self._output.find('-f', 1); |
| 453 | | |
| 454 | 26 | if (format) { |
| 455 | 24 | if (!(format[0] in formats) || !(formats[format[0]].canMux)) { |
| 456 | 2 | return cb(new Error('Output format ' + format[0] + ' is not available')); |
| 457 | | } |
| 458 | | } |
| 459 | | |
| 460 | | // Input format(s) |
| 461 | 24 | var unavailable = self._inputs.reduce(function(fmts, input) { |
| 462 | 24 | var format = input.before.find('-f', 1); |
| 463 | 24 | if (format) { |
| 464 | 4 | if (!(format[0] in formats) || !(formats[format[0]].canDemux)) { |
| 465 | 1 | fmts.push(format[0]); |
| 466 | | } |
| 467 | | } |
| 468 | | |
| 469 | 24 | return fmts; |
| 470 | | }, []); |
| 471 | | |
| 472 | 24 | if (unavailable.length === 1) { |
| 473 | 1 | cb(new Error('Input format ' + unavailable[0] + ' is not available')); |
| 474 | 23 | } else if (unavailable.length > 1) { |
| 475 | 0 | cb(new Error('Input formats ' + unavailable.join(', ') + ' are not available')); |
| 476 | | } else { |
| 477 | 23 | cb(); |
| 478 | | } |
| 479 | | }, |
| 480 | | |
| 481 | | // Get available codecs |
| 482 | | function(cb) { |
| 483 | 23 | self.availableCodecs(cb); |
| 484 | | }, |
| 485 | | |
| 486 | | // Check whether specified codecs are available |
| 487 | | function(codecs, cb) { |
| 488 | | // Audio codec |
| 489 | 23 | var acodec = self._audio.find('-acodec', 1); |
| 490 | 23 | if (acodec && acodec[0] !== 'copy') { |
| 491 | 21 | if (!(acodec[0] in codecs) || codecs[acodec[0]].type !== 'audio' || !(codecs[acodec[0]].canEncode)) { |
| 492 | 1 | return cb(new Error('Audio codec ' + acodec[0] + ' is not available')); |
| 493 | | } |
| 494 | | } |
| 495 | | |
| 496 | | // Video codec |
| 497 | 22 | var vcodec = self._video.find('-vcodec', 1); |
| 498 | 22 | if (vcodec && vcodec[0] !== 'copy') { |
| 499 | 20 | if (!(vcodec[0] in codecs) || codecs[vcodec[0]].type !== 'video' || !(codecs[vcodec[0]].canEncode)) { |
| 500 | 1 | return cb(new Error('Video codec ' + vcodec[0] + ' is not available')); |
| 501 | | } |
| 502 | | } |
| 503 | | |
| 504 | 21 | cb(); |
| 505 | | } |
| 506 | | ], callback); |
| 507 | | }; |
| 508 | | }; |
| 509 | | |
lib/ffprobe.js
| Line | Hits | Source |
|---|
| 1 | | /*jshint node:true, laxcomma:true*/ |
| 2 | | 'use strict'; |
| 3 | | |
| 4 | 1 | var spawn = require('child_process').spawn; |
| 5 | | |
| 6 | | |
| 7 | 263 | function legacyTag(key) { return key.match(/^TAG:/); } |
| 8 | 263 | function legacyDisposition(key) { return key.match(/^DISPOSITION:/); } |
| 9 | | |
| 10 | | |
| 11 | 1 | module.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 | | */ |
| 26 | 1 | proto.ffprobe = function(callback) { |
| 27 | 10 | if (!this._currentInput) { |
| 28 | 1 | return callback(new Error('No input specified')); |
| 29 | | } |
| 30 | | |
| 31 | 9 | if (typeof this._currentInput.source !== 'string') { |
| 32 | 1 | return callback(new Error('Cannot run ffprobe on non-file input')); |
| 33 | | } |
| 34 | | |
| 35 | | // Find ffprobe |
| 36 | 8 | var self = this; |
| 37 | 8 | this._getFfprobePath(function(err, path) { |
| 38 | 8 | if (err) { |
| 39 | 0 | return callback(err); |
| 40 | 8 | } else if (!path) { |
| 41 | 0 | return callback(new Error('Cannot find ffprobe')); |
| 42 | | } |
| 43 | | |
| 44 | 8 | var stdout = ''; |
| 45 | 8 | var stdoutClosed = false; |
| 46 | 8 | var stderr = ''; |
| 47 | 8 | var stderrClosed = false; |
| 48 | | |
| 49 | | // Spawn ffprobe |
| 50 | 8 | var ffprobe = spawn(path, [ |
| 51 | | '-print_format', 'json', |
| 52 | | '-show_streams', |
| 53 | | '-show_format', |
| 54 | | self._currentInput.source |
| 55 | | ]); |
| 56 | | |
| 57 | 8 | ffprobe.on('error', function(err) { |
| 58 | 0 | callback(err); |
| 59 | | }); |
| 60 | | |
| 61 | | // Ensure we wait for captured streams to end before calling callback |
| 62 | 8 | var exitError = null; |
| 63 | | function handleExit(err) { |
| 64 | 24 | if (err) { |
| 65 | 1 | exitError = err; |
| 66 | | } |
| 67 | | |
| 68 | 24 | if (processExited && stdoutClosed && stderrClosed) { |
| 69 | 8 | if (exitError) { |
| 70 | 1 | if (stderr) { |
| 71 | 1 | exitError.message += '\n' + stderr; |
| 72 | | } |
| 73 | | |
| 74 | 1 | return callback(exitError); |
| 75 | | } |
| 76 | | |
| 77 | | // Process output |
| 78 | 7 | var data; |
| 79 | | |
| 80 | 7 | try { |
| 81 | 7 | data = JSON.parse(stdout); |
| 82 | | } catch(e) { |
| 83 | 0 | return callback(e); |
| 84 | | } |
| 85 | | |
| 86 | | // Handle legacy output with "TAG:x" and "DISPOSITION:x" keys |
| 87 | 7 | [data.format].concat(data.streams).forEach(function(target) { |
| 88 | 15 | var legacyTagKeys = Object.keys(target).filter(legacyTag); |
| 89 | | |
| 90 | 15 | if (legacyTagKeys.length) { |
| 91 | 0 | target.tags = target.tags || {}; |
| 92 | | |
| 93 | 0 | legacyTagKeys.forEach(function(tagKey) { |
| 94 | 0 | target.tags[tagKey.substr(4)] = target[tagKey]; |
| 95 | 0 | delete target[tagKey]; |
| 96 | | }); |
| 97 | | } |
| 98 | | |
| 99 | 15 | var legacyDispositionKeys = Object.keys(target).filter(legacyDisposition); |
| 100 | | |
| 101 | 15 | if (legacyDispositionKeys.length) { |
| 102 | 0 | target.disposition = target.disposition || {}; |
| 103 | | |
| 104 | 0 | legacyDispositionKeys.forEach(function(dispositionKey) { |
| 105 | 0 | target.disposition[dispositionKey.substr(12)] = target[dispositionKey]; |
| 106 | 0 | delete target[dispositionKey]; |
| 107 | | }); |
| 108 | | } |
| 109 | | }); |
| 110 | | |
| 111 | 7 | callback(null, data); |
| 112 | | } |
| 113 | | } |
| 114 | | |
| 115 | | // Handle ffprobe exit |
| 116 | 8 | var processExited = false; |
| 117 | 8 | ffprobe.on('exit', function(code, signal) { |
| 118 | 8 | processExited = true; |
| 119 | | |
| 120 | 8 | if (code) { |
| 121 | 1 | handleExit(new Error('ffprobe exited with code ' + code)); |
| 122 | 7 | } else if (signal) { |
| 123 | 0 | handleExit(new Error('ffprobe was killed with signal ' + signal)); |
| 124 | | } else { |
| 125 | 7 | handleExit(); |
| 126 | | } |
| 127 | | }); |
| 128 | | |
| 129 | | // Handle stdout/stderr streams |
| 130 | 8 | ffprobe.stdout.on('data', function(data) { |
| 131 | 18 | stdout += data; |
| 132 | | }); |
| 133 | | |
| 134 | 8 | ffprobe.stdout.on('close', function() { |
| 135 | 8 | stdoutClosed = true; |
| 136 | 8 | handleExit(); |
| 137 | | }); |
| 138 | | |
| 139 | 8 | ffprobe.stderr.on('data', function(data) { |
| 140 | 33 | stderr += data; |
| 141 | | }); |
| 142 | | |
| 143 | 8 | ffprobe.stderr.on('close', function() { |
| 144 | 8 | stderrClosed = true; |
| 145 | 8 | handleExit(); |
| 146 | | }); |
| 147 | | }); |
| 148 | | }; |
| 149 | | }; |
| 150 | | |
| 151 | | |
lib/fluent-ffmpeg.js
| Line | Hits | Source |
|---|
| 1 | | /*jshint node:true*/ |
| 2 | | 'use strict'; |
| 3 | | |
| 4 | 1 | var path = require('path'); |
| 5 | 1 | var util = require('util'); |
| 6 | 1 | var EventEmitter = require('events').EventEmitter; |
| 7 | | |
| 8 | 1 | var 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 | | */ |
| 29 | | function FfmpegCommand(input, options) { |
| 30 | | // Make 'new' optional |
| 31 | 207 | if (!(this instanceof FfmpegCommand)) { |
| 32 | 1 | return new FfmpegCommand(input, options); |
| 33 | | } |
| 34 | | |
| 35 | 206 | EventEmitter.call(this); |
| 36 | | |
| 37 | 206 | if (typeof input === 'object' && !('readable' in input)) { |
| 38 | | // Options object passed directly |
| 39 | 89 | options = input; |
| 40 | | } else { |
| 41 | | // Input passed first |
| 42 | 117 | options = options || {}; |
| 43 | 117 | options.source = input; |
| 44 | | } |
| 45 | | |
| 46 | | // Add input if present |
| 47 | 206 | this._inputs = []; |
| 48 | 206 | if (options.source) { |
| 49 | 94 | this.addInput(options.source); |
| 50 | | } |
| 51 | | |
| 52 | | // Create argument lists |
| 53 | 206 | this._audio = utils.args(); |
| 54 | 206 | this._audioFilters = utils.args(); |
| 55 | 206 | this._video = utils.args(); |
| 56 | 206 | this._videoFilters = utils.args(); |
| 57 | 206 | this._sizeFilters = utils.args(); |
| 58 | 206 | this._output = utils.args(); |
| 59 | | |
| 60 | | // Set default option values |
| 61 | 206 | options.presets = options.presets || options.preset || path.join(__dirname, 'presets'); |
| 62 | 206 | options.niceness = options.niceness || options.priority || 0; |
| 63 | | |
| 64 | | // Save options |
| 65 | 206 | this.options = options; |
| 66 | | |
| 67 | | // Setup logger |
| 68 | 206 | this.logger = options.logger || { |
| 69 | | debug: function() {}, |
| 70 | | info: function() {}, |
| 71 | | warn: function() {}, |
| 72 | | error: function() {} |
| 73 | | }; |
| 74 | | } |
| 75 | 1 | util.inherits(FfmpegCommand, EventEmitter); |
| 76 | 1 | module.exports = FfmpegCommand; |
| 77 | | |
| 78 | | |
| 79 | | /* Add methods from options submodules */ |
| 80 | | |
| 81 | 1 | require('./options/inputs')(FfmpegCommand.prototype); |
| 82 | 1 | require('./options/audio')(FfmpegCommand.prototype); |
| 83 | 1 | require('./options/video')(FfmpegCommand.prototype); |
| 84 | 1 | require('./options/videosize')(FfmpegCommand.prototype); |
| 85 | 1 | require('./options/output')(FfmpegCommand.prototype); |
| 86 | 1 | require('./options/custom')(FfmpegCommand.prototype); |
| 87 | 1 | require('./options/misc')(FfmpegCommand.prototype); |
| 88 | | |
| 89 | | |
| 90 | | /* Add processor methods */ |
| 91 | | |
| 92 | 1 | require('./processor')(FfmpegCommand.prototype); |
| 93 | | |
| 94 | | |
| 95 | | /* Add capabilities methods */ |
| 96 | | |
| 97 | 1 | require('./capabilities')(FfmpegCommand.prototype); |
| 98 | | |
| 99 | 1 | FfmpegCommand.availableFilters = |
| 100 | | FfmpegCommand.getAvailableFilters = function(callback) { |
| 101 | 1 | (new FfmpegCommand()).availableFilters(callback); |
| 102 | | }; |
| 103 | | |
| 104 | 1 | FfmpegCommand.availableCodecs = |
| 105 | | FfmpegCommand.getAvailableCodecs = function(callback) { |
| 106 | 1 | (new FfmpegCommand()).availableCodecs(callback); |
| 107 | | }; |
| 108 | | |
| 109 | 1 | FfmpegCommand.availableFormats = |
| 110 | | FfmpegCommand.getAvailableFormats = function(callback) { |
| 111 | 1 | (new FfmpegCommand()).availableFormats(callback); |
| 112 | | }; |
| 113 | | |
| 114 | | |
| 115 | | /* Add ffprobe methods */ |
| 116 | | |
| 117 | 1 | require('./ffprobe')(FfmpegCommand.prototype); |
| 118 | | |
| 119 | 1 | FfmpegCommand.ffprobe = function(file, callback) { |
| 120 | 4 | (new FfmpegCommand(file)).ffprobe(callback); |
| 121 | | }; |
| 122 | | |
| 123 | | |
lib/options/audio.js
| Line | Hits | Source |
|---|
| 1 | | /*jshint node:true*/ |
| 2 | | 'use strict'; |
| 3 | | |
| 4 | | /* |
| 5 | | *! Audio-related methods |
| 6 | | */ |
| 7 | | |
| 8 | 1 | module.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 | | */ |
| 17 | 1 | proto.withNoAudio = |
| 18 | | proto.noAudio = function() { |
| 19 | 2 | this._audio.clear(); |
| 20 | 2 | this._audio('-an'); |
| 21 | | |
| 22 | 2 | 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 | | */ |
| 36 | 1 | proto.withAudioCodec = |
| 37 | | proto.audioCodec = function(codec) { |
| 38 | 26 | this._audio('-acodec', codec); |
| 39 | 26 | 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 | | */ |
| 53 | 1 | proto.withAudioBitrate = |
| 54 | | proto.audioBitrate = function(bitrate) { |
| 55 | 22 | this._audio('-b:a', ('' + bitrate).replace(/k?$/, 'k')); |
| 56 | 22 | 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 | | */ |
| 70 | 1 | proto.withAudioChannels = |
| 71 | | proto.audioChannels = function(channels) { |
| 72 | 22 | this._audio('-ac', channels); |
| 73 | 22 | 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 | | */ |
| 87 | 1 | proto.withAudioFrequency = |
| 88 | | proto.audioFrequency = function(freq) { |
| 89 | 20 | this._audio('-ar', freq); |
| 90 | 20 | 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 | | */ |
| 104 | 1 | proto.withAudioQuality = |
| 105 | | proto.audioQuality = function(quality) { |
| 106 | 1 | this._audio('-aq', quality); |
| 107 | 1 | 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 | | */ |
| 132 | 1 | proto.withAudioFilter = |
| 133 | | proto.withAudioFilters = |
| 134 | | proto.audioFilter = |
| 135 | | proto.audioFilters = function(filters) { |
| 136 | 3 | if (arguments.length > 1) { |
| 137 | 1 | filters = [].slice.call(arguments); |
| 138 | | } |
| 139 | | |
| 140 | 3 | this._audioFilters(filters); |
| 141 | 3 | return this; |
| 142 | | }; |
| 143 | | }; |
| 144 | | |
lib/options/custom.js
| Line | Hits | Source |
|---|
| 1 | | /*jshint node:true*/ |
| 2 | | 'use strict'; |
| 3 | | |
| 4 | | /* |
| 5 | | *! Custom options methods |
| 6 | | */ |
| 7 | | |
| 8 | 1 | module.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 | | */ |
| 33 | 1 | proto.addInputOption = |
| 34 | | proto.addInputOptions = |
| 35 | | proto.withInputOption = |
| 36 | | proto.withInputOptions = |
| 37 | | proto.inputOption = |
| 38 | | proto.inputOptions = function(options) { |
| 39 | 5 | if (!this._currentInput) { |
| 40 | 1 | throw new Error('No input specified'); |
| 41 | | } |
| 42 | | |
| 43 | 4 | var doSplit = true; |
| 44 | | |
| 45 | 4 | if (arguments.length > 1) { |
| 46 | 2 | options = [].slice.call(arguments); |
| 47 | 2 | doSplit = false; |
| 48 | | } |
| 49 | | |
| 50 | 4 | if (!Array.isArray(options)) { |
| 51 | 1 | options = [options]; |
| 52 | | } |
| 53 | | |
| 54 | 4 | this._currentInput.before(options.reduce(function(options, option) { |
| 55 | 7 | var split = option.split(' '); |
| 56 | | |
| 57 | 7 | if (doSplit && split.length === 2) { |
| 58 | 3 | options.push(split[0], split[1]); |
| 59 | | } else { |
| 60 | 4 | options.push(option); |
| 61 | | } |
| 62 | | |
| 63 | 7 | return options; |
| 64 | | }, [])); |
| 65 | 4 | 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 | | */ |
| 88 | 1 | 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) { |
| 98 | 6 | var doSplit = true; |
| 99 | | |
| 100 | 6 | if (arguments.length > 1) { |
| 101 | 2 | options = [].slice.call(arguments); |
| 102 | 2 | doSplit = false; |
| 103 | | } |
| 104 | | |
| 105 | 6 | if (!Array.isArray(options)) { |
| 106 | 1 | options = [options]; |
| 107 | | } |
| 108 | | |
| 109 | 6 | this._output(options.reduce(function(options, option) { |
| 110 | 45 | var split = option.split(' '); |
| 111 | | |
| 112 | 45 | if (doSplit && split.length === 2) { |
| 113 | 19 | options.push(split[0], split[1]); |
| 114 | | } else { |
| 115 | 26 | options.push(option); |
| 116 | | } |
| 117 | | |
| 118 | 45 | return options; |
| 119 | | }, [])); |
| 120 | 6 | return this; |
| 121 | | }; |
| 122 | | }; |
| 123 | | |
| Line | Hits | Source |
|---|
| 1 | | /*jshint node:true*/ |
| 2 | | 'use strict'; |
| 3 | | |
| 4 | 1 | var utils = require('../utils'); |
| 5 | | |
| 6 | | /* |
| 7 | | *! Input-related methods |
| 8 | | */ |
| 9 | | |
| 10 | 1 | module.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 | | */ |
| 26 | 1 | proto.mergeAdd = |
| 27 | | proto.addInput = |
| 28 | | proto.input = function(source) { |
| 29 | 101 | if (typeof source !== 'string') { |
| 30 | 6 | if (!('readable' in source)) { |
| 31 | 1 | throw new Error('Invalid input'); |
| 32 | | } |
| 33 | | |
| 34 | 5 | var hasInputStream = this._inputs.some(function(input) { |
| 35 | 1 | return typeof input.source !== 'string'; |
| 36 | | }); |
| 37 | | |
| 38 | 5 | if (hasInputStream) { |
| 39 | 1 | throw new Error('Only one input stream is supported'); |
| 40 | | } |
| 41 | | |
| 42 | 4 | source.pause(); |
| 43 | | } |
| 44 | | |
| 45 | 99 | this._inputs.push(this._currentInput = { |
| 46 | | source: source, |
| 47 | | before: utils.args(), |
| 48 | | after: utils.args(), |
| 49 | | }); |
| 50 | | |
| 51 | 99 | 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 | | */ |
| 65 | 1 | proto.withInputFormat = |
| 66 | | proto.inputFormat = |
| 67 | | proto.fromFormat = function(format) { |
| 68 | 6 | if (!this._currentInput) { |
| 69 | 1 | throw new Error('No input specified'); |
| 70 | | } |
| 71 | | |
| 72 | 5 | this._currentInput.before('-f', format); |
| 73 | 5 | 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 | | */ |
| 88 | 1 | 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) { |
| 96 | 2 | if (!this._currentInput) { |
| 97 | 1 | throw new Error('No input specified'); |
| 98 | | } |
| 99 | | |
| 100 | 1 | this._currentInput.before('-r', fps); |
| 101 | 1 | 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 | | */ |
| 116 | 1 | proto.setStartTime = |
| 117 | | proto.seekTo = |
| 118 | | proto.seek = function(seek, fast) { |
| 119 | 4 | if (!this._currentInput) { |
| 120 | 2 | throw new Error('No input specified'); |
| 121 | | } |
| 122 | | |
| 123 | 2 | if (fast) { |
| 124 | 1 | this._currentInput.before('-ss', seek); |
| 125 | | } else { |
| 126 | 1 | this._currentInput.after('-ss', seek); |
| 127 | | } |
| 128 | | |
| 129 | 2 | 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 | | */ |
| 143 | 1 | proto.fastSeek = |
| 144 | | proto.fastSeekTo = function(seek) { |
| 145 | 1 | 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 | | */ |
| 158 | 1 | proto.loop = function(duration) { |
| 159 | 4 | if (!this._currentInput) { |
| 160 | 1 | throw new Error('No input specified'); |
| 161 | | } |
| 162 | | |
| 163 | 3 | this._currentInput.before('-loop', '1'); |
| 164 | | |
| 165 | 3 | if (typeof duration !== 'undefined') { |
| 166 | 2 | this.duration(duration); |
| 167 | | } |
| 168 | | |
| 169 | 3 | return this; |
| 170 | | }; |
| 171 | | }; |
| 172 | | |
lib/options/misc.js
| Line | Hits | Source |
|---|
| 1 | | /*jshint node:true*/ |
| 2 | | 'use strict'; |
| 3 | | |
| 4 | 1 | var path = require('path'); |
| 5 | | |
| 6 | | /* |
| 7 | | *! Miscellaneous methods |
| 8 | | */ |
| 9 | | |
| 10 | 1 | module.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 | | */ |
| 20 | 1 | proto.usingPreset = |
| 21 | | proto.preset = function(preset) { |
| 22 | 23 | if (typeof preset === 'function') { |
| 23 | 1 | preset(this); |
| 24 | | } else { |
| 25 | 22 | try { |
| 26 | 22 | var modulePath = path.join(this.options.presets, preset); |
| 27 | 22 | var module = require(modulePath); |
| 28 | | |
| 29 | 21 | if (typeof module.load === 'function') { |
| 30 | 20 | module.load(this); |
| 31 | | } else { |
| 32 | 1 | throw new Error('preset ' + modulePath + ' has no load() function'); |
| 33 | | } |
| 34 | | } catch (err) { |
| 35 | 2 | throw new Error('preset ' + modulePath + ' could not be loaded: ' + err.message); |
| 36 | | } |
| 37 | | } |
| 38 | | |
| 39 | 21 | 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 | | */ |
| 52 | 1 | proto.withStrictExperimental = |
| 53 | | proto.strict = function() { |
| 54 | 20 | this._output('-strict', 'experimental'); |
| 55 | 20 | 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 | | */ |
| 68 | 1 | proto.updateFlvMetadata = |
| 69 | | proto.flvmeta = function() { |
| 70 | 18 | this.options.flvmeta = true; |
| 71 | 18 | return this; |
| 72 | | }; |
| 73 | | }; |
| 74 | | |
lib/options/output.js
| Line | Hits | Source |
|---|
| 1 | | /*jshint node:true*/ |
| 2 | | 'use strict'; |
| 3 | | |
| 4 | | /* |
| 5 | | *! Output-related methods |
| 6 | | */ |
| 7 | | |
| 8 | 1 | module.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 | | */ |
| 19 | 1 | proto.withDuration = |
| 20 | | proto.setDuration = |
| 21 | | proto.duration = function(duration) { |
| 22 | 3 | this._output('-t', duration); |
| 23 | 3 | 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 | | */ |
| 37 | 1 | proto.toFormat = |
| 38 | | proto.withOutputFormat = |
| 39 | | proto.outputFormat = |
| 40 | | proto.format = function(format) { |
| 41 | 27 | this._output('-f', format); |
| 42 | 27 | return this; |
| 43 | | }; |
| 44 | | }; |
| 45 | | |
lib/options/video.js
| Line | Hits | Source |
|---|
| 1 | | /*jshint node:true*/ |
| 2 | | 'use strict'; |
| 3 | | |
| 4 | | /* |
| 5 | | *! Video-related methods |
| 6 | | */ |
| 7 | | |
| 8 | 1 | module.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 | | */ |
| 18 | 1 | proto.withNoVideo = |
| 19 | | proto.noVideo = function() { |
| 20 | 2 | this._video.clear(); |
| 21 | 2 | this._video('-vn'); |
| 22 | | |
| 23 | 2 | 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 | | */ |
| 37 | 1 | proto.withVideoCodec = |
| 38 | | proto.videoCodec = function(codec) { |
| 39 | 27 | this._video('-vcodec', codec); |
| 40 | 27 | 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 | | */ |
| 55 | 1 | proto.withVideoBitrate = |
| 56 | | proto.videoBitrate = function(bitrate, constant) { |
| 57 | 22 | bitrate = ('' + bitrate).replace(/k?$/, 'k'); |
| 58 | | |
| 59 | 22 | this._video('-b:v', bitrate); |
| 60 | 22 | if (constant) { |
| 61 | 1 | this._video( |
| 62 | | '-maxrate', bitrate, |
| 63 | | '-minrate', bitrate, |
| 64 | | '-bufsize', '3M' |
| 65 | | ); |
| 66 | | } |
| 67 | | |
| 68 | 22 | 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 | | */ |
| 93 | 1 | proto.withVideoFilter = |
| 94 | | proto.withVideoFilters = |
| 95 | | proto.videoFilter = |
| 96 | | proto.videoFilters = function(filters) { |
| 97 | 4 | if (arguments.length > 1) { |
| 98 | 2 | filters = [].slice.call(arguments); |
| 99 | | } |
| 100 | | |
| 101 | 4 | if (Array.isArray(filters)) { |
| 102 | 2 | this._videoFilters.apply(null, filters); |
| 103 | | } else { |
| 104 | 2 | this._videoFilters(filters); |
| 105 | | } |
| 106 | | |
| 107 | 4 | 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 | | */ |
| 121 | 1 | 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) { |
| 133 | 19 | this._video('-r', fps); |
| 134 | 19 | 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 | | */ |
| 148 | 1 | proto.takeFrames = |
| 149 | | proto.withFrames = |
| 150 | | proto.frames = function(frames) { |
| 151 | 5 | this._video('-vframes', frames); |
| 152 | 5 | return this; |
| 153 | | }; |
| 154 | | }; |
| 155 | | |
lib/options/videosize.js
| Line | Hits | Source |
|---|
| 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 | | */ |
| 19 | | function 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 | | |
| 27 | 10 | 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 | | */ |
| 72 | | function createSizeFilters(command, key, value) { |
| 73 | | // Store parameters |
| 74 | 80 | var data = command._sizeData = command._sizeData || {}; |
| 75 | 80 | data[key] = value; |
| 76 | | |
| 77 | 80 | if (!('size' in data)) { |
| 78 | | // No size requested, keep original size |
| 79 | 2 | return []; |
| 80 | | } |
| 81 | | |
| 82 | | // Try to match the different size string formats |
| 83 | 78 | var fixedSize = data.size.match(/([0-9]+)x([0-9]+)/); |
| 84 | 78 | var fixedWidth = data.size.match(/([0-9]+)x\?/); |
| 85 | 78 | var fixedHeight = data.size.match(/\?x([0-9]+)/); |
| 86 | 78 | var percentRatio = data.size.match(/\b([0-9]{1,3})%/); |
| 87 | 78 | var width, height, aspect; |
| 88 | | |
| 89 | 78 | if (percentRatio) { |
| 90 | 5 | var ratio = Number(percentRatio[1]) / 100; |
| 91 | 5 | return ['scale=trunc(iw*' + ratio + '/2)*2:trunc(ih*' + ratio + '/2)*2']; |
| 92 | 73 | } else if (fixedSize) { |
| 93 | | // Round target size to multiples of 2 |
| 94 | 21 | width = Math.round(Number(fixedSize[1]) / 2) * 2; |
| 95 | 21 | height = Math.round(Number(fixedSize[2]) / 2) * 2; |
| 96 | | |
| 97 | 21 | aspect = width / height; |
| 98 | | |
| 99 | 21 | if (data.pad) { |
| 100 | 5 | return getScalePadFilters(width, height, aspect, data.pad); |
| 101 | | } else { |
| 102 | | // No autopad requested, rescale to target size |
| 103 | 16 | return ['scale=' + width + ':' + height]; |
| 104 | | } |
| 105 | 52 | } else if (fixedWidth || fixedHeight) { |
| 106 | 51 | if ('aspect' in data) { |
| 107 | | // Specified aspect ratio |
| 108 | 14 | width = fixedWidth ? fixedWidth[1] : Math.round(Number(fixedHeight[1]) * data.aspect); |
| 109 | 14 | height = fixedHeight ? fixedHeight[1] : Math.round(Number(fixedWidth[1]) / data.aspect); |
| 110 | | |
| 111 | | // Round to multiples of 2 |
| 112 | 14 | width = Math.round(width / 2) * 2; |
| 113 | 14 | height = Math.round(height / 2) * 2; |
| 114 | | |
| 115 | 14 | if (data.pad) { |
| 116 | 5 | return getScalePadFilters(width, height, data.aspect, data.pad); |
| 117 | | } else { |
| 118 | | // No autopad requested, rescale to target size |
| 119 | 9 | return ['scale=' + width + ':' + height]; |
| 120 | | } |
| 121 | | } else { |
| 122 | | // Keep input aspect ratio |
| 123 | | |
| 124 | 37 | if (fixedWidth) { |
| 125 | 31 | return ['scale=' + (Math.round(Number(fixedWidth[1]) / 2) * 2) + ':trunc(ow/a/2)*2']; |
| 126 | | } else { |
| 127 | 6 | return ['scale=trunc(oh*a/2)*2:' + (Math.round(Number(fixedHeight[1]) / 2) * 2)]; |
| 128 | | } |
| 129 | | } |
| 130 | | } else { |
| 131 | 1 | throw new Error('Invalid size specified: ' + data.size); |
| 132 | | } |
| 133 | | } |
| 134 | | |
| 135 | | |
| 136 | | /* |
| 137 | | *! Video size-related methods |
| 138 | | */ |
| 139 | | |
| 140 | 1 | module.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 | | */ |
| 154 | 1 | proto.keepPixelAspect = // Only for compatibility, this is not about keeping _pixel_ aspect ratio |
| 155 | | proto.keepDisplayAspect = |
| 156 | | proto.keepDisplayAspectRatio = |
| 157 | | proto.keepDAR = function() { |
| 158 | 1 | 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 | | */ |
| 183 | 1 | proto.withSize = |
| 184 | | proto.setSize = |
| 185 | | proto.size = function(size) { |
| 186 | 52 | var filters = createSizeFilters(this, 'size', size); |
| 187 | | |
| 188 | 51 | this._sizeFilters.clear(); |
| 189 | 51 | this._sizeFilters(filters); |
| 190 | | |
| 191 | 51 | 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 | | */ |
| 205 | 1 | proto.withAspect = |
| 206 | | proto.withAspectRatio = |
| 207 | | proto.setAspect = |
| 208 | | proto.setAspectRatio = |
| 209 | | proto.aspect = |
| 210 | | proto.aspectRatio = function(aspect) { |
| 211 | 15 | var a = Number(aspect); |
| 212 | 15 | if (isNaN(a)) { |
| 213 | 3 | var match = aspect.match(/^(\d+):(\d+)$/); |
| 214 | 3 | if (match) { |
| 215 | 2 | a = Number(match[1]) / Number(match[2]); |
| 216 | | } else { |
| 217 | 1 | throw new Error('Invalid aspect ratio: ' + aspect); |
| 218 | | } |
| 219 | | } |
| 220 | | |
| 221 | 14 | var filters = createSizeFilters(this, 'aspect', a); |
| 222 | | |
| 223 | 14 | this._sizeFilters.clear(); |
| 224 | 14 | this._sizeFilters(filters); |
| 225 | | |
| 226 | 14 | 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 | | */ |
| 240 | 1 | 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) |
| 251 | 14 | if (typeof pad === 'string') { |
| 252 | 1 | color = pad; |
| 253 | 1 | pad = true; |
| 254 | | } |
| 255 | | |
| 256 | | // Allow autopad() and autopad(undefined, color) |
| 257 | 14 | if (typeof pad === 'undefined') { |
| 258 | 1 | pad = true; |
| 259 | | } |
| 260 | | |
| 261 | 14 | var filters = createSizeFilters(this, 'pad', pad ? color || 'black' : false); |
| 262 | | |
| 263 | 14 | this._sizeFilters.clear(); |
| 264 | 14 | this._sizeFilters(filters); |
| 265 | | |
| 266 | 14 | return this; |
| 267 | | }; |
| 268 | | }; |
| 269 | | |
lib/presets/flashvideo.js
| Line | Hits | Source |
|---|
| 1 | | /*jshint node:true */ |
| 2 | | 'use strict'; |
| 3 | | |
| 4 | 1 | exports.load = function(ffmpeg) { |
| 5 | 18 | 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
| Line | Hits | Source |
|---|
| 1 | | /*jshint node:true */ |
| 2 | | 'use strict'; |
| 3 | | |
| 4 | 1 | exports.load = function(ffmpeg) { |
| 5 | 1 | 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
| Line | Hits | Source |
|---|
| 1 | | /*jshint node:true*/ |
| 2 | | 'use strict'; |
| 3 | | |
| 4 | 1 | var spawn = require('child_process').spawn; |
| 5 | 1 | var PassThrough = require('stream').PassThrough; |
| 6 | 1 | var path = require('path'); |
| 7 | 1 | var fs = require('fs'); |
| 8 | 1 | var async = require('async'); |
| 9 | 1 | var 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 | | */ |
| 23 | | function _process(command, target, pipeOptions) { |
| 24 | 19 | var isStream; |
| 25 | | |
| 26 | 19 | if (typeof target === 'string') { |
| 27 | 16 | isStream = false; |
| 28 | | } else { |
| 29 | 3 | isStream = true; |
| 30 | 3 | pipeOptions = pipeOptions || {}; |
| 31 | | } |
| 32 | | |
| 33 | | // Ensure we send 'end' or 'error' only once |
| 34 | 19 | var ended = false; |
| 35 | | function emitEnd(err, stdout, stderr) { |
| 36 | 24 | if (!ended) { |
| 37 | 19 | ended = true; |
| 38 | | |
| 39 | 19 | if (err) { |
| 40 | 5 | command.emit('error', err, stdout, stderr); |
| 41 | | } else { |
| 42 | 14 | command.emit('end', stdout, stderr); |
| 43 | | } |
| 44 | | } |
| 45 | | } |
| 46 | | |
| 47 | 19 | command._prepare(function(err, args) { |
| 48 | 19 | if (err) { |
| 49 | 1 | return emitEnd(err); |
| 50 | | } |
| 51 | | |
| 52 | 18 | if (isStream) { |
| 53 | 3 | args.push('pipe:1'); |
| 54 | | |
| 55 | 3 | if (command.options.flvmeta) { |
| 56 | 3 | command.logger.warn('Updating flv metadata is not supported for streams'); |
| 57 | 3 | command.options.flvmeta = false; |
| 58 | | } |
| 59 | | } else { |
| 60 | 15 | args.push('-y', target); |
| 61 | | } |
| 62 | | |
| 63 | | // Get input stream if any |
| 64 | 18 | var inputStream = command._inputs.filter(function(input) { |
| 65 | 18 | return typeof input.source !== 'string'; |
| 66 | | })[0]; |
| 67 | | |
| 68 | | // Run ffmpeg |
| 69 | 18 | var stdout = null; |
| 70 | 18 | var stderr = ''; |
| 71 | 18 | command._spawnFfmpeg( |
| 72 | | args, |
| 73 | | |
| 74 | | { niceness: command.options.niceness }, |
| 75 | | |
| 76 | | function processCB(ffmpegProc) { |
| 77 | 18 | command.ffmpegProc = ffmpegProc; |
| 78 | 18 | command.emit('start', 'ffmpeg ' + args.join(' ')); |
| 79 | | |
| 80 | | // Pipe input stream if any |
| 81 | 18 | if (inputStream) { |
| 82 | 2 | inputStream.source.on('error', function(err) { |
| 83 | 0 | emitEnd(new Error('Input stream error: ' + err.message)); |
| 84 | 0 | ffmpegProc.kill(); |
| 85 | | }); |
| 86 | | |
| 87 | 2 | inputStream.source.resume(); |
| 88 | 2 | inputStream.source.pipe(ffmpegProc.stdin); |
| 89 | | } |
| 90 | | |
| 91 | | // Setup timeout if requested |
| 92 | 18 | var processTimer; |
| 93 | 18 | if (command.options.timeout) { |
| 94 | 4 | processTimer = setTimeout(function() { |
| 95 | 3 | var msg = 'process ran into a timeout (' + command.options.timeout + 's)'; |
| 96 | | |
| 97 | 3 | emitEnd(new Error(msg), stdout, stderr); |
| 98 | 3 | ffmpegProc.kill(); |
| 99 | | }, command.options.timeout * 1000); |
| 100 | | } |
| 101 | | |
| 102 | 18 | if (isStream) { |
| 103 | | // Pipe ffmpeg stdout to output stream |
| 104 | 3 | ffmpegProc.stdout.pipe(target, pipeOptions); |
| 105 | | |
| 106 | | // Handle output stream events |
| 107 | 3 | target.on('close', function() { |
| 108 | 2 | 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. |
| 114 | 2 | setTimeout(function() { |
| 115 | 2 | emitEnd(new Error('Output stream closed')); |
| 116 | 2 | ffmpegProc.kill(); |
| 117 | | }, 20); |
| 118 | | }); |
| 119 | | |
| 120 | 3 | target.on('error', function(err) { |
| 121 | 0 | command.logger.debug('Output stream error, killing ffmpgeg process'); |
| 122 | 0 | emitEnd(new Error('Output stream error: ' + err.message)); |
| 123 | 0 | ffmpegProc.kill(); |
| 124 | | }); |
| 125 | | } else { |
| 126 | | // Gather ffmpeg stdout |
| 127 | 15 | stdout = ''; |
| 128 | 15 | ffmpegProc.stdout.on('data', function (data) { |
| 129 | 0 | stdout += data; |
| 130 | | }); |
| 131 | | } |
| 132 | | |
| 133 | | // Process ffmpeg stderr data |
| 134 | 18 | command._codecDataSent = false; |
| 135 | 18 | ffmpegProc.stderr.on('data', function (data) { |
| 136 | 324 | stderr += data; |
| 137 | | |
| 138 | 324 | if (!command._codecDataSent && command.listeners('codecData').length) { |
| 139 | 11 | utils.extractCodecData(command, stderr); |
| 140 | | } |
| 141 | | |
| 142 | 324 | if (command.listeners('progress').length) { |
| 143 | 26 | var duration = 0; |
| 144 | | |
| 145 | 26 | if (command._ffprobeData && command._ffprobeData.format && command._ffprobeData.format.duration) { |
| 146 | 21 | duration = Number(command._ffprobeData.format.duration); |
| 147 | | } |
| 148 | | |
| 149 | 26 | utils.extractProgress(command, stderr, duration); |
| 150 | | } |
| 151 | | }); |
| 152 | | }, |
| 153 | | |
| 154 | | function endCB(err) { |
| 155 | 18 | delete command.ffmpegProc; |
| 156 | | |
| 157 | 18 | if (err) { |
| 158 | 4 | emitEnd(err, stdout, stderr); |
| 159 | | } else { |
| 160 | 14 | if (command.options.flvmeta) { |
| 161 | 11 | command._getFlvtoolPath(function(err, flvtool) { |
| 162 | | // No error possible here, _getFlvtoolPath was called by _prepare |
| 163 | | |
| 164 | 11 | spawn(flvtool, ['-U', target]) |
| 165 | | .on('error', function(err) { |
| 166 | 0 | emitEnd(new Error('Error running ' + flvtool + ': ' + err.message)); |
| 167 | | }) |
| 168 | | .on('exit', function(code, signal) { |
| 169 | 11 | if (code !== 0 || signal) { |
| 170 | 0 | emitEnd( |
| 171 | | new Error(flvtool + ' ' + |
| 172 | | (signal ? 'received signal ' + signal |
| 173 | | : 'exited with code ' + code)) |
| 174 | | ); |
| 175 | | } else { |
| 176 | 11 | emitEnd(null, stdout, stderr); |
| 177 | | } |
| 178 | | }); |
| 179 | | }); |
| 180 | | } else { |
| 181 | 3 | 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 | | */ |
| 196 | | function runFfprobe(command) { |
| 197 | 1 | command.ffprobe(function(err, data) { |
| 198 | 1 | command._ffprobeData = data; |
| 199 | | }); |
| 200 | | } |
| 201 | | |
| 202 | | |
| 203 | 1 | module.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 | | */ |
| 263 | 1 | proto._spawnFfmpeg = function(args, options, processCB, endCB) { |
| 264 | | // Enable omitting options |
| 265 | 30 | if (typeof options === 'function') { |
| 266 | 8 | endCB = processCB; |
| 267 | 8 | processCB = options; |
| 268 | 8 | options = {}; |
| 269 | | } |
| 270 | | |
| 271 | | // Enable omitting processCB |
| 272 | 30 | if (typeof endCB === 'undefined') { |
| 273 | 12 | endCB = processCB; |
| 274 | 12 | processCB = function() {}; |
| 275 | | } |
| 276 | | |
| 277 | | // Find ffmpeg |
| 278 | 30 | this._getFfmpegPath(function(err, command) { |
| 279 | 30 | if (err) { |
| 280 | 0 | return endCB(err); |
| 281 | 30 | } else if (!command || command.length === 0) { |
| 282 | 0 | return endCB(new Error('Cannot find ffmpeg')); |
| 283 | | } |
| 284 | | |
| 285 | | // Apply niceness |
| 286 | 30 | if (options.niceness && options.niceness !== 0 && !utils.isWindows) { |
| 287 | 0 | args.unshift('-n', options.niceness, command); |
| 288 | 0 | command = 'nice'; |
| 289 | | } |
| 290 | | |
| 291 | 30 | var stdout = null; |
| 292 | 30 | var stdoutClosed = false; |
| 293 | | |
| 294 | 30 | var stderr = null; |
| 295 | 30 | var stderrClosed = false; |
| 296 | | |
| 297 | | // Spawn process |
| 298 | 30 | var ffmpegProc = spawn(command, args, options); |
| 299 | | |
| 300 | 30 | if (ffmpegProc.stderr && options.captureStderr) { |
| 301 | 1 | ffmpegProc.stderr.setEncoding('utf8'); |
| 302 | | } |
| 303 | | |
| 304 | 30 | ffmpegProc.on('error', function(err) { |
| 305 | 0 | endCB(err); |
| 306 | | }); |
| 307 | | |
| 308 | | // Ensure we wait for captured streams to end before calling endCB |
| 309 | 30 | var exitError = null; |
| 310 | | function handleExit(err) { |
| 311 | 35 | if (err) { |
| 312 | 4 | exitError = err; |
| 313 | | } |
| 314 | | |
| 315 | 35 | if (processExited && |
| 316 | | (stdoutClosed || !options.captureStdout) && |
| 317 | | (stderrClosed || !options.captureStderr)) { |
| 318 | 30 | endCB(exitError, stdout, stderr); |
| 319 | | } |
| 320 | | } |
| 321 | | |
| 322 | | // Handle process exit |
| 323 | 30 | var processExited = false; |
| 324 | 30 | ffmpegProc.on('exit', function(code, signal) { |
| 325 | 30 | processExited = true; |
| 326 | | |
| 327 | 30 | if (code) { |
| 328 | 3 | handleExit(new Error('ffmpeg exited with code ' + code)); |
| 329 | 27 | } else if (signal) { |
| 330 | 1 | handleExit(new Error('ffmpeg was killed with signal ' + signal)); |
| 331 | | } else { |
| 332 | 26 | handleExit(); |
| 333 | | } |
| 334 | | }); |
| 335 | | |
| 336 | | // Capture stdout if specified |
| 337 | 30 | if (options.captureStdout) { |
| 338 | 4 | stdout = ''; |
| 339 | | |
| 340 | 4 | ffmpegProc.stdout.on('data', function(data) { |
| 341 | 11 | stdout += data; |
| 342 | | }); |
| 343 | | |
| 344 | 4 | ffmpegProc.stdout.on('close', function() { |
| 345 | 4 | stdoutClosed = true; |
| 346 | 4 | handleExit(); |
| 347 | | }); |
| 348 | | } |
| 349 | | |
| 350 | | // Capture stderr if specified |
| 351 | 30 | if (options.captureStderr) { |
| 352 | 1 | stderr = ''; |
| 353 | | |
| 354 | 1 | ffmpegProc.stderr.on('data', function(data) { |
| 355 | 0 | stderr += data; |
| 356 | | }); |
| 357 | | |
| 358 | 1 | ffmpegProc.stderr.on('close', function() { |
| 359 | 1 | stderrClosed = true; |
| 360 | 1 | handleExit(); |
| 361 | | }); |
| 362 | | } |
| 363 | | |
| 364 | | // Call process callback |
| 365 | 30 | 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 | | */ |
| 377 | 1 | proto._getArguments = function() { |
| 378 | 53 | var audioFilters = this._audioFilters.get(); |
| 379 | 53 | var videoFilters = this._videoFilters.get().concat(this._sizeFilters.get()); |
| 380 | | |
| 381 | 53 | return this._inputs.reduce(function(args, input) { |
| 382 | 54 | var source = (typeof input.source === 'string') ? input.source : '-'; |
| 383 | | |
| 384 | 54 | 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 | | */ |
| 411 | 1 | proto._prepare = function(callback, readMetadata) { |
| 412 | 21 | var self = this; |
| 413 | | |
| 414 | 21 | async.waterfall([ |
| 415 | | // Check codecs and formats |
| 416 | | function(cb) { |
| 417 | 21 | self._checkCapabilities(cb); |
| 418 | | }, |
| 419 | | |
| 420 | | // Read metadata if required |
| 421 | | function(cb) { |
| 422 | 20 | if (!readMetadata) { |
| 423 | 18 | return cb(); |
| 424 | | } |
| 425 | | |
| 426 | 2 | self.ffprobe(function(err, data) { |
| 427 | 2 | if (!err) { |
| 428 | 2 | self._ffprobeData = data; |
| 429 | | } |
| 430 | | |
| 431 | 2 | cb(); |
| 432 | | }); |
| 433 | | }, |
| 434 | | |
| 435 | | // Check for flvtool2/flvmeta if necessary |
| 436 | | function(cb) { |
| 437 | 20 | if (self.options.flvmeta) { |
| 438 | 18 | self._getFlvtoolPath(function(err) { |
| 439 | 18 | cb(err); |
| 440 | | }); |
| 441 | | } else { |
| 442 | 2 | cb(); |
| 443 | | } |
| 444 | | }, |
| 445 | | |
| 446 | | // Build argument list |
| 447 | | function(cb) { |
| 448 | 20 | var args; |
| 449 | 20 | try { |
| 450 | 20 | args = self._getArguments(); |
| 451 | | } catch(e) { |
| 452 | 0 | return cb(e); |
| 453 | | } |
| 454 | | |
| 455 | 20 | cb(null, args); |
| 456 | | } |
| 457 | | ], callback); |
| 458 | | |
| 459 | 21 | if (!readMetadata) { |
| 460 | | // Read metadata as soon as 'progress' listeners are added |
| 461 | | |
| 462 | 19 | if (this.listeners('progress').length > 0) { |
| 463 | | // Read metadata in parallel |
| 464 | 1 | runFfprobe(this); |
| 465 | | } else { |
| 466 | | // Read metadata as soon as the first 'progress' listener is added |
| 467 | 18 | this.once('newListener', function(event) { |
| 468 | 0 | if (event === 'progress') { |
| 469 | 0 | 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 | | */ |
| 487 | 1 | proto.saveToFile = |
| 488 | | proto.save = function(output) { |
| 489 | 16 | _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 | | */ |
| 508 | 1 | proto.writeToStream = |
| 509 | | proto.pipe = |
| 510 | | proto.stream = function(stream, options) { |
| 511 | 3 | if (stream && !('writable' in stream)) { |
| 512 | 1 | options = stream; |
| 513 | 1 | stream = undefined; |
| 514 | | } |
| 515 | | |
| 516 | 3 | if (!stream) { |
| 517 | 1 | if (process.version.match(/v0\.8\./)) { |
| 518 | 0 | throw new Error('PassThrough stream is not supported on node v0.8'); |
| 519 | | } |
| 520 | | |
| 521 | 1 | stream = new PassThrough(); |
| 522 | | } |
| 523 | | |
| 524 | 3 | _process(this, stream, options); |
| 525 | 3 | 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 | | */ |
| 539 | 1 | proto.mergeToFile = function(targetfile) { |
| 540 | 1 | var outputfile = path.normalize(targetfile); |
| 541 | 1 | if(fs.existsSync(outputfile)){ |
| 542 | 0 | return this.emit('error', new Error('Output file already exists, merge aborted')); |
| 543 | | } |
| 544 | | |
| 545 | 1 | var self = this; |
| 546 | | |
| 547 | | // creates intermediate copies of each video. |
| 548 | | function makeIntermediateFile(_mergeSource,_callback) { |
| 549 | 3 | var fname = _mergeSource + '.temp.mpg'; |
| 550 | 3 | var args = self._output.get().concat(['-i', _mergeSource, '-qscale:v', 1, fname]); |
| 551 | | |
| 552 | 3 | self._spawnFfmpeg(args, function(err) { |
| 553 | 3 | _callback(err, fname); |
| 554 | | }); |
| 555 | | } |
| 556 | | |
| 557 | | // concat all created intermediate copies |
| 558 | | function concatIntermediates(target, intermediatesList, _callback) { |
| 559 | 1 | var fname = path.normalize(target) + '.temp.merged.mpg'; |
| 560 | | |
| 561 | 1 | 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 | | |
| 569 | 1 | self._spawnFfmpeg(args, {captureStdout:true,captureStderr:true}, function(err) { |
| 570 | 1 | _callback(err, fname); |
| 571 | | }); |
| 572 | | } |
| 573 | | |
| 574 | | function quantizeConcat(concatResult, numFiles, _callback) { |
| 575 | 1 | var args = [ |
| 576 | | '-i', concatResult, |
| 577 | | '-qscale:v',numFiles, |
| 578 | | targetfile |
| 579 | | ]; |
| 580 | | |
| 581 | 1 | self._spawnFfmpeg(args, function(err) { |
| 582 | 1 | _callback(err); |
| 583 | | }); |
| 584 | | } |
| 585 | | |
| 586 | | function deleteIntermediateFiles(intermediates, callback) { |
| 587 | 2 | async.each(intermediates, function(item,cb){ |
| 588 | 8 | fs.exists(item,function(exists){ |
| 589 | 8 | if(exists){ |
| 590 | 4 | fs.unlink(item ,cb); |
| 591 | | } |
| 592 | | else{ |
| 593 | 4 | cb(); |
| 594 | | } |
| 595 | | |
| 596 | | }); |
| 597 | | }, callback); |
| 598 | | } |
| 599 | | |
| 600 | | function makeProgress() { |
| 601 | 5 | progress.createdFiles = progress.createdFiles + 1; |
| 602 | 5 | progress.percent = progress.createdFiles / progress.totalFiles * 100; |
| 603 | 5 | self.emit('progress', progress); |
| 604 | | } |
| 605 | | |
| 606 | 1 | if (this._inputs.length < 2) { |
| 607 | 0 | return this.emit('error', new Error('No file added to be merged')); |
| 608 | | } |
| 609 | | |
| 610 | 4 | var mergeList = this._inputs.map(function(input) { return input.source; }); |
| 611 | | |
| 612 | 1 | 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 | | |
| 621 | 4 | var toDelete = mergeList.map(function(name) { return name + '.temp.mpg'; }); |
| 622 | 1 | toDelete.push(outputfile + '.temp.merged.mpg'); |
| 623 | 1 | deleteIntermediateFiles(toDelete); |
| 624 | | |
| 625 | 1 | var intermediateFiles = []; |
| 626 | | |
| 627 | 1 | async.whilst( |
| 628 | | function(){ |
| 629 | 4 | return (mergeList.length !== 0); |
| 630 | | }, |
| 631 | | function (callback){ |
| 632 | 3 | makeIntermediateFile(mergeList.shift(), function(err, createdIntermediateFile) { |
| 633 | 3 | if(err) { |
| 634 | 0 | return callback(err); |
| 635 | | } |
| 636 | | |
| 637 | 3 | if(!createdIntermediateFile) { |
| 638 | 0 | return callback(new Error('Invalid intermediate file')); |
| 639 | | } |
| 640 | | |
| 641 | 3 | intermediateFiles.push(createdIntermediateFile); |
| 642 | 3 | makeProgress(); |
| 643 | 3 | callback(); |
| 644 | | }); |
| 645 | | }, |
| 646 | | function(err) { |
| 647 | 1 | if (err) { |
| 648 | 0 | return self.emit('error', err); |
| 649 | | } |
| 650 | | |
| 651 | 1 | concatIntermediates(targetfile, intermediateFiles, function(err, concatResult) { |
| 652 | 1 | if(err) { |
| 653 | 0 | return self.emit('error', err); |
| 654 | | } |
| 655 | | |
| 656 | 1 | if(!concatResult) { |
| 657 | 0 | return self.emit('error', new Error('Invalid concat result file')); |
| 658 | | } |
| 659 | | |
| 660 | 1 | makeProgress(); |
| 661 | 1 | quantizeConcat(concatResult, intermediateFiles.length, function() { |
| 662 | 1 | makeProgress(); |
| 663 | | // add concatResult to intermediates list so it can be deleted too. |
| 664 | 1 | intermediateFiles.push(concatResult); |
| 665 | 1 | deleteIntermediateFiles(intermediateFiles, function(err) { |
| 666 | 1 | if (err) { |
| 667 | 0 | self.emit('error', err); |
| 668 | | } else { |
| 669 | 1 | 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 | | */ |
| 703 | 1 | proto.takeScreenshots = function(config, folder) { |
| 704 | 2 | var width, height; |
| 705 | 2 | var self = this; |
| 706 | | |
| 707 | | function _computeSize(size) { |
| 708 | | // Select video stream with biggest resolution |
| 709 | 2 | var vstream = self._ffprobeData.streams.reduce(function(max, stream) { |
| 710 | 2 | if (stream.codec_type !== 'video') return max; |
| 711 | 2 | return max.width * max.height < stream.width * stream.height ? stream : max; |
| 712 | | }, { width: 0, height: 0 }); |
| 713 | | |
| 714 | 2 | var w = vstream.width; |
| 715 | 2 | var h = vstream.height; |
| 716 | 2 | var a = w / h; |
| 717 | | |
| 718 | 2 | var fixedSize = size.match(/([0-9]+)x([0-9]+)/); |
| 719 | 2 | var fixedWidth = size.match(/([0-9]+)x\?/); |
| 720 | 2 | var fixedHeight = size.match(/\?x([0-9]+)/); |
| 721 | 2 | var percentRatio = size.match(/\b([0-9]{1,3})%/); |
| 722 | | |
| 723 | 2 | if (fixedSize) { |
| 724 | 0 | width = Number(fixedSize[1]); |
| 725 | 0 | height = Number(fixedSize[2]); |
| 726 | 2 | } else if (fixedWidth) { |
| 727 | 2 | width = Number(fixedWidth[1]); |
| 728 | 2 | height = width / a; |
| 729 | 0 | } else if (fixedHeight) { |
| 730 | 0 | height = Number(fixedHeight[1]); |
| 731 | 0 | width = height * a; |
| 732 | | } else { |
| 733 | 0 | var pc = Number(percentRatio[0]) / 100; |
| 734 | 0 | width = w * pc; |
| 735 | 0 | height = h * pc; |
| 736 | | } |
| 737 | | } |
| 738 | | |
| 739 | | function _zeroPad(number, len) { |
| 740 | 4 | len = len-String(number).length+2; |
| 741 | 4 | return new Array(len<0?0:len).join('0')+number; |
| 742 | | } |
| 743 | | |
| 744 | | function _renderOutputName(j, offset) { |
| 745 | 4 | var result = filename; |
| 746 | 4 | if(/%0*i/.test(result)) { |
| 747 | 4 | var numlen = String(result.match(/%(0*)i/)[1]).length; |
| 748 | 4 | result = result.replace(/%0*i/, _zeroPad(j, numlen)); |
| 749 | | } |
| 750 | 4 | result = result.replace('%s', offset); |
| 751 | 4 | result = result.replace('%w', width); |
| 752 | 4 | result = result.replace('%h', height); |
| 753 | 4 | result = result.replace('%r', width+'x'+height); |
| 754 | 4 | result = result.replace('%f', path.basename(inputfile)); |
| 755 | 4 | result = result.replace('%b', path.basename(inputfile, path.extname(inputfile))); |
| 756 | 4 | return result; |
| 757 | | } |
| 758 | | |
| 759 | | function _screenShotInternal() { |
| 760 | 2 | self._prepare(function(err, args) { |
| 761 | 2 | if(err) { |
| 762 | 0 | return self.emit('error', err); |
| 763 | | } |
| 764 | | |
| 765 | 2 | _computeSize(self._sizeData.size); |
| 766 | | |
| 767 | 2 | var duration = 0; |
| 768 | 2 | if (self._ffprobeData && self._ffprobeData.format && self._ffprobeData.format.duration) { |
| 769 | 2 | duration = Number(self._ffprobeData.format.duration); |
| 770 | | } |
| 771 | | |
| 772 | 2 | if (!duration) { |
| 773 | 0 | var errString = 'meta data contains no duration, aborting screenshot creation'; |
| 774 | 0 | return self.emit('error', new Error(errString)); |
| 775 | | } |
| 776 | | |
| 777 | | // check if all timemarks are inside duration |
| 778 | 2 | if (Array.isArray(timemarks)) { |
| 779 | 2 | for (var i = 0; i < timemarks.length; i++) { |
| 780 | | /* convert percentage to seconds */ |
| 781 | 4 | if( timemarks[i].indexOf('%') > 0 ) { |
| 782 | 0 | timemarks[i] = (parseInt(timemarks[i], 10) / 100) * duration; |
| 783 | | } |
| 784 | 4 | if (parseInt(timemarks[i], 10) > duration) { |
| 785 | | // remove timemark from array |
| 786 | 0 | timemarks.splice(i, 1); |
| 787 | 0 | --i; |
| 788 | | } |
| 789 | | } |
| 790 | | // if there are no more timemarks around, add one at end of the file |
| 791 | 2 | if (timemarks.length === 0) { |
| 792 | 0 | timemarks[0] = (duration * 0.9); |
| 793 | | } |
| 794 | | } |
| 795 | | // get positions for screenshots (using duration of file minus 10% to remove fade-in/fade-out) |
| 796 | 2 | var secondOffset = (duration * 0.9) / screenshotcount; |
| 797 | | |
| 798 | | // reset iterator |
| 799 | 2 | var j = 1; |
| 800 | | |
| 801 | 2 | var filenames = []; |
| 802 | | |
| 803 | | // use async helper function to generate all screenshots and |
| 804 | | // fire callback just once after work is done |
| 805 | 2 | async.until( |
| 806 | | function() { |
| 807 | 6 | return j > screenshotcount; |
| 808 | | }, |
| 809 | | function(taskcallback) { |
| 810 | 4 | var offset; |
| 811 | 4 | if (Array.isArray(timemarks)) { |
| 812 | | // get timemark for current iteration |
| 813 | 4 | offset = timemarks[(j - 1)]; |
| 814 | | } else { |
| 815 | 0 | offset = secondOffset * j; |
| 816 | | } |
| 817 | | |
| 818 | 4 | var fname = _renderOutputName(j, offset) + (fileextension ? fileextension : '.jpg'); |
| 819 | 4 | var target = path.join(folder, fname); |
| 820 | | |
| 821 | | // build screenshot command |
| 822 | 4 | 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 | | |
| 834 | 4 | j++; |
| 835 | | |
| 836 | 4 | self._spawnFfmpeg(allArgs, taskcallback); |
| 837 | 4 | filenames.push(fname); |
| 838 | | }, |
| 839 | | function(err) { |
| 840 | 2 | if (err) { |
| 841 | 0 | self.emit('error', err); |
| 842 | | } else { |
| 843 | 2 | self.emit('end', filenames); |
| 844 | | } |
| 845 | | } |
| 846 | | ); |
| 847 | | }, true); |
| 848 | | } |
| 849 | | |
| 850 | 2 | var timemarks, screenshotcount, filename, fileextension; |
| 851 | 2 | if (typeof config === 'object') { |
| 852 | | // use json object as config |
| 853 | 2 | if (config.count) { |
| 854 | 2 | screenshotcount = config.count; |
| 855 | | } |
| 856 | 2 | if (config.timemarks) { |
| 857 | 2 | timemarks = config.timemarks; |
| 858 | | } |
| 859 | 2 | if (config.fileextension){ |
| 860 | 0 | fileextension = config.fileextension; |
| 861 | | } |
| 862 | | } else { |
| 863 | | // assume screenshot count as parameter |
| 864 | 0 | screenshotcount = config; |
| 865 | 0 | timemarks = null; |
| 866 | | } |
| 867 | | |
| 868 | 2 | if (!this._sizeData || !this._sizeData.size) { |
| 869 | 0 | throw new Error('Size must be specified'); |
| 870 | | } |
| 871 | | |
| 872 | 2 | var inputfile = this._currentInput.source; |
| 873 | | |
| 874 | 2 | filename = config.filename || 'tn_%ss'; |
| 875 | 2 | 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 |
| 878 | 1 | filename += '_%i'; |
| 879 | | } |
| 880 | 2 | folder = folder || '.'; |
| 881 | | |
| 882 | | // check target folder |
| 883 | 2 | fs.exists(folder, function(exists) { |
| 884 | 2 | if (!exists) { |
| 885 | 2 | fs.mkdir(folder, '0755', function(err) { |
| 886 | 2 | if (err !== null) { |
| 887 | 0 | self.emit('error', err); |
| 888 | | } else { |
| 889 | 2 | _screenShotInternal(); |
| 890 | | } |
| 891 | | }); |
| 892 | | } else { |
| 893 | 0 | _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 | | */ |
| 910 | 1 | proto.renice = function(niceness) { |
| 911 | 2 | if (!utils.isWindows) { |
| 912 | 2 | niceness = niceness || 0; |
| 913 | | |
| 914 | 2 | if (niceness < -20 || niceness > 20) { |
| 915 | 1 | this.logger.warn('Invalid niceness value: ' + niceness + ', must be between -20 and 20'); |
| 916 | | } |
| 917 | | |
| 918 | 2 | niceness = Math.min(20, Math.max(-20, niceness)); |
| 919 | 2 | this.options.niceness = niceness; |
| 920 | | |
| 921 | 2 | if (this.ffmpegProc) { |
| 922 | 1 | var logger = this.logger; |
| 923 | 1 | var pid = this.ffmpegProc.pid; |
| 924 | 1 | var renice = spawn('renice', [niceness, '-p', pid]); |
| 925 | | |
| 926 | 1 | renice.on('error', function(err) { |
| 927 | 0 | logger.warn('could not renice process ' + pid + ': ' + err.message); |
| 928 | | }); |
| 929 | | |
| 930 | 1 | renice.on('exit', function(code, signal) { |
| 931 | 1 | if (code) { |
| 932 | 0 | logger.warn('could not renice process ' + pid + ': renice exited with ' + code); |
| 933 | 1 | } else if (signal) { |
| 934 | 0 | logger.warn('could not renice process ' + pid + ': renice was killed by signal ' + signal); |
| 935 | | } else { |
| 936 | 1 | logger.info('successfully reniced process ' + pid + ' to ' + niceness + ' niceness'); |
| 937 | | } |
| 938 | | }); |
| 939 | | } |
| 940 | | } |
| 941 | | |
| 942 | 2 | 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 | | */ |
| 955 | 1 | proto.kill = function(signal) { |
| 956 | 3 | if (!this.ffmpegProc) { |
| 957 | 0 | this.options.logger.warn('No running ffmpeg process, cannot send signal'); |
| 958 | | } else { |
| 959 | 3 | this.ffmpegProc.kill(signal || 'SIGKILL'); |
| 960 | | } |
| 961 | | |
| 962 | 3 | return this; |
| 963 | | }; |
| 964 | | }; |
| 965 | | |
lib/utils.js
| Line | Hits | Source |
|---|
| 1 | | /*jshint node:true*/ |
| 2 | | 'use strict'; |
| 3 | | |
| 4 | 1 | var exec = require('child_process').exec; |
| 5 | 1 | var isWindows = require('os').platform().match(/win(32|64)/); |
| 6 | | |
| 7 | 1 | var whichCache = {}; |
| 8 | | |
| 9 | | /** |
| 10 | | * Parse progress line from ffmpeg stderr |
| 11 | | * |
| 12 | | * @param {String} line progress line |
| 13 | | * @return progress object |
| 14 | | * @private |
| 15 | | */ |
| 16 | | function parseProgressLine(line) { |
| 17 | 26 | var progress = {}; |
| 18 | | |
| 19 | | // Remove all spaces after = and trim |
| 20 | 26 | line = line.replace(/=\s+/g, '=').trim(); |
| 21 | 26 | var progressParts = line.split(' '); |
| 22 | | |
| 23 | | // Split every progress part by "=" to get key and value |
| 24 | 26 | for(var i = 0; i < progressParts.length; i++) { |
| 25 | 110 | var progressSplit = progressParts[i].split('=', 2); |
| 26 | 110 | var key = progressSplit[0]; |
| 27 | 110 | var value = progressSplit[1]; |
| 28 | | |
| 29 | | // This is not a progress line |
| 30 | 110 | if(typeof value === 'undefined') |
| 31 | 14 | return null; |
| 32 | | |
| 33 | 96 | progress[key] = value; |
| 34 | | } |
| 35 | | |
| 36 | 12 | return progress; |
| 37 | | } |
| 38 | | |
| 39 | | |
| 40 | 1 | var 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() { |
| 56 | 1434 | var list = []; |
| 57 | 1434 | var argfunc = function() { |
| 58 | 326 | if (arguments.length === 1 && Array.isArray(arguments[0])) { |
| 59 | 90 | list = list.concat(arguments[0]); |
| 60 | | } else { |
| 61 | 236 | list = list.concat([].slice.call(arguments)); |
| 62 | | } |
| 63 | | }; |
| 64 | | |
| 65 | 1434 | argfunc.clear = function() { |
| 66 | 83 | list = []; |
| 67 | | }; |
| 68 | | |
| 69 | 1434 | argfunc.get = function() { |
| 70 | 485 | return list; |
| 71 | | }; |
| 72 | | |
| 73 | 1434 | argfunc.find = function(arg, count) { |
| 74 | 95 | var index = list.indexOf(arg); |
| 75 | 95 | if (index !== -1) { |
| 76 | 69 | return list.slice(index + 1, index + 1 + (count || 0)); |
| 77 | | } |
| 78 | | }; |
| 79 | | |
| 80 | 1434 | argfunc.remove = function(arg, count) { |
| 81 | 0 | var index = list.indexOf(arg); |
| 82 | 0 | if (index !== -1) { |
| 83 | 0 | list.splice(index, (count || 0) + 1); |
| 84 | | } |
| 85 | | }; |
| 86 | | |
| 87 | 1434 | 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) { |
| 101 | 9 | if (name in whichCache) { |
| 102 | 6 | return callback(null, whichCache[name]); |
| 103 | | } |
| 104 | | |
| 105 | 3 | var cmd = 'which ' + name; |
| 106 | 3 | if (isWindows) { |
| 107 | 0 | cmd = 'where ' + name + '.exe'; |
| 108 | | } |
| 109 | | |
| 110 | 3 | exec(cmd, function(err, stdout) { |
| 111 | 3 | if (err) { |
| 112 | | // Treat errors as not found |
| 113 | 0 | callback(null, whichCache[name] = ''); |
| 114 | | } else { |
| 115 | 3 | 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) { |
| 129 | 12 | if(timemark.indexOf(':') === -1 && timemark.indexOf('.') >= 0) |
| 130 | 0 | return Number(timemark); |
| 131 | | |
| 132 | 12 | var parts = timemark.split(':'); |
| 133 | | |
| 134 | | // add seconds |
| 135 | 12 | var secs = Number(parts.pop()); |
| 136 | | |
| 137 | 12 | if (parts.length) { |
| 138 | | // add minutes |
| 139 | 12 | secs += Number(parts.pop()) * 60; |
| 140 | | } |
| 141 | | |
| 142 | 12 | if (parts.length) { |
| 143 | | // add hours |
| 144 | 12 | secs += Number(parts.pop()) * 3600; |
| 145 | | } |
| 146 | | |
| 147 | 12 | 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) { |
| 159 | 11 | var format= /Input #[0-9]+, ([^ ]+),/.exec(stderr); |
| 160 | 11 | var dur = /Duration\: ([^,]+)/.exec(stderr); |
| 161 | 11 | var audio = /Audio\: (.*)/.exec(stderr); |
| 162 | 11 | var video = /Video\: (.*)/.exec(stderr); |
| 163 | 11 | var codecObject = { format: '', audio: '', video: '', duration: '' }; |
| 164 | | |
| 165 | 11 | if (format && format.length > 1) { |
| 166 | 8 | codecObject.format = format[1]; |
| 167 | | } |
| 168 | | |
| 169 | 11 | if (dur && dur.length > 1) { |
| 170 | 8 | codecObject.duration = dur[1]; |
| 171 | | } |
| 172 | | |
| 173 | 11 | if (audio && audio.length > 1) { |
| 174 | 7 | audio = audio[1].split(', '); |
| 175 | 7 | codecObject.audio = audio[0]; |
| 176 | 7 | codecObject.audio_details = audio; |
| 177 | | } |
| 178 | 11 | if (video && video.length > 1) { |
| 179 | 7 | video = video[1].split(', '); |
| 180 | 7 | codecObject.video = video[0]; |
| 181 | 7 | codecObject.video_details = video; |
| 182 | | } |
| 183 | | |
| 184 | 11 | var codecInfoPassed = /Press (\[q\]|ctrl-c) to stop/.test(stderr); |
| 185 | 11 | if (codecInfoPassed) { |
| 186 | 1 | command.emit('codecData', codecObject); |
| 187 | 1 | 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) { |
| 199 | 26 | var lines = stderr.split(/\r\n|\r|\n/g); |
| 200 | 26 | var lastline = lines[lines.length - 2]; |
| 201 | 26 | var progress; |
| 202 | | |
| 203 | 26 | if (lastline) { |
| 204 | 26 | progress = parseProgressLine(lastline); |
| 205 | | } |
| 206 | | |
| 207 | 26 | if (progress) { |
| 208 | | // build progress report object |
| 209 | 12 | 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 |
| 218 | 12 | if (duration && duration > 0) { |
| 219 | 12 | ret.percent = (utils.timemarkToSeconds(ret.timemark) / duration) * 100; |
| 220 | | } |
| 221 | | |
| 222 | 12 | command.emit('progress', ret); |
| 223 | | } |
| 224 | | } |
| 225 | | }; |
| 226 | | |
make[1]: quittant le répertoire « /home/niko/dev/forks/node-fluent-ffmpeg »