使用WebAssembly+FFmpeg实现前端转码(下)

上一篇中我们简单介绍了WebAssembly(wasm)的生成过程以及一些必要工具的配置。

本篇将开始正式编译以及之后导出的工作,着重将介绍emscripten api以及js层调用wasm代码的一些手段,想通过这个例子,让大家简单了解一下js和wasm之间互操作的方法

开始编译FFmpeg到LLVM

跟着上篇的进度,现在我们已经拥有了Emscripten的编译环境,现在让我们正式开始编译

# 首先,从github等地方获取ffmpeg的源代码
git clone https://github.com/FFmpeg/FFmpeg
cd FFmpeg

# 开始configure
# 这里的参数参考自videoconverter.js,其中注意需要额外带上下面第一行的CPPFLAGS
# 否则不能在最新的emcripten下编译通过
# 这里通过--cc="emcc"来指定编译器为emcc,emcc会调用clang来将target设置成LLVM
CPPFLAGS="-D_POSIX_C_SOURCE=200112 -D_XOPEN_SOURCE=600" \
emconfigure ./configure --cc="emcc" \
--prefix=$(pwd)/../dist --enable-cross-compile --target-os=none --arch=x86_64 \
--cpu=generic --disable-ffplay --disable-ffprobe --disable-ffserver \
--disable-asm --disable-doc --disable-devices --disable-pthreads \
--disable-w32threads --disable-network --disable-hwaccels \
--disable-parsers --disable-bsfs --disable-debug --disable-protocols \
--disable-indevs --disable-outdevs --enable-protocol=file

# ?等待运行完成,执行make开始编译
make

作为一个1GB1核心的废柴机器,我编译了几十分钟然后跪了...


好吧链接阶段内存不够用了?(机器还顽强地在跑其他个人项目,相信各位是遇不到这样的问题的。。如果一切正常,你将看到下面的结果


你会得到一个叫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 <file> 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 <file> 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命令行参数
  • 通过虚拟文件系统传入输入文件以及获取输出文件

这里放出我最终自己使用pre.js和post.js代码

好啦好啦,扯了这么多,终于万事俱备可以愉快的开始最后一步编译啦

Final Battle 编译LLVM到WebAssmbly

这里使用的命令依旧是emcc,但是注意此时emcc的输入为LLVM bitcode,它将会调用emscriptem来将其编译到js (和第一步emcc的行为不同,因为输入格式不同,target也会不同)

# 这里的ffmpeg是上一步编译输出的LLVM bitcode
cp ffmpeg ffmpeg.bc

# 最终的输出是 -o 指定的,这些 -s 参数的意义可以从emcc的文档中找到
# 这里打开了ALLOW_MEMORY_GROWTH是因为在移动端测试下会遇到内存(wasm/asm.js的虚拟内存)
# 不够的情况,默认内存大小是TOTAL_MEMORY指定的
# 设置WASM=1就会编译到WebAssembly,默认编译到asm.js
emcc -s ASSERTIONS=1 -s VERBOSE=1 -s TOTAL_MEMORY=33554432 \
-s ALLOW_MEMORY_GROWTH=1 -s WASM=1 -O2 -v ffmpeg.bc \
-o ../ffmpeg.js --pre-js ../ffmpeg_pre.js --post-js ../ffmpeg_post.js

如果一切顺利,你就会得到最终的输出,包括这2货


因为我们之前在pre.js里写了入口函数并将其export,所以我们只需要直接require("ffmpeg.js"),或者在worker里直接importScript("ffmpeg.js")就可以拿到入口函数。

注意ffmpeg.js和ffmpeg.wasm需要放在一个路径下,执行ffmpeg.js的时候会去请求wasm文件

下面贴上使用案例,worker下的

self.importScripts('ffmpeg.js');

onmessage = function(e) {
  console.log('ffmpeg_run', ffmpeg_run);
  var files = e.data;
  console.log(files);
  ffmpeg_run({
    arguments: ['-i', '/input/' + files[0].name, '-b:v', '64k', '-bufsize', '64k', '-vf', 'showinfo', '-strict', '-2', 'out.mp4'],
    files: files,
  }, function(results) {
    console.log('result',results);
    self.postMessage(results[0].data, [results[0].data]);
  });
}

workers的输入就是一个WebFile,通过emscriptenAPI的workerfs可以直接将html5的File对象传入虚拟文件系统

输出就是输出文件的ArrayBuffer,最后再贴个效果图



完结撒花

好吧也算是自己絮絮叨叨了很久,希望大家通过这个实战案例能对WebAssembly有个基本的认识。其实像WebAssembly还有很大的发挥空间,通信和传参方式也不局限于文中提到的d方法,像通过heap对象传透明指针到c层可以实现很多有意思的feature


好啦,就码到这里,希望本文能对你有所帮助,晚安~

编辑于 2017-07-16

文章被以下专栏收录