本文主要记录一些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 | SDL_Window * SDLCALL SDL_CreateWindow(const char *title, |
参数含义如下:
- 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 | SDL_Renderer * SDLCALL SDL_CreateRenderer(SDL_Window * window, |
参数含义如下:
- 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 | SDL_Texture * SDLCALL SDL_CreateTexture(SDL_Renderer * renderer, |
参数的含义如下:
- renderer:目标渲染器。
- format :纹理的格式,即像素的各种属性集合。
- access :可以取以下值(定义位于SDL_TextureAccess中)
- SDL_TEXTUREACCESS_STATIC :变化极少
- SDL_TEXTUREACCESS_STREAMING :变化频繁
- SDL_TEXTUREACCESS_TARGET :暂时没有理解
- w :纹理的宽
- h :纹理的高
创建成功则返回纹理的ID,失败返回0
循环显示画面
设置纹理的数据
使用SDL_UpdateTexture()设置纹理的像素数据。SDL_UpdateTexture()的原型如下。
1 | int SDLCALL SDL_UpdateTexture(SDL_Texture * texture, |
参数的含义如下:
- texture:目标纹理。
- rect:更新像素的矩形区域。设置为NULL的时候更新整个区域。
- pixels:像素数据。
- pitch:一行像素数据的字节数。
成功的话返回0,失败的话返回-1。
官方文档说该函数较慢,旨在用于不经常更改的静态纹理。如果要经常更新纹理,则最好将纹理创建为流式传输(SDL_TEXTUREACCESS_STREAMING)并使用下面的锁定函数以进行只写像素访问。(用OpenGL好像有bug,Direct3D正常)
1 | int SDL_LockTexture(SDL_Texture* texture, |
成功时返回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 | int SDLCALL SDL_RenderCopy(SDL_Renderer * renderer, |
参数的含义如下:
- 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 | int SDL_PeepEvents(SDL_Event* events, |
返回检索到的事件数,参数如下:
events: 检索到的事件的目标缓冲区
numevents: 想要检索的事件数
action:SDL_ADDEVENT表示添加事件到队列末尾;SDL_PEEKEVENT和SDL_GETEVENT均是表示获取队列前面满足最大最小范围内的事件,但前者不从队列中删除事件,后者会删除。
minType:纳入考虑的事件类型的最小值,通常用SDL_FIRSTEVENT
maxType:纳入考虑的事件类型的最小值,通常用SDL_LASTEVENT
事件常见用法:
1 | while (1) { |
上例的用法中内层循环不断取事件进行判断,外层循环使得队列为空后重新开始内存循环,由于SDL_PollEvent不阻塞,会造成CPU跑到100%,所以至少得在外层循环的最后delay一下,释放一下CPU。
设置某种类型事件的处理状态:
1 | Uint8 SDL_EventState(Uint32 type, |
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 | SDL_Thread* SDL_CreateThread(SDL_ThreadFunction fn, |
- fn: 线程要运行的函数,当fn返回时,线程退出。原型必须是int SDL_ThreadFunction(void* data)。
- name: 线程名。
- data: fn的参数,是传递给fn的详细数据。
等待线程
1 | void SDL_WaitThread(SDL_Thread* thread, |
等待线程结束。
创建互斥量
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 | int SDL_CondWait(SDL_cond* cond, |
其中mutex是用于协调线程访问的互斥量。当得到信号后返回0。此函数解锁指定的互斥量,并等待另一个线程在条件变量cond上调用SDL_CondSignal()或SDL_CondBroadcast()来发信号。一旦条件变量发出信号,互斥量就会被重新锁定,然后该函数返回。在调用这个函数之前,互斥量必须被锁定。
1 | int SDL_CondWaitTimeout(SDL_cond* cond, |
与上一个函数类似,等待条件变量发出信号或经过指定的时间,所以可以用于延时。当得到信号后返回0。
SDL音频播放的大致流程:
- 创建 SDL_AudioSpec 结构体,设置音频播放数据。包括:采样率(freq)、音频格式(format)、通道数(channels)、采样大小(samples)、回调函数(callback)和用户数据(userdata)等。当开始播放音频时,SDL会持续调用这个回调方法来填充固定数量的字节到音频缓冲区。
- 调用 SDL_OpenAudio() 函数,传入上述 SDL_AudioSpec 结构体数据。这时它会打开音频设备并返回一个另外的 SDL_AudioSpec,这个才是我们真正使用的 SDL_AudioSpec,跟我们传入的可能有所不同。
创建 SDL_AudioSpec 结构体
1 | typedef struct 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 | SDL_AudioDeviceID SDL_OpenAudioDevice(const char* device, |
- device:由SDL_GetAudioDeviceName()得到的UTF-8字符串,传入NULL表示请求最合理的默认值(相当于调用SDL_OpenAudio(),似乎有所矛盾?)
- iscapture:传入非零值表示设备应该打开进行录制,而不是播放。
- desired:预先构造的期望的SDL_AudioSpec,以该参数打开设备。
- obtained:将实际硬件参数放在其中。如果为NULL,则传递给回调函数的音频数据将被保证为所请求的格式,如果需要的话,将自动转换为实际的硬件音频格式。如果获得的是NULL,那么desired会修改字段。
- allowed_changes:由于实际参数可能不符合期望参数,这里设置是否允许改变,可以为0,或者是一个或多个内部定义的标志。
成功则返回设备ID,失败返回0。
1 | int SDL_OpenAudio(SDL_AudioSpec* desired, |
- desired:同上。
- obtained:同上。
成功返回0。
暂停音频
1 | void SDL_PauseAudio(int pause_on) |
与SDL_OpenAudio配对。
- pause_on:非零值表示暂停, 0表示不暂停(播放)。
关闭音频设备
1 | void SDL_CloseAudio(void) |
与SDL_OpenAudio配对。
混音
1 | void SDL_MixAudio(Uint8* dst, |
与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。