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做测试
  •  

二、遇到的问题

  1. 花屏问题

    1. 原因是打开的文件不是YUV420P格式的
  2.  

     花屏问题2。(纹理的对齐方式不对)

     SDL_UpdateTexture(sdlTexture, NULL, data, width*1.5);应该是SDL_UpdateTexture(sdlTexture, NULL, data, width);

  3. Qt窗口关闭时析构函数不执行。在窗口构造方法中加入下面这句即可
    setAttribute(Qt::WA_DeleteOnClose, true);  // 关闭时自动销毁窗口
  4. 一旦使用了第3条,则Qt在打开窗口时每次都要new 窗口并show()。因为窗口销毁后需要重新创建

  5. 捕获窗口关闭事件的方法
    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() << "窗口已关闭";
}

 

posted on 2025-05-13 15:33  飘杨......  阅读(77)  评论(0)    收藏  举报