FFmpeg 4.3 音视频-多路H265监控录放C++开发十. 多线程控制帧率。循环播放,QT connect 细节,
在前面,我们总结一下前面的代码。
在 FactoryModeForAVFrameShowSDL 构造函数中 init SDL。
通过 QT timerevent机制,通过startTimer(10);每隔10ms,就会调用timerEvent事件。
在timerEvent事件中,真正的去 读取数据,刷新UI 界面。
上述行为都是在 主线程完成的。
下面我们改变机制:
在 FactoryModeForAVFrameShowSDL 构造函数中 init SDL。
然后开启一个线程,在线程中每隔10ms发送一次信号,那么这个信号就在子线程。信号只需要定义,不需要声明
在这个子线程中,我们通过 发送 这个信号,启动这个信号对应的槽函数,当然槽函数我们要先定义和声明,然后再 FactoryModeForAVFrameShowSDL 的构造函数 中 绑定 信号与槽。
实现细节:
1. 线程的相关细节
在构造方法中完成
//我们这里使用类的成员函数 threadfor 作为 线程函数入口。,注意成员函数做为线程函数作为入口的写法// thread 这块如果忘记了,可以参考 https://blog.csdn.net/hunandede/article/details/135535979_threadfor = std::thread(&FactoryModeForAVFrameShowSDL::threadformethod,this);
在析构函数中,要调用 线程 的join方法,避免主线程结束后,子线程还在运行
FactoryModeForAVFrameShowSDL::~FactoryModeForAVFrameShowSDL()
{_is_exit = true;//主线程结束前,一定要等待 子线程 join,然后会有问题。if (_threadfor.joinable()) {_threadfor.join();}if (_view_fps == nullptr) {delete _view_fps;_view_fps = nullptr;}if (spin_Box == nullptr) {delete spin_Box;spin_Box = nullptr;}
}
我们这代码中打印了线程id,来看信号 和 槽函数分别是在哪里线程中运行的
在线程中打印 id
void FactoryModeForAVFrameShowSDL::threadformethod() {//每隔10ms 发送一次信号,发送信号是在子线程,我们发送了ViewSingle的信号后,会调用 ViewSingleHandle的槽函数处理,ViewSingleHandle函数是在主线程做的cout << "threadformethod single thread::get_id = " << std::this_thread::get_id() << endl;
在槽函数中打印id
void FactoryModeForAVFrameShowSDL::ViewSingleHandle() {//槽函数的实现,cout << "ViewSingleHandle thread::get_id = " << std::this_thread::get_id() << endl;
在构造函数中打印线程id
cout << "FactoryModeForAVFrameShowSDL constructor getpid() = " << std::this_thread::get_id() << endl;
引出QT 的 信号与槽 链接方法 connect方法的最后一个参数的学习
先来看connect 的方法
static QMetaObject::Connection connect(const QObject *sender, const char *signal,const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection);static QMetaObject::Connection connect(const QObject *sender,const QMetaMethod &signal,const QObject *receiver, const QMetaMethod &method,Qt::ConnectionType type = Qt::AutoConnection);inline QMetaObject::Connection connect(const QObject *sender, const char *signal,const char *member, Qt::ConnectionType type = Qt::AutoConnection) const;
ConnectionType可以是以下几种类型:Qt::AutoConnection(默认): Qt自动决定连接类型。如果信号发射和接收在同一个线程,它就选择 Qt::DirectConnection;如果在不同线程,它就选择 Qt::QueuedConnection。
Qt::DirectConnection: 槽函数将直接、立即在信号发射的环境中被调用,这通常在同一个线程中。
Qt::QueuedConnection: 信号发射后,槽函数将被放入事件队列,并在控制权返回事件循环时被调用,常用于跨线程通信。
Qt::BlockingQueuedConnection: 类似于 Qt::QueuedConnection,但是发射信号的线程会阻塞直到槽函数返回。这种方式在处理跨线程通信时要小心使用,以防止死锁。
Qt::UniqueConnection: 连接的信号和槽之间如果已经存在则不会再次连接,确保了同一个信号和槽之间只有一个连接。
2.关于循环播放的问题
C++ 如果读取到了文件的最后,那么如果要seekg 到文件的开头,需要 先使用clear方法
if (_yuv_file.eof()) //读取到文件结尾{cout << "last yuv_file" << endl;//实验测试如果当文件走到 eof()的时候,如果想要seek 到文件开头,需要先clear 一下,//在调用ifstream的定位操作如seekg()之前,必须先调用clear()来清除流的错误标志。这是因为如果流处于错误状态,seekg()等操作将无法正常执行_yuv_file.clear();_yuv_file.seekg(0, ios::beg);}
3.如何计算帧率
帧率就是1s中,我们播放了多少张画面
我们通过槽函数 最终往上画,也就是,里面上槽函数在1秒中 调用了多少次,就是fps。
实际上更加确切的说,需要在 真正 画的那个函数里面计数 会比较合理
bool X_Video_View::DrawAVFrame(AVFrame* frame) {if (frame == nullptr || frame->data[0] == nullptr) {cout << "X_Video_View::DrawAVFrame error because (frame == nullptr || frame->data[0] == nullptr)" << endl;return false;}_count++; // 只要画一次 count 就++;//第一次的时候 _beg_ms =0的,因此要赋初值,后面就走不到了if (_beg_ms <= 0){_beg_ms = clock();}//计算显示帧率,当前时间减去 上一次 记录的时间, 超过 1000 ms 后,将count的值 赋值出去,然后count还原0,重新记录 _beg_ms的值else if ((clock() - _beg_ms) / (CLOCKS_PER_SEC / 1000) >= 1000) //一秒计算一次fps{_render_fps = _count;_count = 0;_beg_ms = clock();}
全部代码
x_video_view.h
#pragma once
#include <iostream>
#include <mutex>
extern "C" {
#include <libavutil/frame.h>
}using namespace std;void MSleep(unsigned int ms);class X_Video_View
{
public:enum RenderType{SDL_TYPE = 0};enum Format{RGBA = 0,ARGB,YUV420P};static X_Video_View* CreateVideoAudio(RenderType type = SDL_TYPE);//初始化渲染窗口 线程安全//@para w 窗口宽度//@para h 窗口高度//@para fmt 绘制的像素格式//@para win_id 窗口句柄,如果为空,创建新窗口//@return 是否创建成功virtual bool Init(int w, int h,Format fmt = RGBA,void* win_id = nullptr) = 0;//
/// 渲染图像 线程安全
///@para data 渲染的二进制数据
///@para linesize 一行数据的字节数,对于YUV420P就是Y一行字节数
/// linesize<=0 就根据宽度和像素格式自动算出大小
/// 纯虚函数,并不需要在 x_video_view.cpp 中实现,需要在子类中实现
/// @return 渲染是否成功virtual bool Draw(const unsigned char* data, int linesize = 0) = 0;/// <summary>
/// 前面提供了 CreateVideoAudio 方法,在子类内部实现是 new XSDL 或者 new XOpenGL
/// 那么应该提供一种方法让user 可以delete XSDL的方法才合理。
/// 那么这个 DestoryVideoAudio 方法具体应该干些啥才合理呢?
/// 也就是说,要不要把 SDLQuit 和 delete SDL 放在一起
/// </summary>virtual void DestoryVideoAudio() = 0;void changedWindow(int changed_w, int changed_h);//如果传递的是 AVFrame,那么在cpp 中根据 AVFrame的format来决定是调用子类的那个Drawbool DrawAVFrame(AVFrame* frame);//
/// 渲染图像 for avframe 线程安全
///@para y y数据的指针
///@para y_pitch y数据的大小
///@para u u数据的指针
///@para u_pitch u数据的大小
///@para v v数据的指针
///@para v_pitch v数据的大小
/// @return 渲染是否成功
/// 此函数需要在子类中实现,因此是纯虚函数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;//基类的 析构函数 需要写成 虚函数virtual ~X_Video_View();int render_fps();
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; //统计显示次数
};
x_video_view.cpp
#include "x_video_view.h"
#include "xsdlview.h"X_Video_View* X_Video_View::CreateVideoAudio(RenderType type)
{switch (type){case X_Video_View::SDL_TYPE:return new XSDLView();break;default:break;}return nullptr;
}void X_Video_View::changedWindow(int changed_w, int changed_h) {_changed_w = changed_w;_changed_h = changed_h;
}bool X_Video_View::DrawAVFrame(AVFrame* frame) {if (frame == nullptr || frame->data[0] == nullptr) {cout << "X_Video_View::DrawAVFrame error because (frame == nullptr || frame->data[0] == nullptr)" << endl;return false;}_count++; // 只要画一次 count 就++;//第一次的时候 _beg_ms =0的,因此要赋初值,后面就走不到了if (_beg_ms <= 0){_beg_ms = clock();}//计算显示帧率,当前时间减去 上一次 记录的时间, 超过 1000 ms 后,将count的值 赋值出去,然后count还原0,重新记录 _beg_ms的值else if ((clock() - _beg_ms) / (CLOCKS_PER_SEC / 1000) >= 1000) //一秒计算一次fps{_render_fps = _count;_count = 0;_beg_ms = clock();}switch (frame->format){case AV_PIX_FMT_YUV420P://如果是YUV420p, 则要调用显示 YUV420P的接口return Draw(frame->data[0],frame->linesize[0],frame->data[1], frame->linesize[1],frame->data[2],frame->linesize[2]);case AV_PIX_FMT_BGRA: //如果不是YUV的数据,则使用最开始的Draw接口就可以。return Draw(frame->data[0],frame->linesize[0]);default:break;}}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;}
}
X_Video_View::~X_Video_View() {}int X_Video_View::render_fps()
{return _render_fps;
}
xsdlview.h
#pragma once
#include "x_video_view.h";
struct SDL_Window;
struct SDL_Renderer;
struct SDL_Texture;
class XSDLView : public X_Video_View
{
protected:bool Init(int w,int h,Format fmt = RGBA,void* win_id = nullptr) override;void endSDL();//没有线程锁的,用于在 init 或者 show的时候,有错误发生后,调用。这是由于init 或者 draw 方法的开头,都是线程 lock 的,再次使用线程 lock 就会有运行时异常,用的是同一个锁子bool Draw(const unsigned char* data, int linesize = 0) override;void DestoryVideoAudio() override; //线程相关的,线程安全的。,用于客户销毁sdl相关数据bool Draw(const unsigned char* y, int y_pitch,const unsigned char* u, int u_pitch,const unsigned char* v, int v_pitch) override;~XSDLView();private:SDL_Window *_sdlwindow = nullptr;SDL_Renderer * _sdlrenderer = nullptr;SDL_Texture * _sdltexture = nullptr;char* _sdltitles = (char *)"sdlshow";
};
xsdlview.cpp
#include "xsdlview.h"
#include "sdl/SDL.h"
#include <iostream>#include <mutex>
using namespace std;
#pragma comment(lib,"SDL2.lib")void XSDLView::endSDL() {if (_sdltexture) {SDL_DestroyTexture(_sdltexture);_sdltexture = nullptr;}if (_sdlrenderer) {SDL_DestroyRenderer(_sdlrenderer);_sdlrenderer = nullptr;}if (_sdlwindow) {SDL_DestroyWindow(_sdlwindow);_sdlwindow = nullptr;}//SDL_Quit();
}
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_INIT ERROR" << SDL_GetError() << endl;return false;}//设定缩放算法,解决锯齿问题,线性插值算法SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");return true;
}bool XSDLView::Init(int w,int h,Format fmt,void* win_id) {//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 (win_id) {_sdlwindow = SDL_CreateWindowFrom(win_id);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;
}bool XSDLView::Draw(const unsigned char* data, int linesize) {//1.先做判断if (data == nullptr) {cout << "xsdlview Draw error becase data = nullptr " << endl;return false;}unique_lock<mutex> sdl_lock(_mtx);if (!_sdltexture || !_sdlrenderer || !_sdlwindow || _width <= 0 || _height <= 0) {cout << "xsdlview Draw error _sdltexture = " << _sdltexture<<" _sdlrenderer = " << _sdlrenderer<<" _sdlwindow = " << _sdlwindow<< " _width = " << _width <<" _height = "<< _height << endl;return false;}//如果user 没有指定 linesize,则需要通过fmt计算一下if (linesize <= 0){switch (_fmt){case X_Video_View::RGBA:case X_Video_View::ARGB:linesize = _width * 4;break;case X_Video_View::YUV420P:linesize = _width;break;default:break;}}if (linesize <= 0) {cout << "xsdlview Draw error becase linesize <= 0 _fmt = " << _fmt << endl;return false;}int ret = 0;// 将数据copy 到 材质auto re = SDL_UpdateTexture(_sdltexture, NULL, data, linesize);if (re != 0){cout << "xsdlview Draw error becase SDL_UpdateTexture " << SDL_GetError() << endl;return false;}//清空屏幕SDL_RenderClear(_sdlrenderer);//将材质copy 到 渲染器//第三个参数,const SDL_Rect * srcrect,意思是,你要将 texture的哪些部分拿出来显示,传递NULL,表示整个texture//第四个参数,const SDL_Rect * dstrect,意思是:要显示的数据,应该放置在window的什么位置SDL_Rect rect;rect.x = 0;rect.y = 0;if (_changed_w <= 0) {_changed_w = _width;}if (_changed_h <= 0) {_changed_h = _height;}rect.w = _changed_w;rect.h = _changed_h;re = SDL_RenderCopy(_sdlrenderer, _sdltexture, NULL, &rect);if (re != 0){cout << "xsdlview Draw error becase SDL_RenderCopy error " << SDL_GetError() << endl;return false;}SDL_RenderPresent(_sdlrenderer);return true;}bool XSDLView::Draw(const unsigned char* y, int y_pitch,const unsigned char* u, int u_pitch,const unsigned char* v, int v_pitch
) {//1.先做判断if (y == nullptr || u == nullptr || v == nullptr) {cout << "xsdlview Draw error becase y == nullptr || u == nullptr || v == nullptr " << endl;return false;}unique_lock<mutex> sdl_lock(_mtx);if (!_sdltexture || !_sdlrenderer || !_sdlwindow || _width <= 0 || _height <= 0) {cout << "xsdlview Draw error _sdltexture = " << _sdltexture<< " _sdlrenderer = " << _sdlrenderer<< " _sdlwindow = " << _sdlwindow<< " _width = " << _width<< " _height = " << _height << endl;return false;}int ret = 0;// 将数据copy 到 材质ret = SDL_UpdateYUVTexture(_sdltexture, nullptr,y, y_pitch, u, u_pitch, v, v_pitch);if (ret != 0){cout << "xsdlview Draw error becase SDL_UpdateTexture " << SDL_GetError() << endl;return false;}//清空屏幕SDL_RenderClear(_sdlrenderer);//将材质copy 到 渲染器//第三个参数,const SDL_Rect * srcrect,意思是,你要将 texture的哪些部分拿出来显示,传递NULL,表示整个texture//第四个参数,const SDL_Rect * dstrect,意思是:要显示的数据,应该放置在window的什么位置SDL_Rect rect;rect.x = 0;rect.y = 0;if (_changed_w <= 0) {_changed_w = _width;}if (_changed_h <= 0) {_changed_h = _height;}rect.w = _changed_w;rect.h = _changed_h;ret = SDL_RenderCopy(_sdlrenderer, _sdltexture, NULL, &rect);if (ret != 0){cout << "xsdlview Draw error becase SDL_RenderCopy error " << SDL_GetError() << endl;return false;}SDL_RenderPresent(_sdlrenderer);return true;}void XSDLView::DestoryVideoAudio() {unique_lock<mutex> sdl_lock(_mtx);endSDL();
}XSDLView::~XSDLView()
{SDL_Quit();
}
factorymodeforavframeshowsdl.h
#pragma once#include <QtWidgets/QMainWindow>
#include "ui_factorymodeforavframeshowsdl.h"
#include <iostream>
#include <fstream>
#include <QMessageBox>
#include "x_video_view.h"
#include <thread>
#include <ctime>
#include <sstream>
#include <QSpinBox>using namespace std;class FactoryModeForAVFrameShowSDL : public QMainWindow
{Q_OBJECTpublic:FactoryModeForAVFrameShowSDL(QWidget *parent = nullptr);~FactoryModeForAVFrameShowSDL();void timerEvent(QTimerEvent* ev) override;void resizeEvent(QResizeEvent* ev) override;//线程函数,用于刷新视频void threadformethod();signals: //自定义信号void ViewSingle(); //信号只需要声明,不需要定义,也就是说不需要 在 .cpp文件写东西public slots: // 自定义槽函数void ViewSingleHandle();//槽函数需要声明,需要定义,需要在.cpp文件中写实现。private:Ui::FactoryModeForAVFrameShowSDLClass ui;ifstream _yuv_file;int _sdl_width = 400;int _sdl_height = 300;X_Video_View* _view = nullptr;unsigned char* _yuvdata = NULL;int _pix_size = 2;//这个_pix_size是为了分盘内存用的, 这里我们用的YUV420p,在不考虑字节对齐的case下,是乘以 1.5的,这里写成2,是内存只要够用就可以了AVFrame* avframe = nullptr;std::thread _threadfor; // thread 和QT 的thread冲突,因此需要加上std 。参考QThread *QObject::thread() constbool _is_exit = false;//处理线程退出QLabel* _view_fps = nullptr;QSpinBox* spin_Box = nullptr;int _spin_box_value = 0;
};
factorymodeforavframeshowsdl.cpp
#include "factorymodeforavframeshowsdl.h"
#include "x_video_view.h"FactoryModeForAVFrameShowSDL::FactoryModeForAVFrameShowSDL(QWidget *parent): QMainWindow(parent)
{//在这里都是初始化的行为。ui.setupUi(this);//打开yuv文件_yuv_file.open("400_300_25.yuv", ios::binary);if (!_yuv_file){QMessageBox::information(this, "", "open yuv failed!");return;}//绑定信号与槽connect(this, SIGNAL(ViewSingle()), this, SLOT(ViewSingleHandle()));// new 一个组件,目的是显示 fps,那么这个fps 的值应该怎么计算呢?应该是1s中播放了多少次//那么我们可以在 真正显示的方法 DrawAVFrame()方法中用一个计数器,显示一次就加1,当超过1s的时候,显示这个count,当然超过1s后还需要将count再变成0//因此我们需要提供一个 render_fps 方法_view_fps = new QLabel(this);_view_fps->setText("正在计算fps");spin_Box = new QSpinBox(this);spin_Box->move(200, 0);spin_Box->setValue(25);spin_Box->setRange(1, 200);_spin_box_value = spin_Box->value();_sdl_width = 400;_sdl_height = 300;//将windows 的宽高 设置成和 yuv的宽和高一样resize(_sdl_width, _sdl_height);//将label 的宽高 设置成和 yuv的宽和高一样ui.label->resize(_sdl_width, _sdl_height);//将label 的x y 坐标设置为window的0,0 ,这样label就能完全显示在window中ui.label->move(0, 0);_view = X_Video_View::CreateVideoAudio();_view->Init(_sdl_width, _sdl_height,X_Video_View::YUV420P, (void*)ui.label->winId());//准备 avframeavframe = av_frame_alloc();if (avframe == nullptr) {cout << "av_frame_alloc error " << endl;_view->DestoryVideoAudio();delete _view;return;}avframe->width = _sdl_width;avframe->height = _sdl_height;//AVPixelFormatavframe->format = AV_PIX_FMT_YUV420P;avframe->linesize[0] = _sdl_width; //我们这里虽然是在后面使用0作为第二个参数,但是也可以自己设定 linesize的大小,av_frame_get_buffer(avframe, 0)avframe->linesize[1] = _sdl_width/2; //这里为啥是 width/2 呢?参考前面007的项目的测试avframe->linesize[2] = _sdl_width/2; // YUV420P不是 4个Y对应一个U+一个V 吗?为什么是除以2呢?实际上 存储的时候假设有8个Y,2个U,2个V ,那么存储的时候,是 YYYY 一行,然后YYYY一行,UU一行,VV一行,因此是除以2的int ret = 0;ret = av_frame_get_buffer(avframe, 0);if (ret < 0 ) {char buf[1024] = { 0 };av_strerror(ret, buf, sizeof(buf));cout << "av_frame_get_buffer error " << endl;_view->DestoryVideoAudio();delete _view;av_frame_free(&avframe);return;}cout << "FactoryModeForAVFrameShowSDL constructor getpid() = " << std::this_thread::get_id() << endl;//cout << "FactoryModeForAVFrameShowSDL constructor thread::get_id = " << thread::get_id << endl;//我们之前的做法是通过startTimer来开启一个定时器,// 让定时器不断地 读取一张一张的图片 到avframe中,读取一张,显示一张// 我们这里不在使用 startTimer(10)来完成显示行为,而是通过 thread 来做//startTimer(10);//现在我们的想法是要通过 线程 来读取数据,那么就先要弄一个线程出来//我们这里使用类的成员函数 threadfor 作为 线程函数入口。,注意成员函数做为线程函数作为入口的写法// thread 这块如果忘记了,可以参考 https://blog.csdn.net/hunandede/article/details/135535979_threadfor = std::thread(&FactoryModeForAVFrameShowSDL::threadformethod,this);//那么这个线程用来干什么呢?用来发送信号,对应信号的槽函数收到信号之后,会自动调用//因此 我们这里要有信号和槽的概念。//也就是说,我们在子线程_threadfor对应的 threadformethod 函数中,每隔 10ms 发送一个信号,// 我们假设这个信号叫做 ViewSingle,对应的槽函数叫做 ViewSingleHandler 函数//那么这个ViewSingle信号的发送是在 子线程做的。这里可以通过打印 threadid来证明//问题是 对应的槽函数 ViewSingleHandler,是在哪个线程进行的呢?实验测试一下。//在这之前,首先我们需要绑定 信号和槽函数。
}void FactoryModeForAVFrameShowSDL::threadformethod() {//每隔10ms 发送一次信号,发送信号是在子线程,我们发送了ViewSingle的信号后,会调用 ViewSingleHandle的槽函数处理,ViewSingleHandle函数是在主线程做的cout << "threadformethod single thread::get_id = " << std::this_thread::get_id() << endl;while (!_is_exit){ViewSingle();//this_thread::sleep_for(10ms);_spin_box_value = spin_Box->value();if (_spin_box_value > 0) {MSleep(1000 / _spin_box_value);}else {MSleep(10);}}}void FactoryModeForAVFrameShowSDL::ViewSingleHandle() {//槽函数的实现,cout << "ViewSingleHandle thread::get_id = " << std::this_thread::get_id() << endl;cout << "avframe event" << endl;_yuv_file.read((char*)avframe->data[0], _sdl_width * _sdl_height);//Y_yuv_file.read((char*)avframe->data[1], _sdl_width * _sdl_height / 4);//U_yuv_file.read((char*)avframe->data[2], _sdl_width * _sdl_height / 4);//Vif (_yuv_file.eof()) //读取到文件结尾{cout << "last yuv_file" << endl;//实验测试如果当文件走到 eof()的时候,如果想要seek 到文件开头,需要先clear 一下,//在调用ifstream的定位操作如seekg()之前,必须先调用clear()来清除流的错误标志。这是因为如果流处于错误状态,seekg()等操作将无法正常执行_yuv_file.clear();_yuv_file.seekg(0, ios::beg);}_view->DrawAVFrame(avframe);stringstream ss;ss << "fps:" << _view->render_fps();//只能在槽函数中调用_view_fps->setText(ss.str().c_str());}void FactoryModeForAVFrameShowSDL::timerEvent(QTimerEvent* ev) {// yuv420p
// 4*2
// yyyy yyyy
// u u
// v v 这里是从yuv_file 中 读取一张图片的大小到 Y,U,V 中cout << "avframe event" << endl;_yuv_file.read((char*)avframe->data[0], _sdl_width * _sdl_height);//Y_yuv_file.read((char*)avframe->data[1], _sdl_width * _sdl_height / 4);//U_yuv_file.read((char*)avframe->data[2], _sdl_width * _sdl_height / 4);//Vif(_yuv_file.bad()) {//debug 测试,读取到文件末尾 不会 走到这一行cout << "bad" << endl;}if (_yuv_file.good()) {cout << "good" << endl;}if (_yuv_file.eof()) //读取到文件结尾{cout << "last yuv_file" << endl;//实验测试如果当文件走到 eof()的时候,如果想要seek 到文件开头,需要先clear 一下,//在调用ifstream的定位操作如seekg()之前,必须先调用clear()来清除流的错误标志。这是因为如果流处于错误状态,seekg()等操作将无法正常执行_yuv_file.clear();_yuv_file.seekg(0, ios::beg);}_view->DrawAVFrame(avframe);}//当窗口的大小变化的时候,会调用到这个函数,该函数由QT 驱动
//测试发现,在第一次的时候窗口显示的时候就会显示
void FactoryModeForAVFrameShowSDL::resizeEvent(QResizeEvent* ev) {cout << "resizeEvent" << endl;//在窗口变化后,先得到窗口的大小QSize aa = size();//将label 的大小也变成和窗口一样大ui.label->resize(aa);//然后是将视频的显示大小变得和这个一样大。//这里要注意的是:视频的大小是不变的,变化的是 视频显示 的大小//视频显示 的大小 在SDL里面是在 SDL_RenderCopy(_sdlrenderer, _sdltexture, NULL, &rect);//那么意味着 我们要记录这个 窗口变化的值,想办法传递给 SDL_RenderCopy 函数cout << "aa.width() = " << aa.width() << " aa.height() = " << aa.height() << endl;_view->changedWindow(aa.width(), aa.height());
}FactoryModeForAVFrameShowSDL::~FactoryModeForAVFrameShowSDL()
{_is_exit = true;//主线程结束前,一定要等待 子线程 join,然后会有问题。if (_threadfor.joinable()) {_threadfor.join();}if (_view_fps == nullptr) {delete _view_fps;_view_fps = nullptr;}if (spin_Box == nullptr) {delete spin_Box;spin_Box = nullptr;}
}
main.cpp
#include "factorymodeforavframeshowsdl.h"
#include <QtWidgets/QApplication>int main(int argc, char *argv[])
{QApplication a(argc, argv);FactoryModeForAVFrameShowSDL w;w.show();return a.exec();
}
相关文章:

FFmpeg 4.3 音视频-多路H265监控录放C++开发十. 多线程控制帧率。循环播放,QT connect 细节,
在前面,我们总结一下前面的代码。 在 FactoryModeForAVFrameShowSDL 构造函数中 init SDL。 通过 QT timerevent机制,通过startTimer(10);每隔10ms,就会调用timerEvent事件。 在timerEvent事件中,真正的去 读取数据,…...

近百万奖金!2024 Web3.0 创新大赛重磅来袭!
10月30日,中国互联网协会与香港Web3.0协会共同组织举办的2024 Web3.0 创新大赛在上海举行启动会,宣布大赛正式在DataFountain竞赛平台(简称DF平台,http://www.datafountain.cn)启动上线。 大赛面向社会各界征集参赛团队…...

gRPC 一种现代、开源、高性能的远程过程调用 (RPC) 可以在任何地方运行的框架
背景介绍 gRPC 是一种现代开源高性能远程过程调用 (RPC) 可以在任何环境中运行的框架。它可以有效地连接服务 在数据中心内和数据中心之间,具有对负载平衡、跟踪、 运行状况检查和身份验证。它也适用于最后一英里 分布式计算,用于…...

cmake系列-怎么构建不同的C++程序目标文件(可执行程序、动态库、静态库)
目录 生成可执行程序生成动态库生成静态库 我们编写的C代码不仅仅只是为了生成可执行程序,有的时候可能是为了生成动态库或者静态库,那么如果用cmake来构建的话,应该怎么做呢,怎么指定是生成可执行程序,还是生成动态库…...

使用ffmpeg和mediamtx模拟多通道rtsp相机
首先下载ffmpeg,在windows系统上直接下载可执行文件,并配置环境变量即可在命令行当中调用执行。 下载地址: https://ffmpeg.org/再在github上下载mediamtx搭建rtsp服务器,使用ffmpeg将码流推流到rtsp服务器。 下载地址࿱…...

windows系统类似于linux的nohup命令后台启动jar服务
一、首先新建一个后缀名为.bat文件 二、将jar包放在与jar包同一个路径下 三、编写.bat文件 echo off start javaw -Xms512m -Xmx1024m -XX:PermSize256m -XX:MaxPermSize512m -XX:MaxNewSize512m -jar xxxxx-22900.jar >> StartupLog.log 2>&1 & exit 四…...

2024 Rust现代实用教程 流程控制与函数
文章目录 一、if流程控制与match模式匹配1.流程控制2. IF流程控制3.match 表达式 二、循环与break continue以及与迭代的区别1.Rust中的循环Loops2.break && continue3.迭代4.循环与迭代的不同 三、函数基础与Copy值参数传递1.函数的基础知识2.Copy by value 四、函数值…...

stm32入门教程--USART外设 超详细!!!
目录 简介 什么是UART? 什么是USART? 简介 USART(Universal Synchron /Asynchronous Receiver /Transmitter)通用同步/异步收发器 1、USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧…...

再探“构造函数”(2)友元and内部类
文章目录 一. 友元‘全局函数’作友元‘成员函数’作友元‘类‘作友元 内部类 一. 友元 何时会用到友元呢? 当想让(类外面的某个函数/其它的类)访问 某个类里面的(私有或保护的)内容时,可以选择使用友元。 友元提供了一种突破&a…...
ffmpeg+vue2
一、安装依赖 npm install ffmpeg/core ffmpeg/ffmpeg "ffmpeg/core": "^0.10.0", "ffmpeg/ffmpeg": "^0.10.1",二、配置ffmpeg 安装好插件以后,需要配置一下代码,否则会报错: 1、在vue.config.js…...

基于深度学习YOLOv10的电动二轮车目标检测、轨迹跟踪、测距算法
基于深度学习YOLOv10的电动二轮车目标检测、轨迹跟踪、测距算法 基于深度学习YOLOv10的电动二轮车目标检测、轨迹跟踪、测距算法引言YOLOv10简介目标检测轨迹跟踪测距算法实际应用结论 基于深度学习YOLOv10的电动二轮车目标检测、轨迹跟踪、测距算法 二轮电动车的目标检测、跟踪…...

鸿蒙ArkTS中的image组件
开发文档很详尽,就在DevEco中的API参考,可以随时调出来进行学习。 在鸿蒙官网也有非常详尽的资料,地址:开发说明-API参考概述 - 华为HarmonyOS开发者 (huawei.com) 这里,就学习image组件的一般用法以及使用SVG图标和字…...

LeetCode 684.冗余连接:拓扑排序+哈希表(O(n)) 或 并查集(O(nlog n)-O(nα(n)))
【LetMeFly】684.冗余连接:拓扑排序哈希表(O(n)) 或 并查集(O(nlog n)-O(nα(n))) 力扣题目链接:https://leetcode.cn/problems/redundant-connection/ 树可以看成是一个连通且 无环 的 无向 图。 给定往…...

让空气净化器“很听话”-置入NRK3502离线语音控制芯片
一、产品市场 随着智能家居的快速发展,人们对家居环境的舒适度与健康性要求日益提升,空气净化器作为改善室内空气质量的重要设备,其智能化升级变得尤为关键。让空气净化器“很听话”,不再仅仅是一个遥不可及的设想,而…...

8个Visio最佳替代软件推荐,每一款都堪称绘图神器
上午好,我的网工朋友。 绘图软件Visio是微软旗下知名的绘图软件,可用来绘制各种可视化图形,包括但不限于:流程图、人物关系图、组织架构图、思维导图、UML图、泳道图、甘特图、知识地图、软件架构图、鱼骨图等 它支持绘制的图形…...

微服务day02
教学文档: 黑马教学文档 Docker Docker的安装 镜像和容器 命令解读 常见命令 案例 查看DockerHub,拉取Nginx镜像,创建并运行容器 搜索Nginx镜像:在 www.hub.docker.com 网站进行查询 拉取镜像: docker pull ngin…...
使用 Logback 的最佳实践:`logback.xml` 与 `logback-spring.xml` 的区别与用法
在开发 Spring Boot 项目时,日志是调试和监控的重要工具。Spring Boot 默认支持 Logback 作为日志系统,并提供了 logback.xml 和 logback-spring.xml 两种配置方式。这篇文章将详细介绍这两者的区别、各自的优缺点以及最佳实践。 目录 一、什么是 Logbac…...

NSET or MSET算法--原理解析
1.背景 NSET/MSET是一种非线性的多元预测诊断技术,广泛应用于系统状态估计、故障诊断和预测等领域;相比于传统的线性模型和方法,NSET/MSET能够更好地处理非线性系统,并提供更准确的预测和诊断能力。在早期,MSET融合了…...

NC6 系统配置的消息渠道配置配置涉及相关的表,用户使用admin登录
NC6 系统配置的消息渠道配置配置涉及相关的表 --电子邮件、公共短信属性值配置表,比如邮箱类型、邮件发送服务器、用户、密码、发件人地址、url等。 SELECT * FROM sm_msg_stypeprop;--消息发送方式配置:电子邮件,公共短信。 SELECT * FROM sm_msg_stypebase WHERE active …...
PXC数据库性能测试对比
mysql单机 #初始化测试数据 sysbench /usr/share/sysbench/oltp_read_write.lua --mysql-host=xxx.xxx.xxx.xxx --mysql-db=test --mysql-user=hzhadmin --mysql-password=Admi --tables=10 --table-size=1000000 prepare#运行性能测试 sysbench /usr/share/sysbench/oltp_rea…...
web vue 项目 Docker化部署
Web 项目 Docker 化部署详细教程 目录 Web 项目 Docker 化部署概述Dockerfile 详解 构建阶段生产阶段 构建和运行 Docker 镜像 1. Web 项目 Docker 化部署概述 Docker 化部署的主要步骤分为以下几个阶段: 构建阶段(Build Stage):…...
测试markdown--肇兴
day1: 1、去程:7:04 --11:32高铁 高铁右转上售票大厅2楼,穿过候车厅下一楼,上大巴车 ¥10/人 **2、到达:**12点多到达寨子,买门票,美团/抖音:¥78人 3、中饭&a…...

Python实现prophet 理论及参数优化
文章目录 Prophet理论及模型参数介绍Python代码完整实现prophet 添加外部数据进行模型优化 之前初步学习prophet的时候,写过一篇简单实现,后期随着对该模型的深入研究,本次记录涉及到prophet 的公式以及参数调优,从公式可以更直观…...

dify打造数据可视化图表
一、概述 在日常工作和学习中,我们经常需要和数据打交道。无论是分析报告、项目展示,还是简单的数据洞察,一个清晰直观的图表,往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server,由蚂蚁集团 AntV 团队…...
鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南
1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发,使用DevEco Studio作为开发工具,采用Java语言实现,包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...
大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...

HashMap中的put方法执行流程(流程图)
1 put操作整体流程 HashMap 的 put 操作是其最核心的功能之一。在 JDK 1.8 及以后版本中,其主要逻辑封装在 putVal 这个内部方法中。整个过程大致如下: 初始判断与哈希计算: 首先,putVal 方法会检查当前的 table(也就…...

20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...

Linux nano命令的基本使用
参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时,显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …...
OD 算法题 B卷【正整数到Excel编号之间的转换】
文章目录 正整数到Excel编号之间的转换 正整数到Excel编号之间的转换 excel的列编号是这样的:a b c … z aa ab ac… az ba bb bc…yz za zb zc …zz aaa aab aac…; 分别代表以下的编号1 2 3 … 26 27 28 29… 52 53 54 55… 676 677 678 679 … 702 703 704 705;…...