SDL将YUV视频渲染到Qt窗口播放
一、概述
在多媒体开发中,使用 SDL 将 YUV 视频渲染到 Qt 窗口实现播放是常见需求。SDL 提供强大的底层音视频处理能力,Qt 则擅长构建用户界面。
通过初始化 SDL 视频子系统,解析 YUV 数据格式,结合 Qt 的窗口机制,将 YUV 帧转换为 SDL 纹理,再利用 SDL_Render 将纹理绘制到 Qt 窗口上,实现流畅播放。
本篇就结合SDL2+QT实现YUV文件播放的两种方式。
主要步骤如下:
- 生成yuv420p文件
ffmpeg -i original1080.mp4 -s 400x300 -pix_fmt yuv420p output400_300.yuv
- 测试生成的yuv420p文件是否正常
ffplay -f rawvideo -video_size 400x300 -pixel_format yuv420p -framerate 30 output400_300.yuv
- 编写SDL渲染工具类
- 初始化SDL_Init
- 创建窗口SDL_CreateWindowFrom
- 创建渲染器SDL_CreateRenderer
- 创建纹理/材质SDL_CreateTexture
- 更新纹理SDL_UpdateTexture/SDL_UpdateYUVTexture
- 清理屏幕SDL_RenderClear
- 复制材质或纹理到渲染器SDL_RenderCopy
- 渲染SDL_RenderPresent
- 编写读取yuv420p文件的工具类
- 单独创建一个thread做测试
-
二、遇到的问题
- 花屏问题
- 原因是打开的文件不是YUV420P格式的
-
花屏问题2。(纹理的对齐方式不对)
SDL_UpdateTexture(sdlTexture, NULL, data, width*1.5);应该是SDL_UpdateTexture(sdlTexture, NULL, data, width);
- Qt窗口关闭时析构函数不执行。在窗口构造方法中加入下面这句即可
setAttribute(Qt::WA_DeleteOnClose, true); // 关闭时自动销毁窗口
-
一旦使用了第3条,则Qt在打开窗口时每次都要new 窗口并show()。因为窗口销毁后需要重新创建
- 捕获窗口关闭事件的方法
protected: // 重写关闭事件处理函数 void closeEvent(QCloseEvent* event) override;
三、代码示例
1.读取YUV文件数据。源码
/// <summary> /// 读取yuv数据 /// </summary> /// <param name="data"></param> /// <returns></returns> bool ReadYUV420P(char* yuv) { int dataSize = width * height * 1.5; in.read(yuv, dataSize); if (in.gcount() == 0) { // 未读取任何字节:可能已到文件结尾或出现错误 if (in.eof()) { qDebug() << "已到达文件结尾"; return false; } else if (in.fail()) { qDebug() << "读取失败(非EOF原因)"; return false; } } return true; }
2.SDLRenderUtil.h类,次类中有读取RGB24文件的方法,也有读取YUVI420文件的方法。重点看标红区域。需要理解的是不管是使用SDL_UpdateTexture方法或者SDL_UpdateYUVTexture方法更新纹理,都是可行的(亲测)。
#pragma #include <SDL.h> #include <QDebug> class SDLRenderUtil { private: SDL_Window* sdlWindow; SDL_Renderer* sdlRender; SDL_Texture* sdlTexture; public: int width = 400; int height = 300; void* winId; int PIX_FMT = 0; public: void Init() { Close(); //初始化SDL库 if (SDL_Init(SDL_INIT_VIDEO)) { qDebug() << "SDL初始化失败->" << SDL_GetError(); return; } //2 生成SDL 窗口 if (winId) { sdlWindow = SDL_CreateWindowFrom(winId); } else { sdlWindow = SDL_CreateWindow("SDL窗口", SDL_WINDOWPOS_CENTERED,//窗口位置 SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE ); } if (!sdlWindow) { qDebug() << "SDL窗口创建失败->" << SDL_GetError(); return; } //3 生成渲染器 sdlRender = SDL_CreateRenderer(sdlWindow, -1, SDL_RENDERER_ACCELERATED); if (!sdlRender) { qDebug() << "SDL渲染器创建失败->" << SDL_GetError(); return; } int pix_fmt_type = PIX_FMT == 0 ? SDL_PIXELFORMAT_RGB24 : SDL_PIXELFORMAT_IYUV; //4 生成材质 //SDL_PIXELFORMAT_ARGB8888 sdlTexture = SDL_CreateTexture(sdlRender, pix_fmt_type, SDL_TEXTUREACCESS_STREAMING,// 可加锁 width, height ); if (!sdlTexture) { qDebug() << "SDL材质创建失败->" << SDL_GetError() << endl; return; } } void Render(char* data) { //5 内存数据写入材质 if (PIX_FMT == 0) { SDL_UpdateTexture(sdlTexture, NULL, data, width * 3); } else { /* // 提取 Y、U、V 平面数据(关键!) size_t y_size = width * height; size_t uv_size = (width / 2) * (height / 2); uint8_t* y_data = (uint8_t*)data; uint8_t* u_data = y_data + y_size; uint8_t* v_data = u_data + uv_size; // 使用 SDL_UpdateYUVTexture 更新纹理 SDL_UpdateYUVTexture( sdlTexture, NULL, // 更新全区域 y_data, // Y平面指针 width, // Y步长 = 宽度(无填充) u_data, // U平面指针 width / 2, // U步长 = 宽度/2(无填充) v_data, // V平面指针 width / 2 // V步长 = 宽度/2(无填充) ); */ SDL_UpdateTexture(sdlTexture, NULL, data, width); } //6 清理屏幕 SDL_RenderClear(sdlRender); SDL_Rect sdl_rect; sdl_rect.x = 0; sdl_rect.y = 0; sdl_rect.w = width; sdl_rect.h = height; //7 复制材质到渲染器 SDL_RenderCopy(sdlRender, sdlTexture, NULL,//原图位置和尺寸 &sdl_rect//目标位置和尺寸 ); //8 渲染 SDL_RenderPresent(sdlRender); } //初始化SDL事件 bool isEventQuit() { //判断退出 SDL_Event ev; SDL_WaitEventTimeout(&ev, 10); if (ev.type == SDL_QUIT) { Close(); return true; } return false; } void Close() { if (sdlTexture) { SDL_DestroyTexture(sdlTexture); sdlTexture = NULL; } if (sdlRender) { SDL_DestroyRenderer(sdlRender); sdlRender = NULL; } if (sdlWindow) { SDL_DestroyWindow(sdlWindow); sdlWindow = NULL; } } static SDLRenderUtil* Get() { static SDLRenderUtil sdlRenderUtil; return &sdlRenderUtil; } };
3.开始使用
void SDLRenderYUVWindow::choiceYUV420PFile() { mWidth = ui.labelVideo->width(); mHeight = ui.labelVideo->height(); QString filter = "RGB 文件 (*.*);;所有文件 (*.*)"; // 打开文件选择对话框,选择单个文件 QString filePath = QFileDialog::getOpenFileName(this, "选择 RGB 文件", "", filter); BinaryFileReader::Get()->filename = filePath.toStdString(); BinaryFileReader::Get()->width = mWidth; BinaryFileReader::Get()->height = mHeight; BinaryFileReader::Get()->Init(); SDLRenderUtil::Get()->width = mWidth; SDLRenderUtil::Get()->height = mHeight; SDLRenderUtil::Get()->winId = (void*)ui.labelVideo->winId(); SDLRenderUtil::Get()->PIX_FMT = 1; SDLRenderUtil::Get()->Init(); // 创建并启动后台线程 renderThread = std::make_unique<std::thread>(&SDLRenderYUVWindow::RenderVideo, this); /*renderThread([=] { char* data = new char[mWidth * mHeight * 1.5]; for (;;) { SDLRenderUtil::Get()->isEventQuit(); bool ret = BinaryFileReader::Get()->ReadYUV420P(data); if (!ret) { break; } SDLRenderUtil::Get()->Render(data); std::this_thread::sleep_for(33ms); } delete data; });*/ //renderThread.detach();//线程分离 //renderThread.join();//等待子线程结束 } void SDLRenderYUVWindow::RenderVideo() { char* data = new char[mWidth * mHeight * 1.5]; for (;;) { if (!isRunning)break; SDLRenderUtil::Get()->isEventQuit(); bool ret = BinaryFileReader::Get()->ReadYUV420P(data); if (!ret) { break; } SDLRenderUtil::Get()->Render(data); std::this_thread::sleep_for(33ms); } if (data) { delete data; } qDebug() << "已释放了data内存"; } void SDLRenderYUVWindow::closeEvent(QCloseEvent* event) { isRunning = false; // 等待线程结束 if (renderThread && renderThread->joinable()) { renderThread->join(); } BinaryFileReader::Get()->Close(); SDLRenderUtil::Get()->Close(); event->accept(); // 接受关闭事件(允许关闭) qDebug() << "窗口已关闭"; }