登录 用户中心() [退出] 后台管理 注册
   
您的位置: 首页 >> CLQ工作室开源代码 >> 主题: [ffmpeg]ffmpeg 中两个很容易内存泄漏的地方 -- av_packet_unref,av_bitstream_filter_filter     [回主站]     [分站链接]
标题
[ffmpeg]ffmpeg 中两个很容易内存泄漏的地方 -- av_packet_unref,av_bitstream_filter_filter
clq
浏览(211) + 2021-07-27 22:13:56 发表 编辑

关键字:

[2021-07-27 22:15:33 最后更新]
[ffmpeg]ffmpeg 中两个很容易内存泄漏的地方 -- av_packet_unref,av_bitstream_filter_filter

特别是 av_bitstream_filter_filter 引起的问题,即便是雷神 雷霄骅 的版本也是内存泄漏的,网上有纠正的版本,但概念理解上也是错误的。
相关的代码主要出现在以下场合:

--------------------------------------------------------

分离某些封装格式中的H.264
分离某些封装格式(例如MP4/FLV/MKV等)中的H.264的时候,需要首先写入SPS和PPS,否则会导致分离出来的数据没有SPS、PPS而无法播放。H.264码流的SPS和PPS信息存储在AVCodecContext结构体的extradata中。需要使用ffmpeg中名称为“h264_mp4toannexb”的bitstream filter处理。有两种处理方式:

(1)使用bitstream filter处理每个AVPacket(简单)

把每个AVPacket中的数据(data字段)经过bitstream filter“过滤”一遍。关键函数是av_bitstream_filter_filter()。示例代码如下。

AVBitStreamFilterContext* h264bsfc = av_bitstream_filter_init("h264_mp4toannexb");

--------------------------------------------------------
网上的文章相当多这个内容,最早应该就来自雷神。这里有个严重的问题是 av_bitstream_filter_filter 会产生亲的内存块,必须用 av_free 。同时这还有一个坑,这个函数并不一定会产生
新内存,必须要判断。而判断还不能比较前后的数据块指针,得用返回值。原因是过滤器可能并未改变数据缓冲区,但返回的内存块并不一定是原内存块的起始位置 -- 即有可能是偏移的。
我目前看到的网上所有的判断是否要释放过滤器函数 av_bitstream_filter_filter 的结果的代码全部都是错误的。

正确的我后面会给出我测试了数天的代码。现在先说另一个极容易引起内存泄漏的地方:即压缩时要使用的 AVPacket 结构体指针。在很多代码里只分配了一次内存,释放了一次内存,这些代码
本身倒没有什么问题。但它们对初学者误导了一个问题,以为这里不用释放内存,实际上每次接收到解码器的解压包后都要用 av_packet_unref 函数去释放得到的数据包缓冲,但这个函数和
av_packet_free, av_free_packet 又有什么区别呢?
首先 av_packet_unref,是用来释放解压的数据的,没有与之配对的内存分配函数。它只是释放 AVPacket 相关的数据缓冲本身,而 AVPacket 结构体本身是要用 av_packet_free 来释放的,它对应
的内存分配函数是 av_packet_alloc ,而 av_free_packet 已经在 ff 的注释中表明是废弃了。这在使用 xcode 时会明确提示出来的(别的开发工具就不一定会了)。

av_packet_unref(ou_pkt); //clq 应该加上这个,否则内存泄漏 //删除包数据?
//av_free_packet(ou_pkt); //废弃的函数
av_packet_free(&ou_pkt); //删除包指针?

理论上说整个解压过程一个 AVPacket 是可以复用的,只需要多次调用 av_packet_unref 就好了,只需要调用 av_packet_free/av_packet_alloc 函数对一次。不过从编程干净的思想来看,每次
都调用这两函数一次从代码上更让人放心。

很累,实在不想解释太多。附上实际无内存泄漏的代码版本,大家参考。

--------------------------------------------------------

//压缩并写入一帧
//结束时写入 mp4 文件尾,并关闭各资源
void write_mp4_h264(struct st_ff_outfile_mp4_h264 * of, AVFrame * frame)
{
//这是个很重要的函数,有两个很容易出错造成内存泄漏的地方。首先是 AVPacket 其实在函数外生成一次释放一次就可以了,在接收到压缩结果包时用 av_packet_unref(ou_pkt); 就可以释放压缩过程中新产生的内存了,这里仍然调用 了 av_packet_free(&ou_pkt); 更方便从开发语言的角度去理解,而不用按 ff 那种特有易错的方式。如果强调性能当然可以另外再写一版按 ff 角度理解的函数。
//2 是 av_bitstream_filter_filter 是可能生成新的数据缓冲,导致内存泄漏,要注意释放。

//int r = 0;

int ret = 0; //ffmpeg 操作的返回值

//--------
// 编码前要设置好pts的值,如果en_ctx->time_base为{1,fps},那么这里pts的值即为帧的个数值
//不设置这个的话,第二帧编码时就会出错
frame->pts = of->pts_frame_num_forVideo++;

//如何强制ffmpeg编码时输出一个关键帧
//在编码前设置//确实可以,不过这样会让文件很大,除非是要发送给对方,希望马上显示时//不影响苹果播放兼容性
////blankFrame->pict_type = AV_PICTURE_TYPE_I; //FF_I_TYPE;
////blankFrame->key_frame=1;

//--------------------
ret = avcodec_send_frame(of->out_video_encode, frame); //用自己生成的编码器

if (ret != 0)
{
printf("编码失败:%d.", ret);
print_ff_error(ret);
}

while (1) //因为可能不只得到一帧,所以要循环
{
AVPacket *ou_pkt = av_packet_alloc(); //不用释放吗,要释放的
//av_free_packet(ou_pkt); //av_free_packet 被 ff 标明放弃,那么应该用什么接口呢
//参考 https://zhuanlan.zhihu.com/p/242448731 实际上 av_packet_alloc 和
//av_free_packet 还是需要的,并且只需要的整个编解码过程中调用一次就可以。编解码过程中
//会填充 AVPacket 中的数据区,而前两者应该只是操作 AVPacket 自身的结构体指针。
//而编解码过程中填充的数据缓冲是要用 av_packet_unref 来释放的。
if (avcodec_receive_packet(of->out_video_encode, ou_pkt) < 0) //换用自己的编码器
{
av_packet_unref(ou_pkt); //奇怪,老是返回 AVERROR(EAGAIN); -- 是因为没有 pts
//第一帧,未必能写入,据说这是表示需要更多的 avcodec_send_frame 加入图片
//av_free_packet(ou_pkt);
av_packet_free(&ou_pkt);
break;

}//if

// 成功编码了;写入之前要进行时间基的转换 //参考 https://blog.csdn.net/u011588166/article/details/94384481
//转换前的时间基 - 即本帧的时间位置是以帧为单位的
//转换后的时间基 - 即本帧的时间位置是以压缩后的字节流位置为单位的
//所以要用 frame 帧播放时间计算出 packet 压缩包的播放时间
//"压缩后的数据(对应的结构体为AVPacket)对应的时间基为AVStream的time_base,AVRational{1,90000}"
//另外一个比较乱的解释

//AVStream *stream = ou_fmt->streams[video_ou_index];
//av_packet_rescale_ts(ou_pkt, en_ctx->time_base, stream->time_base);
//LOGD("video pts %d(%s)",ou_pkt->pts,av_ts2timestr(ou_pkt->pts, &stream->time_base));

//av_write_frame(ou_fmt, ou_pkt); //输出后不用释放? 由流自己释放吗?
////av_packet_rescale_ts(ou_pkt, h264encodec->time_base, h264encodec->time_base); //这样有用吗?
//不对,一般第二个参数要使用 av 流的的 time_base ,而不是编码器的.所以还是要把生成的 mp4 文件的视频流指针引出来

//// av_packet_rescale_ts(ou_pkt, rec->ff.out_encode->time_base, avs->time_base); //应该是这个,但前提应该是 avs 本身是对的

//参数是输出编码器和输出流的 time_base (帧率)
av_packet_rescale_ts(ou_pkt,
of->out_video_encode->time_base,
of->out_video_stream->time_base);


//----
//奇怪,生成的文件并没有什么不同//https://www.cnblogs.com/vczf/p/13818609.html
//转换成兼容格式,就是前面提到的什么 "h264_mp4toannexb" //据说 FLV/MP4/MKV等结构中,h264需要h264_mp4toannexb处理。添加SPS/PPS等信息。

//av_bitstream_filter_filter 有内存泄漏 //似乎是因为 av_bitstream_filter_filter 会生成新的 av_packet 或者是 av_packet 的相关数据
//https://blog.csdn.net/LG1259156776/article/details/73283920

//AVPacket new_pkt;// = *ou_pkt; //网上的方法基本上都是生成一个新包,不过理论上应该只释放 ->data 部分应该就可以了
uint8_t * old_data = ou_pkt->data;
int old_size = ou_pkt->size;

uint8_t * new_data = NULL;//ou_pkt->data;
int new_size = 0;//ou_pkt->size;
//uint8_t * new_data = ou_pkt->data;
//int new_size = ou_pkt->size;

ret = av_bitstream_filter_filter(
of->mH264bsfc,
of->out_video_encode,
NULL,
&new_data, //&new_pkt.data, //输出
&new_size, //&new_pkt.size, //输出
ou_pkt->data, //输入
ou_pkt->size, //输入
ou_pkt->flags & AV_PKT_FLAG_KEY //很多示例,这个参数都是 0
);

//if (ret != 0)
if (ret < 0) //小于 0 才是错误
{
printf("编码过滤转换时失败:%d.", ret);
print_ff_error(ret);
}

int free_new_data = 0;//是否要释放新内存
if (1 == ret) free_new_data = 1;

//av_bitstream_filter_filter 返回值的含义翻译如下:
//如果返回值为正,则分配输出缓冲区并 在*poutbuf中可用,与输入缓冲区不同。
//如果返回值为0,则不分配输出缓冲区,并且应视为与输入缓冲区相同,或
//poutbuf被设置为指向输入缓冲区(不一定指向它的起始地址)。
//一种特殊情况是*poutbuf设置为NULL,并且 size被设置为0,这表示应该丢弃数据包。

//av_free_packet(ou_pkt);
//ou_pkt->data = new_pkt.data;
//ou_pkt->size = new_pkt.size;

//更新包的数据缓冲和长度,以便写入帧。释放前一定要恢复
ou_pkt->data = new_data;
ou_pkt->size = new_size;

////ou_pkt.stream_index = 0; //不需要指定吗?
//if (av_interleaved_write_frame(of->out_fmt_mp4, ou_pkt) < 0) break;
//av_interleaved_write_frame(of->out_fmt_mp4, ou_pkt); //用这个只显示第一帧
av_write_frame(of->out_fmt_mp4, ou_pkt);

//av_free(new_pkt.data);
//av_free(old_data);
//确实不应当直接释放新旧的包数据,因为旧包的生成路径未必是这样释放的,应该用原路径释放
//新包的释放也应当先判断一下是否是真的生成了新包,确实有新包时才释放
//另外根据前面 av_bitstream_filter_filter 的编辑值的说明,应当判断其返回值,直接比较新旧
//指针是不准确的,因为没分配新内存的返回的地址也可能是偏移过的
//if (new_size > 0 && old_data != new_data)
if (new_size > 0 && 1 == free_new_data)
av_free(new_data);

//----
//释放旧包前应当恢复旧包的数据
ou_pkt->data = old_data;
ou_pkt->size = old_size;

//----

av_packet_unref(ou_pkt); //clq 应该加上这个,否则内存泄漏 //删除包数据?
//av_free_packet(ou_pkt); //废弃的函数
av_packet_free(&ou_pkt); //删除包指针?

//---------------------------------------------------------------
//av_write_frame 和 av_interleaved_write_frame 的区别?
//不管怎样说,这里 av_interleaved_write_frame 没有生效。
//据说 av_write_frame 和 av_interleaved_write_frame 都有内存泄漏
//实测是 av_bitstream_filter_filter 造成的
//av_interleaved_write_frame(of->out_fmt_mp4, ou_pkt);
//感觉 av_interleaved_write_frame 这个函数慢一点,不知是不是错觉
//据说 av_interleaved_write_frame 还计算 dts 并进行了排序
//这个和 av_interleaved_write_frame 有什么区别
//据说 " 好了,现在整体逻辑就清晰了,得出的结论是:在有多个流的情况下要用av_interleaved_write_frame,只有单一流2个函数都可以用。 "
//---------------------------------------------------------------


}//while //因为可能不只得到一帧,所以要循环

}//


clq
2021-07-27 22:15:33 发表 编辑

雷神的几个链接很重要,给出一下。

https://github.com/leixiaohua1020/simplest_ffmpeg_picture_encoder/blob/master/simplest_ffmpeg_picture_encoder/simplest_ffmpeg_picture_encoder.cpp

http://leixiaohua1020.github.io/

另外有个开源工具
https://handbrake.fr/



总数:1 页次:1/1 首页 尾页  
总数:1 页次:1/1 首页 尾页  


所在合集/目录
ffmpeg 更多



发表评论:
文本/html模式切换 插入图片 文本/html模式切换


附件:



NEWBT官方QQ群1: 276678893
可求档连环画,漫画;询问文本处理大师等软件使用技巧;求档softhub软件下载及使用技巧.
但不可"开车",严禁国家敏感话题,不可求档涉及版权的文档软件.
验证问题说明申请入群原因即可.

Copyright © 2005-2020 clq, All Rights Reserved
版权所有
桂ICP备15002303号-1