| |
| 'use strict'; |
|
|
| var fs = require('fs'); |
| var path = require('path'); |
| var async = require('async'); |
| var utils = require('./utils'); |
|
|
| |
| |
| |
|
|
| 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) { |
| |
| |
| |
| |
| |
| |
| |
| |
| proto.setFfmpegPath = function(ffmpegPath) { |
| cache.ffmpegPath = ffmpegPath; |
| return this; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| proto.setFfprobePath = function(ffprobePath) { |
| cache.ffprobePath = ffprobePath; |
| return this; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| proto.setFlvtoolPath = function(flvtool) { |
| cache.flvtoolPath = flvtool; |
| return this; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| proto._forgetPaths = function() { |
| delete cache.ffmpegPath; |
| delete cache.ffprobePath; |
| delete cache.flvtoolPath; |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| proto._getFfmpegPath = function(callback) { |
| if ('ffmpegPath' in cache) { |
| return callback(null, cache.ffmpegPath); |
| } |
|
|
| async.waterfall([ |
| |
| 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, ''); |
| } |
| }, |
|
|
| |
| 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 || '')); |
| } |
| }); |
| }; |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| proto._getFfprobePath = function(callback) { |
| var self = this; |
|
|
| if ('ffprobePath' in cache) { |
| return callback(null, cache.ffprobePath); |
| } |
|
|
| async.waterfall([ |
| |
| 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, ''); |
| } |
| }, |
|
|
| |
| function(ffprobe, cb) { |
| if (ffprobe.length) { |
| return cb(null, ffprobe); |
| } |
|
|
| utils.which('ffprobe', function(err, ffprobe) { |
| cb(err, ffprobe); |
| }); |
| }, |
|
|
| |
| 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 || '')); |
| } |
| }); |
| }; |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| proto._getFlvtoolPath = function(callback) { |
| if ('flvtoolPath' in cache) { |
| return callback(null, cache.flvtoolPath); |
| } |
|
|
| async.waterfall([ |
| |
| 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, ''); |
| } |
| }, |
|
|
| |
| 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, ''); |
| } |
| }, |
|
|
| |
| function(flvtool, cb) { |
| if (flvtool.length) { |
| return cb(null, flvtool); |
| } |
|
|
| utils.which('flvmeta', function(err, flvmeta) { |
| cb(err, flvmeta); |
| }); |
| }, |
|
|
| |
| 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 || '')); |
| } |
| }); |
| }; |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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); |
| }); |
| }; |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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); |
| }); |
| }; |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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); |
| }); |
| }; |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| proto.availableFormats = |
| proto.getAvailableFormats = function(callback) { |
| if ('formats' in cache) { |
| return callback(null, cache.formats); |
| } |
|
|
| |
| this._spawnFfmpeg(['-formats'], { 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(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); |
| }); |
| }; |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| proto._checkCapabilities = function(callback) { |
| var self = this; |
| async.waterfall([ |
| |
| function(cb) { |
| self.availableFormats(cb); |
| }, |
|
|
| |
| function(formats, cb) { |
| var unavailable; |
|
|
| |
| 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')); |
| } |
|
|
| |
| 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(); |
| }, |
|
|
| |
| function(cb) { |
| self.availableEncoders(cb); |
| }, |
|
|
| |
| function(encoders, cb) { |
| var unavailable; |
|
|
| |
| 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')); |
| } |
|
|
| |
| 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); |
| }; |
| }; |
|
|