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

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

文章目录

  • 前言
  • 一、先看下与AVPacket相关的几个重要函数
      • 1.AVPacket *av_packet_alloc(void)
      • 2. int av_new_packet(AVPacket *pkt, int size)
      • 3. void av_packet_free(AVPacket **pkt)
      • 4.int av_packet_ref(AVPacket *dst, const AVPacket *src)
      • 5.void av_packet_unref(AVPacket *pkt)
      • 6.void av_init_packet(AVPacket *pkt)
  • 二、解决问题
        • 1.av_packet_unref 是否能释放整个AVPacket
        • 2. 多线程中应该如何使用以上函数


前言

在多线程编程中使用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 完全释放

你可能感兴趣的:(C++,ffmpeg,ffmpeg)