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

  这样只需引入写好的这段代码即可。

 

posted @ 2020-12-20 22:24  blogCblog  阅读(2474)  评论(0编辑  收藏  举报