音视频/FFmpeg #Qt

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 <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、完整源代码

Qt-FFmpeg开发-打开本地摄像头录制视频(7)的更多相关文章

  1. ffmpeg命令 从网络摄像头录制视频

    安装 sudo apt-get install ffmpeg 录制视频为record.mp4文件 ffmpeg -y -i rtsp://cameral_ip:port -vcodec copy -a ...

  2. javaCV开发详解之2:推流器实现,推本地摄像头视频到流媒体服务器以及摄像头录制视频功能实现(基于javaCV-FFMPEG、javaCV-openCV)

    javaCV系列文章: javacv开发详解之1:调用本机摄像头视频 javaCV开发详解之2:推流器实现,推本地摄像头视频到流媒体服务器以及摄像头录制视频功能实现(基于javaCV-FFMPEG.j ...

  3. WebRTC打开本地摄像头

    本文使用WebRTC的功能,打开电脑上的摄像头,并且把摄像头预览到的图像显示出来. 纯网页实现,能支持除IE外的多数浏览器.手机浏览器也可用. 引入依赖 我们需要引入adapter-latest.js ...

  4. Android Camera系列开发 (二)通过Intent录制视频

    Android Camera系列开发 (二)通过Intent录制视频 作者:雨水  2013-8-18 CSDN博客:http://blog.csdn.net/gobitan/ 概述 使用Camera ...

  5. OpenCV x64 vs2010 下打开摄像头录制视频写成avi(代码为转载)

    首先参照下面这里进行opencv x64位机器下面的配置 http://wiki.opencv.org.cn/index.php/VC_2010%E4%B8%8B%E5%AE%89%E8%A3%85O ...

  6. 1.0.3-学习Opencv与MFC混合编程之---打开本地摄像头

    源代码:http://download.csdn.net/detail/nuptboyzhb/3961643 版本1.0.3新增内容 打开摄像头 Ø 新建菜单项,Learning OpenCV——&g ...

  7. 摄像头录制视频并且保存成mp4

    import cv2import numpy as npimport os cap = cv2.VideoCapture(1)#v4l2-ctl --list-devices 查看设备号,非正常中断时 ...

  8. h5 Video打开本地摄像头和离开页面关闭摄像头

    <div> <video id="video" style="width=100%; height=100%; object-fit: fill&quo ...

  9. WebRTC网页打开摄像头并录制视频

    前面我们能打开本地摄像头,并且在网页上看到摄像头的预览图像. 本文我们使用MediaRecorder来录制视频.在网页上播放录制好的视频,并能提供下载功能. html 首先创建一个html界面,放上一 ...

  10. 与众不同 windows phone (21) - Device(设备)之摄像头(拍摄照片, 录制视频)

    原文:与众不同 windows phone (21) - Device(设备)之摄像头(拍摄照片, 录制视频) [索引页][源码下载] 与众不同 windows phone (21) - Device ...

随机推荐

  1. task 如何终止线程

    前言 这个直接上代码. 代码逻辑是使用var tokenSource = new CancellationTokenSource(); 让tokenSource 去取消.值得注意的是取消线程后,线程会 ...

  2. MMDeploy部署实战系列【第五章】:Windows下Release x64编译mmdeploy(C++),对TensorRT模型进行推理

    MMDeploy部署实战系列[第五章]:Windows下Release x64编译mmdeploy(C++),对TensorRT模型进行推理 这个系列是一个随笔,是我走过的一些路,有些地方可能不太完善 ...

  3. 如何将 ASP.NET Core MVC 项目的视图分离到另一个项目

    如何将 ASP.NET Core MVC 项目的视图分离到另一个项目 在当下这个年代 SPA 已是主流,人们早已忘记了 MVC 以及 Razor 的故事.但是在某些场景下 SSR 还是有意想不到效果. ...

  4. 使用 Docker 部署 TailChat 开源即时通讯平台

    1)介绍 TailChat 官网: https://tailchat.msgbyte.com/ 作者:https://www.moonrailgun.com/about/ GitHub : https ...

  5. 为什么我要迁移 SpringBoot 到函数计算

    简介: 面对流量洪峰,我们再也不会手忙脚乱了,函数计算自动会帮我们扩容!很好的解决了我们的 API 场景和不定时执行各种不同任务的场景. 作者:榴莲   为什么要迁移? 我们的业务有很多对外提供服务的 ...

  6. EasyNLP开源|中文NLP+大模型落地,EasyNLP is all you need

    ​简介:EasyNLP背后的技术框架如何设计?未来有哪些规划?今天一起来深入了解. 作者 | 临在.岑鸣.熊兮 来源 | 阿里开发者公众号 一 导读 随着BERT.Megatron.GPT-3等预训练 ...

  7. [FAQ] 夏玉米 按规则查询域名靠谱吗 ?

    很早就有一个网站叫 夏玉米,可以按规则查询和注册域名,那么它真如我们想的那样 可以找到好域名吗? 虽然看起来很好用,实际上夏玉米的查询只是针对它自己的数据库,不包含未在其平台注册的域名,所以大家要失望 ...

  8. petalinux 报错总结

    Failed to menu config project component.... 解决办法 此处是由于Terminal(终端)的界面太窄导致的,把Terminal(终端)界面拉宽即可:重新执行命 ...

  9. 混合开发中,app内嵌h5页面时,安卓ios遇到的一些兼容问题及解决方法

    1.input[type=checkbox]在ios端样式显示异常,黑色背景或边框,安卓正常 解决: input[type=checkbox]:checked{ background-color: t ...

  10. Java面试题:你知道Spring的IOC吗?那么,它为什么这么重要呢?

    Spring的IOC(控制反转)是一种设计模式,它允许开发者将对象的创建和管理交给Spring框架来完成.在Spring中,IOC允许开发者将对象依赖关系从代码中分离出来,从而使代码更加灵活.可重用和 ...