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

2024-09-06 12:18:37 浏览数 (1)

博客源码下载 : 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 filen"); // 输出错误信息
        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 - %sn", 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:%sn",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 filen"); // 输出错误信息
        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:%dn",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、执行效果

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

0 人点赞