FFmpeg 4.3 音视频-多路H265监控录放C++开发十. 多线程控制帧率。循环播放,QT connect 细节,

news2024/11/27 0:46:47

在前面,我们总结一下前面的代码。

在 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来决定是调用子类的那个Draw
    bool 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_OBJECT

public:
    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() const
    bool _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());


    //准备 avframe
    avframe = 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;
    //AVPixelFormat
    avframe->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);//V

    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);

    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);//V
    
    if(_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();
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2236110.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

WebRTC 环境搭建

主题 本文主要描述webrtc开发过程中所需的环境搭建 环境&#xff1a; 运行环境&#xff1a;ubuntu 20.04 Node.js环境搭建 安装编译 Node.js 所需的依赖包: sudo apt-get update sudo apt-get install -y build-essential libssl-dev 下载 Node.js 源码: curl -sL htt…

libgdiplus在MacOS M1上问题:Unable to load shared library ‘libgdiplus‘

libgdiplus在MacOS M1上问题&#xff1a;Unable to load shared library libgdiplus 问题解决步骤1步骤2 问题 在mac上的pycharm中执行下面的代码时出现下面的错误 slide.get_thumbnail( RuntimeError: Proxy error(TypeInitializationException): The type initializer for…

【VScode】VScode内的ChatGPT插件——CodeMoss全解析与实用教程

在当今快速发展的编程世界中&#xff0c;开发者们面临着越来越多的挑战。如何提高编程效率&#xff0c;如何快速获取解决方案&#xff0c;成为了每位开发者心中的疑问。今天&#xff0c;我们将深入探讨一款颠覆传统编程体验的插件——CodeMoss&#xff0c;它将ChatGPT的强大功能…

关于我、重生到500年前凭借C语言改变世界科技vlog.16——万字详解指针概念及技巧

文章目录 1. sizeof 和 strlen1.1 sizeof1.2 strlen 2. 数组和指针结合的试题深入解析2.1 一维数组2.2 字符数组代码1代码2代码3代码4代码5代码6 2.3 二维数组 3.指针运算的试题深入解析题1题2题3题4题5题6题7 希望读者们多多三连支持小编会继续更新你们的鼓励就是我前进的动力…

flutter 专题四 Flutter渲染流程

一、 Widget - Element - RenderObject关系 二、 Widget 、Element 、RenderObject 分别表示什么 2.1 Widget Widget描述和配置子树的样子 Widget就是一个个描述文件&#xff0c;这些描述文件在我们进行状态改变时会不断的build。但是对于渲染对象来说&#xff0c;只会使用最…

Netty 编码器 解码器 正确使用姿势

Netty 编码器 & 解码器 正确使用姿势 通过前面文章的例子&#xff0c;相信读者也感受到了Netty 开发核心工作在于处理读事件&#xff08;解码&#xff09;、写事件&#xff08;编码&#xff09;。 Netty 的编解码器是处理网络数据编码和解码的核心组件&#xff0c;编解码…

基于微信小程序的电子购物系统的设计与实现(lw+演示+源码+运行)

摘 要 由于APP软件在开发以及运营上面所需成本较高&#xff0c;而用户手机需要安装各种APP软件&#xff0c;因此占用用户过多的手机存储空间&#xff0c;导致用户手机运行缓慢&#xff0c;体验度比较差&#xff0c;进而导致用户会卸载非必要的APP&#xff0c;倒逼管理者必须改…

FIPS203 后量子安全ML-KEM(标准简读)

FIPS 203是美国国家标准与技术研究院&#xff08;NIST&#xff09;发布的关于模块格基密钥封装机制&#xff08;ML-KEM&#xff09;的标准&#xff0c;旨在提供一种能抵御量子计算机攻击的密钥建立方案。以下是对该文档的详细总结&#xff1a; 标准概述 目的与范围&#xff…

鸿萌数据迁移服务: 企业服务器整机在线热迁移, 实现不停机业务转移

天津鸿萌科贸发展有限公司从事数据安全服务二十余年&#xff0c;致力于为各领域客户提供专业的数据存储、数据恢复、数据备份、数据迁移等解决方案与服务&#xff0c;并针对企业面临的数据安全风险&#xff0c;提供专业的相关数据安全培训。 鸿萌数据迁移业务为众多企业顺利高效…

macOS15.1及以上系统bug:开发者证书无法打开,钥匙串访问无法打开一直出现图标后立马闪退

团队紧跟苹果最新系统发现bug:今日设备信息如下,希望能带给遇到这个问题的开发者一点帮助。 错误图如下: 点击证书文件后,先出现钥匙串访问图标,后立马闪退消失 中间试过很多方法,都是一样的表现,最后好在解决了,看网上也没有相关的帖子,这里直接写解决办法和导致原因…

20241102在荣品PRO-RK3566开发板的预置Android13下适配宸芯的数传模块CX6603N

20241102在荣品PRO-RK3566开发板的预置Android13下适配宸芯的数传模块CX6603N 2024/11/2 18:04 在WIN10使用程序&#xff1a;ViewLink-4.0.7_0708-windows-x64.exe 在荣品PRO-RK3566开发板的预置Android13下使用&#xff1a;ViewLink-2023_12_21-release-0.2.6.apk adb install…

【STM32】DMA直接存储器读取

文章目录 DMA简介DMA定义DMA传输方式DMA传输参数STM32的存储器映像DMA基本结构DMA的具体应用数据转运 DMAADC扫描模式 DMA DMA库函数 DMA数据传输&#xff08;数据转运 DMA&#xff09;接线图MyDMA模块main.c 源程序 DMA AD多通道&#xff08;ADC扫描模式 DMA&#xff09;…

兰空图床配置域名访问

图床已经创建完毕并且可以访问了&#xff0c;但是使用IP地址多少还是差点意思&#xff0c;而且不方便记忆&#xff0c;而NAT模式又没法直接像普通服务器一样DNS解析完就可以访问。 尝试了很多办法&#xff0c;nginx配置了半天也没配好&#xff0c;索性直接重定向&#xff0c;反…

React 入门课程 - 使用CDN编程React

1. 第一个React 注意&#xff1a;在vscode里&#xff0c;使用Live Server来运行html文件。 index.html <html><head><link rel"stylesheet" href"index.css"><script crossorigin src"https://unpkg.com/react17/umd/react.de…

flink 内存配置(一):设置Flink进程内存

flink 内存配置&#xff08;一&#xff09;&#xff1a;设置Flink进程内存 flink 内存配置&#xff08;二&#xff09;&#xff1a;设置TaskManager内存 flink 内存配置&#xff08;三&#xff09;&#xff1a;设置JobManager内存 flink 内存配置&#xff08;四&#xff09;…

快讯,Flutter PC 多窗口新进展,已在 Ubuntu/Canonical 展示

相信 Flutter 开发者对于 Flutter PC 多窗口的支持一直是「望眼欲穿」&#xff0c;而根据 #142845 相关内容展示&#xff0c; 在上月 27 号的 Ubuntu 峰会&#xff0c;Flutter 展示了多窗口相关进展。 事实上 Ubuntu 和 Flutter 的进一步合作关系应该是在 2021 年就开始了&…

HTB:Nibbles[WriteUP]

目录 连接至HTB服务器并启动靶机 1.How many open TCP ports are listening on Nibbles? 使用nmap对靶机TCP端口进行开放扫描 2.What is the relative path on the webserver to a blog? 使用ffuf对靶机80端口Web进行路径FUZZ 3.What content management system (CMS) …

AI资讯快报(2024.11.3-11.8)

1.<字节跳动上线名为炉米 Lumi的 AI 模型交流社区> 近日&#xff0c;字节跳动上线了一款名为【炉米 Lumi】的 AI 模型交流社区&#xff0c;这是一个专门给AI爱好者、研究人员和开发者准备的AI模型分享社区平台。该平台目前还在内部测试阶段&#xff0c;只有白名单用户才…

使用最新版的wvp和ZLMediaKit搭建Gb28181测试服务器

文章目录 说明安装1.安装nodejs简介安装步骤 2.安装java环境3.安装mysql安装修改密码 4.安装redis5.安装编译器6.安装cmake7.安装依赖库8.编译ZLMediaKit9.编译wvp-GB28181-pro 配置1.ZLMediaKit配置2.wvp-GB28181-pro配置2.1.配置ZLMediaKit连接信息2.2.28181服务器的配置2.3.…

AutoOps 使每个 Elasticsearch 部署都更易于管理

作者&#xff1a;来自 Elastic Ziv Segal&#xff0c;Ori Shafir AutoOps for Elasticsearch 通过性能建议、资源利用率和成本洞察、实时问题检测和解决路径显著简化了集群管理。 虽然 Elasticsearch 是一款功能强大且可扩展的搜索引擎&#xff0c;可提供多种功能&#xff0c;但…