前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【FFmpeg】SDL 音视频开发 ⑥ ( SDL 播放 YUV 视频 | YUV 4:2:0 采样 | YUV420P 格式介绍 | 获取 YUV 视频文件 | 读取并加载 YUV 画面数据 )

【FFmpeg】SDL 音视频开发 ⑥ ( SDL 播放 YUV 视频 | YUV 4:2:0 采样 | YUV420P 格式介绍 | 获取 YUV 视频文件 | 读取并加载 YUV 画面数据 )

作者头像
韩曙亮
发布2024-09-06 12:18:37
1120
发布2024-09-06 12:18:37
举报
文章被收录于专栏:韩曙亮的移动开发专栏

博客源码下载 : https://download.csdn.net/download/han1202012/89717218 ;

一、SDL 播放 YUV 视频

1、YUV 4:2:0 采样

【音视频原理】图像相关概念 ④ ( YUV 排列格式 | 打包格式 | 平面格式 | YUV 表示法 | YUV 采样格式表示方法 | YUV 4:2:2 采样 | 上采样与下采样概念 ) 博客中 的 YUV 4:2:0 采样 章节 , 介绍了 YUV420 格式的采样详情 ;

YUV420 格式的 视频中 , 4 个 Y 灰度值 分量 , 才会有一个 UV 色度值 分量 对应 ; 也就是说 四个 Y 灰度值 使用 相同的 UV 色度值 进行编码显示 ;

下图展示的是 YUV 4:2:0 采样的示意图 , 四个 Y 灰度值 分量 , 对应这 一个 UV 色度值 分量 ;

YUV420 采样 , 存储时 , 水平方向进行下采样 , 垂直方向也进行了下采样 , 数据进行了压缩 , YUV 的比例是 4:1:1 , 即 4 和 Y 分量 对应 1 个 UV 分量 ;

2、YUV420P 格式介绍

【音视频原理】图像相关概念 ⑤ ( YUV 数据存储 | I444 格式说明 | I422 格式说明 | I420 格式说明 | NV12 格式说明 | NV21 格式说明 ) 博客中 , 讲解了 YUV420P 格式的具体像素编码排列 ;

YUV420P 数据存储 格式如下图所示 :

不同类型的分量放在不同的数组中 ,

  • Y 灰度值 分量 , 存储在 最上面的数组中 , 在下图的 Y0 ~ Y7 的 灰度值 就是存放在一个数组中 ;
  • U 色度值 分量 , 存储在 中间数组中 , 在下图的 U0 ~ U3 的 色度值 就是存放在一个数组中 , U 的个数只有 4 个 , 是 Y 分量个数的一半 ;
  • V 色度值 分量 , 存储在 最下面的数组中 , 在下图的 V0 ~ V3 的 色度值 就是存放在一个数组中 , V 分量 的个数只有 4 个 , 是 Y 分量 个数的一半 ;

上面的 数据 中 ,

  • Y0 , Y1 , Y4 , Y5 灰度值 使用 U0V0 色度值 , 4 个像素用了 6 字节 , 一个像素 1.5 字节 ;
  • Y2 , Y3 , Y6 , Y7 灰度值 使用 U1V1 色度值 , 4 个像素用了 6 字节 , 一个像素 1.5 字节 ;
  • Y8 , Y9 , Y12 , Y13 灰度值 使用 U2V2 色度值 , 4 个像素用了 6 字节 , 一个像素 1.5 字节 ;
  • Y10 , Y11 , Y14 , Y15 灰度值 使用 U3V3 色度值 , 4 个像素用了 6 字节 , 一个像素 1.5 字节 ;

3、获取 YUV 视频文件

使用 如下命令 , 将 H.264 格式的 视频文件 转为 YUV 格式的文件 ;

代码语言:javascript
复制
ffmpeg -i input.mp4 -pix_fmt yuv420p output.yuv

上述命令中 -pix_fmt yuv420p 参数的作用是

该 YUV 视频的 画面分辨率是 848x480 ;

这里特别注意 , YUV 视频是 未经压缩的 视频格式 , mp4 格式的视频有 59.3MB , YUV 格式的视频有 1.12GB ;

4、读取 YUV 画面数据

YUV 画面中 , 一个 UV 颜色值 分量 对应 4 个 Y 灰度值 分量 ;

一张画面帧中 , 有 video_width * video_height 个像素点 ,

  • Y 灰度值 分量 有 video_width * video_height 字节 , 则 UV 分量是这个大小的 1/4 ;
  • UV 颜色值 分量 各有 video_width * video_height / 4 字节大小 ;
代码语言:javascript
复制
    // YUV 格式相关长度计算
    //  Y 分量 是 灰度值分量 , UV 分量 是 颜色值分量
    //  4 个 Y 灰度值分量 对应 1 个 UV 颜色值分量
    uint32_t y_frame_len = video_width * video_height;      // Y分量长度
    uint32_t u_frame_len = video_width * video_height / 4;  // U分量长度
    uint32_t v_frame_len = video_width * video_height / 4;  // V分量长度

这样可以计算出 YUV420P 格式中 每张画面的大小 ;

代码语言:javascript
复制
    uint32_t yuv_frame_len = y_frame_len + u_frame_len + v_frame_len; // 总长度

数据准备部分代码 :

代码语言:javascript
复制
    // YUV文件句柄
    FILE *video_fd = NULL;   // 文件指针 , 用于读取 YUV 视频文件路径
    const char *yuv_path = "yuv420p_848x480.yuv"; // YUV文件路径 , 这是一个相对路径

    // 设置 视频缓冲区长度 读取文件时 每次读取多少字节的数据
    size_t video_buff_len = 0;

    // 视频数据缓冲区
    // 读取的 YUV 视频数据存储在该缓冲区中
    uint8_t *video_buf = NULL;

    // YUV 格式相关长度计算
    //  Y 分量 是 灰度值分量 , UV 分量 是 颜色值分量
    //  4 个 Y 灰度值分量 对应 1 个 UV 颜色值分量
    uint32_t y_frame_len = video_width * video_height;      // Y分量长度
    uint32_t u_frame_len = video_width * video_height / 4;  // U分量长度
    uint32_t v_frame_len = video_width * video_height / 4;  // V分量长度
    uint32_t yuv_frame_len = y_frame_len + u_frame_len + v_frame_len; // 总长度

5、加载 YUV 视频数据

首先 , 使用 malloc 为 YUV 缓存空间分配堆内存 , 这个缓冲空间正好可以存放 一帧画面帧的数据 ;

代码语言:javascript
复制
    // 分配 YUV 视频数据 缓冲区空间
    video_buf = (uint8_t*)malloc(yuv_frame_len); // 分配YUV帧的内存
    if(!video_buf)  // 如果分配失败
    {
        fprintf(stderr, "Failed to alloce yuv frame space!\n"); // 输出错误信息
        goto _FAIL;  // 跳转到失败处理
    }

然后 , 打开 YUV 文件 ;

代码语言:javascript
复制
    // 打开YUV文件
    video_fd = fopen(yuv_path, "rb");  // 以只读方式打开文件
    if( !video_fd )  // 如果打开失败
    {
        fprintf(stderr, "Failed to open yuv file\n"); // 输出错误信息
        goto _FAIL;  // 跳转到失败处理
    }

最后 , 每次刷新画面时 , 从 YUV 视频文件中 , 读取一帧画面数据 , 然后更新到 SDL_Texture 纹理数据中 ;

代码语言:javascript
复制
            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;  // 跳转到失败处理
            }
            // 更新纹理数据
            SDL_UpdateTexture(texture, NULL, video_buf, video_width);

二、完整代码示例


1、代码示例

代码语言:javascript
复制
#include <stdio.h>  // 引入标准输入输出库
#include <string.h> // 引入字符串处理库

#include <SDL.h>    // 引入SDL库

// 自定义消息类型
// 画面刷新事件 , 每秒刷新的次数又称为 FPS , 使用 SDL 现成控制画面帧刷新
#define REFRESH_EVENT   (SDL_USEREVENT + 1)
// 退出事件 , 在 main 函数中的主循环中 , 不停地在循环刷新视频画面 ,
#define QUIT_EVENT      (SDL_USEREVENT + 2)

// 定义分辨率
#define YUV_WIDTH   848   // YUV视频宽度
#define YUV_HEIGHT  480   // YUV视频高度
#define YUV_FORMAT  SDL_PIXELFORMAT_IYUV // YUV格式

// 退出标志,非0值表示退出 , 在 refresh_video_timer 函数中使用该标志位作为循环判定条件
int s_thread_exit = 0;

// 该函数用于 在子线程 中 控制画面的刷新速度
// 子线程 中 向主线程发送 刷新事件 , 主线程收到 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;
}
#undef main  // 取消主函数宏定义
int main(int argc, char* argv[])
{
    // 初始化 SDL
    if(SDL_Init(SDL_INIT_VIDEO))  // 初始化SDL视频模块
    {
        fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());  // 输出错误信息
        return -1;  // 返回错误码
    }

    // SDL相关变量初始化
    SDL_Event event;                    // SDL 事件
    SDL_Rect rect;                      // 矩形区域
    SDL_Window *window = NULL;          // SDL 窗口
    SDL_Renderer *renderer = NULL;      // SDL 渲染器
    SDL_Texture *texture = NULL;        // SDL 纹理
    SDL_Thread *timer_thread = NULL;    // 刷新线程
    uint32_t pixformat = YUV_FORMAT;    // YUV格式

    // YUV 视频 的 分辨率设置
    int video_width = YUV_WIDTH;        // 视频宽度
    int video_height = YUV_HEIGHT;      // 视频高度

    // SDL 播放窗口 分辨率设置
    int win_width = YUV_WIDTH;          // 窗口宽度
    int win_height = YUV_HEIGHT;        // 窗口高度

    // YUV文件句柄
    FILE *video_fd = NULL;   // 文件指针 , 用于读取 YUV 视频文件路径
    const char *yuv_path = "yuv420p_848x480.yuv"; // YUV文件路径 , 这是一个相对路径

    // 设置 视频缓冲区长度 读取文件时 每次读取多少字节的数据
    size_t video_buff_len = 0;

    // 视频数据缓冲区
    // 读取的 YUV 视频数据存储在该缓冲区中
    uint8_t *video_buf = NULL;

    // YUV 格式相关长度计算
    //  Y 分量 是 灰度值分量 , UV 分量 是 颜色值分量
    //  4 个 Y 灰度值分量 对应 1 个 UV 颜色值分量
    uint32_t y_frame_len = video_width * video_height;      // Y分量长度
    uint32_t u_frame_len = video_width * video_height / 4;  // U分量长度
    uint32_t v_frame_len = video_width * video_height / 4;  // V分量长度
    uint32_t yuv_frame_len = y_frame_len + u_frame_len + v_frame_len; // 总长度

    // 创建窗口
    window = SDL_CreateWindow("Simplest YUV Player",  // 窗口标题
                           SDL_WINDOWPOS_UNDEFINED,  // 窗口x坐标
                           SDL_WINDOWPOS_UNDEFINED,  // 窗口y坐标
                           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); // 创建纹理

    // 分配 YUV 视频数据 缓冲区空间
    video_buf = (uint8_t*)malloc(yuv_frame_len); // 分配YUV帧的内存
    if(!video_buf)  // 如果分配失败
    {
        fprintf(stderr, "Failed to alloce yuv frame space!\n"); // 输出错误信息
        goto _FAIL;  // 跳转到失败处理
    }

    // 打开YUV文件
    video_fd = fopen(yuv_path, "rb");  // 以只读方式打开文件
    if( !video_fd )  // 如果打开失败
    {
        fprintf(stderr, "Failed to open yuv file\n"); // 输出错误信息
        goto _FAIL;  // 跳转到失败处理
    }
    // 创建 YUV 画面 刷新线程 , 该线程与主线程 并行执行
    timer_thread = SDL_CreateThread(refresh_video_timer, NULL, NULL); // 创建刷新线程

    // 在下面 主循环 中 , 不断刷新 YUV 画面数据
    while (1)  // 主循环
    {
        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;  // 跳转到失败处理
            }
            // 更新纹理数据
            SDL_UpdateTexture(texture, NULL, video_buf, video_width);

            // 设置显示区域
            rect.x = 0;  // 区域左上角x坐标
            rect.y = 0;  // 区域左上角y坐标
            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(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; // 退出主循环
        }
    }

_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();  // 退出SDL

    return 0;  // 返回成功
}

2、执行效果

运行上述程序 , 效果如下 :

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-09-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、SDL 播放 YUV 视频
    • 1、YUV 4:2:0 采样
      • 2、YUV420P 格式介绍
        • 3、获取 YUV 视频文件
          • 4、读取 YUV 画面数据
            • 5、加载 YUV 视频数据
            • 二、完整代码示例
              • 1、代码示例
                • 2、执行效果
                相关产品与服务
                数据保险箱
                数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档