博客源码下载 : https://download.csdn.net/download/han1202012/89717218 ;
在 【音视频原理】图像相关概念 ④ ( 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 分量 ;
在 【音视频原理】图像相关概念 ⑤ ( YUV 数据存储 | I444 格式说明 | I422 格式说明 | I420 格式说明 | NV12 格式说明 | NV21 格式说明 ) 博客中 , 讲解了 YUV420P 格式的具体像素编码排列 ;
YUV420P 数据存储 格式如下图所示 :
不同类型的分量放在不同的数组中 ,
上面的 数据 中 ,
使用 如下命令 , 将 H.264 格式的 视频文件 转为 YUV 格式的文件 ;
ffmpeg -i input.mp4 -pix_fmt yuv420p output.yuv
上述命令中 -pix_fmt yuv420p
参数的作用是
该 YUV 视频的 画面分辨率是 848x480 ;
这里特别注意 , YUV 视频是 未经压缩的 视频格式 , mp4 格式的视频有 59.3MB , YUV 格式的视频有 1.12GB ;
YUV 画面中 , 一个 UV 颜色值 分量 对应 4 个 Y 灰度值 分量 ;
一张画面帧中 , 有 video_width * video_height 个像素点 ,
// 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 格式中 每张画面的大小 ;
uint32_t yuv_frame_len = y_frame_len + u_frame_len + v_frame_len; // 总长度
数据准备部分代码 :
// 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; // 总长度
首先 , 使用 malloc 为 YUV 缓存空间分配堆内存 , 这个缓冲空间正好可以存放 一帧画面帧的数据 ;
// 分配 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 文件 ;
// 打开YUV文件
video_fd = fopen(yuv_path, "rb"); // 以只读方式打开文件
if( !video_fd ) // 如果打开失败
{
fprintf(stderr, "Failed to open yuv file\n"); // 输出错误信息
goto _FAIL; // 跳转到失败处理
}
最后 , 每次刷新画面时 , 从 YUV 视频文件中 , 读取一帧画面数据 , 然后更新到 SDL_Texture 纹理数据中 ;
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);
#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; // 返回成功
}
运行上述程序 , 效果如下 :