【AVD】FFmpeg av_packet_unref() 不严谨导致的一次内存泄漏,每次 6MB

422 阅读2分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

AVPacketAVFrame 是 FFmpeg 编解码 api 中最常用的两个结构体,每一帧视频画面或者音频数据从视频文件中通过 av_read_frame 读取到结构体 AVPacket 中,再用 avcodec_send_packet 接口将 AVPacket 送入解码器,最后通过 avcodec_receive_frame 把解码后的数据读取到结构体 AVFrame 中,这就是一帧完整的解码过程。所以,对 AVPacketAVFrame 的用法,尤其是有关内存申请释放相关的用法极为重要,否则虽然一次泄漏一点点儿,但一个视频文件下来成百上千帧之后,泄漏的可不是一点点儿了。

  AVPacket pkt;
  av_init_packet(&pkt);
  while (av_read_frame(format_context_, &pkt) >= 0) {
    if (pkt.stream_index == streamidx) {
      avcodec_send_packet(codec_context_, &pkt);
      while ((ret = avcodec_receive_frame(codec_context_, frame_)) != 0) {
        if (ret == AVERROR_EOF) break;
        avcodec_send_packet(codec_context_, nullptr);
      }
      av_packet_unref(&pkt);
      break;
    } 
  }

这么写有问题吗? 有!严重的内存泄漏。每次循环会有 6MB 内存没有被释放。av_read_frame 会产生 6MB 的堆内存。如果不进行 av_packet_unref,则会导致内存泄漏。即使是同一个栈变量 pkt,即使出了这个栈变量的作用范围、这个栈变量被系统收回,那些每次产生的 6MB 堆内存们,也不会被收回。 泄漏的原因在于,当 ·av_read_frame() < 0 时,它也产生了堆内存,也需要用 av_packet_unref 进行释放;当 pkt.stream_idx != streamidx 时,也需要用 av_packet_unref 进行释放。 所以,正确的写法如下:

  AVPacket pkt;
  av_init_packet(&pkt);
  while (av_read_frame(format_context_, &pkt) >= 0) {
    if (pkt.stream_index == streamidx) {
      avcodec_send_packet(codec_context_, &pkt);
      while ((ret = avcodec_receive_frame(codec_context_, frame_)) != 0) {
        if (ret == AVERROR_EOF) break;
        avcodec_send_packet(codec_context_, nullptr);
      }
      av_packet_unref(&pkt);
      break;
    } else {
      av_packet_unref(&pkt);
    }
  }
  av_packet_unref(&pkt);

然而这种做法到了 FFmpeg 5.0 版本以后也成了不推荐的写法。因为接口 av_init_packet 已经被弃用(deprecated),再这么写可能在严格的编译环境中是编译不过的,根据 FFmpeg 5.0 packet.h 中的写法,sizeof(AVPacket) 也将随着 av_init_packet 去除。正确的使用方法是直接使用 av_packet_alloc 申请即初始化。而且貌似是只能通过这种方法获取,写成栈上的局部变量这种方法也不合理,因为其 AVPacket 结构体中并未定义构造方法,而 av_packet_alloc 方法中,除了 av_malloc 一块内存给 AVPacket 之外,还通过 get_packet_default 给申请的 AVPacket 赋默认值。因此,最好的用法还是 AVPacket *pkt = av_packet_alloc()