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

推荐订阅源

博客园 - 司徒正美
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
腾讯CDC
WordPress大学
WordPress大学
爱范儿
爱范儿
GbyAI
GbyAI
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 聂微东
Google DeepMind News
Google DeepMind News
Recent Announcements
Recent Announcements
Latest news
Latest news
Last Week in AI
Last Week in AI
V2EX - 技术
V2EX - 技术
I
InfoQ
N
News | PayPal Newsroom
SecWiki News
SecWiki News
Microsoft Azure Blog
Microsoft Azure Blog
美团技术团队
T
Troy Hunt's Blog
H
Hacker News: Front Page
S
SegmentFault 最新的问题
TaoSecurity Blog
TaoSecurity Blog
V
Visual Studio Blog
Martin Fowler
Martin Fowler
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园_首页
S
Security @ Cisco Blogs
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
小众软件
小众软件
L
LangChain Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tor Project blog
L
LINUX DO - 热门话题
月光博客
月光博客
S
Schneier on Security
Y
Y Combinator Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
P
Proofpoint News Feed
Forbes - Security
Forbes - Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Hugging Face - Blog
Hugging Face - Blog
A
Arctic Wolf
Webroot Blog
Webroot Blog
博客园 - 叶小钗
F
Fortinet All Blogs
S
Securelist
AI
AI
B
Blog RSS Feed
Security Latest
Security Latest

音视频开发进阶

音视频教程-第二节 真的,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 音视频开发进阶

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

音视频系列教程

课程目标

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


知识点

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);
    

参考

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