惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

aimingoo的专栏
aimingoo的专栏
Microsoft Security Blog
Microsoft Security Blog
S
SegmentFault 最新的问题
Forbes - Security
Forbes - Security
爱范儿
爱范儿
Stack Overflow Blog
Stack Overflow Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
CERT Recently Published Vulnerability Notes
S
Schneier on Security
Scott Helme
Scott Helme
C
Check Point Blog
T
Tenable Blog
博客园 - 三生石上(FineUI控件)
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
Lohrmann on Cybersecurity
Google DeepMind News
Google DeepMind News
人人都是产品经理
人人都是产品经理
N
News and Events Feed by Topic
B
Blog
P
Privacy International News Feed
I
Intezer
T
Threatpost
Google DeepMind News
Google DeepMind News
L
LangChain Blog
Last Week in AI
Last Week in AI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
L
LINUX DO - 最新话题
博客园_首页
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Hacker News - Newest:
Hacker News - Newest: "LLM"
C
Cybersecurity and Infrastructure Security Agency CISA
N
News and Events Feed by Topic
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
N
News | PayPal Newsroom
The Hacker News
The Hacker News
S
Security @ Cisco Blogs
罗磊的独立博客
PCI Perspectives
PCI Perspectives
Y
Y Combinator Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
大猫的无限游戏
大猫的无限游戏
U
Unit 42
Hacker News: Ask HN
Hacker News: Ask HN
D
Docker
AI
AI
小众软件
小众软件
博客园 - 叶小钗
H
Help Net Security
TaoSecurity Blog
TaoSecurity Blog
P
Privacy & Cybersecurity Law Blog

音视频开发进阶

音视频教程-第三节 真的,AI 可能就是新时代的信息差 充值 Cursor 之后,工作有了哪些变化?🤔 个人'蒸馏'大模型能做哪些有意思的事情 DeepSeek 大模型在 Mac 上的部署和运行 音视频教程-第一节 【WebRTC 专栏】-- Android 开发集成 WebRTC 库的几种方式 【WebRTC 专栏】-- 在 Mac M1 等系列芯片编译和开发 WebRTC-Android 库 Meta Llama3 大模型在 Mac 上的部署和运行 iOS VideoToolBox 解码 HEVC Open-GOP 视频的问题排查 Flutter 状态管理之 InheritedWidget 使用和分析 用 ChatGPT 回答技术问题怎么样 ? 音视频开发系统入门大致路线 UE 4.27 添加自定义 ShadingModel 用 UE4 虚幻引擎做个捏脸小功能~~ UE4 材质练习 之 凹凸贴图偏移的使用 UE4 材质练习系列基础 OpenGL上下文创建以及共享机制 007 | 播放器系列专栏-解析 MP4 文件读取信息 006 | 播放器系列专栏-在 Mac 上查看 MP4 格式信息 干货 | 快速抽取缩略图是怎么练成的? 关于直播、WebRTC、FFmpeg 的那些事 005 | 播放器系列专栏-在 Windows 上查看 MP4 格式信息 将音视频中的花屏、绿屏、黑屏问题一网打尽 关于音视频里面的解码帧率和渲染帧率 004 | 播放器系列专栏-认识MP4视频(下) 003 | 播放器系列专栏-认识MP4视频(上) 入门或者转行音视频,应该要怎么做? H264视频文件如何缩放分辨率 002 | 播放器系列专栏-FFmpeg依赖库的配置 001 | 播放器系列专栏-关于播放器项目的一个小实践 Seek策略以及在有B帧情况下的处理 目前流媒体开发工程师工作内容主要是什么? 一个音视频领域专业问答的小圈子 干货收藏 || Vulkan Game Engine 视频教程 音视频春节假期内卷指南(实操) Vulkan 在 FFmpeg 中的支持 Windows 下 FFmpeg 和 LibX264 的编译和配置 Metal 开发 | 使用 C++ 进行接口调用 音视频开发工作经验分享 || 视频版 FFmpeg 调用 MediaCodec 硬解码到 Surface 上 代码吸猫 | 用 OpenGL 图像渲染的养猫计划 百倍变速--解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!! 老生常谈-FFmpeg 的编译问题轻松搞定 FFmpeg 调用 Android MediaCodec 进行硬解码(附源码) 【WebRTC 专栏】--创建相机预览 Unity Shader 光照基础之 Half Lambert 光照模型 Unity Shader 光照基础之Lambert光照模型 Unity Shader 光照基础内容 Unity Shader 显示一张图片纹理 UnityShader 的基本概念 Unity 物体的基本操作 C++ 模板系列小结07-尾置返回类型 C++ 模板系列小结06-可变参数模板特性 C++ 中的多线程的使用和线程池建设 C++ 模板系列小结05-模板类型作为模板参数 C++ 模板系列小结04-类模板中的成员模板 C++ 模板系列小结03-在模板中指定变量类型 C++ 模板系列小结02-非类型模板参数 C++ 模板系列小结01-函数模板和类模板 从零打造渲染引擎系列01-什么是渲染引擎 iOS开发 - 在 Swift 中去调用 C/C++ 代码 2021 技术新番 - 从零打造渲染引擎系列 iOS 音视频开发的一些基础准备工作 音视频交流群又来啦~~~ 【WebRTC 专栏】WebRTC & Android 开发学习环境搭建~ 【喜大普奔】域名终于备案通过啦 Shader 优化 | OpenGL 绘制网格效果 【音视频连载-011】第二季 FFmpeg 一层一层获取文件信息 KodeLife | Shader 实时编辑预览的强大工具使用实践 推荐几个堪称教科书级别的 Android 音视频入门项目 【音视频连载-010】第二季 FFmpeg 日志打印 【音视频连载-008】基础学习篇-SDL 播放 PCM 音频文件(下) 【音视频连载-007】基础学习篇-SDL 播放 PCM 音频文件(上) 【音视频连载-006】基础学习篇-SDL 播放 YUV 视频文件 【音视频连载-005】基础学习篇-SDL 加载 YUV 文件并显示 【音视频连载-004】基础学习篇-SDL 加载图片并显示 【音视频连载-003】基础学习篇-SDL 消息循环和事件响应 【音视频连载-002】基础学习篇-SDL 创建窗口并显示颜色 【音视频连载-001】基础学习篇- SDL 介绍以及工程配置 LearnOpenGL 源码在 MAC 上的编译与调试 2019 年终总结与回顾 Android NDK 开发的免费技术视频来啦~~ OpenGL 实现视频编辑中的转场效果 OpenGL 实践之贝塞尔曲线绘制 图像库 libjpeg-turbo 编译与实践 图像库 libpng 编译与实践 rust 开发编译 Android 动态库实践 Android NDK 开发 —— 从 Assets 文件夹加载图片并上传纹理 简单易用的图像解码库介绍 —— stb_image 博客图床迁移记 进击的 Vulkan 移动开发之 SwapChain 进击的 Vulkan 移动开发之 Command Buffer 进击的 Vulkan 移动开发之 Instance & Device & Queue 进击的 Vulkan 移动开发(一)之今生前世 Java 显式锁 Lock 与条件队列 C++ 标准容器库小结 MediaCodec 硬编码之相机内容编码成 H264 文件 一文读懂 YUV 的采样与格式 《OpenGL ES 3.x 游戏开发》碰撞检测之 AABB 包围盒
音视频教程-第二节
音视频开发进阶 · 2026-03-13 · via 音视频开发进阶

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

音视频系列教程

课程目标

学习如何使用 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

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


参考

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