裸 YUV 是最原始的视频:没有文件头、没有压缩、没有时间戳。 你告诉 SDL「这是 YUV420p、宽高多少」,它就能画——这一节证明 SDL 只认像素。
前三节把环境和驱动讲清了,这一节正式把一帧画出来。 这是整章最底层的一次显示:没有解码、没有同步、没有队列, 只有「一堆字节 → 一个纹理 → 屏幕」。 下一节再把 FFmpeg 接进来,那时你就知道多出来的部分都在干什么。
一、YUV420p 为什么是 1.5 字节
亮度每个像素一个,色度四个像素共用一个,所以是 1 + 0.25 + 0.25。
| 平面 | 大小 | 说明 |
|---|---|---|
| Y | w × h | 亮度,最影响清晰度观感 |
| U | w/2 × h/2 | 蓝色差 |
| V | w/2 × h/2 | 红色差 |
| 合计 | w × h × 1.5 | 这就是 1.5 的来源 |
为什么色度可以少采样:人眼对亮度敏感、对颜色不敏感。 把颜色信息砍掉四分之三,肉眼几乎看不出来,码率却省一半。 这是所有视频编码的第一条基本假设,H.264 也是在这个基础上做的。
二、三个 SDL 函数就够
建纹理、贴数据、画上去——D32 全部内容就这三步。
| 函数 | 干什么 | 关键参数 |
|---|---|---|
SDL_CreateTexture |
建一张「画布」 | 格式、访问方式 |
SDL_UpdateTexture |
把字节填进去 | 行跨度 pitch |
SDL_RenderCopy |
画到渲染器 | 源矩形、目标矩形 |
第二行的 pitch 是最容易错的一个参数:它不是你数据的长度,是「每行占多少字节」。
给错不会报错,只会画歪——这类错误只能靠眼睛发现。
第一行的访问方式要选 STREAMING:每帧都换内容的东西用流式,静态的用 STATIC。
选错了性能差,但不影响正确性。
三、dummy 驱动下这个帧率没有意义
D32 量出来 4415 帧/秒,这个数不能用。
| 环节 | dummy 下 | 真机上 |
|---|---|---|
| 贴纹理 | 真的拷内存 | 同样 |
| 画到渲染器 | 记一笔就过 | 真的进显卡 |
| 上屏 | 直接丢弃 | 等垂直同步,16.7 毫秒 |
第二、三行在真机上要花时间,dummy 下全省了。所以这个数只能当「搬运成本」看, 不能当「显示能力」。 这一点必须写清——否则读者会以为「SDL 能跑四千帧」,到真机上一试发现只有 60 帧,就以为哪里坏了。
四、这段代码在真实播放器里的位置
它就是播放器的最后一环,前面还差三样:
| 还差什么 | 在哪一节补 |
|---|---|
| 解码(H.264 → YUV) | 3.3.6 接 FFmpeg |
| 同步(按 pts 显示) | 第二章 2.2.1 |
| 缓冲(网络抖动) | 第五章做成系统时 |
三样补齐,才是一个能用的播放器。 这一节先只管第四环,因为一次只看一件事,才看得清。
五、动手 Demo:造一段裸 YUV,用 SDL 贴上去
用 numpy 现造 60 帧 YUV420p 写进文件,再读回来逐帧贴到纹理上,量一次搬运成本。 顺便验证「pitch 给错不报错,只是画歪」这条。
w, h, n = 320, 240, 60
with open("_t35_yuv.yuv", "wb") as f:
for i in range(n):
Y, Uu, Vv = _yuv(w, h, i)
f.write(Y.tobytes())
f.write(Uu.tobytes())
f.write(Vv.tobytes())
tex = sdl2.SDL_CreateTexture(ren, sdl2.SDL_PIXELFORMAT_IYUV,
sdl2.SDL_TEXTUREACCESS_STREAMING, w, h)
for buf in frames:
sdl2.SDL_UpdateTexture(tex, None, buf, w) # 最后这个 w 是 pitch
sdl2.SDL_RenderClear(ren)
sdl2.SDL_RenderCopy(ren, tex, None, None)
sdl2.SDL_RenderPresent(ren)
实跑输出:
--------------------------------------------------------------------
SDL2 直接播一段裸 YUV,不用 FFmpeg
--------------------------------------------------------------------
**裸 YUV 是最原始的视频格式:没有文件头、没有压缩、没有时间戳。** 你只要告诉 SDL「这是 YUV420p、
宽高多少」,它就能画。
**这一节的目的是把「SDL 负责什么」划清楚**:它只认像素,不认协议。
造了一段 320×240、60 帧的裸 YUV420p:**6.59 MB**
每帧应为 320×240×1.5 = **115200 字节**,60 帧正好 6.59 MB。
项 值 说明
-------------------------------------------------------
Y 平面 76800 字节 亮度,每个像素一个
U 平面 19200 字节 色度,四个像素共用一个
V 平面 19200 字节 同上
每帧合计 115200 字节 这就是 1.5 的来源
纹理建好了:格式 `SDL_PIXELFORMAT_IYUV`,流式访问(每帧都要换内容)。
贴 60 帧用了 **10.9 毫秒**,折合 **5515 帧/秒**。
**这个数没有意义,必须说清**:驱动是 dummy,`RenderPresent` 直接把画面扔了,一个像素都没上屏。
**它量的是「搬运成本」,不是「显示能力」。** 真机上这个数会低很多,
而且要算上垂直同步的 16.7 毫秒。
**顺手验一个必须记住的约束**:纹理尺寸和数据的行跨度必须对上。
把行跨度故意写成 `w // 2`,它不报错——**因为 SDL 不知道你的数据有多长**,
只会按你给的跨度去读,读出来的画面是斜的。
**这类错误没有任何提示,只能靠肉眼看歪不歪。**
每帧 115200 字节 = 320×240×1.5,60 帧 6.59 MB,一分不差。
这个数对上了,说明 YUV420p 的账没算错。
贴 60 帧 13.6 毫秒,折合 4415 帧/秒——这是 dummy 驱动下的搬运成本,不是显示能力。
真机上 RenderPresent 要等垂直同步,一帧至少 16.7 毫秒,所以真实上限是 60 帧/秒。
段落末尾那次故意把 pitch 写成 w // 2,它不抛异常、不返回错误码,只是把画面画成斜的。
这就是 SDL 的风格:它相信你给的参数,所以所有「画得不对」的问题,先查参数,别查驱动。
30 秒实验
- 把
SDL_TEXTUREACCESS_STREAMING换成STATIC,看有没有区别——正确性不变,性能会差。 - 把纹理格式从
IYUV换成YV12,看颜色是不是反了(这两个格式的 U、V 平面顺序相反)。 - 把 60 帧循环里加上
time.sleep(0.033),量出来的帧率就变成 30 了——这就是「同步」最原始的样子。
常见误区
- 以为 pitch 可以填数据长度。 pitch 是「每行多少字节」,填错不报错,只是画歪。
- 以为 YUV420p 每帧是 w×h×3。 那是 RGB。YUV420p 是 w×h×1.5。
- 以为 SDL 能跑到四千帧。 那是 dummy 驱动下不上屏的数字,真机受垂直同步限制,上限 60。
自测三问(点开对答案)
1. YUV420p 一帧 320×240 是多少字节? —— 320×240×1.5 = 115200。亮度每像素 1 字节,两个色度平面各四分之一。 2. `SDL_UpdateTexture` 的第四个参数是什么? —— pitch,每行占多少字节。不是数据总长度,填错只会画歪,不报错。 3. 为什么 dummy 驱动下帧率高得离谱? —— `RenderPresent` 直接把画面丢弃,不等垂直同步,也不进显卡。量到的只是内存搬运时间。下一节把 FFmpeg 接进来:不再用手造的 YUV,而是真去采设备。
- ☐ (本节完,下一节 03.03.06 使用FFmpeg+SDL2读取本地摄像头并渲染)