JSDoc: Source: capabilities.js

# Source: capabilities.js

    /*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);
      };
    };