前言
在使用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)
這樣只需引入寫好的這段代碼即可。
