文件树:
1.xvideo_view.h
cpp
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
cpp
#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
cpp
#pragma once
#include <QtWidgets/QWidget>
#include "ui_sdlqtrgb.h"
#include <thread>
// 定义继承自 QWidget 的 SdlQtRGB 类
class SdlQtRGB : public QWidget
{
Q_OBJECT
public:
// 构造函数
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
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],//Y
frame->data[1], frame->linesize[1], //U
frame->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
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
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 v
yuv_file.read((char*)frame->data[0], sdl_width * sdl_height);//Y
yuv_file.read((char*)frame->data[1], sdl_width * sdl_height / 4);//U
yuv_file.read((char*)frame->data[2], sdl_width * sdl_height / 4);//V
if (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);//Y
yuv_file.read((char*)frame->data[1], sdl_width * sdl_height / 4);//U
yuv_file.read((char*)frame->data[2], sdl_width * sdl_height / 4);//V
if (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);
}
else
MSleep(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");
//设置fps
set_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 Y
frame->linesize[0] = sdl_width; //Y
frame->linesize[1] = sdl_width / 2; //U
frame->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函数中,最后两行,
cpp
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控件来调整视频播放的帧率,帧率越高播放速度越快。