【ffmpeg】解决fluent-ffmpeg使用ffprobe无效问题
前言
在使用fluent-ffmpeg时,ffprobe方法无论添加什么选项都只返回视频的元信息。
如下图:下图是获取视频信息的函数
如下图:下图为调用并打印出视频信息
使用ffprobe不管添加什么参数,第一张图添加了 ['-v', 'quiet', '-select_streams', 'v', '-show_entries', 'frame=pkt_pts_time,pict_type'] (获取IBP帧时间点), 但是获取到的结果还是一些视频的元信息。
查看源代码
在 fluent-ffmpeg/lib/ffprobe.js 中:
1 var ffprobe = spawn(path, ['-show_streams', '-show_format'].concat(options, src));
这段代码可以看出,是在使用ffprobe执行命令,并且默认添加了 '-show_streams' 和 '-show_format' 选项,而options则是调用者传进来的,两者进行合并。这里既然合并并执行了,为何没效呢,说明问题不在这,继续看。
1 ffprobe.stdout.on('data', function(data) { 2 console.log(data.toString()) 3 stdout += data; 4 }); 5 6 ffprobe.stdout.on('close', function() { 7 stdoutClosed = true; 8 handleExit(); 9 }); 10 11 ffprobe.stderr.on('data', function(data) { 12 stderr += data; 13 }); 14 15 ffprobe.stderr.on('close', function() { 16 stderrClosed = true; 17 handleExit(); 18 });
这些代码则是使用 spawn 执行的事件监听,可以直接看看执行的结果
可以看到,确实是有结果的。说明在处理这些数据的时候没有处理这些数据,只处理了他原本默认选项的数据。
再看 spawn 的 close事件,调用了 handleExit
1 function handleExit(err) { 2 if (err) { 3 exitError = err; 4 } 5 6 if (processExited && stdoutClosed && stderrClosed) { 7 if (exitError) { 8 if (stderr) { 9 exitError.message += '\n' + stderr; 10 } 11 12 return handleCallback(exitError); 13 } 14 15 // Process output 16 var data = parseFfprobeOutput(stdout); 17 18 // Handle legacy output with "TAG:x" and "DISPOSITION:x" keys 19 [data.format].concat(data.streams).forEach(function(target) { 20 if (target) { 21 var legacyTagKeys = Object.keys(target).filter(legacyTag); 22 23 if (legacyTagKeys.length) { 24 target.tags = target.tags || {}; 25 26 legacyTagKeys.forEach(function(tagKey) { 27 target.tags[tagKey.substr(4)] = target[tagKey]; 28 delete target[tagKey]; 29 }); 30 } 31 32 var legacyDispositionKeys = Object.keys(target).filter(legacyDisposition); 33 34 if (legacyDispositionKeys.length) { 35 target.disposition = target.disposition || {}; 36 37 legacyDispositionKeys.forEach(function(dispositionKey) { 38 target.disposition[dispositionKey.substr(12)] = target[dispositionKey]; 39 delete target[dispositionKey]; 40 }); 41 } 42 } 43 }); 44 45 handleCallback(null, data); 46 } 47 }
具体关键代码为,上面代码片段的 16 行,parseFfprobeOutput, 解析ffprobe的输出
1 function parseFfprobeOutput(out) { 2 var lines = out.split(/\r\n|\r|\n/); 3 4 lines = lines.filter(function (line) { 5 return line.length > 0; 6 }); 7 8 var data = { 9 streams: [], 10 format: {}, 11 chapters: [] 12 13 }; 14 15 function parseBlock(name) { 16 var data = {}; 17 18 var line = lines.shift(); 19 while (typeof line !== 'undefined') { 20 if (line.toLowerCase() == '[/'+name+']') { 21 return data; 22 } else if (line.match(/^\[/)) { 23 line = lines.shift(); 24 continue; 25 } 26 27 var kv = line.match(/^([^=]+)=(.*)$/); 28 if (kv) { 29 if (!(kv[1].match(/^TAG:/)) && kv[2].match(/^[0-9]+(\.[0-9]+)?$/)) { 30 data[kv[1]] = Number(kv[2]); 31 } else { 32 data[kv[1]] = kv[2]; 33 } 34 } 35 36 line = lines.shift(); 37 } 38 39 return data; 40 } 41 42 var line = lines.shift(); 43 while (typeof line !== 'undefined') { 44 if (line.match(/^\[stream/i)) { 45 var stream = parseBlock('stream'); 46 data.streams.push(stream); 47 } else if (line.match(/^\[chapter/i)) { 48 var chapter = parseBlock('chapter'); 49 data.chapters.push(chapter); 50 51 52 53 } else if (line.toLowerCase() === '[format]') { 54 data.format = parseBlock('format'); 55 } 56 57 line = lines.shift(); 58 } 59 60 return data; 61 }
这里代码就是具体解析ffprobe执行命令后输出的字符串的函数,不管有什么结果,都只解析了 stream、chapter、format 三个字段的值。
修改代码
-
手动修改
只需将 while 循环里的代码修改即可。
修改前:
1 while (typeof line !== 'undefined') { 2 if (line.match(/^\[stream/i)) { 3 var stream = parseBlock('stream'); 4 data.streams.push(stream); 5 } else if (line.match(/^\[chapter/i)) { 6 var chapter = parseBlock('chapter'); 7 data.chapters.push(chapter); 8 9 10 11 } else if (line.toLowerCase() === '[format]') { 12 data.format = parseBlock('format'); 13 } 14 15 line = lines.shift(); 16 }
修改后:
1 while (typeof line !== 'undefined') { 2 3 if (line.match(/^\[stream/i)) { 4 var stream = parseBlock('stream'); 5 data.streams.push(stream); 6 } else if (line.match(/^\[chapter/i)) { 7 var chapter = parseBlock('chapter'); 8 data.chapters.push(chapter); 9 } else if (line.toLowerCase() === '[format]') { 10 data.format = parseBlock('format'); 11 } else if (line.match(/^\[[^\/].*?/i)) { 12 13 let name = line.slice(1,-1).toLowerCase() 14 if(!data[name] || !(data[name] instanceof Array)) data[name] = [] 15 var res = parseBlock(name) 16 data[name].push(res) 17 } 18 19 line = lines.shift(); 20 }
上面是手动的修改 fluent-ffmpeg内的源代码,每次安装 fluent-ffmpeg 都要重新修改非常麻烦。
-
自动修改
原理是读取 fluent-ffmpeg/lib/ffprobe.js 文件的代码字符串,将代码字符串转换为 AST,再修改 AST,最后将 AST 转换为代码,再将代码写到 ffprobe.js 文件中。
1 require('fluent-ffmpeg/lib/ffprobe.js') // 导入 ffprobe 2 const esprima = require('esprima') 3 const escodegen = require('escodegen') 4 const estraverse = require('estraverse') 5 const fs = require('fs') 6 7 const sourcePath = module.children[0].id // 用 module 获取到 ffprobe 的路径(得先导入ffprobe) 8 9 10 // 修改后的代码的字符串 11 const newParseCode = ` 12 while (typeof line !== 'undefined') { 13 14 if (line.match(/^\\[stream/i)) { 15 var stream = parseBlock('stream'); 16 data.streams.push(stream); 17 } else if (line.match(/^\\[chapter/i)) { 18 var chapter = parseBlock('chapter'); 19 data.chapters.push(chapter); 20 } else if (line.toLowerCase() === '[format]') { 21 data.format = parseBlock('format'); 22 } else if (line.match(/^\\[[^\\/].*?/i)) { 23 24 let name = line.slice(1,-1).toLowerCase() 25 if(!data[name] || !(data[name] instanceof Array)) data[name] = [] 26 var res = parseBlock(name) 27 data[name].push(res) 28 } 29 30 line = lines.shift(); 31 } 32 ` 33 34 // 读取 ffprobe 的源代码为字符串 35 const oldParseCode = fs.readFileSync(sourcePath).toString() 36 37 // 将修改后的代码字符串转换为 AST 38 const newParseAST = esprima.parseScript(newParseCode) 39 40 // 将 ffprobe 源代码字符串转换为 AST 41 var oldParseAST = esprima.parseScript(oldParseCode) 42 43 // 用 estraverse 找到 parseFfprobeOutput 函数的位置 44 estraverse.traverse(oldParseAST, { 45 enter: (node) => { 46 47 if (node.type == 'FunctionDeclaration' && node.id.name == 'parseFfprobeOutput') { 48 49 // 再找到 while 循环的位置 50 estraverse.replace(node, { 51 enter: (node, parent) => { 52 if (node.type == 'WhileStatement' && parent.body.length > 4) { 53 54 // 直接将 while 循环位置的 AST 进行替换为修改后代码字符串的 AST 55 return newParseAST 56 } 57 } 58 59 }) 60 61 62 return 63 } 64 } 65 }) 66 67 // 用 escodegen 将 AST 转换为代码 68 const code = escodegen.generate(oldParseAST) 69
70 // 将代码字符串写到文件中
72 fs.writeFileSync(sourcePath, code)
这样只需引入写好的这段代码即可。