使用SDL顯示YUV

前面寫了如何使用 SDL 顯示一張 BMP 圖片,但這并不是我們最終的目的。我們最終的目的是使用 SDL 顯示 YUV 數(shù)據(jù)。下面將演示如何使用 SDL 顯示一張 YUV 格式的圖片和一段 YUV 格式的視頻。

一、SDL 顯示 YUV 圖片:

顯示 YUV 圖片和顯示 BMP 圖片的大致流程是一樣的。顯示 BMP 圖片我們可以直接獲取到 BMP 圖片的 surface,然后直接從 surface 創(chuàng)建紋理。顯示 YUV 格式的圖片,我們需要先創(chuàng)建一個對應(yīng)像素格式的空白紋理,然后讀取 YUV 數(shù)據(jù),再把 YUV 數(shù)據(jù)更新到紋理上面。

Example 使用 Qt 工程開發(fā),首先保證本地安裝了 SDL2,我本地的 SDL2 安裝路徑是 /usr/local/Cellar/sdl2,首先在 .pro 文件中導(dǎo)入 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++ 代碼中調(diào)用不需要加 extern “C”,是因為 SDL 內(nèi)部做了判斷,如果是 C++ 環(huán)境自動幫我們添加 extern “C”

1、初始化 Video 子系統(tǒng)

SDL_Init(SDL_INIT_VIDEO);

我們的目的是顯示 YUV 圖片,所以只初始化 SDL_INIT_VIDEO 子系統(tǒng)就可以。

2、創(chuàng)建窗口

window = SDL_CreateWindow(
            // 窗口的標(biāo)題
            "Display YUV",
            // 窗口的 x 坐標(biāo)(SDL_WINDOWPOS_UNDEFINED:不指定 SDL_WINDOWPOS_CENTERED:中間)
            SDL_WINDOWPOS_UNDEFINED,
            // 窗口的 y 坐標(biāo)
            SDL_WINDOWPOS_UNDEFINED,
            // 窗口的寬度,以像素為單位
            surface->w,
            // 窗口的高度,以像素為單位
            surface->h,
            // SDL_WindowFlags 枚舉,設(shè)置窗口顯示樣式效果
            SDL_WINDOW_SHOWN
         );

3、創(chuàng)建渲染上下文

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

4、創(chuàng)建空白紋理

texture = SDL_CreateTexture(renderer,  // 渲染上下文
                            SDL_PIXELFORMAT_IYUV,  // 很關(guān)鍵的參數(shù),顯示的像素數(shù)據(jù)格式,我們顯示的 YUV 圖片像素格式是 yuv420p,其實 SDL_PIXELFORMAT_IYUV 就是 yuv420p 像素格式
                            SDL_TEXTUREACCESS_STATIC, // 之前我們把同一個 texture 在窗口繪制多次時,我們設(shè)置的是 SDL_TEXTUREACCESS_TARGET,這里我們設(shè)置 SDL_TEXTUREACCESS_STATIC,當(dāng)然設(shè)置成 SDL_TEXTUREACCESS_STREAMING 也可以
                            512,  // 紋理的寬度
                            512); // 紋理的高度
if (!texture) {
       qDebug() << "SDL_CreateTexture Error:" << SDL_GetError();
       goto end;
}

此時我們僅僅是創(chuàng)建了一個 yuv420p 像素格式的空白紋理,其上面并沒有像素格式的數(shù)據(jù)。所以后面需要加載 YUV 數(shù)據(jù),把 YUV 格式像素數(shù)據(jù)加載到紋理上面。PS:和加載 BMP 圖片比較,加載 YUV 數(shù)據(jù)構(gòu)建紋理的過程發(fā)生了變化,加載 BMP 圖片我們使用的是 SDL_CreateTextureFromSurface,加載 YUV 我們先創(chuàng)建了一個空的紋理,重要的是一定要設(shè)置好像素格式,以便后面能夠正確解析我們的 YUV 數(shù)據(jù)。

/**
 *  \brief The access pattern allowed for a texture.
 */
typedef enum
{
    SDL_TEXTUREACCESS_STATIC,    /**< 靜態(tài)(圖片) */
    SDL_TEXTUREACCESS_STREAMING, /**< 數(shù)據(jù)流(視頻) */
    SDL_TEXTUREACCESS_TARGET     /**< 紋理可以作為渲染目標(biāo)使用,比如我們需要把同一個圖形在 window 中繪制多次。我們可以創(chuàng)建一個紋理并設(shè)置成 Target,把圖形繪制到此紋理上,然后設(shè)置 Target 為 window,再把紋理拷貝到 window(可多次拷貝) */
} SDL_TextureAccess;

5、打開文件,將 YUV 數(shù)據(jù)填充到紋理

// 將 YUV 數(shù)據(jù)填充到 texture
if (!file.open(QFile::ReadOnly)) {
    qDebug() << "open file failure:" << FILE_NAME;
    goto end;
}

// 需要把yuv文件數(shù)據(jù)加載進(jìn)內(nèi)存,pixels指向內(nèi)存中的yuv數(shù)據(jù)
// 讀取所有文件數(shù)據(jù)到內(nèi)存中
SDL_UpdateTexture(texture, // 前面創(chuàng)建的空白紋理
                  nullptr, // 更新像素的矩形區(qū)域,設(shè)置為 nullptr 更新整個紋理區(qū)域
                  file.readAll().data(), // 原始像素數(shù)據(jù)
                  512); // 一行像素數(shù)據(jù)的字節(jié)數(shù),這里傳圖片寬度即可

6、復(fù)制紋理到渲染目標(biāo)

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;
    // 紋理(直接跟特定驅(qū)動程序相關(guān)的像素數(shù)據(jù))
    SDL_Texture *texture = nullptr;
    // 事件
    SDL_Event event;

    QFile file(FILE_NAME);

    // 返回值
    int ret = 0;

    // 初始化Video子系統(tǒng)
    if (SDL_Init(SDL_INIT_VIDEO)) {
        qDebug() << "SDL_Init Error:" << SDL_GetError();
        return;
    }

    // 創(chuàng)建窗口
    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;
    }

    // 創(chuàng)建渲染上下文
    renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
    if (!renderer) {
        qDebug() << "SDL_CreateRenderer Error:" << SDL_GetError();
        goto end;
    }

    // 創(chuàng)建紋理
    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 數(shù)據(jù)填充到 texture
    ret = SDL_UpdateTexture(texture, nullptr, file.readAll().data(), IMAGE_WIDTH);
    if (ret < 0) {
        qDebug() << "SDL_UpdateTexture error:" << SDL_GetError();
        goto end;
    }

    // 復(fù)制紋理到渲染目標(biāo)(渲染目標(biāo)默認(rèn)是 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,播放時最終都要解碼成原始數(shù)據(jù),一般就是 YUV 格式數(shù)據(jù)。顯示 YUV 視頻和顯示 YUV 圖片的大致流程也是一樣的。不同之處就是我們要循環(huán)的顯示視頻的每一幀像素數(shù)據(jù)。

在按鈕的事件響應(yīng)中開啟一個定時器, startTimerQObject 中的方法,繼承自 QObject 的對象中都可以調(diào)用這個方法。 調(diào)用方法 startTimer就會開啟一個定時器,并且開啟成功會返回一個定時器Id,定時器調(diào)用間隔是 1000ms / 幀率

void MainWindow::on_displayButton_clicked()
{
    // 開啟定時器
    _timerId = startTimer(1000 / 30.0);
}

定時器會不斷的調(diào)用下面的方法,每次從 YUV 文件中讀取一幀像素數(shù)據(jù),這就需要我們計算出一幀像素數(shù)據(jù)的大小,yuv420p 像素格式每個像素占 1.5 字節(jié),通過 視頻寬度 * 視頻高度 * 1.5 就可算出一幀像素數(shù)據(jù)大小,或者使用 FFmpeg 提供的函數(shù) av_image_get_buffer_size(在 libavutil/imgutils.h 中),然后將讀取的一幀圖素數(shù)據(jù)更新到紋理,并復(fù)制紋理到渲染目標(biāo),最后更新所有的渲染操作到屏幕上,這一幀像素就顯示出來了。重復(fù)相同的操作,就達(dá)到了視頻播放的效果。YUV 文件數(shù)據(jù)讀取完畢,要記得調(diào)用 killTimer 殺死定時器。

void MainWindow::timerEvent(QTimerEvent *event)
{
    // yuv420p 像素格式每個像素占 1.5 字節(jié)
    int imageSize = VIDEO_WIDTH * VIDEO_HEIGHT * 1.5;
    char data[imageSize];
    // 每次讀取一幀圖像
    if (_file.read(data, imageSize) > 0) {
        // 使用像素數(shù)據(jù)更新紋理
        SDL_UpdateTexture(_texture, nullptr, data, VIDEO_WIDTH);
        // 復(fù)制紋理到渲染目標(biāo)
        int ret = SDL_RenderCopy(_renderer, _texture, nullptr, nullptr);
        if (ret < 0) {
            qDebug() << "SDL_RenderCopy Error:" << SDL_GetError();
            return;
        }
        // 更新所有的渲染操作到屏幕上
        SDL_RenderPresent(_renderer);
    } else {
        // 文件數(shù)據(jù)已經(jīng)讀取完畢
        killTimer(_timerId);
    }
}

示例代碼:

在 .pro 文件中導(dǎo)入 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;
    // 紋理(直接和特定程序相關(guān)的像素數(shù)據(jù))
    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子系統(tǒng)
    if (SDL_Init(SDL_INIT_VIDEO)) {
        qDebug() << "SDL_Init Error:" << SDL_GetError();
        return;
    }

    // 從一個已經(jīng)存在的本地窗口創(chuàng)建 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;
    }

    // 創(chuàng)建渲染上下文
    _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;
        }
    }

    // 創(chuàng)建紋理 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 字節(jié)
    int imageSize = VIDEO_WIDTH * VIDEO_HEIGHT * 1.5;
    char data[imageSize];
    // 每次讀取一幀圖像
    if (_file.read(data, imageSize) > 0) {
        // 使用像素數(shù)據(jù)更新紋理
        SDL_UpdateTexture(_texture, nullptr, data, VIDEO_WIDTH);
        // 復(fù)制紋理到渲染目標(biāo)
        int ret = SDL_RenderCopy(_renderer, _texture, nullptr, nullptr);
        if (ret < 0) {
            qDebug() << "SDL_RenderCopy Error:" << SDL_GetError();
            return;
        }
        // 更新所有的渲染操作到屏幕上
        SDL_RenderPresent(_renderer);
    } else {
        // 文件數(shù)據(jù)已經(jīng)讀取完畢
        killTimer(_timerId);
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容