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