🎬 第六章 SDL 2开发库及高级应用
📄 一句话概括

SDL 有四个官方扩展库,各补一块:读图、排字、放声音、走网络。 每一个都要单独初始化,而且失败时不抛异常、只返回 0 或 -1。

📝 本节和前面几节的关系

上一节的四个对象都在 SDL 本体里。 但本体故意不管「读文件格式」这件事——它只接受你已经解好的像素和 PCM。 所以真做播放器,一定还会用到 SDL_image 之类的扩展库。 这一节讲清它们的分工和初始化规矩。

一、四个扩展库,各补一块

扩展库 补上什么 初始化函数
SDL_image PNG / JPG / WebP / GIF IMG_Init(IMG_INIT_PNG)
SDL_ttf 用 TTF 字体排文字 TTF_Init()
SDL_mixer MP3 / OGG / WAV 播放 Mix_OpenAudio(...)
SDL_net TCP / UDP 套接字 SDLNet_Init()

为什么本体不内置这些:因为每一种都拖着别的库。 PNG 拖着 libpng 和 zlib,JPG 拖着 libjpeg,MP3 拖着解码器——要是全内置,SDL 就从「薄胶水」变成一个巨大的发行包。

这个设计选择跟我们这一课的主线完全一致:解码交给 FFmpeg,SDL 只管显示。 所以如果你已经用 FFmpeg 解码, SDL_image 和 SDL_mixer 其实都可以不要。 只有 SDL_ttf(要在画面上叠字)和 SDL_net 还有用。

第八、九章讲 FFplay 时会看到这个结论:FFplay 只链了 SDL 本体, 读文件、解码、解复用全是 FFmpeg 干的。 它不是「SDL 播放器」,是「FFmpeg 加一层 SDL 显示」。

二、每个都要自己 Init,而且返回值不是布尔

扩展库的初始化有几个共同点,记一遍就够: 独立于 SDL_Init:SDL_Init 成功不代表扩展库能用。 失败不抛异常:返回 0 或 -1。 失败原因在各自的 GetError:IMG_GetError、TTF_GetError、Mix_GetError。

IMG_Init 最特别,它的返回值是「实际初始化成功的位」:

int got = IMG_Init(IMG_INIT_PNG | IMG_INIT_JPG);
if ((got & IMG_INIT_PNG) == 0) { /* PNG 没成功 */ }

所以这三种写法对错分明: if (IMG_Init(...) == 0) —— 只判了「全军覆没」,一半成功算通过。 if (IMG_Init(...)) —— 把「成功了几位」当布尔,只要有任意一位成功就算全成功。 if ((got & IMG_INIT_PNG) == 0) —— 正确,逐位判。

这种「返回值是位掩码」的接口最容易误用,因为它长得像成功/失败。 同类还有 SDL_Init 的返回值——不过那个是 0/负,不是位掩码,反而简单些。

三、SDL_GetError 的三个使用规矩

这个函数看着无害,实际上有三个坑:

规矩 为什么
调用前先 SDL_ClearError() 它返回的是「上一次设置过的错误」,不会自动清空
失败后立刻读,别隔几行 中间任何一个 SDL 调用都可能覆盖它
不要长期持有返回的指针 它指向内部静态缓冲,下次调用就变

第一条在 Demo 里能直接看到: 代码里显式写了 SDL_ClearError(),所以 IMG_GetError() 返回的是空串——说明这次初始化真的成功了。 如果不先清,你看到的可能是上一次失败留下的旧消息,然后你会去查一个已经不存在的问题。

第三条也常被踩:把 SDL_GetError() 直接交给 printf 打出来是安全的, 但 const char *e = SDL_GetError(); 然后过一会儿再用 e 就不安全。要留就 strdup 一份。

还有一个跨库的注意点:IMG_GetError 与 SDL_GetError 不是同一个缓冲, Mix_GetError 又是一个。 所以出错时要看是哪个库的函数失败了,再去问对应的那个。 问错了库,得到的是空串——而空串看起来像「没有错误」,非常误导。

四、动手 Demo:四个扩展库,各自补一块

这段列出四个扩展库的分工,然后真跑一次 PNG 初始化,看返回值是什么、错误串是什么。 重点看那个返回值:它是 2,不是 1。

import sdl2, sdl2.sdlimage as IM

sdl2.SDL_ClearError()        # 先清,否则读到旧消息
got = IM.IMG_Init(IM.IMG_INIT_PNG)
print(got, IM.IMG_INIT_PNG)  # 2, 2
print(IM.IMG_GetError())

# 正确判法:逐位判,不要当布尔
if (got & IM.IMG_INIT_PNG) == 0:
    print("PNG 没初始化成功")

实跑输出:

--------------------------------------------------------------------
四个扩展库,各自补一块
--------------------------------------------------------------------
**SDL 本体只管「窗口、渲染、事件、音频输出」,不管「读 PNG、排文字、解码音乐、走网络」。**
**这四件事各有一个官方扩展库。**

  扩展库          补上什么                   初始化函数                   要不要单独 Init
  -------------------------------------------------------------------------------------------
  `SDL_image`     PNG / JPG / WebP / GIF     `IMG_Init(IMG_INIT_PNG)`     要
  `SDL_ttf`       用 TTF 字体排文字          `TTF_Init()`                 要
  `SDL_mixer`     MP3 / OGG / WAV 播放       `Mix_OpenAudio(...)`         要
  `SDL_net`       TCP / UDP 套接字           `SDLNet_Init()`              要

**最后一列是全部关键**:**扩展库不是「链上就能用」,每一个都要自己初始化一次**,
**而且初始化失败时返回 0(或者 -1),不抛异常。**

**真跑一次 PNG 初始化**:

  `IMG_Init(IMG_INIT_PNG)` = **2**(要的位是 2)
  `IMG_GetError()` = **(空)**

**`IMG_Init` 的返回值是「实际初始化成功的位」**,不是 0/1。
**所以判成功要写成 `(got & IMG_INIT_PNG) != 0`**,**不能写 `if (IMG_Init(...) == 0)`**,
更不能写 `if (IMG_Init(...))` —— **后者在「只成功了一半」时会误判成全部成功。**

**`SDL_GetError` 这类函数还有个坑**:**它返回的是「上一次设置过的错误」,不会自动清空。**
**所以调用前要先 `SDL_ClearError()`**,否则你看到的是上一次失败留下的旧消息。
**这段代码里那句 `SDL_ClearError()` 就是这个用途。**

IMG_Init 返回 2、要的位也是 2,说明成功了。 如果返回 0,就是一位都没成。 如果返回 2 但你还要了 JPG(那一位是 8),返回就还是 2——意思就是「PNG 成了、JPG 没成」。

错误串是空的那一行也有意义:它证明「先清再调」这个顺序是对的。 不写那句 SDL_ClearError(),这里可能出现一句莫名其妙的旧错误,而实际上什么都没出错。

这张表最下面那句「FFplay 只链了 SDL 本体」要提前记住: 很多人以为「SDL 播放器」就是「用 SDL 读文件」,其实 SDL 连 mp4 都打不开。 播放器之所以是播放器,是因为它有 FFmpeg。

最后一个实践建议:能用 FFmpeg 解的格式,就别再加 SDL_image / SDL_mixer。 每多一个库,就多一份 dll、多一次部署失败的机会,而能力反而重复了。

30 秒实验

  1. 把 IMG_Init 的参数改成 IMG_INIT_PNG | IMG_INIT_JPG,看返回值是不是还是 2。
  2. 算一下:IMG_INIT_PNG 是 2、IMG_INIT_JPG 是 8,两个一起要的位掩码是多少?
  3. 判断一下:已经用 FFmpeg 解码的项目,还需要 SDL_image 吗?

常见误区

  1. if (IMG_Init(...)) 当布尔用。 它返回的是成功了几位,任意一位成功都非零。 要逐位判 (got & 想要的那位)。
  2. 不先 SDL_ClearError()。 读到的是上一次留下的旧消息,你会去查一个已经不存在的问题。
  3. 问错库的错误串。 IMG_GetError、TTF_GetError、Mix_GetError 各是一个缓冲, 问错了得到空串,看着像「没出错」。
  4. 以为 SDL_Init 成功就等于扩展库可用。 两者完全独立,每个扩展库都要自己初始化。
自测三问(点开对答案) 1. `IMG_Init` 的返回值是什么含义? —— 是「实际初始化成功的位掩码」,不是 0/1。要判断某个格式,得用 `(got & IMG_INIT_PNG)` 逐位判。 2. `SDL_GetError()` 为什么要在调用前先清? —— 因为它返回的是上一次设置过的错误,不会自动清空。不清就可能读到旧消息,误判成这次失败。 3. 为什么 SDL 本体不内置图片和音频格式支持? —— 因为每种格式都拖着别的库,全内置会让 SDL 变成一个巨大的发行包,违背「薄胶水」的定位。

扩展库补完了,回到主线:怎么把一帧视频画出来。下一节是本章的技术核心——YUV 三个平面怎么直接进纹理。

相关笔记