4、解封装原理:AVFormatContext使用、打开媒体文件、读取流信息
各位同学,今天我们聊聊解封装。说白了,就是把一个封装好的媒体文件——比如MP4、MKV、FLV——给“拆开”,把里面的音频流、视频流、字幕流等信息提取出来。
我刚开始接触FFmpeg时,觉得解封装挺简单的,不就是打开文件、读几个包嘛。但后来在项目中踩过不少坑,才发现这里面门道不少。今天我就把核心知识点掰开揉碎了讲给你听。
4.1 什么是解封装?
一个媒体文件,比如你手机里的视频,它其实是个“容器”。容器里装着视频数据、音频数据,还有各种元数据(比如时长、分辨率、码率)。
解封装要做的,就是把这个容器的“盖子”打开,把里面的流信息读出来,但不解码。注意,解封装和解码是两码事。解封装只负责“拆包”,不负责“翻译”。
核心概念:解封装 = 读取容器格式 + 提取流信息 + 获取数据包。数据包里的内容还是压缩后的,比如H.264、AAC。
4.2 AVFormatContext:一切从这里开始
在FFmpeg里,AVFormatContext 是解封装的核心结构体。你可以把它理解成整个媒体文件的“总管家”。
这个结构体里存了什么?我列几个关键的:
- nb_streams:流的总数。比如一个视频文件有1路视频、1路音频,那这个值就是2。
- streams:指向所有流的指针数组。每个元素是一个
AVStream。 - duration:文件时长,单位是微秒。
- bit_rate:总码率。
- iformat:指向
AVInputFormat的指针,告诉你这个文件是什么格式(MP4、MOV、FLV等)。
我个人习惯,拿到一个媒体文件后,第一件事就是打印 AVFormatContext 里的基本信息,看看文件到底对不对。有一次我在项目中遇到一个奇怪的MP4文件,播放器死活打不开,一打印才发现它的 iformat 是 NULL,说明FFmpeg压根没识别出格式。后来发现是文件头损坏了。
4.3 打开媒体文件:avformat_open_input
打开媒体文件,用的是 avformat_open_input 这个函数。它的原型是这样的:
int avformat_open_input(AVFormatContext **ps, const char *url,
AVInputFormat *fmt, AVDictionary **options);
参数解释一下:
- ps:指向
AVFormatContext指针的指针。如果传入NULL,函数内部会帮你分配一个。 - url:文件路径或URL。可以是本地文件,也可以是网络流。
- fmt:指定输入格式。传
NULL表示让FFmpeg自动检测。 - options:一些选项,比如设置超时时间、指定协议等。
嗯,这里要注意:avformat_open_input 只负责读取文件头,不会读取完整的流信息。它只是把“门”打开,让你能看到里面大概有什么。
小技巧:如果你打开网络流,记得设置超时选项。我曾经在直播项目中没设超时,结果网络断了,程序直接卡死。后来加了 AVDictionary 设置 timeout 参数,问题就解决了。
4.4 读取流信息:avformat_find_stream_info
打开文件后,你还需要调用 avformat_find_stream_info 来获取完整的流信息。这个函数会读取文件中的一部分数据,解析出每个流的详细信息。
int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
调用完这个函数后,AVFormatContext 里的 streams 数组就填充完整了。每个 AVStream 里包含了:
- codecpar:编解码参数。比如视频的宽高、音频的采样率。
- time_base:时间基。这个很重要,后面做时间戳转换全靠它。
- start_time:流的起始时间。
- duration:流的时长。
为什么需要这一步?你想想看,有些封装格式(比如MP4)的元数据是分散在文件各处的,光靠文件头读不全。所以必须再读一些数据才能拿到完整信息。
避坑指南:我曾经遇到过一个视频文件,调用 avformat_find_stream_info 后,某个流的 codecpar 里的 codec_id 居然是 AV_CODEC_ID_NONE。这说明FFmpeg无法识别这个流的编码格式。后来发现是文件被截断了。所以,记得检查返回值,别直接拿 codecpar 就用。
4.5 完整流程:打开 + 读取 + 打印
光说不练假把式。我写个简单的示例,把上面两步串起来:
AVFormatContext *fmt_ctx = NULL;
int ret;
// 1. 打开文件
ret = avformat_open_input(&fmt_ctx, "input.mp4", 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, "读取流信息失败: %s\n", av_err2str(ret));
avformat_close_input(&fmt_ctx);
return -1;
}
// 3. 打印基本信息
printf("文件格式: %s\n", fmt_ctx->iformat->name);
printf("时长: %lld 微秒\n", fmt_ctx->duration);
printf("流数量: %d\n", fmt_ctx->nb_streams);
// 4. 遍历每个流
for (int i = 0; i < fmt_ctx->nb_streams; i++) {
AVStream *stream = fmt_ctx->streams[i];
AVCodecParameters *codecpar = stream->codecpar;
printf("流 #%d: 类型=%d, 编码器ID=%d\n",
i, codecpar->codec_type, codecpar->codec_id);
if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
printf(" 视频: %dx%d, 帧率=%d/%d\n",
codecpar->width, codecpar->height,
stream->avg_frame_rate.num,
stream->avg_frame_rate.den);
} else if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
printf(" 音频: 采样率=%d, 通道数=%d\n",
codecpar->sample_rate, codecpar->ch_layout.nb_channels);
}
}
// 5. 清理
avformat_close_input(&fmt_ctx);
这段代码很简单,但很实用。我每次拿到一个新格式的文件,都会先用这段代码看看它的“底细”。
4.6 关键数据结构一览
为了方便你记忆,我把今天涉及的核心结构体和函数整理成了一张表:
| 结构体/函数 | 作用 | 注意事项 |
|---|---|---|
AVFormatContext |
媒体文件的“总管家”,管理所有流和元数据 | 需要手动分配或由 avformat_open_input 自动分配 |
AVStream |
代表一个流(视频、音频、字幕等) | 通过 AVFormatContext.streams[] 访问 |
AVCodecParameters |
存储编解码参数,如宽高、采样率 | 位于 AVStream.codecpar 中 |
avformat_open_input |
打开媒体文件,读取文件头 | 不读取完整流信息,需要配合下一步 |
avformat_find_stream_info |
读取完整的流信息 | 会消耗一些时间,但必须调用 |
avformat_close_input |
关闭文件,释放资源 | 别忘了调用,否则内存泄漏 |
4.7 常见问题与避坑
最后,我分享几个实际项目中遇到过的问题:
- 问题1:打开网络流超时。我曾经在做一个直播拉流工具时,没设置超时参数,结果网络一波动,程序就卡死了。解决方案是用
AVDictionary设置timeout和stimeout。 - 问题2:流信息不全。有些文件调用
avformat_find_stream_info后,codecpar里的某些字段还是0。这时候可以尝试多读几个包,或者检查文件是否完整。 - 问题3:内存泄漏。我见过不少新手忘了调用
avformat_close_input,导致内存越用越多。记住,打开和关闭要成对出现。
总结一下:解封装是音视频管道的“入口”。你只要掌握了 avformat_open_input 和 avformat_find_stream_info 这两个函数,再配合 AVFormatContext 和 AVStream 这两个结构体,就能把任何媒体文件的“底细”摸清楚。下一节课,我们会讲如何从这些流中读取数据包,也就是 av_read_frame 的使用。到时候见。