文章目录

  • 一、SDL 播放 YUV 视频
  • 1、前置知识回顾
  • 2、SDL 播放 YUV 画面流程
  • 3、YUV 视频存放位置
  • 4、刷新控制子线程
  • 5、主线程事件处理


博客源码下载 : 本博客暂无可执行代码 ;






一、SDL 播放 YUV 视频




1、前置知识回顾



在 【FFmpeg】SDL 音视频开发 ① ( SDL 窗口绘制 | SDL 视频显示函数 | SDL_Window 窗口 | SDL_Renderer 渲染器 | SDL_Texture 纹理 ) 博客中 , 介绍了

  • SDL_Init
  • SDL_CreateWindow
  • SDL_CreateRenderer
  • SDL_CreateTexture
  • SDL_Quit

的用法 , 这些函数中 , 前四个函数是显示 视频画面 前的准备工作 , 最后一个 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_Render 渲染器对象 : 调用 SDL_CreateRender 函数
  • 创建 SDL_Texture 纹理对象 : 调用 SDL_CreateTexture
  • SDL_Window 窗口 / SDL_Render 渲染器 / SDL_Texture 纹理 关系 :
  • 渲染器 需要 在 窗口 上绘制
  • 渲染器 需要调用 SDL_SetRenderTarget 函数 设置 渲染目标 , 渲染目标 是 纹理对象 ;
  • 更新纹理 : 调用 SDL_UpdateTexture
  • 清除渲染器纹理 : 调用 SDL_RenderClear
  • 拷贝渲染器纹理 : 调用 SDL_RenderCopy
  • 渲染纹理 : 调用 SDL_RenderPresent

【FFmpeg】SDL 音视频开发 ⑤ ( SDL 播放 YUV 视频 | SDL 播放 YUV 画面流程 | YUV 视频存放位置 | 刷新控制子线程 | 主线程事件处理 )_YUV



3、YUV 视频存放位置



代码编译后 , SDL_Demo 工程会在本地生成 编译 后的可执行文件目录 build-SDL_Demo-Desktop_Qt_5_14_2_MSVC2015_32bit-Debug ,

【FFmpeg】SDL 音视频开发 ⑤ ( SDL 播放 YUV 视频 | SDL 播放 YUV 画面流程 | YUV 视频存放位置 | 刷新控制子线程 | 主线程事件处理 )_音视频_02

将要播放的 YUV 420P 格式的 视频文件 , 拷贝到这个 build-SDL_Demo-Desktop_Qt_5_14_2_MSVC2015_32bit-Debug 目录中 ,

将 视频文件 拷贝到根目录即可 ;

【FFmpeg】SDL 音视频开发 ⑤ ( SDL 播放 YUV 视频 | SDL 播放 YUV 画面流程 | YUV 视频存放位置 | 刷新控制子线程 | 主线程事件处理 )_音视频_03



4、刷新控制子线程



使用 SDL 播放 YUV 视频时 , 视频画面刷新是在 主线程 中执行的 ;

此处专门开启了一个子线程 , 用于控制 YUV 画面的刷新 ;

在下面的代码中 , 开启了子线程 , 子线程中执行 refresh_video_timer 函数 , 然后再启动主线程 , 主线程直接无限循环执行 , 每次执行时 都要接收 子线程 中传递的事件 , 受子线程控制 ;

// 创建 YUV 画面 刷新线程 , 该线程与主线程 并行执行
    timer_thread = SDL_CreateThread(refresh_video_timer, NULL, NULL); // 创建刷新线程

    // 在下面 主循环 中 , 不断刷新 YUV 画面数据
    while (1)  // 主循环
    {

子线程 执行的 refresh_video_timer 函数内容如下 , 该函数用于 在子线程 中 控制画面的刷新速度 , 子线程 中 向主线程发送 刷新事件 , 主线程收到 REFRESH_EVENT 事件 , 就会刷新界面 ;

YUV 视频 播放完毕后 子线程会向主线程发送 QUIT_EVENT 事件 , 主线程 收到 QUIT_EVENT 事件 , 就会停止播放 ;

// 该函数用于 在子线程 中 控制画面的刷新速度
// 子线程 中 向主线程发送 刷新事件 , 主线程收到 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 自定义事件 , 就会退出 主循环 ;


主循环部分代码示例 :

// 在下面 主循环 中 , 不断刷新 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:%d\n",win_width, win_height); // 输出新尺寸
        }
        else if(event.type == SDL_QUIT) // 如果是退出事件 , SDL_QUIT 是标准退出事件
        {
            s_thread_exit = 1; // 设置退出标志
        }
        else if(event.type == QUIT_EVENT) // 自定义退出事件
        {
            break; // 退出主循环
        }