mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-26 08:12:25 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			666 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			666 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /*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);
 | |
|   };
 | |
| };
 |