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 秒实验
- 把
IMG_Init的参数改成IMG_INIT_PNG | IMG_INIT_JPG,看返回值是不是还是 2。 - 算一下:
IMG_INIT_PNG是 2、IMG_INIT_JPG是 8,两个一起要的位掩码是多少? - 判断一下:已经用 FFmpeg 解码的项目,还需要
SDL_image吗?
常见误区
if (IMG_Init(...))当布尔用。 它返回的是成功了几位,任意一位成功都非零。 要逐位判(got & 想要的那位)。- 不先
SDL_ClearError()。 读到的是上一次留下的旧消息,你会去查一个已经不存在的问题。 - 问错库的错误串。
IMG_GetError、TTF_GetError、Mix_GetError各是一个缓冲, 问错了得到空串,看着像「没出错」。 - 以为
SDL_Init成功就等于扩展库可用。 两者完全独立,每个扩展库都要自己初始化。
自测三问(点开对答案)
1. `IMG_Init` 的返回值是什么含义? —— 是「实际初始化成功的位掩码」,不是 0/1。要判断某个格式,得用 `(got & IMG_INIT_PNG)` 逐位判。 2. `SDL_GetError()` 为什么要在调用前先清? —— 因为它返回的是上一次设置过的错误,不会自动清空。不清就可能读到旧消息,误判成这次失败。 3. 为什么 SDL 本体不内置图片和音频格式支持? —— 因为每种格式都拖着别的库,全内置会让 SDL 变成一个巨大的发行包,违背「薄胶水」的定位。扩展库补完了,回到主线:怎么把一帧视频画出来。下一节是本章的技术核心——YUV 三个平面怎么直接进纹理。
- ☐ (本节完,下一节 06.04 SDL 2播放YUV视频)