Fork me on GitHub

SDL基本知识

本文主要记录一些SDL的基础知识,基于SDL2.0,有时间会慢慢增改。SDL是一个跨平台的库,通过封装OpenGL和Direct3D等提供对音频、键盘、鼠标、操纵杆和图形硬件的底层访问。FFmpeg只能实现视音频解码,而具体渲染需要配合SDL来实现,官方的ffplay用的也是SDL。

使用SDL播放一个视频代码流程大体如下:

  • 初始化:
    • SDL_Init(): 初始化SDL
    • SDL_CreateWindow(): 创建窗口(Window)。
    • SDL_CreateRenderer(): 基于窗口创建渲染器(Render)。
    • SDL_CreateTexture(): 创建纹理(Texture)。
  • 循环渲染数据:
    • SDL_UpdateTexture(): 设置纹理的数据。
    • SDL_RenderCopy(): 纹理复制给渲染器。
    • SDL_RenderPresent(): 显示

具体如下:

初始化SDL

使用SDL_Init()初始化SDL。该函数可以确定希望激活的子系统。SDL_Init()函数原型如下:

1
int SDLCALL SDL_Init(Uint32 flags)

其中,flags可以取下列值:

  • SDL_INIT_TIMER:定时器
  • SDL_INIT_AUDIO:音频
  • SDL_INIT_VIDEO:视频
  • SDL_INIT_JOYSTICK:摇杆
  • SDL_INIT_HAPTIC:触摸屏
  • SDL_INIT_GAMECONTROLLER:游戏控制器
  • SDL_INIT_EVENTS:事件
  • SDL_INIT_NOPARACHUTE:不捕获关键信号
  • SDL_INIT_EVERYTHING:包含上述所有选项

SDL_Init()有一点需要注意:初始化的时候尽量只激活需要用到的子系统,而不要用SDL_INIT_EVERYTHING。因为有些情况下使用SDL_INIT_EVERYTHING会出现一些不可预知的问题。

创建窗口(Window)

使用SDL_CreateWindow()创建一个用于视频播放的窗口,这是一种逻辑上的窗口,必须经过渲染才能显示。SDL_CreateWindow()的原型如下:

1
2
3
SDL_Window * SDLCALL SDL_CreateWindow(const char *title,
int x, int y, int w,
int h, Uint32 flags);

参数含义如下:

  • title :窗口标题
  • x :窗口位置x坐标。也可以设置为SDL_WINDOWPOS_CENTERED或- SDL_WINDOWPOS_UNDEFINED。
  • y :窗口位置y坐标。同上。
  • w :窗口的宽
  • h :窗口的高
  • flags :支持下列标识。包括了窗口的是否最大化、最小化,能否调整边界等等属性。
    ::SDL_WINDOW_FULLSCREEN, ::SDL_WINDOW_OPENGL,
    ::SDL_WINDOW_HIDDEN, ::SDL_WINDOW_BORDERLESS,
    ::SDL_WINDOW_RESIZABLE, ::SDL_WINDOW_MAXIMIZED,
    ::SDL_WINDOW_MINIMIZED, ::SDL_WINDOW_INPUT_GRABBED,
    ::SDL_WINDOW_ALLOW_HIGHDPI

返回创建完成的窗口的ID。如果创建失败则返回0。

若要从Qt窗口创建一个SDL的子窗口,则利用QWidget的窗口id在构造函数中使用:

1
SDL_Window *window = SDL_CreateWindowFrom((void*)this->winId())

基于窗口创建渲染器(Render)

有了窗口后。我们需要创建渲染上下文,该上下文中一方面存放着要渲染的目标,也就是windows窗口;另一方面是存放着一个缓冲区,该缓冲区用于存放渲染的内容。缓冲区包括SDL_Surface和SDL_Texture,前者存放的像素数据,后者存放的是图像的某种描述信息,借助别的手段实现与前者相近的绘制,而且效率更高。

使用SDL_CreateRenderer()基于窗口创建渲染器。SDL_CreateRenderer()原型如下:

1
2
SDL_Renderer * SDLCALL SDL_CreateRenderer(SDL_Window * window,
int index, Uint32 flags);

参数含义如下:

  • window : 渲染的目标窗口。
  • index :打算初始化的渲染设备的索引。设置“-1”则初始化默认的渲染设备。
  • flags :支持以下值(位于SDL_RendererFlags定义中)
    • SDL_RENDERER_SOFTWARE :使用软件渲染
    • SDL_RENDERER_ACCELERATED :使用硬件加速
    • SDL_RENDERER_PRESENTVSYNC:和显示器的刷新率同步
    • SDL_RENDERER_TARGETTEXTURE :暂不明确

返回创建完成的渲染器的ID。如果创建失败则返回NULL。

基于渲染器创建纹理(Texture)

使用SDL_CreateTexture()基于渲染器创建一个纹理。SDL_CreateTexture()的原型如下。

1
2
3
4
SDL_Texture * SDLCALL SDL_CreateTexture(SDL_Renderer * renderer,
Uint32 format,
int access, int w,
int h);

参数的含义如下:

  • renderer:目标渲染器。
  • format :纹理的格式,即像素的各种属性集合。
  • access :可以取以下值(定义位于SDL_TextureAccess中)
    • SDL_TEXTUREACCESS_STATIC :变化极少
    • SDL_TEXTUREACCESS_STREAMING :变化频繁
    • SDL_TEXTUREACCESS_TARGET :暂时没有理解
  • w :纹理的宽
  • h :纹理的高

创建成功则返回纹理的ID,失败返回0

循环显示画面

设置纹理的数据

使用SDL_UpdateTexture()设置纹理的像素数据。SDL_UpdateTexture()的原型如下。

1
2
3
4
int SDLCALL SDL_UpdateTexture(SDL_Texture * texture,
const SDL_Rect * rect,
const void *pixels,
int pitch);

参数的含义如下:

  • texture:目标纹理。
  • rect:更新像素的矩形区域。设置为NULL的时候更新整个区域。
  • pixels:像素数据。
  • pitch:一行像素数据的字节数。

成功的话返回0,失败的话返回-1。

官方文档说该函数较慢,旨在用于不经常更改的静态纹理。如果要经常更新纹理,则最好将纹理创建为流式传输(SDL_TEXTUREACCESS_STREAMING)并使用下面的锁定函数以进行只写像素访问。(用OpenGL好像有bug,Direct3D正常)

1
2
3
4
int SDL_LockTexture(SDL_Texture*    texture,
const SDL_Rect* rect,
void** pixels,
int* pitch)

成功时返回0,如果纹理无效或未使用SDL_TEXTUREACCESS_STREAMING创建,则返回负的错误代码。其中可用于编辑的像素不一定需要包含旧纹理数据。

配对的函数用于解锁纹理,并将更改上传到视频内存:

1
void SDL_UnlockTexture(SDL_Texture* texture)

不过实际上用于播放视频时并未发现SDL_UpdateTexture有速度上的问题。

纹理复制给渲染目标

使用SDL_RenderCopy()将纹理数据复制给渲染目标。在使用SDL_RenderCopy()之前,可以使用SDL_RenderClear()先使用清空渲染目标。实际上视频播放的时候不使用SDL_RenderClear()也是可以的,因为视频的后一帧会完全覆盖前一帧。SDL_RenderClear()原型如下。

1
int SDLCALL SDL_RenderClear(SDL_Renderer * renderer);

参数renderer用于指定渲染目标。

SDL_RenderCopy()原型如下。

1
2
3
4
int SDLCALL SDL_RenderCopy(SDL_Renderer * renderer,
SDL_Texture * texture,
const SDL_Rect * srcrect,
const SDL_Rect * dstrect);

参数的含义如下:

  • renderer:渲染目标。
  • texture:输入纹理。
  • srcrect:选择输入纹理的一块矩形区域作为输入。设置为NULL的时候整个纹- 理作为输入。
  • dstrect:选择渲染目标的一块矩形区域作为输出。设置为NULL的时候整个渲染目标作为输出。

成功的话返回0,失败的话返回-1。

显示

使用SDL_RenderPresent()显示画面。SDL_RenderPresent()原型如下:

1
void SDLCALL SDL_RenderPresent(SDL_Renderer * renderer);

参数renderer用于指定渲染目标。

事件

几个常见的事件:

  • SDL_WindowEvent : Window窗口相关的事件。
  • SDL_KeyboardEvent : 键盘相关的事件。
  • SDL_MouseMotionEvent : 鼠标移动相关的事件。
  • SDL_QuitEvent : 退出事件。
  • SDL_UserEvent : 用户自定义事件。

SDL将所有事件都存放在一个队列中。所有对事件的操作,其实就是对队列的操作。常用的事件API:

  • SDL_PollEvent: 将队列中的事件取出。由于不会阻塞,可能会占用较多的CPU资源,在对于游戏来说等实时处理多的情况下适用。
  • SDL_WaitEvent: 当队列中有事件时,取出事件。否则处于阻塞状态,释放CPU。实时性处理不多时使用。
  • SDL_WaitEventTimeout: 与SDL_WaitEvent的区别时,当到达超时时间后,退出- 阻塞状态。
  • SDL_PumpEvents:从设备收集所有挂起的输入信息并将其放入事件队列中。如果不调用SDL_PumpEvents,则不会在队列上放置任何事件。SDL_PollEvent和SDL_WaitEvent会隐式调用SDL_PumpEvents。但是,如果没有使用上述两个函数轮询或等待事件,则必须调用SDL_PumpEvents以强制更新事件队列。
  • SDL_PushEvent: 向队列中插入事件。
  • SDL_PeekEvent: 检查事件队列中的消息,并可选地返回它们。原型如下:
1
2
3
4
5
int SDL_PeepEvents(SDL_Event*      events,
int numevents,
SDL_eventaction action,
Uint32 minType,
Uint32 maxType)

返回检索到的事件数,参数如下:

events: 检索到的事件的目标缓冲区

numevents: 想要检索的事件数

action:SDL_ADDEVENT表示添加事件到队列末尾;SDL_PEEKEVENT和SDL_GETEVENT均是表示获取队列前面满足最大最小范围内的事件,但前者不从队列中删除事件,后者会删除。

minType:纳入考虑的事件类型的最小值,通常用SDL_FIRSTEVENT

maxType:纳入考虑的事件类型的最小值,通常用SDL_LASTEVENT

事件常见用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
while (1) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
/* handle your event here */
switch(event.type)
{
case SDL_QUIT:
break;
...
}

/* do some other stuff here -- draw your app, etc. */
}

上例的用法中内层循环不断取事件进行判断,外层循环使得队列为空后重新开始内存循环,由于SDL_PollEvent不阻塞,会造成CPU跑到100%,所以至少得在外层循环的最后delay一下,释放一下CPU。

设置某种类型事件的处理状态:

1
2
Uint8 SDL_EventState(Uint32 type,
int state):

state有三个值:

  • SDL_QUERY返回指定事件的当前处理状态。
  • SDL_IGNORE (又名SDL_DISABLE)事件:将自动从事件队列中删除,并且不会被过滤。
  • SDL_ENABLE:事件将被正常处理。

显示结束后的收尾工作

销毁Texture

1
void SDL_DestroyTexture(SDL_Texture* texture)

消毁渲染上下文

释放渲染上下文相关的资源。

1
void SDL_DestroyRenderer(SDL_Renderer* renderer)

销毁窗口

1
void SDL_DestroyWindow(SDL_Window* window)

退出SDL

1
void SDL_Quit()

多线程

创建线程

1
2
3
SDL_Thread* SDL_CreateThread(SDL_ThreadFunction fn,
const char* name,
void* data)
  • fn: 线程要运行的函数,当fn返回时,线程退出。原型必须是int SDL_ThreadFunction(void* data)。
  • name: 线程名。
  • data: fn的参数,是传递给fn的详细数据。

等待线程

1
2
void SDL_WaitThread(SDL_Thread* thread,
int* status)

等待线程结束。

创建互斥量

1
SDL_mutex* SDL_CreateMutex(void)

销毁互斥量

1
void SDL_DestroyMutex(SDL_mutex* mutex)

互斥量加锁

1
int SDL_LockMutex(SDL_mutex* mutex)

返回0代表成功。

互斥量解锁

1
int SDL_UnlockMutex(SDL_mutex* mutex)

返回0代表成功。

创建条件变量

1
SDL_cond* SDL_CreateCond(void)

用于线程同步。

重新启动另一个等待着条件变量的线程

1
int SDL_CondSignal(SDL_cond* cond)

返回0代表成功。

等待条件变量发出信号

1
2
int SDL_CondWait(SDL_cond*  cond,
SDL_mutex* mutex)

其中mutex是用于协调线程访问的互斥量。当得到信号后返回0。此函数解锁指定的互斥量,并等待另一个线程在条件变量cond上调用SDL_CondSignal()或SDL_CondBroadcast()来发信号。一旦条件变量发出信号,互斥量就会被重新锁定,然后该函数返回。在调用这个函数之前,互斥量必须被锁定。

1
2
3
int SDL_CondWaitTimeout(SDL_cond*  cond,
SDL_mutex* mutex,
Uint32 ms)

与上一个函数类似,等待条件变量发出信号或经过指定的时间,所以可以用于延时。当得到信号后返回0。

SDL音频播放的大致流程:

  1. 创建 SDL_AudioSpec 结构体,设置音频播放数据。包括:采样率(freq)、音频格式(format)、通道数(channels)、采样大小(samples)、回调函数(callback)和用户数据(userdata)等。当开始播放音频时,SDL会持续调用这个回调方法来填充固定数量的字节到音频缓冲区。
  2. 调用 SDL_OpenAudio() 函数,传入上述 SDL_AudioSpec 结构体数据。这时它会打开音频设备并返回一个另外的 SDL_AudioSpec,这个才是我们真正使用的 SDL_AudioSpec,跟我们传入的可能有所不同。

创建 SDL_AudioSpec 结构体

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct SDL_AudioSpec {
int freq; /**< DSP frequency -- samples per second */
Uint16 format; /**< Audio data format */
Uint8 channels; /**< Number of channels: 1 mono, 2 stereo */
Uint8 silence; /**< Audio buffer silence value (calculated) */
Uint16 samples; /**< Audio buffer size in samples (power of 2) */
Uint16 padding; /**< Necessary for some compile environments */
Uint32 size; /**< Audio buffer size in bytes (calculated) */

void (SDLCALL *callback)(void *userdata, Uint8 *stream, int len);
void *userdata;
} SDL_AudioSpec;

SDL_AudioSpec是包含音频输出格式的结构。它还包含一个回调(callback),当音频设备需要更多数据时调用。

  • freq: 指定了每秒向音频设备发送的sample数。常用的值为:11025,22050,44100。值越高质量越好。
  • format: 指定了每个sample元素的大小和类型。一般用AUDIO_S16SYS,表示16位无符号,取决于系统大小端。
  • channels 指定了声音的通道数:1(单声道);2(立体声)。
  • samples 这个值表示音频缓存区的大小(以sample计)。一个sample是一段大小为 format * channels 的音频数据。推荐值为 512-8192,ffplay 使用的是 1024。
  • size: 这个值表示音频缓存区的大小(以byte计)。可由打开设备的函数根据format、channels、samples等参数计算。
  • silence: 设置表示静音的值。可由打开设备的函数计算。
  • padding: 考虑到兼容性的一个参数。
  • userdata:程序特定的参数,传给callback。
  • callback: 是获取音频数据后的持续调用的回调函数,可以将解码获取的音频码流进行混音并输出到设备。callback的参数:
    • userdata:如上,一般传入解码器上下文AVCodecContext等,ffplay中是包含了丰富内容的VideoState。
    • stream:指向缓存区的指针,这个缓存区将被这个回调函数填满。回调函数必须填满该缓冲区,若没有声音可播放则应填满静音。一旦回调函数返回,缓冲区将不再有效。(应该是SDL自己传入的)
    • len:即上面缓存区的大小(应该是与size相同,SDL自己传入的)。

打开音频设备

SDL_OpenAudioDevice和SDL_OpenAudio:均是打开特定的音频设备,在SDL2中SDL_OpenAudioDevice用于替代原来的SDL_OpenAudio。但是只能用来打开AudioDeviceID大于等于2的设备,而默认的设备,即AudioDeviceID等于1,仍然只能用SDL_OpenAudio函数打开。

1
2
3
4
5
SDL_AudioDeviceID SDL_OpenAudioDevice(const char*          device,
int iscapture,
const SDL_AudioSpec* desired,
SDL_AudioSpec* obtained,
int allowed_changes)
  • device:由SDL_GetAudioDeviceName()得到的UTF-8字符串,传入NULL表示请求最合理的默认值(相当于调用SDL_OpenAudio(),似乎有所矛盾?)
  • iscapture:传入非零值表示设备应该打开进行录制,而不是播放。
  • desired:预先构造的期望的SDL_AudioSpec,以该参数打开设备。
  • obtained:将实际硬件参数放在其中。如果为NULL,则传递给回调函数的音频数据将被保证为所请求的格式,如果需要的话,将自动转换为实际的硬件音频格式。如果获得的是NULL,那么desired会修改字段。
  • allowed_changes:由于实际参数可能不符合期望参数,这里设置是否允许改变,可以为0,或者是一个或多个内部定义的标志。

成功则返回设备ID,失败返回0。

1
2
int SDL_OpenAudio(SDL_AudioSpec* desired,
SDL_AudioSpec* obtained)
  • desired:同上。
  • obtained:同上。

成功返回0。

暂停音频

1
void SDL_PauseAudio(int pause_on)

与SDL_OpenAudio配对。

  • pause_on:非零值表示暂停, 0表示不暂停(播放)。

关闭音频设备

1
void SDL_CloseAudio(void)

与SDL_OpenAudio配对。

混音

1
2
3
4
void SDL_MixAudio(Uint8*       dst,
const Uint8* src,
Uint32 len,
int volume)

与SDL_OpenAudio配对。

  • dst:放置混音结果,比如回调函数的stream。
  • src:要混音的源音频缓冲区。
  • len:上述缓冲区的字节数。
  • volume:音量,0 - 128, 设置为SDL_MIX_MAXVOLUME可以获得完整的(100%)音量。

杂项

打印日志

1
void SDL_Log(const char* fmt, ...)

延时若干毫秒

1
void SDL_Delay(Uint32 ms)

可用于控制帧率等。

内存赋值

1
SDL_memset( void *dst, int c, size_t len)

大概是封装了一下memset,就是初始化某段内存的。可用于初始化SDL_AudioSpec和回调函数的stream。