Remove fluent-ffmpeg dependency

This commit is contained in:
advplyr 2022-07-06 17:38:19 -05:00
parent 8562b8d1b3
commit b61ecefce4
35 changed files with 4405 additions and 50 deletions

22
package-lock.json generated
View File

@ -510,15 +510,6 @@
"array-back": "^3.0.1"
}
},
"fluent-ffmpeg": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz",
"integrity": "sha512-IZTB4kq5GK0DPp7sGQ0q/BWurGHffRtQQwVkiqDgeO6wYJLLV5ZhgNOQ65loZxxuPMKZKZcICCUnaGtlxBiR0Q==",
"requires": {
"async": ">=0.2.9",
"which": "^1.1.1"
}
},
"follow-redirects": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz",
@ -650,11 +641,6 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"jsonwebtoken": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
@ -1148,14 +1134,6 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"requires": {
"isexe": "^2.0.0"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",

View File

@ -38,7 +38,6 @@
"express": "^4.17.1",
"express-fileupload": "^1.2.1",
"express-rate-limit": "^5.3.0",
"fluent-ffmpeg": "^2.1.2",
"htmlparser2": "^8.0.1",
"jsonwebtoken": "^8.5.1",
"socket.io": "^4.4.1",

19
server/libs/async/LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2010-2018 Caolan McMahon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
(The MIT License)
Copyright (c) 2011-2015 The fluent-ffmpeg contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,665 @@
/*jshint node:true*/
'use strict';
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 ffEncodersRegexp = /\(encoders:([^\)]+)\)/;
var ffDecodersRegexp = /\(decoders:([^\)]+)\)/;
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 cache = {};
module.exports = function (proto) {
/**
* Manually define the ffmpeg binary full path.
*
* @method FfmpegCommand#setFfmpegPath
*
* @param {String} ffmpegPath The full path to the ffmpeg binary.
* @return FfmpegCommand
*/
proto.setFfmpegPath = function (ffmpegPath) {
cache.ffmpegPath = ffmpegPath;
return this;
};
/**
* Manually define the ffprobe binary full path.
*
* @method FfmpegCommand#setFfprobePath
*
* @param {String} ffprobePath The full path to the ffprobe binary.
* @return FfmpegCommand
*/
proto.setFfprobePath = function (ffprobePath) {
cache.ffprobePath = ffprobePath;
return this;
};
/**
* Manually define the flvtool2/flvmeta binary full path.
*
* @method FfmpegCommand#setFlvtoolPath
*
* @param {String} flvtool The full path to the flvtool2 or flvmeta binary.
* @return FfmpegCommand
*/
proto.setFlvtoolPath = function (flvtool) {
cache.flvtoolPath = flvtool;
return this;
};
/**
* Forget executable paths
*
* (only used for testing purposes)
*
* @method FfmpegCommand#_forgetPaths
* @private
*/
proto._forgetPaths = function () {
delete cache.ffmpegPath;
delete cache.ffprobePath;
delete cache.flvtoolPath;
};
/**
* Check for ffmpeg availability
*
* If the FFMPEG_PATH environment variable is set, try to use it.
* If it is unset or incorrect, try to find ffmpeg in the PATH instead.
*
* @method FfmpegCommand#_getFfmpegPath
* @param {Function} callback callback with signature (err, path)
* @private
*/
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, '');
}
});
} 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 {
callback(null, cache.ffmpegPath = (ffmpeg || ''));
}
});
};
/**
* Check for ffprobe availability
*
* If the FFPROBE_PATH environment variable is set, try to use it.
* If it is unset or incorrect, try to find ffprobe in the PATH instead.
* If this still fails, try to find ffprobe in the same directory as ffmpeg.
*
* @method FfmpegCommand#_getFfprobePath
* @param {Function} callback callback with signature (err, path)
* @private
*/
proto._getFfprobePath = function (callback) {
var self = this;
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);
}
utils.which('ffprobe', function (err, ffprobe) {
cb(err, ffprobe);
});
},
// Search in the same directory as ffmpeg
function (ffprobe, cb) {
if (ffprobe.length) {
return cb(null, 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
*
* If the FLVTOOL2_PATH or FLVMETA_PATH environment variable are set, try to use them.
* If both are either unset or incorrect, try to find flvtool2 or flvmeta in the PATH instead.
*
* @method FfmpegCommand#_getFlvtoolPath
* @param {Function} callback callback with signature (err, path)
* @private
*/
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, '');
}
},
// 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, '');
}
},
// 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);
});
},
// 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 || ''));
}
});
};
/**
* A callback passed to {@link FfmpegCommand#availableFilters}.
*
* @callback FfmpegCommand~filterCallback
* @param {Error|null} err error object or null if no error happened
* @param {Object} filters filter object with filter names as keys and the following
* properties for each filter:
* @param {String} filters.description filter description
* @param {String} filters.input input type, one of 'audio', 'video' and 'none'
* @param {Boolean} filters.multipleInputs whether the filter supports multiple inputs
* @param {String} filters.output output type, one of 'audio', 'video' and 'none'
* @param {Boolean} filters.multipleOutputs whether the filter supports multiple outputs
*/
/**
* Query ffmpeg for available filters
*
* @method FfmpegCommand#availableFilters
* @category Capabilities
* @aliases getAvailableFilters
*
* @param {FfmpegCommand~filterCallback} callback callback function
*/
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
};
}
});
callback(null, cache.filters = data);
});
};
/**
* A callback passed to {@link FfmpegCommand#availableCodecs}.
*
* @callback FfmpegCommand~codecCallback
* @param {Error|null} err error object or null if no error happened
* @param {Object} codecs codec object with codec names as keys and the following
* properties for each codec (more properties may be available depending on the
* ffmpeg version used):
* @param {String} codecs.description codec description
* @param {Boolean} codecs.canDecode whether the codec is able to decode streams
* @param {Boolean} codecs.canEncode whether the codec is able to encode streams
*/
/**
* Query ffmpeg for available codecs
*
* @method FfmpegCommand#availableCodecs
* @category Capabilities
* @aliases getAvailableCodecs
*
* @param {FfmpegCommand~codecCallback} callback callback function
*/
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'
};
}
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;
});
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);
});
};
/**
* A callback passed to {@link FfmpegCommand#availableEncoders}.
*
* @callback FfmpegCommand~encodersCallback
* @param {Error|null} err error object or null if no error happened
* @param {Object} encoders encoders object with encoder names as keys and the following
* properties for each encoder:
* @param {String} encoders.description codec description
* @param {Boolean} encoders.type "audio", "video" or "subtitle"
* @param {Boolean} encoders.frameMT whether the encoder is able to do frame-level multithreading
* @param {Boolean} encoders.sliceMT whether the encoder is able to do slice-level multithreading
* @param {Boolean} encoders.experimental whether the encoder is experimental
* @param {Boolean} encoders.drawHorizBand whether the encoder supports draw_horiz_band
* @param {Boolean} encoders.directRendering whether the encoder supports direct encoding method 1
*/
/**
* Query ffmpeg for available encoders
*
* @method FfmpegCommand#availableEncoders
* @category Capabilities
* @aliases getAvailableEncoders
*
* @param {FfmpegCommand~encodersCallback} callback callback function
*/
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'
};
}
});
callback(null, cache.encoders = data);
});
};
/**
* A callback passed to {@link FfmpegCommand#availableFormats}.
*
* @callback FfmpegCommand~formatCallback
* @param {Error|null} err error object or null if no error happened
* @param {Object} formats format object with format names as keys and the following
* properties for each format:
* @param {String} formats.description format description
* @param {Boolean} formats.canDemux whether the format is able to demux streams from an input file
* @param {Boolean} formats.canMux whether the format is able to mux streams into an output file
*/
/**
* Query ffmpeg for available formats
*
* @method FfmpegCommand#availableFormats
* @category Capabilities
* @aliases getAvailableFormats
*
* @param {FfmpegCommand~formatCallback} callback callback function
*/
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);
}
// 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;
}
});
}
});
callback(null, cache.formats = data);
});
};
/**
* Check capabilities before executing a command
*
* Checks whether all used codecs and formats are indeed available
*
* @method FfmpegCommand#_checkCapabilities
* @param {Function} callback callback with signature (err)
* @private
*/
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);
if (format) {
if (!(format[0] in formats) || !(formats[format[0]].canMux)) {
fmts.push(format);
}
}
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'));
}
// 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)) {
fmts.push(format[0]);
}
}
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;
// 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;
}, []);
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]);
}
}
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'));
}
cb();
}
], callback);
};
};

View File

@ -0,0 +1,261 @@
/*jshint node:true, laxcomma:true*/
'use strict';
var spawn = require('child_process').spawn;
function legacyTag(key) { return key.match(/^TAG:/); }
function legacyDisposition(key) { return key.match(/^DISPOSITION:/); }
function parseFfprobeOutput(out) {
var lines = out.split(/\r\n|\r|\n/);
lines = lines.filter(function (line) {
return line.length > 0;
});
var data = {
streams: [],
format: {},
chapters: []
};
function parseBlock(name) {
var data = {};
var line = lines.shift();
while (typeof line !== 'undefined') {
if (line.toLowerCase() == '[/'+name+']') {
return data;
} else if (line.match(/^\[/)) {
line = lines.shift();
continue;
}
var kv = line.match(/^([^=]+)=(.*)$/);
if (kv) {
if (!(kv[1].match(/^TAG:/)) && kv[2].match(/^[0-9]+(\.[0-9]+)?$/)) {
data[kv[1]] = Number(kv[2]);
} else {
data[kv[1]] = kv[2];
}
}
line = lines.shift();
}
return data;
}
var line = lines.shift();
while (typeof line !== 'undefined') {
if (line.match(/^\[stream/i)) {
var stream = parseBlock('stream');
data.streams.push(stream);
} else if (line.match(/^\[chapter/i)) {
var chapter = parseBlock('chapter');
data.chapters.push(chapter);
} else if (line.toLowerCase() === '[format]') {
data.format = parseBlock('format');
}
line = lines.shift();
}
return data;
}
module.exports = function(proto) {
/**
* A callback passed to the {@link FfmpegCommand#ffprobe} method.
*
* @callback FfmpegCommand~ffprobeCallback
*
* @param {Error|null} err error object or null if no error happened
* @param {Object} ffprobeData ffprobe output data; this object
* has the same format as what the following command returns:
*
* `ffprobe -print_format json -show_streams -show_format INPUTFILE`
* @param {Array} ffprobeData.streams stream information
* @param {Object} ffprobeData.format format information
*/
/**
* Run ffprobe on last specified input
*
* @method FfmpegCommand#ffprobe
* @category Metadata
*
* @param {?Number} [index] 0-based index of input to probe (defaults to last input)
* @param {?String[]} [options] array of output options to return
* @param {FfmpegCommand~ffprobeCallback} callback callback function
*
*/
proto.ffprobe = function() {
var input, index = null, options = [], callback;
// the last argument should be the callback
var callback = arguments[arguments.length - 1];
var ended = false
function handleCallback(err, data) {
if (!ended) {
ended = true;
callback(err, data);
}
};
// map the arguments to the correct variable names
switch (arguments.length) {
case 3:
index = arguments[0];
options = arguments[1];
break;
case 2:
if (typeof arguments[0] === 'number') {
index = arguments[0];
} else if (Array.isArray(arguments[0])) {
options = arguments[0];
}
break;
}
if (index === null) {
if (!this._currentInput) {
return handleCallback(new Error('No input specified'));
}
input = this._currentInput;
} else {
input = this._inputs[index];
if (!input) {
return handleCallback(new Error('Invalid input index'));
}
}
// Find ffprobe
this._getFfprobePath(function(err, path) {
if (err) {
return handleCallback(err);
} else if (!path) {
return handleCallback(new Error('Cannot find ffprobe'));
}
var stdout = '';
var stdoutClosed = false;
var stderr = '';
var stderrClosed = false;
// Spawn ffprobe
var src = input.isStream ? 'pipe:0' : input.source;
var ffprobe = spawn(path, ['-show_streams', '-show_format'].concat(options, src), {windowsHide: true});
if (input.isStream) {
// Skip errors on stdin. These get thrown when ffprobe is complete and
// there seems to be no way hook in and close stdin before it throws.
ffprobe.stdin.on('error', function(err) {
if (['ECONNRESET', 'EPIPE', 'EOF'].indexOf(err.code) >= 0) { return; }
handleCallback(err);
});
// Once ffprobe's input stream closes, we need no more data from the
// input
ffprobe.stdin.on('close', function() {
input.source.pause();
input.source.unpipe(ffprobe.stdin);
});
input.source.pipe(ffprobe.stdin);
}
ffprobe.on('error', callback);
// Ensure we wait for captured streams to end before calling callback
var exitError = null;
function handleExit(err) {
if (err) {
exitError = err;
}
if (processExited && stdoutClosed && stderrClosed) {
if (exitError) {
if (stderr) {
exitError.message += '\n' + stderr;
}
return handleCallback(exitError);
}
// Process output
var data = parseFfprobeOutput(stdout);
// Handle legacy output with "TAG:x" and "DISPOSITION:x" keys
[data.format].concat(data.streams).forEach(function(target) {
if (target) {
var legacyTagKeys = Object.keys(target).filter(legacyTag);
if (legacyTagKeys.length) {
target.tags = target.tags || {};
legacyTagKeys.forEach(function(tagKey) {
target.tags[tagKey.substr(4)] = target[tagKey];
delete target[tagKey];
});
}
var legacyDispositionKeys = Object.keys(target).filter(legacyDisposition);
if (legacyDispositionKeys.length) {
target.disposition = target.disposition || {};
legacyDispositionKeys.forEach(function(dispositionKey) {
target.disposition[dispositionKey.substr(12)] = target[dispositionKey];
delete target[dispositionKey];
});
}
}
});
handleCallback(null, data);
}
}
// Handle ffprobe exit
var processExited = false;
ffprobe.on('exit', function(code, signal) {
processExited = true;
if (code) {
handleExit(new Error('ffprobe exited with code ' + code));
} else if (signal) {
handleExit(new Error('ffprobe was killed with signal ' + signal));
} else {
handleExit();
}
});
// Handle stdout/stderr streams
ffprobe.stdout.on('data', function(data) {
stdout += data;
});
ffprobe.stdout.on('close', function() {
stdoutClosed = true;
handleExit();
});
ffprobe.stderr.on('data', function(data) {
stderr += data;
});
ffprobe.stderr.on('close', function() {
stderrClosed = true;
handleExit();
});
});
};
};

View File

@ -0,0 +1,231 @@
/*jshint node:true*/
'use strict';
//
// modified for use with audiobookshelf
// Source: https://github.com/fluent-ffmpeg/node-fluent-ffmpeg
//
var path = require('path');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var utils = require('./utils');
var ARGLISTS = ['_global', '_audio', '_audioFilters', '_video', '_videoFilters', '_sizeFilters', '_complexFilters'];
/**
* Create an ffmpeg command
*
* Can be called with or without the 'new' operator, and the 'input' parameter
* may be specified as 'options.source' instead (or passed later with the
* addInput method).
*
* @constructor
* @param {String|ReadableStream} [input] input file path or readable stream
* @param {Object} [options] command options
* @param {Object} [options.logger=<no logging>] logger object with 'error', 'warning', 'info' and 'debug' methods
* @param {Number} [options.niceness=0] ffmpeg process niceness, ignored on Windows
* @param {Number} [options.priority=0] alias for `niceness`
* @param {String} [options.presets="fluent-ffmpeg/lib/presets"] directory to load presets from
* @param {String} [options.preset="fluent-ffmpeg/lib/presets"] alias for `presets`
* @param {String} [options.stdoutLines=100] maximum lines of ffmpeg output to keep in memory, use 0 for unlimited
* @param {Number} [options.timeout=<no timeout>] ffmpeg processing timeout in seconds
* @param {String|ReadableStream} [options.source=<no input>] alias for the `input` parameter
*/
function FfmpegCommand(input, options) {
// Make 'new' optional
if (!(this instanceof FfmpegCommand)) {
return new FfmpegCommand(input, options);
}
EventEmitter.call(this);
if (typeof input === 'object' && !('readable' in input)) {
// Options object passed directly
options = input;
} else {
// Input passed first
options = options || {};
options.source = input;
}
// Add input if present
this._inputs = [];
if (options.source) {
this.input(options.source);
}
// Add target-less output for backwards compatibility
this._outputs = [];
this.output();
// Create argument lists
var self = this;
['_global', '_complexFilters'].forEach(function (prop) {
self[prop] = utils.args();
});
// Set default option values
options.stdoutLines = 'stdoutLines' in options ? options.stdoutLines : 100;
options.presets = options.presets || options.preset || path.join(__dirname, 'presets');
options.niceness = options.niceness || options.priority || 0;
// Save options
this.options = options;
// Setup logger
this.logger = options.logger || {
debug: function () { },
info: function () { },
warn: function () { },
error: function () { }
};
}
util.inherits(FfmpegCommand, EventEmitter);
module.exports = FfmpegCommand;
/**
* Clone an ffmpeg command
*
* This method is useful when you want to process the same input multiple times.
* It returns a new FfmpegCommand instance with the exact same options.
*
* All options set _after_ the clone() call will only be applied to the instance
* it has been called on.
*
* @example
* var command = ffmpeg('/path/to/source.avi')
* .audioCodec('libfaac')
* .videoCodec('libx264')
* .format('mp4');
*
* command.clone()
* .size('320x200')
* .save('/path/to/output-small.mp4');
*
* command.clone()
* .size('640x400')
* .save('/path/to/output-medium.mp4');
*
* command.save('/path/to/output-original-size.mp4');
*
* @method FfmpegCommand#clone
* @return FfmpegCommand
*/
FfmpegCommand.prototype.clone = function () {
var clone = new FfmpegCommand();
var self = this;
// Clone options and logger
clone.options = this.options;
clone.logger = this.logger;
// Clone inputs
clone._inputs = this._inputs.map(function (input) {
return {
source: input.source,
options: input.options.clone()
};
});
// Create first output
if ('target' in this._outputs[0]) {
// We have outputs set, don't clone them and create first output
clone._outputs = [];
clone.output();
} else {
// No outputs set, clone first output options
clone._outputs = [
clone._currentOutput = {
flags: {}
}
];
['audio', 'audioFilters', 'video', 'videoFilters', 'sizeFilters', 'options'].forEach(function (key) {
clone._currentOutput[key] = self._currentOutput[key].clone();
});
if (this._currentOutput.sizeData) {
clone._currentOutput.sizeData = {};
utils.copy(this._currentOutput.sizeData, clone._currentOutput.sizeData);
}
utils.copy(this._currentOutput.flags, clone._currentOutput.flags);
}
// Clone argument lists
['_global', '_complexFilters'].forEach(function (prop) {
clone[prop] = self[prop].clone();
});
return clone;
};
/* Add methods from options submodules */
require('./options/inputs')(FfmpegCommand.prototype);
require('./options/audio')(FfmpegCommand.prototype);
require('./options/video')(FfmpegCommand.prototype);
require('./options/videosize')(FfmpegCommand.prototype);
require('./options/output')(FfmpegCommand.prototype);
require('./options/custom')(FfmpegCommand.prototype);
require('./options/misc')(FfmpegCommand.prototype);
/* Add processor methods */
require('./processor')(FfmpegCommand.prototype);
/* Add capabilities methods */
require('./capabilities')(FfmpegCommand.prototype);
FfmpegCommand.setFfmpegPath = function (path) {
(new FfmpegCommand()).setFfmpegPath(path);
};
FfmpegCommand.setFfprobePath = function (path) {
(new FfmpegCommand()).setFfprobePath(path);
};
FfmpegCommand.setFlvtoolPath = function (path) {
(new FfmpegCommand()).setFlvtoolPath(path);
};
FfmpegCommand.availableFilters =
FfmpegCommand.getAvailableFilters = function (callback) {
(new FfmpegCommand()).availableFilters(callback);
};
FfmpegCommand.availableCodecs =
FfmpegCommand.getAvailableCodecs = function (callback) {
(new FfmpegCommand()).availableCodecs(callback);
};
FfmpegCommand.availableFormats =
FfmpegCommand.getAvailableFormats = function (callback) {
(new FfmpegCommand()).availableFormats(callback);
};
FfmpegCommand.availableEncoders =
FfmpegCommand.getAvailableEncoders = function (callback) {
(new FfmpegCommand()).availableEncoders(callback);
};
/* Add ffprobe methods */
require('./ffprobe')(FfmpegCommand.prototype);
FfmpegCommand.ffprobe = function (file) {
var instance = new FfmpegCommand(file);
instance.ffprobe.apply(instance, Array.prototype.slice.call(arguments, 1));
};
/* Add processing recipes */
require('./recipes')(FfmpegCommand.prototype);

View File

@ -0,0 +1,178 @@
/*jshint node:true*/
'use strict';
var utils = require('../utils');
/*
*! Audio-related methods
*/
module.exports = function(proto) {
/**
* Disable audio in the output
*
* @method FfmpegCommand#noAudio
* @category Audio
* @aliases withNoAudio
* @return FfmpegCommand
*/
proto.withNoAudio =
proto.noAudio = function() {
this._currentOutput.audio.clear();
this._currentOutput.audioFilters.clear();
this._currentOutput.audio('-an');
return this;
};
/**
* Specify audio codec
*
* @method FfmpegCommand#audioCodec
* @category Audio
* @aliases withAudioCodec
*
* @param {String} codec audio codec name
* @return FfmpegCommand
*/
proto.withAudioCodec =
proto.audioCodec = function(codec) {
this._currentOutput.audio('-acodec', codec);
return this;
};
/**
* Specify audio bitrate
*
* @method FfmpegCommand#audioBitrate
* @category Audio
* @aliases withAudioBitrate
*
* @param {String|Number} bitrate audio bitrate in kbps (with an optional 'k' suffix)
* @return FfmpegCommand
*/
proto.withAudioBitrate =
proto.audioBitrate = function(bitrate) {
this._currentOutput.audio('-b:a', ('' + bitrate).replace(/k?$/, 'k'));
return this;
};
/**
* Specify audio channel count
*
* @method FfmpegCommand#audioChannels
* @category Audio
* @aliases withAudioChannels
*
* @param {Number} channels channel count
* @return FfmpegCommand
*/
proto.withAudioChannels =
proto.audioChannels = function(channels) {
this._currentOutput.audio('-ac', channels);
return this;
};
/**
* Specify audio frequency
*
* @method FfmpegCommand#audioFrequency
* @category Audio
* @aliases withAudioFrequency
*
* @param {Number} freq audio frequency in Hz
* @return FfmpegCommand
*/
proto.withAudioFrequency =
proto.audioFrequency = function(freq) {
this._currentOutput.audio('-ar', freq);
return this;
};
/**
* Specify audio quality
*
* @method FfmpegCommand#audioQuality
* @category Audio
* @aliases withAudioQuality
*
* @param {Number} quality audio quality factor
* @return FfmpegCommand
*/
proto.withAudioQuality =
proto.audioQuality = function(quality) {
this._currentOutput.audio('-aq', quality);
return this;
};
/**
* Specify custom audio filter(s)
*
* Can be called both with one or many filters, or a filter array.
*
* @example
* command.audioFilters('filter1');
*
* @example
* command.audioFilters('filter1', 'filter2=param1=value1:param2=value2');
*
* @example
* command.audioFilters(['filter1', 'filter2']);
*
* @example
* command.audioFilters([
* {
* filter: 'filter1'
* },
* {
* filter: 'filter2',
* options: 'param=value:param=value'
* }
* ]);
*
* @example
* command.audioFilters(
* {
* filter: 'filter1',
* options: ['value1', 'value2']
* },
* {
* filter: 'filter2',
* options: { param1: 'value1', param2: 'value2' }
* }
* );
*
* @method FfmpegCommand#audioFilters
* @aliases withAudioFilter,withAudioFilters,audioFilter
* @category Audio
*
* @param {...String|String[]|Object[]} filters audio filter strings, string array or
* filter specification array, each with the following properties:
* @param {String} filters.filter filter name
* @param {String|String[]|Object} [filters.options] filter option string, array, or object
* @return FfmpegCommand
*/
proto.withAudioFilter =
proto.withAudioFilters =
proto.audioFilter =
proto.audioFilters = function(filters) {
if (arguments.length > 1) {
filters = [].slice.call(arguments);
}
if (!Array.isArray(filters)) {
filters = [filters];
}
this._currentOutput.audioFilters(utils.makeFilterStrings(filters));
return this;
};
};

View File

@ -0,0 +1,212 @@
/*jshint node:true*/
'use strict';
var utils = require('../utils');
/*
*! Custom options methods
*/
module.exports = function(proto) {
/**
* Add custom input option(s)
*
* When passing a single string or an array, each string containing two
* words is split (eg. inputOptions('-option value') is supported) for
* compatibility reasons. This is not the case when passing more than
* one argument.
*
* @example
* command.inputOptions('option1');
*
* @example
* command.inputOptions('option1', 'option2');
*
* @example
* command.inputOptions(['option1', 'option2']);
*
* @method FfmpegCommand#inputOptions
* @category Custom options
* @aliases addInputOption,addInputOptions,withInputOption,withInputOptions,inputOption
*
* @param {...String} options option string(s) or string array
* @return FfmpegCommand
*/
proto.addInputOption =
proto.addInputOptions =
proto.withInputOption =
proto.withInputOptions =
proto.inputOption =
proto.inputOptions = function(options) {
if (!this._currentInput) {
throw new Error('No input specified');
}
var doSplit = true;
if (arguments.length > 1) {
options = [].slice.call(arguments);
doSplit = false;
}
if (!Array.isArray(options)) {
options = [options];
}
this._currentInput.options(options.reduce(function(options, option) {
var split = String(option).split(' ');
if (doSplit && split.length === 2) {
options.push(split[0], split[1]);
} else {
options.push(option);
}
return options;
}, []));
return this;
};
/**
* Add custom output option(s)
*
* @example
* command.outputOptions('option1');
*
* @example
* command.outputOptions('option1', 'option2');
*
* @example
* command.outputOptions(['option1', 'option2']);
*
* @method FfmpegCommand#outputOptions
* @category Custom options
* @aliases addOutputOption,addOutputOptions,addOption,addOptions,withOutputOption,withOutputOptions,withOption,withOptions,outputOption
*
* @param {...String} options option string(s) or string array
* @return FfmpegCommand
*/
proto.addOutputOption =
proto.addOutputOptions =
proto.addOption =
proto.addOptions =
proto.withOutputOption =
proto.withOutputOptions =
proto.withOption =
proto.withOptions =
proto.outputOption =
proto.outputOptions = function(options) {
var doSplit = true;
if (arguments.length > 1) {
options = [].slice.call(arguments);
doSplit = false;
}
if (!Array.isArray(options)) {
options = [options];
}
this._currentOutput.options(options.reduce(function(options, option) {
var split = String(option).split(' ');
if (doSplit && split.length === 2) {
options.push(split[0], split[1]);
} else {
options.push(option);
}
return options;
}, []));
return this;
};
/**
* Specify a complex filtergraph
*
* Calling this method will override any previously set filtergraph, but you can set
* as many filters as needed in one call.
*
* @example <caption>Overlay an image over a video (using a filtergraph string)</caption>
* ffmpeg()
* .input('video.avi')
* .input('image.png')
* .complexFilter('[0:v][1:v]overlay[out]', ['out']);
*
* @example <caption>Overlay an image over a video (using a filter array)</caption>
* ffmpeg()
* .input('video.avi')
* .input('image.png')
* .complexFilter([{
* filter: 'overlay',
* inputs: ['0:v', '1:v'],
* outputs: ['out']
* }], ['out']);
*
* @example <caption>Split video into RGB channels and output a 3x1 video with channels side to side</caption>
* ffmpeg()
* .input('video.avi')
* .complexFilter([
* // Duplicate video stream 3 times into streams a, b, and c
* { filter: 'split', options: '3', outputs: ['a', 'b', 'c'] },
*
* // Create stream 'red' by cancelling green and blue channels from stream 'a'
* { filter: 'lutrgb', options: { g: 0, b: 0 }, inputs: 'a', outputs: 'red' },
*
* // Create stream 'green' by cancelling red and blue channels from stream 'b'
* { filter: 'lutrgb', options: { r: 0, b: 0 }, inputs: 'b', outputs: 'green' },
*
* // Create stream 'blue' by cancelling red and green channels from stream 'c'
* { filter: 'lutrgb', options: { r: 0, g: 0 }, inputs: 'c', outputs: 'blue' },
*
* // Pad stream 'red' to 3x width, keeping the video on the left, and name output 'padded'
* { filter: 'pad', options: { w: 'iw*3', h: 'ih' }, inputs: 'red', outputs: 'padded' },
*
* // Overlay 'green' onto 'padded', moving it to the center, and name output 'redgreen'
* { filter: 'overlay', options: { x: 'w', y: 0 }, inputs: ['padded', 'green'], outputs: 'redgreen'},
*
* // Overlay 'blue' onto 'redgreen', moving it to the right
* { filter: 'overlay', options: { x: '2*w', y: 0 }, inputs: ['redgreen', 'blue']},
* ]);
*
* @method FfmpegCommand#complexFilter
* @category Custom options
* @aliases filterGraph
*
* @param {String|Array} spec filtergraph string or array of filter specification
* objects, each having the following properties:
* @param {String} spec.filter filter name
* @param {String|Array} [spec.inputs] (array of) input stream specifier(s) for the filter,
* defaults to ffmpeg automatically choosing the first unused matching streams
* @param {String|Array} [spec.outputs] (array of) output stream specifier(s) for the filter,
* defaults to ffmpeg automatically assigning the output to the output file
* @param {Object|String|Array} [spec.options] filter options, can be omitted to not set any options
* @param {Array} [map] (array of) stream specifier(s) from the graph to include in
* ffmpeg output, defaults to ffmpeg automatically choosing the first matching streams.
* @return FfmpegCommand
*/
proto.filterGraph =
proto.complexFilter = function(spec, map) {
this._complexFilters.clear();
if (!Array.isArray(spec)) {
spec = [spec];
}
this._complexFilters('-filter_complex', utils.makeFilterStrings(spec).join(';'));
if (Array.isArray(map)) {
var self = this;
map.forEach(function(streamSpec) {
self._complexFilters('-map', streamSpec.replace(utils.streamRegexp, '[$1]'));
});
} else if (typeof map === 'string') {
this._complexFilters('-map', map.replace(utils.streamRegexp, '[$1]'));
}
return this;
};
};

View File

@ -0,0 +1,178 @@
/*jshint node:true*/
'use strict';
var utils = require('../utils');
/*
*! Input-related methods
*/
module.exports = function(proto) {
/**
* Add an input to command
*
* Also switches "current input", that is the input that will be affected
* by subsequent input-related methods.
*
* Note: only one stream input is supported for now.
*
* @method FfmpegCommand#input
* @category Input
* @aliases mergeAdd,addInput
*
* @param {String|Readable} source input file path or readable stream
* @return FfmpegCommand
*/
proto.mergeAdd =
proto.addInput =
proto.input = function(source) {
var isFile = false;
var isStream = false;
if (typeof source !== 'string') {
if (!('readable' in source) || !(source.readable)) {
throw new Error('Invalid input');
}
var hasInputStream = this._inputs.some(function(input) {
return input.isStream;
});
if (hasInputStream) {
throw new Error('Only one input stream is supported');
}
isStream = true;
source.pause();
} else {
var protocol = source.match(/^([a-z]{2,}):/i);
isFile = !protocol || protocol[0] === 'file';
}
this._inputs.push(this._currentInput = {
source: source,
isFile: isFile,
isStream: isStream,
options: utils.args()
});
return this;
};
/**
* Specify input format for the last specified input
*
* @method FfmpegCommand#inputFormat
* @category Input
* @aliases withInputFormat,fromFormat
*
* @param {String} format input format
* @return FfmpegCommand
*/
proto.withInputFormat =
proto.inputFormat =
proto.fromFormat = function(format) {
if (!this._currentInput) {
throw new Error('No input specified');
}
this._currentInput.options('-f', format);
return this;
};
/**
* Specify input FPS for the last specified input
* (only valid for raw video formats)
*
* @method FfmpegCommand#inputFps
* @category Input
* @aliases withInputFps,withInputFPS,withFpsInput,withFPSInput,inputFPS,inputFps,fpsInput
*
* @param {Number} fps input FPS
* @return FfmpegCommand
*/
proto.withInputFps =
proto.withInputFPS =
proto.withFpsInput =
proto.withFPSInput =
proto.inputFPS =
proto.inputFps =
proto.fpsInput =
proto.FPSInput = function(fps) {
if (!this._currentInput) {
throw new Error('No input specified');
}
this._currentInput.options('-r', fps);
return this;
};
/**
* Use native framerate for the last specified input
*
* @method FfmpegCommand#native
* @category Input
* @aliases nativeFramerate,withNativeFramerate
*
* @return FfmmegCommand
*/
proto.nativeFramerate =
proto.withNativeFramerate =
proto.native = function() {
if (!this._currentInput) {
throw new Error('No input specified');
}
this._currentInput.options('-re');
return this;
};
/**
* Specify input seek time for the last specified input
*
* @method FfmpegCommand#seekInput
* @category Input
* @aliases setStartTime,seekTo
*
* @param {String|Number} seek seek time in seconds or as a '[hh:[mm:]]ss[.xxx]' string
* @return FfmpegCommand
*/
proto.setStartTime =
proto.seekInput = function(seek) {
if (!this._currentInput) {
throw new Error('No input specified');
}
this._currentInput.options('-ss', seek);
return this;
};
/**
* Loop over the last specified input
*
* @method FfmpegCommand#loop
* @category Input
*
* @param {String|Number} [duration] loop duration in seconds or as a '[[hh:]mm:]ss[.xxx]' string
* @return FfmpegCommand
*/
proto.loop = function(duration) {
if (!this._currentInput) {
throw new Error('No input specified');
}
this._currentInput.options('-loop', '1');
if (typeof duration !== 'undefined') {
this.duration(duration);
}
return this;
};
};

View File

@ -0,0 +1,41 @@
/*jshint node:true*/
'use strict';
var path = require('path');
/*
*! Miscellaneous methods
*/
module.exports = function(proto) {
/**
* Use preset
*
* @method FfmpegCommand#preset
* @category Miscellaneous
* @aliases usingPreset
*
* @param {String|Function} preset preset name or preset function
*/
proto.usingPreset =
proto.preset = function(preset) {
if (typeof preset === 'function') {
preset(this);
} else {
try {
var modulePath = path.join(this.options.presets, preset);
var module = require(modulePath);
if (typeof module.load === 'function') {
module.load(this);
} else {
throw new Error('preset ' + modulePath + ' has no load() function');
}
} catch (err) {
throw new Error('preset ' + modulePath + ' could not be loaded: ' + err.message);
}
}
return this;
};
};

View File

@ -0,0 +1,162 @@
/*jshint node:true*/
'use strict';
var utils = require('../utils');
/*
*! Output-related methods
*/
module.exports = function(proto) {
/**
* Add output
*
* @method FfmpegCommand#output
* @category Output
* @aliases addOutput
*
* @param {String|Writable} target target file path or writable stream
* @param {Object} [pipeopts={}] pipe options (only applies to streams)
* @return FfmpegCommand
*/
proto.addOutput =
proto.output = function(target, pipeopts) {
var isFile = false;
if (!target && this._currentOutput) {
// No target is only allowed when called from constructor
throw new Error('Invalid output');
}
if (target && typeof target !== 'string') {
if (!('writable' in target) || !(target.writable)) {
throw new Error('Invalid output');
}
} else if (typeof target === 'string') {
var protocol = target.match(/^([a-z]{2,}):/i);
isFile = !protocol || protocol[0] === 'file';
}
if (target && !('target' in this._currentOutput)) {
// For backwards compatibility, set target for first output
this._currentOutput.target = target;
this._currentOutput.isFile = isFile;
this._currentOutput.pipeopts = pipeopts || {};
} else {
if (target && typeof target !== 'string') {
var hasOutputStream = this._outputs.some(function(output) {
return typeof output.target !== 'string';
});
if (hasOutputStream) {
throw new Error('Only one output stream is supported');
}
}
this._outputs.push(this._currentOutput = {
target: target,
isFile: isFile,
flags: {},
pipeopts: pipeopts || {}
});
var self = this;
['audio', 'audioFilters', 'video', 'videoFilters', 'sizeFilters', 'options'].forEach(function(key) {
self._currentOutput[key] = utils.args();
});
if (!target) {
// Call from constructor: remove target key
delete this._currentOutput.target;
}
}
return this;
};
/**
* Specify output seek time
*
* @method FfmpegCommand#seek
* @category Input
* @aliases seekOutput
*
* @param {String|Number} seek seek time in seconds or as a '[hh:[mm:]]ss[.xxx]' string
* @return FfmpegCommand
*/
proto.seekOutput =
proto.seek = function(seek) {
this._currentOutput.options('-ss', seek);
return this;
};
/**
* Set output duration
*
* @method FfmpegCommand#duration
* @category Output
* @aliases withDuration,setDuration
*
* @param {String|Number} duration duration in seconds or as a '[[hh:]mm:]ss[.xxx]' string
* @return FfmpegCommand
*/
proto.withDuration =
proto.setDuration =
proto.duration = function(duration) {
this._currentOutput.options('-t', duration);
return this;
};
/**
* Set output format
*
* @method FfmpegCommand#format
* @category Output
* @aliases toFormat,withOutputFormat,outputFormat
*
* @param {String} format output format name
* @return FfmpegCommand
*/
proto.toFormat =
proto.withOutputFormat =
proto.outputFormat =
proto.format = function(format) {
this._currentOutput.options('-f', format);
return this;
};
/**
* Add stream mapping to output
*
* @method FfmpegCommand#map
* @category Output
*
* @param {String} spec stream specification string, with optional square brackets
* @return FfmpegCommand
*/
proto.map = function(spec) {
this._currentOutput.options('-map', spec.replace(utils.streamRegexp, '[$1]'));
return this;
};
/**
* Run flvtool2/flvmeta on output
*
* @method FfmpegCommand#flvmeta
* @category Output
* @aliases updateFlvMetadata
*
* @return FfmpegCommand
*/
proto.updateFlvMetadata =
proto.flvmeta = function() {
this._currentOutput.flags.flvmeta = true;
return this;
};
};

View File

@ -0,0 +1,184 @@
/*jshint node:true*/
'use strict';
var utils = require('../utils');
/*
*! Video-related methods
*/
module.exports = function(proto) {
/**
* Disable video in the output
*
* @method FfmpegCommand#noVideo
* @category Video
* @aliases withNoVideo
*
* @return FfmpegCommand
*/
proto.withNoVideo =
proto.noVideo = function() {
this._currentOutput.video.clear();
this._currentOutput.videoFilters.clear();
this._currentOutput.video('-vn');
return this;
};
/**
* Specify video codec
*
* @method FfmpegCommand#videoCodec
* @category Video
* @aliases withVideoCodec
*
* @param {String} codec video codec name
* @return FfmpegCommand
*/
proto.withVideoCodec =
proto.videoCodec = function(codec) {
this._currentOutput.video('-vcodec', codec);
return this;
};
/**
* Specify video bitrate
*
* @method FfmpegCommand#videoBitrate
* @category Video
* @aliases withVideoBitrate
*
* @param {String|Number} bitrate video bitrate in kbps (with an optional 'k' suffix)
* @param {Boolean} [constant=false] enforce constant bitrate
* @return FfmpegCommand
*/
proto.withVideoBitrate =
proto.videoBitrate = function(bitrate, constant) {
bitrate = ('' + bitrate).replace(/k?$/, 'k');
this._currentOutput.video('-b:v', bitrate);
if (constant) {
this._currentOutput.video(
'-maxrate', bitrate,
'-minrate', bitrate,
'-bufsize', '3M'
);
}
return this;
};
/**
* Specify custom video filter(s)
*
* Can be called both with one or many filters, or a filter array.
*
* @example
* command.videoFilters('filter1');
*
* @example
* command.videoFilters('filter1', 'filter2=param1=value1:param2=value2');
*
* @example
* command.videoFilters(['filter1', 'filter2']);
*
* @example
* command.videoFilters([
* {
* filter: 'filter1'
* },
* {
* filter: 'filter2',
* options: 'param=value:param=value'
* }
* ]);
*
* @example
* command.videoFilters(
* {
* filter: 'filter1',
* options: ['value1', 'value2']
* },
* {
* filter: 'filter2',
* options: { param1: 'value1', param2: 'value2' }
* }
* );
*
* @method FfmpegCommand#videoFilters
* @category Video
* @aliases withVideoFilter,withVideoFilters,videoFilter
*
* @param {...String|String[]|Object[]} filters video filter strings, string array or
* filter specification array, each with the following properties:
* @param {String} filters.filter filter name
* @param {String|String[]|Object} [filters.options] filter option string, array, or object
* @return FfmpegCommand
*/
proto.withVideoFilter =
proto.withVideoFilters =
proto.videoFilter =
proto.videoFilters = function(filters) {
if (arguments.length > 1) {
filters = [].slice.call(arguments);
}
if (!Array.isArray(filters)) {
filters = [filters];
}
this._currentOutput.videoFilters(utils.makeFilterStrings(filters));
return this;
};
/**
* Specify output FPS
*
* @method FfmpegCommand#fps
* @category Video
* @aliases withOutputFps,withOutputFPS,withFpsOutput,withFPSOutput,withFps,withFPS,outputFPS,outputFps,fpsOutput,FPSOutput,FPS
*
* @param {Number} fps output FPS
* @return FfmpegCommand
*/
proto.withOutputFps =
proto.withOutputFPS =
proto.withFpsOutput =
proto.withFPSOutput =
proto.withFps =
proto.withFPS =
proto.outputFPS =
proto.outputFps =
proto.fpsOutput =
proto.FPSOutput =
proto.fps =
proto.FPS = function(fps) {
this._currentOutput.video('-r', fps);
return this;
};
/**
* Only transcode a certain number of frames
*
* @method FfmpegCommand#frames
* @category Video
* @aliases takeFrames,withFrames
*
* @param {Number} frames frame count
* @return FfmpegCommand
*/
proto.takeFrames =
proto.withFrames =
proto.frames = function(frames) {
this._currentOutput.video('-vframes', frames);
return this;
};
};

View File

@ -0,0 +1,291 @@
/*jshint node:true*/
'use strict';
/*
*! Size helpers
*/
/**
* Return filters to pad video to width*height,
*
* @param {Number} width output width
* @param {Number} height output height
* @param {Number} aspect video aspect ratio (without padding)
* @param {Number} color padding color
* @return scale/pad filters
* @private
*/
function getScalePadFilters(width, height, aspect, color) {
/*
let a be the input aspect ratio, A be the requested aspect ratio
if a > A, padding is done on top and bottom
if a < A, padding is done on left and right
*/
return [
/*
In both cases, we first have to scale the input to match the requested size.
When using computed width/height, we truncate them to multiples of 2
*/
{
filter: 'scale',
options: {
w: 'if(gt(a,' + aspect + '),' + width + ',trunc(' + height + '*a/2)*2)',
h: 'if(lt(a,' + aspect + '),' + height + ',trunc(' + width + '/a/2)*2)'
}
},
/*
Then we pad the scaled input to match the target size
(here iw and ih refer to the padding input, i.e the scaled output)
*/
{
filter: 'pad',
options: {
w: width,
h: height,
x: 'if(gt(a,' + aspect + '),0,(' + width + '-iw)/2)',
y: 'if(lt(a,' + aspect + '),0,(' + height + '-ih)/2)',
color: color
}
}
];
}
/**
* Recompute size filters
*
* @param {Object} output
* @param {String} key newly-added parameter name ('size', 'aspect' or 'pad')
* @param {String} value newly-added parameter value
* @return filter string array
* @private
*/
function createSizeFilters(output, key, value) {
// Store parameters
var data = output.sizeData = output.sizeData || {};
data[key] = value;
if (!('size' in data)) {
// No size requested, keep original size
return [];
}
// Try to match the different size string formats
var fixedSize = data.size.match(/([0-9]+)x([0-9]+)/);
var fixedWidth = data.size.match(/([0-9]+)x\?/);
var fixedHeight = data.size.match(/\?x([0-9]+)/);
var percentRatio = data.size.match(/\b([0-9]{1,3})%/);
var width, height, aspect;
if (percentRatio) {
var ratio = Number(percentRatio[1]) / 100;
return [{
filter: 'scale',
options: {
w: 'trunc(iw*' + ratio + '/2)*2',
h: 'trunc(ih*' + ratio + '/2)*2'
}
}];
} else if (fixedSize) {
// Round target size to multiples of 2
width = Math.round(Number(fixedSize[1]) / 2) * 2;
height = Math.round(Number(fixedSize[2]) / 2) * 2;
aspect = width / height;
if (data.pad) {
return getScalePadFilters(width, height, aspect, data.pad);
} else {
// No autopad requested, rescale to target size
return [{ filter: 'scale', options: { w: width, h: height }}];
}
} else if (fixedWidth || fixedHeight) {
if ('aspect' in data) {
// Specified aspect ratio
width = fixedWidth ? fixedWidth[1] : Math.round(Number(fixedHeight[1]) * data.aspect);
height = fixedHeight ? fixedHeight[1] : Math.round(Number(fixedWidth[1]) / data.aspect);
// Round to multiples of 2
width = Math.round(width / 2) * 2;
height = Math.round(height / 2) * 2;
if (data.pad) {
return getScalePadFilters(width, height, data.aspect, data.pad);
} else {
// No autopad requested, rescale to target size
return [{ filter: 'scale', options: { w: width, h: height }}];
}
} else {
// Keep input aspect ratio
if (fixedWidth) {
return [{
filter: 'scale',
options: {
w: Math.round(Number(fixedWidth[1]) / 2) * 2,
h: 'trunc(ow/a/2)*2'
}
}];
} else {
return [{
filter: 'scale',
options: {
w: 'trunc(oh*a/2)*2',
h: Math.round(Number(fixedHeight[1]) / 2) * 2
}
}];
}
}
} else {
throw new Error('Invalid size specified: ' + data.size);
}
}
/*
*! Video size-related methods
*/
module.exports = function(proto) {
/**
* Keep display aspect ratio
*
* This method is useful when converting an input with non-square pixels to an output format
* that does not support non-square pixels. It rescales the input so that the display aspect
* ratio is the same.
*
* @method FfmpegCommand#keepDAR
* @category Video size
* @aliases keepPixelAspect,keepDisplayAspect,keepDisplayAspectRatio
*
* @return FfmpegCommand
*/
proto.keepPixelAspect = // Only for compatibility, this is not about keeping _pixel_ aspect ratio
proto.keepDisplayAspect =
proto.keepDisplayAspectRatio =
proto.keepDAR = function() {
return this.videoFilters([
{
filter: 'scale',
options: {
w: 'if(gt(sar,1),iw*sar,iw)',
h: 'if(lt(sar,1),ih/sar,ih)'
}
},
{
filter: 'setsar',
options: '1'
}
]);
};
/**
* Set output size
*
* The 'size' parameter can have one of 4 forms:
* - 'X%': rescale to xx % of the original size
* - 'WxH': specify width and height
* - 'Wx?': specify width and compute height from input aspect ratio
* - '?xH': specify height and compute width from input aspect ratio
*
* Note: both dimensions will be truncated to multiples of 2.
*
* @method FfmpegCommand#size
* @category Video size
* @aliases withSize,setSize
*
* @param {String} size size string, eg. '33%', '320x240', '320x?', '?x240'
* @return FfmpegCommand
*/
proto.withSize =
proto.setSize =
proto.size = function(size) {
var filters = createSizeFilters(this._currentOutput, 'size', size);
this._currentOutput.sizeFilters.clear();
this._currentOutput.sizeFilters(filters);
return this;
};
/**
* Set output aspect ratio
*
* @method FfmpegCommand#aspect
* @category Video size
* @aliases withAspect,withAspectRatio,setAspect,setAspectRatio,aspectRatio
*
* @param {String|Number} aspect aspect ratio (number or 'X:Y' string)
* @return FfmpegCommand
*/
proto.withAspect =
proto.withAspectRatio =
proto.setAspect =
proto.setAspectRatio =
proto.aspect =
proto.aspectRatio = function(aspect) {
var a = Number(aspect);
if (isNaN(a)) {
var match = aspect.match(/^(\d+):(\d+)$/);
if (match) {
a = Number(match[1]) / Number(match[2]);
} else {
throw new Error('Invalid aspect ratio: ' + aspect);
}
}
var filters = createSizeFilters(this._currentOutput, 'aspect', a);
this._currentOutput.sizeFilters.clear();
this._currentOutput.sizeFilters(filters);
return this;
};
/**
* Enable auto-padding the output
*
* @method FfmpegCommand#autopad
* @category Video size
* @aliases applyAutopadding,applyAutoPadding,applyAutopad,applyAutoPad,withAutopadding,withAutoPadding,withAutopad,withAutoPad,autoPad
*
* @param {Boolean} [pad=true] enable/disable auto-padding
* @param {String} [color='black'] pad color
*/
proto.applyAutopadding =
proto.applyAutoPadding =
proto.applyAutopad =
proto.applyAutoPad =
proto.withAutopadding =
proto.withAutoPadding =
proto.withAutopad =
proto.withAutoPad =
proto.autoPad =
proto.autopad = function(pad, color) {
// Allow autopad(color)
if (typeof pad === 'string') {
color = pad;
pad = true;
}
// Allow autopad() and autopad(undefined, color)
if (typeof pad === 'undefined') {
pad = true;
}
var filters = createSizeFilters(this._currentOutput, 'pad', pad ? color || 'black' : false);
this._currentOutput.sizeFilters.clear();
this._currentOutput.sizeFilters(filters);
return this;
};
};

View File

@ -0,0 +1,14 @@
/*jshint node:true */
'use strict';
exports.load = function(ffmpeg) {
ffmpeg
.format('avi')
.videoBitrate('1024k')
.videoCodec('mpeg4')
.size('720x?')
.audioBitrate('128k')
.audioChannels(2)
.audioCodec('libmp3lame')
.outputOptions(['-vtag DIVX']);
};

View File

@ -0,0 +1,16 @@
/*jshint node:true */
'use strict';
exports.load = function(ffmpeg) {
ffmpeg
.format('flv')
.flvmeta()
.size('320x?')
.videoBitrate('512k')
.videoCodec('libx264')
.fps(24)
.audioBitrate('96k')
.audioCodec('aac')
.audioFrequency(22050)
.audioChannels(2);
};

View File

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

View File

@ -0,0 +1,660 @@
/*jshint node:true*/
'use strict';
var spawn = require('child_process').spawn;
var async = require('../async');
var utils = require('./utils');
/*
*! Processor methods
*/
/**
* Run ffprobe asynchronously and store data in command
*
* @param {FfmpegCommand} command
* @private
*/
function runFfprobe(command) {
const inputProbeIndex = 0;
if (command._inputs[inputProbeIndex].isStream) {
// Don't probe input streams as this will consume them
return;
}
command.ffprobe(inputProbeIndex, function (err, data) {
command._ffprobeData = data;
});
}
module.exports = function (proto) {
/**
* Emitted just after ffmpeg has been spawned.
*
* @event FfmpegCommand#start
* @param {String} command ffmpeg command line
*/
/**
* Emitted when ffmpeg reports progress information
*
* @event FfmpegCommand#progress
* @param {Object} progress progress object
* @param {Number} progress.frames number of frames transcoded
* @param {Number} progress.currentFps current processing speed in frames per second
* @param {Number} progress.currentKbps current output generation speed in kilobytes per second
* @param {Number} progress.targetSize current output file size
* @param {String} progress.timemark current video timemark
* @param {Number} [progress.percent] processing progress (may not be available depending on input)
*/
/**
* Emitted when ffmpeg outputs to stderr
*
* @event FfmpegCommand#stderr
* @param {String} line stderr output line
*/
/**
* Emitted when ffmpeg reports input codec data
*
* @event FfmpegCommand#codecData
* @param {Object} codecData codec data object
* @param {String} codecData.format input format name
* @param {String} codecData.audio input audio codec name
* @param {String} codecData.audio_details input audio codec parameters
* @param {String} codecData.video input video codec name
* @param {String} codecData.video_details input video codec parameters
*/
/**
* Emitted when an error happens when preparing or running a command
*
* @event FfmpegCommand#error
* @param {Error} error error object, with optional properties 'inputStreamError' / 'outputStreamError' for errors on their respective streams
* @param {String|null} stdout ffmpeg stdout, unless outputting to a stream
* @param {String|null} stderr ffmpeg stderr
*/
/**
* Emitted when a command finishes processing
*
* @event FfmpegCommand#end
* @param {Array|String|null} [filenames|stdout] generated filenames when taking screenshots, ffmpeg stdout when not outputting to a stream, null otherwise
* @param {String|null} stderr ffmpeg stderr
*/
/**
* Spawn an ffmpeg process
*
* The 'options' argument may contain the following keys:
* - 'niceness': specify process niceness, ignored on Windows (default: 0)
* - `cwd`: change working directory
* - 'captureStdout': capture stdout and pass it to 'endCB' as its 2nd argument (default: false)
* - 'stdoutLines': override command limit (default: use command limit)
*
* The 'processCB' callback, if present, is called as soon as the process is created and
* receives a nodejs ChildProcess object. It may not be called at all if an error happens
* before spawning the process.
*
* The 'endCB' callback is called either when an error occurs or when the ffmpeg process finishes.
*
* @method FfmpegCommand#_spawnFfmpeg
* @param {Array} args ffmpeg command line argument list
* @param {Object} [options] spawn options (see above)
* @param {Function} [processCB] callback called with process object and stdout/stderr ring buffers when process has been created
* @param {Function} endCB callback called with error (if applicable) and stdout/stderr ring buffers when process finished
* @private
*/
proto._spawnFfmpeg = function (args, options, processCB, endCB) {
// Enable omitting options
if (typeof options === 'function') {
endCB = processCB;
processCB = options;
options = {};
}
// Enable omitting processCB
if (typeof endCB === 'undefined') {
endCB = processCB;
processCB = function () { };
}
var maxLines = 'stdoutLines' in options ? options.stdoutLines : this.options.stdoutLines;
// Find ffmpeg
this._getFfmpegPath(function (err, command) {
if (err) {
return endCB(err);
} else if (!command || command.length === 0) {
return endCB(new Error('Cannot find ffmpeg'));
}
// Apply niceness
if (options.niceness && options.niceness !== 0 && !utils.isWindows) {
args.unshift('-n', options.niceness, command);
command = 'nice';
}
var stdoutRing = utils.linesRing(maxLines);
var stdoutClosed = false;
var stderrRing = utils.linesRing(maxLines);
var stderrClosed = false;
// Spawn process
var ffmpegProc = spawn(command, args, options);
if (ffmpegProc.stderr) {
ffmpegProc.stderr.setEncoding('utf8');
}
ffmpegProc.on('error', function (err) {
endCB(err);
});
// Ensure we wait for captured streams to end before calling endCB
var exitError = null;
function handleExit(err) {
if (err) {
exitError = err;
}
if (processExited && (stdoutClosed || !options.captureStdout) && stderrClosed) {
endCB(exitError, stdoutRing, stderrRing);
}
}
// Handle process exit
var processExited = false;
ffmpegProc.on('exit', function (code, signal) {
processExited = true;
if (signal) {
handleExit(new Error('ffmpeg was killed with signal ' + signal));
} else if (code) {
handleExit(new Error('ffmpeg exited with code ' + code));
} else {
handleExit();
}
});
// Capture stdout if specified
if (options.captureStdout) {
ffmpegProc.stdout.on('data', function (data) {
stdoutRing.append(data);
});
ffmpegProc.stdout.on('close', function () {
stdoutRing.close();
stdoutClosed = true;
handleExit();
});
}
// Capture stderr if specified
ffmpegProc.stderr.on('data', function (data) {
stderrRing.append(data);
});
ffmpegProc.stderr.on('close', function () {
stderrRing.close();
stderrClosed = true;
handleExit();
});
// Call process callback
processCB(ffmpegProc, stdoutRing, stderrRing);
});
};
/**
* Build the argument list for an ffmpeg command
*
* @method FfmpegCommand#_getArguments
* @return argument list
* @private
*/
proto._getArguments = function () {
var complexFilters = this._complexFilters.get();
var fileOutput = this._outputs.some(function (output) {
return output.isFile;
});
return [].concat(
// Inputs and input options
this._inputs.reduce(function (args, input) {
var source = (typeof input.source === 'string') ? input.source : 'pipe:0';
// For each input, add input options, then '-i <source>'
return args.concat(
input.options.get(),
['-i', source]
);
}, []),
// Global options
this._global.get(),
// Overwrite if we have file outputs
fileOutput ? ['-y'] : [],
// Complex filters
complexFilters,
// Outputs, filters and output options
this._outputs.reduce(function (args, output) {
var sizeFilters = utils.makeFilterStrings(output.sizeFilters.get());
var audioFilters = output.audioFilters.get();
var videoFilters = output.videoFilters.get().concat(sizeFilters);
var outputArg;
if (!output.target) {
outputArg = [];
} else if (typeof output.target === 'string') {
outputArg = [output.target];
} else {
outputArg = ['pipe:1'];
}
return args.concat(
output.audio.get(),
audioFilters.length ? ['-filter:a', audioFilters.join(',')] : [],
output.video.get(),
videoFilters.length ? ['-filter:v', videoFilters.join(',')] : [],
output.options.get(),
outputArg
);
}, [])
);
};
/**
* Prepare execution of an ffmpeg command
*
* Checks prerequisites for the execution of the command (codec/format availability, flvtool...),
* then builds the argument list for ffmpeg and pass them to 'callback'.
*
* @method FfmpegCommand#_prepare
* @param {Function} callback callback with signature (err, args)
* @param {Boolean} [readMetadata=false] read metadata before processing
* @private
*/
proto._prepare = function (callback, readMetadata) {
var self = this;
async.waterfall([
// Check codecs and formats
function (cb) {
self._checkCapabilities(cb);
},
// Read metadata if required
function (cb) {
if (!readMetadata) {
return cb();
}
self.ffprobe(0, function (err, data) {
if (!err) {
self._ffprobeData = data;
}
cb();
});
},
// Check for flvtool2/flvmeta if necessary
function (cb) {
var flvmeta = self._outputs.some(function (output) {
// Remove flvmeta flag on non-file output
if (output.flags.flvmeta && !output.isFile) {
self.logger.warn('Updating flv metadata is only supported for files');
output.flags.flvmeta = false;
}
return output.flags.flvmeta;
});
if (flvmeta) {
self._getFlvtoolPath(function (err) {
cb(err);
});
} else {
cb();
}
},
// Build argument list
function (cb) {
var args;
try {
args = self._getArguments();
} catch (e) {
return cb(e);
}
cb(null, args);
},
// Add "-strict experimental" option where needed
function (args, cb) {
self.availableEncoders(function (err, encoders) {
for (var i = 0; i < args.length; i++) {
if (args[i] === '-acodec' || args[i] === '-vcodec') {
i++;
if ((args[i] in encoders) && encoders[args[i]].experimental) {
args.splice(i + 1, 0, '-strict', 'experimental');
i += 2;
}
}
}
cb(null, args);
});
}
], callback);
if (!readMetadata) {
// Read metadata as soon as 'progress' listeners are added
if (this.listeners('progress').length > 0) {
// Read metadata in parallel
runFfprobe(this);
} else {
// Read metadata as soon as the first 'progress' listener is added
this.once('newListener', function (event) {
if (event === 'progress') {
runFfprobe(this);
}
});
}
}
};
/**
* Run ffmpeg command
*
* @method FfmpegCommand#run
* @category Processing
* @aliases exec,execute
*/
proto.exec =
proto.execute =
proto.run = function () {
var self = this;
// Check if at least one output is present
var outputPresent = this._outputs.some(function (output) {
return 'target' in output;
});
if (!outputPresent) {
throw new Error('No output specified');
}
// Get output stream if any
var outputStream = this._outputs.filter(function (output) {
return typeof output.target !== 'string';
})[0];
// Get input stream if any
var inputStream = this._inputs.filter(function (input) {
return typeof input.source !== 'string';
})[0];
// Ensure we send 'end' or 'error' only once
var ended = false;
function emitEnd(err, stdout, stderr) {
if (!ended) {
ended = true;
if (err) {
self.emit('error', err, stdout, stderr);
} else {
self.emit('end', stdout, stderr);
}
}
}
self._prepare(function (err, args) {
if (err) {
return emitEnd(err);
}
// Run ffmpeg
self._spawnFfmpeg(
args,
{
captureStdout: !outputStream,
niceness: self.options.niceness,
cwd: self.options.cwd,
windowsHide: true
},
function processCB(ffmpegProc, stdoutRing, stderrRing) {
self.ffmpegProc = ffmpegProc;
self.emit('start', 'ffmpeg ' + args.join(' '));
// Pipe input stream if any
if (inputStream) {
inputStream.source.on('error', function (err) {
var reportingErr = new Error('Input stream error: ' + err.message);
reportingErr.inputStreamError = err;
emitEnd(reportingErr);
ffmpegProc.kill();
});
inputStream.source.resume();
inputStream.source.pipe(ffmpegProc.stdin);
// Set stdin error handler on ffmpeg (prevents nodejs catching the error, but
// ffmpeg will fail anyway, so no need to actually handle anything)
ffmpegProc.stdin.on('error', function () { });
}
// Setup timeout if requested
if (self.options.timeout) {
self.processTimer = setTimeout(function () {
var msg = 'process ran into a timeout (' + self.options.timeout + 's)';
emitEnd(new Error(msg), stdoutRing.get(), stderrRing.get());
ffmpegProc.kill();
}, self.options.timeout * 1000);
}
if (outputStream) {
// Pipe ffmpeg stdout to output stream
ffmpegProc.stdout.pipe(outputStream.target, outputStream.pipeopts);
// Handle output stream events
outputStream.target.on('close', function () {
self.logger.debug('Output stream closed, scheduling kill for ffmpeg process');
// Don't kill process yet, to give a chance to ffmpeg to
// terminate successfully first This is necessary because
// under load, the process 'exit' event sometimes happens
// after the output stream 'close' event.
setTimeout(function () {
emitEnd(new Error('Output stream closed'));
ffmpegProc.kill();
}, 20);
});
outputStream.target.on('error', function (err) {
self.logger.debug('Output stream error, killing ffmpeg process');
var reportingErr = new Error('Output stream error: ' + err.message);
reportingErr.outputStreamError = err;
emitEnd(reportingErr, stdoutRing.get(), stderrRing.get());
ffmpegProc.kill('SIGKILL');
});
}
// Setup stderr handling
if (stderrRing) {
// 'stderr' event
if (self.listeners('stderr').length) {
stderrRing.callback(function (line) {
self.emit('stderr', line);
});
}
// 'codecData' event
if (self.listeners('codecData').length) {
var codecDataSent = false;
var codecObject = {};
stderrRing.callback(function (line) {
if (!codecDataSent)
codecDataSent = utils.extractCodecData(self, line, codecObject);
});
}
// 'progress' event
if (self.listeners('progress').length) {
stderrRing.callback(function (line) {
utils.extractProgress(self, line);
});
}
}
},
function endCB(err, stdoutRing, stderrRing) {
clearTimeout(self.processTimer);
delete self.ffmpegProc;
if (err) {
if (err.message.match(/ffmpeg exited with code/)) {
// Add ffmpeg error message
err.message += ': ' + utils.extractError(stderrRing.get());
}
emitEnd(err, stdoutRing.get(), stderrRing.get());
} else {
// Find out which outputs need flv metadata
var flvmeta = self._outputs.filter(function (output) {
return output.flags.flvmeta;
});
if (flvmeta.length) {
self._getFlvtoolPath(function (err, flvtool) {
if (err) {
return emitEnd(err);
}
async.each(
flvmeta,
function (output, cb) {
spawn(flvtool, ['-U', output.target], { windowsHide: true })
.on('error', function (err) {
cb(new Error('Error running ' + flvtool + ' on ' + output.target + ': ' + err.message));
})
.on('exit', function (code, signal) {
if (code !== 0 || signal) {
cb(
new Error(flvtool + ' ' +
(signal ? 'received signal ' + signal
: 'exited with code ' + code)) +
' when running on ' + output.target
);
} else {
cb();
}
});
},
function (err) {
if (err) {
emitEnd(err);
} else {
emitEnd(null, stdoutRing.get(), stderrRing.get());
}
}
);
});
} else {
emitEnd(null, stdoutRing.get(), stderrRing.get());
}
}
}
);
});
return this;
};
/**
* Renice current and/or future ffmpeg processes
*
* Ignored on Windows platforms.
*
* @method FfmpegCommand#renice
* @category Processing
*
* @param {Number} [niceness=0] niceness value between -20 (highest priority) and 20 (lowest priority)
* @return FfmpegCommand
*/
proto.renice = function (niceness) {
if (!utils.isWindows) {
niceness = niceness || 0;
if (niceness < -20 || niceness > 20) {
this.logger.warn('Invalid niceness value: ' + niceness + ', must be between -20 and 20');
}
niceness = Math.min(20, Math.max(-20, niceness));
this.options.niceness = niceness;
if (this.ffmpegProc) {
var logger = this.logger;
var pid = this.ffmpegProc.pid;
var renice = spawn('renice', [niceness, '-p', pid], { windowsHide: true });
renice.on('error', function (err) {
logger.warn('could not renice process ' + pid + ': ' + err.message);
});
renice.on('exit', function (code, signal) {
if (signal) {
logger.warn('could not renice process ' + pid + ': renice was killed by signal ' + signal);
} else if (code) {
logger.warn('could not renice process ' + pid + ': renice exited with ' + code);
} else {
logger.info('successfully reniced process ' + pid + ' to ' + niceness + ' niceness');
}
});
}
}
return this;
};
/**
* Kill current ffmpeg process, if any
*
* @method FfmpegCommand#kill
* @category Processing
*
* @param {String} [signal=SIGKILL] signal name
* @return FfmpegCommand
*/
proto.kill = function (signal) {
if (!this.ffmpegProc) {
this.logger.warn('No running ffmpeg process, cannot send signal');
} else {
this.ffmpegProc.kill(signal || 'SIGKILL');
}
return this;
};
};

View File

@ -0,0 +1,456 @@
/*jshint node:true*/
'use strict';
var fs = require('fs');
var path = require('path');
var PassThrough = require('stream').PassThrough;
var async = require('../async');
var utils = require('./utils');
/*
* Useful recipes for commands
*/
module.exports = function recipes(proto) {
/**
* Execute ffmpeg command and save output to a file
*
* @method FfmpegCommand#save
* @category Processing
* @aliases saveToFile
*
* @param {String} output file path
* @return FfmpegCommand
*/
proto.saveToFile =
proto.save = function (output) {
this.output(output).run();
return this;
};
/**
* Execute ffmpeg command and save output to a stream
*
* If 'stream' is not specified, a PassThrough stream is created and returned.
* 'options' will be used when piping ffmpeg output to the output stream
* (@see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options)
*
* @method FfmpegCommand#pipe
* @category Processing
* @aliases stream,writeToStream
*
* @param {stream.Writable} [stream] output stream
* @param {Object} [options={}] pipe options
* @return Output stream
*/
proto.writeToStream =
proto.pipe =
proto.stream = function (stream, options) {
if (stream && !('writable' in stream)) {
options = stream;
stream = undefined;
}
if (!stream) {
if (process.version.match(/v0\.8\./)) {
throw new Error('PassThrough stream is not supported on node v0.8');
}
stream = new PassThrough();
}
this.output(stream, options).run();
return stream;
};
/**
* Generate images from a video
*
* Note: this method makes the command emit a 'filenames' event with an array of
* the generated image filenames.
*
* @method FfmpegCommand#screenshots
* @category Processing
* @aliases takeScreenshots,thumbnail,thumbnails,screenshot
*
* @param {Number|Object} [config=1] screenshot count or configuration object with
* the following keys:
* @param {Number} [config.count] number of screenshots to take; using this option
* takes screenshots at regular intervals (eg. count=4 would take screens at 20%, 40%,
* 60% and 80% of the video length).
* @param {String} [config.folder='.'] output folder
* @param {String} [config.filename='tn.png'] output filename pattern, may contain the following
* tokens:
* - '%s': offset in seconds
* - '%w': screenshot width
* - '%h': screenshot height
* - '%r': screenshot resolution (same as '%wx%h')
* - '%f': input filename
* - '%b': input basename (filename w/o extension)
* - '%i': index of screenshot in timemark array (can be zero-padded by using it like `%000i`)
* @param {Number[]|String[]} [config.timemarks] array of timemarks to take screenshots
* at; each timemark may be a number of seconds, a '[[hh:]mm:]ss[.xxx]' string or a
* 'XX%' string. Overrides 'count' if present.
* @param {Number[]|String[]} [config.timestamps] alias for 'timemarks'
* @param {Boolean} [config.fastSeek] use fast seek (less accurate)
* @param {String} [config.size] screenshot size, with the same syntax as {@link FfmpegCommand#size}
* @param {String} [folder] output folder (legacy alias for 'config.folder')
* @return FfmpegCommand
*/
proto.takeScreenshots =
proto.thumbnail =
proto.thumbnails =
proto.screenshot =
proto.screenshots = function (config, folder) {
var self = this;
var source = this._currentInput.source;
config = config || { count: 1 };
// Accept a number of screenshots instead of a config object
if (typeof config === 'number') {
config = {
count: config
};
}
// Accept a second 'folder' parameter instead of config.folder
if (!('folder' in config)) {
config.folder = folder || '.';
}
// Accept 'timestamps' instead of 'timemarks'
if ('timestamps' in config) {
config.timemarks = config.timestamps;
}
// Compute timemarks from count if not present
if (!('timemarks' in config)) {
if (!config.count) {
throw new Error('Cannot take screenshots: neither a count nor a timemark list are specified');
}
var interval = 100 / (1 + config.count);
config.timemarks = [];
for (var i = 0; i < config.count; i++) {
config.timemarks.push((interval * (i + 1)) + '%');
}
}
// Parse size option
if ('size' in config) {
var fixedSize = config.size.match(/^(\d+)x(\d+)$/);
var fixedWidth = config.size.match(/^(\d+)x\?$/);
var fixedHeight = config.size.match(/^\?x(\d+)$/);
var percentSize = config.size.match(/^(\d+)%$/);
if (!fixedSize && !fixedWidth && !fixedHeight && !percentSize) {
throw new Error('Invalid size parameter: ' + config.size);
}
}
// Metadata helper
var metadata;
function getMetadata(cb) {
if (metadata) {
cb(null, metadata);
} else {
self.ffprobe(function (err, meta) {
metadata = meta;
cb(err, meta);
});
}
}
async.waterfall([
// Compute percent timemarks if any
function computeTimemarks(next) {
if (config.timemarks.some(function (t) { return ('' + t).match(/^[\d.]+%$/); })) {
if (typeof source !== 'string') {
return next(new Error('Cannot compute screenshot timemarks with an input stream, please specify fixed timemarks'));
}
getMetadata(function (err, meta) {
if (err) {
next(err);
} else {
// Select video stream with the highest resolution
var vstream = meta.streams.reduce(function (biggest, stream) {
if (stream.codec_type === 'video' && stream.width * stream.height > biggest.width * biggest.height) {
return stream;
} else {
return biggest;
}
}, { width: 0, height: 0 });
if (vstream.width === 0) {
return next(new Error('No video stream in input, cannot take screenshots'));
}
var duration = Number(vstream.duration);
if (isNaN(duration)) {
duration = Number(meta.format.duration);
}
if (isNaN(duration)) {
return next(new Error('Could not get input duration, please specify fixed timemarks'));
}
config.timemarks = config.timemarks.map(function (mark) {
if (('' + mark).match(/^([\d.]+)%$/)) {
return duration * parseFloat(mark) / 100;
} else {
return mark;
}
});
next();
}
});
} else {
next();
}
},
// Turn all timemarks into numbers and sort them
function normalizeTimemarks(next) {
config.timemarks = config.timemarks.map(function (mark) {
return utils.timemarkToSeconds(mark);
}).sort(function (a, b) { return a - b; });
next();
},
// Add '_%i' to pattern when requesting multiple screenshots and no variable token is present
function fixPattern(next) {
var pattern = config.filename || 'tn.png';
if (pattern.indexOf('.') === -1) {
pattern += '.png';
}
if (config.timemarks.length > 1 && !pattern.match(/%(s|0*i)/)) {
var ext = path.extname(pattern);
pattern = path.join(path.dirname(pattern), path.basename(pattern, ext) + '_%i' + ext);
}
next(null, pattern);
},
// Replace filename tokens (%f, %b) in pattern
function replaceFilenameTokens(pattern, next) {
if (pattern.match(/%[bf]/)) {
if (typeof source !== 'string') {
return next(new Error('Cannot replace %f or %b when using an input stream'));
}
pattern = pattern
.replace(/%f/g, path.basename(source))
.replace(/%b/g, path.basename(source, path.extname(source)));
}
next(null, pattern);
},
// Compute size if needed
function getSize(pattern, next) {
if (pattern.match(/%[whr]/)) {
if (fixedSize) {
return next(null, pattern, fixedSize[1], fixedSize[2]);
}
getMetadata(function (err, meta) {
if (err) {
return next(new Error('Could not determine video resolution to replace %w, %h or %r'));
}
var vstream = meta.streams.reduce(function (biggest, stream) {
if (stream.codec_type === 'video' && stream.width * stream.height > biggest.width * biggest.height) {
return stream;
} else {
return biggest;
}
}, { width: 0, height: 0 });
if (vstream.width === 0) {
return next(new Error('No video stream in input, cannot replace %w, %h or %r'));
}
var width = vstream.width;
var height = vstream.height;
if (fixedWidth) {
height = height * Number(fixedWidth[1]) / width;
width = Number(fixedWidth[1]);
} else if (fixedHeight) {
width = width * Number(fixedHeight[1]) / height;
height = Number(fixedHeight[1]);
} else if (percentSize) {
width = width * Number(percentSize[1]) / 100;
height = height * Number(percentSize[1]) / 100;
}
next(null, pattern, Math.round(width / 2) * 2, Math.round(height / 2) * 2);
});
} else {
next(null, pattern, -1, -1);
}
},
// Replace size tokens (%w, %h, %r) in pattern
function replaceSizeTokens(pattern, width, height, next) {
pattern = pattern
.replace(/%r/g, '%wx%h')
.replace(/%w/g, width)
.replace(/%h/g, height);
next(null, pattern);
},
// Replace variable tokens in pattern (%s, %i) and generate filename list
function replaceVariableTokens(pattern, next) {
var filenames = config.timemarks.map(function (t, i) {
return pattern
.replace(/%s/g, utils.timemarkToSeconds(t))
.replace(/%(0*)i/g, function (match, padding) {
var idx = '' + (i + 1);
return padding.substr(0, Math.max(0, padding.length + 1 - idx.length)) + idx;
});
});
self.emit('filenames', filenames);
next(null, filenames);
},
// Create output directory
function createDirectory(filenames, next) {
fs.exists(config.folder, function (exists) {
if (!exists) {
fs.mkdir(config.folder, function (err) {
if (err) {
next(err);
} else {
next(null, filenames);
}
});
} else {
next(null, filenames);
}
});
}
], function runCommand(err, filenames) {
if (err) {
return self.emit('error', err);
}
var count = config.timemarks.length;
var split;
var filters = [split = {
filter: 'split',
options: count,
outputs: []
}];
if ('size' in config) {
// Set size to generate size filters
self.size(config.size);
// Get size filters and chain them with 'sizeN' stream names
var sizeFilters = self._currentOutput.sizeFilters.get().map(function (f, i) {
if (i > 0) {
f.inputs = 'size' + (i - 1);
}
f.outputs = 'size' + i;
return f;
});
// Input last size filter output into split filter
split.inputs = 'size' + (sizeFilters.length - 1);
// Add size filters in front of split filter
filters = sizeFilters.concat(filters);
// Remove size filters
self._currentOutput.sizeFilters.clear();
}
var first = 0;
for (var i = 0; i < count; i++) {
var stream = 'screen' + i;
split.outputs.push(stream);
if (i === 0) {
first = config.timemarks[i];
self.seekInput(first);
}
self.output(path.join(config.folder, filenames[i]))
.frames(1)
.map(stream);
if (i > 0) {
self.seek(config.timemarks[i] - first);
}
}
self.complexFilter(filters);
self.run();
});
return this;
};
/**
* Merge (concatenate) inputs to a single file
*
* @method FfmpegCommand#concat
* @category Processing
* @aliases concatenate,mergeToFile
*
* @param {String|Writable} target output file or writable stream
* @param {Object} [options] pipe options (only used when outputting to a writable stream)
* @return FfmpegCommand
*/
proto.mergeToFile =
proto.concatenate =
proto.concat = function (target, options) {
// Find out which streams are present in the first non-stream input
var fileInput = this._inputs.filter(function (input) {
return !input.isStream;
})[0];
var self = this;
this.ffprobe(this._inputs.indexOf(fileInput), function (err, data) {
if (err) {
return self.emit('error', err);
}
var hasAudioStreams = data.streams.some(function (stream) {
return stream.codec_type === 'audio';
});
var hasVideoStreams = data.streams.some(function (stream) {
return stream.codec_type === 'video';
});
// Setup concat filter and start processing
self.output(target, options)
.complexFilter({
filter: 'concat',
options: {
n: self._inputs.length,
v: hasVideoStreams ? 1 : 0,
a: hasAudioStreams ? 1 : 0
}
})
.run();
});
return this;
};
};

View File

@ -0,0 +1,454 @@
/*jshint node:true*/
'use strict';
var isWindows = require('os').platform().match(/win(32|64)/);
var which = require('../which');
var nlRegexp = /\r\n|\r|\n/g;
var streamRegexp = /^\[?(.*?)\]?$/;
var filterEscapeRegexp = /[,]/;
var whichCache = {};
/**
* Parse progress line from ffmpeg stderr
*
* @param {String} line progress line
* @return progress object
* @private
*/
function parseProgressLine(line) {
var progress = {};
// Remove all spaces after = and trim
line = line.replace(/=\s+/g, '=').trim();
var progressParts = line.split(' ');
// Split every progress part by "=" to get key and value
for (var i = 0; i < progressParts.length; i++) {
var progressSplit = progressParts[i].split('=', 2);
var key = progressSplit[0];
var value = progressSplit[1];
// This is not a progress line
if (typeof value === 'undefined')
return null;
progress[key] = value;
}
return progress;
}
var utils = module.exports = {
isWindows: isWindows,
streamRegexp: streamRegexp,
/**
* Copy an object keys into another one
*
* @param {Object} source source object
* @param {Object} dest destination object
* @private
*/
copy: function (source, dest) {
Object.keys(source).forEach(function (key) {
dest[key] = source[key];
});
},
/**
* Create an argument list
*
* Returns a function that adds new arguments to the list.
* It also has the following methods:
* - clear() empties the argument list
* - get() returns the argument list
* - find(arg, count) finds 'arg' in the list and return the following 'count' items, or undefined if not found
* - remove(arg, count) remove 'arg' in the list as well as the following 'count' items
*
* @private
*/
args: function () {
var list = [];
// Append argument(s) to the list
var argfunc = function () {
if (arguments.length === 1 && Array.isArray(arguments[0])) {
list = list.concat(arguments[0]);
} else {
list = list.concat([].slice.call(arguments));
}
};
// Clear argument list
argfunc.clear = function () {
list = [];
};
// Return argument list
argfunc.get = function () {
return list;
};
// Find argument 'arg' in list, and if found, return an array of the 'count' items that follow it
argfunc.find = function (arg, count) {
var index = list.indexOf(arg);
if (index !== -1) {
return list.slice(index + 1, index + 1 + (count || 0));
}
};
// Find argument 'arg' in list, and if found, remove it as well as the 'count' items that follow it
argfunc.remove = function (arg, count) {
var index = list.indexOf(arg);
if (index !== -1) {
list.splice(index, (count || 0) + 1);
}
};
// Clone argument list
argfunc.clone = function () {
var cloned = utils.args();
cloned(list);
return cloned;
};
return argfunc;
},
/**
* Generate filter strings
*
* @param {String[]|Object[]} filters filter specifications. When using objects,
* each must have the following properties:
* @param {String} filters.filter filter name
* @param {String|Array} [filters.inputs] (array of) input stream specifier(s) for the filter,
* defaults to ffmpeg automatically choosing the first unused matching streams
* @param {String|Array} [filters.outputs] (array of) output stream specifier(s) for the filter,
* defaults to ffmpeg automatically assigning the output to the output file
* @param {Object|String|Array} [filters.options] filter options, can be omitted to not set any options
* @return String[]
* @private
*/
makeFilterStrings: function (filters) {
return filters.map(function (filterSpec) {
if (typeof filterSpec === 'string') {
return filterSpec;
}
var filterString = '';
// Filter string format is:
// [input1][input2]...filter[output1][output2]...
// The 'filter' part can optionaly have arguments:
// filter=arg1:arg2:arg3
// filter=arg1=v1:arg2=v2:arg3=v3
// Add inputs
if (Array.isArray(filterSpec.inputs)) {
filterString += filterSpec.inputs.map(function (streamSpec) {
return streamSpec.replace(streamRegexp, '[$1]');
}).join('');
} else if (typeof filterSpec.inputs === 'string') {
filterString += filterSpec.inputs.replace(streamRegexp, '[$1]');
}
// Add filter
filterString += filterSpec.filter;
// Add options
if (filterSpec.options) {
if (typeof filterSpec.options === 'string' || typeof filterSpec.options === 'number') {
// Option string
filterString += '=' + filterSpec.options;
} else if (Array.isArray(filterSpec.options)) {
// Option array (unnamed options)
filterString += '=' + filterSpec.options.map(function (option) {
if (typeof option === 'string' && option.match(filterEscapeRegexp)) {
return '\'' + option + '\'';
} else {
return option;
}
}).join(':');
} else if (Object.keys(filterSpec.options).length) {
// Option object (named options)
filterString += '=' + Object.keys(filterSpec.options).map(function (option) {
var value = filterSpec.options[option];
if (typeof value === 'string' && value.match(filterEscapeRegexp)) {
value = '\'' + value + '\'';
}
return option + '=' + value;
}).join(':');
}
}
// Add outputs
if (Array.isArray(filterSpec.outputs)) {
filterString += filterSpec.outputs.map(function (streamSpec) {
return streamSpec.replace(streamRegexp, '[$1]');
}).join('');
} else if (typeof filterSpec.outputs === 'string') {
filterString += filterSpec.outputs.replace(streamRegexp, '[$1]');
}
return filterString;
});
},
/**
* Search for an executable
*
* Uses 'which' or 'where' depending on platform
*
* @param {String} name executable name
* @param {Function} callback callback with signature (err, path)
* @private
*/
which: function (name, callback) {
if (name in whichCache) {
return callback(null, whichCache[name]);
}
which(name, function (err, result) {
if (err) {
// Treat errors as not found
return callback(null, whichCache[name] = '');
}
callback(null, whichCache[name] = result);
});
},
/**
* Convert a [[hh:]mm:]ss[.xxx] timemark into seconds
*
* @param {String} timemark timemark string
* @return Number
* @private
*/
timemarkToSeconds: function (timemark) {
if (typeof timemark === 'number') {
return timemark;
}
if (timemark.indexOf(':') === -1 && timemark.indexOf('.') >= 0) {
return Number(timemark);
}
var parts = timemark.split(':');
// add seconds
var secs = Number(parts.pop());
if (parts.length) {
// add minutes
secs += Number(parts.pop()) * 60;
}
if (parts.length) {
// add hours
secs += Number(parts.pop()) * 3600;
}
return secs;
},
/**
* Extract codec data from ffmpeg stderr and emit 'codecData' event if appropriate
* Call it with an initially empty codec object once with each line of stderr output until it returns true
*
* @param {FfmpegCommand} command event emitter
* @param {String} stderrLine ffmpeg stderr output line
* @param {Object} codecObject object used to accumulate codec data between calls
* @return {Boolean} true if codec data is complete (and event was emitted), false otherwise
* @private
*/
extractCodecData: function (command, stderrLine, codecsObject) {
var inputPattern = /Input #[0-9]+, ([^ ]+),/;
var durPattern = /Duration\: ([^,]+)/;
var audioPattern = /Audio\: (.*)/;
var videoPattern = /Video\: (.*)/;
if (!('inputStack' in codecsObject)) {
codecsObject.inputStack = [];
codecsObject.inputIndex = -1;
codecsObject.inInput = false;
}
var inputStack = codecsObject.inputStack;
var inputIndex = codecsObject.inputIndex;
var inInput = codecsObject.inInput;
var format, dur, audio, video;
if (format = stderrLine.match(inputPattern)) {
inInput = codecsObject.inInput = true;
inputIndex = codecsObject.inputIndex = codecsObject.inputIndex + 1;
inputStack[inputIndex] = { format: format[1], audio: '', video: '', duration: '' };
} else if (inInput && (dur = stderrLine.match(durPattern))) {
inputStack[inputIndex].duration = dur[1];
} else if (inInput && (audio = stderrLine.match(audioPattern))) {
audio = audio[1].split(', ');
inputStack[inputIndex].audio = audio[0];
inputStack[inputIndex].audio_details = audio;
} else if (inInput && (video = stderrLine.match(videoPattern))) {
video = video[1].split(', ');
inputStack[inputIndex].video = video[0];
inputStack[inputIndex].video_details = video;
} else if (/Output #\d+/.test(stderrLine)) {
inInput = codecsObject.inInput = false;
} else if (/Stream mapping:|Press (\[q\]|ctrl-c) to stop/.test(stderrLine)) {
command.emit.apply(command, ['codecData'].concat(inputStack));
return true;
}
return false;
},
/**
* Extract progress data from ffmpeg stderr and emit 'progress' event if appropriate
*
* @param {FfmpegCommand} command event emitter
* @param {String} stderrLine ffmpeg stderr data
* @private
*/
extractProgress: function (command, stderrLine) {
var progress = parseProgressLine(stderrLine);
if (progress) {
// build progress report object
var ret = {
frames: parseInt(progress.frame, 10),
currentFps: parseInt(progress.fps, 10),
currentKbps: progress.bitrate ? parseFloat(progress.bitrate.replace('kbits/s', '')) : 0,
targetSize: parseInt(progress.size || progress.Lsize, 10),
timemark: progress.time
};
// calculate percent progress using duration
if (command._ffprobeData && command._ffprobeData.format && command._ffprobeData.format.duration) {
var duration = Number(command._ffprobeData.format.duration);
if (!isNaN(duration))
ret.percent = (utils.timemarkToSeconds(ret.timemark) / duration) * 100;
}
command.emit('progress', ret);
}
},
/**
* Extract error message(s) from ffmpeg stderr
*
* @param {String} stderr ffmpeg stderr data
* @return {String}
* @private
*/
extractError: function (stderr) {
// Only return the last stderr lines that don't start with a space or a square bracket
return stderr.split(nlRegexp).reduce(function (messages, message) {
if (message.charAt(0) === ' ' || message.charAt(0) === '[') {
return [];
} else {
messages.push(message);
return messages;
}
}, []).join('\n');
},
/**
* Creates a line ring buffer object with the following methods:
* - append(str) : appends a string or buffer
* - get() : returns the whole string
* - close() : prevents further append() calls and does a last call to callbacks
* - callback(cb) : calls cb for each line (incl. those already in the ring)
*
* @param {Numebr} maxLines maximum number of lines to store (<= 0 for unlimited)
*/
linesRing: function (maxLines) {
var cbs = [];
var lines = [];
var current = null;
var closed = false
var max = maxLines - 1;
function emit(line) {
cbs.forEach(function (cb) { cb(line); });
}
return {
callback: function (cb) {
lines.forEach(function (l) { cb(l); });
cbs.push(cb);
},
append: function (str) {
if (closed) return;
if (str instanceof Buffer) str = '' + str;
if (!str || str.length === 0) return;
var newLines = str.split(nlRegexp);
if (newLines.length === 1) {
if (current !== null) {
current = current + newLines.shift();
} else {
current = newLines.shift();
}
} else {
if (current !== null) {
current = current + newLines.shift();
emit(current);
lines.push(current);
}
current = newLines.pop();
newLines.forEach(function (l) {
emit(l);
lines.push(l);
});
if (max > -1 && lines.length > max) {
lines.splice(0, lines.length - max);
}
}
},
get: function () {
if (current !== null) {
return lines.concat([current]).join('\n');
} else {
return lines.join('\n');
}
},
close: function () {
if (closed) return;
if (current !== null) {
emit(current);
lines.push(current);
if (max > -1 && lines.length > max) {
lines.shift();
}
current = null;
}
closed = true;
}
};
}
};

15
server/libs/isexe/LICENSE Normal file
View File

@ -0,0 +1,15 @@
The ISC License
Copyright (c) 2016-2022 Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@ -0,0 +1,61 @@
//
// used by async
// SOURCE: https://github.com/isaacs/isexe
//
var core
if (process.platform === 'win32' || global.TESTING_WINDOWS) {
core = require('./windows.js')
} else {
core = require('./mode.js')
}
module.exports = isexe
isexe.sync = sync
function isexe(path, options, cb) {
if (typeof options === 'function') {
cb = options
options = {}
}
if (!cb) {
if (typeof Promise !== 'function') {
throw new TypeError('callback not provided')
}
return new Promise(function (resolve, reject) {
isexe(path, options || {}, function (er, is) {
if (er) {
reject(er)
} else {
resolve(is)
}
})
})
}
core(path, options || {}, function (er, is) {
// ignore EACCES because that just means we aren't allowed to run it
if (er) {
if (er.code === 'EACCES' || options && options.ignoreErrors) {
er = null
is = false
}
}
cb(er, is)
})
}
function sync(path, options) {
// my kingdom for a filtered catch
try {
return core.sync(path, options || {})
} catch (er) {
if (options && options.ignoreErrors || er.code === 'EACCES') {
return false
} else {
throw er
}
}
}

41
server/libs/isexe/mode.js Normal file
View File

@ -0,0 +1,41 @@
module.exports = isexe
isexe.sync = sync
var fs = require('fs')
function isexe(path, options, cb) {
fs.stat(path, function (er, stat) {
cb(er, er ? false : checkStat(stat, options))
})
}
function sync(path, options) {
return checkStat(fs.statSync(path), options)
}
function checkStat(stat, options) {
return stat.isFile() && checkMode(stat, options)
}
function checkMode(stat, options) {
var mod = stat.mode
var uid = stat.uid
var gid = stat.gid
var myUid = options.uid !== undefined ?
options.uid : process.getuid && process.getuid()
var myGid = options.gid !== undefined ?
options.gid : process.getgid && process.getgid()
var u = parseInt('100', 8)
var g = parseInt('010', 8)
var o = parseInt('001', 8)
var ug = u | g
var ret = (mod & o) ||
(mod & g) && gid === myGid ||
(mod & u) && uid === myUid ||
(mod & ug) && myUid === 0
return ret
}

View File

@ -0,0 +1,42 @@
module.exports = isexe
isexe.sync = sync
var fs = require('fs')
function checkPathExt(path, options) {
var pathext = options.pathExt !== undefined ?
options.pathExt : process.env.PATHEXT
if (!pathext) {
return true
}
pathext = pathext.split(';')
if (pathext.indexOf('') !== -1) {
return true
}
for (var i = 0; i < pathext.length; i++) {
var p = pathext[i].toLowerCase()
if (p && path.substr(-p.length).toLowerCase() === p) {
return true
}
}
return false
}
function checkStat(stat, path, options) {
if (!stat.isSymbolicLink() && !stat.isFile()) {
return false
}
return checkPathExt(path, options)
}
function isexe(path, options, cb) {
fs.stat(path, function (er, stat) {
cb(er, er ? false : checkStat(stat, path, options))
})
}
function sync(path, options) {
return checkStat(fs.statSync(path), path, options)
}

View File

@ -1,5 +1,11 @@
'use strict';
//
// used by njodb
// Source: https://github.com/moxystudio/node-proper-lockfile
//
const lockfile = require('./lib/lockfile');
const { toPromise, toSync, toSyncOptions } = require('./lib/adapter');

View File

@ -1,15 +1,20 @@
//
// used by properLockFile
// Source: https://github.com/tim-kos/node-retry
//
var RetryOperation = require('./retry_operation');
exports.operation = function(options) {
exports.operation = function (options) {
var timeouts = exports.timeouts(options);
return new RetryOperation(timeouts, {
forever: options && options.forever,
unref: options && options.unref,
maxRetryTime: options && options.maxRetryTime
forever: options && options.forever,
unref: options && options.unref,
maxRetryTime: options && options.maxRetryTime
});
};
exports.timeouts = function(options) {
exports.timeouts = function (options) {
if (options instanceof Array) {
return [].concat(options);
}
@ -39,14 +44,14 @@ exports.timeouts = function(options) {
}
// sort the array numerically ascending
timeouts.sort(function(a,b) {
timeouts.sort(function (a, b) {
return a - b;
});
return timeouts;
};
exports.createTimeout = function(attempt, opts) {
exports.createTimeout = function (attempt, opts) {
var random = (opts.randomize)
? (Math.random() + 1)
: 1;
@ -57,7 +62,7 @@ exports.createTimeout = function(attempt, opts) {
return timeout;
};
exports.wrap = function(obj, options, methods) {
exports.wrap = function (obj, options, methods) {
if (options instanceof Array) {
methods = options;
options = null;
@ -73,15 +78,15 @@ exports.wrap = function(obj, options, methods) {
}
for (var i = 0; i < methods.length; i++) {
var method = methods[i];
var method = methods[i];
var original = obj[method];
obj[method] = function retryWrapper(original) {
var op = exports.operation(options);
var args = Array.prototype.slice.call(arguments, 1);
var op = exports.operation(options);
var args = Array.prototype.slice.call(arguments, 1);
var callback = args.pop();
args.push(function(err) {
args.push(function (err) {
if (op.retry(err)) {
return;
}
@ -91,7 +96,7 @@ exports.wrap = function(obj, options, methods) {
callback.apply(this, arguments);
});
op.attempt(function() {
op.attempt(function () {
original.apply(obj, args);
});
}.bind(obj, original);

View File

@ -37,7 +37,7 @@ function generateXML(data) {
var channel = [];
channel.push({ title: { _cdata: data.title } });
channel.push({ description: { _cdata: data.description || data.title } });
channel.push({ link: data.site_url || 'http://github.com/dylang/node-rss' });
channel.push({ link: data.site_url || 'https://github.com/advplyr/audiobookshelf' });
// image_url set?
if (data.image_url) {
channel.push({ image: [{ url: data.image_url }, { title: data.title }, { link: data.site_url }] });

View File

@ -1,3 +1,8 @@
//
// used by properLockFile
// Source: https://github.com/tapjs/signal-exit
//
// Note: since nyc uses this module to output coverage, any lines
// that are in the direct sync flow of nyc's outputCoverage are
// ignored, since we can never get coverage for them.
@ -20,7 +25,7 @@ const processOk = function (process) {
/* istanbul ignore if */
if (!processOk(process)) {
module.exports = function () {
return function () {}
return function () { }
}
} else {
var assert = require('assert')
@ -54,7 +59,7 @@ if (!processOk(process)) {
module.exports = function (cb, opts) {
/* istanbul ignore if */
if (!processOk(global.process)) {
return function () {}
return function () { }
}
assert.equal(typeof cb, 'function', 'a callback must be provided for exit handler')
@ -70,7 +75,7 @@ if (!processOk(process)) {
var remove = function () {
emitter.removeListener(ev, cb)
if (emitter.listeners('exit').length === 0 &&
emitter.listeners('afterexit').length === 0) {
emitter.listeners('afterexit').length === 0) {
unload()
}
}
@ -79,7 +84,7 @@ if (!processOk(process)) {
return remove
}
var unload = function unload () {
var unload = function unload() {
if (!loaded || !processOk(global.process)) {
return
}
@ -88,7 +93,7 @@ if (!processOk(process)) {
signals.forEach(function (sig) {
try {
process.removeListener(sig, sigListeners[sig])
} catch (er) {}
} catch (er) { }
})
process.emit = originalProcessEmit
process.reallyExit = originalProcessReallyExit
@ -96,7 +101,7 @@ if (!processOk(process)) {
}
module.exports.unload = unload
var emit = function emit (event, code, signal) {
var emit = function emit(event, code, signal) {
/* istanbul ignore if */
if (emitter.emitted[event]) {
return
@ -108,7 +113,7 @@ if (!processOk(process)) {
// { <signal>: <listener fn>, ... }
var sigListeners = {}
signals.forEach(function (sig) {
sigListeners[sig] = function listener () {
sigListeners[sig] = function listener() {
/* istanbul ignore if */
if (!processOk(global.process)) {
return
@ -141,7 +146,7 @@ if (!processOk(process)) {
var loaded = false
var load = function load () {
var load = function load() {
if (loaded || !processOk(global.process)) {
return
}
@ -168,7 +173,7 @@ if (!processOk(process)) {
module.exports.load = load
var originalProcessReallyExit = process.reallyExit
var processReallyExit = function processReallyExit (code) {
var processReallyExit = function processReallyExit(code) {
/* istanbul ignore if */
if (!processOk(global.process)) {
return
@ -182,7 +187,7 @@ if (!processOk(process)) {
}
var originalProcessEmit = process.emit
var processEmit = function processEmit (ev, arg) {
var processEmit = function processEmit(ev, arg) {
if (ev === 'exit' && processOk(global.process)) {
/* istanbul ignore else */
if (arg !== undefined) {

View File

@ -1,5 +1,10 @@
'use strict'
//
// used by fsExtra
// Source: https://github.com/RyanZim/universalify
//
exports.fromCallback = function (fn) {
return Object.defineProperty(function (...args) {
if (typeof args[args.length - 1] === 'function') fn.apply(this, args)

15
server/libs/which/LICENSE Normal file
View File

@ -0,0 +1,15 @@
The ISC License
Copyright (c) Isaac Z. Schlueter and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

130
server/libs/which/index.js Normal file
View File

@ -0,0 +1,130 @@
//
// used by fluentFfmpeg
// SOURCE: https://github.com/isaacs/isexe
//
const isWindows = process.platform === 'win32' ||
process.env.OSTYPE === 'cygwin' ||
process.env.OSTYPE === 'msys'
const path = require('path')
const COLON = isWindows ? ';' : ':'
const isexe = require('../isexe')
const getNotFoundError = (cmd) =>
Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' })
const getPathInfo = (cmd, opt) => {
const colon = opt.colon || COLON
// If it has a slash, then we don't bother searching the pathenv.
// just check the file itself, and that's it.
const pathEnv = cmd.match(/\//) || isWindows && cmd.match(/\\/) ? ['']
: (
[
// windows always checks the cwd first
...(isWindows ? [process.cwd()] : []),
...(opt.path || process.env.PATH ||
/* istanbul ignore next: very unusual */ '').split(colon),
]
)
const pathExtExe = isWindows
? opt.pathExt || process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM'
: ''
const pathExt = isWindows ? pathExtExe.split(colon) : ['']
if (isWindows) {
if (cmd.indexOf('.') !== -1 && pathExt[0] !== '')
pathExt.unshift('')
}
return {
pathEnv,
pathExt,
pathExtExe,
}
}
const which = (cmd, opt, cb) => {
if (typeof opt === 'function') {
cb = opt
opt = {}
}
if (!opt)
opt = {}
const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt)
const found = []
const step = i => new Promise((resolve, reject) => {
if (i === pathEnv.length)
return opt.all && found.length ? resolve(found)
: reject(getNotFoundError(cmd))
const ppRaw = pathEnv[i]
const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw
const pCmd = path.join(pathPart, cmd)
const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd
: pCmd
resolve(subStep(p, i, 0))
})
const subStep = (p, i, ii) => new Promise((resolve, reject) => {
if (ii === pathExt.length)
return resolve(step(i + 1))
const ext = pathExt[ii]
isexe(p + ext, { pathExt: pathExtExe }, (er, is) => {
if (!er && is) {
if (opt.all)
found.push(p + ext)
else
return resolve(p + ext)
}
return resolve(subStep(p, i, ii + 1))
})
})
return cb ? step(0).then(res => cb(null, res), cb) : step(0)
}
const whichSync = (cmd, opt) => {
opt = opt || {}
const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt)
const found = []
for (let i = 0; i < pathEnv.length; i++) {
const ppRaw = pathEnv[i]
const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw
const pCmd = path.join(pathPart, cmd)
const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd
: pCmd
for (let j = 0; j < pathExt.length; j++) {
const cur = p + pathExt[j]
try {
const is = isexe.sync(cur, { pathExt: pathExtExe })
if (is) {
if (opt.all)
found.push(cur)
else
return cur
}
} catch (ex) { }
}
}
if (opt.all && found.length)
return found
if (opt.nothrow)
return null
throw getNotFoundError(cmd)
}
module.exports = which
which.sync = whichSync

View File

@ -1,4 +1,4 @@
const Ffmpeg = require('fluent-ffmpeg')
const Ffmpeg = require('../libs/fluentFfmpeg')
const EventEmitter = require('events')
const Path = require('path')
const fs = require('../libs/fsExtra')

View File

@ -1,4 +1,4 @@
const Ffmpeg = require('fluent-ffmpeg')
const Ffmpeg = require('../libs/fluentFfmpeg')
if (process.env.FFMPEG_PATH) {
Ffmpeg.setFfmpegPath(process.env.FFMPEG_PATH)

View File

@ -1,4 +1,4 @@
const Ffmpeg = require('fluent-ffmpeg')
const Ffmpeg = require('../libs/fluentFfmpeg')
const fs = require('../libs/fsExtra')
const Path = require('path')
const package = require('../../package.json')