音视频开发入门,可能绕不开 ffmpeg 这个项目,最近看了篇 知乎专栏,觉得这个事情很有意思。
比起直接编辑整个 ffmpeg 项目的 CLI 到前端,更符合实际需求的方式,是先基于 ffmpeg 各种 lib 二次开发出合适的功能,此时结果是可执行的二进制文件,可以用 lldb 或者 gdb 调试。然后再使用 Emscripten 编译到 Webassembly,如此一来可以解决 wasm 不易调试的问题。
跟着教程实现一个功能:解析出视频任意一帧的图像并绘制到 canvas 上。
Web demo 应用流程
demo 页的简单流程
|
从源码编译 ffmpeg
本文写就的时候使用的是 ffmpeg n4.2-dev
版,将其源码置于项目相对目录 lib/ffmpeg
下。
ffmpeg 是一个很大的项目,包含的很多功能对于我们的需求来说,都用不上,可以通过 configure 配置留下合适的功能集。这个其实就是一个可执行的 sh 脚本,比较复杂的项目,通常在实际编译之前,可以使用 configure 根据参数和环境生成实际编译过程需要的 Makefile。
项目 Makefile
参考 ffmpeg.js 项目的一些配置
|
说下 lib/ffmpeg/libavcodec/libavcodec.a
这个目标,分成几步:
emconfigure
是 emsdk 提供的工具,执行完这一步之后,会生成lib/ffmpeg/Makefile
emmake make
便是开始编译了,由于我们在前一步 configure 的时候有--enable-avcodec
,所以用这个 Makefile 编译,会生成lib/ffmpeg/libavcodec/libavcodec.a
这个静态库文件- 编译原本的 ffmpeg 代码会报错,定位到
libswscale/swscale.c
文件里,为了编译通过,在编译前加了个不影响主要功能的简单的 patch
运行 make lib/ffmpeg/libavcodec/libavcodec.a
,等待大约一两分钟,emscripten 编译 ffmpeg 静态库完成,闪过了一堆 warning 可以优雅地无视掉。接下来就是编写我们的应用接口代码并编译到 WebAssembly 了。
编译和使用
生成 JS 和 WASM 文件
|
使用 emcc
编译:
- 存放暴露给浏览器的相关接口的
web.c
- 存放通用的 ffmpeg 方法调用的
process.c
- 以及之前生成个几个静态库文件
.a
其中一些参数说明一下: - -s MODULARIZE=1
让 emcc 生成模块工厂函数(而且还是 UMD 格式的),留待之后调用。否则默认情况下生成的 JS 会立刻执行,而且还会污染其所在全局环境(例如添加一个 self.Module 对象)
一起生成目标文件 dist/vidy-standalone.js
,由于传递了 -s WASM=1
,还会生成同名的 dist/vidy-standalone.wasm
文件。JS 是一个一千来行的胶水代码,负责 WASM 模块的初始化和调用适配。WASM 文件大概 4.7M。
查看 emcc 文档 和 关于 -s
的全部可选 setting。
使用方法
简单的例子
|
稍微复杂点的例子
为了将模块更好地整合到前端工程中,有必要考虑在使用 webpack 的情况下如何引入。
参考 GoogleChromeLabs/squoosh 项目中的一些经验,首先看下 webpack 配置。webpack 团队在 v4 以后做了很多努力,想要让 WASM 模块的引入和使用与 js 文件一样方便,但实际实用中有很多边边角角 奇怪的问题和报错,而且处理一个好几兆的 wasm 文件拖慢 webpack 冷启动许多,我们可以用一下配置让 webpack 不去读取 WASM 文件。使用 file-loader 也可以简单地配置带哈希的文件名,比起在项目中硬编码 WASM 文件路径,少去一些缓存问题。
|
跟上面简单例子里效果相似的写法可以变成这样:
|
emcc 生成的胶水代码里,默认请求的 WASM 文件路径是 vidy-standalone.wasm
,但看看 emcc 这一部分实现 知道,如果给模块工厂函数 Module 传递了 locateFile
函数,就可以改写其内部会去请求的 WASM 文件路径。使用模块工厂函数的话,也不用自己去调用 fetch
了。
一些具体实现的代码
首先看看 web.c
里暴露出的方法签名:
|
buffer 数组头指针 buff
,buffer 长度 buff_length
,以及用单精度浮点数表示的需要提取图像的时间。返回数据为我们自定义的结构。
[JS] 将视频数据写入 WASM 线性内存
在 post.js
里,添加的一部分代码。
- 根据 C 的方法签名,使用 emscripten 的胶水代码工具函数
Module.cwrap
包装一个 JS 的调用方法 - 给 emscripten 模块加上了
Module.getImage
方法,供外部调用
|
[C] 程序头部
首先声明一些方便数据读取的全局变量:
|
全局变量 global_buffer_data
留作存放原始视频数据的结构,它所在的内存区域会被 JS 直接写入。
[C] avcodec 解析视频文件
我们需要让 ffmpeg 能够从内存(而不是文件)中读取视频数据。
|
新建 AVIOContext *avio_ctx
,指定目标 buffer 指针,目标 buffer 大小,以及我们提供的读取数据的 read_packet
函数,该 iocontext 需要读下一段数据时, read_packet
函数就将 global_buffer_data
中指定大小的数据写入目标 *buf
位置
|
[C] 获取图片 rgb 数据
这里内容太多,主要涉及 FFMpeg 的接口和视频编解码的知识,准备另写一篇。
[C] 将图像等数据写入内存
当拿到包含 RGB 格式图像数据的 AVFrame *pFrameRGB
后,是时候将其中的颜色信息取出,转化为线性存储的,利于 JS 中 Canvas 元素使用的数据格式。
|
此函数返回的 buffer
指针指向的内存区域,会按照 rgbrgb...
的顺序存储图像颜色数据。每个像素需要 3 个存储单元,所以整个的 buffer_size
会是 height * width * 3
。
接下来我们回到 JS 端。
[JS] 根据指针读出数据,构建 ImageData
WASM 返回的只是一个内存偏移量,此时我们手上有整个 WASM 实例的内存区域,得想办法把有用的数据读取出来。
首先我们知道 MyImageData
结构体宽和高都是用 uint32_t
,紧接着存放颜色信息的数组单元类型为 uint8_t
。
Emscripten 的胶水代码有提供 HEAPU (8/16/32/64) 几种步长的 dataviewer,可以按照以下方法读出数字和颜色数组。
|
绘制到 canvas 上就很简单了
|
总结
跟着别人的文章思路,小小修改,跑通了一个 demo,大致熟悉一下 C 项目使用 Emscripten 转化为前端可用模块的方案。
不是很熟悉 C 语言,同时在 JS 和 C 端手动管理内存虽然对于入门者来说很容易操作,但稍显繁琐。
Emscripten 多用于翻译现有的 C/C++ 库代码,对于 Web API 和前端生态的支持,明显没有隔壁 Mozilla 的 Rust 社区积极。不过音视频技术实现,的确是 C 的传统强项领域,若想少造轮子,还是要好好学习的。
参考
https://zhuanlan.zhihu.com/p/40786748