最近学习了一下 wasm
WebAssembly 是由主流浏览器厂商组成的 W3C 社区团体 制定的一个新的规范。
这是一个运行于沙箱中的指令集,其二进制的格式能最大效率发挥硬件能力,运行速度是 js 的几倍十几倍
而因为其是二进制格式,你可以将 C/C++/java 等等等语言编译成 wasm 格式,并在各种语言的虚拟机中运行,自然也包括 JavaScript 虚拟机
这也就意味着我们能在前端实现一些以前只能在后端实现的功能
这里我们以 ffmpeg 举例,这是一个 C++ 开发的很有名的开源库,能实现大部分视频的编码格式处理,包括压缩,转码等功能
我们将 ffmpeg 转成 wasm 格式后就能在前端实现视频处理
注:如果只是想直接使用,请直接滑到最下面
Emscripten & asm.js
Emscripten: An LLVM-to-JavaScript Compiler
Asm.js an extraordinarily optimizable, low-level subset of JavaScript
这是 wasm 过程中 2 个比较重要的工具,官网链接在上:
- Emscripten: wasm 的编译器,根据上面的介绍,这是一个 LLVM 到 js 的编译器。咱对于编译原理这些底层的东西了解也不太多,简单理解的话 LLVM 就是一个编译的中间状态,使用 llvm-gcc 或者 clang 这些编译工具可以将 c/c++编译到 LLVM bitcode 这样一个中间代码的状态, 同样类似像 rust 的语言也可以编译到 LLVM。有了 LLVM bitcode,就可以使用 Emscripten 这个工具来将其编译成更底层的汇编 js 代码。
- 那么更底层的汇编 js 代码是啥,早期在 wasm 还没有实现之前,汇编 js 的实现就是 Asm.js(字如其名。。)。asm.js 的核心功能就是模拟一个汇编语言的运行环境,通过一个大的 UintArray 数组来模拟机器内存,然后通过 js 实现各种汇编指令来对这个虚拟的内存(UintArray 对象)进行操作。
通过上面这 2 个步骤,我们先是把 c/c++通过编译器编译到 LLVM,再利用 emscripten 将 LLVM 编译成 asm.js,当我们在运行这段 asm.js 代码的时候,就会省去大量的解释器开销,相当于直接使用汇编代码一样来直接操作内存空间运行。
但是 asm.js 实现的汇编环境毕竟还是基于 js 的,在性能上还是不能完全满意,这个时候,WebAssebmly 就应运而生了,由浏览器来实现汇编环境,规定好 webassembly 的汇编格式,使得性能进一步提示
编译过程
扯了这么多,还是具体说说编译 FFmpeg 的过程吧。编译 ffmpeg 参考了 github 上的一个现有项目,videoconverter.js,下面从 0 开始按照顺序说明一下
- 编译 Emcc 工具链
首先我们需要编译 Emcripten 到你的机器上,我这里用的是一台 Ubuntu14.04.
在执行下面命令之前,请先确保你的机器上有如下依赖
cmake git python2.7.X
1 | git clone https://github.com/juj/emsdk && cd emsdk |
编译完成之后,按照输出给出的提示,以后只要直接 source ./emsdk_env.sh 就会自动配置环境变量,将各种编译工具的设置成 emsdk 下的 path
至此,在命令行输入 emcc –help,看到正常的输出说明编译和配置成功。现在你就拥有了一个编译 wasm 的编译器。
开始编译 FFmpeg 到 LLVM
跟着上篇的进度,现在我们已经拥有了 Emscripten 的编译环境,现在让我们正式开始编译
1 | # 首先,从github等地方获取ffmpeg的源代码 |
如果一切正常,你将看到下面的结果
你会得到一个叫 ffmpeg 的输出文件(如果最后阶段报 strip error 直接无视掉就好,和我们的栗子没有关系)
这个 ffmpeg 文件就是我们第一步要得到的 LLVM bitcode,下一步我们就可以将这个 LLVM bitcode 编译到 js 或者 wasm 里面啦
行百里者半九十
在执行最后一步之前,有个疑问,ffmpeg 正常编译出来的二进制应该是个命令行文件,接受命令行参数执行操作。但当我们把它编译到 js 里时,该怎么向它传递参数呢?传递了参数之后又要怎么样才能拿到它的执行结果呢?
理想的状况下,我们希望最终编译出来的 wasm/asm.js 文件能向外 export 一个函数来作为入口,我们通过这个函数入口传入参数,通过返回值或者回调函数等方法获取结果。
其实这就是 wasm/asm.js 文件和浏览器 js 的信息通信问题,Emscripten 为我们提供了这样一些解决方案,以下摘自 Emscripten 官网文档 API Reference - Emscripten 1.37.10 documentation
- pre.js 和 post.js
–pre-js
Specify a file whose contents are added before the generated code. This is done before optimization, so it will be minified properly if the Closure Compiler is run. –post-js Specify a file whose contents are added after the generated code. This is done before optimization, so it will be minified properly if the Closure Compiler is run.
这 2 个其实是 emcc 编译时的 2 个可选项,通过传入用户自己编写的 pre.js 和 post.js,来将最终生成的代码包裹在 pre.js 和 post.js 之下,就像这样 [pre.js]–[生成的 wasm/asm.js]–[post.js]
也就是可以让我在 wasm 被执行之前,运行 pre.js 里的代码,在 wasm 代码被执行后(但并不是执行结束后)运行 post.js 里的代码。
那这样有什么意义呢?其实最方便的就是我们可以在 pre.js 里 export 一个自定义的函数给外部,或者比如说在 pre.js 里做一些必要的初始化,具体做法看下面的介绍
- Module 对象
Module is a global JavaScript object with attributes that Emscripten-generated code calls at various points in its execution.
简单来说,这里的 Module 是一个特殊全局对象,当我们执行最终的 wasm/asm.js 文件时,在不同的阶段,程序会自动调用 Module 下不同的方法。比如执行结束时(Module.postrun),输出到 stdout(Module.print)时等等。我们就可以自己实现 Module 下那些阶段的函数来挂钩子,比如将返回值返回给回调函数,将 log 打印到浏览器 console 等等。
结合上面的 pre.js,是不是想到什么了?
没错,我们可以在 pre.js 里编写定义 Module 函数,来控制 wasm/asm.js 程序的输入和输出。
Module.arguments The commandline arguments. The value of arguments contains the values returned if compiled code checks argc and argv.
通过给 Module 的 arguments 传入值,emscripten 就会把这个值带到 wasm/asm.js 程序的命令行输入里,也就是 ffmpeg 的命令行输入。
这样,我们就大概解决了命令行参数输入的问题,但是还有几个点需要考虑。
ffmpeg 输入文件名之后,但是文件要怎么传给他? ffmpeg 输出的文件我们又要怎么得到?
- File System API
File System API - Emscripten 1.37.10 documentation
没错,解决方案依旧是 EmscriptenAPI
emscripten 为我们提供了一套文件系统 api,通过这套 API,就可以用 ArrayBuffer,Web File 等方法来创建文件,并将这个文件写入 emscripten 为我们提供的一个虚拟文件系统。
之后 wasm/asm.js 程序请求文件都会去使用这个虚拟的文件系统,包括读入和写出。
具体 api 的使用方法这里就不说太多了有兴趣的可以看看文档。
至此,我们通过这 3 个方法,解决了我们的需求:
- 导出一个入口函数到上层 js
- 传入函数参数来控制 ffmpeg 命令行参数
- 通过虚拟文件系统传入输入文件以及获取输出文件
Final Battle 编译 LLVM 到 WebAssmbly
这里使用的命令依旧是 emcc,但是注意此时 emcc 的输入为 LLVM bitcode,它将会调用 emscriptem 来将其编译到 js (和第一步 emcc 的行为不同,因为输入格式不同,target 也会不同)
1 | # 这里的ffmpeg是上一步编译输出的LLVM bitcode |
如果一切顺利,你就会得到最终的输出,包括这 2 货
因为我们之前在 pre.js 里写了入口函数并将其 export,所以我们只需要直接 require(“ffmpeg.js”),或者在 worker 里直接 importScript(“ffmpeg.js”)就可以拿到入口函数。
注意 ffmpeg.js 和 ffmpeg.wasm 需要放在一个路径下,执行 ffmpeg.js 的时候会去请求 wasm 文件
下面贴上使用案例,worker 下的
1 | self.importScripts("ffmpeg.js"); |
workers 的输入就是一个 WebFile,通过 emscriptenAPI 的 workerfs 可以直接将 html5 的 File 对象传入虚拟文件系统
输出就是输出文件的 ArrayBuffer,最后再贴个效果图
使用 ffmpeg.wasm 在前端实现视频压缩
当然如果我们不想自己编译,可以直接用已经编译好的wasm文件
比如这个:github 仓库地址: https://github.com/ffmpegwasm/ffmpeg.wasm
实例代码:
1 |
|
压缩一个 30M 的视频能达到 2.3M 左右,但是因为 ffmpeg 库本身比较大,我们又是全部编译过来的,有大概 20 多 M,因此不建议在移动端使用
当然如果你是 C++ 大佬,可以自己精简 ffmpeg 库之后再编译