音视频/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. UML 哲学之道——启航篇[一]

    前言 简单去介绍一下uml的哲学之道也是自我整理之道. 正文 什么是uml,全程是统一建模语言(unified modeling language),简单的说就是用图形来表示文档. 是描述构造和文档化 ...

  2. Redis 性能优化实战

    Redis 作为内存数据库,其性能表现非常出色,单机 OPS 很容易达到 10万以上,这主要得益于其高效的内存数据结构.单线程无锁设计.IO 多路复用等技术实现.但是在线上生产环境的使用中,我们仍然会 ...

  3. 力扣626(MySQL)-换座位(中等)

    题目: 表: Seat 编写SQL查询来交换每两个连续的学生的座位号.如果学生的数量是奇数,则最后一个学生的id不交换. 按 id 升序 返回结果表. 查询结果格式如下所示. 示例1: 解释: 请注意 ...

  4. 力扣592(java)-分数加减运算(中等)

    题目: 给定一个表示分数加减运算的字符串 expression ,你需要返回一个字符串形式的计算结果. 这个结果应该是不可约分的分数,即最简分数. 如果最终结果是一个整数,例如 2,你需要将它转换成分 ...

  5. Spring Cloud Stream 体系及原理介绍

    简介: Spring Cloud Stream在 Spring Cloud 体系内用于构建高度可扩展的基于事件驱动的微服务,其目的是为了简化消息在 Spring Cloud 应用程序中的开发. 作者 ...

  6. 用手机写代码:基于 Serverless 的在线编程能力探索

    ​简介:Serverless 架构的按量付费模式,可以在保证在线编程功能性能的前提下,进一步降低成本.本文将会以阿里云函数计算为例,通过 Serverless 架构实现一个 Python 语言的在线编 ...

  7. 阿里云消息队列 RocketMQ 5.0 全新升级:消息、事件、流融合处理平台

    ​简介: RocketMQ5.0 的发布标志着阿里云消息从消息领域正式迈向了"消息.事件.流"场景大融合的新局面.未来阿里云消息产品的演进也将继续围绕消息.事件.流核心场景而开展. ...

  8. dotnet SemanticKernel 入门 开篇

    本文将开坑告诉大家什么是 SemanticKernel 以及如何使用框架 众所周知 GPT 是一个大语言模型,能够参与的输入和输出是文本内容.而想要让 GPT 完成各项功能,则需要对接现有的编程世界. ...

  9. 习题8 #第8章 Verilog有限状态机设计-4 #Verilog #Quartus #modelsim

    4. 用状态机设计交通灯控制器,设计要求:A路和B路,每路都有红.黄.绿三种灯,持续时间为:红灯45s,黄灯5s,绿灯40秒. A路和B路灯的状态转换是: (1) A红,B绿(持续时间40s): (2 ...

  10. sh角本操作数据库

    #!/bin/bash HOST="127.0.0.1" PORT="3306" USERNAME="root" PASSWORD=&quo ...