qt Creator + ffmpeg图片合集生成mp4视频

1 篇文章 0 订阅

前言:

本篇介绍ffmpeg视频编码的基本流程,根据官方历程muxing中的历程提取的,做个笔记也让以后用到有机会ctrl+c ctrl+v。除了主要基本流程中的函数外,还用到了其它ffmpeg中的一些相关函数,我都会做简要介绍说明。


运行环境:

IDE: Qt Creator;Qt版本:Qt5.6.3 ;FFMpeg编译时间: git-2019-10-22-0b8956b。


运行效果:

图集编码成视频效果图


一、ffmpeg视频编码基本流程

ffmpeg视频编码流程


二、相关变量和函数的简要说明

结构体:

AVFormatContext:

描述了一个媒体文件或媒体流的构成和基本信息,位于avformat.h文件中。

AVOutputFormat:

输出数据的封装格式。仅封装用,调用者在avformat_write_header()之前设置。

AVCodecContext:

描述编解码器上下文的数据结构,包含了众多编解码器需要的参数信息,位于avcodec.h文件中。

AVDictionary:

简单的key/value存储,经常使用AVDictionary设置或读取内部参数,具体实现在libavutil模块中的dict.c/h,

AVCodec:

存储编解码器信息的结构体,位于avcodec.h文件中。

AVStream:

是存储每一个视频/音频流信息的结构体,位于avformat.h文件中。

AVPacket:

是FFmpeg中很重要的一个数据结构,它保存了解复用(demuxer)之后,解码(decode)之前的数据(仍然是压缩后的数据)和关于这些数据的一些附加的信息,如显示时间戳(pts),解码时间戳(dts),数据时长(duration),所在流媒体的索引(stream_index)等等。

AVFrame:

存储一帧的数据,在解码中,AVFrame是解码器的输出;在编码中,AVFrame是编码器的输入。

SwsContext:

主要用于图片格式转换,用sws_getContext()获取,sws_freeContext()释放。


流程中的主要函数(不想看具体参数参考流程图即可)

av_register_all():

注册FFmpeg所有协议、容器、编解码器。

void av_register_all(void);
avformat_alloc_output_context2():

初始化输出码流的AVFormatContext。

/**
 * Allocate an AVFormatContext for an output format.
 * avformat_free_context() can be used to free the context and
 * everything allocated by the framework within it.
 *
 * @param *ctx is set to the created format context, or to NULL in
 * case of failure
 * @param oformat format to use for allocating the context, if NULL
 * format_name and filename are used instead
 * @param format_name the name of output format to use for allocating the
 * context, if NULL filename is used instead
 * @param filename the name of the filename to use for allocating the
 * context, may be NULL
 * @return >= 0 in case of success, a negative AVERROR code in case of
 * failure
 */
int avformat_alloc_output_context2(AVFormatContext **ctx, ff_const59 AVOutputFormat *oformat,
                                   const char *format_name, const char *filename);
avio_open():

打开输出文件。

/**
 * Create and initialize a AVIOContext for accessing the
 * resource indicated by url.
 * @note When the resource indicated by url has been opened in
 * read+write mode, the AVIOContext can be used only for writing.
 *
 * @param s Used to return the pointer to the created AVIOContext.
 * In case of failure the pointed to value is set to NULL.
 * @param url resource to access
 * @param flags flags which control how the resource indicated by url
 * is to be opened
 * @return >= 0 in case of success, a negative value corresponding to an
 * AVERROR code in case of failure
 */
int avio_open(AVIOContext **s, const char *url, int flags);
avcodec_find_encoder():

找到编码器。

/**
 * Find a registered encoder with a matching codec ID.
 *
 * @param id AVCodecID of the requested encoder
 * @return An encoder if one was found, NULL otherwise.
 */
AVCodec *avcodec_find_encoder(enum AVCodecID id);
av_new_stream():

创建输出码流的AVStream。

/**
 * Add a new stream to a media file.
 *
 * When demuxing, it is called by the demuxer in read_header(). If the
 * flag AVFMTCTX_NOHEADER is set in s.ctx_flags, then it may also
 * be called in read_packet().
 *
 * When muxing, should be called by the user before avformat_write_header().
 *
 * User is required to call avcodec_close() and avformat_free_context() to
 * clean up the allocation by avformat_new_stream().
 *
 * @param s media file handle
 * @param c If non-NULL, the AVCodecContext corresponding to the new stream
 * will be initialized to use this codec. This is needed for e.g. codec-specific
 * defaults to be set, so codec should be provided if it is known.
 *
 * @return newly created stream or NULL on error.
 */
AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c);
avcodec_open2():

打开编码器。

/**
 * Initialize the AVCodecContext to use the given AVCodec. Prior to using this
 * function the context has to be allocated with avcodec_alloc_context3().
 *
 * The functions avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(),
 * avcodec_find_decoder() and avcodec_find_encoder() provide an easy way for
 * retrieving a codec.
 *
 * @warning This function is not thread safe!
 *
 * @note Always call this function before using decoding routines (such as
 * @ref avcodec_receive_frame()).
 *
 * @code
 * avcodec_register_all();
 * av_dict_set(&opts, "b", "2.5M", 0);
 * codec = avcodec_find_decoder(AV_CODEC_ID_H264);
 * if (!codec)
 *     exit(1);
 *
 * context = avcodec_alloc_context3(codec);
 *
 * if (avcodec_open2(context, codec, opts) < 0)
 *     exit(1);
 * @endcode
 *
 * @param avctx The context to initialize.
 * @param codec The codec to open this context for. If a non-NULL codec has been
 *              previously passed to avcodec_alloc_context3() or
 *              for this context, then this parameter MUST be either NULL or
 *              equal to the previously passed codec.
 * @param options A dictionary filled with AVCodecContext and codec-private options.
 *                On return this object will be filled with options that were not found.
 *
 * @return zero on success, a negative value on error
 * @see avcodec_alloc_context3(), avcodec_find_decoder(), avcodec_find_encoder(),
 *      av_dict_set(), av_opt_find().
 */
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
avformat_write_header():

写入流标头,也就是文件格式的文件头(对于某些没有文件头的封装格式,不需要此函数。比如说MPEG2TS)。

/**
 * Allocate the stream private data and write the stream header to
 * an output media file.
 *
 * @param s Media file handle, must be allocated with avformat_alloc_context().
 *          Its oformat field must be set to the desired output format;
 *          Its pb field must be set to an already opened AVIOContext.
 * @param options  An AVDictionary filled with AVFormatContext and muxer-private options.
 *                 On return this parameter will be destroyed and replaced with a dict containing
 *                 options that were not found. May be NULL.
 *
 * @return AVSTREAM_INIT_IN_WRITE_HEADER on success if the codec had not already been fully initialized in avformat_init,
 *         AVSTREAM_INIT_IN_INIT_OUTPUT  on success if the codec had already been fully initialized in avformat_init,
 *         negative AVERROR on failure.
 *
 * @see av_opt_find, av_dict_set, avio_open, av_oformat_next, avformat_init_output.
 */
int avformat_write_header(AVFormatContext *s, AVDictionary **options);
avcodec_encode_video2():

编码一帧视频。即将AVFrame(存储YUV像素数据)编码为AVPacket(存储视频格式的码流数据)。

/**
 * Encode a frame of video.
 *
 * Takes input raw video data from frame and writes the next output packet, if
 * available, to avpkt. The output packet does not necessarily contain data for
 * the most recent frame, as encoders can delay and reorder input frames
 * internally as needed.
 *
 * @param avctx     codec context
 * @param avpkt     output AVPacket.
 *                  The user can supply an output buffer by setting
 *                  avpkt->data and avpkt->size prior to calling the
 *                  function, but if the size of the user-provided data is not
 *                  large enough, encoding will fail. All other AVPacket fields
 *                  will be reset by the encoder using av_init_packet(). If
 *                  avpkt->data is NULL, the encoder will allocate it.
 *                  The encoder will set avpkt->size to the size of the
 *                  output packet. The returned data (if any) belongs to the
 *                  caller, he is responsible for freeing it.
 *
 *                  If this function fails or produces no output, avpkt will be
 *                  freed using av_packet_unref().
 * @param[in] frame AVFrame containing the raw video data to be encoded.
 *                  May be NULL when flushing an encoder that has the
 *                  AV_CODEC_CAP_DELAY capability set.
 * @param[out] got_packet_ptr This field is set to 1 by libavcodec if the
 *                            output packet is non-empty, and to 0 if it is
 *                            empty. If the function returns an error, the
 *                            packet can be assumed to be invalid, and the
 *                            value of got_packet_ptr is undefined and should
 *                            not be used.
 * @return          0 on success, negative error code on failure
 *
 * @deprecated use avcodec_send_frame()/avcodec_receive_packet() instead
 */
int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt,
                          const AVFrame *frame, int *got_packet_ptr);
av_write_frame():

将编码后的视频码流写入文件。

/**
 * Write a packet to an output media file.
 *
 * This function passes the packet directly to the muxer, without any buffering
 * or reordering. The caller is responsible for correctly interleaving the
 * packets if the format requires it. Callers that want libavformat to handle
 * the interleaving should call av_interleaved_write_frame() instead of this
 * function.
 *
 * @param s media file handle
 * @param pkt The packet containing the data to be written. Note that unlike
 *            av_interleaved_write_frame(), this function does not take
 *            ownership of the packet passed to it (though some muxers may make
 *            an internal reference to the input packet).
 *            <br>
 *            This parameter can be NULL (at any time, not just at the end), in
 *            order to immediately flush data buffered within the muxer, for
 *            muxers that buffer up data internally before writing it to the
 *            output.
 *            <br>
 *            Packet's @ref AVPacket.stream_index "stream_index" field must be
 *            set to the index of the corresponding stream in @ref
 *            AVFormatContext.streams "s->streams".
 *            <br>
 *            The timestamps (@ref AVPacket.pts "pts", @ref AVPacket.dts "dts")
 *            must be set to correct values in the stream's timebase (unless the
 *            output format is flagged with the AVFMT_NOTIMESTAMPS flag, then
 *            they can be set to AV_NOPTS_VALUE).
 *            The dts for subsequent packets passed to this function must be strictly
 *            increasing when compared in their respective timebases (unless the
 *            output format is flagged with the AVFMT_TS_NONSTRICT, then they
 *            merely have to be nondecreasing).  @ref AVPacket.duration
 *            "duration") should also be set if known.
 * @return < 0 on error, = 0 if OK, 1 if flushed and there is no more data to flush
 *
 * @see av_interleaved_write_frame()
 */
int av_write_frame(AVFormatContext *s, AVPacket *pkt);
av_write_trailer():

写文件尾(对于某些没有文件头的封装格式,不需要此函数。比如说MPEG2TS)。

/**
 * Write the stream trailer to an output media file and free the
 * file private data.
 *
 * May only be called after a successful call to avformat_write_header.
 *
 * @param s media file handle
 * @return 0 if OK, AVERROR_xxx on error
 */
int av_write_trailer(AVFormatContext *s);

源码

void MainWindow::pngTomp4_2()         //png图集转mp4
{
    QString folder_path = QFileDialog::getExistingDirectory(this,"选择文件目录","",QFileDialog::ShowDirsOnly
                                                            | QFileDialog::DontResolveSymlinks);
    if(folder_path.isEmpty())   return;


    QString outFilename = QFileDialog::getSaveFileName(this,"选择保存路径",folder_path+"/视频文件","*.mp4;; *.avi");
    if(outFilename.isEmpty())   return;

//    QStringList filters;
//    filters<<QString("*.png");

//    定义迭代器并设置过滤器
//    QDirIterator dir_iterator(folder_path,filters, QDir::Files | QDir::NoSymLinks, QDirIterator::Subdirectories);
//    QList<QString > pnglist;
//    while(dir_iterator.hasNext())       //获取目录下的png文件
//    {
//        dir_iterator.next();
//        QFileInfo file_info = dir_iterator.fileInfo();
//        pnglist.append(QString(tr("%1").arg(file_info.absoluteFilePath())));
//    }


    AVFormatContext *kAVFormatContext;
    AVOutputFormat *kAVOutputFormat;
    AVCodecContext *kAVCodecContext;
    AVDictionary *kAVDictionary = NULL;
    AVDictionary *opt = NULL;
    AVCodec *video_codec;
    AVStream *kAVStream;
    AVFrame *frame;
    AVFrame *tmp_frame;
    AVPacket kAVPacket = { 0 };
    struct SwsContext *kSwsContext;
    AVRational time{1,24};  /*1s25帧*/
    AVRational time_1{1,1};  /*1s25帧*/
    uint8_t *intBuffer = 0;                           // 图片数据缓冲区

    int ret;
    int got_packer = 0;
    int64_t next_pts = 0;

    tmp_frame = av_frame_alloc();
    av_init_packet(&kAVPacket);

    av_register_all();     //1、注册所有容易和编解码器

    /* 2、分配输出媒体上下文 */
    avformat_alloc_output_context2(&kAVFormatContext, NULL, NULL, outFilename.toLocal8Bit().data());      //上下文结构体、输出结构体、文件类型、路径

    if(!kAVFormatContext)        //如果根据文件名没有找到对应的格式则默认mpeg格式
    {
        ret = avformat_alloc_output_context2(&kAVFormatContext, NULL, "mpeg",  outFilename.toLocal8Bit().data());    //没有找到文件类型默认mpeg(MP4)
    }
    if(!kAVOutputFormat) { goto end;}

    kAVOutputFormat = kAVFormatContext->oformat;    //把输出操作结构体赋值出来

    if (!(kAVOutputFormat->flags & AVFMT_NOFILE))   /* 如果需要,打开输出文件*/
    {
        ret = avio_open(&kAVFormatContext->pb, outFilename.toLocal8Bit().data(), AVIO_FLAG_WRITE);  //3、打开输出文件
        if (ret < 0) {qDebug()<<"打开输出文件失败。"<<ret; }
    }

    /* 使用默认格式的编解码器添加音频和视频流,并初始化编解码器。*/
    if (kAVOutputFormat->video_codec != AV_CODEC_ID_NONE)
    {
        video_codec = avcodec_find_encoder(kAVOutputFormat->video_codec);   //4、找到编码器
        if(!video_codec)    goto end;

        kAVStream = avformat_new_stream(kAVFormatContext,NULL);         //5、新建一个输出流
        if(!kAVStream) {qDebug()<<"创建流kAVStream失败。";goto end;}

        kAVCodecContext = avcodec_alloc_context3(video_codec);      //初始化一个AVCodecContext
        if(!kAVCodecContext)    {qDebug()<<"用编码器video_codec初始化的kAVCodecContext默认参数失败";goto end;}

        switch(video_codec->type)
        {
        case AVMEDIA_TYPE_VIDEO:

            kAVCodecContext->codec_id = video_codec->id;
            kAVCodecContext->bit_rate = 800000;
            kAVCodecContext->width = 720;
            kAVCodecContext->height = 404;
            kAVStream->time_base = time;
            kAVCodecContext->time_base = time;
            kAVCodecContext->gop_size = 12; /*最多每十二帧发射一个内帧 */
            kAVCodecContext->pix_fmt = AV_PIX_FMT_YUV420P;
            if (kAVCodecContext->codec_id == AV_CODEC_ID_MPEG2VIDEO)
            {
                kAVCodecContext->max_b_frames = 2;  /*为了测试,我们还添加了b帧*/
            }
            if (kAVCodecContext->codec_id == AV_CODEC_ID_MPEG1VIDEO)
            {
                /* 需要避免使用一些coeffs溢出的宏块。这在正常的视频中不会发生,
                 * 只是在色度平面的运动与luma平面不匹配时才会发生。 */
                kAVCodecContext->mb_decision = 2;
            }

            break;
        case AVMEDIA_TYPE_AUDIO:
            break;

        default:
            break;
        }

        if(kAVOutputFormat->flags & AVFMT_GLOBALHEADER)/*有些格式希望流标头是单独的*/
        {
            kAVCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
        }
    }

    av_dict_copy(&opt,kAVDictionary, 0);
    ret = avcodec_open2(kAVCodecContext,video_codec,&opt);      //6、打开编码器
    if(ret<0){qDebug()<<"打开视频编解码器失败"<<ret; goto end;}
    av_dict_free(&opt);

    frame = av_frame_alloc();
    if(!frame)  {qDebug()<<"分配帧失败."; goto end;}
    frame->format = kAVCodecContext->pix_fmt;
    frame->width = kAVCodecContext->width;
    frame->height = kAVCodecContext->height;

    ret = av_frame_get_buffer(frame, 32);   //分配内存空间frame必须要有上三条件,32也就是4字节对齐
    if(ret < 0){qDebug()<<"frame分配内存失败"; goto end;}

    ret = avcodec_parameters_from_context(kAVStream->codecpar,kAVCodecContext);/*将流参数复制到muxer */
    if (ret < 0) {qDebug()<<"无法复制流参数";goto end; }

    av_dump_format(kAVFormatContext, 0, outFilename.toLocal8Bit().data(), 1);     //打印输出文件的详细信息



    ret = avformat_write_header(kAVFormatContext,&kAVDictionary);/* 7、写流标头(如果有的话)*/
    if(ret < 0){qDebug()<<"写流标题失败"; goto end;}


    //8、写入每一帧数据
    for(int i = 0; i<527; i++)
    {
//        if(av_compare_ts(next_pts, kAVCodecContext->time_base,  10.0, time_1) > 0)      /*这里只生成10s的视频*/
//        {
//            qDebug()<<"暂时不需要生成新的帧"; break;
//        }
        if (av_frame_make_writable(frame) < 0)  {goto end;}  /*当我们传递一个帧给编码器时,它可能会在内部保留一个对它的引用;确保我们没有在这里覆盖它*/

        QImage img(tr("%1/%2.png").arg(folder_path).arg(i));
        if(img.isNull()){qDebug()<<"打开图片失败";break;}
        img = img.convertToFormat(QImage::Format_RGB888);

        kSwsContext = sws_getContext(kAVCodecContext->width,kAVCodecContext->height,
                                     AV_PIX_FMT_RGB24,kAVCodecContext->width,kAVCodecContext->height,
                                     AV_PIX_FMT_YUV420P,SWS_LANCZOS | SWS_ACCURATE_RND,NULL,NULL,NULL);
        if(!kSwsContext){qDebug()<<"无法初始化图片转换器";    goto end;}


        intBuffer = (uint8_t*)malloc(sizeof(uint8_t)*img.bytesPerLine()*img.height());
        memcpy(intBuffer,img.constBits(),sizeof(uint8_t)*img.bytesPerLine()*img.height());
        avpicture_fill((AVPicture *)tmp_frame,intBuffer,AV_PIX_FMT_RGB24,kAVCodecContext->width,kAVCodecContext->height); //pAVFrame32的data指针指向了outBuffer

        sws_scale(kSwsContext,(const uint8_t * const *)tmp_frame->data,
                  tmp_frame->linesize,0,kAVCodecContext->height,frame->data,frame->linesize);   //图片信息转换
        sws_freeContext(kSwsContext);
        kSwsContext = 0;

        frame->pts = next_pts++;
        //frame->pts = i*(kAVStream->time_base.den)/((kAVStream->time_base.num)*24);

        got_packer = 0;
        ret = avcodec_encode_video2(kAVCodecContext,&kAVPacket,frame,&got_packer);  //8、编码、把一帧数据编码成AVPacket
        if(ret < 0) {qDebug()<<"错误编码视频帧"<<ret; goto end;}

        if(got_packer)
        {
            av_packet_rescale_ts(&kAVPacket, kAVCodecContext->time_base, kAVStream->time_base);
            kAVPacket.stream_index = kAVStream->index;
            ret = av_write_frame(kAVFormatContext, &kAVPacket); /*将压缩帧写入媒体文件。 */
            av_free_packet(&kAVPacket);
        }else
        {ret = 0;}
        free(intBuffer);intBuffer = 0;
        if(ret<0){ qDebug()<<"写入video文件失败"<<ret<<kAVPacket.stream_index; break; }
    }

    /*  选择写预告片,如果有的话。预告片必须在你之前写好 当你写标题时关闭CodecContexts打开;否则
        av_write_trailer()可以尝试使用被释放的内存  av_codec_close()。要编码的流 */
    av_write_trailer(kAVFormatContext);

end:
    /* 关闭每个编解码器。 */
    avcodec_free_context(&kAVCodecContext);
    av_frame_free(&frame);

    if (!(kAVOutputFormat->flags & AVFMT_NOFILE))
        avio_closep(&kAVFormatContext->pb);/*关闭输出文件*/


    avformat_free_context(kAVFormatContext);   /*释放上下文*/
}

  • 7
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值