3. FFmpeg核心数据结构:AVFormatContext、AVCodecContext、AVPacket、AVFrame的深度解析与生命周期管理
做流媒体开发,说白了就是在跟这四个结构体打交道。我见过不少新手,API调得飞起,但一问到内存谁分配谁释放、引用计数怎么加怎么减,立马卡壳。今天咱们就把这四位「主角」彻底扒清楚。
核心观点:FFmpeg的四个核心结构体——AVFormatContext(封装格式上下文)、AVCodecContext(编解码器上下文)、AVPacket(压缩数据包)、AVFrame(解码后帧)——构成了从文件读取到音视频输出的完整数据管道。理解它们的生命周期,是避免内存泄漏和野指针的关键。
3.1 四者关系全景图
先上一张图,把整体脉络理清楚。我个人习惯在开始写代码前,先在脑子里过一遍这张图。
3.2 AVFormatContext:封装格式的「大管家」
AVFormatContext 是整个解复用过程的入口。它负责管理输入文件的封装格式、所有流的信息,以及底层的IO操作。
3.2.1 创建与销毁
创建方式有两种,我一般用第一种,更灵活:
// 方式一:手动分配(推荐)
AVFormatContext *fmt_ctx = avformat_alloc_context();
// ... 设置自定义IO、探测参数等 ...
avformat_open_input(&fmt_ctx, "input.mp4", NULL, NULL);
// 方式二:自动分配(avformat_open_input内部会分配)
AVFormatContext *fmt_ctx = NULL;
avformat_open_input(&fmt_ctx, "input.mp4", NULL, NULL);
// 销毁
avformat_close_input(&fmt_ctx); // 会同时释放内部所有资源
我曾经踩过的坑:avformat_close_input 会调用 avformat_free_context 释放内部资源,但如果你之前手动调了 avformat_alloc_context 然后又调了 avformat_open_input,千万别再手动 free。close_input 已经帮你做了所有清理。双重释放会导致crash,我在线上环境遇到过,排查了半天。
3.2.2 关键字段
| 字段名 | 类型 | 说明 |
|---|---|---|
| nb_streams | unsigned int | 流数量(视频/音频/字幕等) |
| streams | AVStream ** | 流指针数组,每个元素指向一个AVStream |
| duration | int64_t | 文件总时长(微秒单位) |
| bit_rate | int64_t | 总码率(bps) |
| pb | AVIOContext * | IO上下文,可自定义回调实现内存读取 |
| iformat | AVInputFormat * | 输入封装格式(如MP4、FLV) |
3.3 AVCodecContext:编解码器的「控制面板」
AVCodecContext 是编解码器的配置中心。你想想看,编码器要压缩到什么质量、解码器用多少线程、像素格式怎么选,全在这里设置。
3.3.1 生命周期
// 1. 从AVStream获取(解码场景)
AVCodecContext *codec_ctx = avcodec_alloc_context3(NULL);
avcodec_parameters_to_context(codec_ctx, stream->codecpar);
// 2. 查找并打开解码器
AVCodec *codec = avcodec_find_decoder(codec_ctx->codec_id);
avcodec_open2(codec_ctx, codec, NULL);
// 3. 使用...(发送packet,接收frame)
// 4. 销毁
avcodec_free_context(&codec_ctx);
我的习惯:在打开解码器之前,一定要检查 codec_ctx->codec_id 是否有效。有些封装格式的流可能没有正确的 codec_id,直接 avcodec_find_decoder 会返回 NULL,这时候如果不做判断,avcodec_open2 会直接段错误。
3.3.2 关键参数调优
| 参数 | 典型值 | 说明 |
|---|---|---|
| thread_count | 0(自动)或 4/8 | 解码线程数,0表示自动检测CPU核心数 |
| thread_type | FF_THREAD_FRAME / FF_THREAD_SLICE | 帧级并行或片级并行 |
| refcounted_frames | 1(推荐) | 启用引用计数,减少内存拷贝 |
| flags2 | AV_CODEC_FLAG2_FAST | 允许不精确但更快的解码 |
3.4 AVPacket:压缩数据的「快递员」
AVPacket 承载的是编码后的数据——H.264的NALU、AAC的ADTS帧,都在这里面。它不直接管理数据内存,而是通过引用计数来共享缓冲区。
3.4.1 内存管理机制
这里有个关键点:AVPacket 的 data 和 buf 字段。buf 是 AVBufferRef 类型,负责真正的引用计数。当 buf 的引用计数降到0时,数据才会被释放。
// 正确用法:av_read_frame 返回的 packet 需要手动 unref
AVPacket *pkt = av_packet_alloc();
while (av_read_frame(fmt_ctx, pkt) >= 0) {
// 处理 packet...
av_packet_unref(pkt); // 减少引用计数,但不释放 pkt 本身
}
av_packet_free(&pkt); // 释放 pkt 结构体
我曾经踩过的坑:av_packet_unref 和 av_packet_free 的区别一定要搞清楚。unref 只是减少引用计数,pkt 结构体还能继续用;free 是把 pkt 本身也释放了。我在做RTP推流时,因为多线程共享同一个 packet,忘记在某个分支 unref,导致内存泄漏,流推了半小时后OOM了。
3.4.2 关键字段
| 字段 | 说明 |
|---|---|
| data / size | 压缩数据指针和大小 |
| pts / dts | 显示时间戳和解码时间戳(流时间基) |
| duration | 该帧的持续时间(流时间基) |
| stream_index | 所属流的索引 |
| flags | AV_PKT_FLAG_KEY(关键帧)等标志 |
| side_data / side_data_elems | 附加数据(如H.264的SEI) |
3.5 AVFrame:解码后的「原材料」
AVFrame 存储的是解码后的原始数据——YUV420P的视频帧、FLTP的音频采样。它同样使用引用计数管理内存。
3.5.1 生命周期
// 分配
AVFrame *frame = av_frame_alloc();
// 解码循环
while (avcodec_receive_frame(codec_ctx, frame) == 0) {
// 处理 frame...
// 如果要长期持有,必须 av_frame_ref
av_frame_unref(frame); // 减少引用计数
}
// 销毁
av_frame_free(&frame);
3.5.2 数据布局
视频帧的数据在 data[0]~data[3] 数组中,linesize 表示每行的字节数。对于YUV420P:
// YUV420P 的数据布局
uint8_t *y_plane = frame->data[0]; // Y分量
uint8_t *u_plane = frame->data[1]; // U分量
uint8_t *v_plane = frame->data[2]; // V分量
int y_linesize = frame->linesize[0]; // Y平面每行字节数
int u_linesize = frame->linesize[1]; // U平面每行字节数
int v_linesize = frame->linesize[2]; // V平面每行字节数
注意:linesize 不一定等于 width。由于内存对齐,linesize 可能比 width 大几个字节。如果你直接按 width 去拷贝数据,会出现花屏。我在做屏幕录制时遇到过这个问题,后来用 linesize 代替 width 就正常了。
3.6 生命周期管理最佳实践
总结一下,我这些年积累的几个原则:
- 谁分配谁释放:av_read_frame 返回的 packet,由调用者 unref;avcodec_receive_frame 返回的 frame,由调用者 unref。
- 引用计数要配对:av_packet_ref 和 av_packet_unref 必须成对出现。av_frame_ref 和 av_frame_unref 同理。
- 不要混用新旧API:新API(avcodec_send_packet/receive_frame)和旧API(avcodec_decode_video2)不要混用,否则生命周期管理会乱套。
- 多线程要加锁:如果多个线程共享同一个 packet 或 frame,必须用互斥锁保护引用计数操作。
核心原则:FFmpeg的内存管理核心就是「引用计数 + 所有权转移」。搞清楚每个API调用后,谁拥有数据的所有权,谁负责释放,你就掌握了90%的内存管理技巧。
嗯,这四个结构体就讲到这里。理解了它们的生命周期,后面讲RTP打包、音视频同步、滤镜链的时候,你就能更从容地应对了。