Qt-FFmpeg开发-打开本地摄像头(6)

news/2024/5/6 15:21:58/文章来源:https://blog.csdn.net/qq_43627907/article/details/128184141

Qt-FFmpeg开发-打开本地摄像头【软解码+ OpenGL显示YUV】

文章目录

  • Qt-FFmpeg开发-打开本地摄像头【软解码+ OpenGL显示YUV】
    • 1、概述
    • 2、实现效果
    • 3、FFmpeg打开摄像头解码流程
    • 4、查询可用摄像头
    • 5、设置摄像头打开参数
    • 6、主要代码
      • 6.1 解码代码
      • 5.2 OpenGL显示RGB图像代码
    • 7、完整源代码

更多精彩内容
👉个人内容分类汇总 👈
👉音视频开发 👈

1、概述

  • 最近研究了一下FFmpeg开发,功能实在是太强大了,网上ffmpeg3、4的文章还是很多的,但是学习嘛,最新的还是不能放过,就选了一个最新的ffmpeg n5.1.2版本,和3、4版本api变化还是挺大的;
  • 在这个Demo里主要使用Qt + FFmpeg开发一个摄像头播放器,这里主要使用的是【软解码】,需要使用硬解码的可以看之前的文章;
  • 在之前的文章中使用了QPainter进行绘制显示,也讲了使用OpenGL显示RGB、YUV图像方式;
  • 由于FFmpeg解码得到的像素格式为YUVJ422P,将YUVJ422P转换为RGB或者YUV都很麻烦,并且会消耗CPU资源,所以这里直接使用OpenGL显示YUVJ422P图像,(将YUVJ422P转RGB的步骤放到了GPU中进行)。

开发环境说明

  • 系统:Windows10、Ubuntu20.04
  • Qt版本:V5.12.5
  • 编译器:MSVC2017-64、GCC/G++64
  • FFmpeg版本:n5.1.2
    • 官方下载
    • 我使用的库

2、实现效果

  1. 使用ffmpeg音视频库【软解码】打开本地摄像头;
  2. 采用【OpenGL显示YUV】图像,支持自适应窗口缩放,支持使用QOpenGLWidget、QOpenGLWindow显示;
  3. 将YUV转RGB的步骤由CPU转换改为使用GPU转换,降低CPU占用率;
  4. 支持Windows、Linux打开本地摄像头;
  5. 视频解码、线程控制、显示各部分功能分离,低耦合度。
  6. 采用最新的5.1.2版本ffmpeg库进行开发,超详细注释信息,将所有踩过的坑、解决办法、注意事项都得很写清楚。

在这里插入图片描述

  • 使用CPU软解码 + OpenGL绘制

在这里插入图片描述

3、FFmpeg打开摄像头解码流程

  • FFmpeg打开本地摄像头与打开网络摄像头、视频文件的区别就是蓝色部分。

在这里插入图片描述

4、查询可用摄像头

  • 打开摄像头之前可通过Qt自带的QCameraInfo类获取系统中可用的摄像头列表;

    • windows下打开摄像头需要在摄像头名称前加上video=,linux下不用。
        // 获取可用摄像头列表QList<QCameraInfo> cameras = QCameraInfo::availableCameras();for(auto camera : cameras){
    #if defined(Q_OS_WIN)ui->com_url->addItem("video=" + camera.description());    // ffmpeg在Window下要使用video=description()
    #elif defined(Q_OS_LINUX)ui->com_url->addItem(camera.deviceName());                // ffmpeg在linux下要使用deviceName()
    #elif defined(Q_OS_MAC)
    #endif}
    

5、设置摄像头打开参数

  • 打开摄像头和读取网络图像不同,可以选择摄像头的分辨率、解码器、帧率、像素格式等信息,设置这些信息需要打开的【摄像头支持】才可以打开,否则会打开失败。

  • 查看摄像头硬件支持的参数:

    • Windows:可使用ffmpeg -list_options true -f dshow -i video="Lenovo EasyCamera"命令查看摄像头支持的编码器、帧率、分辨率等信息;
    • Linux:可使用ffmpeg -list_formats all -i /dev/video0ffplay -f video4linux2 -list_formats all /dev/video0命令查看摄像头支持的支持的像素格式、编解码器和帧大小
        AVDictionary* dict = nullptr;// 设置解码器(Linux下打开本地摄像头默认为rawvideo解码器,输入图像为YUYV420,不方便显示,有两种解决办法,1:使用sws_scale把YUYV422转为YUVJ422P;2:指定mjpeg解码器输出YUVJ422P图像)av_dict_set(&dict, "input_format", "mjpeg", 0);
    //    av_dict_set(&dict, "framerate", "30", 0);             // 设置帧率
    //    av_dict_set(&dict, "pixel_format", "yuvj422p", 0);   // 设置像素格式av_dict_set(&dict, "video_size", "800x600", 0);       // 设置视频分辨率(如果该分辨率摄像头不支持则会报错)
    

    在这里插入图片描述

6、主要代码

  • 啥也不说了,直接上代码,一切有注释

6.1 解码代码

  • videodecode.h文件

    /******************************************************************************* @文件名     videodecode.h* @功能       视频解码类,在这个类中调用ffmpeg打开摄像头进行解码** @开发者     mhf* @邮箱       1603291350@qq.com* @时间       2022/09/15* @备注*****************************************************************************/
    #ifndef VIDEODECODE_H
    #define VIDEODECODE_H#include <QString>
    #include <QSize>struct AVFormatContext;
    struct AVCodecContext;
    struct AVRational;
    struct AVPacket;
    struct AVFrame;
    struct SwsContext;
    struct AVBufferRef;
    struct AVInputFormat;
    class QImage;class VideoDecode
    {
    public:VideoDecode();~VideoDecode();bool open(const QString& url = QString());    // 打开媒体文件,或者流媒体rtmp、strp、httpAVFrame* read();                               // 读取视频图像void close();                                 // 关闭bool isEnd();                                 // 是否读取完成const qint64& pts();                          // 获取当前帧显示时间private:void initFFmpeg();                            // 初始化ffmpeg库(整个程序中只需加载一次)void showError(int err);                      // 显示ffmpeg执行错误时的错误信息qreal rationalToDouble(AVRational* rational); // 将AVRational转换为doublevoid clear();                                 // 清空读取缓冲void free();                                  // 释放private:const AVInputFormat* m_inputFormat = nullptr;AVFormatContext* m_formatContext = nullptr;   // 解封装上下文AVCodecContext*  m_codecContext  = nullptr;   // 解码器上下文SwsContext*      m_swsContext    = nullptr;   // 图像转换上下文AVPacket* m_packet = nullptr;                 // 数据包AVFrame*  m_frame  = nullptr;                 // 解码后的视频帧int    m_videoIndex   = 0;                    // 视频流索引qint64 m_totalTime    = 0;                    // 视频总时长qint64 m_totalFrames  = 0;                    // 视频总帧数qint64 m_obtainFrames = 0;                    // 视频当前获取到的帧数qint64 m_pts          = 0;                    // 图像帧的显示时间qreal  m_frameRate    = 0;                    // 视频帧率QSize  m_size;                                // 视频分辨率大小char*  m_error = nullptr;                     // 保存异常信息bool   m_end = false;                         // 视频读取完成uchar* m_buffer = nullptr;                    // YUV图像需要转换位RGBA图像,这里保存转换后的图形数据
    };#endif // VIDEODECODE_H
  • videodecode.cpp文件

    #include "videodecode.h"
    #include <QDebug>
    #include <QImage>
    #include <QMutex>
    #include <qdatetime.h>extern "C" {        // 用C规则编译指定的代码
    #include "libavcodec/avcodec.h"
    #include "libavformat/avformat.h"
    #include "libavutil/avutil.h"
    #include "libswscale/swscale.h"
    #include "libavutil/imgutils.h"
    #include "libavdevice/avdevice.h"    // 调用输入设备需要的头文件
    }#define ERROR_LEN 1024  // 异常信息数组长度
    #define PRINT_LOG 1VideoDecode::VideoDecode()
    {initFFmpeg();      // 5.1.2版本不需要调用了m_error = new char[ERROR_LEN];/*** dshow:  Windows 媒体输入设备。目前仅支持音频和视频设备。* gdigrab:基于 Win32 GDI 的屏幕捕获设备* video4linux2:Linux输入视频设备*/
    #if defined(Q_OS_WIN)m_inputFormat = av_find_input_format("dshow");            // Windows下如果没有则不能打开摄像头
    #elif defined(Q_OS_LINUX)m_inputFormat = av_find_input_format("video4linux2");       // Linux也可以不需要就可以打开摄像头
    #elif defined(Q_OS_MAC)m_inputFormat = av_find_input_format("avfoundation");
    #endifif(!m_inputFormat){qWarning() << "查询AVInputFormat失败!";}
    }VideoDecode::~VideoDecode()
    {close();
    }/*** @brief 初始化ffmpeg库(整个程序中只需加载一次)*        旧版本的ffmpeg需要注册各种文件格式、解复用器、对网络库进行全局初始化。*        在新版本的ffmpeg中纷纷弃用了,不需要注册了*/
    void VideoDecode::initFFmpeg()
    {static bool isFirst = true;static QMutex mutex;QMutexLocker locker(&mutex);if(isFirst){//        av_register_all();         // 已经从源码中删除/*** 初始化网络库,用于打开网络流媒体,此函数仅用于解决旧GnuTLS或OpenSSL库的线程安全问题。* 一旦删除对旧GnuTLS和OpenSSL库的支持,此函数将被弃用,并且此函数不再有任何用途。*/avformat_network_init();// 初始化libavdevice并注册所有输入和输出设备。avdevice_register_all();isFirst = false;}
    }/*** @brief      打开媒体文件,或者流媒体,例如rtmp、strp、http* @param url  视频地址* @return     true:成功  false:失败*/
    bool VideoDecode::open(const QString &url)
    {if(url.isNull()) return false;AVDictionary* dict = nullptr;/*** Windows:可使用【ffmpeg -list_options true -f dshow -i video="Lenovo EasyCamera"】命令查看摄像头支持的编码器、帧率、分辨率等信息* Linux:可使用【ffmpeg -list_formats all -i /dev/video0】或【ffplay -f video4linux2 -list_formats all /dev/video0】命令查看摄像头支持的支持的像素格式、编解码器和帧大小*/// 设置解码器(Linux下打开本地摄像头默认为rawvideo解码器,输入图像为YUYV420,不方便显示,有两种解决办法,1:使用sws_scale把YUYV422转为YUVJ422P;2:指定mjpeg解码器输出YUVJ422P图像)av_dict_set(&dict, "input_format", "mjpeg", 0);
    //    av_dict_set(&dict, "framerate", "30", 0);             // 设置帧率
    //    av_dict_set(&dict, "pixel_format", "yuvj422p", 0);   // 设置像素格式av_dict_set(&dict, "video_size", "800x600", 0);       // 设置视频分辨率(如果该分辨率摄像头不支持则会报错)// 打开输入流并返回解封装上下文int ret = avformat_open_input(&m_formatContext,          // 返回解封装上下文url.toStdString().data(),  // 打开视频地址m_inputFormat,             // 如果非null,此参数强制使用特定的输入格式。自动选择解封装器(文件格式)&dict);                    // 参数设置// 释放参数字典if(dict){av_dict_free(&dict);}// 打开视频失败if(ret < 0){showError(ret);free();return false;}// 读取媒体文件的数据包以获取流信息。ret = avformat_find_stream_info(m_formatContext, nullptr);if(ret < 0){showError(ret);free();return false;}m_totalTime = m_formatContext->duration / (AV_TIME_BASE / 1000); // 计算视频总时长(毫秒)
    #if PRINT_LOGqDebug() << QString("视频总时长:%1 ms,[%2]").arg(m_totalTime).arg(QTime::fromMSecsSinceStartOfDay(int(m_totalTime)).toString("HH:mm:ss zzz"));
    #endif// 通过AVMediaType枚举查询视频流ID(也可以通过遍历查找),最后一个参数无用m_videoIndex = av_find_best_stream(m_formatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);if(m_videoIndex < 0){showError(m_videoIndex);free();return false;}AVStream* videoStream = m_formatContext->streams[m_videoIndex];  // 通过查询到的索引获取视频流// 获取视频图像分辨率(AVStream中的AVCodecContext在新版本中弃用,改为使用AVCodecParameters)m_size.setWidth(videoStream->codecpar->width);m_size.setHeight(videoStream->codecpar->height);m_frameRate = rationalToDouble(&videoStream->avg_frame_rate);  // 视频帧率// 通过解码器ID获取视频解码器(新版本返回值必须使用const)const AVCodec* codec = avcodec_find_decoder(videoStream->codecpar->codec_id);m_totalFrames = videoStream->nb_frames;#if PRINT_LOGqDebug() << QString("分辨率:[w:%1,h:%2] 帧率:%3  总帧数:%4  解码器:%5").arg(m_size.width()).arg(m_size.height()).arg(m_frameRate).arg(m_totalFrames).arg(codec->name);
    #endif// 分配AVCodecContext并将其字段设置为默认值。m_codecContext = avcodec_alloc_context3(codec);if(!m_codecContext){
    #if PRINT_LOGqWarning() << "创建视频解码器上下文失败!";
    #endiffree();return false;}// 使用视频流的codecpar为解码器上下文赋值ret = avcodec_parameters_to_context(m_codecContext, videoStream->codecpar);if(ret < 0){showError(ret);free();return false;}m_codecContext->flags2 |= AV_CODEC_FLAG2_FAST;    // 允许不符合规范的加速技巧。m_codecContext->thread_count = 8;                 // 使用8线程解码// 初始化解码器上下文,如果之前avcodec_alloc_context3传入了解码器,这里设置NULL就可以ret = avcodec_open2(m_codecContext, nullptr, nullptr);if(ret < 0){showError(ret);free();return false;}// 分配AVPacket并将其字段设置为默认值。m_packet = av_packet_alloc();if(!m_packet){
    #if PRINT_LOGqWarning() << "av_packet_alloc() Error!";
    #endiffree();return false;}// 分配AVFrame并将其字段设置为默认值。m_frame = av_frame_alloc();if(!m_frame){
    #if PRINT_LOGqWarning() << "av_frame_alloc() Error!";
    #endiffree();return false;}// 分配图像空间int size = av_image_get_buffer_size(AV_PIX_FMT_RGBA, m_size.width(), m_size.height(), 4);/*** 【注意:】这里可以多分配一些,否则如果只是安装size分配,大部分视频图像数据拷贝没有问题,*         但是少部分视频图像在使用sws_scale()拷贝时会超出数组长度,在使用使用msvc debug模式时delete[] m_buffer会报错(HEAP CORRUPTION DETECTED: after Normal block(#32215) at 0x000001AC442830370.CRT delected that the application wrote to memory after end of heap buffer)*         特别是这个视频流http://vfx.mtime.cn/Video/2019/02/04/mp4/190204084208765161.mp4*/m_buffer = new uchar[size + 1000];    // 这里多分配1000个字节就基本不会出现拷贝超出的情况了,反正不缺这点内存
    //    m_image = new QImage(m_buffer, m_size.width(), m_size.height(), QImage::Format_RGBA8888);  // 这种方式分配内存大部分情况下也可以,但是因为存在拷贝超出数组的情况,delete时也会报错m_end = false;return true;
    }/*** @brief* @return*/
    AVFrame* VideoDecode::read()
    {// 如果没有打开则返回if(!m_formatContext){return nullptr;}// 读取下一帧数据int readRet = av_read_frame(m_formatContext, m_packet);if(readRet < 0){avcodec_send_packet(m_codecContext, m_packet); // 读取完成后向解码器中传如空AVPacket,否则无法读取出最后几帧}else{if(m_packet->stream_index == m_videoIndex)     // 如果是图像数据则进行解码{// 计算当前帧时间(毫秒)
    #if 1       // 方法一:适用于所有场景,但是存在一定误差m_packet->pts = qRound64(m_packet->pts * (1000 * rationalToDouble(&m_formatContext->streams[m_videoIndex]->time_base)));m_packet->dts = qRound64(m_packet->dts * (1000 * rationalToDouble(&m_formatContext->streams[m_videoIndex]->time_base)));
    #else       // 方法二:适用于播放本地视频文件,计算每一帧时间较准,但是由于网络视频流无法获取总帧数,所以无法适用m_obtainFrames++;m_packet->pts = qRound64(m_obtainFrames * (qreal(m_totalTime) / m_totalFrames));
    #endif// 将读取到的原始数据包传入解码器int ret = avcodec_send_packet(m_codecContext, m_packet);if(ret < 0){showError(ret);}}}av_packet_unref(m_packet);  // 释放数据包,引用计数-1,为0时释放空间av_frame_unref(m_frame);int ret = avcodec_receive_frame(m_codecContext, m_frame);if(ret < 0){av_frame_unref(m_frame);if(readRet < 0){m_end = true;     // 当无法读取到AVPacket并且解码器中也没有数据时表示读取完成}return nullptr;}m_pts = m_frame->pts;return m_frame;
    }/*** @brief 关闭视频播放并释放内存*/
    void VideoDecode::close()
    {clear();free();m_totalTime     = 0;m_videoIndex    = 0;m_totalFrames   = 0;m_obtainFrames  = 0;m_pts           = 0;m_frameRate     = 0;m_size          = QSize(0, 0);
    }/*** @brief  视频是否读取完成* @return*/
    bool VideoDecode::isEnd()
    {return m_end;
    }/*** @brief    返回当前帧图像播放时间* @return*/
    const qint64 &VideoDecode::pts()
    {return m_pts;
    }/*** @brief        显示ffmpeg函数调用异常信息* @param err*/
    void VideoDecode::showError(int err)
    {
    #if PRINT_LOGmemset(m_error, 0, ERROR_LEN);        // 将数组置零av_strerror(err, m_error, ERROR_LEN);qWarning() << "DecodeVideo Error:" << m_error;
    #elseQ_UNUSED(err)
    #endif
    }/*** @brief          将AVRational转换为double,用于计算帧率* @param rational* @return*/
    qreal VideoDecode::rationalToDouble(AVRational* rational)
    {qreal frameRate = (rational->den == 0) ? 0 : (qreal(rational->num) / rational->den);return frameRate;
    }/*** @brief 清空读取缓冲*/
    void VideoDecode::clear()
    {// 因为avformat_flush不会刷新AVIOContext (s->pb)。如果有必要,在调用此函数之前调用avio_flush(s->pb)。if(m_formatContext && m_formatContext->pb){avio_flush(m_formatContext->pb);}if(m_formatContext){avformat_flush(m_formatContext);   // 清理读取缓冲}
    }void VideoDecode::free()
    {// 释放上下文swsContext。if(m_swsContext){sws_freeContext(m_swsContext);m_swsContext = nullptr;             // sws_freeContext不会把上下文置NULL}// 释放编解码器上下文和与之相关的所有内容,并将NULL写入提供的指针if(m_codecContext){avcodec_free_context(&m_codecContext);}// 关闭并失败m_formatContext,并将指针置为nullif(m_formatContext){avformat_close_input(&m_formatContext);}if(m_packet){av_packet_free(&m_packet);}if(m_frame){av_frame_free(&m_frame);}if(m_buffer){delete [] m_buffer;m_buffer = nullptr;}
    }

5.2 OpenGL显示RGB图像代码

  • 鼠标右键->Add New…

    在这里插入图片描述

  • 创建两个GLSL着色器文件

    在这里插入图片描述

  • 创建一个资源文件,将刚创建的两个GLSL文件添加进资源文件

    在这里插入图片描述

  • 结果如下图所示

    在这里插入图片描述

  • 顶点着色器 vertex.vsh

    #version 330 core
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec2 aTexCord;
    out vec2 TexCord;    // 纹理坐标
    void main()
    {gl_Position =  vec4(aPos.x, -aPos.y, aPos.z, 1.0);   // 图像坐标和OpenGL坐标Y轴相反,TexCord = aTexCord;
    }
  • 片段着色器fragment.fsh

    #version 330 core
    in  vec2 TexCord;            // 纹理坐标
    uniform sampler2D tex_y;
    uniform sampler2D tex_u;
    uniform sampler2D tex_v;void main()
    {vec3 yuv;vec3 rgb;// YUV转RGByuv.x = texture2D(tex_y, TexCord).r;yuv.y = texture2D(tex_u, TexCord).r-0.5;yuv.z = texture2D(tex_v, TexCord).r-0.5;rgb = mat3(1.0, 1.0, 1.0,0.0, -0.39465, 2.03211,1.13983, -0.58060, 0.0) * yuv;gl_FragColor = vec4(rgb, 1.0);
    }
  • OpenGL显示YUVJ422P图像这里可以采用QOpenGLWidget或者QOpenGLWIndow进行显示,直接将解码后的AVFrame传入。

  • playimage.h

    /******************************************************************************* @文件名     playimage.h* @功能       使用OpenGL实现YUV图像的绘制,可通过USE_WINDOW宏切换使用QOpenGLWindow还是QOpenGLWidget** @开发者     mhf* @邮箱       1603291350@qq.com* @时间       2022/10/14* @备注*****************************************************************************/
    #ifndef PLAYIMAGE_H
    #define PLAYIMAGE_H#include <QWidget>
    #include <QOpenGLFunctions_3_3_Core>
    #include <qopenglshaderprogram.h>
    #include <QOpenGLTexture>
    #include <qopenglpixeltransferoptions.h>struct AVFrame;#define USE_WINDOW 0    // 1:使用QOpenGLWindow显示, 0:使用QOpenGLWidget显示#if USE_WINDOW
    #include <QOpenGLWindow>
    class PlayImage : public QOpenGLWindow, public  QOpenGLFunctions_3_3_Core
    #else
    #include <QOpenGLWidget>
    class PlayImage : public QOpenGLWidget, public  QOpenGLFunctions_3_3_Core
    #endif
    {Q_OBJECT
    public:
    #if USE_WINDOWexplicit PlayImage(UpdateBehavior updateBehavior = NoPartialUpdate, QWindow *parent = nullptr);
    #elseexplicit PlayImage(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
    #endif~PlayImage() override;void repaint(AVFrame* frame);             // 重绘protected:void initializeGL() override;               // 初始化glvoid resizeGL(int w, int h) override;       // 窗口尺寸变化void paintGL() override;                    // 刷新显示private:QOpenGLShaderProgram* m_program = nullptr;QOpenGLTexture* m_texY = nullptr;           // 存储YUV图像中的Y数据QOpenGLTexture* m_texU = nullptr;           // 存储YUV图像中的U数据QOpenGLTexture* m_texV = nullptr;           // 存储YUV图像中的V数据QOpenGLPixelTransferOptions m_options;GLuint VBO = 0;       // 顶点缓冲对象,负责将数据从内存放到缓存,一个VBO可以用于多个VAOGLuint VAO = 0;       // 顶点数组对象,任何随后的顶点属性调用都会储存在这个VAO中,一个VAO可以有多个VBOGLuint EBO = 0;       // 元素缓冲对象,它存储 OpenGL 用来决定要绘制哪些顶点的索引QSize  m_size;QSizeF  m_zoomSize;QPointF m_pos;
    };#endif // PLAYIMAGE_H
  • playimage.cpp

    #include "playimage.h"extern "C" {        // 用C规则编译指定的代码
    #include "libavcodec/avcodec.h"}#if USE_WINDOW
    PlayImage::PlayImage(QOpenGLWindow::UpdateBehavior updateBehavior, QWindow *parent):QOpenGLWindow(updateBehavior, parent)
    {// 初始化视图大小,由于Shader里面有YUV转RGB的代码,会初始化显示为绿色,这里通过将视图大小设置为0避免显示绿色背景m_pos = QPointF(0, 0);m_zoomSize = QSize(0, 0);
    }
    #else
    PlayImage::PlayImage(QWidget *parent, Qt::WindowFlags f): QOpenGLWidget(parent, f)
    {// 初始化视图大小,由于Shader里面有YUV转RGB的代码,会初始化显示为绿色,这里通过将视图大小设置为0避免显示绿色背景m_pos = QPointF(0, 0);m_zoomSize = QSize(0, 0);
    }
    #endifPlayImage::~PlayImage()
    {if(!isValid()) return;        // 如果控件和OpenGL资源(如上下文)已成功初始化,则返回true。this->makeCurrent(); // 通过将相应的上下文设置为当前上下文并在该上下文中绑定帧缓冲区对象,为呈现此小部件的OpenGL内容做准备。// 释放纹理if(m_texY){m_texY->destroy();delete m_texY;}if(m_texU){m_texU->destroy();delete m_texU;}if(m_texV){m_texV->destroy();delete m_texV;}this->doneCurrent();    // 释放上下文// 释放glDeleteBuffers(1, &VBO);glDeleteBuffers(1, &EBO);glDeleteVertexArrays(1, &VAO);
    }void PlayImage::repaint(AVFrame *frame)
    {// 如果帧长宽为0则不需要绘制if(!frame || frame->width == 0 || frame->height == 0) return;// 当切换显示的视频后,如果分辨率不同则需要重新创建纹理,否则会崩溃if(frame->width != m_size.width() || frame->height != m_size.height()){if(m_texY && m_texU && m_texV){m_texY->destroy();m_texU->destroy();m_texV->destroy();delete m_texY;delete m_texU;delete m_texV;m_texY = nullptr;m_texU = nullptr;m_texV = nullptr;}}if(!m_texY) // 初始化纹理{// 创建2D纹理m_texY = new QOpenGLTexture(QOpenGLTexture::Target2D);m_texU = new QOpenGLTexture(QOpenGLTexture::Target2D);m_texV = new QOpenGLTexture(QOpenGLTexture::Target2D);// 设置纹理大小                                                //  YUV444    YUV422    YUV422m_texY->setSize(frame->width, frame->height);                //   w,h      w,h       w,hm_texU->setSize(frame->width / 2, frame->height);            //   w,h      w/2,h      w/2,h/2m_texV->setSize(frame->width / 2, frame->height);            //   w,h      w/2,h      w/2,h/2// 设置放大、缩小过滤器m_texY->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Linear);m_texU->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Linear);m_texV->setMinMagFilters(QOpenGLTexture::LinearMipMapLinear,QOpenGLTexture::Linear);// 设置图像格式m_texY->setFormat(QOpenGLTexture::R8_UNorm);m_texU->setFormat(QOpenGLTexture::R8_UNorm);m_texV->setFormat(QOpenGLTexture::R8_UNorm);// 分配内存m_texY->allocateStorage();m_texU->allocateStorage();m_texV->allocateStorage();// 记录图像分辨率m_size.setWidth(frame->width);m_size.setHeight(frame->height);resizeGL(this->width(), this->height());}m_options.setImageHeight(frame->height);m_options.setRowLength(frame->linesize[0]);m_texY->setData(QOpenGLTexture::Red, QOpenGLTexture::UInt8, static_cast<const void *>(frame->data[0]), &m_options);   // 设置图像数据 Ym_options.setRowLength(frame->linesize[1]);m_texU->setData(QOpenGLTexture::Red, QOpenGLTexture::UInt8, static_cast<const void *>(frame->data[1]), &m_options);   // 设置图像数据 Um_options.setRowLength(frame->linesize[2]);m_texV->setData(QOpenGLTexture::Red, QOpenGLTexture::UInt8, static_cast<const void *>(frame->data[2]), &m_options);   // 设置图像数据 Vav_frame_unref(frame);  //  取消引用帧引用的所有缓冲区并重置帧字段。this->update();
    }// 三个顶点坐标XYZ,VAO、VBO数据播放,范围时[-1 ~ 1]直接
    static GLfloat vertices[] = {  // 前三列点坐标,后两列为纹理坐标1.0f,  1.0f, 0.0f, 1.0f, 1.0f,      // 右上角1.0f, -1.0f, 0.0f, 1.0f, 0.0f,      // 右下-1.0f, -1.0f, 0.0f, 0.0f, 0.0f,      // 左下-1.0f,  1.0f, 0.0f, 0.0f, 1.0f      // 左上
    };
    static GLuint indices[] = {0, 1, 3,1, 2, 3
    };
    void PlayImage::initializeGL()
    {initializeOpenGLFunctions();// 加载shader脚本程序m_program = new QOpenGLShaderProgram(this);m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/vertex.vsh");m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/fragment.fsh");m_program->link();// 绑定YUV 变量值m_program->bind();m_program->setUniformValue("tex_y", 0);m_program->setUniformValue("tex_u", 1);m_program->setUniformValue("tex_v", 2);// 返回属性名称在此着色器程序的参数列表中的位置。如果名称不是此着色器程序的有效属性,则返回-1。GLuint posAttr = GLuint(m_program->attributeLocation("aPos"));GLuint texCord = GLuint(m_program->attributeLocation("aTexCord"));glGenVertexArrays(1, &VAO);glBindVertexArray(VAO);glGenBuffers(1, &VBO);glBindBuffer(GL_ARRAY_BUFFER, VBO);glGenBuffers(1, &EBO);    // 创建一个EBOglBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);// 为当前绑定到的缓冲区对象创建一个新的数据存储target。任何预先存在的数据存储都将被删除。glBufferData(GL_ARRAY_BUFFER,        // 为VBO缓冲绑定顶点数据sizeof (vertices),      // 数组字节大小vertices,               // 需要绑定的数组GL_STATIC_DRAW);        // 指定数据存储的预期使用模式,GL_STATIC_DRAW: 数据几乎不会改变glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);  // 将顶点索引数组传入EBO缓存// 设置顶点坐标数据glVertexAttribPointer(posAttr,                     // 指定要修改的通用顶点属性的索引3,                     // 指定每个通用顶点属性的组件数(如vec3:3,vec4:4)GL_FLOAT,              // 指定数组中每个组件的数据类型(数组中一行有几个数)GL_FALSE,              // 指定在访问定点数据值时是否应规范化 ( GL_TRUE) 或直接转换为定点值 ( GL_FALSE),如果vertices里面单个数超过-1或者1可以选择GL_TRUE5 * sizeof(GLfloat),   // 指定连续通用顶点属性之间的字节偏移量。nullptr);              // 指定当前绑定到目标的缓冲区的数据存储中数组中第一个通用顶点属性的第一个组件的偏移量。初始值为0 (一个数组从第几个字节开始读)// 启用通用顶点属性数组glEnableVertexAttribArray(posAttr);                // 属性索引是从调用glGetAttribLocation接收的,或者传递给glBindAttribLocation。// 设置纹理坐标数据glVertexAttribPointer(texCord, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), reinterpret_cast<const GLvoid *>(3 * sizeof (GLfloat)));              // 指定当前绑定到目标的缓冲区的数据存储中数组中第一个通用顶点属性的第一个组件的偏移量。初始值为0 (一个数组从第几个字节开始读)// 启用通用顶点属性数组glEnableVertexAttribArray(texCord);                // 属性索引是从调用glGetAttribLocation接收的,或者传递给glBindAttribLocation。// 释放glBindBuffer(GL_ARRAY_BUFFER, 0);glBindVertexArray(0);                        // 设置为零以破坏现有的顶点数组对象绑定glClearColor(0.0f, 0.0f, 0.0f, 1.0f);        // 指定颜色缓冲区的清除值(背景色)}void PlayImage::resizeGL(int w, int h)
    {if(m_size.width()  < 0 || m_size.height() < 0) return;// 计算需要显示图片的窗口大小,用于实现长宽等比自适应显示if((double(w) / h) < (double(m_size.width()) / m_size.height())){m_zoomSize.setWidth(w);m_zoomSize.setHeight(((double(w) / m_size.width()) * m_size.height()));   // 这里不使用QRect,使用QRect第一次设置时有误差bug}else{m_zoomSize.setHeight(h);m_zoomSize.setWidth((double(h) / m_size.height()) * m_size.width());}m_pos.setX(double(w - m_zoomSize.width()) / 2);m_pos.setY(double(h - m_zoomSize.height()) / 2);this->update(QRect(0, 0, w, h));
    }void PlayImage::paintGL()
    {glClear(GL_COLOR_BUFFER_BIT);     // 将窗口的位平面区域(背景)设置为先前由glClearColor、glClearDepth和选择的值glViewport(m_pos.x(), m_pos.y(), m_zoomSize.width(), m_zoomSize.height());  // 设置视图大小实现图片自适应m_program->bind();               // 绑定着色器// 绑定纹理if(m_texY && m_texU && m_texV){m_texY->bind(0);m_texU->bind(1);m_texV->bind(2);}glBindVertexArray(VAO);           // 绑定VAOglDrawElements(GL_TRIANGLES,      // 绘制的图元类型6,                 // 指定要渲染的元素数(点数)GL_UNSIGNED_INT,   // 指定索引中值的类型(indices)nullptr);          // 指定当前绑定到GL_ELEMENT_array_buffer目标的缓冲区的数据存储中数组中第一个索引的偏移量。glBindVertexArray(0);if(m_texY && m_texU && m_texV){m_texY->release();m_texU->release();m_texV->release();}m_program->release();
    }

7、完整源代码

  • github
  • gitee

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

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

相关文章

【前端】面试题1~5

目录 一、说一说cookie、sessionStorage和localStorage 区别&#xff1f; 1、相同点 2、区别 二、说一说JS数据类型有哪些,区别是什么&#xff1f; &#x1f342; 1、数据类型 &#x1f342; 2、数据类型区别 &#x1f342; 3、注意 &#x1f342; 4、总结 三、说一说…

企业子网划分详解

一、IP协议 1、IP协议简介 IP协议是TCP/IP协议族的基石,它为上层提供无状态、无连接、不可靠的服务,也是Socket网络编程的基础之一。 IP协议特点: 无状态:指IP通信双方不同步传输数据的状态信息,因此所有IP数据报的发送,传输,接收都是相互独立的。这种服务最大缺点是…

html+css鼠标悬停发光按钮![HTML鼠标悬停的代码]使用HTML + CSS实现鼠标悬停的一些奇幻效果!

源码如下: <!DOCTYPE html> <html> <head> <meta charset"utf-8"> <title></title> <style> *{ /* 初始化 清除页面元素得内外边距 */ paddin…

Python开发6年,整理的《Python从入门到精通学习笔记》免费下载

前言 首先明确一点&#xff1a;为什么要学习python&#xff1f; 我说几个最主要的。 1.简单易学&#xff0c;入门友好 python其实就是英文句子&#xff0c;你只要能够认识基本的英文单词&#xff0c;你就可以非常熟练地使用python。 &#xff08;文末送读者福利&#xff09…

第二证券|沪指冲上3200点,中字头太火爆!

轿车整车概念股拉升&#xff0c;多要素共同刺激下&#xff0c;新能源车或将呈现一轮涨价潮。 今天早盘&#xff0c;三大股指走势分解&#xff0c;沪指拉升涨1.56%&#xff0c;站上3200点关口&#xff1b;创业板指则围绕平盘线展开震动&#xff0c;跌0.39%。 以稳妥股为首的大金…

微服务架构下的认证鉴权解决方案

背景 单体应用在向微服务化架构演进时&#xff0c;需要考虑如何解决服务认证授权的问题。如果处理不好&#xff0c;会引发架构的混乱&#xff0c;带来安全、性能、难以维护的问题。 以最典型的包含WEB页面的具备登录态管理的系统为例。在最初阶段&#xff0c;登录鉴权一般通过…

MySQL学习笔记(十三)count(*),count(id),count(1),count(字段)区别

count count 是MySQL的一个查询数量统计的函数&#xff0c;我们在平常的工作中经常会用到&#xff0c;count(*),count(id),count(1),count(字段)这4种写法有什么区别呢&#xff1f; //星号 select count(*) from user; //常数 select count(1) from user; //id(主键) select …

[附源码]计算机毕业设计JAVA疫情居家隔离服务系统

[附源码]计算机毕业设计JAVA疫情居家隔离服务系统 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM my…

易基因课程回顾|表观遗传学和表观育种在品种改良中的应用研究

大家好&#xff0c;这里是专注表观组学十余年&#xff0c;领跑多组学科研服务的易基因。 传统作物育种包括杂交、选择所需性状的遗传变异&#xff0c;导致遗传基础缩窄和遗传多样性缺失&#xff0c;从而阻碍作物改良。表型性状受遗传学和表观遗传学影响&#xff0c;利用表观遗传…

Java进阶架构师之如何画好架构图?阿里大神手把手教你!

1、什么是架构 架构就是对系统中的实体以及实体之间的关系所进行的抽象描述&#xff0c;是一系列的决策。 架构是结构和愿景。 系统架构是概念的体现&#xff0c;是对物/信息的功能与形式元素之间的对应情况所做的分配&#xff0c;是对元素之间的关系以及元素同周边环境之间…

小程序webView页面转发后,进入页面空白

小程序webView页面&#xff0c;在点击右上角按钮分享后&#xff0c;进入分享的链接页面空白 重新进入页面后&#xff0c;页面空白。使用电脑打开之后报错提示如下 一、排查页面转发后&#xff0c;页面地址有没有解码 webview页面转发后&#xff0c;小程序会将url参数转码&…

莱特飞行优化及其使用场景

莱特飞行优化及其使用场景 一&#xff0c;莱特飞行 但是产生一个莱特飞行分布的随机数是比较难的&#xff0c;莱特只给出了一个积分&#xff0c;所以后面就有人提出了如何制造这样分布的随机数。 Mantegna 在1994年提出的一种用正态分布求解随机数的方法&#xff0c;有时也叫…

【多线程(三)】生产者和消费者模式

文章目录3.生产者和消费者模式前言3.1生产者和消费者模式概述3.2生产者和消费者案例3.3 阻塞队列基本使用3.4 阻塞队列实现等待唤醒机制总结3.生产者和消费者模式 前言 在线程世界里&#xff0c;生产者就是生产数据的线程&#xff0c;消费者就是消费数据的线程。在多线 程开发…

使用OpenCV的函数hconcat()、vconcat()实现图像或矩阵的连接

使用OpenCV的函数hconcat()、vconcat()实现图像或矩阵的连接 函数hconcat()在水平方向上连接图像或矩阵&#xff1b; 函数vconcat()在垂直方向上连接图像或矩阵。 两个函数的原型和使用方法一模一样&#xff0c;所以在下面的函数原型介绍中&#xff0c;只介绍函数hconcat()的…

人工智能:声纹相关基础概念介绍

❤️作者主页&#xff1a;IT技术分享社区 ❤️作者简介&#xff1a;大家好,我是IT技术分享社区的博主&#xff0c;从事C#、Java开发九年&#xff0c;对数据库、C#、Java、前端、运维、电脑技巧等经验丰富。 ❤️个人荣誉&#xff1a; 数据库领域优质创作者&#x1f3c6;&#x…

软件测试工程师涨薪攻略!3年如何达到30K!

1.软件测试如何实现涨薪 首先涨薪并不是从8000涨到9000这种涨薪&#xff0c;而是从8000涨到15K加到25K的涨薪。基本上三年之内就可以实现。 如果我们只是普通的有应届毕业生或者是普通本科那我们就只能从小公司开始慢慢往上走。 有些同学想去做测试&#xff0c;是希望能够日…

[附源码]计算机毕业设计设备运维平台出入库模块APPSpringboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【数据结构】建堆的方式、堆排序以及TopK问题

建堆的方式、堆排序以及TopK问题1、建堆的两种方式1.1 向上调整建堆1.2 向下调整建堆2、堆排序3、TopK问题4、建堆、堆排序、TopK问题全部代码1、建堆的两种方式 我们知道&#xff0c;堆是二叉树的一种&#xff0c;二叉树的建立是借助结构体与数组完成的&#xff08;通过在结构…

Java IO流(详解)

1. File1. 创建2. 操作1. 获取文件信息2. 目录创建/删除2. IO流1. FileInputStream1. 简单使用2. 读取中文2. FileOutputStream1. 简单使用2. 追加写入3. 文件拷贝4. FileReader1. 简单使用2. 提高读取速度5. FileWriter1. 简单使用6. 节点流和处理流简介7. BufferedReader1. 简…

yolo后处理操作-如何获取我们想要的目标框及置信度?

yolo后处理就是模型的输出进行处理&#xff0c;得到我们想要的坐标框的xywhxywhxywh以及confidenceconfidenceconfidence 学习笔记 这是yolov1的模型&#xff0c;他将图像划分成了7x7个网格&#xff0c;每个网格负责预测两个边界框&#xff0c;每个边界框都有5个信息$x、y、w、…