本文是 RenderingNG 系列文章的第四篇:
-
[译] Chromium 下一代渲染架构(四):VideoNG
我是 Dale Curtis,Chromium 媒体播放的工程主管。我的团队负责 Web 视频播放的相关 API,例如 MSE 和 WebCodecs,也包括在各平台下实现解复用(从音视频信号源中分流出不同的音频和视频流)、解码(将数据包变成音频和视频)和音视频渲染。
在本文中,我将带你了解 Chromium 的音视频渲染架构。有关可扩展性的一些细节可能是 Chromium 特有的,但这里讨论的大多数概念和设计都适用于其他音视频渲染引擎,甚至包括一些 Native 的视频播放应用。
多年来,Chromium 的视频播放架构发生了很大的变化。虽然我们没有从本系列第一篇文章中描述的成功金字塔的想法开始,但我们最终遵循了类似的步骤:可靠性、性能,然后是可扩展性。
刚开始的时候,视频渲染非常简单 —— 只是一个 for 循环,选择将哪些解码后的视频帧发送到合成器。多年来,这已经足够可靠了,但随着 Web 复杂度的增加,对更高性能和效率的需求导致架构发生变化。许多改进需要特定于操作系统的底层来实现;因此,我们的架构也必须变得更具可扩展性才能覆盖 Chromium 的所有平台。
视频渲染可以分为两个步骤:选择要呈现的内容和有效地呈现该信息。为了便于阅读,在深入探讨 Chromium 如何选择呈现内容之前,我将先介绍如何高效呈现内容。
术语和整体架构
由于本文侧重于渲染,因此我将仅简要介绍流水线的解复用和解码方面。
关键术语
解复用是媒体流水线将字节流转换为单独的编码音频和视频数据包的过程。
关键术语
解码是将这些数据包转换为原始音频和视频帧的过程。在媒体播放的上下文中,渲染是选择及时呈现那些解码的音频和视频帧的位置。
在我们现在具有安全意识的世界中,解码和解复用需要相当小心。二进制解析器有安全隐患,是安全问题的重要目标,而媒体播放又充满了二进制解析。因此,媒体解析器中的安全问题非常普遍。
Chromium 实施了深度防御,以降低用户面临安全问题的风险。实际上,这意味着解复用和软件解码总是发生在低权限进程中,而硬件解码发生在具有与 GPU 对话的权限的进程中。
Chromium 的跨进程通信机制称为 Mojo。虽然我们不会在本文中详细介绍 Mojo,但作为进程之间的抽象层,它是 Chromium 可扩展媒体流水线的基石。当我们了解播放流水线时,意识到 Mojo 的存在很重要,因为它告诉我们跨进程组件之间是通过复杂编排的交互,来完成接收、解复用、解码和最终显示的工作。
非常多的比特(So many bits)
想要了解现在的视频渲染流水线就需要先了解视频为何如此特殊:因为内存带宽。如果以每秒 60 帧的速度播放 3840 x 2160 (4K) 分辨率的视频,需要使用 9 - 12 Gb/秒的内存带宽。尽管现在的系统内存带宽可能有每秒数百 Gb 的峰值,但视频播放仍然在其中占了很大一部分。如果不小心处理,由于 GPU 和 CPU 内存之间的复制和往返,总带宽很容易成倍的增加。
任何追求效率的视频播放引擎的目标都是最小化解码器和最终渲染步骤之间的内存带宽消耗。所以,视频渲染在很大程度上与 Chromium 的主要渲染流水线解耦。具体来说,从网页渲染流水线的角度来看,视频只是一个不透明的固定大小的洞。Chromium 使用表面(Surface)的概念来实现这一点 —— 每个视频都直接与 Viz 对话。
由于移动计算的普及,功耗和效率已经成为重要关注点。带来的结果是,解码和渲染在硬件层面比以往任何时候都更加耦合 —— 导致视频看起来就像一个不透明的洞,甚至对操作系统本身也是如此!平台级解码器通常只提供一个 opaque 缓冲区,Chromium 直接以层叠(overlay)的形式传递给平台级的合成系统。
每个平台都有自己的层叠格式,对应平台的解码 API 可以与之协同工作。Windows 有 Direct Composition 和 Media Foundation Transforms,macOS 有 CoreAnimation Layers 和 VideoToolbox,Android 有 SurfaceView 和 MediaCodec,Linux 有 VASurfaces 和 VA-API。Chromium 对这些概念的抽象分别由OverlayProcessor 和 mojo::VideoDecoder 接口处理。
在某些情况下,这些缓冲区可以映射到系统内存中,因此它们甚至不需要是 opaque 的,而且在被访问之前不会消耗任何内存带宽 —— Chromium 将这些称为 GpuMemoryBuffers。在 Windows 上,这些由 DXGI 缓冲区 支持。macOS 有 IOSurfaces、Android 有 AHardwareBuffers、Linux 有 DMA 缓冲区。虽然视频播放通常不需要这种访问,但这些缓冲区对于视频捕获很重要,以确保在捕获设备和最终编码器之间最小化内存带宽的使用。
由于 GPU 通常同时负责解码和显示,使用这些 opaque 缓冲区可确保高内存带宽的视频数据永远不会真正离开 GPU。正如我们之前所讨论的,将数据保存在 GPU 上对于提高效率非常重要。特别是在高分辨率和高帧率下。
我们越能利用操作系统的底层能力(如层叠和 GPU 缓冲区),花在不必要地移动视频字节上的内存带宽消耗就越少。将所有内容从解码一直到渲染都放在一个地方可以带来令人难以置信的功耗效率提升。例如,当在 macOS 上启用 Chromium 层叠时,全屏视频播放期间的功耗减半!在 Windows、Android 和 ChromeOS 等其他平台上,我们甚至可以在非全屏情况下使用层叠,几乎在任何地方都可以节省高达 50% 的功耗。
渲染
我们已经介绍了 Chromium 对呈现效率的优化,现在我们可以讨论 Chromium 如何选择呈现什么。Chromium 的播放堆栈使用基于“拉”的架构,这意味着堆栈中的每个组件都以分层顺序从其下一个组件请求输入。堆栈的顶部是音频和视频帧的渲染,接下来是解码,然后是解复用,最后是 I/O。每个渲染的音频帧都会推进一个时钟片段,当与呈现间隔结合时,该时钟用于选择要渲染的视频帧。
在每个显示间隔(就是显示的每次刷新)上,视频渲染器都需要通过 SurfaceLayer 的 CompositorFrameSink 来提供一个视频帧。对于帧率小于显示率的内容,这意味着多次显示同一帧,而如果帧率大于显示率,则某些帧永远不会被显示。
同步音频和视频来改善用户体验的工作还有很多。有关如何在 Chromium 中实现最佳视频平滑度的详细讨论,请参阅 Project Butter。它解释了如何将视频渲染分解为每帧应显示多少次的一个序列。例如:
- 每 1 个“显示间隔” 1 帧([1],60 fps,60 Hz)
- 每 2 个“显示间隔” 1 帧([2],30 fps,60 Hz)
- 或更复杂的模式,如 [2:3:2:3:2](25 fps,60 Hz),多个帧对应多个显示间隔
视频渲染器越接近这种理想模式,用户就越有可能将播放视为流畅。
虽然大多数 Chromium 平台逐帧渲染,但并非所有平台都这样做,我们的可扩展架构也允许批量渲染。批处理渲染是一种提高效率的技术,操作系统级的合成器提前拿到多个帧并处理,再根据应用程序提供的时间安排来展示这些帧。
未来已来?
我们专注于 Chromium 如何利用操作系统底层能力来提供一流的播放体验。但是那些想要超越简单视频播放的网站呢?我们能否为他们提供 Chromium 正在使用的这种强大的底层能力来引入下一代 Web 内容?
我们认为答案是肯定的!可扩展性是 Web 平台的核心。我们一直在与其他浏览器和开发人员合作,开发 WebGPU 和 WebCodecs 等新技术,以便 Web 开发人员可以通过 Chromium 来使用操作系统的底层能力。WebGPU 带来了对 GPU 缓冲区的支持,而 WebCodecs 带来了平台的解码和编码底层能力,与层叠和 GPU 缓冲区兼容。
结束
谢谢阅读!我希望你更好地了解了现代视频播放系统以及 Chromium 如何为每天数亿小时的视频观看赋能。如果你正在寻找有关编解码器和现代网络视频的更多阅读资料,我推荐 Sid Bala 的 H.264 is magic、Erica Beaves 的 How Modern Video Players Work 以及 Cyril Concolato 的 Packaging award-winning shows with award-winning technology。