📑 目录
▼课程目标
学习如何解析媒体文件中的所有流,获取每个流的编码信息,识别视频流和音频流,并统计它们的详细参数。
知识点
1. AVStream 结构体
AVStream 是 FFmpeg 中表示媒体流的结构体,每个流(视频、音频、字幕等)对应一个 AVStream。
主要字段:
index:流索引(在AVFormatContext->streams数组中的位置)id:流 ID(在容器中的标识,可能与 index 不同)codecpar:编码参数(AVCodecParameters*),包含编码格式、分辨率、采样率等信息time_base:时间基(用于时间戳转换)duration:流时长(以时间基为单位)r_frame_rate:帧率(视频流)avg_frame_rate:平均帧率(视频流)start_time:起始时间戳
2. AVCodecParameters 结构体
AVCodecParameters 包含流的编码参数信息,是获取流详细信息的主要来源。
通用字段:
codec_type:流类型(AVMEDIA_TYPE_VIDEO、AVMEDIA_TYPE_AUDIO等)codec_id:编码格式 ID(AV_CODEC_ID_H264、AV_CODEC_ID_AAC等)bit_rate:码率(bps)
视频流特有字段:
width:视频宽度height:视频高度format:像素格式(AVPixelFormat,如AV_PIX_FMT_YUV420P)sample_aspect_ratio:像素宽高比color_space:颜色空间color_range:颜色范围
音频流特有字段:
sample_rate:采样率(Hz)ch_layout:声道布局(AVChannelLayout,包含声道数和声道配置)format:采样格式(AVSampleFormat,如AV_SAMPLE_FMT_FLTP)frame_size:音频帧大小(样本数)
3. 流类型判断
使用 AVCodecParameters->codec_type 判断流类型:
AVCodecParameters* codecpar = stream->codecpar;
if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
// 视频流
} else if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
// 音频流
} else if (codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
// 字幕流
}
4. 帧率获取
视频流的帧率可以通过以下方式获取:
r_frame_rate:原始帧率(从容器中读取)avg_frame_rate:平均帧率(通过分析计算得出,更准确)- 从
time_base计算:fps = 1.0 / av_q2d(stream->time_base)
推荐使用 avg_frame_rate,因为它更准确。
5. 流的时长和时间基计算
时间基(Time Base):AVStream->time_base 是一个 AVRational 结构体,表示时间戳的单位。例如,如果 time_base = {1, 1000},表示时间戳的单位是 1/1000 秒(毫秒)。
流的时长(Duration):AVStream->duration 是以时间基为单位的时长值。
计算流的实际时长(秒):
AVStream* stream = fmt_ctx->streams[stream_index];
// 方法1:使用 av_q2d 转换时间基,然后乘以 duration
double time_base_seconds = av_q2d(stream->time_base); // 时间基转换为秒
double stream_duration_seconds = stream->duration * time_base_seconds;
// 方法2:直接使用 AVRational 计算(更精确)
double stream_duration_seconds = stream->duration * av_q2d(stream->time_base);
// 方法3:手动计算(避免浮点误差)
double stream_duration_seconds = (double)stream->duration * stream->time_base.num / stream->time_base.den;
示例:
AVStream* video_stream = fmt_ctx->streams[video_index];
// 获取时间基
AVRational time_base = video_stream->time_base;
LOG("Time base: %d/%d = %.6f seconds",
time_base.num, time_base.den, av_q2d(time_base));
// 获取流的时长(时间基单位)
int64_t stream_duration = video_stream->duration;
LOG("Stream duration: %lld (time_base units)", stream_duration);
// 转换为秒
double duration_seconds = stream_duration * av_q2d(time_base);
LOG("Stream duration: %.2f seconds", duration_seconds);
容器时长 vs 流时长:
- 容器时长(
AVFormatContext->duration):以AV_TIME_BASE(微秒)为单位,通常是所有流中最长的时长 - 流时长(
AVStream->duration):以该流的时间基为单位,每个流可能有不同的时间基
转换关系:
// 容器时长(秒)
double container_duration = (double)fmt_ctx->duration / AV_TIME_BASE;
// 流时长(秒)
double stream_duration = stream->duration * av_q2d(stream->time_base);
// 两者可能略有差异(通常差异很小)
LOG("Container duration: %.2f seconds", container_duration);
LOG("Stream duration: %.2f seconds", stream_duration);
注意事项:
- 如果
stream->duration是AV_NOPTS_VALUE,表示时长未知 - 不同流可能有不同的时间基(视频流和音频流的时间基通常不同)
- 时间基的精度可能不同(有些是 1/1000,有些是 1/90000)
- 推荐使用
av_q2d进行转换,它处理了除零等边界情况
实践内容
实践1:遍历所有流并识别类型
API: AVFormatContext->streams、AVStream->codecpar
AVFormatContext* fmt_ctx = nullptr;
avformat_open_input(&fmt_ctx, filename, nullptr, nullptr);
avformat_find_stream_info(fmt_ctx, nullptr);
// 遍历所有流
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
AVStream* stream = fmt_ctx->streams[i];
AVCodecParameters* codecpar = stream->codecpar;
const char* stream_type = "Unknown";
if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
stream_type = "Video";
} else if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
stream_type = "Audio";
} else if (codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE) {
stream_type = "Subtitle";
}
LOG("Stream #%u: %s, codec_id: %d", i, stream_type, codecpar->codec_id);
}
关键点:
nb_streams表示流的数量streams是流数组,通过索引访问codecpar包含流的编码参数
实践2:获取视频流信息
API: AVCodecParameters(视频字段)
// 找到视频流
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];
AVCodecParameters* codecpar = video_stream->codecpar;
// 分辨率
LOG("Video resolution: %dx%d", codecpar->width, codecpar->height);
// 编码格式
const char* codec_name = avcodec_get_name(codecpar->codec_id);
LOG("Video codec: %s", codec_name);
// 像素格式
const char* pix_fmt_name = av_get_pix_fmt_name((AVPixelFormat)codecpar->format);
LOG("Pixel format: %s", pix_fmt_name ? pix_fmt_name : "unknown");
// 帧率
AVRational frame_rate = video_stream->avg_frame_rate;
double fps = av_q2d(frame_rate);
LOG("Frame rate: %.2f fps", fps);
// 码率
LOG("Bitrate: %lld bps", codecpar->bit_rate);
}
关键点:
avcodec_get_name获取编码格式名称av_get_pix_fmt_name获取像素格式名称avg_frame_rate是AVRational类型,使用av_q2d转换为浮点数
实践3:获取音频流信息
API: AVCodecParameters(音频字段)
// 找到音频流
int audio_index = -1;
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
audio_index = i;
break;
}
}
if (audio_index >= 0) {
AVStream* audio_stream = fmt_ctx->streams[audio_index];
AVCodecParameters* codecpar = audio_stream->codecpar;
// 编码格式
const char* codec_name = avcodec_get_name(codecpar->codec_id);
LOG("Audio codec: %s", codec_name);
// 采样率
LOG("Sample rate: %d Hz", codecpar->sample_rate);
// 声道数和声道布局
AVChannelLayout* ch_layout = &codecpar->ch_layout;
LOG("Channels: %u", ch_layout->nb_channels);
char ch_layout_str[64];
av_channel_layout_describe(ch_layout, ch_layout_str, sizeof(ch_layout_str));
LOG("Channel layout: %s", ch_layout_str);
// 采样格式
const char* sample_fmt_name = av_get_sample_fmt_name((AVSampleFormat)codecpar->format);
LOG("Sample format: %s", sample_fmt_name ? sample_fmt_name : "unknown");
// 码率
LOG("Bitrate: %lld bps", codecpar->bit_rate);
}
关键点:
ch_layout是AVChannelLayout结构体(FFmpeg 5.0+),包含声道信息av_channel_layout_describe将声道布局转换为字符串(如 “stereo”、“5.1”)av_get_sample_fmt_name获取采样格式名称
实践4:识别主视频流和主音频流
主流的识别规则:
- 通常索引最小的视频流是主视频流
- 通常索引最小的音频流是主音频流
- 某些容器格式可能有
disposition标志标记主流
int main_video_index = -1;
int main_audio_index = -1;
// 查找主视频流(第一个视频流)
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
AVStream* stream = fmt_ctx->streams[i];
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
main_video_index = i;
break;
}
}
// 查找主音频流(第一个音频流)
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
AVStream* stream = fmt_ctx->streams[i];
if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
main_audio_index = i;
break;
}
}
LOG("Main video stream index: %d", main_video_index);
LOG("Main audio stream index: %d", main_audio_index);
使用 disposition 标志(更准确):
// 查找标记为主流的视频流
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
AVStream* stream = fmt_ctx->streams[i];
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
// 检查是否有主流标志(某些格式可能没有)
if (stream->disposition & AV_DISPOSITION_DEFAULT) {
main_video_index = i;
break;
}
// 如果没有标志,使用第一个找到的
if (main_video_index < 0) {
main_video_index = i;
}
}
}
实践5:统计所有流的信息
int video_count = 0;
int audio_count = 0;
int subtitle_count = 0;
int other_count = 0;
for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
AVStream* stream = fmt_ctx->streams[i];
AVCodecParameters* codecpar = stream->codecpar;
switch (codecpar->codec_type) {
case AVMEDIA_TYPE_VIDEO:
video_count++;
LOG("Video stream #%d: %dx%d, %s",
i, codecpar->width, codecpar->height,
avcodec_get_name(codecpar->codec_id));
break;
case AVMEDIA_TYPE_AUDIO:
audio_count++;
LOG("Audio stream #%d: %d Hz, %u channels, %s",
i, codecpar->sample_rate, codecpar->ch_layout.nb_channels,
avcodec_get_name(codecpar->codec_id));
break;
case AVMEDIA_TYPE_SUBTITLE:
subtitle_count++;
LOG("Subtitle stream #%d: %s",
i, avcodec_get_name(codecpar->codec_id));
break;
default:
other_count++;
break;
}
}
LOG("Total streams: %u (Video: %d, Audio: %d, Subtitle: %d, Other: %d)",
fmt_ctx->nb_streams, video_count, audio_count, subtitle_count, other_count);
运行测试
编译项目
在运行测试前,需要先编译项目:
cmake --build build/Release
运行所有第3课的测试
# 注意:在 zsh 中需要用引号包裹参数,避免 * 被解释为通配符
./build/Release/unit-test --gtest_filter="Lesson3_Stream.*"
运行直接使用 API 的测试
./build/Release/unit-test --gtest_filter="Lesson3_Stream.*DirectAPI"
编译并运行(推荐)
一条命令完成编译和运行:
cmake --build build/Release && ./build/Release/unit-test --gtest_filter="Lesson3_Stream.*"
常见问题
Q1: 如何判断一个流是视频流还是音频流?
A: 使用 codecpar->codec_type 判断:
if (codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
// 视频流
} else if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
// 音频流
}
Q2: 如何获取视频流的帧率?
A: 推荐使用 avg_frame_rate:
AVRational frame_rate = stream->avg_frame_rate;
double fps = av_q2d(frame_rate);
如果 avg_frame_rate 无效(分子或分母为 0),可以尝试使用 r_frame_rate。
Q3: 如何获取音频流的声道数?
A: 使用 ch_layout.nb_channels:
unsigned int channels = codecpar->ch_layout.nb_channels;
Q4: 如何获取编码格式的名称?
A: 使用 avcodec_get_name:
const char* codec_name = avcodec_get_name(codecpar->codec_id);
Q5: 一个文件可能有多个视频流或音频流吗?
A: 是的,某些容器格式(如 MKV)支持多个视频流和音频流。通常第一个找到的流是主流,但也可以通过 disposition 标志判断。
Q6: codecpar->format 对于视频和音频的含义不同吗?
A: 是的:
- 视频流:
format是像素格式(AVPixelFormat),如AV_PIX_FMT_YUV420P - 音频流:
format是采样格式(AVSampleFormat),如AV_SAMPLE_FMT_FLTP
使用对应的函数获取名称:
- 视频:
av_get_pix_fmt_name((AVPixelFormat)codecpar->format) - 音频:
av_get_sample_fmt_name((AVSampleFormat)codecpar->format)
Q7: 如何将流的 duration 转换为秒?
A: 使用时间基进行转换:
// 方法1:使用 av_q2d(推荐)
double duration_seconds = stream->duration * av_q2d(stream->time_base);
// 方法2:手动计算
double duration_seconds = (double)stream->duration * stream->time_base.num / stream->time_base.den;
注意: 如果 stream->duration 是 AV_NOPTS_VALUE,表示时长未知。
Q8: 为什么不同流的时间基可能不同?
A: 这是正常现象,原因如下:
- 视频流:时间基通常与帧率相关(如 1/30、1/25),或者使用标准时间基(如 1/90000)
- 音频流:时间基通常与采样率相关(如 1/48000、1/44100)
- 不同容器格式:可能使用不同的时间基精度
实际应用: 在音视频同步时,需要将不同流的时间戳统一转换到同一个时间基(通常是容器的 AV_TIME_BASE)。
Q9: 容器时长和流时长有什么区别?
A: 主要区别:
-
单位不同:
- 容器时长:以
AV_TIME_BASE(微秒,1/1000000 秒)为单位 - 流时长:以该流的时间基为单位(每个流可能不同)
- 容器时长:以
-
值可能不同:
- 容器时长通常是所有流中最长的时长
- 各流的时长可能略有差异(同步问题)
-
转换方式:
// 容器时长(秒) double container_duration = (double)fmt_ctx->duration / AV_TIME_BASE; // 流时长(秒) double stream_duration = stream->duration * av_q2d(stream->time_base);
参考
原创文章,转载请注明来源: 音视频教程-第三节