读取YUV数据到AVFrame并用多线程控制帧率

 文件树:

 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_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


#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


#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 = &rect;
    }
    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 = &rect;
    }
    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 数据的优点主要体现在以下几个方面:

  1. 精确控制每个分量:第一个函数能够分别处理 Y、U、V 三个分量的数据,可以对每个分量进行精确的控制和处理,适用于需要对图像的亮度和色度信息进行精细调节的场景。

  2. 灵活性:通过分别处理每个分量,可以实现更多样化的图像处理操作,如亮度调整、对比度调整、色调转换等。这种灵活性使得第一个函数在一些特定的应用场景中更加适用。

  3. 兼容性:在某些情况下,需要对 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    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函数中,最后两行,

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控件来调整视频播放的帧率,帧率越高播放速度越快。

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Word下一页怎么操作?6个操作方法分享!

在使用Word进行文档编辑时&#xff0c;经常需要设置分页、跳转页面或者对页面进行特定的布局调整。其中&#xff0c;“下一页”这一操作通常涉及到页面布局、分页符的插入、页面跳转等多个方面。 Word下一页怎么操作&#xff1f;很简单&#xff0c;方法已经准备好啦&#xff0…

Python代码:二十五、有序的列表

1、题目 创建一个依次包含字符串P、y、t、h、o和n的列表my_list&#xff0c;先使用sorted函数对列表my_list进行临时排序&#xff0c;第一行输出排序后的完整列表&#xff0c;第二行输出原始的列表。再使用sort函数对列表my_list进行降序排序&#xff0c;第三行输出排序后完整…

20240531瑞芯微官方Toybrick TB-RK3588开发板在Debian11下的关机尝试

20240531瑞芯微官方Toybrick TB-RK3588开发板在Debian11下的关机尝试 2024/5/31 9:10 https://t.rock-chips.com/forum.php?modforumdisplay&fid51 在Toybrick的论坛以关键词搜索&#xff1a;关机 可以找到抱怨这个的人很多&#xff01; &#xff08;一&#xff09;【失败…

深入分析 Android Service (三)

文章目录 深入分析 Android Service (三)1. Service 与 Activity 之间的通信2. 详细示例&#xff1a;通过绑定服务进行通信2.1 创建一个绑定服务2.2 绑定和通信 3. 优化建议4. 使用场景5. 总结 深入分析 Android Service (三) 1. Service 与 Activity 之间的通信 在 Android …

linux nohup命令详解:持久运行命令,无视终端退出

nohup &#xff08;全称为 “no hang up”&#xff09;&#xff0c;用于运行一个命令&#xff0c;使其在你退出 shell 或终端会话后继续运行。 基本语法 nohup command [arg1 ...] [&> output_file] &command 是你想要运行的命令。[arg1 ...] 是该命令的参数。&am…

Spring Boot 整合 spring-boot-starter-mail 实现邮件发送和账户激活

&#x1f604; 19年之后由于某些原因断更了三年&#xff0c;23年重新扬帆起航&#xff0c;推出更多优质博文&#xff0c;希望大家多多支持&#xff5e; &#x1f337; 古之立大事者&#xff0c;不惟有超世之才&#xff0c;亦必有坚忍不拔之志 &#x1f390; 个人CSND主页——Mi…

Wireshark抓包后的报文太大,如何拆分?

背景&#xff1a;抓包获取到一个400多兆的网络数据包.pcapng文件&#xff0c;使用wireshark软件可以正常打开。但需要把文件导出为.json文件&#xff0c;从而方便对报文内容做过滤分析。使用wireshark自带的导出功能导出后发现生成的.json文件大小为2G多&#xff0c;使用notepa…

Java-----Comparable接口和Comparator接口

在Java中&#xff0c;我们会经常使用到自定义类&#xff0c;那我们如何进行自定义类的比较呢? 1.Comparable接口 普通数据的比较 int a10;int b91;System.out.println(a<b); 那自定义类型可不可以这样比较呢&#xff1f;看一下代码 我们发现会报错&#xff0c;因为自定义…

电机控制系列模块解析(25)—— 过压抑制与欠压抑制

一、概念解析 变频器作为一种重要的电机驱动装置&#xff0c;其内置的保护功能对于确保系统安全、稳定运行至关重要。以下是关于变频器过压抑制、欠压抑制&#xff08;晃电抑制&#xff09;、发电功率限制、电动功率限制等保护功能的详细说明&#xff1a; 过压抑制 过压抑制是…

YoloV8改进策略:卷积篇|基于PConv的二次创新|附结构图|性能和精度得到大幅度提高(独家原创)

摘要 在PConv的基础上做了二次创新,创新后的模型不仅在精度和速度上有了质的提升,还可以支持Stride为2的降采样。 改进方法简单高效,需要发论文的同学不要错过! 论文指导 PConv在论文中的描述 论文: 下面我们展示了可以通过利用特征图的冗余来进一步优化成本。如图3所…

golang web补充知识:单元测试

文章目录 golang 单元测试引言单元测试的重要性Go语言单元测试的优势 Go语言单元测试基础testing包辅助测试函数运行单元测试 Mock技术Mock简介GoMock框架介绍编写Mock代码使用Mock进行单元测试 面向测试编程&#xff08;TDD&#xff09;TDD简介TDD的优势TDD的基本步骤Go语言中…

discuz如何添加主导航

大家好&#xff0c;今天教大家怎么样给discuz添加主导航。方法其实很简单&#xff0c;大家跟着我操作既可。一个网站的导航栏是非常重要的&#xff0c;一般用户进入网站的第一印象就是看网站的导航栏。如果大家想看效果的话可以搜索下网创有方&#xff0c;或者直接点击查看效果…

大数据中的电商数仓项目:探秘业务的核心

我学习完一个电商数仓的项目和电影实时推荐项目&#xff0c;便兴冲冲的去面试大数据开发岗&#xff0c;在面试的时候&#xff0c;面试官总是喜欢问&#xff0c;聊聊你为什么要做这个项目以及你这个项目有哪些业务&#xff1f; 我心想&#xff0c;为什么要做这个业务&#xff1f…

重学java 52.集合 斗地主案例

你太锐利了&#xff0c;这些年来&#xff0c;风霜雨雪&#xff0c;踉跄清冷&#xff0c;我相信你所有的苦楚 —— 24.5.30 1 案例介绍 按照斗地主的规则&#xff0c;完成洗牌发牌的动作。 具体规则: 使用54张牌打乱顺序,三个玩家参与游戏&#xff0c;三人交替摸牌&#xff0c…

ipv6基础

地址 前缀子网主机位 PI法则3.14 前缀&#xff1a;3个16位 子网&#xff1a;1个16位 接口ID&#xff1a;4个16位 地址分类 未指定地址 ::/128 &#xff0c;类似于0.0.0.0 本地回环地址 ::1/128 &#xff0c;用于本地测试&#xff0c;类似于127.0.0.1 本地链路地址&#x…

【云原生】kubernetes中pod的生命周期、探测钩子的实战应用案例解析

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

正则匹配优化:匹配排除多个字符串的其他字符串

(^entity|^with|...)\w优化 (?!entity|with|has|index|associations|input)\w(?!): 匹配排除项 效果 继续优化 匹配会过滤掉带有关键字的字段&#xff0c;在过滤的时候是可以加上尾部结束匹配符的 效果&#xff1a;

JS-Lodash工具库

文档&#xff1a;Lodash Documentation orderBy函数&#xff1a;根据条件进行排序 注&#xff1a;第一个是要排序的数组&#xff0c;第二个是根据什么字段进行排序&#xff0c;第三个是排序的方式&#xff08;desc倒序&#xff09; 安装方式&#xff1a;Lodash npm i lodash…

【码银送书第二十期】《游戏运营与出海实战:策略、方法与技巧》

市面上的游戏品种繁杂&#xff0c;琳琅满目&#xff0c;它们是如何在历史的长河中逐步演变成今天的模式的呢&#xff1f;接下来&#xff0c;我们先回顾游戏的发展史&#xff0c;然后按照时间轴来叙述游戏运营的兴起。 作者&#xff1a;艾小米 本文经机械工业出版社授权转载&a…

平衡二叉树(oj题)

一、题目链接&#xff1a; https://leetcode.cn/problems/balanced-binary-tree/submissions/536133365 二、思路 调用深度计算函数&#xff0c;得到每次当前的根结点的左右子树的深度。比较每次得到的左右子树深度之差 如果当前根节点的左右子树的深度差大于1,说明不是平衡…