将FFmpeg编译为WebAssembly(= ffmpeg.js):第3部分-将avi转换为mp4



该系列的翻译部分清单:


  1. 做饭
  2. 用Emscripten编译
  3. 将AVI转换为mp4 (您在这里)



在这一部分中,我们将分析:



  1. 使用优化的参数编译FFmpeg库。
  2. Emscripten文件系统管理。
  3. ffmpeg.js v0.1.0和视频转换的开发。



使用优化的参数编译FFmpeg库


尽管本部分的最终目标是创建ffmpeg.js v0.1.0以便将avi转换为mp4,但在上一部分中,我们仅创建了“裸”版本的FFmpeg,可以使用多个参数进行优化。


  1. -Oz :优化代码并减小其大小(从30到15 MB)
  2. -o javascript / ffmpeg-core.js :将js和wasm文件保存到javascript目录。 (我们将从ffmpeg.js包装器库中调用ffmpeg-core.js,该包装器库提供了漂亮的API)
  3. -s MODULARIZE = 1 :创建一个库,而不是一个命令行实用程序(您将需要修改源,以下详细信息)
  4. -s EXPORTED_FUNCTIONS =“ [_ ffmpeg]” :将C函数“ ffmpeg”导出到JavaScript世界
  5. -s EXTRA_EXPORTED_RUNTIME_METHODS =“ [cwrap,FS,getValue,setValue]” :用于处理文件系统和指针的附加功能,有关详细信息,请参见文章与代码交互
  6. -s ALLOW_MEMORY_GROWTH = 1 :取消对已用内存的限制
  7. -lpthread :已删除,因为我们计划创建自己的工作程序。 (这是出版物第四部分的待办事项)

有关每个参数的更多详细信息,可以在github存储库脚本中的src / settings.js中找到。


当添加-s MODULARIZE = 1时,我们将需要修改源代码以满足模块化的要求(实际上,摆脱了main()函数)。 您只需要更改三行。


1. fftools / ffmpeg.c :将main重命名为ffmpeg


- int main(int argc, char **argv) + int ffmpeg(int argc, char **argv) 

2. fftools / ffmpeg.h :将ffmpeg添加到文件末尾以导出功能


 + int ffmpeg(int argc, char** argv); #endif /* FFTOOLS_FFMPEG_H */ 

3. fftools / cmdutils.c :注释退出(退出),以便我们的库不会为我们退出运行时(稍后我们将对此进行改进)。


 void exit_program(int ret){ if (program_exit) program_exit(ret); - exit(ret); + // exit(ret); } 

我们新版本的编译脚本:


 emcc \ -Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample \ -Qunused-arguments -Oz \ -o javascript/ffmpeg-core.js fftools/ffmpeg_opt.o fftools/ffmpeg_filter.o fftools/ffmpeg_hw.o fftools/cmdutils.o fftools/ffmpeg.o \ -lavdevice -lavfilter -lavformat -lavcodec -lswresample -lswscale -lavutil -lm \ -s MODULARIZE=1 \ -s EXPORTED_FUNCTIONS="[_ffmpeg]" \ -s EXTRA_EXPORTED_RUNTIME_METHODS="[cwrap, FS, getValue, setValue]" \ -s TOTAL_MEMORY=33554432 \ -s ALLOW_MEMORY_GROWTH=1 

ffmpeg-core.js准备好了!


如果您有使用ffmpeg的经验,那么您已经知道典型命令如下所示:


 $ ffmpeg -i input.avi output.mp4 

而且由于我们使用ffmpeg函数而不是main,因此命令调用将如下所示:


 const args = ['./ffmpeg', '-i', 'input.avi', 'output.mp4']; ffmpeg(args.length, args); 

当然,并非所有事情都那么简单,我们需要在JavaScript和C语言世界之间架起一座桥梁,所以让我们从emscripten文件系统开始。


Emscripten文件系统管理


Emscripten有一个虚拟文件系统来支持对C的读取/写入,ffmpeg-core.js使用该文件系统来处理视频文件。


File System API中阅读有关此内容的更多信息。


为了使一切正常,我们从emscripten中导出FS API,这是由于上述参数所致:


 -s EXTRA_EXPORTED_RUNTIME_METHODS="[cwrap, FS, getValue, setValue]" 

要保存文件,您需要在Node.js环境中以Uint8Array格式准备数组,可以通过以下方式进行操作:


 const fs = require('fs'); const data = new Uint8Array(fs.readFileSync('./input.avi')); 

并使用FS.writeFile()将其保存到emscripten文件系统中:


 require('./ffmpeg-core.js)() .then(Module => { Module.FS.writeFile('input.avi', data); }); 

并从emscripten下载文件:


 require('./ffmpeg-core.js)() .then(Module => { const data = Module.FS.readFile('output.mp4'); }); 

让我们开始开发ffmpeg.js来将这些复杂性隐藏在漂亮的API之后。


开发ffmpeg.js v0.1.0和视频转换


ffmpeg.js的开发并非易事,因为您经常需要在JavaScript和C的世界之间切换,但是如果您熟悉指针 ,将会更容易理解这里发生的事情。


我们的任务是像这样开发ffmpeg.js:


 const fs = require('fs'); const ffmpeg = require('@ffmpeg/ffmpeg'); (async () => { await ffmpeg.load(); const data = ffmpeg.transcode('./input.avi', 'mp4'); fs.writeFileSync('./output.mp4', data); })(); 

首先,下载ffmpeg-core.js,它通常是异步完成的,以免阻塞主线程。


看起来是这样的:


 const { setModule } = require('./util/module'); const FFmpegCore = require('./ffmpeg-core'); module.exports = () => ( new Promise((resolve, reject) => { FFmpegCore() .then((Module) => { setModule(Module); resolve(); }); }) ); 

将一个promise 换成另一个promise似乎很奇怪,这是因为FFmpegCore()不是真正的promise,而只是一个模拟promise API的函数。


下一步是使用Module通过cwrap函数获取ffmpeg函数:


 // int ffmpeg(int argc, char **argv) const ffmpeg = Module.cwrap('ffmpeg', 'number', ['number', 'number']); 

cwrap的第一个参数是函数的名称(必须在EXPORTED_FUNCTIONS中,且带有下划线),第二个是返回值的类型,第三个是函数的参数的类型(int argc和char ** argv)。


很清楚为什么argc是一个数字,但是为什么argv也是一个数字? argv是一个指针,并且该指针将地址存储在内存中(类型为0xfffffff),因此该指针类型是WebAssembly中的32位无符号。 这就是为什么我们将数字指定为argv类型的原因。


要调用ffmpeg(),第一个参数在JavaScript中将是一个常规数,但第二个参数应是一个指向字符数组的指针(JavaScript中的Uint8)。


我们将此任务分为两个子任务:


  1. 如何创建一个指向字符数组的指针?
  2. 如何创建一个指向指针数组的指针?

我们将通过创建str2ptr实用程序解决第一个问题:


 const { getModule } = require('./module'); module.exports = (s) => { const Module = getModule(); const ptr = Module._malloc((s.length+1)*Uint8Array.BYTES_PER_ELEMENT); for (let i = 0; i < s.length; i++) { Module.setValue(ptr+i, s.charCodeAt(i), 'i8'); } Module.setValue(ptr+s.length, 0, 'i8'); return ptr; }; 

Module._malloc()与C中的malloc()类似,它在堆上分配一块内存。 Module.setValue()通过指针设置特定值。


切记在字符数组的末尾添加0,以免发生意外情况。


处理完第一个子任务后,创建strList2ptr来解决第二个任务:


 const { getModule } = require('./module'); const str2ptr = require('./str2ptr'); module.exports = (strList) => { const Module = getModule(); const listPtr = Module._malloc(strList.length*Uint32Array.BYTES_PER_ELEMENT); strList.forEach((s, idx) => { const strPtr = str2ptr(s); Module.setValue(listPtr + (4*idx), strPtr, 'i32'); }); return listPtr; }; 

在这里主要要了解的是,指针是JavaScript中的Uint32值,因此listPtr是指向Uint32数组的指针,该数组存储指向Uint8数组的指针。


放在一起,我们得到以下ffmepg.transcode()的实现


 const fs = require('fs'); const { getModule } = require('./util/module'); const strList2ptr = require('./util/strList2ptr'); module.exports = (inputPath, outputExt) => { const Module = getModule(); const data = new Uint8Array(fs.readFileSync(inputPath)); const ffmpeg = Module.cwrap('ffmpeg', 'number', ['number', 'number']); const args = ['./ffmpeg', '-i', 'input.avi', `output.${outputExt}`]; Module.FS.writeFile('input.avi', data); ffmpeg(args.length, strList2ptr(args)); return Buffer.from(Module.FS.readFile(`output.${outputExt}`)); }; 

做完了! 现在我们有了ffmpeg.js v0.1.0,可以将avi转换为mp4。


您可以通过安装库自己测试结果:


 $ npm install @ffmpeg/ffmpeg@0.1.0 

然后像这样转换文件:


 const fs = require('fs'); const ffmpeg = require('@ffmpeg/ffmpeg'); (async () => { await ffmpeg.load(); const data = ffmpeg.transcode('./input.avi', 'mp4'); fs.writeFileSync('./output.mp4', data); })(); 

请记住,到目前为止,该库仅适用于Node.js,但是在下一部分中,我们将添加对Web-worker(以及Node.js中的child_process)的支持。


源代码:


Source: https://habr.com/ru/post/zh-CN473166/


All Articles