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

推荐订阅源

A
About on SuperTechFans
D
DataBreaches.Net
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
V
Visual Studio Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
B
Blog RSS Feed
Recent Announcements
Recent Announcements
The Register - Security
The Register - Security
S
Secure Thoughts
Y
Y Combinator Blog
The Last Watchdog
The Last Watchdog
L
LINUX DO - 最新话题
V2EX - 技术
V2EX - 技术
腾讯CDC
GbyAI
GbyAI
G
Google Developers Blog
博客园 - 司徒正美
博客园 - 三生石上(FineUI控件)
T
The Exploit Database - CXSecurity.com
T
Threat Research - Cisco Blogs
P
Proofpoint News Feed
Schneier on Security
Schneier on Security
Microsoft Security Blog
Microsoft Security Blog
Jina AI
Jina AI
WordPress大学
WordPress大学
aimingoo的专栏
aimingoo的专栏
MyScale Blog
MyScale Blog
Help Net Security
Help Net Security
K
Kaspersky official blog
P
Privacy & Cybersecurity Law Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
AI
AI
MongoDB | Blog
MongoDB | Blog
Scott Helme
Scott Helme
J
Java Code Geeks
Engineering at Meta
Engineering at Meta
H
Heimdal Security Blog
H
Help Net Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
S
Security Affairs
TaoSecurity Blog
TaoSecurity Blog
The GitHub Blog
The GitHub Blog
Hacker News: Ask HN
Hacker News: Ask HN
Martin Fowler
Martin Fowler
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Project Zero
Project Zero
T
The Blog of Author Tim Ferriss
Last Week in AI
Last Week in AI

音视频开发进阶

音视频教程-第二节 真的,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);
    

参考

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