目录
4.3.2、AVStream的获取--flv_read_packet
C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)https://blog.csdn.net/chenlycly/article/details/125529931C/C++实战专栏(专栏文章已更新400多篇,持续更新中...)
https://blog.csdn.net/chenlycly/category_11931267.htmlC++ 软件开发从入门到精通(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/category_12695902.htmlVC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)
https://blog.csdn.net/chenlycly/article/details/124272585C++软件分析工具从入门到精通案例集锦(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/article/details/131405795开源组件及数据库技术(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/category_12458859.html网络编程与网络问题分享(专栏文章,持续更新中...)
https://blog.csdn.net/chenlycly/category_2276111.html一般音视频编解码器处理的是原始的裸音视频数据(编码器对原始音视频数据进行编码,解码器从编码后的音视频数据中解析出原始的音视频数据)。flv/avi/mp4等格式文件中包含的是编码后的音视频数据,解复用的目的就是从封装容器中解析出各音频流或视频流等。本文以FLV解复用器分析ffmpeg解复用的处理逻辑,介绍了ffmeg中解复用器的注册,并以FLV解复用器为例分析解复用器如何从封装格式中,解析出不同的视频、音频、数据流。
1、FFmpeg简介
FFmpeg是一个开源的多媒体框架,广泛用于处理音视频处理领域。它提供了一套丰富的工具和库,支持广泛的多媒体格式的解码、编码、转码、等功能。支持多种格式的音视频编码和解码,以及音视频的转换、剪辑、过滤、流处理等功能。
FFmpeg主要特性如下:
1)音视频编解码
- FFmpeg包含了非常先进的音频/视频编解码库libavcodec,其中很多codec都是从头开发的,以保证高可移植性和编解码质量。
- 支持几乎所有的音视频格式和编解码器,包括常见的MP4、AVI、MOV、MKV、FLV、MP3、AAC等文件封装格式,以及H.264、H.265、AAC、VP9、AV1等音视频编解码器。
2)音视频处理
- 提供了丰富的音视频处理功能,如去噪、模糊视频、色彩转换、视频旋转、提取帧、缩放视频尺寸等。
- 可以通过其提供的滤镜库libavfilter对音视频数据进行各种处理,如缩放、裁剪、添加水印等。
3)流媒体处理
- FFmpeg支持将本地音视频文件转换为流媒体格式,并通过网络进行传输和播放。
- 完全支持使用HLS和MPEG-DASH打包视频,并可以配置为使用RTMP或其他协议来传输视频。
4)封装与解封装
- 提供了对各种音视频封装格式的读写支持,如MP4、FLV等。
- AVFormat模块封装了Protocol层和Demuxer、Muxer层,使得协议和格式对于开发者来说是透明的。
5)高效稳定
FFmpeg在音视频处理方面表现出色,具有高效且稳定的性能,能够处理大量的音视频数据。
6)开源FFmpeg采用LGPL或GPL许可证,其开源属性意味着任何人都能修改并使用它。
7)跨平台FFmpeg可以在多种操作系统上运行,包括但不限于Linux、Windows、macOS等,这使其具有非常广泛的适用性。
8)社区支持作为一个开源项目,FFmpeg拥有活跃的社区,用户可以从社区获得帮助和最新的开发动态。
几乎所有的视频播放器都使用到了FFmpeg的音视频解码功能,比如国内知名的暴风影音、QQ影音、腾讯视频、爱奇艺视频、优酷视频等。
FFmpeg内部支持多种音视频格式之间的相互转换,很多音视频转化软件都用到了FFmpeg的音视频转换功能,比如大家常用的格式工厂、暴风转码(暴风影音自带的工具)、QQ音影自带的视频格式转换工具、狸窝视频转换器、迅捷视频转换器等。
2、FLV文件格式介绍
FLV视频格式是Adobe公司设计开发的一种流媒体的封装格式,总体上看,FLV包括文件头(Flv Header)和文件体(Flv Body)两部分,其中文件体由一系列的Tag及Tag Size对组成。Tag又可以分成三类:audio,video,script,分别代表音频流,视频流,脚本流(关键字或者文件信息之类)。
header部分记录了flv的类型、版本等信息,是flv的开头,一般都差不多,占9bytes。具体格式如下:
body部分由一个个Tag组成,每个Tag的下面有一块4bytes的空间,用来记录这个tag的长度,这个后置用于逆向读取处理。 每个Tag由也是由两部分组成的:Tag Header和Tag Data。Tag Header里存放的是当前Tag的类型、数据区(Tag Data)长度等信息,具体如下:
3、注册解复用器
调用av_register_all()接口注册所有格式的编解码器和复用/解复用器、硬件加速器、解析等。调用关系图如下:
所有的注册逻辑都是相同的,注册解复用也一样,其链表头为first_iformat:
Ffmpeg/libavformat/allformats.c
真正实现注册解复用器的函数是av_register_input_format(),如下:
Ffmpeg/libavformat/formats.c
由此可以看出,所有解复用器按注册的先后顺序以链表结构存储起来,链表头为first_iformat。以注册flv、avi为例:
First_iformat是AVInputFormat*类型指针,指向第一个注册的flv解复用器ff_flv_demuxer,flv解复用器对象的next变量(AVInputFormat*类型)指向avi解复用器对象ff_avi_demuxer,依次向后。
需要注意的是,ff_flv_demuxer、ff_avi_demuxer都是按AVInputFormat结构定义好的解复用器对象,以flv为例:
Flv解复用:Ffmpeg/libavformat/flvdec.c
通过上面的分析,向ffmpeg注册新的解码器的步骤如下:
1)在/libavformat下,创建源文件,在该源文件中按AVInputFormat类型接口定义该算法处理。
2)调用REGISTER_DEMUXER宏,注册该解码器。
在这里,给大家重点推荐一下我的几个热门畅销专栏,欢迎订阅:(博客主页还有其他专栏,可以去查看)
专栏1:(该精品技术专栏的订阅量已达到510多个,专栏中包含大量项目实战分析案例,有很强的实战参考价值,广受好评!专栏文章持续更新中,预计更新到200篇以上!欢迎订阅!)
C++软件调试与异常排查从入门到精通系列文章汇总https://blog.csdn.net/chenlycly/article/details/125529931
本专栏根据多年C++软件异常排查的项目实践,系统地总结了引发C++软件异常的常见原因以及排查C++软件异常的常用思路与方法,详细讲述了C++软件的调试方法与手段,以图文并茂的方式给出具体的项目问题实战分析实例(很有实战参考价值),带领大家逐步掌握C++软件调试与异常排查的相关技术,适合基础进阶和想做技术提升的相关C++开发人员!
考察一个开发人员的水平,一是看其编码及设计能力,二是要看其软件调试能力!所以软件调试能力(排查软件异常的能力)很重要,必须重视起来!能解决一般人解决不了的问题,既能提升个人能力及价值,也能体现对团队及公司的贡献!
专栏中的文章都是通过项目实战总结出来的,包含大量项目问题实战分析案例,有很强的实战参考价值!专栏文章还在持续更新中,预计文章篇数能更新到200篇以上!
专栏2:
C++常用软件分析工具从入门到精通案例集锦汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/131405795
常用的C++软件辅助分析工具有SPY++、PE工具、Dependency Walker、GDIView、Process Explorer、Process Monitor、API Monitor、Clumsy、Windbg、IDA Pro等,本专栏详细介绍如何使用这些工具去巧妙地分析和解决日常工作中遇到的问题,很有实战参考价值!
专栏3:(本专栏涵盖了C++多方面的内容,是当前重点打造的专栏,专栏文章已经更新到400多篇,持续更新中...)
C/C++实战进阶(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_11931267.html
以多年的开发实战为基础,总结并讲解一些的C/C++基础与项目实战进阶内容,以图文并茂的方式对相关知识点进行详细地展开与阐述!专栏涉及了C/C++领域多个方面的内容,包括C++基础及编程要点(模版泛型编程、STL容器及算法函数的使用等)、C++11及以上新特性(不仅看开源代码会用到,日常编码中也会用到部分新特性,面试时也会涉及到)、常用C++开源库的介绍与使用、代码分享(调用系统API、使用开源库)、常用编程技术(动态库、多线程、多进程、数据库及网络编程等)、软件UI编程(Win32/duilib/QT/MFC)、C++软件调试技术(排查软件异常的手段与方法、分析C++软件异常的基础知识、常用软件分析工具使用、实战问题分析案例等)、设计模式、网络基础知识与网络问题分析进阶内容等。
专栏4:
VC++常用功能开发汇总(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/article/details/124272585
将10多年C++开发实践中常用的功能,以高质量的代码展现出来。这些常用的高质量规范代码,可以直接拿到项目中使用,能有效地解决软件开发过程中遇到的问题。
专栏5:
C++ 软件开发从入门到精通(专栏文章,持续更新中...)https://blog.csdn.net/chenlycly/category_12695902.html
根据多年C++软件开发实践,详细地总结了C/C++软件开发相关技术实现细节,分享了大量的实战案例,很有实战参考价值。
4、解复用器的处理
解复用器处理过程中,涉及到若干结构体类型,这里先介绍下。
4.1、AVFormatContext
媒体封装上下文,包含如下成员,下面我们分析这几个结构体类型:
4.1.1、AVClass
AVClass是一个成员管理系统,记录了其关联类的类名、并通过其AVOption对象设置关联类的成员变量等,AVFormatContext、AVIOContext、AVCodecContext等都有其对应的AVClass成员管理对象AVFormatContext对象调用avformat_get_context_defaults()接口初始化默认值时,将其av_class对象指向全局的AVClass对象av_format_context_class, av_format_context_class的AVOption成员option指向全局的avformat_options数组,这里列出一部分如下:
/ffmpeg/libavformat/options.c
/ffmpeg/libavformat/options_table.h
AVFormatContext、AVClass与AVOption之间的关系如下:
图中AVFormatContext结构体的第一个变量为AVClass类型的指针av_class,它在AVFormatContext结构体初始化的时候,被赋值指向了全局静态变量av_format_context_class结构体(定义位于libavformat\options.c)。而AVClass类型的av_format_context_class结构体中的option变量指向了全局静态数组avformat_options(定义位于libavformat\options_table.h)。
Ps:应该也可以自定义一个AVClass及AVOption对象,赋给AVFormatContext。
4.1.2、AVOption
AVClass中存储了AVOption类型的option数组,用于存储AVClass关联类的成员变量信息,每一个成员变量对应一个AVOption。可以使用av_opt_set()函数通过字符串的方式(传入参数为变量名称的字符串和变量值的字符串)设置AVOption的值。
/ffmpeg/libavutil/opt.c
该函数首先通过av_opt_find2()在**(AVClass**)obj中查找到与name匹配的AVOption对象o,然后根据该对象的类型o->type调用相应的类型设置函数(如set_string_bool、set_string、set_string_image_size等),将字符串val转换为相应type的数据并对该AVOption对象o进行赋值。
Ps: av_opt_get通过字符串获取变量值:
4.1.3 AVDictionary—AV字典
AVDictionary键值对类型,结构体定义如下:
/ffmpeg/libavutil/dict.c
/ffmpeg/libavutil/dict.h
这里介绍设置键值和获取键值的两个接口
/ffmpeg/libavutil/dict.h
功能:将键值对(key, value)添加到pm中,如果pm为*pm为NULL,将会创建一个新的AVDictionaryEntry对象并将该对象地址赋值给*pm
/ffmpeg/libavutil/dict.h
功能:在m中遍历,获得匹配key值的AVDictionaryEntry对象(即返回值)。
4.1.4、AVIOContext
协议(文件)操作的顶层结构是AVIOContext,这个对象实现了带缓冲的读写操作;FFMPEG的输入对象AVFormat的pb字段指向一个AVIOContext。
AVIOContext的opaque实际指向一个URLContext对象,这个对象封装了协议对象及协议操作对象,其中prot指向具体的协议操作对象,priv_data指向具体的协议对象。
URLProtocol为协议操作对象,针对每种协议,会有一个这样的对象,每个协议操作对象和一个协议对象关联,比如,文件操作对象为ff_file_protocol,它关联的结构体是FileContext。
4.1.4.1、URLProtocol
URLProtocol是FFMPEG操作文件的结构(包括文件,网络数据流等等),包括open、close、read、write、seek等操作。
在在av_register_all()函数中,通过调用REGISTER_PROTOCOL()宏,所有的URLProtocol都保存在以first_protocol为链表头的链表中
/ffmpeg/libavformat/url.h
以file文件为例:
查看file_open发现, ff_file_protocol操作的FileContext数据就是URLContext的priv_data
4.1.4.2、AVIOContext的初始化及获取
/ffmpeg/libavformat/aviobuf.c
从上图发现,最终实现AVIOContext初始化并获取对文件对象的是ffio_open_whitelist。
4.1.5、AVInputFormat
从前文了解到AVInputFormat实际上就是解复用器结构类型,各类型的解复用器已经在av_register_all()是注册成全局的链表,表头为first_iformat。以flv为例:
/ffmpeg/libavformat/flvdec.c
- .name:解复用器名称
- .long_name:对应的文件格式
- .priv_date_size:该解复用器关联的文件结构体类型
- .read_probe:检测输入AVProbeData文件名数据与该解复用器的匹配度
- .read_header:
- .read_packet:
- .read_seek:
- .extensions:后缀名
后边几个函数分别为探测、读取头信息、读取包信息、搜索、关闭,从这几个函数的源代码可以看出,解复用器处理的FLVContext数据指针保存在其关联AVFormatContext->priv_data中。
4.1.6、AVStream
AVStream是存储媒体流信息的结构体,其初始化函数avformat_new_stream(),解复用器负责把不同媒体流从容器(媒体文件)中解析出来并保存到不同的AVStream中。
4.2、解复用器的获取
AVInputFormat解复用器对象的获取有两种方法:
1)由文件名获取:/ffmpeg/libavformat/utils.cinit_input()
Pd中包含了文件名,由文件名检测出对应的解复用器。
av_probe_input_format2调用av_probe_input_format3,在av_probe_input_format3中,遍历所有注册的解复用器fmt1(AVInputFormat),对于每个解复用器fmt1,调用fmt1的检测函数read_probe检测其与文件名的匹配度,如果没有该函数,则通过fmt1的后缀名extensions匹配。
2)如果文件名检测不出来,则通过AVIOContext获取:/ffmpeg/libavformat/utils.cinit_input()
s->pb为解复用器AVInputFormat对象所在AVFormatContext对象的AVIOContext成员,其获取方法如下:
在av_probe_input_buffer2函数中,实际上也是通过avio_read接口从s->pb中得到媒体相关的AVProbeData数据,然后再调用av_probe_input_format2接口(即方法一),获得解复用器。需要注意的是:
i)s->io_open在s(AVFormatContext类型)初始化时构建
/ffmpeg/libavformat/options.c
查看io_open_default发现,该函数调用的是ffio_open_whitelist函数,由3.1.4.2可知正是该函数根据文件名获取了其对应的AVIOContext对象pb。
ii)方法一和方法二虽然最终都调用av_probe_input_format2检测,但在该函数中看,方法一、二的参数和处理流程是不一样的。
/ffmpeg/libavformat/avformat.h
方法一中,pd = {filename, NULL, 0} 因为pd->buf = NULL,所以调用fmt1->read_probe是检测不到对应的解复用器的,只能通过pd->filename和fmt1->extensions比较得到结果。
方法二中,pd = {filename, buf, 0} ,其中buf是在av_probe_input_buffer2中调用avio_read获取的。由于pd->buf不为NULL,所以在fmt1->read_probe可检测到其对应的解复用器。
4.3、Flv解复用器处理流程分析
Flv解复用器按照flv文件格式解析出数据,根据不同的音频、视频数据,创建出对应的AVStream并记录到AVFormatContext中。这里介绍主要的两个函数
4.3.1、读取头信息--flv_read_header
/ffmpeg/libavformat/flvdec.c
首先我们要知道s->pb是AVIOContext,该结构体中存在一个读写指针buf_ptr记录当前文件的读写位置。下面我们对每行代码进行分析:
line632:s->priv_data在avformat_open_input函数中被指向ff_flv_demuxer.priv_class即flv_class(参 看ff_flv_demuxer定义),显然这里flv_class应该是在初始化FLVContext对象时,被赋值给FLVContext成员管理对象的,但是我没有找到FLVContext初始化的地方。
/ffmpeg/libavformat/utils.c
Line635:跳过4个字节
Line636:读取1个字节,也就是第5个字节,对照FLV文件格式,我们发现改字节记录了流信息
Line640:读取4个字节,获取header长度
Line641:从文件开始,跳过文件头Header,到FLV body部分
Line642:跳过4个字节,这4个字节记录上一个Tag的大小即表中的PreTagSize0
到这里flv_read_header就结束了,我们发现该函数的作用仅仅是把读写指针buf_ptr指向flv的第一个有效媒体数据部分即Tag0。
需要说明的是,不同媒体格式的处理都不一样,avi的avi_read_header函数,就实现了AVStream的创建即参数设置,而FLV的AVStream则是在flv_read_packet()中创建的。
4.3.2、AVStream的获取--flv_read_packet
该函数代码较多,这里分析主要代码行
/ffmpeg/libavformat/flvdec.c
Line850:获取AVIOContext当前读写位置
Line851:读1字节,该字节标识当前tag是音频流(8)/视频流(9)/脚本数据(18)
Line853:3字节,记录数据区长度
Line854:记录已经读取的所有tag总长度
Line855:获取时间戳(3字节)
Line856:获取扩展时间戳(1字节)并和line855行获取的时间戳组合成完整的时间戳
Line860:跳过3个字节(这3个字节为streamid),读写指针指向数据区TagData
Line886:根据Line851获取的type,判断该tag是否为音频数据
Line888:读1个字节flags,该字节高4位标识音频格式、2-3两位标识采样率,最后两位分别标识位宽和声道
Line890:根据type判断该tag是否为视频数据
Line892:读1字节flags,该字节高4位标识帧类型(I/P/B),低四位标识视频格式
Line896-901:脚本数据,flv_read_metabody函数获取码流宽高等信息,并保持到AVFormatContext
通过以上两段代码的分析,我们发现flv解复用器所做的操作都是按照flv语法获取数据,很显然其他解复用器也是同样的原理。
Line936-950:遍历已有的AVStream队列,查找和当前tag数据类型相同的AVStream流,显然从tag1开始处理时,是没有AVStream队列的,应该跳转到Line951行
Line940:根据Line888得到的flags,获取音频解码器id(st->codecpar->codec_id),如AV_CODEC_ID_AAC
Line944:根据Line892得到的flags,获取视频解码器id(st->codecpar->codec_id),如AV_CODEC_ID_H264
Line954:根据Line887/891的stream_type创建并初始化AVStream流。查看create_stream函数发现,该函数就是通过avformat_new_stream创建的AVStream对象。
Line1009-1012:根据flags获取音频通道数channels、采样率sample_rate和位宽bits_per_coded_sample
Line1015-1025:更新创建的AVStream流的解码器参数信息
Line1042:设置AVStream的视频解码器信息
Line1112:读取tagData数据到AVPacktet
Line1115:设置解码时间戳dts
Line1116:设置显示时间戳pts
Line1117:设置该AVPacket所在的AVStream队列
flv_read_header()和flv_read_packet()两个函数基本完成了flv媒体的解复用功能,这两个函数分别在avformat_open_input()和avformat_find_stream_info()两个接口中用到,并完成对AVFormatContext和AVStream信息的赋值。
调用avformat_open_input()获取的信息如下:通过之前的分析,我们知道flv_read_header没有获取信息,仅仅是移动了读写指针。
调用avformat_find_stream_info()获取的信息如下,该函数中会调用flv_read_packet(),并解析相关数据,可以完整填充AVFormatContext和AVStream信息:
至此,解复用器逻辑分析结束。
以上代码截图中的代码查看工具是Source Insight,该工具是轻量级的代码查看与编辑工具,虽不能编译代码,但查看和编辑代码很方便,我们经常使用该工具去查看开源代码。关于如何使用Source Insight工具,可以查看我之前写的文章:
如何使用 Source Insight 查看编辑C/C++源代码?https://blog.csdn.net/chenlycly/article/details/124347857
5、总结
简单来讲,解复用器功能就是获取音视频码流所需解码器,在读取包AVPacket后,根据数据包AVPacket所在流AVStream,找到对应的解码器进行解码。