3、解封装流程:avformat_open_input → avformat_find_stream_info → 读取数据包
解封装,说白了就是把一个封装格式的文件「拆开」。比如你手里有个 MP4 文件,里面包着 H.264 视频和 AAC 音频。解封装要做的,就是把这两路流从容器里剥离出来,准备好让你后续去解码。
FFmpeg 的解封装流程,核心就三步:打开文件、探测流信息、循环读包。我做了这么多年多媒体开发,可以说 90% 的播放器卡顿、花屏、音画不同步问题,根源都能追溯到这三步里。
3.1 avformat_open_input:打开文件的「第一把钥匙」
这个函数负责打开媒体文件,并读取文件头。它不负责探测流的具体信息,只做两件事:
- 根据文件路径或 URL,找到对应的解封装器(demuxer)
- 读取文件头,填充
AVFormatContext的基本信息
函数原型很简单:
int avformat_open_input(AVFormatContext **ps, const char *url,
AVInputFormat *fmt, AVDictionary **options);
参数说明:
| 参数 | 含义 | 注意点 |
|---|---|---|
ps |
AVFormatContext 指针的指针 | 传入 NULL 时内部会自行分配;传入已有对象则复用 |
url |
文件路径或网络 URL | 支持 file://、http://、rtmp:// 等协议 |
fmt |
强制指定解封装器 | 传 NULL 让 FFmpeg 自动探测 |
options |
解封装器参数 | 比如设置探测时长、缓冲区大小等 |
我的习惯:第二个参数
fmt 我通常传 NULL,让 FFmpeg 自己去猜。但如果你明确知道文件格式,比如裸 H.264 流,可以传 av_find_input_format("h264"),能省掉探测时间。
调用成功后,AVFormatContext 里会填充好 nb_streams(流数量)和 streams(流数组),但每个流的 codecpar 信息可能还不完整。嗯,这里要注意——很多新手以为打开文件后就能直接拿参数去解码,结果踩坑。
3.2 avformat_find_stream_info:把流信息「挖」出来
这个函数才是真正的「重头戏」。它会读取文件中的一部分数据,尝试解码一些帧,从而获取完整的流信息。包括:
- 视频的宽、高、帧率、编码格式
- 音频的采样率、声道数、编码格式
- 关键帧索引、时长、比特率等
函数原型:
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
它的工作原理,我画了张图:
你想想看,为什么需要这个函数?因为很多封装格式的文件头里,信息是不完整的。比如 MP4 的 moov box 可能放在文件末尾,或者某些直播流需要先读几个包才能确定编码参数。没有这一步,你拿到的 codecpar 可能是空的。
我曾经踩过的坑:有一次处理一个损坏的 MP4 文件,
avformat_find_stream_info 返回了 0(成功),但视频流的 codecpar->width 和 codecpar->height 都是 0。后来发现是文件头里的 avcC box 被截断了。所以调用完这个函数后,一定要检查关键参数是否有效,别盲目相信返回值。
3.3 读取数据包:av_read_frame 的循环艺术
信息探测完毕,接下来就是真正的读取环节。av_read_frame 每次从文件中读取一个 AVPacket。这个包可能包含:
- 一帧完整的视频数据(关键帧或非关键帧)
- 一帧或多帧音频数据
- 字幕数据
- 附带的元数据包(如 SEI)
典型的读取循环:
AVPacket *pkt = av_packet_alloc();
while (av_read_frame(fmt_ctx, pkt) >= 0) {
// 判断数据包属于哪个流
if (pkt->stream_index == video_stream_idx) {
// 处理视频包
} else if (pkt->stream_index == audio_stream_idx) {
// 处理音频包
}
av_packet_unref(pkt); // 重要!释放引用
}
av_packet_free(&pkt);
这里有几个关键点:
| 要点 | 说明 | 常见错误 |
|---|---|---|
| 包所有权 | av_read_frame 返回的 pkt 内部有引用计数,需要手动 unref | 忘记 unref 导致内存泄漏 |
| 流索引 | 通过 pkt->stream_index 判断属于哪个流 | 硬编码索引,文件结构变化时崩溃 |
| pts/dts | 包的时间戳,单位是流的时间基 | 直接比较不同流的 pts,导致音画不同步 |
| 关键帧标记 | pkt->flags & AV_PKT_FLAG_KEY 判断是否为关键帧 | seek 时忽略关键帧对齐 |
我个人习惯:在读取循环里,我会先判断
pkt->stream_index 是否在有效范围内。曾经有个 HLS 直播流,中间动态添加了字幕流,索引变了,没做检查直接崩了。加个范围判断,成本极低,收益极高。
3.4 完整流程代码示例
把上面三步串起来,就是一个完整的解封装流程:
#include <libavformat/avformat.h>
int main(int argc, char *argv[]) {
AVFormatContext *fmt_ctx = NULL;
int ret;
// 1. 打开文件
ret = avformat_open_input(&fmt_ctx, argv[1], NULL, NULL);
if (ret < 0) {
fprintf(stderr, "打开文件失败: %s\n", av_err2str(ret));
return -1;
}
// 2. 探测流信息
ret = avformat_find_stream_info(fmt_ctx, NULL);
if (ret < 0) {
fprintf(stderr, "探测流信息失败\n");
avformat_close_input(&fmt_ctx);
return -1;
}
// 打印流信息
av_dump_format(fmt_ctx, 0, argv[1], 0);
// 3. 循环读取数据包
AVPacket *pkt = av_packet_alloc();
while (av_read_frame(fmt_ctx, pkt) >= 0) {
// 这里处理数据包
printf("读取到流 %d 的数据包,pts=%ld\n",
pkt->stream_index, pkt->pts);
av_packet_unref(pkt);
}
// 4. 清理
av_packet_free(&pkt);
avformat_close_input(&fmt_ctx);
return 0;
}
核心要点总结:
avformat_open_input只读文件头,不保证流参数完整avformat_find_stream_info会读一部分数据并尝试解码,确保参数就绪av_read_frame返回的包必须调用av_packet_unref释放引用- 始终检查
stream_index的有效性
这三个函数,说白了就是解封装的「三板斧」。我见过不少开发者,把这三步写在一个函数里,出了问题根本不知道是哪个环节挂了。我的建议是:每一步都做独立的错误处理,打印清晰的日志。调试的时候,你会感谢自己的。