📑 目录

一个专注音视频领域的小圈子

音视频系列教程

课程目标

学习如何解析媒体文件中的所有流,获取每个流的编码信息,识别视频流和音频流,并统计它们的详细参数。


知识点

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_VIDEOAVMEDIA_TYPE_AUDIO 等)
  • codec_id:编码格式 ID(AV_CODEC_ID_H264AV_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. 帧率获取

视频流的帧率可以通过以下方式获取:

  1. r_frame_rate:原始帧率(从容器中读取)
  2. avg_frame_rate:平均帧率(通过分析计算得出,更准确)
  3. 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);

注意事项:

  1. 如果 stream->durationAV_NOPTS_VALUE,表示时长未知
  2. 不同流可能有不同的时间基(视频流和音频流的时间基通常不同)
  3. 时间基的精度可能不同(有些是 1/1000,有些是 1/90000)
  4. 推荐使用 av_q2d 进行转换,它处理了除零等边界情况

实践内容

实践1:遍历所有流并识别类型

API: AVFormatContext->streamsAVStream->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_rateAVRational 类型,使用 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_layoutAVChannelLayout 结构体(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->durationAV_NOPTS_VALUE,表示时长未知。

Q8: 为什么不同流的时间基可能不同?

A: 这是正常现象,原因如下:

  1. 视频流:时间基通常与帧率相关(如 1/30、1/25),或者使用标准时间基(如 1/90000)
  2. 音频流:时间基通常与采样率相关(如 1/48000、1/44100)
  3. 不同容器格式:可能使用不同的时间基精度

实际应用: 在音视频同步时,需要将不同流的时间戳统一转换到同一个时间基(通常是容器的 AV_TIME_BASE)。

Q9: 容器时长和流时长有什么区别?

A: 主要区别:

  1. 单位不同

    • 容器时长:以 AV_TIME_BASE(微秒,1/1000000 秒)为单位
    • 流时长:以该流的时间基为单位(每个流可能不同)
  2. 值可能不同

    • 容器时长通常是所有流中最长的时长
    • 各流的时长可能略有差异(同步问题)
  3. 转换方式

    // 容器时长(秒)
    double container_duration = (double)fmt_ctx->duration / AV_TIME_BASE;
    
    // 流时长(秒)
    double stream_duration = stream->duration * av_q2d(stream->time_base);
    

参考

原创文章,转载请注明来源:    音视频教程-第三节