该系列的翻译部分清单:
- 做饭
- 用Emscripten编译
- 将AVI转换为mp4 (您在这里)
在这一部分中,我们将分析:
- 使用优化的参数编译FFmpeg库。
- Emscripten文件系统管理。
- ffmpeg.js v0.1.0和视频转换的开发。
使用优化的参数编译FFmpeg库
尽管本部分的最终目标是创建ffmpeg.js v0.1.0以便将avi转换为mp4,但在上一部分中,我们仅创建了“裸”版本的FFmpeg,可以使用多个参数进行优化。
- -Oz :优化代码并减小其大小(从30到15 MB)
- -o javascript / ffmpeg-core.js :将js和wasm文件保存到javascript目录。 (我们将从ffmpeg.js包装器库中调用ffmpeg-core.js,该包装器库提供了漂亮的API)
- -s MODULARIZE = 1 :创建一个库,而不是一个命令行实用程序(您将需要修改源,以下详细信息)
- -s EXPORTED_FUNCTIONS =“ [_ ffmpeg]” :将C函数“ ffmpeg”导出到JavaScript世界
- -s EXTRA_EXPORTED_RUNTIME_METHODS =“ [cwrap,FS,getValue,setValue]” :用于处理文件系统和指针的附加功能,有关详细信息,请参见文章与代码交互 。
- -s ALLOW_MEMORY_GROWTH = 1 :取消对已用内存的限制
- -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)。
我们将此任务分为两个子任务:
- 如何创建一个指向字符数组的指针?
- 如何创建一个指向指针数组的指针?
我们将通过创建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)的支持。
源代码: