前言
本人从事 Web 音视频行业五年了, 最近一年我都在研究如何提升 Web 上音视频处理能力,拓宽 Web 上音视频处理业务场景。到目前为止算是有一点方向和想法,写下此文与大家交流交流。
背景
简单介绍一下 Web 上的音视频发展史。总共可分为几个时代。
首先是 Flash 时代,早在 2002 年人们就可以在网页上通过安装 Flash 插件来播放视频,这也是当时能在网页上播放视频的唯一方法;因此 Flash 得以爆火,Flash 也成为了网页开发上的主角;人们不仅可以用它播放视频,还可以做游戏、广告、各种酷炫的动效。
之后是 2008 年开始的 HTML5 时代,HTML5 带来的 video 和 audio 元素可以让我们在网页上不依赖 Flash 直接播放音频和视频,但其支持播放的音视频格式有限所以此时还无法正面与 Flash 交锋。直到 2015 年左右,由于各种各样的原因各大浏览器厂商宣布将来不再支持 Flash 插件,开始出现以 MSE (MediaSource Extension) 为技术核心的纯 HTML5 音视频开发路线,开源社区出现了一些大家都耳熟能详的项目,这其中就有 B 站开源的 flv.js ,还有其他诸如 hls.js、mpegts.js、dash.js 等等。
再之后是 2017 年左右出现了以 WebRTC 为核心的技术路线,将实时音视频通信带到了 Web 上。严格来说我认为 MSE 和 WebRTC 应该同属一个时代,因为它们的使用场景不是竞争关系。
而现在我们应该处在 Web 音视频发展真正的下一个时代的开始;这个时代将是以 WebCodecs、WebGPU、WebTransport、WebAssembly 为技术核心的时代。这个时代我们将完全掌控网络传输、编解码、渲染等所有环节;将极大的拓宽 Web 上的使用场景。
当前面临的问题
正如前面所说,Web 的下一代音视频技术将是一个可以大显拳脚的方向。网络传输方面,WebTransport 可以支撑可靠性场景和实时性场景;音视频处理方面不仅可以使用 WebCodecs 提供的硬件编解码来高效和灵活的处理数据,还可以通过 WebAssembly 将现有的大量 C/C++ 算法编译成 Wasm 带到 Web 上使用;渲染上有 WebGPU 提供更加高效的视频渲染,还可以处理各种复杂的前处理和后处理需求。明明这一切的基础目前在浏览器上都已经支持,但似乎现在还未见这方面有什么出名的产品落地,开源社区也只见零星几个使用其中一两个技术的项目。
出现这样的现象宏观上看是因为虽然这些前置技术都有了,但它们就像一座座孤岛,还未有一张网络将他们连接起来成为整体;或许大家都还在探索,但到目前为止我还没看到比较好的解决方案。而阻碍这张网络的拦路石我认为主要有两个:
-
最大的阻碍是 Web 上目前非常鸡肋的多线程方案。我们知道音视频场景往往是计算密集型场景,这是需要多线程来支撑的。而目前 Web 上的多线程编程更接近于多进程模型,线程间通信是一定会面临的瓶颈。想象一下这样的场景:你有一个 4k 的视频需要解码播放,你的解码器是从 FFmpeg 编译出的 Wasm 模块;正常的一个思路是将解码和渲染单独一个线程执行,但由于 Web 上多线程的通信模型,你需要将每帧高达 14M 的视频帧数据(60fps 的视频就是每秒 840M)从解码线程拷贝到渲染线程,这是一个很严重的开销;那只能将解码和渲染都放到一个线程,但这样能处理的帧率就严重降低了。
-
其次是 Wasm 模块和 JavaScript 模块之间的通信问题。我们知道当前的 Wasm 和 JavaScript 之间只能互相传递 number 类型的数据,更复杂的数据结构往往需要做序列化和反序列化,这大大影响了程序的性能,可以说是目前 Wasm 已经出现好些年但在 web 平台上还未流行使用的一个重要原因。你在互联网上随手一搜就发现大家都在吐槽这个通信开销。还是上面的 4k 解码场景,由于解码器是 Wasm 模块,那解码出的帧数据必定在一个比较复杂的 C 代码的 struct 结构中,如何把这个复杂的数据结构拿给 JavaScript 模块去读写使用呢,对这个结构体进行序列化似乎是一个解决方案,但它同样是一个不可忽视的瓶颈。
如何解决
既然核心问题也不多,就两个;那我们把它们都解决不就行了,但没这么容易。我们来分析一下有哪些可行的方案。
首先大家想到的可能就是 Web Worker 的通信可以是 transfer 的,比如 ArrayBuffer;是否可以通过转移所有权的方式来避免做数据拷贝呢。其实这种方式只在某些场景下可行,只有这个数据是自己独立的比如单独的一个 ArrayBuffer 才能适用;而如果这个数据它是 Wasm 模块的 Memory 中的一部分;这你就无法转移了,转移了那 Wasm 模块就不能继续工作了。其次是我这个数据若要在两个线程都要进行读写,转移只能让一个线程能持有这个数据。
既然要在两个线程中同时使用某个数据,那可不可以直接用 SharedArrayBuffer 呢。SharedArrayBuffer 能让多个 Worker 同时持有,同时读写,并且可以使用 Atomic 来实现线程同步;Atomic 有原子操作,有线程挂起和唤醒,就可以实现锁、条件变量、信号量这些线程同步方案。这似乎是一个可行的方向,但它还面临一些问题,我们接着来分析。
使用 SharedArrayBuffer 要面临的首要问题是如何在其上面处理数据结构。简单说就是数据它的具体表现形式在 JavaScript 中我们用 Object 来表示。我们的 Object 它的内容不一样就代表不同的数据结构;但 SharedArrayBuffer 仅仅表示一段内存,无法像操作 Object 那样简单的去进行读写操作。我们需要数据结构按照一定的布局规则映射到 SharedArrayBuffer 上,当我们对数据结构的属性进行操作时就翻译成操作该属性在 SharedArrayBuffer 上的偏移来操作这块内存,这其实也是 C 语言编译成汇编的过程。想到这其实思路已经出来了。
接下来不做过多分析了,我直接总结我的思路如下:
- 实现一个全局的内存分配器 Allocator,它可以分配出给定大小的内存并返回起始地址,可以回收已被分配的内存,这个全局的内存在 WebAssembly.Memory 中,称其为 Heap;而分配出的地址我们在 ts 中使用一个指针类型来标记,它表示一个数据结构在 SharedArrayBuffer 上的布局起始位置。
- 使用 typescript transformer api 编写一个插件来在编译期间对 ts 代码中数据结构做内存布局以及将指针访问编译成函数调用,实现对数据结构属性的读写操作。
- 创建 worker 时将当前线程的全局 Heap 传递到创建的 worker 中,并初始化相关配置得到多线程数据共享的环境。这要求 Allocator 是要线程安全的。各个线程分配内存由各自线程的 Allocator 负责,它们都在同一块内存上进行分配。
如此一来当我们要在不同的线程中传递数据时传递指针,既避免了数据拷贝开销,也能在多个线程中保留对数据的引用。
有了上面的方案那实现 Wasm 模块和 JavaScript 模块之间高效的通信也实现了;首先我们的 Wasm 模块来自于 C/C++,那我们让上面的内存布局规则和 C/C++ 保持一致,则双方之间的指针可以共用。我可以将在 JavaScript 中申请的结构体指针传到 Wasm 中直接使用,也可以将 Wasm 中申请的结构体指针拿给 JavaScript 去读写,直接规避掉复杂数据的序列化和反序列化开销。但这样做的前提是 Wasm 模块的内存被我们上面的 Heap 托管了。也就是所有 Wasm 模块使用动态链接库的模式来编译,内部要分配内存时使用上面 1 中实现的 Allocator 来分配,包括 Data 段的内存也需要通过 1 中的 Allocator 分配之后动态导入;emscripten 工具有固定的配置来开启这种编译。这样我们发现还顺带解决了另一个问题,可以让多个不同的 Wasm 模块之间实现数据共享;由此我们可以将 Wasm 划分为一个个的模块,这样我们可以将 Wasm 内部的功能进行模块化提升加载速度和工程效率。
如此是不是算是找到了一条路将这些一座座的孤岛连接起来了呢。
Wasm 在 Web 上的角色问题
最后我们来思考一下 Wasm 在 Web 开发中应该处于什么样的角色定位呢,也就是 Wasm 和 JavaScript 的主次问题,简单说就是谁来占据主导,谁调谁的问题。
相信大家在互联网上看到了大量的文章都在说 Wasm 比 JavaScript 如何如何快,把某某功能使用 Wasm 又提高了多少性能。这好像给我们一种只要把更多的模块搬进 Wasm,就能不断的提高整个项目的性能,但事实真是如此吗?之前也有一些团队尝试只使用 Wasm 在 Web 上构建自己的项目,JavaScript 只充当胶水的作用,但似乎都不太理想甚至可以说是失败了。有一个项目 ffmpeg.wasm 将 ffmpeg 整个编译到 Web 平台上,大家可以去搜搜用用,为了兼容 ffmpeg 的同步 IO,在处理文件之前需要将文件数据整个写到内存中,稍微大一点的文件就 OOM 了。问题的根源我认为在于将 ffmpeg 中的同步 IO 放到 Web 上是不合适的,我们知道 Web 上的 IO 都是异步的,将 C 的同步 IO 放到 Wasm 中运行在 Web 上有很大的缺陷,会导致会多问题没法很好的解决。
究其原因我认为是对 Wasm 的角色定位错误。在 Web 上 Wasm 由于既不能进行系统调用,也不能操作 DOM,只是一个拥有普遍较快运行指令的纯计算模块,这也是它诞生之初的目的。只是它出现之后人们发现可以在 Web 之外做更多的事情,但由于 Web 平台的特殊性,它只能有补充 JavaScript 在密集计算场景下不足的功能。所以我认为在 Web 平台上应以 JavaScript 为主,Wasm 为次,为辅。具体就是把 IO 和业务逻辑放在 JavaScript 中,把密集计算的逻辑放到 Wasm 中。以音视频为例,我们应该把数据的网络处理、音视频的解封装和封装处理、音视频的渲染放在 JavaScript 中,只把音视频的编解码放到 Wasm 模块之中。
总结 Wasm 目前在 Web 上充当的角色有两个;一是对 JavaScript 密集计算场景的补充;二是将目前现有的大量 C/C++ 实现的算法移植到 Web 平台,而这些算法往往是不适合使用 JavaScript 来编写的。
以上就是我对未来 Web 上音视频的技术发展的一些思考。目前我也将这些想法思考实现到了自己的开源项目中,希望能给大家一些这方面的启发和帮助。后面有时间再写写和开源项目相关的文章,介绍一下实现的细节。
结语
近几年我总感觉到前端的发展进入瓶颈期了,大家都在卷一些同质化的项目。但其实从上面分析得到的方法也可以在其他应用场景下使用,可以将 Web 应用做得和原生应用一样的的高效,将来是否可以将使用原生开发的生产力软件使用 Web 技术栈实现;依托 Web 的优势,补足 Web 的劣势来抢夺这部分市场也不是没有可能。至少从我目前的实践来看,完全可以做到在 Web 上使用软解播放 4k 视频的效率和原生相当。