提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
在多线程编程中使用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 完全释放