美文网首页
使用SDL显示YUV

使用SDL显示YUV

作者: 村口大白杨 | 来源:发表于2021-04-06 10:12 被阅读0次

    前面写了如何使用 SDL 显示一张 BMP 图片,但这并不是我们最终的目的。我们最终的目的是使用 SDL 显示 YUV 数据。下面将演示如何使用 SDL 显示一张 YUV 格式的图片和一段 YUV 格式的视频。

    一、SDL 显示 YUV 图片:

    显示 YUV 图片和显示 BMP 图片的大致流程是一样的。显示 BMP 图片我们可以直接获取到 BMP 图片的 surface,然后直接从 surface 创建纹理。显示 YUV 格式的图片,我们需要先创建一个对应像素格式的空白纹理,然后读取 YUV 数据,再把 YUV 数据更新到纹理上面。

    Example 使用 Qt 工程开发,首先保证本地安装了 SDL2,我本地的 SDL2 安装路径是 /usr/local/Cellar/sdl2,首先在 .pro 文件中导入 SDL2:

    INCLUDEPATH += /usr/local/Cellar/sdl2/2.0.14_1/include
    LIBS += -L/usr/local/Cellar/sdl2/2.0.14_1/lib -lSDL2
    

    然后引入头文件:

    #include <SDL2/SDL.h>
    

    SDL2 是一个纯 C 语音的库,我们在 C++ 代码中调用不需要加 extern “C”,是因为 SDL 内部做了判断,如果是 C++ 环境自动帮我们添加 extern “C”

    1、初始化 Video 子系统

    SDL_Init(SDL_INIT_VIDEO);
    

    我们的目的是显示 YUV 图片,所以只初始化 SDL_INIT_VIDEO 子系统就可以。

    2、创建窗口

    window = SDL_CreateWindow(
                // 窗口的标题
                "Display YUV",
                // 窗口的 x 坐标(SDL_WINDOWPOS_UNDEFINED:不指定 SDL_WINDOWPOS_CENTERED:中间)
                SDL_WINDOWPOS_UNDEFINED,
                // 窗口的 y 坐标
                SDL_WINDOWPOS_UNDEFINED,
                // 窗口的宽度,以像素为单位
                surface->w,
                // 窗口的高度,以像素为单位
                surface->h,
                // SDL_WindowFlags 枚举,设置窗口显示样式效果
                SDL_WINDOW_SHOWN
             );
    

    3、创建渲染上下文

    renderer = SDL_CreateRenderer(window, 
                                  // 要初始化的渲染设备的索引,设置 -1 则初始化第一个支持 flags 的设备
                                  -1, 
                                  // SDL_RendererFlags
                                  SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
    if (!renderer) {
        // 有可能设备不支持硬件加速
        renderer = SDL_CreateRenderer(window, -1, 0)
        if (!renderer) {
            qDebug() << "SDL_CreateRenderer Error:" << SDL_GetError();
            goto end;
        }
    }
    

    4、创建空白纹理

    texture = SDL_CreateTexture(renderer,  // 渲染上下文
                                SDL_PIXELFORMAT_IYUV,  // 很关键的参数,显示的像素数据格式,我们显示的 YUV 图片像素格式是 yuv420p,其实 SDL_PIXELFORMAT_IYUV 就是 yuv420p 像素格式
                                SDL_TEXTUREACCESS_STATIC, // 之前我们把同一个 texture 在窗口绘制多次时,我们设置的是 SDL_TEXTUREACCESS_TARGET,这里我们设置 SDL_TEXTUREACCESS_STATIC,当然设置成 SDL_TEXTUREACCESS_STREAMING 也可以
                                512,  // 纹理的宽度
                                512); // 纹理的高度
    if (!texture) {
           qDebug() << "SDL_CreateTexture Error:" << SDL_GetError();
           goto end;
    }
    

    此时我们仅仅是创建了一个 yuv420p 像素格式的空白纹理,其上面并没有像素格式的数据。所以后面需要加载 YUV 数据,把 YUV 格式像素数据加载到纹理上面。PS:和加载 BMP 图片比较,加载 YUV 数据构建纹理的过程发生了变化,加载 BMP 图片我们使用的是 SDL_CreateTextureFromSurface,加载 YUV 我们先创建了一个空的纹理,重要的是一定要设置好像素格式,以便后面能够正确解析我们的 YUV 数据。

    /**
     *  \brief The access pattern allowed for a texture.
     */
    typedef enum
    {
        SDL_TEXTUREACCESS_STATIC,    /**< 静态(图片) */
        SDL_TEXTUREACCESS_STREAMING, /**< 数据流(视频) */
        SDL_TEXTUREACCESS_TARGET     /**< 纹理可以作为渲染目标使用,比如我们需要把同一个图形在 window 中绘制多次。我们可以创建一个纹理并设置成 Target,把图形绘制到此纹理上,然后设置 Target 为 window,再把纹理拷贝到 window(可多次拷贝) */
    } SDL_TextureAccess;
    

    5、打开文件,将 YUV 数据填充到纹理

    // 将 YUV 数据填充到 texture
    if (!file.open(QFile::ReadOnly)) {
        qDebug() << "open file failure:" << FILE_NAME;
        goto end;
    }
    
    // 需要把yuv文件数据加载进内存,pixels指向内存中的yuv数据
    // 读取所有文件数据到内存中
    SDL_UpdateTexture(texture, // 前面创建的空白纹理
                      nullptr, // 更新像素的矩形区域,设置为 nullptr 更新整个纹理区域
                      file.readAll().data(), // 原始像素数据
                      512); // 一行像素数据的字节数,这里传图片宽度即可
    

    6、复制纹理到渲染目标

    ret = SDL_RenderCopy(renderer, texture, nullptr, nullptr);
    

    7、更新所有的渲染操作到屏幕上

    SDL_RenderPresent(renderer);
    

    8、释放资源

    file.close();
    SDL_DestroyTexture(texture);
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    

    示例代码:

    #include "mainwindow.h"
    #include "ui_mainwindow.h"
    
    #include <SDL2/SDL.h>
    #include <QDebug>
    #include <QFile>
    
    #define FILE_NAME "/Users/mac/Downloads/pic/out1.yuv"
    #define IMAGE_WIDTH 512
    #define IMAGE_HEIGHT 512
    
    MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent)
        , ui(new Ui::MainWindow)
    {
        ui->setupUi(this);
    }
    
    MainWindow::~MainWindow()
    {
        delete ui;
    }
    
    void MainWindow::on_displayButton_clicked()
    {
        // 窗口
        SDL_Window *window = nullptr;
        // 渲染上下文
        SDL_Renderer *renderer = nullptr;
        // 纹理(直接跟特定驱动程序相关的像素数据)
        SDL_Texture *texture = nullptr;
        // 事件
        SDL_Event event;
    
        QFile file(FILE_NAME);
    
        // 返回值
        int ret = 0;
    
        // 初始化Video子系统
        if (SDL_Init(SDL_INIT_VIDEO)) {
            qDebug() << "SDL_Init Error:" << SDL_GetError();
            return;
        }
    
        // 创建窗口
        window = SDL_CreateWindow("Display YUV", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, IMAGE_WIDTH, IMAGE_HEIGHT, SDL_WINDOW_SHOWN);
        if (!window) {
            qDebug() << "SDL_CreateWindow Error:" << SDL_GetError();
            goto end;
        }
    
        // 创建渲染上下文
        renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
        if (!renderer) {
            qDebug() << "SDL_CreateRenderer Error:" << SDL_GetError();
            goto end;
        }
    
        // 创建纹理
        texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STATIC, IMAGE_WIDTH, IMAGE_HEIGHT);
        if (!texture) {
            qDebug() << "SDL_CreateTextureFromSurface Error:" << SDL_GetError();
            goto end;
        }
    
        // 打开文件
        if (!file.open(QFile::ReadOnly)) {
            qDebug() << "open file failure:" << FILE_NAME;
            goto end;
        }
    
        // 将 YUV 数据填充到 texture
        ret = SDL_UpdateTexture(texture, nullptr, file.readAll().data(), IMAGE_WIDTH);
        if (ret < 0) {
            qDebug() << "SDL_UpdateTexture error:" << SDL_GetError();
            goto end;
        }
    
        // 复制纹理到渲染目标(渲染目标默认是 window)
        ret = SDL_RenderCopy(renderer, texture, nullptr, nullptr);
        if (ret < 0) {
            qDebug() << "SDL_RenderCopy Error:" << SDL_GetError();
            goto end;
        }
    
        // 更新所有的渲染操作到屏幕上
        SDL_RenderPresent(renderer);
    
        while (true) {
            ret = SDL_WaitEvent(&event);
            if (ret < 0) {
                goto end;
            }
            if (event.type == SDL_QUIT) {
                break;
            }
        }
    
        // 释放资源
    end:
        SDL_DestroyTexture(texture);
        SDL_DestroyRenderer(renderer);
        SDL_DestroyWindow(window);
        SDL_Quit();
    }
    
    二、SDL 显示 YUV 视频:

    不管我们的视频是 mp4、mkv 还是 avi,播放时最终都要解码成原始数据,一般就是 YUV 格式数据。显示 YUV 视频和显示 YUV 图片的大致流程也是一样的。不同之处就是我们要循环的显示视频的每一帧像素数据。

    在按钮的事件响应中开启一个定时器, startTimerQObject 中的方法,继承自 QObject 的对象中都可以调用这个方法。 调用方法 startTimer就会开启一个定时器,并且开启成功会返回一个定时器Id,定时器调用间隔是 1000ms / 帧率

    void MainWindow::on_displayButton_clicked()
    {
        // 开启定时器
        _timerId = startTimer(1000 / 30.0);
    }
    

    定时器会不断的调用下面的方法,每次从 YUV 文件中读取一帧像素数据,这就需要我们计算出一帧像素数据的大小,yuv420p 像素格式每个像素占 1.5 字节,通过 视频宽度 * 视频高度 * 1.5 就可算出一帧像素数据大小,或者使用 FFmpeg 提供的函数 av_image_get_buffer_size(在 libavutil/imgutils.h 中),然后将读取的一帧图素数据更新到纹理,并复制纹理到渲染目标,最后更新所有的渲染操作到屏幕上,这一帧像素就显示出来了。重复相同的操作,就达到了视频播放的效果。YUV 文件数据读取完毕,要记得调用 killTimer 杀死定时器。

    void MainWindow::timerEvent(QTimerEvent *event)
    {
        // yuv420p 像素格式每个像素占 1.5 字节
        int imageSize = VIDEO_WIDTH * VIDEO_HEIGHT * 1.5;
        char data[imageSize];
        // 每次读取一帧图像
        if (_file.read(data, imageSize) > 0) {
            // 使用像素数据更新纹理
            SDL_UpdateTexture(_texture, nullptr, data, VIDEO_WIDTH);
            // 复制纹理到渲染目标
            int ret = SDL_RenderCopy(_renderer, _texture, nullptr, nullptr);
            if (ret < 0) {
                qDebug() << "SDL_RenderCopy Error:" << SDL_GetError();
                return;
            }
            // 更新所有的渲染操作到屏幕上
            SDL_RenderPresent(_renderer);
        } else {
            // 文件数据已经读取完毕
            killTimer(_timerId);
        }
    }
    

    示例代码:

    在 .pro 文件中导入 SDL2:

    INCLUDEPATH += /usr/local/Cellar/sdl2/2.0.14_1/include
    LIBS += -L/usr/local/Cellar/sdl2/2.0.14_1/lib -lSDL2
    

    mainwindow.h:

    #ifndef MAINWINDOW_H
    #define MAINWINDOW_H
    
    #include <QMainWindow>
    #include <SDL2/SDL.h>
    #include <QFile>
    
    QT_BEGIN_NAMESPACE
    namespace Ui { class MainWindow; }
    QT_END_NAMESPACE
    
    class MainWindow : public QMainWindow
    {
        Q_OBJECT
    
    public:
        MainWindow(QWidget *parent = nullptr);
        ~MainWindow();
    
    private slots:
        void on_displayButton_clicked();
    
    private:
        Ui::MainWindow *ui;
    
        // 窗口
        SDL_Window *_window = nullptr;
        // 渲染上下文
        SDL_Renderer *_renderer = nullptr;
        // 纹理(直接和特定程序相关的像素数据)
        SDL_Texture *_texture = nullptr;
        // 输入文件
        QFile _file;
        // 定时器Id
        int _timerId;
    
        void timerEvent(QTimerEvent *event);
    };
    #endif // MAINWINDOW_H
    

    mainwindow.cpp:

    #include "mainwindow.h"
    #include "ui_mainwindow.h"
    
    #include <SDL2/SDL.h>
    #include <QDebug>
    #include <QFile>
    
    #define FILE_NAME "/Users/mac/Downloads/pic/Dragon_Ball_640x480_yuv420p.yuv"
    #define VIDEO_WIDTH 640
    #define VIDEO_HEIGHT 480
    
    MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent)
        , ui(new Ui::MainWindow)
    {
        ui->setupUi(this);
    
        // 初始化Video子系统
        if (SDL_Init(SDL_INIT_VIDEO)) {
            qDebug() << "SDL_Init Error:" << SDL_GetError();
            return;
        }
    
        // 从一个已经存在的本地窗口创建 window
        _window = SDL_CreateWindow("Display YUV Video", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, VIDEO_WIDTH, VIDEO_HEIGHT, SDL_WINDOW_SHOWN);
        if (!_window) {
            qDebug() << "SDL_CreateWindow Error:" << SDL_GetError();
            return;
        }
    
        // 创建渲染上下文
        _renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
        if (!_renderer) {
            _renderer = SDL_CreateRenderer(_window, -1, 0);
            if (!_renderer) {
                qDebug() << "SDL_CreateRenderer Error:" << SDL_GetError();
                return;
            }
        }
    
        // 创建纹理 SDL_PIXELFORMAT_IYUV = yuv420p
        _texture = SDL_CreateTexture(_renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, VIDEO_WIDTH, VIDEO_HEIGHT);
        if (!_texture) {
            qDebug() << "SDL_CreateTexture Error:" << SDL_GetError();
            return;
        }
    
        _file.setFileName(FILE_NAME);
        if (!_file.open(QFile::ReadOnly)) {
            qDebug() << "open file failure:" << FILE_NAME;
        }
    }
    
    MainWindow::~MainWindow()
    {
        delete ui;
        _file.close();
        SDL_DestroyTexture(_texture);
        SDL_DestroyRenderer(_renderer);
        SDL_DestroyWindow(_window);
        SDL_Quit();
    }
    
    void MainWindow::on_displayButton_clicked()
    {
        // 开启定时器
        _timerId = startTimer(1000 / 30);
    }
    
    void MainWindow::timerEvent(QTimerEvent *event)
    {
        // yuv420p 像素格式每个像素占 1.5 字节
        int imageSize = VIDEO_WIDTH * VIDEO_HEIGHT * 1.5;
        char data[imageSize];
        // 每次读取一帧图像
        if (_file.read(data, imageSize) > 0) {
            // 使用像素数据更新纹理
            SDL_UpdateTexture(_texture, nullptr, data, VIDEO_WIDTH);
            // 复制纹理到渲染目标
            int ret = SDL_RenderCopy(_renderer, _texture, nullptr, nullptr);
            if (ret < 0) {
                qDebug() << "SDL_RenderCopy Error:" << SDL_GetError();
                return;
            }
            // 更新所有的渲染操作到屏幕上
            SDL_RenderPresent(_renderer);
        } else {
            // 文件数据已经读取完毕
            killTimer(_timerId);
        }
    }
    

    相关文章

      网友评论

          本文标题:使用SDL显示YUV

          本文链接:https://www.haomeiwen.com/subject/fkrskltx.html