Qt中ffmpeg API存储和显示摄像头视频

Qt中ffmpeg API存储和显示摄像头视频的功能需要之前写的视频ffmpegAPI的视频播放的流程。

代码源码位置:https://download.csdn.net/download/qq_43812868/88157743?spm=1001.2014.3001.5503

一、存储和显示摄像头的视频的流程

这是读取打开视频文件的流程,视频文件在avformat_open_input参数中,最终将数据传递到av_frame_alloc创建的AVFrame。

这个是存储的流程,将第一部分的AVFrame数据传递进来,之后通过av_interleaved_write_frame写入到媒体文件。

二、相关结构体介绍

在了解使用api之前,还需要先了解一下ffmpeg中的相关结构体,在了解了这些结构体之后,可以更容易的理解代码。

AVFormatContext:此结构体存储音视频封装格式中包含的信息,并且这个结构体是贯穿整个播放流程的。在这个结构体中主要包含AVInputFormat,AVOutputFormat、AVStream等。

c++ 复制代码
struct AVInputFormat *iformat; // 输入数据的封装格式
AVIOContext *pb; // 输入数据的缓存
unsigned int nb_streams; // 音视频流的个数
AVStream **streams; // 音视频流
char filename[1024]; // 文件名
int64_t duration; // 时长(单位:微秒us,转换为秒需要除以1000000)
int bit_rate; // 比特率(单位bps,转换为kbps需要除以1000)
AVDictionary *metadata; // 元数据

**AVCodecContext:**是一个描述编解码器上下文的结构体,包含了众多编解码器需要的参数信息。

c++ 复制代码
enum AVMediaType codec_type; // 编解码器的类型(视频,音频...)
struct AVCodec  *codec; // 采用的解码器AVCodec(H.264,MPEG2...)
int bit_rate; // 平均比特率
uint8_t *extradata; int extradata_size; // 针对特定编码器包含的附加信息(例如对于H.264解码器来说,存储SPS,PPS等)
AVRational time_base; // 根据该参数,可以把PTS转化为实际的时间(单位为秒s)
int width, height; // 如果是视频的话,代表宽和高
int refs; // 运动估计参考帧的个数(H.264的话会有多帧,MPEG2这类的一般就没有了)
int sample_rate; // 采样率(音频)
int channels; // 声道数(音频)
enum AVSampleFormat sample_fmt; // 采样格式
int profile; // 型(H.264里面就有,其他编码标准应该也有)
int level; // 级(和profile差不太多)

AVCodec:是存储编码器信息的结构体。

c++ 复制代码
const char *name; // 编解码器的名字的简称
const char *long_name; // 编解码器名字的全称
enum AVMediaType type; // 指明了类型,是视频,音频,还是字幕
enum AVCodecID id; // ID,不重复
const AVRational *supported_framerates; // 支持的帧率(仅视频)
const enum AVPixelFormat *pix_fmts; // 支持的像素格式(仅视频),如RGB24、YUV420P等。
const int *supported_samplerates; // 支持的采样率(仅音频)
const enum AVSampleFormat *sample_fmts; // 支持的采样格式(仅音频)
const uint64_t *channel_layouts; // 支持的声道数(仅音频)
int priv_data_size; // 私有数据的大小

AVFrame:该结构描述解码的(原始的)音频或视频数据。AVFrame必须使用av_frame_alloc()进行分配。请注意,这只是分配AVFrame本身,必须管理数据的缓冲区通过其他方式。AVFrame必须使用av_frame_free()释放。

复制代码

AVPacket:是存储压缩编码数据相关信息的结构体。

c++ 复制代码
uint8_t *data; // 压缩编码的数据。
/* 例如对于H.264来说。1个AVPacket的data通常对应一个NAL。

注意:在这里只是对应,而不是一模一样。他们之间有微小的差别:使用FFMPEG类库分离出多媒体文件中的H.264码流。因此在使用FFMPEG进行音视频处理的时候,常常可以将得到的AVPacket的data数据直接写成文件,从而得到音视频的码流文件。*/
int   size; // data的大小
int64_t pts; // 显示时间戳
int64_t dts; // 解码时间戳
int   stream_index; // 标识该AVPacket所属的视频/音频流。

三、ffmpeg函数介绍

c++ 复制代码
int avformat_alloc_output_context2 (AVFormatContext **ctx, ff_const59 AVOutputFormat *oformat, const char *format_name, const char *filename);
功能:为输出格式分配AVFormatContext。
参数:
    ctx:设置为创建的格式上下文,或者在失败时设置为NULL;
    oformat:如果使用NULL format_name和filename,则用于分配上下文的格式;
    format_name:如果使用NULL文件名,则用于分配上下文的输出格式的名称;
    filename:用于分配上下文的文件名,可以是NULL;
返回值:
c++ 复制代码
AVCodec* avcodec_find_encoder (enum AVCodecID id);
功能:根据AVCodecID寻找已经注册的编码器;
参数:已经注册的编码器ID;
返回值:返回寻找到的编码器;
c++ 复制代码
AVStream* avformat_new_stream (AVFormatContext *s, const AVCodec *c);
功能:为AVFormatContext添加一个新的流;
参数:
	s:媒体文件
	c:编码器,如果为空,与新流相对应的AVCodecContext将被初始化以使用该编解码器。这是需要的,例如,要设置特定于编解码器的默认值,因此如果已知,则应提供编解码器
返回值:新创建的流或出现错误时为NULL;
c++ 复制代码
AVCodecContext* avcodec_alloc_context3 (const AVCodec *codec);
功能:分配AVCodecContext并将其字段设置为默认值;
参数:如果非NULL,则分配专用数据并初始化给定编解码器的默认值。然后用不同的编解码器调用avcodec_open2()是非	   法的。如果为NULL,则编解码器特定的默认值将不会初始化,这可能会导致次优的默认设置(这主要对编码器很重	        要,例如libx264)。
返回值:AVCodecContext已填充默认值或失败时为NULL;
C++ 复制代码
int avcodec_open2 (AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
功能:初始化AVCodecContext以使用给定的AVCodec。在使用此函数之前,必须使用avcodec_alloc_text3()分配上下文。
参数:
    avctx:要初始化的上下文;
    codec:要为其打开此上下文的编解码器。如果之前已将非NULL编解码器传递给avcodec_alloc_text3()或此上下文,则此参数必须为NULL或等于之前传递的编解码器;
    options:一个充满AVCodecContext和编解码器专用选项的字典。返回时,此对象将填充未找到的选项。可以为nullptr;
返回值:成功时为零,错误时为负值;
c++ 复制代码
int avcodec_parameters_from_context (AVCodecParameters *par, const AVCodecContext *codec);
功能:根据提供的编解码器上下文中的值填充参数结构。par中任何分配的字段都将被释放,并替换为编解码器中相应字段的副本;
参数:
    par:AVCodecParamteres结构体;
    codec:编码器上下文;
返回值:>=成功时为0,失败时为负AVERROR代码;
c++ 复制代码
int avio_open(AVIOContext **s, const char *url, int flags);
功能:创建并初始化AVIOContext以访问url指示的资源。
参数:
	s 用于返回指向创建的AVIOContext的指针。如果发生故障,则指向的值将设置为NULL;
	url 要访问url的资源;
	flags 标志用于控制url指示的资源的方式的标志将被打开;
返回值:return>=0如果成功,则为负值,对应于发生故障时的AVERROR代码
c++ 复制代码
av_warn_unused_result int avformat_write_header (AVFormatContext *s, AVDictionary **options);
功能:分配流专用数据并将流头写入输出媒体文件。
参数:
    s 媒体文件句柄,必须使用avformat_alloc_context()进行分配。其oformat字段必须设置为所需的输出格         式;其pb字段必须设置为已打开的AVIOContext;
	options充满AVFormatContext和muxer私有选项的AVDictionary。返回时,此参数将被销毁,并替换为包含未	      找到的选项的dict。可能为NULL;
返回值:如果编解码器尚未在avformat_INIT中完全初始化,则AVSTREAM_INIT_IN_WRITE_HEADER成功;如果编解码	       器已在avformat_INIT中完全启动,则AVSTEAM_INIT_INIT_OUTPUT成功;如果失败,则为负AVERROR。
c++ 复制代码
void av_packet_rescale_ts (AVPacket *pkt, AVRational tb_src, AVRational tb_dst);
功能:将数据包中的有效时间字段(时间戳/持续时间)从一个时基转换为另一个时基。具有未知值(AV_NOPTS_VALUE)的时间戳将被忽略。
参数:
    pkt 将对其执行转换的pkt数据包;
	tb_src 源时基,其中表示pkt中的定时字段;
	tb_dst 目标时基,定时字段将转换为该时基;
返回值:
c++ 复制代码
int av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt);
功能:将数据包写入输出媒体文件,确保正确交错;
参数:
    s 媒体文件句柄
	pkt 包含要写入的数据的数据包。
返回值:成功时为0,错误时为负AVERROR

四、代码

c++ 复制代码
#ifndef FFMPEGAPISAVEVIDEO_H
#define FFMPEGAPISAVEVIDEO_H

#include <QObject>
#include <QDebug>
#include <QThread>
extern "C"{
#include "libavutil/avassert.h"
#include "libavutil/channel_layout.h"
#include "libavutil/opt.h"
#include "libavutil/imgutils.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#include "libavdevice/avdevice.h"
#include "libavcodec/avcodec.h"
}

#define MaxFrameNum 10

class ffmpegApiSaveVideo : public QObject
{
    Q_OBJECT
public:

    struct VideoInfo{
        //分辨率
        int wideth;
        int hight;
        //码率
        int kbps;
        //图像编码
        AVPixelFormat pixFormat;
        //帧数
        int fps;
        //编码格式
        AVCodecID codecId;
        //封装格式
        QString packageFormat;
    };

    explicit ffmpegApiSaveVideo(QObject *parent = nullptr);
    ~ffmpegApiSaveVideo();
    QVector<AVFrame *> m_FrameVector;
    void insertFrame(AVFrame *frame);
    void init(QString filePath,AVCodecContext *pCodecCtx,VideoInfo videoInfo);
    void stopWrite();
private:

    void initOutput(QByteArray filepath);
    void addStreamToftc(AVFormatContext *oc);
    void open_video(AVCodec *codec, AVDictionary *opt_arg);
    int write_frame(AVFormatContext *fmt_ctx, const AVRational *time_base, AVStream *st, AVPacket *pkt);
private:
    AVFormatContext *m_pOFormatCtx = nullptr;
    AVOutputFormat *m_pOutFmt = nullptr;      //定义一个输出的格式结构体
    AVCodecContext *m_pCodecCtx = nullptr;
    AVCodecContext *m_ICodecCtx = nullptr;
    AVCodec *m_pCodec = nullptr;
    AVStream *m_pStream = nullptr;
    AVFrame* m_pIFrameYUV = nullptr;
    SwsContext* m_imgYUV_convert_ctx = nullptr;
    int m_next_pts = 0;                     //视频时间戳

    bool issave = false;

    VideoInfo m_videoInfo;

signals:

public slots:
    void writeVideo();
};

#endif // FFMPEGAPISAVEVIDEO_H
c++ 复制代码
#include "ffmpegapisavevideo.h"

ffmpegApiSaveVideo::ffmpegApiSaveVideo(QObject *parent) : QObject(parent)
{

}

ffmpegApiSaveVideo::~ffmpegApiSaveVideo()
{


}

void ffmpegApiSaveVideo::insertFrame(AVFrame *frame)
{
    if(m_FrameVector.length()>MaxFrameNum){
        m_FrameVector.pop_front();
    }
    m_FrameVector.append(frame);
}

void ffmpegApiSaveVideo::init(QString filePath, AVCodecContext *pCodecCtx, ffmpegApiSaveVideo::VideoInfo videoInfo)
{
    m_videoInfo = videoInfo;
    m_ICodecCtx = pCodecCtx;
    initOutput(filePath.toUtf8());

    uint8_t *pIBuffer;  //开辟存储像素点的存储地址
    int pixSize = av_image_get_buffer_size(m_videoInfo.pixFormat,m_ICodecCtx->width,  m_ICodecCtx->height,16);
    //创建保存空间,底层使用malloc进行内存空间的开辟。
    pIBuffer = static_cast<uint8_t *>(av_malloc(static_cast<size_t>(pixSize)));


    //创建图像转换之后的帧
    m_pIFrameYUV = av_frame_alloc();
    av_image_fill_arrays(m_pIFrameYUV->data,
                         m_pIFrameYUV->linesize,
                         pIBuffer,
                         m_videoInfo.pixFormat,
                         1280,
                         720,
                         16);

    m_imgYUV_convert_ctx = sws_getContext(m_ICodecCtx->width,
                                        m_ICodecCtx->height,
                                      m_ICodecCtx->pix_fmt,
                                      1280,
                                      720,
                                      m_videoInfo.pixFormat,
                                      SWS_BICUBIC, nullptr, nullptr, nullptr);

    issave = true;
}

void ffmpegApiSaveVideo::stopWrite()
{
    issave = false;
}

void ffmpegApiSaveVideo::writeVideo()
{

    while(issave){
        int ret;
        int got_packet=0;
        AVPacket pkt;
        if(m_FrameVector.isEmpty()){
            continue;
        }
        AVFrame *pIFrame = m_FrameVector.front();
        int length = m_FrameVector.length();
        m_FrameVector.pop_front();
        if(pIFrame == nullptr){
            continue;
        }

        sws_scale(m_imgYUV_convert_ctx, static_cast<uint8_t const * const *>(pIFrame->data),
                  pIFrame->linesize,0, m_ICodecCtx->height, m_pIFrameYUV->data,
                  m_pIFrameYUV->linesize);
        m_pIFrameYUV->pts = m_next_pts++;

        av_init_packet(&pkt);
        /* 编码图像*/

        ret = avcodec_send_frame(m_pCodecCtx, m_pIFrameYUV);
        if (ret < 0) {
            qDebug()<<"Error sending the frame to the encoder";
            return;
        }

        ret = avcodec_receive_packet(m_pCodecCtx, &pkt);
        if (ret < 0) {
            qDebug()<<"Error encoding audio frame";
             return;
        }
        ret = write_frame(m_pOFormatCtx, &m_pCodecCtx->time_base, m_pStream, &pkt);
        QThread::msleep(10);
        av_packet_unref(&pkt);
    }
    av_write_trailer(m_pOFormatCtx);

    avcodec_free_context(&m_pCodecCtx);
    /*关闭输出文件*/
    if (!(m_pOutFmt->flags & AVFMT_NOFILE))
        avio_closep(&m_pOFormatCtx->pb);

    /*释放流*/
    avformat_free_context(m_pOFormatCtx);

}


void ffmpegApiSaveVideo::initOutput(QByteArray filepath)
{
    int ret;

    //根据文件路径判断获取编码格式,写入到oc中。向m_pOFormatCtx中写入数据

    avformat_alloc_output_context2(&m_pOFormatCtx, nullptr, nullptr, filepath.data());//为输出格式分配一个AVFormatContext。
    if(!m_pOFormatCtx)
    {
        printf("无法从文件扩展名推断出输出格式:使用h264。\n");
        avformat_alloc_output_context2(&m_pOFormatCtx, nullptr, m_videoInfo.packageFormat.toUtf8(), filepath.data());
    }

    //添加流 向m_pOutFmt和m_pCodecCtx m_pCodec写入数据。
    addStreamToftc(m_pOFormatCtx);

    /*现在所有参数都设置好了,我们可以打开音频和视频编解码器并分配必要的编码缓冲器*/
    AVDictionary *opt = nullptr;//AVDictionary是一个健值对存储工具
    open_video(m_pCodec,opt);
    av_dump_format(m_pOFormatCtx,0,filepath.data(),1);// TODO:

    /* 打开输出文件(如果需要) */
    if(!(m_pOFormatCtx->flags & AVFMT_NOFILE))
    {
        //创建并初始化AVIOContext以访问url指示的资源。
        ret=avio_open(&m_pOFormatCtx->pb,filepath.data(),AVIO_FLAG_WRITE);
        if(ret<0)
        {
            qDebug()<<QString("打不开'%1': %2").arg(filepath.data()).arg(av_err2str(ret));
        }
    }

    /* 编写流头(如果有)*/
    ret=avformat_write_header(m_pOFormatCtx, &opt);
    if(ret<0)
    {
        fprintf(stderr, "打开输出文件时发生错误: %s\n",av_err2str(ret));
    }


    // 创建 缓存区

}

//*添加输出流。 */
void ffmpegApiSaveVideo::addStreamToftc(AVFormatContext *fct)
{
    int i;
    /* find the encoder */
    m_pOutFmt = m_pOFormatCtx->oformat;
    m_pOutFmt->video_codec = m_videoInfo.codecId;//设置编码格式

    m_pCodec = avcodec_find_encoder(m_pOutFmt->video_codec);
    if (!(m_pCodec))
    {
        qDebug()<<"Could not find encoder for :" << avcodec_get_name(m_pOutFmt->video_codec);
        return;
    }

    //给媒体文件添加一个流
    m_pStream = avformat_new_stream(fct, nullptr);
    if (!m_pStream) {
        qDebug()<<"Could not allocate stream";
        return;
    }

    m_pStream->id = static_cast<int>(fct->nb_streams-1);

    //创建 编码 上下文。

    m_pCodecCtx = avcodec_alloc_context3(m_pCodec);
    av_opt_set(m_pCodecCtx->priv_data, "tune", "zerolatency", 0);//解决avcodec_receive_packet返回为-11的问题
    if (!m_pCodecCtx) {
        qDebug()<<"Could not alloc an encoding context";
        return;
    }

    switch((m_pCodec)->type)
    {
        case AVMEDIA_TYPE_AUDIO:{
            m_pCodecCtx->sample_fmt  = (m_pCodec)->sample_fmts ?(m_pCodec)->sample_fmts[0] : AV_SAMPLE_FMT_FLTP;
            m_pCodecCtx->bit_rate    = 64000;
            m_pCodecCtx->sample_rate = 44100;
            if ((m_pCodec)->supported_samplerates) {
                m_pCodecCtx->sample_rate = (m_pCodec)->supported_samplerates[0];
                for (i = 0; (m_pCodec)->supported_samplerates[i]; i++) {
                    if ((m_pCodec)->supported_samplerates[i] == 44100)
                    m_pCodecCtx->sample_rate = 44100;
                }
            }
            m_pCodecCtx->channels = av_get_channel_layout_nb_channels(m_pCodecCtx->channel_layout);
            m_pCodecCtx->channel_layout = AV_CH_LAYOUT_STEREO;
            if ((m_pCodec)->channel_layouts) {
                m_pCodecCtx->channel_layout = (m_pCodec)->channel_layouts[0];
                for (i = 0; (m_pCodec)->channel_layouts[i]; i++) {
                    if ((m_pCodec)->channel_layouts[i] == AV_CH_LAYOUT_STEREO)
                    m_pCodecCtx->channel_layout = AV_CH_LAYOUT_STEREO;
                }
            }
            m_pCodecCtx->channels = av_get_channel_layout_nb_channels(m_pCodecCtx->channel_layout);
            m_pStream->time_base = (AVRational){ 1, m_pCodecCtx->sample_rate };
            break;
        }
        case AVMEDIA_TYPE_VIDEO:{
            m_pCodecCtx->codec_id =  m_pOutFmt->video_codec;
            m_pCodecCtx->bit_rate = m_videoInfo.kbps;  //平均比特率,例子代码默认值是400000
            /* 分辨率必须是2的倍数。*/
            m_pCodecCtx->width = m_videoInfo.wideth;
            m_pCodecCtx->height = m_videoInfo.hight;
            /*时基:这是基本的时间单位(以秒为单位)表示其中的帧时间戳。 对于固定fps内容,时基应为1 /framerate,时间戳增量应为等于1。*/
            m_pStream->time_base = (AVRational){1,m_videoInfo.fps};  //帧率设置   帧率为25;
            m_pCodecCtx->time_base = m_pStream->time_base;
            m_pCodecCtx->gop_size = 12; /* 最多每十二帧发射一帧内帧 */
            m_pCodecCtx->pix_fmt = m_videoInfo.pixFormat;
            if(m_pCodecCtx->codec_id == AV_CODEC_ID_MPEG2VIDEO)
            {
                /* 只是为了测试,我们还添加了B帧 */
                m_pCodecCtx->max_b_frames = 2;
            }
            if(m_pCodecCtx->codec_id == AV_CODEC_ID_MPEG1VIDEO)
            {
                /*需要避免使用其中一些系数溢出的宏块。
                 *普通视频不会发生这种情况,因为
                 *色度平面的运动与亮度平面不匹配。 */
                m_pCodecCtx->mb_decision = 2;
            }
            break;
        }
    default:
      break;
    }

    /* 某些格式希望流头分开。 */
    if (fct->oformat->flags & AVFMT_GLOBALHEADER)
        m_pCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}


void ffmpegApiSaveVideo::open_video(AVCodec *codec, AVDictionary *opt_arg)
{
    int ret;
    AVCodecContext *c = m_pCodecCtx;
    AVDictionary *opt = nullptr;//AVDictionary用来保存音视频文件的metadata
    ret = av_dict_copy(&opt, opt_arg, 0);

    /* open the codec */
    ret = avcodec_open2(c, codec, &opt);
    av_dict_free(&opt);
    if (ret < 0)
    {
        qDebug()<<"Could not open video codec:"<<av_err2str(ret);
        return;
    }
    /* 将流参数复制到多路复用器*/
    ret=avcodec_parameters_from_context(m_pStream->codecpar, c);
    if(ret<0)
    {
        qDebug()<<"Could not copy the stream parameters";
        return;
    }
}

int ffmpegApiSaveVideo::write_frame(AVFormatContext *fmt_ctx, const AVRational *time_base, AVStream *st, AVPacket *pkt)
{
  /* 将输出数据包时间戳值从编解码器重新调整为流时基 */
  av_packet_rescale_ts(pkt, *time_base, st->time_base);
  pkt->stream_index = st->index;

  /*将压缩的帧写入媒体文件。*/
//  log_packet(fmt_ctx, pkt);
  return av_interleaved_write_frame(fmt_ctx, pkt);
}
相关推荐
A.A呐2 小时前
【QT第三章】常用控件2
开发语言·qt
笨笨马甲2 小时前
Qt 实现三维坐标系的方法
开发语言·qt
谁动了我的代码?3 小时前
VNC中使用QT的GDB调试,触发断点时与界面窗口交互导致整个VNC冻结
开发语言·qt·svn
肖恭伟3 小时前
QtCreator Linux ubuntu24.04问题集合
linux·windows·qt
王家视频教程图书馆4 小时前
横屏下的全屏视频 右边视频控制器被虚拟按键遮挡了,解决方式隐藏状态栏,竖屏后在恢复
音视频
vegetablesssss4 小时前
QT国际化翻译
qt
EasyDSS4 小时前
EasyDSS如何基于LiveKit/AI大模型/AI会议助手/语音转写STT技术破解音视频应用核心痛点
人工智能·音视频·webrtc·语音识别·点播技术·流媒体直播
困死,根本不会4 小时前
Qt Designer 基础操作学习笔记
开发语言·笔记·qt·学习·microsoft
喜欢喝果茶.5 小时前
Qt MQTT部署
开发语言·qt
浅碎时光8075 小时前
Qt 窗口 (菜单 工具栏 状态栏 浮动窗口 对话框)
qt