# 视频帧 · VideoFrame 与像素缓冲

> 一帧视频在内存里是什么样？这一章讲清 VideoFrame 的结构、I420（YUV）像素格式为什么是视频的通用货币、VideoFrameBuffer 的引用计数与零拷贝，以及帧上的时间戳、旋转等元数据。

- 作者：David（道雾轩）
- 专栏：音视频与 WebRTC 详解（https://daiw.org/manual/webrtc.md）
- 最后更新：2026-07-14
- 原文：https://daiw.org/manual/webrtc/video-frame
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 视频帧 · VideoFrame 与像素缓冲

视频管线传的是**帧**。在深入[编码](https://daiw.org/manual/webrtc/video-encoder-abstraction)、[传输](https://daiw.org/manual/webrtc/video-packetization)之前，先看清「一帧视频在 WebRTC 内存里到底长什么样」——`VideoFrame`（`api/video/video_frame.h`）。

## VideoFrame：像素 + 元数据

一个 `VideoFrame` = 一块**像素缓冲** + 一组**元数据**：

- **像素缓冲（`VideoFrameBuffer`）** —— 真正的图像数据，几十万到几百万个像素。
- **元数据** —— 宽高、**时间戳**（[决定播放时刻与同步](https://daiw.org/manual/webrtc/video-timing)）、**旋转角度**（手机横竖屏）、以及颜色空间等。

它是采集、[自适应](https://daiw.org/manual/webrtc/video-adaptation)、[编码](https://daiw.org/manual/webrtc/video-encoder-abstraction)之间流转的**通用容器**——摄像头产出它，[编码器](https://daiw.org/manual/webrtc/video-encoder-abstraction)消费它，解码后又产出它交给[渲染](https://daiw.org/manual/webrtc/video-decode-recovery)。

## I420：视频的通用货币

像素怎么存？WebRTC 内部的「标准格式」是 **I420**（一种 YUV 4:2:0），这是视频世界的通用货币：

<Callout type="info">
  **I420 把图像存成「亮度 Y + 两个色度 U/V」三个平面，且色度**降采样**——这利用了人眼「对亮度敏感、对颜色迟钝」的生理特点省一半数据。** 具体：Y（亮度）每个像素一个值（全分辨率），U、V（色度）每 2×2 个像素才共享一个值（四分之一分辨率）。于是每像素平均 **1.5 字节**（而非 RGB 的 3 字节），**天然省一半**，还几乎看不出差别。

  为什么统一用 I420？因为**编解码器几乎都吃 YUV**（编码前要从摄像头的各种格式转成 YUV），而 I420 是最通用的一种。WebRTC 把它定为内部标准，各环节就有了共同语言：摄像头的 NV12、RGB 等格式先转成 I420，再进[编码器](https://daiw.org/manual/webrtc/video-encoder-abstraction)；解码出来也是 I420，再转成显示要的格式。**一个统一的中间格式，把「五花八门的输入 / 输出」和「编解码器」解耦了**——又是 WebRTC 常见的[抽象解耦](https://daiw.org/manual/webrtc/source-map)手法。`VideoFrameBuffer` 也支持原生纹理（GPU 上的帧）等其他形态，按需转换。
</Callout>

## 引用计数与零拷贝

视频帧很大（一帧 1080p 的 I420 约 3 MB），**到处拷贝会要命**。所以 `VideoFrameBuffer` 是**引用计数**的（[`scoped_refptr`](https://daiw.org/manual/webrtc/factory-and-context)）：

<Callout type="info">
  **一帧的像素数据只存一份，多个环节通过引用计数**共享**它，而不是各拷一份——这是视频管线性能的关键。** 想象一帧图像同时要给[编码器](https://daiw.org/manual/webrtc/video-encoder-abstraction)、要给本地预览、要给[统计](https://daiw.org/manual/webrtc/stats-collector)，如果每处都 memcpy 一份 3 MB，一秒 30 帧就是每秒几百 MB 的无谓拷贝，CPU 和内存带宽都扛不住。引用计数让它们**共享同一块缓冲**：传递帧就是传递一个智能指针（几字节），真正的像素纹丝不动。只有当某个环节要**修改**帧时，才按需拷贝（copy-on-write 思想）。**大对象共享、避免拷贝**，是所有高性能媒体系统的铁律，WebRTC 用[引用计数](https://daiw.org/manual/webrtc/factory-and-context)把它落地。这也是为什么[像素缓冲和帧元数据分开](https://daiw.org/manual/webrtc/video-frame)——元数据轻，可以随意拷；像素重，必须共享。
</Callout>

## 时间戳：一帧的身份证

帧上的**时间戳**特别重要，贯穿后续多章：

- 采集时打上，标识「这帧是**何时采的**」。
- [编码](https://daiw.org/manual/webrtc/video-encoder-abstraction)、[打包](https://daiw.org/manual/webrtc/video-packetization)时转成 [RTP 时间戳](https://daiw.org/manual/webrtc/rtp-packet)带在包里。
- 接收端靠它[排序、算渲染时刻](https://daiw.org/manual/webrtc/video-timing)、以及和音频[对齐同步](https://daiw.org/manual/webrtc/rtcp-reports)。

一帧从采集到渲染，时间戳是它一路的**身份证**——[音视频同步](https://daiw.org/manual/webrtc/rtcp-reports)、[抖动缓冲](https://daiw.org/manual/webrtc/video-jitter-buffer)、[渲染时机](https://daiw.org/manual/webrtc/video-timing)全靠它。

## 小结与下一章

**`VideoFrame`** = **[I420 像素缓冲](https://daiw.org/manual/webrtc/video-frame)** + **元数据**（宽高、[时间戳](https://daiw.org/manual/webrtc/video-timing)、旋转）。I420（YUV 4:2:0）是视频**通用货币**——利用人眼特点把色度降采样，每像素 1.5 字节省一半，且是编解码器的通用输入。`VideoFrameBuffer` **引用计数共享**，避免大帧到处拷贝（零拷贝、copy-on-write），是性能关键。**时间戳**是帧从采集到[渲染](https://daiw.org/manual/webrtc/video-timing)一路的身份证。

帧从摄像头来，怎么喂给编码器？这中间还有个 source→sink 的连接、以及第一道自适应。下一章看采集到编码器的入口。👉 [采集与编码器输入](https://daiw.org/manual/webrtc/video-source-encoder)。

## 参考资料

- [1] libwebrtc 源码.`api/video/video_frame.h`、`api/video/video_frame_buffer.h`、`api/video/i420_buffer.h`。
- [2] libwebrtc 源码.`common_video/`（帧处理工具）。
- [3] YUV / I420 格式 —— 视频编码的基础表示。
