?文件樹:
?1.xvideo_view.h
class XVideoView
{
public:// 像素格式枚舉enum Format { RGBA = 0, ARGB, YUV420P };// 渲染類型枚舉enum RenderType { SDL = 0 };// 創建渲染對象的靜態方法static XVideoView* Create(RenderType type = SDL);// 繪制幀的方法bool DrawFrame(AVFrame* frame);// 純虛函數,需在派生類中實現virtual bool Init(int w, int h, Format fmt = RGBA, void* win_id = nullptr) = 0;virtual void Close() = 0;virtual bool IsExit() = 0;virtual bool Draw(const unsigned char* data, int linesize = 0) = 0;virtual bool Draw(const unsigned char* y, int y_pitch, const unsigned char* u, int u_pitch, const unsigned char* v, int v_pitch) = 0;// 調整顯示大小的方法void Scale(int w, int h);// 獲取顯示幀率的方法int render_fps();protected:// 成員變量int render_fps_ = 0; // 顯示幀率int width_ = 0; // 材質寬度int height_ = 0; // 材質高度Format fmt_ = RGBA; // 像素格式std::mutex mtx_; // 互斥鎖,確保線程安全int scale_w_ = 0; // 顯示寬度int scale_h_ = 0; // 顯示高度long long beg_ms_ = 0; // 計時開始時間int count_ = 0; // 統計顯示次數
};
2. xsdl.h
#pragma once#include "xvideo_view.h"// 前向聲明 SDL 結構體
struct SDL_Window;
struct SDL_Renderer;
struct SDL_Texture;// 定義繼承自 XVideoView 的 XSDL 類
class XSDL : public XVideoView
{
public:// 關閉渲染窗口,覆蓋基類的純虛函數void Close() override;/// 初始化渲染窗口,線程安全/// @param w 窗口寬度/// @param h 窗口高度/// @param fmt 繪制的像素格式/// @param win_id 窗口句柄,如果為空,創建新窗口/// @return 是否創建成功bool Init(int w, int h,Format fmt = RGBA,void* win_id = nullptr) override;///// 渲染圖像,線程安全/// @param data 渲染的二進制數據/// @param linesize 一行數據的字節數,對于 YUV420P 就是 Y 一行字節數/// @param linesize <= 0 就根據寬度和像素格式自動算出大小/// @return 渲染是否成功bool Draw(const unsigned char* data,int linesize = 0) override;// 渲染 YUV420P 圖像,線程安全bool Draw(const unsigned char* y, int y_pitch,const unsigned char* u, int u_pitch,const unsigned char* v, int v_pitch) override;// 判斷是否退出,覆蓋基類的純虛函數bool IsExit() override;private:// SDL 相關成員變量,用于管理窗口、渲染器和紋理SDL_Window* win_ = nullptr;SDL_Renderer* render_ = nullptr;SDL_Texture* texture_ = nullptr;
};
3.sdlqtrgb.h
#pragma once#include <QtWidgets/QWidget>
#include "ui_sdlqtrgb.h"
#include <thread>// 定義繼承自 QWidget 的 SdlQtRGB 類
class SdlQtRGB : public QWidget
{Q_OBJECTpublic:// 構造函數SdlQtRGB(QWidget* parent = Q_NULLPTR);// 析構函數~SdlQtRGB(){is_exit_ = true;// 等待渲染線程退出th_.join();// 當前線程(主線程)將等待,直到 th 線程完成}// 定時器事件處理void timerEvent(QTimerEvent* ev) override;// 窗口大小調整事件處理void resizeEvent(QResizeEvent* ev) override;// 線程函數,用于刷新視頻void Main();signals:// 信號函數,將任務放入列表void ViewS();public slots:// 顯示的槽函數void View();private:std::thread th_; // 渲染線程bool is_exit_ = false; // 處理線程退出Ui::SdlQtRGBClass ui; // UI 組件
};
4.xvideo_view.cpp
#include "xsdl.h"
#include <thread>
using namespace std;
extern "C"
{
#include <libavcodec/avcodec.h>
}
#pragma comment(lib,"avutil.lib")void MSleep(unsigned int ms)
{auto beg = clock();for (int i = 0; i < ms; i++){this_thread::sleep_for(1ms);if ((clock() - beg) / (CLOCKS_PER_SEC / 1000) >= ms)break;}
}
//MSleep 函數實現了一個基于忙等待和定時器的睡眠功能。它將當前線程暫停執行一段時間(以毫秒為單位)XVideoView* XVideoView::Create(RenderType type)
{switch (type){case XVideoView::SDL:return new XSDL();break;default:break;}return nullptr;
}
bool XVideoView::DrawFrame(AVFrame* frame)
{if (!frame || !frame->data[0])return false;count_++;if (beg_ms_ <= 0){beg_ms_ = clock();}//計算顯示幀率else if ((clock() - beg_ms_) / (CLOCKS_PER_SEC / 1000) >= 1000) //一秒計算一次fps{render_fps_ = count_;count_ = 0;beg_ms_ = clock();}//假如一秒鐘調用了20次DrawFrame,count=20,表示一秒鐘渲染了20次圖像,即FPS=20,count置于零switch (frame->format){case AV_PIX_FMT_YUV420P:return Draw(frame->data[0], frame->linesize[0],//Yframe->data[1], frame->linesize[1], //Uframe->data[2], frame->linesize[2] //V);case AV_PIX_FMT_BGRA:return Draw(frame->data[0], frame->linesize[0]);default:break;}return false;
}
else if ((clock() - beg_ms_) / (CLOCKS_PER_SEC / 1000) >= 1000)
:clock() - beg_ms_
:計算從beg_ms_
到當前時間經過的時鐘周期數。CLOCKS_PER_SEC
:宏定義,表示每秒的時鐘周期數。通常值是 1000000 或 1000,取決于系統。(clock() - beg_ms_) / (CLOCKS_PER_SEC / 1000)
:將經過的時鐘周期數轉換為毫秒,再檢查是否已經過了 1000 毫秒(即 1 秒)。
render_fps_ = count_;
:將當前幀計數count_
賦值給render_fps_
,表示過去一秒內顯示的幀數,即 FPS。count_ = 0;
:重置幀計數器,為下一秒重新計數。beg_ms_ = clock();
:重置開始時間,記錄當前時間,開始新的計時周期。
?5.xsdl.cpp
#include "xsdl.h"
#include <sdl/SDL.h>
#include <iostream>
using namespace std;
#pragma comment(lib,"SDL2.lib")
static bool InitVideo()
{static bool is_first = true;static mutex mux;unique_lock<mutex> sdl_lock(mux);if (!is_first)return true;is_first = false;if (SDL_Init(SDL_INIT_VIDEO)){cout << SDL_GetError() << endl;return false;}//設定縮放算法,解決鋸齒問題,線性插值算法SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");return true;
}
bool XSDL::IsExit()
{SDL_Event ev;SDL_WaitEventTimeout(&ev, 1);if (ev.type == SDL_QUIT)return true;return false;
}
該函數通過調用 SDL 庫提供的函數等待事件,如果接收到退出事件,則返回 true,否則返回 false。這種方法用于輪詢事件隊列,以便及時響應用戶的退出操作。void XSDL::Close()
{//確保線程安全unique_lock<mutex> sdl_lock(mtx_);if (texture_){SDL_DestroyTexture(texture_);texture_ = nullptr;}if (render_){SDL_DestroyRenderer(render_);render_ = nullptr;}if (win_){SDL_DestroyWindow(win_);win_ = nullptr;}
}
該 Close 函數用于關閉 SDL 窗口和相關資源。在關閉窗口之前,它使用互斥量確保線程安全性。然后,依次銷毀 SDL 窗口、渲染器和紋理對象,并將相應的指針置為空,以防止內存泄漏和懸空指針。通過這樣的實現,可以安全地關閉 SDL 窗口和釋放相關資源,確保程序運行的穩定性和正確性。bool XSDL::Init(int w, int h, Format fmt, void* win_id)
{if (w <= 0 || h <= 0)return false;//初始化SDL 視頻庫InitVideo();//確保線程安全unique_lock<mutex> sdl_lock(mtx_);width_ = w;height_ = h;fmt_ = fmt;if (texture_)SDL_DestroyTexture(texture_);if (render_)SDL_DestroyRenderer(render_);///1 創建窗口if (!win_){if (!win_id){//新建窗口win_ = SDL_CreateWindow("",SDL_WINDOWPOS_UNDEFINED,SDL_WINDOWPOS_UNDEFINED,w, h, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);}else{//渲染到控件窗口win_ = SDL_CreateWindowFrom(win_id);}}if (!win_){cerr << SDL_GetError() << endl;return false;}/// 2 創建渲染器render_ = SDL_CreateRenderer(win_, -1, SDL_RENDERER_ACCELERATED);if (!render_){cerr << SDL_GetError() << endl;return false;}//創建材質 (顯存)unsigned int sdl_fmt = SDL_PIXELFORMAT_RGBA8888;switch (fmt){case XVideoView::RGBA:break;case XVideoView::ARGB:sdl_fmt = SDL_PIXELFORMAT_ARGB32;break;case XVideoView::YUV420P:sdl_fmt = SDL_PIXELFORMAT_IYUV;break;default:break;}texture_ = SDL_CreateTexture(render_,sdl_fmt, //像素格式SDL_TEXTUREACCESS_STREAMING, //頻繁修改的渲染(帶鎖)w, h //材質大小);if (!texture_){cerr << SDL_GetError() << endl;return false;}return true;
}
bool XSDL::Draw(const unsigned char* y, int y_pitch,const unsigned char* u, int u_pitch,const unsigned char* v, int v_pitch
)
{//參數檢查if (!y || !u || !v)return false;unique_lock<mutex> sdl_lock(mtx_);if (!texture_ || !render_ || !win_ || width_ <= 0 || height_ <= 0)return false;//復制內存到顯顯存auto re = SDL_UpdateYUVTexture(texture_,NULL,y, y_pitch,u, u_pitch,v, v_pitch);if (re != 0){cout << SDL_GetError() << endl;return false;}//清空屏幕SDL_RenderClear(render_);//材質復制到渲染器SDL_Rect rect;SDL_Rect* prect = nullptr;if (scale_w_ > 0) //用戶手動設置縮放{rect.x = 0; rect.y = 0;rect.w = scale_w_;//渲染的寬高,可縮放rect.h = scale_w_;prect = ▭}re = SDL_RenderCopy(render_, texture_, NULL, prect);if (re != 0){cout << SDL_GetError() << endl;return false;}SDL_RenderPresent(render_);
}
bool XSDL::Draw(const unsigned char* data, int linesize)
{if (!data)return false;unique_lock<mutex> sdl_lock(mtx_);if (!texture_ || !render_ || !win_ || width_ <= 0 || height_ <= 0)return false;if (linesize <= 0){switch (fmt_){case XVideoView::RGBA:case XVideoView::ARGB:linesize = width_ * 4;break;case XVideoView::YUV420P:linesize = width_;break;default:break;}}if (linesize <= 0)return false;//復制內存到顯顯存auto re = SDL_UpdateTexture(texture_, NULL, data, linesize);if (re != 0){cout << SDL_GetError() << endl;return false;}//清空屏幕SDL_RenderClear(render_);//材質復制到渲染器SDL_Rect rect;SDL_Rect* prect = nullptr;if (scale_w_ > 0) //用戶手動設置縮放{rect.x = 0; rect.y = 0;rect.w = scale_w_;//渲染的寬高,可縮放rect.h = scale_w_;prect = ▭}re = SDL_RenderCopy(render_, texture_, NULL, prect);if (re != 0){cout << SDL_GetError() << endl;return false;}SDL_RenderPresent(render_);return true;
}
draw函數解析:
- 首先進行了參數檢查。檢查輸入的 YUV 數據指針是否為非空,如果有任何一個為空,則返回
false
。 - 接著使用獨占鎖
sdl_lock
對 SDL 窗口相關資源進行保護,確保在繪制過程中不會被其他線程干擾。 - 進一步檢查 SDL 相關資源是否已經初始化,并且窗口的寬度和高度是否大于零,如果存在任何不滿足條件的情況,則返回
false
。 - 使用
SDL_UpdateYUVTexture
函數將 YUV 數據復制到顯存中的紋理對象中。這個函數會更新已經存在的 YUV 紋理,以便后續渲染到屏幕上。 - 使用
SDL_RenderClear
函數清空渲染器的渲染目標,即清空屏幕。 - 根據用戶是否手動設置縮放參數,設置渲染區域的大小。
- 使用
SDL_RenderCopy
函數將紋理對象復制到渲染器中,并在屏幕上渲染出來。 - 最后,使用
SDL_RenderPresent
函數將渲染器中的內容呈現到屏幕上,完成一幀的繪制。 - 該
Draw
函數用于在 SDL 窗口中繪制 YUV 格式的圖像。它首先將 YUV 數據復制到紋理對象中,然后清空屏幕并將紋理對象渲染到屏幕上。通過這種方式,可以實現基于 SDL 的視頻播放功能。
對比兩個draw函數:
第二個draw函數處理 YUV 數據的方式相對來說更簡單,因為它只需要處理單個分量的數據(一個數組),而不需要分別處理 Y、U、V 三個分量(三個數組)。這種處理方式可能在一些情況下效率更高,特別是當只需要顯示圖像的亮度信息時,而對色度信息的準確性要求不是很高時,使用單個分量的方法會更加簡潔和高效。
第一個draw函數處理 YUV 數據的優點主要體現在以下幾個方面:
-
精確控制每個分量:第一個函數能夠分別處理 Y、U、V 三個分量的數據,可以對每個分量進行精確的控制和處理,適用于需要對圖像的亮度和色度信息進行精細調節的場景。
-
靈活性:通過分別處理每個分量,可以實現更多樣化的圖像處理操作,如亮度調整、對比度調整、色調轉換等。這種靈活性使得第一個函數在一些特定的應用場景中更加適用。
-
兼容性:在某些情況下,需要對 YUV 數據進行特定格式的處理,比如將 YUV 數據轉換為其他格式或者進行編解碼操作。通過分別處理 Y、U、V 三個分量,可以更容易地滿足這些需求,提高代碼的兼容性和通用性。
總的來說,第一個函數適用于對圖像進行復雜處理和轉換的場景,能夠提供更多的靈活性和控制能力。而第二個函數則更適用于簡單的圖像顯示場景,能夠提供更高的處理效率和性能。選擇哪個函數取決于具體的需求和應用場景。
6.sdlqtrgb.cpp
#include "sdlqtrgb.h"
#include <fstream>
#include <iostream>
#include <QMessageBox>
#include <thread>
#include <sstream>
#include <QSpinBox>
#include "xvideo_view.h"
extern "C"
{
#include <libavcodec/avcodec.h>
}using namespace std;static int sdl_width = 0;
static int sdl_height = 0;
static int pix_size = 2;
static ifstream yuv_file;
static XVideoView* view = nullptr;
static AVFrame* frame = nullptr;
static long long file_size = 0;
static QLabel* view_fps = nullptr; //顯示fps控件
static QSpinBox* set_fps = nullptr;//設置fps控件
int fps = 25; //播放幀率
void SdlQtRGB::timerEvent(QTimerEvent* ev)
{//yuv_file.read((char*)yuv, sdl_width * sdl_height * 1.5);// yuv420p// 4*2// yyyy yyyy // u u// v vyuv_file.read((char*)frame->data[0], sdl_width * sdl_height);//Yyuv_file.read((char*)frame->data[1], sdl_width * sdl_height / 4);//Uyuv_file.read((char*)frame->data[2], sdl_width * sdl_height / 4);//Vif (view->IsExit()){view->Close();exit(0);}view->DrawFrame(frame);//view->Draw(yuv);
}void SdlQtRGB::View()
{yuv_file.read((char*)frame->data[0], sdl_width * sdl_height);//Yyuv_file.read((char*)frame->data[1], sdl_width * sdl_height / 4);//Uyuv_file.read((char*)frame->data[2], sdl_width * sdl_height / 4);//Vif (yuv_file.tellg() == file_size) //讀取到文件結尾{yuv_file.seekg(0, ios::beg);}//yuv_file.gcount()//yuv_file.seekg() 結尾處seekg無效if (view->IsExit()){view->Close();exit(0);}view->DrawFrame(frame);stringstream ss;ss << "fps:" << view->render_fps();//只能在槽函數中調用view_fps->setText(ss.str().c_str());fps = set_fps->value(); //拿到播放幀率
}void SdlQtRGB::Main()
{while (!is_exit_){ViewS();if (fps > 0){MSleep(1000 / fps);}elseMSleep(10);}
}
SdlQtRGB::SdlQtRGB(QWidget* parent): QWidget(parent)
{//打開yuv文件yuv_file.open("400_300_25.yuv", ios::binary);if (!yuv_file){QMessageBox::information(this, "", "open yuv failed!");return;}yuv_file.seekg(0, ios::end); //移到文件結尾file_size = yuv_file.tellg(); //文件指針位置yuv_file.seekg(0, ios::beg);ui.setupUi(this);//綁定渲染信號槽connect(this, SIGNAL(ViewS()), this, SLOT(View()));//顯示fps的控件view_fps = new QLabel(this);view_fps->setText("fps:100");//設置fpsset_fps = new QSpinBox(this);set_fps->move(200, 0);set_fps->setValue(25);set_fps->setRange(1, 300);sdl_width = 400;sdl_height = 300;ui.label->resize(sdl_width, sdl_height);view = XVideoView::Create();//view->Init(sdl_width, sdl_height,// XVideoView::YUV420P);//view->Close();view->Close();view->Init(sdl_width, sdl_height,XVideoView::YUV420P, (void*)ui.label->winId());//生成frame對象空間frame = av_frame_alloc();frame->width = sdl_width;frame->height = sdl_height;frame->format = AV_PIX_FMT_YUV420P;// Y Y// UV// Y Yframe->linesize[0] = sdl_width; //Yframe->linesize[1] = sdl_width / 2; //Uframe->linesize[2] = sdl_width / 2; //V//生成圖像空間 默認32字節對齊auto re = av_frame_get_buffer(frame, 0);if (re != 0){char buf[1024] = { 0 };av_strerror(re, buf, sizeof(buf));cerr << buf << endl;}//startTimer(10);th_ = std::thread(&SdlQtRGB::Main, this);
}void SdlQtRGB::resizeEvent(QResizeEvent* ev)
{ui.label->resize(size());ui.label->move(0, 0);//view->Scale(width(), height());
}
timeEvent函數解析:
- ? 從文件中依次讀取 YUV420P 格式的視頻幀數據,分別存儲到
frame->data[0]
(Y 分量)、frame->data[1]
(U 分量)和frame->data[2]
(V 分量)中。根據 YUV420P 格式的特點,U 和 V 分量的大小是 Y 分量的四分之一。 - 調用
XVideoView
類的DrawFrame
函數來渲染讀取到的視頻幀數據。這個函數會將 YUV 數據傳遞給渲染器進行顯示。
view函數解析:
- ? 如果讀取到文件結尾,就將文件指針移到文件開頭,實現視頻循環播放。
-
view->DrawFrame(frame);
: 這行代碼調用了XVideoView
類的DrawFrame
函數,將從視頻文件中讀取的幀數據frame
渲染到屏幕上。具體的渲染邏輯在DrawFrame
函數中實現。 -
stringstream ss;
: 創建了一個stringstream
對象ss
,用于構建幀率信息的字符串。 -
ss << "fps:" << view->render_fps();
: 將幀率信息拼接到ss
中。view->render_fps()
會調用XVideoView
對象的render_fps()
方法來獲取當前的渲染幀率,然后將其拼接到字符串后面。 -
view_fps->setText(ss.str().c_str());
: 將構建好的幀率信息字符串設置到界面上用于顯示幀率的文本框view_fps
中。ss.str()
將stringstream
對象轉換為std::string
類型,然后通過setText
函數將其設置到界面上。 -
fps = set_fps->value();
: 獲取用戶設置的播放幀率。這里假設set_fps
是一個用戶用于設置播放幀率的控件(如滑塊、輸入框等),通過value
屬性獲取用戶設置的播放幀率,并將其保存在變量fps
中。
Main函數解析:
SdlQtRGB::Main
方法是一個視頻播放的主循環,不斷地顯示視頻幀,控制播放幀率,直到退出條件滿足為止。
sdlQtRGB構造函數解析:
std::thread(&SdlQtRGB::Main, this)
表示創建了一個新的線程,線程的入口函數是SdlQtRGB
類的Main
方法,當前對象的指針作為參數傳遞給線程。-
yuv_file.seekg(0, ios::end);
: 將文件指針移動到文件的末尾。通過將文件指針移動到文件末尾,然后調用tellg()
函數獲取文件指針的位置,就可以得到文件的大小。 -
file_size = yuv_file.tellg();
: 獲取文件指針的位置,即文件的大小,并將其賦值給變量file_size
。這樣,file_size
變量就存儲了 YUV 文件的大小。 -
yuv_file.seekg(0, ios::beg);
: 將文件指針重新移動到文件的開頭。這是為了在后續操作中重新使用文件時將文件指針定位到文件的起始位置。
7.運行過程:
-
程序初始化:
- 包括全局變量的初始化、配置文件的加載等操作。
-
創建
SdlQtRGB
對象:- 在主函數中,會創建一個
SdlQtRGB
對象,這將觸發SdlQtRGB
類的構造函數執行。
- 在主函數中,會創建一個
-
初始化界面和文件:
- 在
SdlQtRGB
類的構造函數中,會初始化界面、打開 YUV 文件,并獲取文件大小等操作。
- 在
-
創建視頻渲染器和幀對象:
- 在構造函數中會創建視頻渲染器對象
view
,并初始化它。 - 創建
AVFrame
對象frame
,分配內存空間并設置幀的寬度、高度和像素格式為 YUV420P。
- 在構造函數中會創建視頻渲染器對象
-
啟動視頻播放主循環線程:
- 在構造函數中,通過創建線程的方式啟動視頻播放主循環,即調用
SdlQtRGB::Main
方法。
- 在構造函數中,通過創建線程的方式啟動視頻播放主循環,即調用
-
主循環運行:
- 在
SdlQtRGB::Main
方法中,程序會進入主循環,不斷地執行視頻播放的相關操作。 - 主循環中會不斷地讀取 YUV 文件中的數據,并將數據傳遞給渲染器進行渲染。
- 在
-
渲染幀和更新界面:
- 在
View
方法中,會不斷地讀取 YUV 數據,然后將其傳遞給渲染器進行渲染。 - 同時,程序會更新界面上顯示的幀率信息。
- 在
-
用戶交互和定時操作:
- 程序會監聽用戶輸入,響應鍵盤、鼠標等事件。
- 如果設置了播放幀率,程序會根據幀率控制視頻播放的速度。
-
退出和清理:
- 當用戶關閉程序或觸發退出條件時,程序會退出主循環。
- 程序會執行必要的清理操作,包括釋放資源、關閉文件等。
8.特別注意對幀率的調整過程:
在view函數中,最后兩行,
view_fps->setText(ss.str().c_str());
//左上角顯示幀率
fps = set_fps->value();
?? 通過調整?QSpinBox控件來拿到要播放幀率,若調整到40,則fps=40,在Main函數中通過fps參數將視頻渲染的fps調整到40幀率
比如 fps = 40,MSleep(1000/40)即MSleep(25),即休眠25ms,即兩幅圖像渲染的時間間隔為25ms,1000ms 共有40個25ms,即40副圖像,一秒渲染40副圖像 ,即fps = 40?
?
9.運行結果:
這里我們可以通過QspinBox控件來調整視頻播放的幀率,幀率越高播放速度越快。
?