[FFmpeg] AVPacket 的使用记录(初始化、引用、解引用、释放)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

在多线程编程中使用ffmpeg时,大家都应该会关注AVPacket的内存问题,不同线程中如何使用同一个AVPacket的数据,保证线程安全,还要减小数据复制

一、先看下与AVPacket相关的几个重要函数

1.AVPacket *av_packet_alloc(void)

初始化一个AVPacket,这里malloc了一个AVPacket,同时初始化了一部分参数参数,av_packet_unref(pkt) --> av_init_packet(pkt);,使用结束后需使用av_packet_free 进行释放,也有人说调用av_packet_unref 进行释放,但看源码就可知道,av_packet_unref 只是解引用了buf的数据区,并不会释放整个AVPacket,第二章节会有一个小测试

AVPacket *av_packet_alloc(void)
{
    AVPacket *pkt = av_mallocz(sizeof(AVPacket));
    if (!pkt)
        return pkt;

    av_packet_unref(pkt);

    return pkt;
}

2. int av_new_packet(AVPacket *pkt, int size)

new一个新的Packet 主要是创建一个新的AVBufferRef ,将pkt->data与buf进行绑定,这将为后面的引用提供条件

int av_new_packet(AVPacket *pkt, int size)
{
    AVBufferRef *buf = NULL;
    int ret = packet_alloc(&buf, size);
    if (ret < 0)
        return ret;

    av_init_packet(pkt);
    pkt->buf      = buf;
    pkt->data     = buf->data;
    pkt->size     = size;

    return 0;
}

3. void av_packet_free(AVPacket **pkt)

完全释放AVPacket,如果pkt存在引用,将会释放与所有引用,也就是说如果调用它,与pkt绑定的与的引用将失效,后续有测试代码说明

void av_packet_free(AVPacket **pkt)
{
    if (!pkt || !*pkt)
        return;

    av_packet_unref(*pkt);
    av_freep(pkt);
}

4.int av_packet_ref(AVPacket *dst, const AVPacket *src)

将dst与src进行引用绑定,线程安全,注意看源码,引用实际是引用的buf,如果没有buf,这里会初始化一个buf并将buf与data绑定
av_packet_copy_props 会拷贝内容参数

int av_packet_ref(AVPacket *dst, const AVPacket *src)
{
    int ret;

    ret = av_packet_copy_props(dst, src);
    if (ret < 0)
        return ret;

    if (!src->buf) {
        ret = packet_alloc(&dst->buf, src->size);
        if (ret < 0)
            goto fail;
        av_assert1(!src->size || src->data);
        if (src->size)
            memcpy(dst->buf->data, src->data, src->size);

        dst->data = dst->buf->data;
    } else {
        dst->buf = av_buffer_ref(src->buf);
        if (!dst->buf) {
            ret = AVERROR(ENOMEM);
            goto fail;
        }
        dst->data = src->data;
    }

    dst->size = src->size;

    return 0;
fail:
    av_packet_free_side_data(dst);
    return ret;
}

5.void av_packet_unref(AVPacket *pkt)

解引用,引用计数减1,没什么好说的

void av_packet_unref(AVPacket *pkt)
{
    av_packet_free_side_data(pkt);
    av_buffer_unref(&pkt->buf);
    av_init_packet(pkt);
    pkt->data = NULL;
    pkt->size = 0;
}

6.void av_init_packet(AVPacket *pkt)

初始化AVPacket里的部分参数,没什么好说的

void av_init_packet(AVPacket *pkt)
{
    pkt->pts                  = AV_NOPTS_VALUE;
    pkt->dts                  = AV_NOPTS_VALUE;
    pkt->pos                  = -1;
    pkt->duration             = 0;
#if FF_API_CONVERGENCE_DURATION
FF_DISABLE_DEPRECATION_WARNINGS
    pkt->convergence_duration = 0;
FF_ENABLE_DEPRECATION_WARNINGS
#endif
    pkt->flags                = 0;
    pkt->stream_index         = 0;
    pkt->buf                  = NULL;
    pkt->side_data            = NULL;
    pkt->side_data_elems      = 0;
}

二、解决问题

1.av_packet_unref 是否能释放整个AVPacket

用一段小代码就可以测试出

    while (1) {
        AVPacket* pkt = av_packet_alloc();
        //        std::this_thread::sleep_for(1ms);
        av_packet_unref(pkt);
        //		  av_packet_free(&pkt);
    }

如果只调用 av_packet_unref 而不调用 av_packet_free 你会发现内存会疯狂的上涨,而调用av_packet_free 内存会很平稳,注意 av_packet_alloc 每次只会占用sizeof(AVPacket)的大小,实际为88字节,如果加了sleep,内存增长速度会很慢
因此可以得出结论,调用 av_packet_alloc 必需使用 av_packet_free 进行内存释放.
av_packet_unref 可以释放 av_new_packet 的内存,可使用以下代码测试

    while (1) {
        AVPacket* pkt = av_packet_alloc();
        av_new_packet(pkt, 1000000);
        std::this_thread::sleep_for(10ms);
        av_packet_unref(pkt);//可以注释此行来查看是否释放内存
        //        av_packet_free(&pkt);
    }
2. 多线程中应该如何使用以上函数

我们设想一个场景:
线程1:负责生产AVPacket,我们不一定是使用的ffmpeg解封装器,有可能是自己创建的,比如接收的视频数据是裸H264/H265的网络数据,或者是live555接收的数据,又或者是直接从文件读取的h264/h265数据
线程2:负责解码AVPacket,这个线程专门解码数据
线程3:负责录像,其实就是将AVPacket写入到文件
线程4:负责推流,需要将AVPacket写入到其它网络流中
这时候就得考虑一些问题,什么时候创建AVPacket,什么时候创建buf,什么时候引用,引用后又什么时候解引用

代码1:

    AVPacket* pkt = av_packet_alloc();
    av_new_packet(pkt, 1000);
    print(pkt, "pkt");

    AVPacket* pkt1 = av_packet_alloc();
    av_packet_ref(pkt1, pkt);
    print(pkt, "pkt");
    print(pkt1, "pkt1");

    AVPacket* pkt2 = av_packet_alloc();
    av_packet_ref(pkt2, pkt);
    print(pkt, "pkt");
    print(pkt2, "pkt2");

    av_packet_unref(pkt);
    av_new_packet(pkt, 2000);
    AVPacket* pkt3 = av_packet_alloc();
    av_packet_ref(pkt3, pkt);
    print(pkt, "pkt");
    print(pkt1, "pkt1");
    print(pkt2, "pkt2");
    print(pkt3, "pkt3");

    av_packet_free(&pkt1);
    av_packet_free(&pkt2);
    av_packet_free(&pkt3);
    print(pkt, "pkt");
    print(pkt1, "pkt1");
    print(pkt2, "pkt2");
    print(pkt3, "pkt3");
    av_packet_unref(pkt);
    print(pkt, "pkt");

void print(AVPacket* pkt, const char* objectname)
{
    if (pkt) {
        if (pkt->data) {
            auto re = av_buffer_get_ref_count(pkt->buf);
            LOG_DEBUG << objectname << ":" << pkt->data << "ref_count:" << re;
        } else {
            LOG_DEBUG << objectname << ":" << pkt->data;
        }
    } else {
        LOG_DEBUG << objectname << ": nullptr";
    }
}

打印输出: //后为说明

pkt : 0x20fac441b00 ref_count: 1
pkt : 0x20fac441b00 ref_count: 2 //创建了一个新avpacket,并引用到了pkt,数据地址同pkt,此时pkt的引用计数加1,
pkt1 : 0x20fac441b00 ref_count: 2
pkt : 0x20fac441b00 ref_count: 3 //又创建一个新的avpacket,并引用到了pkt,数据地址同pkt,此时pkt的引用计数加1,
pkt2 : 0x20fac441b00 ref_count: 3
pkt : 0x20fac4422c0 ref_count: 2//解引用pkt,同时重新av_new_packet  pkt,再创建一个pkt3,引用到pkt,,此时pkt的引用数为2,分别是新的pkt自身和pkt3,此
pkt1 : 0x20fac441b00 ref_count: 2//pkt1和pkt2的引用计数为2,data地址还是最开始的地址,此时pkt以前的data已变
pkt2 : 0x20fac441b00 ref_count: 2//
pkt3 : 0x20fac4422c0 ref_count: 2//pkt3的地址为pkt重新创建的buff地址
pkt : 0x20fac4422c0 ref_count: 1//pkt1,pkt2,pkt3,全部free后,pkt引用为自身1
pkt1 : nullptr
pkt2 : nullptr
pkt3 : nullptr
pkt : 0x0//pkt解引用后,pkt里buff为空,但pkt依然存在

代码2:

    AVPacket* pkt = av_packet_alloc();
    av_new_packet(pkt, 1000);

    AVPacket* pkt1 = av_packet_alloc();
    av_packet_ref(pkt1, pkt);

    AVPacket* pkt2 = av_packet_alloc();
    av_packet_ref(pkt2, pkt);
    print(pkt, "pkt");
    av_packet_free(&pkt);
    print(pkt, "pkt");
    print(pkt1, "pkt1");
    print(pkt2, "pkt2");

输出:

pkt : 0x1aa03791b00 ref_count: 3
pkt : nullptr
pkt1 : 0x1aa03791b00 ref_count: 2  //pkt此时已完全释放,但pkt1和pkt2的data不受任何影响
pkt2 : 0x1aa03791b00 ref_count: 2

结论:
av_packet_free会解自身引用,并进行完全释放,实际上里面的buf是依赖av_packet_unref去释放的
av_packet_unref与 av_packet_ref 和av_new_packet 对应,unref 引用计数减1,计数为0时释放new的packet->buf
av_packet_free 与 av_new_packet 不会影响其它AVPacket av_packet_ref 引用过去的data,代码2中可将av_packet_free(&pkt);改为av_packet_unref(pkt);进行测试

ok所有测试结束,以下就是上述多线程问题的解决方案了
方案1:
线程1:AVPacket 可使用一个静态或者全局的pkt,在通知其它线程结束后,使用av_packet_unref(pkt);新数据到来后av_new_packet(pkt),存新数据即可
线程2,3,4: 使用av_packet_alloc初始化自己的AVPacket,并使用av_packet_ref 引用线程1的pkt,使用结束后,使用av_packet_free 完全释放

方案2:
线程1:AVPacket 可使用局部的pkt,在通知其它线程结束后,使用av_packet_free (&pkt)完全释放pkt,并不会影响其它线程ref的pkt,新数据到来后重新av_packet_alloc并av_new_packet(pkt),存新数据即可
线程2,3,4: 使用av_packet_alloc初始化自己的AVPacket,并使用av_packet_ref 引用线程1的pkt,使用结束后,使用av_packet_free 完全释放

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是使用FFmpeg库进行推流的初始化代码示例: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <signal.h> #include <sys/time.h> #include <sys/select.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <pthread.h> #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libavutil/avutil.h> #include <libavutil/pixdesc.h> #include <libavutil/time.h> #include <libswscale/swscale.h> int main(int argc, char *argv[]) { AVFormatContext *fmt_ctx = NULL; AVOutputFormat *out_fmt = NULL; AVStream *video_st = NULL; AVCodecContext *codec_ctx = NULL; AVCodec *codec = NULL; int ret; // 初始化FFmpegav_register_all(); avformat_network_init(); // 打开输出文件 ret = avformat_alloc_output_context2(&fmt_ctx, NULL, "flv", "rtmp://127.0.0.1/live/stream"); if (ret < 0) { printf("Could not allocate output format context\n"); return -1; } out_fmt = fmt_ctx->oformat; // 添加音视频流 video_st = avformat_new_stream(fmt_ctx, NULL); if (!video_st) { printf("Could not create video stream\n"); return -1; } codec_ctx = video_st->codec; // 设置编码器参数 codec_ctx->codec_id = out_fmt->video_codec; codec_ctx->codec_type = AVMEDIA_TYPE_VIDEO; codec_ctx->width = 640; codec_ctx->height = 480; codec_ctx->time_base.num = 1; codec_ctx->time_base.den = 25; codec = avcodec_find_encoder(codec_ctx->codec_id); if (!codec) { printf("Could not find encoder\n"); return -1; } // 打开编码器 ret = avcodec_open2(codec_ctx, codec, NULL); if (ret < 0) { printf("Could not open encoder\n"); return -1; } // 打开输出流 ret = avio_open(&fmt_ctx->pb, fmt_ctx->filename, AVIO_FLAG_WRITE); if (ret < 0) { printf("Could not open output file\n"); return -1; } // 写文件头 ret = avformat_write_header(fmt_ctx, NULL); if (ret < 0) { printf("Error occurred when writing file header\n"); return -1; } // 推流 AVFrame *frame = av_frame_alloc(); AVPacket pkt; int i; for (i = 0; i < 100; i++) { av_init_packet(&pkt); pkt.data = NULL; pkt.size = 0; // 生成测试图像 uint8_t *data[1]; int linesize[1]; int height = codec_ctx->height; int width = codec_ctx->width; data[0] = (uint8_t*)malloc(width * height * 3); int y; for (y = 0; y < height; y++) { memset(data[0] + y * width * 3, y + i * 3, width * 3); } linesize[0] = width * 3; // 填充AVFrame frame->format = codec_ctx->pix_fmt; frame->width = codec_ctx->width; frame->height = codec_ctx->height; av_image_fill_arrays(frame->data, frame->linesize, data[0], codec_ctx->pix_fmt, codec_ctx->width, codec_ctx->height, 1); // 编码并发送数据 int got_picture = 0; ret = avcodec_encode_video2(codec_ctx, &pkt, frame, &got_picture); if (ret < 0) { printf("Error encoding video frame\n"); return -1; } if (got_picture) { pkt.stream_index = video_st->index; ret = av_write_frame(fmt_ctx, &pkt); if (ret < 0) { printf("Error writing video frame\n"); return -1; } } av_free_packet(&pkt); av_freep(&data[0]); } // 写文件尾 av_write_trailer(fmt_ctx); // 释放资源 avio_close(fmt_ctx->pb); avcodec_close(codec_ctx); avformat_free_context(fmt_ctx); av_frame_free(&frame); return 0; } ``` 这段代码将以FLV格式将一组测试图像推流到`rtmp://127.0.0.1/live/stream`位置。你可以根据需要修改推流地址等参数。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值