3、核心数据结构(上):AVFormatContext、AVStream、AVPacket 的作用与关系
做推流开发,说白了就是跟几个核心结构体打交道。我刚开始接触 FFmpeg 时,看着那几十个结构体一头雾水,后来才发现真正绕不开的其实就三四个。今天咱们先把最关键的三个讲透——AVFormatContext、AVStream 和 AVPacket。
你想想看,推流本质上干的事是什么?从文件或设备读数据,拆成包,再按某种协议发出去。这三个结构体正好对应了「容器管理」「流描述」「数据载体」三个角色。搞懂它们,推流的骨架就有了。
3.1 AVFormatContext:一切容器的总管家
AVFormatContext 是整个 FFmpeg 里最「大」的结构体。它代表一个多媒体文件或流的容器上下文。无论是读文件(解封装)还是写文件(封装),你都得先创建它。
核心职责:管理输入/输出的全局信息,包括封装格式、所有流、元数据、I/O 上下文等。
我个人习惯把 AVFormatContext 想象成一个「集装箱调度中心」。它不直接装数据,但它知道集装箱(流)有几个、每个箱子装什么类型的东西、以及整个运输流程怎么走。
看一下它的关键字段:
| 字段名 | 类型 | 作用 |
|---|---|---|
iformat |
AVInputFormat* |
输入封装格式(读文件时用) |
oformat |
AVOutputFormat* |
输出封装格式(写文件/推流时用) |
nb_streams |
unsigned int |
流的总数量 |
streams |
AVStream** |
指向所有流的指针数组 |
pb |
AVIOContext* |
I/O 上下文(读写数据的底层接口) |
duration |
int64_t |
总时长(微秒单位) |
bit_rate |
int64_t |
总码率 |
metadata |
AVDictionary* |
元数据(标题、作者等) |
创建和销毁的典型代码:
// 分配上下文
AVFormatContext *fmt_ctx = avformat_alloc_context();
// 打开输入文件(内部会填充格式信息)
if (avformat_open_input(&fmt_ctx, "input.mp4", NULL, NULL) < 0) {
// 错误处理
}
// ... 使用 ...
// 关闭并释放
avformat_close_input(&fmt_ctx);
注意:avformat_alloc_context 和 avformat_open_input 的关系容易搞混。前者只分配空壳,后者才会真正探测格式、填充字段。我曾经在项目里只调了 alloc 就以为能读流信息,结果拿到的 nb_streams 永远是 0,排查了半天才发现忘了 open_input。
3.2 AVStream:流的身份证
AVStream 描述的是容器里的一条流。一条流可以是一路视频、一路音频,或者字幕、数据等。每个 AVFormatContext 里都有一个 AVStream 数组,索引从 0 开始。
嗯,这里要注意:AVStream 本身不存原始音视频数据,它存的是描述信息——编码参数、时间基、帧率、元数据等等。真正装数据的是后面要讲的 AVPacket。
关键字段一览:
| 字段名 | 类型 | 作用 |
|---|---|---|
index |
int |
流在容器中的索引号 |
codecpar |
AVCodecParameters* |
编码参数(编码器类型、分辨率、采样率等) |
time_base |
AVRational |
时间基(用于时间戳换算) |
start_time |
int64_t |
流的起始时间戳 |
duration |
int64_t |
流的时长 |
nb_frames |
int64_t |
总帧数(不一定准确) |
avg_frame_rate |
AVRational |
平均帧率 |
r_frame_rate |
AVRational |
实时帧率 |
获取流信息的典型做法:
// 遍历所有流
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
AVStream *stream = fmt_ctx->streams[i];
// 通过 codecpar 判断流类型
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
printf("视频流 #%d: %dx%d\n", i,
stream->codecpar->width,
stream->codecpar->height);
} else if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
printf("音频流 #%d: %d Hz, %d 通道\n", i,
stream->codecpar->sample_rate,
stream->codecpar->ch_layout.nb_channels);
}
}
经验之谈:time_base 是 AVStream 里最容易踩坑的字段。推流时时间戳换算全靠它。我建议你拿到流之后第一时间打印 time_base 的值,确认它是多少。常见的是 1/90000(TS 流)或 1/1000(部分封装格式)。
3.3 AVPacket:数据搬运工
AVPacket 是 FFmpeg 里最「忙」的结构体。它承载的是编码后的数据包——也就是压缩过的视频帧或音频帧。解封装时从流里读出来的是 AVPacket,封装时写进去的也是 AVPacket。
说白了,AVPacket 就是数据在容器和编解码器之间流转的载体。
核心字段:
| 字段名 | 类型 | 作用 |
|---|---|---|
data |
uint8_t* |
压缩数据缓冲区 |
size |
int |
数据大小(字节) |
pts |
int64_t |
显示时间戳(Presentation Timestamp) |
dts |
int64_t |
解码时间戳(Decoding Timestamp) |
stream_index |
int |
所属流的索引 |
duration |
int64_t |
该包的时长(以流的时间基为单位) |
flags |
int |
标志位(如关键帧标记 AV_PKT_FLAG_KEY) |
读写 AVPacket 的标准流程:
// 分配一个 packet
AVPacket *pkt = av_packet_alloc();
// 从输入流读取一个包
while (av_read_frame(fmt_ctx, pkt) >= 0) {
// pkt->stream_index 告诉我们这个包属于哪条流
AVStream *stream = fmt_ctx->streams[pkt->stream_index];
// 处理包(比如推流或解码)
process_packet(pkt);
// 必须解引用,否则内存泄漏
av_packet_unref(pkt);
}
av_packet_free(&pkt);
避坑指南:av_packet_unref 和 av_packet_free 的区别一定要搞清楚。unref 只是释放内部数据缓冲区,packet 对象本身还能复用;free 则是连对象一起销毁。我曾经在循环里用了 free 而不是 unref,结果第二次循环时 packet 已经没了,程序直接崩溃。记住:循环读包用 unref,彻底不用了才用 free。
3.4 三者的关系:一张图说清楚
这三个结构体是怎么配合的?我画了一张图帮你理解:
从图上可以清楚看到:
- AVFormatContext 是顶层容器,持有所有 AVStream 的指针数组
- AVStream 描述每条流的编码参数和时间信息,不存数据
- AVPacket 是实际的数据载体,通过
stream_index指向它所属的流
3.5 推流场景下的协作流程
假设我们要把一个 MP4 文件推成 RTMP 流,这三个结构体的协作顺序大致是这样的:
- 创建输入 AVFormatContext:avformat_open_input 打开 MP4 文件,自动探测格式并填充 streams 数组
- 创建输出 AVFormatContext:avformat_alloc_output_context2 分配输出上下文,指定 RTMP 格式
- 复制流信息:遍历输入的所有 AVStream,用 avformat_new_stream 在输出上下文中创建对应的流,并复制 codecpar 等参数
- 循环读取 AVPacket:av_read_frame 从输入上下文读包,每个包携带 stream_index
- 时间基转换:用 av_rescale_q_rnd 把包的 pts/dts 从输入流的时间基转换到输出流的时间基
- 写入输出:av_interleaved_write_frame 把包写入输出上下文,底层通过 RTMP 协议发出去
个人经验:第 5 步时间基转换是推流时最容易出问题的地方。我见过不少新手直接拿原始 pts 往输出写,结果播放器端要么花屏要么音画不同步。记住一个原则:包的 pts/dts 必须用目标流的时间基重新计算。
3.6 小结
这三个结构体是 FFmpeg 推流开发的基石。AVFormatContext 管全局,AVStream 管描述,AVPacket 管数据。搞懂它们各自管什么、怎么配合,后面学 muxer、demuxer、协议封装都会轻松很多。