前面寫了如何使用 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)中開啟一個定時器, startTimer 是 QObject 中的方法,繼承自 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);
}
}