JSDoc: Source: ffprobe.js

# Source: ffprobe.js

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

          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'].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();
          });
        });
      };
    };