学会SDL的事件与渲染机制之后,增加画面刷新机制就可以成为一个播放器了。
在上一篇文章中讲过,似乎循环执行SDL_RenderPresent(renderer)就可以令视频逐帧播放了,为什么还要引入刷新机制呢?
这是因为在一个循环中,重复执行一个函数的效果通常不是周期性的,因为每次加载和处理的数据所消耗的时间是不固定的,因此单纯地在一个循环中使用SDL_RenderPresent(renderer)会令视频播放产生帧率跳动的情况。因此需要引入一个定期刷新机制,令视频的播放有一个固定的帧率。
通常使用多线程的方式进行画面刷新管理,主线程进入主循环中等待事件,画面刷新线程在一段时间后发送画面刷新事件,主线程收到画面刷新事件后进行画面刷新操作。
画面刷新线程
int refresh_video_timer(void *data)
{
while (!s_thread_exit)// 如果没有收到多线程关闭的消息
{
SDL_Event event;
event.type = REFRESH_EVENT;
SDL_PushEvent(&event);// 发送画面刷新事件
SDL_Delay(40);// 延时40ms,即40ms画面刷新一次
}
//如果收到了线程关闭,即停止渲染事件
s_thread_exit = 0;
//push quit event
SDL_Event event;
event.type = QUIT_EVENT;// 发送退出循环事件
SDL_PushEvent(&event);
return 0;
}
以上函数实现了一个画面刷新线程,或者说画面刷新提醒线程,因为真正的画面刷新操作是在主线程中实现的。
需要刷新画面
当需要不断进行刷新工作时,该线程执行以下内容:
while (!s_thread_exit)// 如果没有收到多线程关闭的消息
{
SDL_Event event;
event.type = REFRESH_EVENT;
SDL_PushEvent(&event);// 发送画面刷新事件
SDL_Delay(40);// 延时40ms,即40ms画面刷新一次
}
内容只有两部分:一部分是发送画面刷新事件,就是发信号喊主线程来干活;另一部分是延时,使用一个定时器,保证自己是定期喊主线程来干活的。
主要看一下怎么发消息的:
#define REFRESH_EVENT (SDL_USEREVENT + 1) // 请求画面刷新事件
SDL_USEREVENT是自定义类型的SDL事件,不属于系统事件,可以由用户自定义,这里通过宏定义便于后续引用。
SDL_PushEvent(&event);// 发送画面刷新事件
SDL_PushEvent是SDL2.0之后引入的方法,该方法能够将事件放入事件队列中,当它从事件队列中被取出时,被接收事件的函数识别,并采取相应操作。
也就是说在刷新操作中,我们使用刷新线程不断将刷新事件放到事件队列,主线程中通过读取队列里的事件,当发现事件是刷新事件时就进行刷新操作。
//push quit event
SDL_Event event;
event.type = QUIT_EVENT;// 发送退出循环事件
SDL_PushEvent(&event);
不刷新了
当收到不再需要刷新的命令的时候刷新线程执行以上代码。
#define QUIT_EVENT (SDL_USEREVENT + 2) // 退出事件
可见,退出事件也是个自定义事件。这个事件是赣神魔的呢,之前我们提到过,主线程在工作的时候一直处在一个循环中,当我们停止刷新了之后需要向主线程发送信号,让它停止该循环,并结束程序。
最后一个问题,这个线程什么时候创建呢?显然,这个线程创建后就会一直进行画面刷新提醒操作,因此他应该在一切初始化工作完成之后和主线程的大循环之间被创建。
主线程
学完以上内容,主线程(main函数)大概做了啥呢?其实可以猜个大概:
1.把所有的组件和变量都先初始化了。
2.初始化完进入一个大循环,同时读取事件队列里的事件,如果是刷新事件那么就进行渲染相关工作,进行画面刷新。当然也会存在一些别的事件,但目前能猜出来的主要就是刷新操作。
初始化工作
// SDL
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
SDL全家桶可以都初始化了,其中rect,renderer,texture,pixformat都是完全为渲染操作服务的。
//缓冲区
uint8_t *video_buf = NULL; //读取数据后先把放到buffer里面
// 我们测试的文件是YUV420P格式,计算y u v各自的长度并累加
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;
// 分配缓冲区空间
video_buf = (uint8_t*)malloc(yuv_frame_len);
if (!video_buf)
{
fprintf(stderr, "Failed to alloce yuv frame space!\n");
goto _FAIL;
}
随后我们需要创建一个缓冲区,每次渲染的时候都是先从视频文件里读一帧,这一帧先存到缓冲区再交给渲染器去渲染。
这个缓冲区的大小应该和视频文件的每一帧大小是相同的,这也意味着需要提前计算该视频文件类型的每帧大小,所以我们提前计算了YUV格式的视频帧的大小。
// 打开YUV文件
errno_t err;
err = fopen_s(&video_fd, yuv_path, "rb");//使用fopen_s函数更安全
if (!video_fd)
{
fprintf(stderr, "Failed to open yuv file\n");
goto _FAIL;
}
在打开视频文件的操作中我使用了fopen_s函数,该函数的安全性更强,推荐使用。
// 创建请求刷新的线程
timer_thread = SDL_CreateThread(refresh_video_timer,
NULL,
NULL);
创建完画面刷新线程就要进入大循环了。
主循环
while (1)
{
// 收取SDL系统里面的事件
SDL_WaitEvent(&event);
if (event.type == REFRESH_EVENT) // 画面刷新事件
{
video_buff_len = fread(video_buf, 1, yuv_frame_len, video_fd);//读视频数据到缓冲区
if (video_buff_len <= 0)
{
fprintf(stderr, "Failed to read data from yuv file!\n");
goto _FAIL;
}
// 设置纹理的数据 video_width = 320, plane
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;
// 320x240 怎么保持原视频的宽高比例
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 (event.type == SDL_WINDOWEVENT)//窗口调整事件
{
//I如果窗口进行了调整,比如用鼠标调整了边框
SDL_GetWindowSize(window, &win_width, &win_height);// 得到当前边框的长与宽数据
printf("SDL_WINDOWEVENT win_width:%d, win_height:%d\n", win_width,// 将当前的长宽数据进行显示
win_height);
}
else if (event.type == SDL_QUIT) // 退出事件
{
s_thread_exit = 1;// 将线程退出,停止画面刷新
}
else if (event.type == QUIT_EVENT) // 当SDL_QUIT事件出现后,出现QUIT_EVENT
{
break;// 退出循环,准备回收所有资源
}
}
可以看到,主循环其实就是在不断读取事件队列里的事件,每读取到一个事件,就进行判断,根据该事件的类型采取不同的操作。
if (event.type == REFRESH_EVENT) // 画面刷新事件
{
video_buff_len = fread(video_buf, 1, yuv_frame_len, video_fd);//读视频数据到缓冲区
if (video_buff_len <= 0)
{
fprintf(stderr, "Failed to read data from yuv file!\n");
goto _FAIL;
}
// 设置纹理的数据 video_width = 320, plane
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;
// 320x240 怎么保持原视频的宽高比例
rect.w = video_width * w_ratio;
rect.h = video_height * h_ratio;
//rect.w = video_width * 0.5;
//rect.h = video_height * 0.5;
// 清除当前显示
SDL_RenderClear(renderer);
// 将纹理的数据拷贝给渲染器
SDL_RenderCopy(renderer, texture, NULL, &rect);
// 显示
SDL_RenderPresent(renderer);
}
其中最重要的是以上代码,当收到需要刷新画面的事件后,开始进行读数据帧并渲染的操作。
else if (event.type == QUIT_EVENT) // 当SDL_QUIT事件出现后,出现QUIT_EVENT
{
break;// 退出循环,准备回收所有资源
}
当刷新线程停止工作后,也会发事件令大循环退出,以实现退出程序的目的。