课程目标
学习如何使用 FFmpeg 打开和读取媒体文件的基本信息,理解 AVFormatContext 的作用。
封装格式简介
在开始之前,先简单了解一下封装格式。
我们常见的视频文件(如 .mp4、.mkv、.avi)都是封装格式,它们把视频、音频、字幕等数据打包在一起。可以简单理解为:封装格式是"盒子",里面装着编码后的视频和音频数据。
封装格式 vs 编码格式:
- 封装格式(如 MP4、MKV):决定如何打包和组织数据
- 编码格式(如 H.264、AAC):决定如何压缩数据
举个例子,一个 .mp4 文件可能包含 H.264 编码的视频和 AAC 编码的音频,MP4 负责把它们打包在一起,并提供同步、元数据等功能。
常见的封装格式有 MP4(兼容性好)、MKV(支持多音轨)、FLV(流媒体)等。FFmpeg 可以处理这些格式,我们只需要知道如何打开和读取它们即可。
知识点
1. AVFormatContext 结构体
AVFormatContext 是 FFmpeg 中表示媒体文件格式上下文的核心结构体,包含了媒体文件的所有信息。
主要字段:
iformat:输入格式(如 MP4、MKV 等)nb_streams:流的数量streams:流数组duration:文件时长(以时间基为单位,通常是所有流中最长的时长)bit_rate:总码率(所有流的码率之和)
重要提示:容器级别 vs 流级别的时长和码率
容器级别(AVFormatContext)的时长和码率与单个流的时长和码率可能不一致:
-
时长差异:
- 容器时长:通常是所有流中最长的时长(例如视频流时长)
- 流时长:每个流有自己的时长,可能略有差异(特别是音频和视频的同步问题)
- 示例:视频流可能是 11.41 秒,音频流可能是 11.40 秒,容器时长取 11.41 秒
-
码率差异:
- 容器总码率:所有流的码率之和(视频码率 + 音频码率 + 其他流码率)
- 流码率:单个流的码率
- 示例:视频码率 2260 kbps + 音频码率 253 kbps ≈ 总码率 2517 kbps
-
如何获取单个流的时长和码率:
cppAVStream* video_stream = fmt_ctx->streams[video_index]; // 流的时长(以流的时间基为单位) int64_t stream_duration = video_stream->duration; // 转换为秒:stream_duration * av_q2d(video_stream->time_base) // 流的码率 int64_t stream_bitrate = video_stream->codecpar->bit_rate;
2. 媒体文件格式识别
FFmpeg 可以自动识别多种媒体文件格式:
- 视频格式:MP4、MKV、AVI、MOV、FLV 等
- 音频格式:MP3、AAC、WAV、FLAC 等
- 流媒体格式:RTMP、HLS、RTSP 等
3. 错误处理机制
FFmpeg 使用返回值表示操作结果:
0:成功- 负数:错误码(使用
av_strerror转换为错误信息) AVERROR_EOF:文件结束
实践内容
实践1:打开本地视频文件
API: avformat_open_input
cpp
AVFormatContext* fmt_ctx = nullptr;
const char* filename = "test.mp4";
int ret = avformat_open_input(&fmt_ctx, filename, nullptr, nullptr);
if (ret == 0) {
// 成功打开
LOG("Format: %s", fmt_ctx->iformat->name);
avformat_close_input(&fmt_ctx);
} else {
// 处理错误
char errbuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errbuf, AV_ERROR_MAX_STRING_SIZE);
LOG("Error: %s", errbuf);
}
关键点:
- 第一个参数是
AVFormatContext**,函数会分配内存 - 第四个参数是
AVDictionary*,可以传递选项(如超时时间) - 使用完后必须调用
avformat_close_input释放资源
实践2:读取媒体文件基本信息
API: avformat_find_stream_info
cpp
// 先打开文件
avformat_open_input(&fmt_ctx, filename, nullptr, nullptr);
// 获取流信息(这一步会读取文件头,获取准确的时长等信息)
int ret = avformat_find_stream_info(fmt_ctx, nullptr);
if (ret >= 0) {
// 获取时长(秒)
double duration = (double)fmt_ctx->duration / AV_TIME_BASE;
LOG("Duration: %.2f seconds", duration);
// 获取码率
LOG("Bitrate: %lld bps", fmt_ctx->bit_rate);
// 获取流数量
LOG("Streams: %u", fmt_ctx->nb_streams);
}
关键点:
avformat_find_stream_info会读取文件头,可能需要一些时间duration的单位是AV_TIME_BASE(微秒),需要除以AV_TIME_BASE得到秒- 如果
duration是AV_NOPTS_VALUE,表示时长未知
注意:容器时长 vs 流时长
容器级别的 duration 和单个流的时长可能不同:
cpp
// 容器级别的时长(所有流中最长的)
double container_duration = (double)fmt_ctx->duration / AV_TIME_BASE;
// 获取视频流的时长
int video_index = -1;
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_index = i;
break;
}
}
if (video_index >= 0) {
AVStream* video_stream = fmt_ctx->streams[video_index];
// 流的时长(以流的时间基为单位)
double stream_duration = video_stream->duration * av_q2d(video_stream->time_base);
LOG("Container duration: %.2f seconds", container_duration);
LOG("Video stream duration: %.2f seconds", stream_duration);
// 两者可能略有差异(通常差异很小,在几毫秒到几十毫秒之间)
}
注意:容器码率 vs 流码率
容器级别的 bit_rate 是总码率,等于所有流的码率之和:
cpp
// 容器级别的总码率
int64_t container_bitrate = fmt_ctx->bit_rate;
// 获取各个流的码率
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
AVStream* stream = fmt_ctx->streams[i];
int64_t stream_bitrate = stream->codecpar->bit_rate;
const char* stream_type = (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) ? "Video" : "Audio";
LOG("%s stream bitrate: %lld bps", stream_type, stream_bitrate);
}
// 验证:总码率 ≈ 视频码率 + 音频码率 + 其他流码率
// 注意:由于编码器设置、容器开销等因素,可能不完全相等
实践3:打印详细的媒体信息
API: av_dump_format
cpp
avformat_open_input(&fmt_ctx, filename, nullptr, nullptr);
avformat_find_stream_info(fmt_ctx, nullptr);
// 打印详细信息(输出到 stderr)
av_dump_format(fmt_ctx, 0, filename, 0);
参数说明:
- 第一个参数:
AVFormatContext*- 格式上下文 - 第二个参数:流索引(0 表示整个文件,>0 表示特定流)
- 第三个参数:URL 或文件名(用于显示,不影响功能)
- 第四个参数:是否为输出(0=输入,1=输出)
工作原理:
av_dump_format 会遍历 AVFormatContext 中的所有信息并格式化输出,包括:
-
文件级别信息:
- 输入格式名称(如
mov,mp4,m4a,3gp,3g2,mj2) - 文件路径/URL
- 元数据(Metadata):如
major_brand、creation_time等 - 总时长(Duration)
- 起始时间(start)
- 总码率(bitrate)
- 输入格式名称(如
-
流级别信息(每个流一行):
- 流索引和 ID
- 编码器信息(如
h264 (High)、aac (LC)) - 编码器 ID(如
avc1、mp4a) - 视频:分辨率、像素格式、颜色空间、码率、帧率
- 音频:采样率、声道数、采样格式、码率
- 流的元数据(如
creation_time、handler_name)
-
输出位置:
- 输出到
stderr(标准错误流),不是stdout - 格式类似
ffprobe或ffmpeg -i的输出
- 输出到
示例输出解析:
yaml
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'test/resources/test-video-1280x720.MP4':
Metadata:
major_brand : mp42 # 主要品牌标识
minor_version : 0 # 次要版本
compatible_brands: mp42mp41isomavc1 # 兼容的品牌
creation_time : 2021-04-30T06:55:38.000000Z # 创建时间
Duration: 00:00:11.41, start: 0.000000, bitrate: 2517 kb/s
Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 1280x720, 2260 kb/s, 30 fps, 30 tbr, 30 tbn (default)
# 流 #0:0 - 视频流
# [0x1] - 流 ID
# (und) - 语言代码(und = undefined)
# h264 (High) - 编码器和 profile
# avc1 - 编码器 ID(FourCC)
# yuv420p - 像素格式
# 1280x720 - 分辨率
# 2260 kb/s - 视频码率
# 30 fps - 帧率
Stream #0:1[0x2](und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 253 kb/s (default)
# 流 #0:1 - 音频流
# aac (LC) - 编码器和 profile
# 48000 Hz - 采样率
# stereo - 声道(立体声)
# fltp - 采样格式(浮点平面)
为什么需要先调用 avformat_find_stream_info?
av_dump_format 需要完整的流信息才能打印详细信息。如果只调用 avformat_open_input,只能打印基本信息,流信息会显示为未知。
实践4:处理打开文件失败的情况
cpp
AVFormatContext* fmt_ctx = nullptr;
int ret = avformat_open_input(&fmt_ctx, "non_existent.mp4", nullptr, nullptr);
if (ret < 0) {
char errbuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(ret, errbuf, AV_ERROR_MAX_STRING_SIZE);
LOG("Failed to open: %s", errbuf);
// 常见错误码:
// AVERROR(ENOENT) - 文件不存在
// AVERROR(EIO) - IO 错误
// AVERROR_INVALIDDATA - 数据无效
}
运行测试
编译项目
在运行测试前,需要先编译项目:
bash
cmake --build build/Release
运行所有第2课的测试
bash
# 注意:在 zsh 中需要用引号包裹参数,避免 * 被解释为通配符
./build/Release/unit-test --gtest_filter="Lesson2_Format.*"
运行直接使用 API 的测试
bash
./build/Release/unit-test --gtest_filter="Lesson2_Format.*DirectAPI"
编译并运行(推荐)
一条命令完成编译和运行:
bash
cmake --build build/Release && ./build/Release/unit-test --gtest_filter="Lesson2_Format.*"
常见问题
Q1: 为什么 duration 是 0?
A: 需要在 avformat_open_input 后调用 avformat_find_stream_info 才能获取准确的时长。
Q2: 如何设置打开文件的超时时间?
A: 使用 AVDictionary 传递选项:
cpp
AVDictionary* opts = nullptr;
av_dict_set(&opts, "timeout", "5000000", 0); // 5秒超时(微秒)
avformat_open_input(&fmt_ctx, filename, nullptr, &opts);
av_dict_free(&opts);
Q3: 如何判断文件格式?
A: 打开文件后,通过 fmt_ctx->iformat->name 获取格式名称。
Q4: 为什么容器级别的时长和单个流的时长不一致?
A: 这是正常现象,原因如下:
- 容器时长:通常是所有流中最长的时长(例如视频流时长)
- 流时长 :每个流有自己的时长,可能略有差异
- 视频流和音频流的时长可能因为同步问题略有不同(通常差异在几毫秒到几十毫秒)
- 某些流可能提前结束(例如字幕流)
- 实际应用:通常使用容器时长作为文件总时长,使用流时长进行精确的同步控制
Q5: 为什么容器级别的码率和单个流的码率不一致?
A: 容器码率是总码率,等于所有流的码率之和:
- 容器总码率 = 视频码率 + 音频码率 + 字幕码率 + 其他流码率
- 流码率 = 单个流的码率
- 示例:视频 2260 kbps + 音频 253 kbps ≈ 总码率 2517 kbps
注意:由于编码器设置、容器开销、元数据等因素,总码率可能不完全等于各流码率的简单相加。