123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455 |
- /*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 {Numebr} maxLines maximum number of lines to store (<= 0 for unlimited)
- */
- linesRing: function(maxLines) {
- var cbs = [];
- var lines = [];
- var current = null;
- var closed = false
- var max = maxLines - 1;
- function emit(line) {
- cbs.forEach(function(cb) { cb(line); });
- }
- return {
- callback: function(cb) {
- lines.forEach(function(l) { cb(l); });
- cbs.push(cb);
- },
- append: function(str) {
- if (closed) return;
- if (str instanceof Buffer) str = '' + str;
- if (!str || str.length === 0) return;
- var newLines = str.split(nlRegexp);
- if (newLines.length === 1) {
- if (current !== null) {
- current = current + newLines.shift();
- } else {
- current = newLines.shift();
- }
- } else {
- if (current !== null) {
- current = current + newLines.shift();
- emit(current);
- lines.push(current);
- }
- current = newLines.pop();
- newLines.forEach(function(l) {
- emit(l);
- lines.push(l);
- });
- if (max > -1 && lines.length > max) {
- lines.splice(0, lines.length - max);
- }
- }
- },
- get: function() {
- if (current !== null) {
- return lines.concat([current]).join('\n');
- } else {
- return lines.join('\n');
- }
- },
- close: function() {
- if (closed) return;
- if (current !== null) {
- emit(current);
- lines.push(current);
- if (max > -1 && lines.length > max) {
- lines.shift();
- }
- current = null;
- }
- closed = true;
- }
- };
- }
- };
|