📹 第三章 FFmpeg二次开发采集并预览本地摄像头
📄 一句话概括

裸 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 秒实验

  1. 把 SDL_TEXTUREACCESS_STREAMING 换成 STATIC,看有没有区别——正确性不变,性能会差。
  2. 把纹理格式从 IYUV 换成 YV12,看颜色是不是反了(这两个格式的 U、V 平面顺序相反)。
  3. 把 60 帧循环里加上 time.sleep(0.033),量出来的帧率就变成 30 了——这就是「同步」最原始的样子。

常见误区

  1. 以为 pitch 可以填数据长度。 pitch 是「每行多少字节」,填错不报错,只是画歪。
  2. 以为 YUV420p 每帧是 w×h×3。 那是 RGB。YUV420p 是 w×h×1.5。
  3. 以为 SDL 能跑到四千帧。 那是 dummy 驱动下不上屏的数字,真机受垂直同步限制,上限 60。
自测三问(点开对答案) 1. YUV420p 一帧 320×240 是多少字节? —— 320×240×1.5 = 115200。亮度每像素 1 字节,两个色度平面各四分之一。 2. `SDL_UpdateTexture` 的第四个参数是什么? —— pitch,每行占多少字节。不是数据总长度,填错只会画歪,不报错。 3. 为什么 dummy 驱动下帧率高得离谱? —— `RenderPresent` 直接把画面丢弃,不等垂直同步,也不进显卡。量到的只是内存搬运时间。

下一节把 FFmpeg 接进来:不再用手造的 YUV,而是真去采设备。

相关笔记