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 |
在输入文件中的字节偏移量 |
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() 不会帮你释放它。
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); // 释放回调
// ... 其他字段
};
流程是这样的:
av_packet_ref()调用时,内部会调用av_buffer_ref(),把buf的引用计数 +1。av_packet_unref()调用时,内部调用av_buffer_unref(),引用计数 -1。- 当引用计数降到 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 上栽过不少跟头。挑几个典型的说说:
我早期写代码,每次循环都
av_init_packet(&pkt),然后 av_read_frame(),最后忘了 av_packet_unref()。结果内存泄漏到怀疑人生。记住:av_init_packet 只是初始化结构体,不分配 data 内存;但 av_read_frame 会分配,所以必须 unref。
如果你自己 malloc 了一块内存,赋值给
pkt->data,然后调 av_packet_unref(),这块内存不会被释放。因为 buf 是 NULL,unref 只处理 buf 指向的内存。你得自己 free。
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 这块就讲这么多。理解了这些,后面讲编解码时的数据流转,你就不会觉得乱了。