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