音视频教程-第二节

作者:glumes日期:2026/3/13

音视频系列教程

课程目标

学习如何使用 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. 如何获取单个流的时长和码率
1AVStream* video_stream = fmt_ctx->streams[video_index];  
2// 流的时长(以流的时间基为单位)  
3int64_t stream_duration = video_stream->duration;  
4// 转换为秒:stream_duration * av_q2d(video_stream->time_base)  
5// 流的码率  
6int64_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

1AVFormatContext* fmt_ctx = nullptr;
2const char* filename = "test.mp4";
3
4int ret = avformat_open_input(&fmt_ctx, filename, nullptr, nullptr);
5if (ret == 0) {
6    // 成功打开
7    LOG("Format: %s", fmt_ctx->iformat->name);
8    avformat_close_input(&fmt_ctx);
9} else {
10    // 处理错误
11    char errbuf[AV_ERROR_MAX_STRING_SIZE];
12    av_strerror(ret, errbuf, AV_ERROR_MAX_STRING_SIZE);
13    LOG("Error: %s", errbuf);
14}
15

关键点:

  • 第一个参数是 AVFormatContext**,函数会分配内存
  • 第四个参数是 AVDictionary*,可以传递选项(如超时时间)
  • 使用完后必须调用 avformat_close_input 释放资源

实践2:读取媒体文件基本信息

API: avformat_find_stream_info

1// 先打开文件
2avformat_open_input(&fmt_ctx, filename, nullptr, nullptr);
3
4// 获取流信息(这一步会读取文件头,获取准确的时长等信息)
5int ret = avformat_find_stream_info(fmt_ctx, nullptr);
6if (ret >= 0) {
7    // 获取时长(秒)
8    double duration = (double)fmt_ctx->duration / AV_TIME_BASE;
9    LOG("Duration: %.2f seconds", duration);
10    
11    // 获取码率
12    LOG("Bitrate: %lld bps", fmt_ctx->bit_rate);
13    
14    // 获取流数量
15    LOG("Streams: %u", fmt_ctx->nb_streams);
16}
17

关键点:

  • avformat_find_stream_info 会读取文件头,可能需要一些时间
  • duration 的单位是 AV_TIME_BASE(微秒),需要除以 AV_TIME_BASE 得到秒
  • 如果 durationAV_NOPTS_VALUE,表示时长未知

注意:容器时长 vs 流时长

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

1// 容器级别的时长(所有流中最长的)
2double container_duration = (double)fmt_ctx->duration / AV_TIME_BASE;
3
4// 获取视频流的时长
5int video_index = -1;
6for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
7    if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
8        video_index = i;
9        break;
10    }
11}
12
13if (video_index >= 0) {
14    AVStream* video_stream = fmt_ctx->streams[video_index];
15    // 流的时长(以流的时间基为单位)
16    double stream_duration = video_stream->duration * av_q2d(video_stream->time_base);
17    LOG("Container duration: %.2f seconds", container_duration);
18    LOG("Video stream duration: %.2f seconds", stream_duration);
19    // 两者可能略有差异(通常差异很小,在几毫秒到几十毫秒之间)
20}
21

注意:容器码率 vs 流码率

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

1// 容器级别的总码率
2int64_t container_bitrate = fmt_ctx->bit_rate;
3
4// 获取各个流的码率
5for (unsigned int i = 0; i < fmt_ctx->nb_streams; i++) {
6    AVStream* stream = fmt_ctx->streams[i];
7    int64_t stream_bitrate = stream->codecpar->bit_rate;
8    const char* stream_type = (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) ? "Video" : "Audio";
9    LOG("%s stream bitrate: %lld bps", stream_type, stream_bitrate);
10}
11
12// 验证:总码率  视频码率 + 音频码率 + 其他流码率
13// 注意:由于编码器设置、容器开销等因素,可能不完全相等
14

实践3:打印详细的媒体信息

API: av_dump_format

1avformat_open_input(&fmt_ctx, filename, nullptr, nullptr);
2avformat_find_stream_info(fmt_ctx, nullptr);
3
4// 打印详细信息(输出到 stderr)
5av_dump_format(fmt_ctx, 0, filename, 0);
6

参数说明:

  • 第一个参数: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 的输出

示例输出解析:

1Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'test/resources/test-video-1280x720.MP4':
2  Metadata:
3    major_brand     : mp42          # 主要品牌标识
4    minor_version   : 0             # 次要版本
5    compatible_brands: mp42mp41isomavc1  # 兼容的品牌
6    creation_time   : 2021-04-30T06:55:38.000000Z  # 创建时间
7  Duration: 00:00:11.41, start: 0.000000, bitrate: 2517 kb/s
8  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)
9    #  #0:0 - 视频流
10    # [0x1] -  ID
11    # (und) - 语言代码(und = undefined)
12    # h264 (High) - 编码器和 profile
13    # avc1 - 编码器 ID(FourCC)
14    # yuv420p - 像素格式
15    # 1280x720 - 分辨率
16    # 2260 kb/s - 视频码率
17    # 30 fps - 帧率
18  Stream #0:1[0x2](und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 253 kb/s (default)
19    #  #0:1 - 音频流
20    # aac (LC) - 编码器和 profile
21    # 48000 Hz - 采样率
22    # stereo - 声道(立体声)
23    # fltp - 采样格式(浮点平面)
24

为什么需要先调用 avformat_find_stream_info

av_dump_format 需要完整的流信息才能打印详细信息。如果只调用 avformat_open_input,只能打印基本信息,流信息会显示为未知。

实践4:处理打开文件失败的情况

1AVFormatContext* fmt_ctx = nullptr;
2int ret = avformat_open_input(&fmt_ctx, "non_existent.mp4", nullptr, nullptr);
3
4if (ret < 0) {
5    char errbuf[AV_ERROR_MAX_STRING_SIZE];
6    av_strerror(ret, errbuf, AV_ERROR_MAX_STRING_SIZE);
7    LOG("Failed to open: %s", errbuf);
8    
9    // 常见错误码:
10    // AVERROR(ENOENT) - 文件不存在
11    // AVERROR(EIO) - IO 错误
12    // AVERROR_INVALIDDATA - 数据无效
13}
14

运行测试

编译项目

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

1cmake --build build/Release
2

运行所有第2课的测试

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

运行直接使用 API 的测试

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

编译并运行(推荐)

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

1cmake --build build/Release && ./build/Release/unit-test --gtest_filter="Lesson2_Format.*"
2

常见问题

Q1: 为什么 duration 是 0?

A: 需要在 avformat_open_input 后调用 avformat_find_stream_info 才能获取准确的时长。

Q2: 如何设置打开文件的超时时间?

A: 使用 AVDictionary 传递选项:

1AVDictionary* opts = nullptr;
2av_dict_set(&opts, "timeout", "5000000", 0);  // 5秒超时(微秒)
3avformat_open_input(&fmt_ctx, filename, nullptr, &opts);
4av_dict_free(&opts);
5

Q3: 如何判断文件格式?

A: 打开文件后,通过 fmt_ctx->iformat->name 获取格式名称。

Q4: 为什么容器级别的时长和单个流的时长不一致?

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

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

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

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

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

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


参考


音视频教程-第二节》 是转载文章,点击查看原文


相关推荐


Interspeech2022论文解读 | CUSIDE:一个流式语音识别新框架,刷新SOTA
成都它思科技有限公司2026/3/4

简介 本文介绍清华大学语音处理与机器智能实验室(Speech Processing and Machine Intelligence, SPMI)与美团的联合工作 — CUSIDE:分块、模拟未来、解码的流式语音识别新框架,刷新了目前Aishell-1上流式模型的SOTA(State Of The Art,最好结果)。该工作已被语音领域的国际会议Interspeech2022接收,论文的作者是安柯宇、郑华焕、欧智坚、向鸿雨、丁科、万广鲁。 论文链接: http://oa.ee.


Django 应用 OOM(Out of Memory)故障的定位思路和排查方法
哈里谢顿2026/2/24

二、定位思路总览 1. 确认现象 → 2. 内存分析 → 3. 代码审查 → 4. 复现验证 → 5. 修复优化 ↑___________________________________________________________| 三、详细排查步骤 第一步:确认内存使用趋势 1.1 系统层面监控 # 查看进程内存(RSS:实际物理内存,VSZ:虚拟内存) ps aux --sort=-%mem | head -20 # 实时观察 watch -n 1 'ps -p <PID>


我又开发了一款桌面APP,功能强大
500佰2026/2/16

最近这段时间,开始沉迷一件事,在抖音录制我AI写代码、做实战开发的视频,用opencode / claudecode / Agent skills 等大模型进行AI项目开发,耗时7个晚上,最晚的一次,写到了夜间3点,录制了5个视频,开发消耗AI大模型token 数1500左右。 这次我开发了一款桌面录屏APP,名字叫做focusME,目前已经开发完成,可一键安装在我们的桌面,接下来讲解一下整个开发过程。 开发成果 开发过程 前面我用opencode里面Agent skills去制定产品


Skills.lc 是什么?为什么我会做(用)这个站
HBLOG2026/2/7

在折腾 AI Agent、CLI 工具和各种自动化脚本的过程中,我一直有一个很现实的问题: 好的 skill / workflow 到底该放哪?怎么复用? Prompt 太零散,放在 Notion、Gist、README 里,时间一长就找不到; 不同项目里反复复制粘贴,又很难维护; 看到 GitHub 上有人写了不错的 skill,也不知道怎么发现、怎么用。 Skills.lc 就是在这样的背景下出现的。 它本质上不是“又一个 AI 平台”,而是一个 技能索引与分发站点,专门用来收集、整理


Spring注解秘籍:优雅地使用 @RequestHeader
独泪了无痕2026/1/29

前言   在 Spring Boot 开发中,HTTP 请求头(Header)是客户端和服务器之间传递元数据的重要方式。通过请求头,客户端可以传递认证信息、内容类型、语言偏好等数据。Spring Boot 提供了 @RequestHeader 注解,用于方便地从 HTTP 请求头中提取数据。本文将详细介绍 @RequestHeader 注解的使用方法,包括基本用法、默认值处理、多值头处理以及实际应用场景。 一、注解定义与核心属性 1.1 @RequestHeader 是什么   在构建现代 W


筑牢金融底座:企业级区块链全球化数据库架构设计白皮书
China_Yanhy2026/1/20

📖 前言:Web3 业务的双重账本 在 Web3 业务中,区块链(AMB)是不可篡改的“链上真理”,而关系型数据库(RDS/Aurora)则是承载用户资产、撮合逻辑和KYC信息的“链下业务核心”。对于追求全球化的高频交易项目,数据库的架构设计必须解决两个核心矛盾:跨国访问的物理延迟 与 资金数据的一致性。 第一部分:旗舰方案 —— Amazon Aurora Global Database (深度解析) 这是针对跨国交易所(如币安、Coinbase 模式)的首选架构。 1. 核心架构


Ansible自动化(十五):加解密详解
cly12026/1/12

Ansible Vault 是 Ansible 提供的一套用于保护敏感数据的机制,可以对各类配置文件进行加密,防止敏感信息(如密码、私钥、API 密钥等)以明文形式暴露在代码仓库或配置文件中。 一、为什么需要 Ansible 加密? 场景说明: Playbook 中包含数据库密码、API Token、SSH 私钥等敏感信息Inventory(主机清单)中直接写入了连接密码(如 ansible_password)变量文件(vars/main.yml)中包含机密配置 ✅ Ansible Vaul


Go 项目结构总是写乱?这个 50 行代码的 Demo 教你标准姿势
Java小成2026/1/4

1. 场景复现:那个让我头疼的时刻 去年,我接手了一个"祖传" Go 项目。打开代码仓库的那一刻,我整个人都不好了——所有代码都塞在一个 main.go 里,足足 3000 多行。想加个功能?先花半小时找代码在哪。想写个单元测试?抱歉,函数全是私有的,而且互相耦合,根本没法单独测。 我当时就在想:如果当初写这个项目的人,能从第一天就用一个规范的结构,后面的人得少掉多少头发? 后来我开始研究 Go 官方和社区推荐的项目布局,发现其实规则很简单,但很多人就是不知道。于是我写了这个 50 行代码的小


Vue 实例挂载的过程是怎样的?
全栈陈序员2025/12/25

一、整体流程概览 当我们执行 new Vue({ ... }) 时,Vue 会经历 初始化 → 编译模板 → 挂载 DOM 三个阶段。整个过程由 _init 方法驱动,最终通过 $mount 完成视图渲染。 核心路径: new Vue() → _init() → initState() → $mount() → mountComponent() → _render() → _update() → 真实 DOM 二、详细步骤解析 1. 构造函数与 _init 初始化 源码位


从已损坏的备份中拯救数据
神奇的程序员2025/12/17

前言 12月15号早上,一觉醒来,拿起手机看到我的邮箱收到了内网服务无法访问的告警邮件,本以为只是简单的服务卡死,将服务器重启后就去上班了。 后来,陆续有好友联系我说网站挂了。 定位问题 晚上下班回家后,尝试将电脑断电重启,发现pve只能存活2分钟左右,然后整个系统卡死,无法进行任何操作。首先,我想到的是:会不会某个vm虚拟机或者ct容器影响到宿主机了。 因为系统只能存活几分钟,在执行禁用操作的时候,强制重启了好几次服务器。当所有的服务都停止启动后,卡死的问题依旧存在。 翻日志 没辙了,这已经

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客