一、SDL 播放 YUV 视频
1、前置知识回顾
在 【FFmpeg】SDL 音视频开发 ① ( SDL 窗口绘制 | SDL 视频显示函数 | SDL_Window 窗口 | SDL_Renderer 渲染器 | SDL_Texture 纹理 ) 博客中 , 介绍了
- SDL_Init 函数 - SDL 初始化环境
- SDL_CreateWindow 函数 - 创建视频窗口
- SDL_CreateRenderer 函数 - 创建画面渲染器
- SDL_CreateTexture 函数 - 创建纹理
- SDL_Quit 函数 - SDL 退出并释放资源
的用法 , 这些函数中 , 前四个函数是显示 视频画面 前的准备工作 , 最后一个 SDL_Quit 函数 是最后退出渲染时 , 释放 SDL 框架占用的各种资源 ;
在 【FFmpeg】SDL 音视频开发 ② ( SDL 视频显示函数 | 设置渲染器目标纹理 | 设置渲染器颜色 | 清除渲染器 | 渲染器绘制矩形 | 纹理拷贝 | 窗口中显示渲染纹理 ) 博客中 , 介绍了
- SDL_SetRenderTarget 函数 - 设置渲染器目标纹理
- SDL_SetRenderDrawColor 函数 - 设置渲染器颜色
- SDL_RenderClear 函数 - 清除渲染器
- SDL_RenderDrawRect 函数 - 渲染器绘制矩形
- SDL_RenderCopy 函数 - 纹理拷贝
- SDL_RenderPresent 函数 - 窗口中显示渲染纹理
几个函数 , 这些都是渲染视频画面的重要函数 ;
在本篇博客中 , 将会使用到上面的部分函数 ;
2、SDL 播放 YUV 画面流程
SDL 播放 YUV 画面流程 :
- 创建 SDL_Window 窗口对象 : 调用 SDL_CreateWindow 函数 创建 SDL 显示窗口 ;
- 创建 SDL_Render 渲染器对象 : 调用 SDL_CreateRender 函数 创建 SDL 渲染器 , 用于 SDL 绘图 ;
- 创建 SDL_Texture 纹理对象 : 调用 SDL_CreateTexture 函数 创建 纹理对象 , 纹理对象用于描述渲染画面的内容 ;
- SDL_Window 窗口 / SDL_Render 渲染器 / SDL_Texture 纹理 关系 :
- 渲染器 需要 在 窗口 上绘制 , 渲染器 创建需要绑定 窗口 ;
- 渲染器 需要调用 SDL_SetRenderTarget 函数 设置 渲染目标 , 渲染目标 是 纹理对象 ;
- 更新纹理 : 调用 SDL_UpdateTexture 函数 , 可以更新 SDL_Texture 纹理对象 的描述内容 ;
- 清除渲染器纹理 : 调用 SDL_RenderClear 函数 , 清除 渲染器 中 原来的目标纹理 ;
- 拷贝渲染器纹理 : 调用 SDL_RenderCopy 函数 , 将 纹理画面 拷贝 到 渲染器的 目标纹理 中 , 这里注意 SDL_SetRenderTarget 函数只是设置渲染目标纹理 , 本函数是开始拷贝 ;
- 渲染纹理 : 调用 SDL_RenderPresent 函数 , 渲染器 将 纹理 渲染到 窗口 中 ;
3、YUV 视频存放位置
代码编译后 , SDL_Demo 工程会在本地生成 编译 后的可执行文件目录 build-SDL_Demo-Desktop_Qt_5_14_2_MSVC2015_32bit-Debug ,
将要播放的 YUV 420P 格式的 视频文件 , 拷贝到这个 build-SDL_Demo-Desktop_Qt_5_14_2_MSVC2015_32bit-Debug 目录中 ,
将 视频文件 拷贝到根目录即可 ;
4、刷新控制子线程
使用 SDL 播放 YUV 视频时 , 视频画面刷新是在 主线程 中执行的 ;
此处专门开启了一个子线程 , 用于控制 YUV 画面的刷新 ;
在下面的代码中 , 开启了子线程 , 子线程中执行 refresh_video_timer 函数 , 然后再启动主线程 , 主线程直接无限循环执行 , 每次执行时 都要接收 子线程 中传递的事件 , 受子线程控制 ;
代码语言:javascript复制 // 创建 YUV 画面 刷新线程 , 该线程与主线程 并行执行
timer_thread = SDL_CreateThread(refresh_video_timer, NULL, NULL); // 创建刷新线程
// 在下面 主循环 中 , 不断刷新 YUV 画面数据
while (1) // 主循环
{
子线程 执行的 refresh_video_timer 函数内容如下 , 该函数用于 在子线程 中 控制画面的刷新速度 , 子线程 中 向主线程发送 刷新事件 , 主线程收到 REFRESH_EVENT 事件 , 就会刷新界面 ;
YUV 视频 播放完毕后 子线程会向主线程发送 QUIT_EVENT 事件 , 主线程 收到 QUIT_EVENT 事件 , 就会停止播放 ;
代码语言:javascript复制// 该函数用于 在子线程 中 控制画面的刷新速度
// 子线程 中 向主线程发送 刷新事件 , 主线程收到 REFRESH_EVENT 事件 , 就会刷新界面
// 播放完毕后 主线程 收到 QUIT_EVENT 事件 , 就会停止播放
// 本函数中设置 每 40ms 刷新一次 , 一秒刷新 25 帧 , 25 FPS
int refresh_video_timer(void *data)
{
while (!s_thread_exit) // 当未请求退出时
{
SDL_Event event; // 创建事件
event.type = REFRESH_EVENT; // 设置事件类型为画面刷新
// 将自定义的 画面刷新事件 推送事件到事件队列
SDL_PushEvent(&event);
SDL_Delay(40); // 延时40毫秒
}
s_thread_exit = 0; // 退出标志重置为0
// 推送退出事件
SDL_Event event;
event.type = QUIT_EVENT; // 设置事件类型为退出
SDL_PushEvent(&event); // 推送事件到事件队列
return 0;
}
在上述 refresh_video_timer 函数中 , 设置 每 40ms 刷新一次 , 一秒刷新 25 帧 , 也就是 帧率为 25 FPS ;
5、主线程事件处理
在主线程中的 while (1) 主循环中 , 执行本程序的核心操作 ;
- 主线程 收到 REFRESH_EVENT 自定义事件 , 就会执行画面刷新事件 ;
- 主线程 收到 SDL_WINDOWEVENT 事件 , 就会执行 WIndows 的窗口事件 , 该事件一般由用户触发 , 如 : 窗口的 最大化 / 最小化 / 关闭 操作 ;
- 主线程 收到 SDL_QUIT 事件 , 此时 会设置 s_thread_exit 标志位为 true , 子线程中使用该标志位作为视频退出标志 ;
- 主线程 首都奥 QUIT_EVENT 自定义事件 , 就会退出 主循环 ;
主循环部分代码示例 :
代码语言:javascript复制 // 在下面 主循环 中 , 不断刷新 YUV 画面数据
while (1) // 主循环
{
SDL_WaitEvent(&event); // 等待事件发生
if(event.type == REFRESH_EVENT) // 如果是画面刷新事件
{// 省略部分代码
// ...
// 更新纹理数据
SDL_UpdateTexture(texture, NULL, video_buf, video_width);
// ...
// 清除当前显示
SDL_RenderClear(renderer);
// 将纹理绘制到渲染器上
SDL_RenderCopy(renderer, texture, NULL, &rect);
// 更新显示
SDL_RenderPresent(renderer);
}
else if(event.type == SDL_WINDOWEVENT) // 如果是窗口事件
{
// 如果窗口尺寸改变
SDL_GetWindowSize(window, &win_width, &win_height); // 获取窗口尺寸
printf("SDL_WINDOWEVENT win_width:%d, win_height:%dn",win_width, win_height); // 输出新尺寸
}
else if(event.type == SDL_QUIT) // 如果是退出事件 , SDL_QUIT 是标准退出事件
{
s_thread_exit = 1; // 设置退出标志
}
else if(event.type == QUIT_EVENT) // 自定义退出事件
{
break; // 退出主循环
}