yt-dlp support

This commit is contained in:
synt-xerror
2026-03-12 01:22:29 -03:00
parent b96332e618
commit 240c392fad
5435 changed files with 991931 additions and 137 deletions

665
node_modules/fluent-ffmpeg/lib/capabilities.js generated vendored Normal file
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 ])\s+([^ ]+)\s+(.*)$/;
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);
};
};

261
node_modules/fluent-ffmpeg/lib/ffprobe.js generated vendored Normal file
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();
});
});
};
};

226
node_modules/fluent-ffmpeg/lib/fluent-ffmpeg.js generated vendored Normal file
View File

@@ -0,0 +1,226 @@
/*jshint node:true*/
'use strict';
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);

178
node_modules/fluent-ffmpeg/lib/options/audio.js generated vendored Normal file
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;
};
};

212
node_modules/fluent-ffmpeg/lib/options/custom.js generated vendored Normal file
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;
};
};

178
node_modules/fluent-ffmpeg/lib/options/inputs.js generated vendored Normal file
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;
};
};

41
node_modules/fluent-ffmpeg/lib/options/misc.js generated vendored Normal file
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;
};
};

162
node_modules/fluent-ffmpeg/lib/options/output.js generated vendored Normal file
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;
};
};

184
node_modules/fluent-ffmpeg/lib/options/video.js generated vendored Normal file
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;
};
};

291
node_modules/fluent-ffmpeg/lib/options/videosize.js generated vendored Normal file
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;
};
};

14
node_modules/fluent-ffmpeg/lib/presets/divx.js generated vendored Normal file
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']);
};

16
node_modules/fluent-ffmpeg/lib/presets/flashvideo.js generated vendored Normal file
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);
};

16
node_modules/fluent-ffmpeg/lib/presets/podcast.js generated vendored Normal file
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' ]);
};

662
node_modules/fluent-ffmpeg/lib/processor.js generated vendored Normal file
View File

@@ -0,0 +1,662 @@
/*jshint node:true*/
'use strict';
var spawn = require('child_process').spawn;
var path = require('path');
var fs = require('fs');
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;
};
};

456
node_modules/fluent-ffmpeg/lib/recipes.js generated vendored Normal file
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;
};
};

455
node_modules/fluent-ffmpeg/lib/utils.js generated vendored Normal file
View File

@@ -0,0 +1,455 @@
/*jshint node:true*/
'use strict';
var exec = require('child_process').exec;
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 {Number} 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;
}
};
}
};