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/);
  var data = {
    streams: []
  };

  function parseBlock() {
    var data = {};

    var line = lines.shift();
    while (line) {
      if (line.match(/^\[\//)) {
        return data;
      }

      var kv = line.match(/^([^=]+)=(.*)$/);
      if (kv) {
        if (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 (line) {
    if (line === '[STREAM]') {
      var stream = parseBlock();
      data.streams.push(stream);
    } else if (line === '[FORMAT]') {
      data.format = parseBlock();
    }

    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 {FfmpegCommand~ffprobeCallback} callback callback function
   *
   */
  proto.ffprobe = function(index, callback) {
    var input;

    if (typeof callback === 'undefined') {
      callback = index;

      if (!this._currentInput) {
        return callback(new Error('No input specified'));
      }

      input = this._currentInput;
    } else {
      input = this._inputs[index];

      if (!input) {
        return callback(new Error('Invalid input index'));
      }
    }

    if (!input.isFile) {
      return callback(new Error('Cannot run ffprobe on non-file input'));
    }

    // Find ffprobe
    this._getFfprobePath(function(err, path) {
      if (err) {
        return callback(err);
      } else if (!path) {
        return callback(new Error('Cannot find ffprobe'));
      }

      var stdout = '';
      var stdoutClosed = false;
      var stderr = '';
      var stderrClosed = false;

      // Spawn ffprobe
      var ffprobe = spawn(path, [
        '-show_streams',
        '-show_format',
        input.source
      ]);

      ffprobe.on('error', function(err) {
        callback(err);
      });

      // 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 callback(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) {
            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];
              });
            }
          });

          callback(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();
      });
    });
  };
};