4、AVPacket 深度剖析:结构体成员、引用计数、内存管理、data与buf的关系

AVPacket 这玩意儿,说白了就是 FFmpeg 里最忙的数据快递员。解封装出来的每一帧压缩数据,都得靠它来搬运。我刚开始啃 FFmpeg 源码时,总觉得它不就是个结构体嘛,有啥好研究的?结果一踩坑才发现,这里面的水比我想象的深多了。

今天咱们就把 AVPacket 扒个底朝天。从它的每个成员变量,到引用计数怎么玩,再到 data 和 buf 到底啥关系,一条龙讲清楚。

4.1 结构体成员:每个字段都是干嘛的?

先看定义。AVPacket 定义在 libavcodec/avcodec.h 里,我挑几个关键字段说说:

typedef struct AVPacket {
    AVBufferRef *buf;        // 引用计数缓冲区
    int64_t pts;             // 显示时间戳
    int64_t dts;             // 解码时间戳
    uint8_t *data;           // 压缩数据指针
    int   size;              // 数据大小
    int   stream_index;      // 所属流索引
    int   flags;             // 标志位
    AVPacketSideData *side_data; // 附加数据
    int side_data_elems;     // 附加数据数量
    int64_t duration;        // 该包的时长
    int64_t pos;             // 在文件中的位置
} AVPacket;

咱们一个个拆:

成员 类型 说明
buf AVBufferRef * 引用计数核心,管理 data 的生命周期
pts int64_t 显示时间戳,单位是 AVStream 的 time_base
dts int64_t 解码时间戳,B 帧场景下和 pts 不同
data uint8_t * 指向实际压缩数据的指针
size int data 指向的数据长度
stream_index int 这个包属于哪个流(视频/音频/字幕)
flags int 关键帧标志 AV_PKT_FLAG_KEY 等
side_data AVPacketSideData * 存放附加信息,比如 GOP 结构、回放增益
duration int64_t 该帧压缩数据的时长
pos int64_t 在输入文件中的字节偏移量
我的习惯:每次拿到 AVPacket,第一件事就是检查 flags & AV_PKT_FLAG_KEY。做切片或者关键帧对齐时,这个标志位能省不少事。

4.2 data 与 buf:到底谁管谁?

这是新手最容易懵的地方。data 和 buf 到底啥关系?

简单说:data 是枪,buf 是弹药库。

  • data 只是一个指针,指向压缩数据的内存地址。
  • buf 是一个 AVBufferRef,它管理着这块内存的引用计数。

当 buf 不为 NULL 时,data 指向的内存就是 buf->data。换句话说,data == buf->data。这时候内存的释放由引用计数机制控制。

那什么时候 buf 是 NULL?

嗯,这种情况我遇到过。当你手动创建 AVPacket,直接给 data 赋值,但没有关联 buf 时,buf 就是 NULL。这时候你得自己负责释放 data 指向的内存。FFmpeg 的 av_packet_unref() 不会帮你释放它。

我曾经踩过的坑:早期写代码时,我手动 malloc 了一块内存塞进 data,然后调了 av_packet_unref() 就以为万事大吉。结果内存泄漏了。后来才明白,没有 buf 的 AVPacket,FFmpeg 不负责释放 data。

4.3 引用计数:AVBufferRef 是怎么工作的?

引用计数是 FFmpeg 内存管理的核心机制。说白了就是:一块内存,多个地方在用,大家共用,谁都不急着释放。只有当最后一个使用者说“我不要了”,内存才真正被回收。

看这个结构:

typedef struct AVBufferRef {
    AVBuffer *buffer;  // 指向实际的缓冲区
    uint8_t *data;     // 数据指针,通常和 buffer->data 相同
    int      size;     // 数据大小
} AVBufferRef;

而 AVBuffer 内部维护了一个引用计数器 refcount:

struct AVBuffer {
    uint8_t *data;          // 实际内存
    int      size;          // 内存大小
    atomic_uint refcount;   // 引用计数
    void (*free)(void *opaque, uint8_t *data); // 释放回调
    // ... 其他字段
};

流程是这样的:

  1. av_packet_ref() 调用时,内部会调用 av_buffer_ref(),把 buf 的引用计数 +1。
  2. av_packet_unref() 调用时,内部调用 av_buffer_unref(),引用计数 -1。
  3. 当引用计数降到 0,free 回调被触发,内存真正释放。

我画了张图,帮你理清这个关系:

AVPacket 引用计数与 data/buf 关系图 AVPacket data: 0x7f...a0 size: 4096 buf: 0x56...b0 AVBufferRef buffer: 0x78...c0 data: 0x7f...a0 size: 4096 AVBuffer data: 0x7f...a0 refcount: 2 free: av_free buf 指针 buffer 指针 data 指针指向同一块内存(0x7f...a0) 关键点 AVPacket.data == AVBufferRef.data == AVBuffer.data 引用计数为 0 时,free 回调释放内存

4.4 内存管理:谁分配谁释放?

FFmpeg 对 AVPacket 的内存管理有一套清晰的规则。我总结成几条:

4.4.1 标准流程:av_read_frame 拿到的包

用 av_read_frame() 读出来的 AVPacket,内部已经帮你分配好了 buf 和 data。用完调 av_packet_unref() 就行,引用计数会自动处理。

AVPacket pkt;
av_init_packet(&pkt);  // 初始化

while (av_read_frame(fmt_ctx, &pkt) >= 0) {
    // 处理 pkt
    av_packet_unref(&pkt);  // 引用计数减1,必要时释放内存
}

4.4.2 手动创建:av_packet_alloc

如果你想自己创建一个 AVPacket,用 av_packet_alloc()。它会分配结构体内存,但 data 是空的,需要你后续填充。

AVPacket *pkt = av_packet_alloc();
// 此时 pkt->data == NULL, pkt->buf == NULL
// 使用完记得释放
av_packet_free(&pkt);

4.4.3 克隆包:av_packet_ref 与 av_packet_clone

这两个函数都是“浅拷贝 + 引用计数增加”。它们不会复制 data 指向的内存,而是让新包的 buf 指向同一个 AVBufferRef,然后引用计数 +1。

AVPacket *src = av_packet_alloc();
// ... 假设 src 已经填充了数据

AVPacket *dst = av_packet_alloc();
av_packet_ref(dst, src);  // dst 和 src 共享同一块 data 内存

// 此时 src->buf->buffer->refcount == 2
// 调 unref 只是减计数,不会释放内存
注意:av_packet_clone() 是 av_packet_alloc() + av_packet_ref() 的合体。它直接返回一个新分配的 AVPacket,内部已经 ref 好了。

4.4.4 深拷贝:什么时候需要?

引用计数虽然高效,但有个问题:多个包共享同一块内存,如果你改了其中一个的 data,其他包也会受影响。

我遇到过这种情况:做视频重封装时,想把一个包塞进队列里延迟处理。如果直接用 av_packet_ref(),后续对原包的操作会污染队列里的包。

解决方案是深拷贝:

AVPacket *deep_copy = av_packet_alloc();
av_packet_copy_props(deep_copy, src);  // 复制属性

// 手动分配 data 并拷贝
deep_copy->data = av_malloc(src->size);
memcpy(deep_copy->data, src->data, src->size);
deep_copy->size = src->size;
// 注意:此时 deep_copy->buf 为 NULL,需要自己管理释放

或者更省事的办法:用 av_packet_ref() 后,确保原包不被修改。如果实在要改,就做深拷贝。

4.5 避坑指南:我踩过的那些雷

做 FFmpeg 开发这几年,我在 AVPacket 上栽过不少跟头。挑几个典型的说说:

坑1:av_init_packet 后忘了 unref
我早期写代码,每次循环都 av_init_packet(&pkt),然后 av_read_frame(),最后忘了 av_packet_unref()。结果内存泄漏到怀疑人生。记住:av_init_packet 只是初始化结构体,不分配 data 内存;但 av_read_frame 会分配,所以必须 unref。
坑2:手动赋值 data 后调 av_packet_unref
如果你自己 malloc 了一块内存,赋值给 pkt->data,然后调 av_packet_unref(),这块内存不会被释放。因为 buf 是 NULL,unref 只处理 buf 指向的内存。你得自己 free。
我的建议:尽量让 FFmpeg 管理内存。用 av_packet_alloc() 创建,用 av_read_frame() 填充,用 av_packet_unref() 释放。别自己手动操作 data,除非你很清楚后果。

4.6 总结一下

AVPacket 的核心就三件事:

  • data 和 buf 的关系:data 是枪口,buf 是弹药库。有 buf 时 data 指向 buf->data,引用计数管理生命周期;没 buf 时 data 是裸指针,你得自己管。
  • 引用计数机制:AVBufferRef -> AVBuffer -> refcount。多个包共享内存时,只有最后一个使用者释放时才会真正回收。
  • 内存管理规则:谁分配谁释放。FFmpeg 分配的用 FFmpeg 的 API 释放;自己 malloc 的自己 free。

嗯,AVPacket 这块就讲这么多。理解了这些,后面讲编解码时的数据流转,你就不会觉得乱了。