Qt-FFmpeg开发-打开本地摄像头录制视频(7)

news2024/10/7 0:54:45

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

文章目录

  • Qt-FFmpeg开发-打开本地摄像头录制视频【软解码+ OpenGL显示YUV】
    • 1、概述
    • 2、实现效果
    • 3、FFmpeg录制视频编码流程
    • 4、主要代码
    • 5、完整源代码

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

1、概述

  • 最近研究了一下FFmpeg开发,功能实在是太强大了,网上ffmpeg3、4的文章还是很多的,但是学习嘛,最新的还是不能放过,就选了一个最新的ffmpeg n5.1.2版本,和3、4版本api变化还是挺大的;
  • 在这个Demo里主要使用Qt + FFmpeg开发一个摄像头【录像机】,这里主要使用的是【软解码】,需要使用硬解码的可以看之前的文章;
  • 在之前的文章中使用了QPainter进行绘制显示,也讲了使用OpenGL显示RGB、YUV图像方式;
  • 由于FFmpeg解码得到的像素格式为YUVJ422P,将YUVJ422P转换为RGB或者YUV420p都很麻烦,并且会消耗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. 视频解码、线程控制、显示各部分功能分离,低耦合度。
  7. 采用最新的5.1.2版本ffmpeg库进行开发,超详细注释信息,将所有踩过的坑、解决办法、注意事项都得很写清楚。

在这里插入图片描述

  • 使用CPU软解码 + OpenGL绘制 + CPU软编码录制

在这里插入图片描述

3、FFmpeg录制视频编码流程

  • 白色部分主要为创建、设置信息步骤,蓝色部分主要为写入数据步骤。

在这里插入图片描述

4、主要代码

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

  • videosave.h文件

    /******************************************************************************
     * @文件名     videosave.h
     * @功能       将视频编码后保存到文件中
     *
     * @开发者     mhf
     * @邮箱       1603291350@qq.com
     * @时间       2022/11/29
     * @备注
     *****************************************************************************/
    #ifndef VIDEOSAVE_H
    #define VIDEOSAVE_H
    
    #include <QString>
    #include <qmutex.h>
    
    
    struct AVCodecParameters;
    struct AVFormatContext;
    struct AVCodecContext;
    struct AVStream;
    struct AVFrame;
    struct AVPacket;
    struct AVOutputFormat;
    
    class VideoSave
    {
    public:
        VideoSave();
        ~VideoSave();
    
        bool open(AVStream *inStream, const QString& fileName);
        void write(AVFrame* frame);
        void close();
    
    private:
        void showError(int err);
    
    private:
        AVFormatContext* m_formatContext = nullptr;
        AVCodecContext * m_codecContext  = nullptr;    // 编码器上下文
        AVStream       * m_videoStream   = nullptr;
        AVPacket       * m_packet        = nullptr;    // 数据包
        int m_index = 0;
        bool             m_writeHeader   = false;      // 是否写入头
        QMutex           m_mutex;
    };
    
    #endif // VIDEOSAVE_H
    
  • videosave.cpp文件

    #include "videosave.h"
    #include <QDebug>
    
    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 1
    #define USE_H264 0      // 使用H264编码器
    
    VideoSave::VideoSave()
    {
    }
    
    VideoSave::~VideoSave()
    {
        close();
    }
    
    /**
     * @brief        显示ffmpeg函数调用异常信息
     * @param err
     */
    void VideoSave::showError(int err)
    {
    #if PRINT_LOG
        static char  m_error[ERROR_LEN];         // 保存异常信息
        memset(m_error, 0, ERROR_LEN);           // 将数组置零
        av_strerror(err, m_error, ERROR_LEN);
        qWarning() << "VideoSave Error:" << m_error;
    #else
        Q_UNUSED(err)
    #endif
    }
    
    bool VideoSave::open(AVStream *inStream, const QString &fileName)
    {
        if(!inStream || fileName.isEmpty()) return false;
    
        // 通过输出文件名为输出格式分配AVFormatContext。
    #if USE_H264
        int ret = avformat_alloc_output_context2(&m_formatContext, nullptr, "h264", fileName.toStdString().data());
    #else
        /**
         * 摄像头打开使用的是mjpeg编码器;
         * MJPEG压缩技术可以获取清晰度很高的视频图像,可以【动态调整帧率】适合保存摄像头视频、分辨率。但由于没有考虑到帧间变化,造成大量冗余信息被重复存储,因此单帧视频的占用空间较大;
         * 如果采用其它编码器,由于摄像头曝光时间长度不一定,所以录像时帧率一直在变,编码器指定固定帧率会导致视频一会快一会慢,效果很不好,适用于录制固定帧率的视频(当然其它编码器应该是有处理办法,不过我还不清楚);
         */
        QString strName = avcodec_find_encoder(inStream->codecpar->codec_id)->name;    // 获取编码器名称
        int ret = avformat_alloc_output_context2(&m_formatContext, nullptr, strName.toStdString().data(), fileName.toStdString().data());  // 这里使用和解码一样的编码器,防止保存的图像颜色出问题
    #endif
        if(ret < 0)
        {
            close();
            showError(ret);
            return false;
        }
        // 创建并初始化AVIOContext以访问url所指示的资源。
        ret = avio_open(&m_formatContext->pb, fileName.toStdString().data(), AVIO_FLAG_WRITE);
        if(ret < 0)
        {
            close();
            showError(ret);
            return false;
        }
    
    
        // 查询编码器
        const AVCodec* codec = avcodec_find_encoder(m_formatContext->oformat->video_codec);
        if(!codec)
        {
            close();
            showError(AVERROR(ENOMEM));
            return false;
        }
    
        // 分配AVCodecContext并将其字段设置为默认值。
        m_codecContext = avcodec_alloc_context3(codec);
        if(!m_codecContext)
        {
            close();
            showError(AVERROR(ENOMEM));
            return false;
        }
        // 设置编码器上下文参数
        m_codecContext->width = inStream->codecpar->width;                          // 图片宽度/高度
        m_codecContext->height = inStream->codecpar->height;
    #if USE_H264
        m_codecContext->pix_fmt = AV_PIX_FMT_YUV420P;
    #else
        m_codecContext->pix_fmt = AVPixelFormat(inStream->codecpar->format);        // 像素格式,也可以使用codec->pix_fmts[0]或AV_PIX_FMT_YUVJ422P(【注意】摄像头解码的图像格式为yuvj422p,如果这里不一样可能保存会出问题,或者后面进行格式转换)
    #endif
        m_codecContext->time_base = {1, 10};                   //设置时间基,20为分母,1为分子,表示以1/20秒时间间隔播放一帧图像
        m_codecContext->framerate = {10, 1};
        m_codecContext->bit_rate = 4000000;                    // 目标的码率,即采样的码率;显然,采样码率越大,视频大小越大,画质越高
        m_codecContext->gop_size = 10;                         // I帧间隔
        m_codecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    //    m_codecContext->max_b_frames = 1;                      // 非B帧之间的最大B帧数(有些格式不支持)
    //    m_codecContext->qmin = 1;
    //    m_codecContext->qmax = 5;
    //    m_codecContext->colorspace = AVCOL_SPC_BT470BG;
    //    m_codecContext->color_range = AVCOL_RANGE_JPEG;
    //    m_codecContext->color_primaries = AVCOL_PRI_BT709;
    //    m_codecContext->bits_per_coded_sample = 24;
    //    m_codecContext->bits_per_raw_sample = 8;
    //    av_opt_set(m_codecContext->priv_data, "preset", "placebo", 0);
    //    qDebug() << m_codecContext->pix_fmt;
    
        // 打开编码器
        ret = avcodec_open2(m_codecContext, nullptr, nullptr);
    #if USE_H264
        ret = avcodec_open2(m_codecContext, codec, nullptr);      // 使用h264时第一次打不开,第二次可以打卡,不知道什么原因
    #endif
        if(ret < 0)
        {
            close();
            showError(ret);
            return false;
        }
    
        // 向媒体文件添加新流
        m_videoStream = avformat_new_stream(m_formatContext, nullptr);
        if(!m_videoStream)
        {
            close();
            showError(AVERROR(ENOMEM));
            return false;
        }
    
        //拷贝一些参数,给codecpar赋值
        ret = avcodec_parameters_from_context(m_videoStream->codecpar,m_codecContext);
        if(ret < 0)
        {
            close();
            showError(ret);
            return false;
        }
    
        // 写入文件头
        ret = avformat_write_header(m_formatContext, nullptr);
        if(ret < 0)
        {
            close();
            showError(ret);
            return false;
        }
        m_writeHeader = true;
    
        // 分配一个AVPacket
        m_packet = av_packet_alloc();
        if(!m_packet)
        {
            close();
            showError(AVERROR(ENOMEM));
            return false;
        }
        qDebug() << "开始录制视频!";
        return true;
    }
    
    /**
     * @brief        写入数据
     * @param frame
     */
    void VideoSave::write(AVFrame *frame)
    {
        QMutexLocker locker(&m_mutex);
        if(!m_packet)
        {
            return;
        }
    
        if(frame)
        {
            frame->pts = m_index;    // 注意:每一帧视频显示时间从0递增,否则录制的视频显示/时长会不对
            m_index++;
        }
    
        // 将图像传入编码器
        avcodec_send_frame(m_codecContext, frame);
    
        // 循环读取所有编码完的帧
        while (true)
        {
            // 从编码器中读取图像帧
            int ret = avcodec_receive_packet(m_codecContext, m_packet);
            if(ret < 0)
            {
                break;
            }
    
            // 将数据包中的有效时间字段(时间戳/持续时间)从一个时基转换为 输出流的时间
            av_packet_rescale_ts(m_packet, m_codecContext->time_base, m_videoStream->time_base);
            av_write_frame(m_formatContext, m_packet);   // 将数据包写入输出媒体文件
            av_packet_unref(m_packet);
        }
    }
    
    /**
     * @brief 关闭保存数据
     */
    void VideoSave::close()
    {
        write(nullptr);   // 传入空帧,读取所有编码数据
        QMutexLocker locker(&m_mutex);    // 如果不加锁可能在点击关闭时,write函数正在写入数据,导致崩溃
        if(m_formatContext)
        {
            // 写入文件尾
            if(m_writeHeader)
            {
                m_writeHeader = false;
                int ret = av_write_trailer(m_formatContext);
                if(ret < 0)
                {
                    showError(ret);
                    return;
                }
            }
            int ret = avio_close(m_formatContext->pb);
            if(ret < 0)
            {
                showError(ret);
                return;
            }
            avformat_free_context(m_formatContext);
            m_formatContext = nullptr;
        }
        // 释放编解码器上下文并置空
        if(m_codecContext)
        {
            avcodec_free_context(&m_codecContext);
        }
        if(m_packet)
        {
            av_packet_free(&m_packet);
            qDebug() << "停止录制视频!";
        }
    }
    

5、完整源代码

  • github
  • gitee

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

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

相关文章

【车辆动力】基于matlab模拟停车动力学【含Matlab源码 2258期】

⛄一、获取代码方式 获取代码方式1: 完整代码已上传我的资源:【车辆动力】基于matlab模拟停车动力学【含Matlab源码 2258期】 点击上面蓝色字体,直接付费下载,即可。 获取代码方式2: 付费专栏物理应用(Matlab) 备注: 点击上面蓝色字体付费专栏物理应用(Matlab),扫…

【教学类-20-02】20221203《世界杯16强国旗-定量版》(大班)

展示效果&#xff1a; &#xff08;1个国家2张&#xff0c;16国旗&#xff0c;共32张&#xff09; 打印效果&#xff1a; 背景需求&#xff1a; 上一份代码打印后发现&#xff1a; 1.打印时发现随机抽取的图案不稳定&#xff0c;30张”澳大利亚”“波兰”的图片特别多。因为…

STM32定时器笔记

学习江科大自化协的stm32教程记录的笔记 一、TIM定时器 定时器可以对输入的时钟进行计数&#xff0c;并在计数值达到设定值时触发中断 例&#xff1a;stm32中定时器的基准时钟一般是72MHZ&#xff0c;【周期是频率的倒数1T 1/72us】&#xff0c;如果计数72个&#xff0c;就是…

[附源码]Python计算机毕业设计Django松林小区疫情防控信息管理系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;…

Netty(三)- NIO三大组件之Channel

文章目录一、Channel 基本介绍二、FileChannel 类三、Channel 应用案例1. 应用实例 1 - 本地文件写数据2. 应用实例 2 - 本地文件读数据3. 应用实例 3 - 使用一个Buffer 完成文件读取、写入4. 应用实例 4 - 拷贝文件transferFrom方法一、Channel 基本介绍 NIO的通道类似于流&am…

Mybatis:自定义映射resultMap(7)

Mybaits笔记框架&#xff1a;https://blog.csdn.net/qq_43751200/article/details/128154837 自定义映射resultMap1. Mybatis环境搭建2. 问题引入3. 解决表中的字段名和对应类的属性名不一致问题方式一&#xff1a; 为字段起别名&#xff0c;保持和属性名的一致方式二&#xff…

html静态网站基于HTML+CSS+JavaScript上海美食介绍网站网页设计与实现共计5个页面

&#x1f380; 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业…

K邻近算法k值选取以及kd树概念、原理、构建方法、最近邻域搜索和案例分析

一、k值选择 K值过小&#xff1a;容易受到异常点的影响k值过大&#xff1a;受到样本均衡的问题 近似误差&#xff1a;对现有训练集的训练误差&#xff0c;关注训练集&#xff0c;如果近似误差过小可能会出现过拟合的现象&#xff0c;对现有的训练集能有很好的预测&#xff0c;…

基于RockyLinux8.7一键安装OpenStack Yoga版本

硬件环境 虚拟软件&#xff1a;vmware workstation16 操作系统&#xff1a;RockyLinux8 虚拟机硬件配置&#xff1a; CPU&#xff1a;2 memory&#xff1a;8G disk&#xff1a;80G net card&#xff1a;1个—VMnet8 ip/netmask&#xff1a;192.168.9.160/24 下载并安装RockyL…

python:最小二乘法拟合原理及代码实现

这里写目录标题原理代码实现原理 最小二乘法适用于对处理的一堆数据&#xff0c;不必精确的经过每一点&#xff0c;而是根据图像到每个数据点的距离和最小确定函数。需要注意的是&#xff0c;最小二乘是对全局进行拟合优化&#xff0c;对噪声比较敏感&#xff0c;所以如果有噪…

知识点7--Docker的容器命令

本篇为大家介绍Docker的容器命令&#xff0c;也顺带着让大家明白Docker和vmware都属于虚拟化技术下的软件&#xff0c;但是他们的不同之处不止在于运行的系统不同&#xff0c;他们的运行逻辑也不同&#xff0c;VMware是虚拟化完整的系统&#xff0c;而docker是隔离一个进程&…

03 - 调试环境的搭建(Bochs)(实验未完)

---- 整理自狄泰软件唐佐林老师课程 1. Bochs&#xff08;另一款优秀的虚拟机软件&#xff09; 专业模拟x86架构的虚拟机 开源且高度可移植&#xff0c;由C编写完成 支持操作系统开发过程中的断点调试 通过简单配置就能运行绝大多数主流的操作系统 2. Bochs的安装与配置 下载…

数字化升级里,RPA的下一步正在走向哪?

如果说&#xff0c;API这种能力在2021年并未成为“刚需”&#xff0c;那么在2022年其已经一跃成为RPA进入企业真正场景的“必需品”。 作者|斗斗 编辑|皮爷 出品|产业家 今年八月&#xff0c;调查机构Gartner发布了2022全球RPA魔力象限。 数据显示&#xff0c;2021年&a…

空间直接坐标系(XYZ)转经纬度(BLH)

本章首先介绍空间直角坐标系与大地坐标系&#xff0c;然后列出XYZ转换BLH的公式&#xff0c;最后基于C语言完成该部分代码设计。 参考书籍&#xff1a; 董大男&#xff0c;陈俊平&#xff0c;王解先等&#xff0c;GNSS高精度定位原理&#xff0c;科学出版社 黄丁发&#xff0c;…

图扑软件荣获第七届“创客中国”中小企业创新创业大赛优胜奖

2022 年 11 月 17 日&#xff0c;由工业和信息化部、财政部共同主办的第七届“创客中国”中小企业创新创业大赛全国总决赛在浙江杭州落下帷幕。 本次《第七届“创客中国”中小企业创新创业大赛》举办目的&#xff0c;意在加大优质中小企业梯度培育力度&#xff0c;进一步提升中…

高新技术企业如何规划

如何提高申报高企的成功率&#xff0c;应该是很多企业关心的一个问题。 2022年高新技术企业申报已经结束&#xff0c;今年第三批申报的企业结果也将公示&#xff0c;有客户会问&#xff0c;历年来你们申报高企通过率这么高&#xff0c;是怎么做到的&#xff1f; 那么现在呢&a…

《痞子衡嵌入式半月刊》 第 66 期

痞子衡嵌入式半月刊&#xff1a; 第 66 期 这里分享嵌入式领域有用有趣的项目/工具以及一些热点新闻&#xff0c;农历年分二十四节气&#xff0c;希望在每个交节之日准时发布一期。 本期刊是开源项目&#xff08;GitHub: JayHeng/pzh-mcu-bi-weekly&#xff09;&#xff0c;欢…

Spring 框架下如何调用kafka

1、Spring 项目代码结构如下&#xff1a; 2、数据库资源配置文件如下&#xff1a; #sql配置文件 spring.datasource.driver-class-namecom.microsoft.sqlserver.jdbc.SQLServerDriver #.19為測試地址&#xff0c;.13為正式地址 spring.datasource.urljdbc:sqlserver://172.12.…

[ 红队知识库 ] 常见防火墙(WAF)拦截页面

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu交叉编译Arm Linux环境下的身份证读卡器so库操作步骤

1、配置环境变量 ①将gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar解压至/home/eastcoms/ sudo或者root运行命令 &#xff1a;sudo tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_aarch64-linux-gnu.tar -C /home/eastcoms .tar用 -xvf .gz用 -zxvf .bz2用 -jxvf …