FFmpeg 4.3 音视频-多路H265监控录放C++开发十二:在屏幕上显示多路视频播放,可以有不同的分辨率,格式和帧率。
上图是在安防领域的要求,一般都是一个屏幕上有显示多个摄像头捕捉到的画面,这一节,我们是从文件中读取多个文件,显示在屏幕上。
一 改动UI文件
这里我们要添加两个label,为了区分我们设置一下背景色(这个是非必须的),并设置不同的objectname,分别为video1 和 video2(这个objectname是组件的唯一性标识)。
UI设计完成后长这样。
width1, width2, height1,heigth2 的值范围设定为 1-9999,表示视频的宽和高的范围
set_fpx1 和 set_fpx2的值范围为1-200,user可以设定 帧率
还有一个显示的fpx ,后续会在 video1 和 video2上显示
二 给 open1 和 open2 添加信号和槽事件
UI上的操作
使用编辑UI 和 信号与槽的按钮 切换。
这里顺便加了两个要显示的fps:
将UI的大小调整一下
代码中的实现
也就是说,我们通过UI的操作,
将 open1的clicked信号 绑定了 SlotOpen1槽函数
将 open2的clicked信号 绑定了 SlotOpen2槽函数
但是现在代码中还并没有 SlotOpen1槽函数 ,也没有 SlotOpen2槽函数
在代码中添加 槽函数的声明和实现
factorymodeforavframeshowsdl.h 声明
public slots: // 自定义槽函数
void ViewSingleHandle();//槽函数需要声明,需要定义,需要在.cpp文件中写实现。
void SlotOpen1();
void SlotOpen2();
///另外,为了扩展,因为后期我们可能要添加10个 button,打开10个视频,因此可以整理一个SlotOpen函数,参数为 int i ,表示打开哪一个file
void SlotOpen(int i);
factorymodeforavframeshowsdl.cpp实现
为了代码整齐,我们将槽函数的实现写在之前的槽函数 ViewSingleHandle之后,方便代码看起来整齐。
我们先想一下slotopen1函数的功能应该是啥?打开一个yuv文件,或者RGBA文件,
1.使用 QFileDialog 让user 选择想要打开的文件
void FactoryModeForAVFrameShowSDL::SlotOpen(int i) {
//1.使用 QFileDialog 让user 选择想要打开的文件
QFileDialog qfd;
QString filename = qfd.getOpenFileName();
if (filename.isEmpty()) {
cout << "SlotOpen can not open filename because filename.isEmpty()" << endl;
return;
}
cout << "SlotOpen filename = " <<filename.toStdString() << endl;
2.打开文件
到现在我们已经选择一个文件了,到这里文件名字就有了,应该使用 C++的打开文件函数了 fstream open。或者 Qfile的open函数
//这里考虑到 文件的打开应该是个常规操作,我们将文件的打开函数 放在 x_video_view中 定义,对于最终调用这来说,只需要传递一个文件名字就OK了
// 也就是 需要 调用 x_video_view.open(filename.toStdString()); 打开文件
x_video_view.h
//打开文件,C++的文件打开是使用 ifstream.open,因此还需要一个文件句柄--类似ifstream ifs
//那么我们就需要定义一个成员变量 ifstream ifs了,又因为我们不想让这个文件句柄给最终使用的user拿到,因此要写成private或者protected的
bool Open(std::string filename);
x_video_view.cpp
bool X_Video_View::Open(std::string filename)
{
//容错处理
if (_ifs.is_open()) {
_ifs.close();
}
//使用二进制的方式打开,C++的file open没有返回值,文件打开或者没打开需要看is_open()方法
_ifs.open(filename, std::ios_base::binary);
//返回值告诉user 该文件是否打开。
return _ifs.is_open();
}
接口已经有了,下来就要调用 x_video_view.open(filename.toStdString()); 打开文件。
那么这个 x_video_view什么时候得到的呢?
我们知道,之前我们是一个画面,在FactoryModeForAVFrameShowSDL的构造函数中,CreateVideoAudio出来一个 x_video_view
// 现在有多个显示视频的窗口,那么就应该createviewaudio多次,且后面要使用 createviuewaudio 出来的X_Video_View*,因此应该还需要保存 多个 X_Video_View*,这里使用vector<X_Video_View*> 保存。
//由于这里 明显要在构造方法中,调用 createviewaudio。并保存多个 X_Video_View* 到vector 中。因此这里先要跳到 构造方法中,完成 createviewaudio 并保存到vector 的操作
三.在构造方法中 创建 vector<X_Video_View*>
第一个问题就是要创建几个显示视频的窗口。
如下是自己的想法:根据有几个 QLabel 来创建 几个 X_Video_View,我们在UI中有4个qlabel,两个显示视频,2个显示 fps,但是这样设计会有要求,就是UI中的和代码中要有 协商。
//这里我们想从widget中找到,有几个显示视频的窗口,就应该要CreateVideoAudio几次。当前UI设定是有4个label的,2个显示video,2个显示fps
//QList<QLabel*> allPButtons = ui.centralWidget->findChildren<QLabel*>(); ///4
//QList<QLabel*> allPButtons = ui.centralWidget->findChildren<QLabel*>("video1"); ///1,video1是第一个显示视频的QLabel。
我们这里直接写2个,如果后续要添加,看有没有比较合理的动态的方法。
_vec.push_back(X_Video_View::CreateVideoAudio());
_vec.push_back(X_Video_View::CreateVideoAudio());
第二个问题,我们之前在init的时候,会绑定一个ui.label->winId
//类似这样的代码_view->Init(_sdl_width, _sdl_height, X_Video_View::YUV420P, (void*)ui.label->winId());
//那么现在有多个winid,我们应该怎么绑定呢?这里我们想到的方法是,在 x_video_view.h 中 声明 _winid,并提供 setwindid的方法,记录这个值。那么在init 调用的时候,就不需要winid的传递了,直接从x_video_view中获得 winid,绑定SDL 创建的window 到winid上
_vec[0]->setWinId((void *) ui.video1->winId());
_vec[1]->setWinId((void *) ui.video2->winId());
代码
x_video_view.h 添加 变量 void * _winid = nullptr; 是protected的,提供public的访问方法 setWinid
public:
void setWinId(void *winid);
protected:
int _width = 0; //材质宽高
int _height = 0;
Format _fmt = RGBA; //像素格式
mutex _mtx; //确保线程安全
int _changed_w = 0; //显示大小
int _changed_h = 0;
int _render_fps = 0; //显示帧率
long long _beg_ms = 0; //计时开始时间
int _count = 0; //统计显示次数
void* _winid = nullptr;
x_video_view.cpp
void X_Video_View::setWinId(void* winid) {
this->_winid = winid;
}
这还意味着,我们在 init 的时候,不需要传递 winid了,,还需要改动一下 之前的 init 接口。这里需要代码改动 init 接口
// 那么之前在构造方法中调用的 init 方法,还在这里调用吗?
//我们可以想象一下只有当user 选择了要播放的文件的时候,我们再去init 是不是更加合理一些呢,因此我们在改造了init方法后,调用的地方应该是 button的click信号发送后的SlotOpen槽函数中更加合理
//_view->Init(_sdl_width, _sdl_height,X_Video_View::YUV420P, (void*)ui.label->winId());
cout << "" << endl;
x_video_view.h
//在多路播放中,winid会被记录,因此需要调整上面的接口
virtual bool Init(int w,
int h,
Format fmt = RGBA) = 0;
xsdlview.h
bool Init(int w,
int h,
Format fmt = RGBA) override;
xsdlview.cpp
注意 更换的核心行
if (this->_winid) {
_sdlwindow = SDL_CreateWindowFrom(this->_winid);
bool XSDLView::Init(int w,
int h,
Format fmt) {
//0.错误检查
if (w <= 0 || h <= 0) {
cout << "SDL Init error because w <= 0, h <= 0 w = " << w
<<" h = " << h
<< SDL_GetError() << endl;
return false;
}
//1.SDLinit(初始化SDL 视频库),由于 SDL init 只需要一次,因此最好做成 static 的
InitVideo();
//2.确保线程安全后,将user 传递的宽 和高 都赋值了
unique_lock<mutex> sdl_lock(_mtx);
_width = w;
_height = h;
_fmt = fmt;
//3. 创建窗口,user创建windows的时候如果没有传递 win_id
//4. 我们这里还要考虑user 多次调用 Init 函数的情况,假设多次调用了init 函数,那么需要考虑_sdlwindow,sdlrenderer,sdltexture,是否需要多次 create出来
//对于sdlwindows,是没有必要create多次的。
if (_sdlwindow == nullptr) {
if (this->_winid) {
_sdlwindow = SDL_CreateWindowFrom(this->_winid);
if (_sdlwindow == nullptr) {
cout << "SDL_CreateWindowFrom win_id error " << SDL_GetError() << endl;
endSDL();
return false;
}
}
else {
_sdlwindow = SDL_CreateWindow(_sdltitles,
0, 0,
_width, _height,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (_sdlwindow == nullptr) {
cout << "SDL_CreateWindow error "
<< " _sdltitles = " << _sdltitles
<< " _width = " << _width
<< " _height = " << _height
<< " SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE "
<< SDL_GetError()
<< endl;
endSDL();
return false;
}
}
}
//4. 创建renderer 渲染器
//对于 renderer如果多次调用init函数,则可能有内存泄漏,因此我们最开始的想法是,和 sdlwindows的处理方法一样
//参考sdlwindow 的处理方法,就是如果sdlwindows存在了就不需要创建了。
// 如下的写法也是可以的,如果 renderer 和texture存在,就直接先destory了
if (_sdltexture) {
SDL_DestroyTexture(_sdltexture);
}
if (_sdlrenderer) {
SDL_DestroyRenderer(_sdlrenderer);
}
_sdlrenderer = SDL_CreateRenderer(_sdlwindow, -1, SDL_RENDERER_ACCELERATED);
if (_sdlrenderer == nullptr) {
cout << "SDL_CreateRenderer SDL_RENDERER_ACCELERATED error " << SDL_GetError() << endl;
_sdlrenderer = SDL_CreateRenderer(_sdlwindow, -1, SDL_RENDERER_SOFTWARE);
if (_sdlrenderer == nullptr) {
cout << "SDL_CreateRenderer SDL_RENDERER_SOFTWARE error " << SDL_GetError() << endl;
endSDL();
return false;
}
}
//5 创建 texture 材质
//转化 fmt 和 sdlfmt
unsigned int sdl_fmt = SDL_PIXELFORMAT_RGBA8888;
switch (fmt)
{
case X_Video_View::RGBA:
break;
case X_Video_View::ARGB:
sdl_fmt = SDL_PIXELFORMAT_ARGB32;
break;
case X_Video_View::YUV420P:
sdl_fmt = SDL_PIXELFORMAT_IYUV;
break;
default:
break;
}
_sdltexture = SDL_CreateTexture(_sdlrenderer,
sdl_fmt,
SDL_TEXTUREACCESS_STREAMING,
_width,
_height);
if (_sdltexture == nullptr) {
cout << "SDL_CreateTexture SDL_RENDERER_SOFTWARE error " << SDL_GetError() << endl;
endSDL();
return false;
}
return true;
}
四。我们再回到 SlotOpen方法中 去init
void FactoryModeForAVFrameShowSDL::SlotOpen(int i) {
//1.使用 QFileDialog 让user 选择想要打开的文件
QFileDialog qfd;
QString filename = qfd.getOpenFileName();
if (filename.isEmpty()) {
cout << "SlotOpen can not open filename because filename.isEmpty()" << endl;
return;
}
cout << "SlotOpen filename = " <<filename.toStdString() << endl;
//2.到这里文件名字就有了,应该使用 C++的打开文件函数了 fstream open。或者 Qfile的open函数
//这里考虑到 文件的打开应该是个常规操作,我们将文件的打开函数 放在 x_video_view中 定义,对于最终调用这来说,只需要传递一个文件名字就OK了
// 也就是 需要 调用 x_video_view.open(filename.toStdString()); 打开文件‘
// 那么理论上就要先弄出来一个 x_video_view了,我们知道,之前我们是一个画面,在FactoryModeForAVFrameShowSDL的构造函数中,CreateVideoAudio出来一个 x_video_view
// 现在有多个显示视频的窗口,那么就应该createviewaudio多次,且后面要使用 createviuewaudio 出来的X_Video_View*,因此应该还需要保存 多个 X_Video_View*,这里使用vector<X_Video_View*> 保存。
//由于这里 明显要在构造方法中,调用 createviewaudio。并保存多个 X_Video_View* 到vector 中
/// <summary>
/// 在Qt中,toLocal8Bit()是一个QString类的函数,
/// 用于将QString对象转换为本地8位字符集编码的QByteArray对象。
/// 这个函数会根据当前系统的本地编码将QString对象转换为对应的8位字符集编码,
/// 比如在中文Windows系统中,toLocal8Bit()会将QString对象转换为GB2312编码的QByteArray对象。
/// 这个函数通常用于将QString对象转换为可以在底层API中使用的8位字符集编码。
/// <param name="i"></param>
this->_vec[i]->Open(filename.toLocal8Bit().toStdString());
//3.回到SlotOpen方法中,来init,init 需要的参数已经变成 了 Init(int w,int h,Format fmt = RGBA);
//那么对于每一个 x_video_view, 都要知道 w,h ,format
int w = 0;
int h = 0;
QString pix = 0; //YUV420P RGBA
X_Video_View::Format format = X_Video_View::YUV420P;
if (i == 0 ) {
w = ui.width1->value();
h = ui.height1->value();
pix = ui.pix1->currentText();
}
else if (i == 1) {
w = ui.width2->value();
h = ui.height2->value();
pix = ui.pix2->currentText();
}
if (pix == "YUV420P")
{
format = X_Video_View::YUV420P;
cout << "444" << endl;
}
else if (pix == "RGBA")
{
format = X_Video_View::RGBA;
}
else if (pix == "ARGB")
{
format = X_Video_View::ARGB;
}
else if (pix == "BGRA")
{
format = X_Video_View::BGRA;
}
bool aa = this->_vec[i]->Init(w, h, format);
cout << "init = " << aa << endl;
}
至此,为了每一个 视频界面都create 了 xvideoview,且都init 了。
五。准备要处理的数据
那么我们下来就要准备要显示的YUV文件,RGB文件,显示了,先准备一下这些数据吧。
ffmpeg -i v1080.mp4 -s 800x400 -pix_fmt rgba 1.rgb
ffmpeg -i v1080.mp4 -s 600x300 -pix_fmt yuv420p 2.yuv
六。 抽取读取AVFrame的接口到 x_video_view
我们现在 使用 ifs 打开了一个文件,那么下来就是要从文件中读取数据到AVFrame,然后将AVFrame再显示到画面上。
目前的做法是:
我们当前在 构造函数中, 创建AVFrame的结构体和给AVFrame分配内存,然后再 每隔10ms发送一次信号,在信号槽函数中,给 AVFrame中读取 一张图片的大小,紧接着画出来。也就是说,当前的做法是在业务逻辑中分配了AVFrame,并在业务逻辑中 给AVFrame分配空间和赋值。
改动这部分,我们将 读取文件的这部分操作放在xvideoview的接口中,让 业务层在调用 这个方法的时候,就可以读取一张图片到AVFrame。
x_video_view.h
//
/// 读取一帧数据,并维护AVFrame空间
/// 每次调用会覆盖上一次数据
AVFrame* ReadAVFrame();
x_video_view.cpp
//在读取数据之前,我们先要做判断,如果读取数据之前,要确定 init 已经完成了。
//也就是,读取文件的ifs已经打开了,width和height都有了值,this->_fmt有了值
AVFrame* X_Video_View::ReadAVFrame() {
//但是这里error 判断 的条件
if (this->_width <= 0 || this->_height <=0 || !_ifs) {
return nullptr;
}
//如果_avframe已经存在,因为每次都要读取数据,因此要释放
///但是不是每次都释放,假设_avframe中的各个参数都是一样的,我们就没有必要free
if (_avframe!=nullptr) {
if (_avframe->width != _width
|| _avframe->height != _height
|| _avframe->format != _fmt)
{
//释放AVFrame对象空间,和buf引用计数减一
av_frame_free(&_avframe);
}
}
//
if (_avframe == nullptr) {
//分配 avframe,并且初始化,并且通过 av_frame_get_buffer,给avframe中赋值。
_avframe = av_frame_alloc();
if (_avframe == nullptr) {
cout << "ReadAVFrame error because av_frame_alloc == nullptr" << endl;
}
_avframe->width = this->_width;
_avframe->height = this->_height;
_avframe->format = this->_fmt;
//根据不同的 format,设置 linesize 的值
if (_avframe->format == AV_PIX_FMT_YUV420P)
{
_avframe->linesize[0] = _width; // Y
_avframe->linesize[1] = _width / 2;//U
_avframe->linesize[2] = _width / 2;//V
}
else if (_avframe->format == AV_PIX_FMT_ARGB) {
_avframe->linesize[0] = _width * 4;
}
else if (_avframe->format == AV_PIX_FMT_RGBA) {
_avframe->linesize[0] = _width * 4;
}
else if (_avframe->format == AV_PIX_FMT_ABGR) {
_avframe->linesize[0] = _width * 4;
}
else if (_avframe->format == AV_PIX_FMT_BGRA) {
_avframe->linesize[0] = _width * 4;
}
int ret = 0;
ret = av_frame_get_buffer(_avframe, 0);
if (ret < 0) {
char errbuf[1024] = { 0 };
//这里给 sizeof(errbuf) - 1, 是为了留下一个 填写 字符\0,方便打印log观察
av_strerror(ret, errbuf, sizeof(errbuf) - 1);
av_frame_free(&_avframe);
cout << "av_frame_get_buffer error _avframe->width = " << _avframe->width
<< endl;
return nullptr;
}
}
//这里再次判断 avframe == nullptr,实际上是冗余的。只是习惯而已
if (_avframe == nullptr) {
return nullptr;
}
//到这里 _avframe就真的可以使用了,那么就要给这里填充数据拉,在这之前,我们的 _ifs中已经有了要读取文件的流
//如果是YUV420P,_avframe->data[0]读取的大小就是 宽度*宽度,_avframe->data[1]读取的大小为 宽度*高度/4,_avframe->data[2]读取大小也是 宽度*高度/4
if (this->_avframe->format == AV_PIX_FMT_YUV420P) {
this->_ifs.read((char*)this->_avframe->data[0], this->_avframe->width * this->_avframe->height);
this->_ifs.read((char*)this->_avframe->data[1], this->_avframe->width * this->_avframe->height /4);
this->_ifs.read((char*)this->_avframe->data[2], this->_avframe->width * this->_avframe->height /4);
}
else if (this->_avframe->format == AV_PIX_FMT_ARGB) {
this->_ifs.read((char*)this->_avframe->data[0], this->_avframe->width * this->_avframe->height * 4 );
}
else if (this->_avframe->format == AV_PIX_FMT_RGBA) {
this->_ifs.read((char*)this->_avframe->data[0], this->_avframe->width * this->_avframe->height * 4);
}
else if (this->_avframe->format == AV_PIX_FMT_ABGR) {
this->_ifs.read((char*)this->_avframe->data[0], this->_avframe->width * this->_avframe->height * 4);
}
else if (this->_avframe->format == AV_PIX_FMT_BGRA) {
this->_ifs.read((char*)this->_avframe->data[0], this->_avframe->width * this->_avframe->height * 4);
}
//gcount()返回已读字符数,也就是最后一次读取到的,实际上这里写成==0 是不严谨的,
//如果我们yuv数据最后一些数据丢失了,那么合理的判断应该是 this->_ifs.gcount()<要实际应该读取的大小
//if (this->_avframe->format == AV_PIX_FMT_YUV420P) {
// if (this->_ifs.gcount() < this->_avframe->width * this->_avframe->height *1.5) {
// return nullptr;
// }
//}
//else if (this->_avframe->format == AV_PIX_FMT_ARGB) {
// if (this->_ifs.gcount() < this->_avframe->width * this->_avframe->height * 4) {
// return nullptr;
// }
//}
//else if (this->_avframe->format == AV_PIX_FMT_RGBA) {
// if (this->_ifs.gcount() < this->_avframe->width * this->_avframe->height * 4) {
// return nullptr;
// }
//}
//else if (this->_avframe->format == AV_PIX_FMT_ABGR) {
// if (this->_ifs.gcount() < this->_avframe->width * this->_avframe->height * 4) {
// return nullptr;
// }
//}
//else if (this->_avframe->format == AV_PIX_FMT_BGRA) {
// if (this->_ifs.gcount() < this->_avframe->width * this->_avframe->height * 4) {
// return nullptr;
// }
//}
if (this->_ifs.gcount()==0) {
this->_ifs.clear();
this->_ifs.seekg(0, std::ios::beg);
//return nullptr;
}
//那么我们在什么时候调用ReadAVFrame 函数 呢?应该是在 业务逻辑 的 收到信号槽函数
return _avframe;
}
七。读取数据,显示数据
void FactoryModeForAVFrameShowSDL::ViewSingleHandle() {
//槽函数的实现,
cout << "ViewSingleHandle thread::get_id = " << std::this_thread::get_id() << endl;
// 新代码改动到这里,我们还是要在 子线程 信号的槽函数这里 进行文件的读取,
///在读取之前,需要向获得 user在界面上设置的 fps是多少,这样的话,
int setfpx1 = ui.set_fpx1->value(); //
int setfpx2 = ui.set_fpx1_2->value();
_set_fps_arr[0] = setfpx1;
_set_fps_arr[1] = setfpx2;
//循环,对每个视频画面进行显示,显示前如果 user 设置的 fpx <=0,则循环到下一个屏幕
for (int i = 0; i < _vec.size(); ++i) {
if (_set_fps_arr[i] < 0) {
continue;
}
//如果user有设置,其实由于我们设置了 set_fps 的默认值是25,因此一定会走到这里
///计算出每隔多少毫秒 渲染一次
int ms = 1000 / _set_fps_arr[i];
//计算当前时间 和 上一次渲染时间 的 差值,如果差值 < ms,就轮换到下一个
if ((NowMs() - _last_avframe_play_time_arr[i]) < ms) {
continue;
}
//如果到这里就说明,真的要读取数据了,那么这时候要记录 当前帧 播放的时间。
_last_avframe_play_time_arr[i] = NowMs();
//读取数据
auto avframedata = _vec[i]->ReadAVFrame();
if (avframedata == nullptr) {
continue;
}
//显示画面
_vec[i]->DrawAVFrame(avframedata);
//显示fps
stringstream ss;
ss << "fps:" << _vec[i]->render_fps();
if (i == 0) {
ui.show_fps1->setText(ss.str().c_str());
}
else {
ui.show_fps2->setText(ss.str().c_str());
}
}
}