📑 目录

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

音视频系列教程

课程目标

学习如何使用 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)的时长和码率与单个流的时长和码率可能不一致:

  1. 时长差异

    • 容器时长:通常是所有流中最长的时长(例如视频流时长)
    • 流时长:每个流有自己的时长,可能略有差异(特别是音频和视频的同步问题)
    • 示例:视频流可能是 11.41 秒,音频流可能是 11.40 秒,容器时长取 11.41 秒
  2. 码率差异

    • 容器总码率:所有流的码率之和(视频码率 + 音频码率 + 其他流码率)
    • 流码率:单个流的码率
    • 示例:视频码率 2260 kbps + 音频码率 253 kbps ≈ 总码率 2517 kbps
  3. 如何获取单个流的时长和码率

    AVStream* 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

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

// 先打开文件
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 得到秒
  • 如果 durationAV_NOPTS_VALUE,表示时长未知

注意:容器时长 vs 流时长

容器级别的 duration 和单个流的时长可能不同:

// 容器级别的时长(所有流中最长的)
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 是总码率,等于所有流的码率之和:

// 容器级别的总码率
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

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 中的所有信息并格式化输出,包括:

  1. 文件级别信息

    • 输入格式名称(如 mov,mp4,m4a,3gp,3g2,mj2
    • 文件路径/URL
    • 元数据(Metadata):如 major_brandcreation_time
    • 总时长(Duration)
    • 起始时间(start)
    • 总码率(bitrate)
  2. 流级别信息(每个流一行):

    • 流索引和 ID
    • 编码器信息(如 h264 (High)aac (LC)
    • 编码器 ID(如 avc1mp4a
    • 视频:分辨率、像素格式、颜色空间、码率、帧率
    • 音频:采样率、声道数、采样格式、码率
    • 流的元数据(如 creation_timehandler_name
  3. 输出位置

    • 输出到 stderr(标准错误流),不是 stdout
    • 格式类似 ffprobeffmpeg -i 的输出

示例输出解析:

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:处理打开文件失败的情况

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 - 数据无效
}

运行测试

编译项目

在运行测试前,需要先编译项目:

cmake --build build/Release

运行所有第2课的测试

# 注意:在 zsh 中需要用引号包裹参数,避免 * 被解释为通配符
./build/Release/unit-test --gtest_filter="Lesson2_Format.*"

运行直接使用 API 的测试

./build/Release/unit-test --gtest_filter="Lesson2_Format.*DirectAPI"

编译并运行(推荐)

一条命令完成编译和运行:

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 传递选项:

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: 这是正常现象,原因如下:

  1. 容器时长:通常是所有流中最长的时长(例如视频流时长)
  2. 流时长:每个流有自己的时长,可能略有差异
    • 视频流和音频流的时长可能因为同步问题略有不同(通常差异在几毫秒到几十毫秒)
    • 某些流可能提前结束(例如字幕流)
  3. 实际应用:通常使用容器时长作为文件总时长,使用流时长进行精确的同步控制

Q5: 为什么容器级别的码率和单个流的码率不一致?

A: 容器码率是总码率,等于所有流的码率之和:

  • 容器总码率 = 视频码率 + 音频码率 + 字幕码率 + 其他流码率
  • 流码率 = 单个流的码率
  • 示例:视频 2260 kbps + 音频 253 kbps ≈ 总码率 2517 kbps

注意:由于编码器设置、容器开销、元数据等因素,总码率可能不完全等于各流码率的简单相加。


参考

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