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


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

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

    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 
      #include 
      
      
      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
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
    • videosave.cpp文件

      #include "videosave.h"
      #include 
      
      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() << "停止录制视频!";
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
      • 123
      • 124
      • 125
      • 126
      • 127
      • 128
      • 129
      • 130
      • 131
      • 132
      • 133
      • 134
      • 135
      • 136
      • 137
      • 138
      • 139
      • 140
      • 141
      • 142
      • 143
      • 144
      • 145
      • 146
      • 147
      • 148
      • 149
      • 150
      • 151
      • 152
      • 153
      • 154
      • 155
      • 156
      • 157
      • 158
      • 159
      • 160
      • 161
      • 162
      • 163
      • 164
      • 165
      • 166
      • 167
      • 168
      • 169
      • 170
      • 171
      • 172
      • 173
      • 174
      • 175
      • 176
      • 177
      • 178
      • 179
      • 180
      • 181
      • 182
      • 183
      • 184
      • 185
      • 186
      • 187
      • 188
      • 189
      • 190
      • 191
      • 192
      • 193
      • 194
      • 195
      • 196
      • 197
      • 198
      • 199
      • 200
      • 201
      • 202
      • 203
      • 204
      • 205
      • 206
      • 207
      • 208
      • 209
      • 210
      • 211
      • 212
      • 213
      • 214
      • 215
      • 216
      • 217
      • 218
      • 219
      • 220
      • 221
      • 222
      • 223
      • 224
      • 225
      • 226
      • 227
      • 228
      • 229
      • 230
      • 231
      • 232
      • 233
      • 234
      • 235
      • 236
      • 237
      • 238
      • 239
      • 240
      • 241
      • 242
      • 243
      • 244

    5、完整源代码

  • 相关阅读:
    maven3.6.3版本下载安装
    OAuth2基础知识
    「 每日一练,快乐水题 」1608. 特殊数组的特征值
    安全厂商安恒信息加入龙蜥社区,完成 与 Anolis OS 兼容适配
    Java毕业设计-药品管理系统
    CentOS下将 /home 目录合并到 / 目录
    抖音小店无货源,正处于红利期内的电商项目,新手能操作吗?
    外包干了2个月,技术退步明显...
    修改了Excel默认打开方式后仍然使用WPS打开的解决办法
    开源|商品识别推荐系统
  • 原文地址:https://blog.csdn.net/qq_43627907/article/details/128185019