SDL(Simple DirectMedia Layer)是一套开放源码代码的跨平台多媒体开发库,使用C语言编写。
SDL提供了多种控制图像、声音、输入输出的函数,让开发者只要用相同或相似的代码就可以开发出跨平台(Linux、Windows、MacOS等)的应用程序软件。
目前SDL多用于开发游戏、模拟器、媒体播放器等多媒体应用领域。
本篇博客学习SDL主要用与辅助学习FFmpeg开发,所以只会记录下与FFmpeg相关的知识点。
SDL将功能分成下列数个子系统(subsystem):(红色项为本博客介绍与使用)
- SDL_INIT_TIMER:定时器
- SDL_INIT_AUDIO:音频
- SDL_INIT_VIDEO:视频
- SDL_INIT_JOYSTICK:摇杆
- SDL_INIT_HAPTIC:触摸屏
- SDL_INIT_GAMECONTROLLER:游戏控制器
- SDL_INIT_EVENTS:事件
- SDL_INIT_EVERYTHING:包含上述所有选项
注意:自己电脑需要有qtcreator环境和Visual Studio 2017及以上开发环境!
目录
一、下载与搭建开发环境
官网:Simple DirectMedia Layer - Homepage
官方文档:SDL2/FrontPage - SDL Wiki
API文档:SDL2/CategoryAPI - SDL Wiki
1.下载
https://github.com/libsdl-org/SDL/releases/https://github.com/libsdl-org/SDL/releases/
2.环境搭建
打开QT新建一个非QT项目
选择32位MinGW编译器
将下载好的SDL解压后拷贝到自己的项目路径下,即与main.c同级路径
在.pro文件中添加把SDL库添加进来
win32 {
INCLUDEPATH += $$PWD/SDL2-2.30.8/include
LIBS += $$PWD/SDL2-2.30.8/lib/x86/SDL2.lib
}
最后在main函数中加入代码测试是否搭建成功
#include <stdio.h>
#include <SDL.h>
#undef main // 忽略main函数
int main()
{
printf("Hello World!\n");
SDL_Window *window = NULL;
// 初始化SDL
SDL_Init(SDL_INIT_VIDEO);
// 创建一个窗体,创建后就会显示
window = SDL_CreateWindow("MySdlWindow", // 窗体标题
SDL_WINDOWPOS_UNDEFINED, // 窗口x位置,这里参数是不关心窗口位置
SDL_WINDOWPOS_UNDEFINED, // 窗口y位置,这里参数是不关心窗口位置
640, // 宽
480, // 高
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); // 一些设置,这里是使用opengl渲染和窗口可设置大小
if (!window) {
printf("Can't create window, err:%s\n", SDL_GetError());
return -1;
}
// 延时3秒
SDL_Delay(3000);
// 销毁窗体
SDL_DestroyWindow(window);
// 释放资源
SDL_Quit();;
return 0;
}
编译后,将 ...\SDL2-2.30.8\lib\x86\SDL2.dll文件拷贝到编译路径的debug文件夹和外面
运行测试:
二、案例讲解
1.window显示
(1).SDL视频显示函数介绍
SDL_Init() 初始化SDL系统
SDL_CreateWindow() 创建窗口SDL_Window
SDL_CreateRenderer() 创建渲染器SDL_Renderer
SDL_CreateTexture() 创建纹理SDL_Texture
SDL_UpdateTexture() 设置纹理的数据
SDL_RenderCopy() 将纹理的数据拷贝给渲染器
SDL_Delay() 工具函数,用于延时
SDL_Quit() 退出SDL系统
(2).SDL数据结构介绍
- SDL_Window 代表了一个 “窗口”
- SDL_Renderer 代表了一个 “渲染器”
- SDL_Texture 代表了一个 “纹理”
- SDL_Rect 一个简单的矩形结构
存储RGB和存储纹理的区别:
比如一个从左到右由红色渐变到蓝色的矩形,用RGB存储的话就需要把矩形中每个点的具体颜色值存储下来;而纹理知识一些描述信息,比如记录了矩形的大小、起始颜色、终止颜色等信息,显卡可以通过这些信息推算出矩形块的详细信息。
所以相对于存储RGB,存储纹理占用的内存要少很多。
- 一个窗口可以有多个render(渲染器)
- render用来渲染纹理
- render用来显示纹理
(3).案例
#include <stdio.h>
#include <SDL.h>
#undef main
int main()
{
int run = 1;
SDL_Window *window = NULL;
SDL_Renderer *renderer = NULL; // 渲染器
SDL_Texture *texture = NULL; // 纹理
SDL_Rect rect; // 矩形,原点在左上角
rect.w = 50;
rect.h = 50;
// 初始化函数,可以确定希望激活的子系统
SDL_Init(SDL_INIT_VIDEO);
// 创建窗口
window = SDL_CreateWindow("Windows",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
640,
480,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (!window) {
printf("Can't create window, err:%s\n", SDL_GetError());
return -1;
}
// 基于窗口 创建 渲染器
renderer = SDL_CreateRenderer(window, -1, 0);
if (!renderer) {
return -1;
}
// 基于渲染器 创建 纹理
texture = SDL_CreateTexture(renderer,
SDL_PIXELFORMAT_RGB888,
SDL_TEXTUREACCESS_TARGET,
640,
480);
if (!texture) {
return -1;
}
int show_count = 0;
while (run) {
rect.x = rand() % 600;
rect.y = rand() % 400;
// 设置渲染目标为纹理
SDL_SetRenderTarget(renderer, texture);
// 纹理背景为黑色
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
// 清屏
SDL_RenderClear(renderer);
// 绘制一个长方形
SDL_RenderDrawRect(renderer, &rect);
// 长方形颜色为白色
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
SDL_RenderFillRect(renderer, &rect);
// 恢复默认,渲染目标为窗口
SDL_SetRenderTarget(renderer, NULL);
// 拷贝纹理到CPU
SDL_RenderCopy(renderer, texture, NULL, NULL);
// 输出到目标窗口上
SDL_RenderPresent(renderer);
SDL_Delay(300);
if (30 < show_count++) {
run = 0; // 结束运行
}
}
// 销毁
SDL_DestroyTexture(texture);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
// 释放资源
SDL_Quit();;
return 0;
}
2.SDL事件
(1).SDL函数介绍
- SDL_WaitEvent() 等待一个事件
- SDL_PushEvent() 发送一个事件
- SDL_PumpEvents() 将硬件设备产生的事件放入事件队列,用于读取事件,在调用该函数之前,必须调用SDL_PumpEvents搜集键盘事件等;
- SDL_PeepEvents() 从事件队列提取一个事件
(2).SDL数据结构介绍
- SDL_Event 代表一个事件
(3).案例
#include <stdio.h>
#include <SDL.h>
#define FF_QUIT_EVENT (SDL_USEREVENT + 1) // 用户自定义事件
#undef main
int main()
{
int run = 1;
SDL_Window *window = NULL;
SDL_Renderer *renderer = NULL; // 渲染器
// 初始化函数
SDL_Init(SDL_INIT_VIDEO);
// 创建窗口
window = SDL_CreateWindow("Windows",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
640,
480,
SDL_WINDOW_SHOWN | SDL_WINDOW_BORDERLESS);
if (!window) {
printf("Can't create window, err:%s\n", SDL_GetError());
return -1;
}
// 基于窗口 创建 渲染器
renderer = SDL_CreateRenderer(window, -1, 0);
if (!renderer) {
return -1;
}
// 设置主窗体的背景色
SDL_SetRenderDrawColor(renderer, 100, 100, 100, 255);
// 清屏
SDL_RenderClear(renderer);
// 输出到窗口上
SDL_RenderPresent(renderer);
SDL_Event event;
int exit = 1;
while (exit) {
// 等待一个事件
SDL_WaitEvent(&event);
switch (event.type) {
case SDL_KEYDOWN: // 键盘按下事件
switch (event.key.keysym.sym) {
case SDLK_a:
printf("key down a\n");
break;
case SDLK_b:
printf("key donw b\n");
break;
case SDLK_c:
printf("key down c\n");
break;
case SDLK_q:
printf("key down q and push quit evnet\n");
SDL_Event event_q;
event_q.type = FF_QUIT_EVENT;
SDL_PushEvent(&event_q);
break;
default:
printf("key down 0x%x\n", event.key.keysym.sym);
}
break;
case SDL_MOUSEBUTTONDOWN: // 鼠标按下事件
if (SDL_BUTTON_LEFT == event.button.button) { // 鼠标左键
printf("mouse donw left\n");
} else if (SDL_BUTTON_RIGHT == event.button.button) { // 鼠标右键
printf("mouse donw right\n");
} else if (SDL_BUTTON_MIDDLE == event.button.button) {
printf("mouse donw middle\n");
} else {
printf("mouse down %d\n", event.button.button);
}
break;
case SDL_MOUSEMOTION: // 鼠标移动事件
printf("mouse move (%d, %d)\n", event.button.x, event.button.y);
break;
case FF_QUIT_EVENT:
printf("receive quit event\n");
exit = 0;
break;
}
}
// 销毁
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
// 释放资源
SDL_Quit();
return 0;
}
3.SDL多线程
(1).SDL函数介绍
- SDL_CreateThread() 创建线程
- SDL_WaitThread() 线程等待
- SDL_CreateMutex() / SDL_DestroyMutex() 创建与销毁互斥锁
- SDL_LockMutex() / SDL_UnlockMutex() 锁定互斥和解锁互斥
- SDL_CreateCond() / SDL_DestoryCond() 创建与销毁条件变量(信号量)
- SDL_CondWait() / SDL_CondSignal() 条件变量(信号量)等待或通知
(2).SDL数据结构介绍
- SDL_Mutex 互斥锁
- SDL_cond 条件变量(互斥锁)
(3).案例
#include <stdio.h>
#include <SDL.h>
SDL_mutex *s_lock = NULL;
SDL_cond *s_cond = NULL;
int thread_work(void *arg) {
SDL_LockMutex(s_lock);
printf(" <=====thread_work sleep\n");
sleep(5);
printf(" <=====thread_work wait\n");
// 释放s_lock资源,并等待signal。之所以释放s_lock是让别的线程能够获取到s_lock
SDL_CondWait(s_cond, s_lock);
printf(" <=====thread_work receive signal, continue todo ~_~!!!\n");
printf(" <=====thread_work end\n");
SDL_UnlockMutex(s_lock);
return 0;
}
#undef main
int main()
{
s_lock = SDL_CreateMutex();
s_cond = SDL_CreateCond();
SDL_Thread *t = SDL_CreateThread(thread_work, "thread_work", NULL);
if (!t) {
printf("%s\n", SDL_GetError());
return -1;
}
sleep(1);
printf("main execute =====>\n");
SDL_LockMutex(s_lock); // 获取锁
printf("main SDL_LockMutex(s_lock) before =====>\n");
SDL_CondSignal(s_cond); // 发送信号,唤醒等待的线程
printf("main ready send signal =====>\n");
printf("main SDL_CondSignal(s_cond) before =====>\n");
SDL_UnlockMutex(s_lock); // 释放锁,让其他线程可以拿到锁
printf("main SDL_UnlockMutex(s_lock) before =====>\n");
SDL_WaitThread(t, NULL);
SDL_DestroyMutex(s_lock);
SDL_DestroyCond(s_cond);
return 0;
}
4.SDL flv播放器
集合前面三点的知识点构成如下示例代码;
需要准备一个flv视频,可通过如下命令提取;格式需要是yuv420p;
ffmpeg -i xxx.mp4 -t 10 -pix_fmt yuv420p -s 320x240 yuv420p_320x240.yuv
#include <stdio.h>
#include <SDL.h>
// 自定义消息类型
#define REFRESH_EVENT (SDL_USEREVENT + 1) // 请求画面刷新事件
#define QUIT_EVENT (SDL_USEREVENT + 2) // 退出事件
// 定义分辨率
#define YUV_WIDTH 320
#define YUV_HEIGHT 240
// 定义YUV格式
#define YUV_FORMAT SDL_PIXELFORMAT_IYUV
int s_thread_exit = 0; // 退出标志,1退出
// 刷新视频画面
int refresh_video_timer(void *data) {
while (!s_thread_exit) {
SDL_Event event;
event.type = REFRESH_EVENT;
SDL_PushEvent(&event);
SDL_Delay(40);
}
s_thread_exit = 0;
// 推送退出事件
SDL_Event event;
event.type = QUIT_EVENT;
SDL_PushEvent(&event);
return 0;
}
#undef main
int main()
{
// 初始化 SDL
if (SDL_Init(SDL_INIT_VIDEO)) {
fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
return -1;
}
SDL_Event event; // 事件
SDL_Rect rect; // 矩形
SDL_Window *window = NULL; // 窗口
SDL_Renderer *renderer = NULL; // 渲染
SDL_Texture *texture = NULL; // 纹理
SDL_Thread *timer_thread = NULL; // 请求刷新线程
uint32_t pixformat = YUV_FORMAT; // YUV420P, 既是SDL_PIXELFORMAT_IYUV
// 分辨率
// YUV的分辨率
int video_width = YUV_WIDTH;
int video_height = YUV_HEIGHT;
// 显示窗口的分辨率
int win_width = YUV_WIDTH;
int win_height = YUV_WIDTH;
// YUV文件句柄
FILE *video_fd = NULL;
const char *yuv_path = "yuv420p_320x240.yuv";
size_t video_buff_len = 0;
uint8_t *video_buf = NULL;
// 测试的视频文件是YUV420P格式的
uint32_t y_frame_len = video_width * video_height;
uint32_t u_frame_len = video_width * video_height / 4;
uint32_t v_frame_len = video_width * video_height / 4;
uint32_t yuv_frame_len = y_frame_len + u_frame_len + v_frame_len;
// 创建窗口
window = SDL_CreateWindow("Simplest YUV Player",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
video_width, video_height,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (!window) {
fprintf(stderr, "SDL: could not create window, err: %s\n", SDL_GetError());
goto _FAIL;
}
// 基于窗口创建渲染器
renderer = SDL_CreateRenderer(window, -1, 0);
// 基于渲染器创建纹理
texture = SDL_CreateTexture(renderer,
pixformat,
SDL_TEXTUREACCESS_STREAMING,
video_width,
video_height);
// 分配空间
video_buf = (uint8_t *)malloc(yuv_frame_len);
if (!video_buf) {
fprintf(stderr, "Failed to alloce yuv frame space!\n");
// todo...
}
// 打开YUV文件
video_fd = fopen(yuv_path, "rb");
if (!video_fd) {
fprintf(stderr, "Failed to open yuv file!\n");
goto _FAIL;
}
// 创建请求刷新线程
timer_thread = SDL_CreateThread(refresh_video_timer, NULL, NULL);
while (1) {
// 收取SDL系统里面的事件
SDL_WaitEvent(&event);
if (REFRESH_EVENT == event.type) { // 画面刷新事件
video_buff_len = fread(video_buf, 1, yuv_frame_len, video_fd);
if (0 >= video_buff_len) {
//fprintf(stderr, "Failed to read data from yuv file!\n");
// 要么读取失败,要么读取完毕!
goto _FAIL;
}
// 设置纹理的数据 参数二矩形传,更新的是一整个画面
SDL_UpdateTexture(texture, NULL, video_buf, video_width);
// 显示区域,可以通过修改w和h进行缩放
rect.x = 0;
rect.y = 0;
float w_ratio = win_width * 1.0 / video_width;
float h_ratio = win_height *1.0 / video_height;
rect.w = video_width * w_ratio;
rect.h = video_height * h_ratio;
// 清除当前显示
SDL_RenderClear(renderer);
// 将纹理的数据拷贝给渲染器
SDL_RenderCopy(renderer, texture, NULL, &rect);
// 显示
SDL_RenderPresent(renderer);
} else if (SDL_WINDOWEVENT == event.type) {
// 通过这里可以实时获取到窗口大小
SDL_GetWindowSize(window, &win_width, &win_height);
} else if (SDL_QUIT == event.type) {
s_thread_exit = 1;
} else if (QUIT_EVENT == event.type) {
break;
}
}
_FAIL:
s_thread_exit = 1; // 保证线程可以正常退出
// 释放资源
if (timer_thread) {
SDL_WaitThread(timer_thread, NULL); // 等待线程退出
}
if (video_buf) {
free(video_buf);
}
if (video_fd) {
fclose(video_fd);
}
if (texture) {
SDL_DestroyTexture(texture);
}
if (renderer) {
SDL_DestroyRenderer(renderer);
}
if (window) {
SDL_DestroyWindow(window);
}
// 释放资源
SDL_Quit();
return 0;
}
5.SDL 播放pcm文件
(1).SDL函数介绍
int SDLCALL SDL_OpenAudio(SDL_AudioSpec * desired, SDL_AudioSpec * obtained);
desired:期望的参数;
obtained:返回的实际音频设备的参数,一般情况下设置为NULL;
typedef struct SDL_AudioSpec
{
int freq; /**< 音频采样率 */
SDL_AudioFormat format; /**< 音频数据格式 */
Uint8 channels; /**< 声道数:1 单声道,2 立体声 */
Uint8 silence; /**< 设置静音的值,因为声音采样是有符号的,所以0当然就是这个值 */
Uint16 samples; /**< 音频缓冲区中的采样个数,要求必须是2的n次方 */
Uint16 padding; /**< 考虑到兼容性的一个参数 */
Uint32 size; /**< 音频缓冲区的大小,以字节为单位 */
SDL_AudioCallback callback; /**< 填充音频缓冲区的回调函数 (NULL to use SDL_QueueAudio()). */
void *userdata; /**< 用户自定义的数据 */
} SDL_AudioSpec;
// 回调函数
typedef void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream, int len);
userdata:SDL_AudioSpec结构体中的用户自定义数据,一般情况下可以不用;
stream:该指针指向需要填充的音频缓冲区;
len:音频缓冲区的大小(以字节为单位)
// 播放音频数据
void SDLCALL SDL_PauseAudio(int pause_on);
pause_on:设置为0的时候即可开始播放音频数据。设置为1的时候,将会播放静音的值。
(2).案例
#include <stdio.h>
#include <SDL.h>
// 每次读取2帧数据,以1024个采样点为一帧,2通道,16bit采样点为例
#define PCM_BUFFER_SIZE (1024 * 2 * 2 * 2) // 第一个2是2帧数据,第二个2是两通道,第三个2是16bit,即两字节
// 音频PCM数据缓存
static Uint8 *s_audio_buf = NULL;
// 目前读取的位置
static Uint8 *s_audio_pos = NULL;
// 缓存结束位置
static Uint8 *s_audio_end = NULL;
// 音频设备回调函数
void fill_audio_pcm(void *udata, Uint8 *stream, int len) {
SDL_memset(stream, 0, len);
// 数据读取完毕
if (s_audio_pos >= s_audio_end) {
return;
}
// 数据够了就读取预设长度,数据不够就只读取部分数据
int remain_buffer_len = s_audio_end - s_audio_pos;
len = (len < remain_buffer_len) ? len : remain_buffer_len;
// 拷贝数据到stream,并调整音量
SDL_MixAudio(stream, s_audio_pos, len, SDL_MIX_MAXVOLUME/8); // /8 主要用于调小点音量,最大值128
printf("len = %d\n", len);
// 移动缓存指针
s_audio_pos += len;
}
/* 提取PCM文件
ffmpeg -i xxx.mp4 -t 10 -codec:a pcm_s16le -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm
测试PCM文件
ffplay -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm
*/
#undef main
int main(int argc, char *argv[]) {
int ret = -1;
FILE *audio_fd = NULL;
SDL_AudioSpec spec;
const char *path = "44100_16bit_2ch.pcm";
// 每次缓存的长度
size_t read_buffer_len = 0;
// SDL初始化
if (SDL_Init(SDL_INIT_AUDIO)) { // 支持AUDIO
fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
return ret;
}
// 打开PCM文件
audio_fd = fopen(path, "rb");
if (!audio_fd) {
fprintf(stderr, "Failed to open pcm file!\n");
goto _FAIL;
}
s_audio_buf = (uint8_t *)malloc(PCM_BUFFER_SIZE);
// 音频参数设置
spec.freq = 44100; // 采样频率
spec.format = AUDIO_S16SYS; // 采用点格式
spec.channels = 2; // 2通道
spec.silence = 0;
spec.samples = 1024; // 每次读取的采用数量,由此值确定多久触发一次回调函数
spec.callback = fill_audio_pcm; // 回调函数
spec.userdata = NULL;
// 打开音频设备
if (SDL_OpenAudio(&spec, NULL)) {
fprintf(stderr, "Failed to open audio device, %s\n", SDL_GetError());
goto _FAIL;
}
// play audio
SDL_PauseAudio(0);
int data_count = 0;
while (1) {
// 从文件读取PCM数据
read_buffer_len = fread(s_audio_buf, 1, PCM_BUFFER_SIZE, audio_fd);
if (0 == read_buffer_len) {
break;
}
data_count += read_buffer_len; // 统计读取的数据总字节
printf("now playing %10d bytes data.\n", data_count);
s_audio_end = s_audio_buf + read_buffer_len; // 更新buffer的结束位置
s_audio_pos = s_audio_buf; // 更新buffer的起始位置
// the main thread wait for a monent
while (s_audio_pos < s_audio_end) {
SDL_Delay(2); // 等待PCM数据消耗
/*
* 延时时间计算:每次读取的采用数量 / 采样频率
* spec.samples / spec.freq
* 在这里的计算是:1024 / 44100 = 0.023 (约等于23毫秒)
*
* 所以建议延时时间设置为最终计算结果的整数倍,否则会出现断音效果!!!
*/
}
}
printf("play PCM finish\n");
// 关闭音频设备
SDL_CloseAudio();
_FAIL:
// release some resources
if (s_audio_buf) {
free(s_audio_buf);
}
if (audio_fd) {
fclose(audio_fd);
}
// quit SDL
SDL_Quit();
return 0;
}
完!