返回文章列表

第十三章|基础专题一:像素、颜色与原始帧

从像素、颜色分量、位深、RGB/RGBA、YUV420P、NV12、stride、颜色参数和 VideoFrame 生命周期入手,建立未压缩视频帧的底层心智模型。

第十三章|基础专题一:像素、颜色与原始帧

原稿学习节奏:第 1 周。

一、本周学习目标

完成本周学习后,你应当能够:

  1. 区分像素、颜色分量、采样点、平面和帧。
  2. 区分分辨率、帧率、码率、位深和每像素位数。
  3. 画出 RGB24RGBAI420/YUV420PNV12 的内存布局。
  4. 解释为什么数字视频领域常说的“YUV”通常实际是 Y′CbCr
  5. 解释 4:4:4、4:2:2、4:2:0 的采样数量和画质差异。
  6. 根据宽高、像素格式、stride 和帧率计算内存及带宽。
  7. 区分 primaries、transfer、matrix、range 四类颜色参数。
  8. 排查视频偏色、画面发灰、黑位压死和透明边缘黑边。
  9. 在 WebCodecs 中正确读取 VideoFrame 信息并及时释放资源。

二、本周安排

时间学习内容产出
第 1 天,1 小时像素、分辨率、帧率、码率、位深完成原始数据量计算
第 2 天,1.5 小时RGB、RGBA、Alpha能解释 straight 与 premultiplied alpha
第 3 天,2 小时Y′CbCr、亮度与色度手算一次 RGB → Y′CbCr
第 4 天,1.5 小时4:4:4、4:2:2、4:2:0画出 2×2 像素的采样关系
第 5 天,1.5 小时Packed、Planar、NV12、I420能根据地址读取任意像素
第 6 天,1.5 小时stride、alignment、颜色管理完成偏色排查表
第 7 天,1 小时WebCodecs 实验与面试复习完成练习和自测题

三、概念地图

本周只研究未压缩媒体数据

相机 / 图片 / AI 模型 / Canvas

       原始像素帧
 RGB / RGBA / I420 / NV12

          视频编码器
      H.264 / H.265 / AV1

          基础码流

         MP4 / WebM

必须建立以下边界:

层次示例本周是否学习
原始像素RGBA、NV12、I420
视频编码标准H.264、H.265
编码器实现x264、x265
基础码流H.264 NALU
容器MP4、WebM
浏览器原始帧接口VideoFrame
浏览器播放接口<video>、MSE只建立边界

原始帧不是图片文件,也不是视频码流。

一块 1920×1080 的 RGBA 内存没有文件头,不会自行告诉你:

  • 宽度和高度;
  • 每行有多少字节;
  • 通道顺序;
  • 是否有 padding;
  • 使用什么颜色空间;
  • Alpha 是否预乘。

这些信息必须由调用者另行保存。


四、像素、采样点与颜色分量

4.1 像素不等于一个字节

像素是图像空间中的一个位置。一个像素可能由多个颜色分量描述:

RGB 像素  = R + G + B
RGBA 像素 = R + G + B + A

8-bit RGBA 中每个分量占一个字节,所以一个像素通常占 4 字节。

但在 Y′CbCr 4:2:0 中:

  • 每个像素都有自己的 Y′ 采样;
  • 多个像素共享 Cb、Cr 采样;
  • 因此不能简单认为每个像素都连续保存着 Y、Cb、Cr 三个字节。

更准确的术语是:

  • Pixel:最终图像中的像素位置。
  • Component:R、G、B、Y、Cb、Cr、A 等颜色分量。
  • Sample:某个分量在某个位置的离散采样值。
  • Plane:保存一个或一组分量的二维内存区域。

4.2 分辨率的三种含义

日常说“1920×1080”,通常指可见画面大小,但底层系统可能同时存在:

  1. Coded size:实际分配或编码的尺寸。
  2. Visible rectangle:真正有效的可见区域。
  3. Display size:考虑像素宽高比、旋转等因素后的显示尺寸。

例如,底层可能为硬件对齐分配 1920×1088,但只显示其中的 1920×1080。

WebCodecs 的 VideoFrame 因此分别提供:

codedWidth / codedHeight
visibleRect
displayWidth / displayHeight

不能默认这三组尺寸始终相等。(W3C)


4.3 帧率与码率不是一回事

帧率 Frame Rate 表示一秒有多少帧:

24 FPS
25 FPS
30 FPS
30000/1001 FPS ≈ 29.97 FPS
60 FPS

码率 Bitrate 表示每秒传输或存储多少比特:

8 Mbit/s
20 Mbit/s

原始视频也可以计算“原始数据率”:

[ \text{Raw data rate}

\text{bytes per frame} \times \text{frames per second} ]

但日常讨论视频码率时,通常指压缩后的码率。


4.4 位深不等于每像素位数

位深通常表示每个颜色分量使用多少有效位:

  • 8-bit RGB:每个 R、G、B 分量 8 bit,总计 24 bit/pixel。
  • 8-bit RGBA:总计 32 bit/pixel。
  • 8-bit Y′CbCr 4:2:0:平均 12 bit/pixel。
  • 10-bit Y′CbCr 4:2:0:有效信息平均 15 bit/pixel。

还要区分:

有效位深

内存容器宽度

10-bit 数据经常存储在 16-bit 整数中。此时每个采样虽然只有 10 个有效位,却可能实际占用 2 字节。


五、RGB、RGBA 与 Alpha

5.1 RGB24

RGB24 通常表示一个像素连续保存 3 个 8-bit 分量:

地址递增 →

R0 G0 B0 | R1 G1 B1 | R2 G2 B2 | ...

紧密排列时:

[ S_{\text{RGB24}} = W \times H \times 3 ]

其中:

  • (W):宽度;
  • (H):高度;
  • (S):字节数。

5.2 RGBA

8-bit RGBA 的字节布局为:

R0 G0 B0 A0 | R1 G1 B1 A1 | ...

紧密排列时:

[ S_{\text{RGBA}} = W \times H \times 4 ]

注意还存在:

BGRA
ARGB
ABGR
RGBX
BGRX

X 通常代表占位字节,图像被视为完全不透明。

WebCodecs 明确定义了 RGBABGRARGBXBGRX 等格式的分量顺序和每像素字节数。(W3C)


5.3 Alpha 表示什么

Alpha 通常表示覆盖率或不透明度:

A = 0       完全透明
A = 1       完全不透明
A = 0.5     半透明

8-bit Alpha 使用 0~255 表示:

0   → 完全透明
255 → 完全不透明

5.4 Straight Alpha

Straight Alpha,也叫 unassociated alpha:

RGB 保存物体本身的颜色
A   单独保存覆盖率

例如半透明红色:

[ (R,G,B,A)=(1,0,0,0.25) ]

使用 source-over 合成时:

[ C_o=C_s\alpha_s+C_d(1-\alpha_s) ]

其中:

  • (C_s):前景颜色;
  • (C_d):背景颜色;
  • (\alpha_s):前景 Alpha;
  • (C_o):输出颜色。

5.5 Premultiplied Alpha

Premultiplied Alpha,即预乘 Alpha,先把 RGB 乘以 Alpha:

[ \widetilde C_s=C_s\alpha_s ]

刚才的半透明红色会存为:

[ (0.25,0,0,0.25) ]

合成公式变成:

[ \widetilde C_o

\widetilde C_s + \widetilde C_d(1-\alpha_s) ]

预乘表示通常更适合插值、缩放、模糊和图层合成;W3C 的颜色插值规则也采用先预乘、插值后再解除预乘的处理方式。(W3C)

常见错误

把预乘数据当成 straight 数据:

RGB 已经乘过一次 Alpha
渲染时又乘一次
→ 半透明边缘变黑

把 straight 数据当成预乘数据:

RGB 没乘 Alpha
渲染器却直接参与合成
→ 边缘过亮或出现白边

在线编辑器内部最好统一一种 Alpha 约定,并在素材导入、GPU 上传和导出阶段明确转换。


六、YUV 与 Y′CbCr

6.1 业界口语中的“YUV”

严格来说:

  • YUV 原本用于模拟视频系统。
  • Y′CbCr 是数字视频中常见的亮度与色差信号表示。

工程交流中,人们经常把 YUV420PYUV frame 当作通用称呼,但真正讨论数字像素值时,通常指的是 Y′CbCr。

其中:

  • (Y’):由非线性的 (R’G’B’) 计算得到的 luma;
  • Cb:蓝色色差;
  • Cr:红色色差;
  • 撇号 ' 表示分量已经经过非线性传递函数处理。

Y′ 不等于物理意义上的线性亮度 Y。


6.2 为什么不直接一直使用 RGB

RGB 很适合显示和图形渲染,但视频压缩更常使用 Y′CbCr,主要因为:

  1. RGB 三通道之间存在较强相关性。
  2. 转换为亮度和色度后,更容易分别处理。
  3. 人眼通常对高频亮度细节比高频色度细节更敏感。
  4. 可以降低色度分辨率,同时保留较多主观细节。
  5. 大量视频编解码硬件和视频标准都围绕 Y′CbCr 工作。

注意:这不是说人眼“看不见颜色”,而是高频色度信息在很多自然画面中可以进行更强压缩。


6.3 RGB 到 Y′CbCr 的通用形式

令:

[ K_g=1-K_r-K_b ]

则:

[ Y’=K_rR’+K_gG’+K_bB’ ]

[ Cb=\frac{B’-Y’}{2(1-K_b)} ]

[ Cr=\frac{R’-Y’}{2(1-K_r)} ]

在归一化表示中:

R′、G′、B′ ∈ [0,1]
Y′          ∈ [0,1]
Cb、Cr      通常位于 [-0.5,0.5]

不同标准使用不同系数:

矩阵(K_r)(K_g)(K_b)常见关联
BT.601 系列0.29900.58700.1140SD 视频
BT.7090.21260.71520.0722HD 视频
BT.2020 NCL0.26270.67800.0593UHD/WCG 视频

BT.601、BT.709 和 BT.2020 分别面向不同电视制作体系;但在真实文件中,不能只根据分辨率猜测颜色矩阵,仍应读取实际元数据。(ITU)


6.4 手算示例:BT.709 纯红色

令:

[ R’=1,\quad G’=0,\quad B’=0 ]

首先计算:

[ Y’=0.2126 ]

然后:

[ Cb

\frac{0-0.2126}{2(1-0.0722)} \approx -0.1146 ]

[ Cr

\frac{1-0.2126}{2(1-0.2126)} =0.5 ]

因此纯红色会表现为:

中等偏低的 Y′
略低于中心值的 Cb
最大的正 Cr

这说明 Y′CbCr 中三个分量不能被分别理解为三个独立颜色通道。


七、色度下采样

7.1 4:4:4

每个像素都有独立的 Y′、Cb、Cr:

像素 0:Y0 Cb0 Cr0
像素 1:Y1 Cb1 Cr1
像素 2:Y2 Cb2 Cr2

8-bit 时:

[ 24\text{ bit/pixel} ]

适合:

  • 高质量中间处理;
  • 颜色键控;
  • 文字和图形素材;
  • 对色度边缘要求高的内容。

7.2 4:2:2

水平方向两个像素共享一组 Cb、Cr:

Y0 Y1 共用 Cb0 Cr0
Y2 Y3 共用 Cb1 Cr1

每两个像素保存:

2 个 Y + 1 个 Cb + 1 个 Cr

8-bit 时平均:

[ 16\text{ bit/pixel} ]


7.3 4:2:0

普通 4:2:0 中,一个 2×2 亮度区域共享一组 Cb、Cr:

Y00 Y01 ┐
Y10 Y11 ┘ 共享 Cb00、Cr00

四个像素总共保存:

4 个 Y
1 个 Cb
1 个 Cr

因此每像素平均采样数为:

[ \frac{4+1+1}{4}=1.5 ]

8-bit 时:

[ 1.5 \times 8=12\text{ bit/pixel} ]

相对于 4:4:4:

[ \frac{12}{24}=50% ]

所以 8-bit 4:2:0 的紧密原始数据量只有 4:4:4 的一半。

WebCodecs 对 I420 的定义同样规定:U、V 平面在水平和垂直方向都相对于 Y 平面缩小 2 倍。(W3C)


7.4 色度下采样的副作用

4:2:0 对自然照片和普通视频通常很有效,但容易影响:

  • 彩色小字号文字;
  • 红色或蓝色细线;
  • UI 屏幕录制;
  • 高饱和度边缘;
  • Chroma key;
  • 多次缩放和重编码。

此外,仅知道“4:2:0”还不完整。不同系统中的 chroma location/chroma siting 可能不同,即色度采样点相对于亮度像素的位置不同。处理错误时会出现颜色边缘水平或垂直错位。


八、Packed、Planar 与 Semi-planar

8.1 Packed

多个分量交错保存在同一个平面中。

RGBA:

R G B A | R G B A | R G B A

YUYV 4:2:2:

Y0 U0 Y1 V0 | Y2 U1 Y3 V1

优点是单个像素或像素组读取直观;缺点是只处理某个分量时可能不够方便。


8.2 Planar

不同分量分别保存。

I420,也常对应 FFmpeg 中的 yuv420p

Plane 0:YYYYYYYYYYYY
Plane 1:UUUUUU
Plane 2:VVVVVV

内存顺序:

Y plane

U plane

V plane

对于偶数宽高:

[ S_Y=W\times H ]

[ S_U=S_V=\frac W2\times\frac H2 ]

总大小:

[ S_{\text{I420}}

WH+\frac{WH}{4}+\frac{WH}{4}

1.5WH ]

对于奇数宽高,不能直接使用 (1.5WH),应计算:

[ S_{\text{I420}}

WH + 2 \left\lceil\frac W2\right\rceil \left\lceil\frac H2\right\rceil ]


8.3 Semi-planar

NV12 有两个平面:

Plane 0:YYYYYYYYYYYY
Plane 1:UVUVUVUVUVUV

它与 I420 的采样数量相同,但 U、V 不再分成两个独立平面。

I420:Y + U + V
NV12:Y + UV

WebCodecs 对 NV12 的定义也是一个 Y 平面加一个交错的 UV 平面。(W3C)

容易混淆的格式

格式布局
I420Y、U、V
YV12Y、V、U
NV12Y、UVUV
NV21Y、VUVU
YUYVY0 U0 Y1 V0
UYVYU0 Y0 V0 Y1

名称相似不代表内存可以直接互用。


九、原始数据量计算

以 1920×1080、8-bit、紧密排列为例:

[ W\times H=2{,}073{,}600 ]

格式字节/帧十进制 MB/帧30 FPS 数据率
RGB246,220,8006.22 MB186.62 MB/s
RGBA8,294,4008.29 MB248.83 MB/s
I4203,110,4003.11 MB93.31 MB/s
NV123,110,4003.11 MB93.31 MB/s
YUV422P4,147,2004.15 MB124.42 MB/s
YUV444P6,220,8006.22 MB186.62 MB/s

RGBA 在 30 FPS 时:

[ 8{,}294{,}400\times30

248{,}832{,}000\text{ bytes/s} ]

换成比特率:

[ 248{,}832{,}000\times8

1.99\text{ Gbit/s} ]

I420 在 30 FPS 时:

[ 3{,}110{,}400\times30

93.312\text{ MB/s} ]

这还没有计算:

  • 对齐 padding;
  • GPU 纹理;
  • 解码器参考帧;
  • JavaScript 复制缓冲区;
  • Canvas 中间表面;
  • 编码器输入队列。

所以编辑器同时缓存 60 帧 1080p RGBA,仅理论像素数据就需要:

[ 8.2944\text{ MB}\times60

497.664\text{ MB} ]

这正是浏览器编辑器不能无限缓存原始帧的原因。


十、Plane、Stride、Alignment 与 Padding

10.1 Plane

Plane 是保存二维采样数据的内存区域。

I420 有三个 plane:

Y plane
U plane
V plane

NV12 有两个:

Y plane
UV plane

RGBA 通常只有一个。


10.2 Stride

Stride,也叫 line size 或 row pitch,表示:

从当前行起始地址走到下一行起始地址,需要跨过多少字节。

它包括有效像素和行尾 padding。

像素地址的一般形式为:

[ \text{address}(x,y)

\text{planeBase} + y\times\text{stride} + x\times\text{bytesPerElement} ]

WebCodecs 将 stride 定义为每一行占用的字节数,包含 padding;每个 plane 可以拥有不同的 offset 和 stride。(W3C)


10.3 为什么需要 Padding

CPU SIMD、GPU 和硬件编解码器经常要求:

16 字节对齐
32 字节对齐
64 字节对齐
128 字节对齐

假设 RGBA 宽度为 1921:

[ 1921\times4=7684\text{ bytes} ]

若每行按 64 字节对齐:

[ \operatorname{align}(7684,64)=7744 ]

所以:

有效字节:7684
stride:  7744
padding:   60 字节/行

真实内存大小是:

[ 7744\times H ]

而不是:

[ 1921\times H\times4 ]


10.4 多平面格式的真实大小

通用公式:

[ S=\sum_i \text{stride}_i\times \text{planeHeight}_i ]

例如 I420:

[ S

\text{stride}_Y H + \text{stride}_U\left\lceil\frac H2\right\rceil + \text{stride}_V\left\lceil\frac H2\right\rceil ]

因此,width × height × 1.5 只适用于:

  • 8-bit;
  • 紧密排列;
  • 没有额外对齐;
  • 正确处理奇数尺寸;
  • 没有额外 plane;
  • CPU 可线性访问的普通 I420。

硬件表面还可能采用 tiled 或厂商私有布局,不能直接按普通线性内存解释。


十一、颜色管理的四个核心参数

仅知道像素是 yuv420p 还不足以正确显示颜色。

至少要知道:

Primaries
Transfer
Matrix
Range

还经常需要:

Chroma location

WebCodecs 的 VideoColorSpace 同样分别暴露 primariestransfermatrixfullRange。(W3C)


11.1 Primaries:原色点

Primaries 定义:

  • 红色原色在哪里;
  • 绿色原色在哪里;
  • 蓝色原色在哪里;
  • 三个原色形成多大的色域。

常见值:

BT.709 / sRGB primaries
Display P3
BT.2020

原色点错误通常表现为:

  • 整体饱和度不对;
  • 某些颜色明显偏色;
  • 广色域内容在普通画布中过饱和或失去饱和度。

11.2 Transfer:传递特性

Transfer function 描述:

非线性编码值

线性光

之所以使用非线性表示,是为了更合理地分配数字精度,并匹配显示和感知系统。

常见传递特性包括:

sRGB
BT.709
PQ
HLG
Linear

传递函数错误可能表现为:

  • 整体过暗或过亮;
  • 阴影层次异常;
  • 重复执行 gamma;
  • HDR 内容灰暗或高光完全错误。

11.3 Matrix:RGB 与 Y′CbCr 的转换矩阵

Matrix 决定:

R′G′B′

Y′CbCr

常见矩阵:

BT.601
BT.709
BT.2020 non-constant luminance

例如,把 BT.709 数据按 BT.601 矩阵转换,可能导致:

  • 肤色变化;
  • 红色、绿色和蓝色饱和度异常;
  • 整体轻微但明显的色相偏移。

11.4 Range:Full 与 Limited

8-bit limited range 的常见标称范围:

Y′:    16~235
Cb/Cr: 16~240
色度中心:128

Full range 通常使用:

0~255

FFmpeg 对 limited range 和 full range 也分别定义了对应的数字范围。(FFmpeg)

解释错误的表现

实际数据被错误解释为结果
LimitedFull黑色不够黑,白色不够白,画面发灰
FullLimited黑位和白位被压缩或裁剪,细节丢失

10-bit limited range 通常对应:

Y′:    64~940
Cb/Cr: 64~960

11.5 一个完整颜色描述

不能只写:

这是 BT.709 视频

更精确的描述应当类似:

Pixel format: I420 8-bit
Primaries:    BT.709
Transfer:     BT.709
Matrix:       BT.709
Range:        Limited
Chroma siting: Left

因为原色点、传递特性和矩阵在元数据中是相互独立的字段。


十二、偏色排查方法

现象优先检查
画面发灰、对比度低Limited 被当作 Full
黑位压死、白色溢出Full 被当作 Limited
红绿蓝色相不正确BT.601/709/2020 matrix 错误
整体太暗或太亮Transfer function、重复 gamma
广色域画面过饱和Primaries 或 gamut conversion
透明边缘发黑Premultiplied 数据被再次乘 Alpha
透明边缘发亮Straight 数据被当作 premultiplied
彩色边缘位置错开Chroma siting 或色度缩放错误
Canvas 正常但导出偏色编码器输入格式或导出元数据错误
预览与最终文件颜色不同浏览器渲染和服务端转换链路不一致

建议按照以下顺序排查:

确认输入像素格式

确认各 plane 和 stride

确认 range

确认 matrix

确认 transfer

确认 primaries

确认 Canvas / GPU 目标颜色空间

确认编码输出元数据

十三、Web 实现视角

13.1 VideoFrame 的关键属性

function inspectFrame(frame: VideoFrame): void {
  console.table({
    format: frame.format,
    codedSize: `${frame.codedWidth}x${frame.codedHeight}`,
    visibleRect: frame.visibleRect
      ? `${frame.visibleRect.x},${frame.visibleRect.y},` +
        `${frame.visibleRect.width}x${frame.visibleRect.height}`
      : "unknown",
    displaySize: `${frame.displayWidth}x${frame.displayHeight}`,
    timestamp: frame.timestamp,
    duration: frame.duration,
    primaries: frame.colorSpace.primaries,
    transfer: frame.colorSpace.transfer,
    matrix: frame.colorSpace.matrix,
    fullRange: frame.colorSpace.fullRange,
  });
}

不要只打印 displayWidth,因为真实内存通常与 coded size 和 plane layout 有关。


13.2 正确复制 VideoFrame

不要自行猜测缓冲区大小,应使用 allocationSize()

async function copyFrameToCPU(
  frame: VideoFrame,
): Promise<{ bytes: Uint8Array; layout: PlaneLayout[] }> {
  if (frame.format === null) {
    throw new Error("当前 VideoFrame 没有可复制的已知像素格式");
  }

  const size = frame.allocationSize();
  const bytes = new Uint8Array(size);
  const layout = await frame.copyTo(bytes);

  return { bytes, layout };
}

allocationSize() 返回满足指定复制选项所需的最小字节数,copyTo() 返回实际 plane 的 offset 和 stride。(W3C)


13.3 及时关闭 VideoFrame

decoder.output = (frame: VideoFrame): void => {
  try {
    canvasContext.drawImage(frame, 0, 0);
  } finally {
    frame.close();
  }
};

VideoFrame 可能引用 CPU 内存、GPU 内存或硬件媒体资源。规范建议在不再使用时立即调用 close(),以降低内存压力并促进资源复用。(W3C)

不要依赖 JavaScript 垃圾回收器及时释放媒体资源。


13.4 Canvas 不一定只是普通 8-bit RGBA

普通 ImageDatargba-unorm8 使用 RGBA 顺序,每像素 4 字节;规范还定义了 rgba-float16,每像素使用四个 16-bit 浮点分量,即 8 字节。(html.spec.whatwg.org)

rgba-unorm8:4 bytes/pixel
rgba-float16:8 bytes/pixel

浏览器编辑器不能默认所有 Canvas 或 GPU 中间表面都只占 width × height × 4


13.5 避免不必要的 CPU 转换

性能较差的链路:

硬件解码 NV12

复制到 CPU

转换 RGBA

上传 GPU

渲染

更理想的链路:

硬件解码 VideoFrame

GPU 直接采样或浏览器直接绘制

滤镜与合成

需要 CPU 读取像素时再执行 copyTo(),不要在每一帧无条件执行。


十四、实际项目中的应用

A. AI 生成视频网页

AI 模型常见输出是:

Float tensor
N × C × H × W

N × H × W × C

输出通常需要经过:

模型 RGB 浮点输出

数值范围和 transfer 确认

色域转换

RGB → Y′CbCr

4:2:0 色度下采样

转换为 NV12 / I420 / P010

视频编码

重点问题:

  1. 模型输出究竟是线性 RGB 还是类似 sRGB 的非线性 RGB。
  2. 输出值是 [0,1][-1,1] 还是其他范围。
  3. 是否需要从模型工作色域转换到 BT.709 或 BT.2020。
  4. RGB → Y′CbCr 时使用哪套 matrix。
  5. 编码器要求 NV12、I420 还是其他格式。
  6. 是否发生 GPU → CPU → GPU 的重复复制。
  7. 生成速度是否超过编码和播放速度,需要 backpressure。

缩略图可使用 RGB/JPEG,但正式连续播放不应长期传输大量独立 PNG 帧。


B. 在线 Web 视频编辑器

推荐像素处理链路:

解码器输出 VideoFrame

保留原始 Y′CbCr 或 GPU 表面

按播放窗口缓存少量帧

GPU 完成缩放、滤镜和合成

转为编码器要求的像素格式

VideoEncoder / 服务端编码

工程原则:

  • 时间线只缓存当前播放位置附近的帧。
  • 缩略图使用较低分辨率。
  • 普通预览避免把每帧复制成 RGBA 数组。
  • 透明贴纸、字幕和图层统一 Alpha 约定。
  • 合成完成后再转换为最终 Y′CbCr 格式。
  • 导出时明确写入 primaries、transfer、matrix 和 range。
  • 取消拖动前产生的旧解码任务。
  • 每个不再使用的 VideoFrame 都应关闭。

十五、FFmpeg 小实验

15.1 查看颜色和像素格式

ffprobe -v error \
  -select_streams v:0 \
  -show_entries \
stream=width,height,pix_fmt,color_range,color_space,color_transfer,color_primaries \
  -of default=noprint_wrappers=1 \
  input.mp4

重点观察:

pix_fmt
color_range
color_space
color_transfer
color_primaries

15.2 导出原始 I420

ffmpeg -i input.mp4 \
  -pix_fmt yuv420p \
  -f rawvideo \
  output.yuv

播放时必须重新提供格式和尺寸:

ffplay \
  -f rawvideo \
  -pixel_format yuv420p \
  -video_size 1920x1080 \
  -framerate 30 \
  output.yuv

原始视频没有容器头,所以读取时必须明确给出像素格式、尺寸和帧率;FFmpeg 的 rawvideo 文档也明确要求调用方提供这些信息。(FFmpeg)


15.3 导出 RGBA

ffmpeg -i input.mp4 \
  -pix_fmt rgba \
  -f rawvideo \
  output.rgba
ffplay \
  -f rawvideo \
  -pixel_format rgba \
  -video_size 1920x1080 \
  -framerate 30 \
  output.rgba

对比两个文件大小,应接近:

RGBA / I420
= 4 / 1.5
≈ 2.67

十六、易错点

  1. 像素不等于字节。
  2. 位深不等于每像素总位数。
  3. YUV420P 不是每个像素保存 Y、U、V 三个字节。
  4. 数字视频中的“YUV”通常实际是 Y′CbCr。
  5. Y′ 不是线性物理亮度。
  6. RGB24 的 24 表示每像素 24 bit,不是每通道 24 bit。
  7. I420、YV12、NV12、NV21 不能直接互换。
  8. 4:2:0 只描述采样数量还不够,还可能涉及 chroma siting。
  9. width × height × 4 只适用于紧密排列的 8-bit RGBA。
  10. stride 的单位通常是字节,不是像素。
  11. coded size 不一定等于 visible size。
  12. BT.709 不只是一个 RGB → Y′CbCr 矩阵名称。
  13. BT.2020 不自动等于 HDR。
  14. sRGB 与 BT.709 虽然原色点相同,但不能笼统视为完全相同。
  15. Limited range 数据不是“动态范围比较差”的错误数据。
  16. Alpha 约定不一致会造成透明边缘黑边或白边。
  17. VideoFrame 被 JavaScript 引用并不意味着只占少量 JS 堆内存。
  18. copyTo() 可能产生昂贵的格式转换和内存复制。
  19. Canvas 预览正确不代表编码导出元数据正确。
  20. 原始帧体积与压缩后视频文件大小不是一个量级。

十七、面试题

1. 为什么视频通常不直接使用 RGB 编码?

标准回答:

RGB 适合显示,但三个通道存在相关性。转换到 Y′CbCr 后,可以分离亮度和色度,并对色度进行 4:2:2 或 4:2:0 下采样,从而减少输入数据量,同时较好地保持主观画质。

追问:

是不是所有视频编码器内部都必须使用 Y′CbCr?

高级回答:

不是绝对必须,具体标准和 profile 可能支持 RGB 或 4:4:4,但面向消费级视频的主流硬件链路通常围绕 4:2:0 Y′CbCr 优化。


2. 1920×1080 YUV420P 一帧有多大?

偶数宽高、8-bit、紧密排列时:

[ 1920\times1080\times1.5

3{,}110{,}400\text{ bytes} ]

约 3.11 MB。


3. 为什么乘以 1.5?

四个像素包含:

4 个 Y
1 个 U
1 个 V

平均每像素:

[ 6/4=1.5\text{ bytes} ]


4. I420 和 NV12 有什么区别?

两者都是 8-bit 4:2:0:

I420:Y + U + V,三个 plane
NV12:Y + UV,两个 plane

采样数量相同,内存组织不同。


5. stride 为什么可能大于 width × bytesPerPixel?

因为每行可能为了 SIMD、GPU 或硬件要求进行对齐,行尾存在 padding。


6. Full range 和 Limited range 错误会产生什么结果?

Limited 当 Full:画面发灰。

Full 当 Limited:黑白细节被压缩或裁剪。


7. Primaries、Transfer、Matrix 分别表示什么?

Primaries:定义色域
Transfer:定义信号值与线性光的关系
Matrix:定义 RGB 与 Y′CbCr 的转换

8. 为什么透明 PNG 缩放后可能出现黑边?

常见原因是 straight 与 premultiplied alpha 淌用,或在错误的 Alpha 表示下执行缩放、模糊和滤波。


9. 为什么 width × height × 4 不一定是真实占用?

因为还可能存在:

  • stride padding;
  • coded size padding;
  • GPU 纹理对齐;
  • 多缓冲;
  • 硬件私有表面; -额外的 CPU 复制和中间表面。

10. 为什么需要调用 VideoFrame.close()

因为 VideoFrame 可能引用较大的 CPU、GPU 或硬件编解码资源。关闭它可以立即释放当前对象对媒体资源的引用,而不是等待不确定的垃圾回收。


十八、练习

练习 1:720p I420

计算 1280×720、8-bit I420 在 25 FPS 时的原始数据率。

答案

[ 1280\times720\times1.5

1{,}382{,}400\text{ bytes/frame} ]

[ 1{,}382{,}400\times25

34{,}560{,}000\text{ bytes/s} ]

约:

34.56 MB/s
276.48 Mbit/s

练习 2:4K 10-bit 4:2:0

假设 10-bit 采样均存储在 16-bit 容器中,计算 3840×2160、60 FPS 的内存吞吐。

每像素实际存储:

Y:2 bytes/pixel
UV:平均 1 byte/pixel
合计:3 bytes/pixel

[ 3840\times2160\times3

24{,}883{,}200\text{ bytes/frame} ]

[ 24{,}883{,}200\times60

1{,}492{,}992{,}000\text{ bytes/s} ]

约:

1.493 GB/s
11.94 Gbit/s

练习 3:奇数尺寸 I420

计算 1921×1081 的紧密 I420 大小。

[ S_Y=1921\times1081=2{,}076{,}601 ]

色度尺寸:

[ W_C=\left\lceil\frac{1921}{2}\right\rceil=961 ]

[ H_C=\left\lceil\frac{1081}{2}\right\rceil=541 ]

[ S_U+S_V=961\times541\times2=1{,}039{,}802 ]

总大小:

[ S=3{,}116{,}403\text{ bytes} ]

这说明奇数尺寸不能直接使用 (W\times H\times1.5)。


练习 4:Alpha 排查

一个像素以 straight alpha 保存为:

[ (1,0,0,0.25) ]

转换为 premultiplied alpha:

[ (0.25,0,0,0.25) ]

若把后者错误地当作 straight,再乘一次 Alpha:

[ 0.25\times0.25=0.0625 ]

红色贡献被错误降低为原来的四分之一,边缘会明显变暗。


练习 5:系统排查题

现象:

浏览器 Canvas 预览正常
服务端导出 MP4 后整体发灰

排查顺序:

  1. 用 ffprobe 查看导出文件的 color_range
  2. 检查编码器输入究竟是 full 还是 limited。
  3. 检查 RGB → Y′CbCr 转换时是否做了 range 映射。
  4. 检查编码器和容器是否写入正确的颜色元数据。
  5. 在不同播放器中交叉验证,区分文件问题与播放器问题。

十九、本周速查表

项目结论
RGB243 bytes/pixel
RGBA4 bytes/pixel
I420Y + U + V
NV12Y + UV
8-bit 4:4:424 bit/pixel
8-bit 4:2:216 bit/pixel
8-bit 4:2:012 bit/pixel
I420 偶数尺寸(1.5WH) bytes
RGBA 紧密尺寸(4WH) bytes
真实多平面尺寸(\sum stride_i \times height_i)
Limited 8-bit Y′16~235
Limited 8-bit Cb/Cr16~240
色度中心值128
Primaries色域
Transfer信号值与线性光的关系
MatrixRGB 与 Y′CbCr 转换
Range数字码值映射范围
Straight AlphaRGB 未乘 Alpha
Premultiplied AlphaRGB 已乘 Alpha
WebCodecs 资源释放frame.close()

本周通过标准

你能够独立回答以下四个问题,就可以进入第二周:

  1. 为什么 1920×1080 I420 一帧约为 3.11 MB?
  2. 为什么实际内存不能总按 width × height × bytesPerPixel 计算?
  3. 一段视频偏色时,如何区分 matrix、transfer、primaries 和 range 问题?
  4. 为什么 Web 视频编辑器应避免长期缓存大量 RGBA VideoFrame