21_显示YUV图片&视频

一、显示YUV图片

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

宏定义

#include 
#include 

#define END(judge, func) \
    if (judge) { \
        qDebug() << #func << "error" << SDL_GetError(); \
        goto end; \
    }

#define FILENAME "F:/res/in.yuv"
#define PIXEL_FORMAT SDL_PIXELFORMAT_IYUV
#define IMG_W 512
#define IMG_H 512

变量定义

// 窗口
SDL_Window *window = nullptr;

// 渲染上下文
SDL_Renderer *renderer = nullptr;

// 纹理(直接跟特定驱动程序相关的像素数据)
SDL_Texture *texture = nullptr;

// 文件
QFile file(FILENAME);

初始化子系统

// 初始化Video子系统
END(SDL_Init(SDL_INIT_VIDEO), SDL_Init);

创建窗口

// 创建窗口
window = SDL_CreateWindow(
             // 窗口标题
             "SDL显示YUV图片",
             // 窗口X(未定义)
             SDL_WINDOWPOS_UNDEFINED,
             // 窗口Y(未定义)
             SDL_WINDOWPOS_UNDEFINED,
             // 窗口宽度(跟图片宽度一样)
             surface->w,
             // 窗口高度(跟图片高度一样)
             surface->h,
             // 显示窗口
             SDL_WINDOW_SHOWN
         );
END(!window, SDL_CreateWindow);

创建渲染上下文

// 创建渲染上下文(默认的渲染目标是window)
renderer = SDL_CreateRenderer(window, 
                              -1,// 要初始化的渲染设备的索引,设置 -1 则初始化第一个支持flags的设备
                              SDL_RENDERER_ACCELERATED |
                              SDL_RENDERER_PRESENTVSYNC);
if (!renderer) { // 说明开启硬件加速失败
    renderer = SDL_CreateRenderer(window, -1, 0);
}
END(!renderer, SDL_CreateRenderer);

创建纹理

// 创建纹理
texture = SDL_CreateTexture(renderer,
                            //显示的像素数据格式,我们显示的YUV图片像素格式是yuv420p,
                            //其实SDL_PIXELFORMAT_IYUV就是yuv420p像素格式
                            PIXEL_FORMAT,
                            //之前我们把同一个texture在窗口绘制多次时,我们设置的是SDL_TEXTUREACCESS_TARGET,
                            //这里我们设置SDL_TEXTUREACCESS_STATIC,当然设置成SDL_TEXTUREACCESS_STREAMING也可以
                            SDL_TEXTUREACCESS_STREAMING,
                            IMG_W, IMG_H);
END(!texture, SDL_CreateTexture);

这里我们仅仅是创建了一个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;

打开文件

// 打开文件
if (!file.open(QFile::ReadOnly)) {
    qDebug() << "file open error" << FILENAME;
    goto end;
}

渲染

// 将YUV的像素数据填充到texture
END(SDL_UpdateTexture(texture,
                      nullptr,// SDL_Rect:更新像素的矩形区域,传nullptr表示更新整个纹理区域
                      file.readAll().data(),// 原始像素数据
                      IMG_W),// 一行像素数据的字节数,这里传图片宽度即可
        SDL_UpdateTexture);

// 设置绘制颜色(画笔颜色)
END(SDL_SetRenderDrawColor(renderer,
                           0, 0, 0, SDL_ALPHA_OPAQUE),
    SDL_SetRenderDrawColor);

// 用绘制颜色(画笔颜色)清除渲染目标
END(SDL_RenderClear(renderer),
    SDL_RenderClear);

// 复制纹理到渲染目标上(默认是window)可以使用SDL_SetRenderTarget()修改渲染目标
// srcrect源矩形框,dstrect目标矩形框,两者都传nullptr表示整个纹理渲染到整个目标上去
END(SDL_RenderCopy(renderer, texture, nullptr, nullptr),
    SDL_RenderCopy);

// 更新所有的渲染操作到屏幕上
SDL_RenderPresent(renderer);

延迟退出

// 延迟3秒退出
SDL_Delay(3000);

释放资源

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

代码链接

二、显示YUV视频

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

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

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

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

// 每隔一段时间就会调用
void MainWindow::timerEvent(QTimerEvent *event){
    // yuv420p 像素格式每个像素占 1.5 字节
    int imgSize = IMG_W * IMG_H * 1.5;
    char data[imgSize];
    // 每次读取一帧图像
    if(_file.read(data,imgSize) > 0){
        // 将YUV的像素数据填充到texture
        RET(SDL_UpdateTexture(_texture,
                              nullptr,// SDL_Rect:更新像素的矩形区域,传nullptr表示更新整个纹理区域
                              data,// 原始像素数据
                              IMG_W),// 一行像素数据的字节数,这里传图片宽度即可
            SDL_UpdateTexture);
        // 渲染
        // 设置绘制颜色(这里随便设置了一个颜色:黄色)
        RET(SDL_SetRenderDrawColor(_renderer,255,255,0,SDL_ALPHA_OPAQUE),SDL_SetRenderDrawColor);

        // 用DrawColor清除渲染目标
        RET(SDL_RenderClear(_renderer),SDL_RenderClear);

        // 复制纹理到渲染目标上(默认是window)可以使用SDL_SetRenderTarget()修改渲染目标
        // srcrect源矩形框,dstrect目标矩形框,两者都传nullptr表示整个纹理渲染到整个目标上去
        RET(SDL_RenderCopy(_renderer,_texture,nullptr,nullptr),SDL_RenderCopy);

        // 将此前的所有需要渲染的内容更新到屏幕上
        SDL_RenderPresent(_renderer);
    }else{
        // 文件数据已经读取完毕
        killTimer(_timerId);
    }
}

具体代码

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include 
#include 
#include 

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
    void timerEvent(QTimerEvent *event);
private slots:
    void on_playButton_clicked();

private:
    Ui::MainWindow *ui;
    QWidget *_widget;
    // 窗口
    SDL_Window *_window = nullptr;
    // 渲染上下文
    SDL_Renderer *_renderer = nullptr;
    // 纹理(直接跟特定驱动程序相关的像素数据)
    SDL_Texture *_texture = nullptr;
    QFile _file;
    int _timerId;

};
#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include 

#ifdef Q_OS_WIN
    #define FILENAME "../test/out.yuv"
#else
    #define FILENAME "/Users/zuojie/QtProjects/audio-video-dev/test/in.yuv"
#endif

// 出错了就执行goto end
#define RET(judge, func) \
    if (judge) { \
        qDebug() << #func << "Error" << SDL_GetError(); \
        return; \
    }

#define PIXEL_FORMAT SDL_PIXELFORMAT_IYUV
#define IMG_W 848
#define IMG_H 480


MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    _widget = new QWidget(this);
    _widget->setGeometry(0,50,IMG_W,IMG_H);

    // 初始化Video子系统
    RET(SDL_Init(SDL_INIT_VIDEO),SDL_Init);

    // 创建窗口
    _window  = SDL_CreateWindowFrom((void *)_widget->winId());
    RET(!_window, SDL_CreateWindow);

    // 创建渲染上下文(默认的渲染目标是window)
    _renderer = SDL_CreateRenderer(_window,
                       // 要初始化的渲染设备的索引,设置 -1 则初始化第一个支持 flags 的设备
                       -1,
                       SDL_RENDERER_ACCELERATED |
                       SDL_RENDERER_PRESENTVSYNC);
    if (!_renderer) { // 说明开启硬件加速失败
        _renderer = SDL_CreateRenderer(_window, -1, 0);
    }
    RET(!_renderer, SDL_CreateRenderer);

    // 创建纹理
    _texture = SDL_CreateTexture(_renderer,
                                //显示的像素数据格式,我们显示的YUV图片像素格式是yuv420p,
                                //其实SDL_PIXELFORMAT_IYUV就是yuv420p像素格式
                                PIXEL_FORMAT,
                                //之前我们把同一个texture在窗口绘制多次时,我们设置的是SDL_TEXTUREACCESS_TARGET,
                                //这里我们设置SDL_TEXTUREACCESS_STATIC,当然设置成SDL_TEXTUREACCESS_STREAMING也可以
                                SDL_TEXTUREACCESS_STATIC,
                                IMG_W,IMG_H);
    RET(!_texture, SDL_CreateTexture);

    // 打开文件
    _file.setFileName(FILENAME);
    if(!_file.open(QFile::ReadOnly)){
        qDebug() << "file open error" << FILENAME;
    }
}

MainWindow::~MainWindow()
{
    delete ui;
    _file.close();
    SDL_DestroyTexture(_texture);
    SDL_DestroyRenderer(_renderer);
    SDL_DestroyWindow(_window);
    SDL_Quit();
}

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

// 每隔一段时间就会调用
void MainWindow::timerEvent(QTimerEvent *event){
    // yuv420p 像素格式每个像素占 1.5 字节
    int imgSize = IMG_W * IMG_H * 1.5;
    char data[imgSize];
    // 每次读取一帧图像
    if(_file.read(data,imgSize) > 0){
        // 将YUV的像素数据填充到texture
        RET(SDL_UpdateTexture(_texture,
                              nullptr,// SDL_Rect:更新像素的矩形区域,传nullptr表示更新整个纹理区域
                              data,// 原始像素数据
                              IMG_W),// 一行像素数据的字节数,这里传图片宽度即可
            SDL_UpdateTexture);
        // 渲染
        // 设置绘制颜色(这里随便设置了一个颜色:黄色)
        RET(SDL_SetRenderDrawColor(_renderer,255,255,0,SDL_ALPHA_OPAQUE),SDL_SetRenderDrawColor);

        // 用DrawColor清除渲染目标
        RET(SDL_RenderClear(_renderer),SDL_RenderClear);

        // 复制纹理到渲染目标上(默认是window)可以使用SDL_SetRenderTarget()修改渲染目标
        // srcrect源矩形框,dstrect目标矩形框,两者都传nullptr表示整个纹理渲染到整个目标上去
        RET(SDL_RenderCopy(_renderer,_texture,nullptr,nullptr),SDL_RenderCopy);

        // 将此前的所有需要渲染的内容更新到屏幕上
        SDL_RenderPresent(_renderer);
    }else{
        // 文件数据已经读取完毕
        killTimer(_timerId);
    }
}

代码链接

你可能感兴趣的:(21_显示YUV图片&视频)