【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)

  這樣只需引入寫好的這段代碼即可。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM