音频
Android
的
SDK
(指的是
Java
层提供的
API
,对应的
NDK
是
Native
层
提供的
API
,即
C
或者
C++
层可以调用的
API
)提供了
3
套音频播放的
API
,分别是:
MediaPlayer
、
SoundPool
和
AudioTrack
。这三个
API
的使
用场景各不相同,简单来说具体如下。
·MediaPlayer
:适合在后台长时间播放本地音乐文件或者在线的流
式媒体文件,它的封装层次比较高,使用方式比较简单。
·SoundPool
:适合播放比较短的音频片段,比如游戏声音、按键声
音、铃声片段等,它可以同时播放多个音频。
·AudioTrack
:适合低延迟的播放,是更加底层的
API
,提供了非常
强大的控制能力,适合流媒体的播放等场景,由于其属于底层
API
,所
以需要结合解码器来使用。
Android
的
NDK
提供了
OpenSL ES
的
C
语言的接口,可以提供非常强
大的音效处理、低延时播放等功能,比如在
Android
手机上可实现实时
耳返的功能。本书的项目案例中会更多地使用到底层
API
的功能,下面
就来详细地介绍
AudioTrack
与
OpenSL ES
这两个
API
的使用。
-----------------------------------------------------------------------------------------------------------------------------------------
OpenSL ES
1
)创建一个引擎对象接口。引擎对象是
OpenSL ES
提供
API
的唯一
入口,开发者需要调用全局函数
slCreateEngine
来获取
SLObjectItf
类型的
引擎对象接口:
SLObjectItf engineObject;
SLEngineOption engineOptions[] = { { (SLuint32) SL_ENGINEOPTION_THREADSAFE,
(SLuint32) SL_BOOLEAN_TRUE } };
slCreateEngine(&engineObject, ARRAY_LEN(engineOptions), engineOptions, 0, 0, 0);
2
)实例化引擎对象,需要通过在第
1
步得到的引擎对象接口来实例
化引擎对象,否则会无法使用这个对象,其实在
OpenSL ES
的使用中,
任何对象都需要使用接口来进行实例化,所以这里也需要封装出一个实
例化对象的方法,代码如下:
RealizeObject(engineObject);
SLresult RealizeObject(SLObjectItf object) {
return (*object)->Realize(object, SL_BOOLEAN_FALSE);
};
3
)获取这个引擎对象的方法接口,通过
GetInterface
方法,使用第
2
步已经实例化好了的对象,获取对应的
SLEngineItf
类型的对象接口,该
接口将会是开发者使用所有其他
API
的入口:
SLEngineItf engineEngine;
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
4
)创建需要的对象接口,通过调用
SLEngineItf
类型的对象接口的
CreateXXX
方法返回新的对象的接口,比如,调用
CreateOutputMix
方法
来获取一个
outputMixObject
接口,或者调用
CreateAudioPlayer
方法来获
取一个
audioPlayerObject
接口。由于篇幅有限,这里仅仅列出创建
outputMixObject
的接口代码,播放器接口的获取可以参考代码仓库中的
代码:
SLObjectItf outputMixObject;
(*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, 0, 0);
5
)实例化新的对象,任何对象接口获取出来之后,都必须要实例
化,与第
2
步操作其实是一样的:
realizeObject(outputMixObject);
realizeObject(audioPlayerObject);
6
)对于某些比较复杂的对象,需要获取新的接口来访问对象的状
态或者维护对象的状态,比如在播放器
AudioPlayer
或录音器
AudioRecorder
中注册一些回调方法等,代码如下:
SLPlayItf audioPlayerPlay;
(*audioPlayerObject)->GetInterface(audioPlayerObject, SL_IID_PLAY,
&audioPlayerPlay);
//
设置播放状态
(*audioPlayerPlay)->SetPlayState(audioPlayerPlay, SL_PLAYSTATE_PLAYING);
//
设置暂停状态
(*audioPlayerPlay)->SetPlayState(audioPlayerPlay, SL_PLAYSTATE_PAUSED);
7
)待使用完该对象之后,要记得调用
Destroy
方法来销毁对象以及
相关的资源:
destroyObject(audioPlayerObject);
destroyObject(outputMixObject);
void AudioOutput::destroyObject(SLObjectItf& object) {
if (0 != object)
(*object)->Destroy(object);
object = 0;
}
使用
OpenSL ES
实现一个播放媒体文件的功能,即一个音频播放器
的渲染端逻辑的实例,请读者查看代码仓库中的
AudioPlayer
项目的
OpenSL ES
部分。注意,需要把
resource
目录下的音频文件放入运行的
sdcard
根目录下。
FFMPEG
·
容器/文件(
Conainer/File
):即特定格式的多媒体文件,比如
MP4
、
flv
、
mov
等。
·
媒体流(
Stream
):表示时间轴上的一段连续数据,如一段声音数
据、一段视频数据或一段字幕数据,可以是压缩的,也可以是非压缩
的,压缩的数据需要关联特定的编解码器。
·
数据帧/数据包(
Frame/Packet
):通常,一个媒体流是由大量的
数据帧组成的,对于压缩数据,帧对应着编解码器的最小处理单元,分
属于不同媒体流的数据帧交错存储于容器之中。
·
编解码器:编解码器是以帧为单位实现压缩数据和原始数据之间
的相互转换的。
4个可执行文件
于转码、推流、
Dump
媒体文件的
ffmpeg
用于播放媒体文件的
ffplay
用于获取媒体文件信息的
ffprobe
,
以及作为简单流媒体服务器的
ffserver
。
8个静态库
·AVUtil
:核心工具库,该模块是最基础的模块之一,下面的许多
其他模块都会依赖该库做一些基本的音视频处理操作。
·AVFormat
:文件格式和协议库,该模块是最重要的模块之一,封
装了
Protocol
层和
Demuxer
、
Muxer
层,使得协议和格式对于开发者来说
是透明的。
·AVCodec
:编解码库,该模块也是最重要的模块之一,封装了
Codec
层,但是有一些
Codec
是具备自己的
License
的,
FFmpeg
是不会默
认添加像
libx264
、
FDK-AAC
、
lame
等库的,但是
FFmpeg
就像一个平台
一样,可以将其他的第三方的
Codec
以插件的方式添加进来,然后为开
发者提供统一的接口。
·AVFilter
:音视频滤镜库,该模块提供了包括音频特效和视频特效
的处理,在使用
FFmpeg
的
API
进行编解码的过程中,直接使用该模块为
音视频数据做特效处理是非常方便同时也非常高效的一种方式。
·AVDevice
:输入输出设备库,比如,需要编译出播放声音或者视
频的工具
ffplay
,就需要确保该模块是打开的,同时也需要
libSDL
的预
先编译,因为该设备模块播放声音与播放视频使用的都是
libSDL
库。
·SwrRessample
:该模块可用于音频重采样,可以对数字音频进行
声道数、数据格式、采样率等多种基本信息的转换。
·SWScale
:该模块是将图像进行格式转换的模块,比如,可以将
YUV
的数据转换为
RGB
的数据。
·PostProc
:该模块可用于进行后期处理,当我们使用
AVFilter
的时
候需要打开该模块的开关,因为
Filter
中会使用到该模块的一些基础函
数。
如果是比较老的
FFmpeg
版本,那么有可能还会编译出来
avresample
模块,该模块其实也是用于对音频原始数据进行重采样,但是现在已经
被废弃掉了,不再推荐使用该库,而是使用
swrresample
库进行替代。
AAC编码
常见的有两种封
装格式:一种是
ADTS
格式的流,是
AAC
定义在
MPEG2
里面的格式;另
外一种是封装在
MPEG4
里面的格式,这种格式会在每一帧前面拼接一
个用声道、采样率等信息组成的头。开发者完全可以手动拼接该头信
息,即将
AAC
编码器输出的原始码流(
ADTS
头
+ES
流)封装进
MP4
、
FLV
或者
MOV
等格式的容器中时,需要先将
ADTS
头转换为
MPEG-4
AudioSpecficConfig
(描述了编码器的配置参数)头,并去掉原始码流
中的
ADTS
头(只剩下
ES
流)。但是使用
FFmpeg
提供好的
aac_adtstoasc
类型的
bit stream filter
可以非常方便地进行转换,
FFmpeg
为开发者隐藏
了实现的细节,并且提供了更好的代码可读性。
AVFormat
AVFormatContext
是
API
层直接接触到的结构体,它会进行格式的封
装与解封装,它的数据部分由底层提供,底层使用了
AVIOContext
,这
个
AVIOContext
实际上就是为普通的
I/O
增加了一层
Buffer
缓冲区,再往
底层就是
URLContext
,也就是到达了协议层,协议层的具体实现有很
多,包括
rtmp
、
http
、
hls
、
file
等,这就是
libavformat
的内部封装了
AVCodec
对于开发者来说,这一层我们能接触到的最顶层的结构体就是
AVCodecContext
,该结构体包含的就是与实际的编解码有关的部分。首
先,
AVCodecContext
是包含在一个
AVStream
里面的,即描述了这路流
的编码格式是什么,其中存放了具体的编码格式信息,根据
Codec
的信
息可以打开编码器或者解码器,然后利用该编码器或者解码器进行
AVPacket
与
AVFrame
之间的转换(实际上就是解码或者编码的过程),
这是
FFmpeg
中最重要的一部分。那么,接下来就来看一下在
API
中调用
了
FFmpeg
的一些方法之后,
FFmpeg
内部到底做了些什么呢?
API
1.av_register_all分析
还记得前面最开始编译
FFmpeg
的时候,做了一个
configure
的配置
吗?其中开启(
enable
)或者关闭(
disable
)了很多选项,当初可是留
了一句话,
configure
的配置会生成两个文件:
config.mk
与
config.h
。
config.mk
实际上就是
makefile
文件需要包含进去的子模块,会作用在编
译阶段,帮助开发者编译出正确的库;而
config.h
是作用在运行阶段,
这一阶段将确定需要注册哪些容器以及编解码格式到
FFmpeg
框架中。
所以该函数的内部实现会先调用
avcodec_register_all
来注册所有
config.h
里面开放的编解码器,然后会注册所有的
Muxer
和
Demuxer
(也就是封
装格式),最后注册所有的
Protocol
(即协议层的东西)。这样一来,
在
configure
过程中开启(
enable
)或者关闭(
disable
)的选项就就作用
到了运行时,该函数的源码分析涉及的源码文件包括:
url.c
、
allformats.c
、
mux.c
、
format.c
等文件。
2.av_find_codec
分析
这里面其实包含了两部分的内容:一部分是寻找解码器,一部分是
寻找编码器。其实在第一步的
avcodec_register_all
函数里面已经把编码
器和解码器都存放到一个链表中了,在这里寻找编码器或者解码器都是
从第一步构造的链表中进行遍历,通过
Codec
的
ID
或者
name
进行条件匹
配,最终返回对应的
Codec
。
3.avcodec_open2
分析
该函数是打开编解码器(
Codec
)的函数,无论是编码过程还是解
码过程,都会用到该函数,该函数的输入参数有三个:第一个是
AVCodecContext
,解码过程由
FFmpeg
引擎填充,编码过程由开发者自
己构造,如果想要传入私有参数,则为它的
priv_data
设置参数,比如在
libx264
编码器中设置
preset
、
tune
、
profile
等;第二个参数是上一步通过
av_find_codec
寻找出来的编解码器(
Codec
);第三个参数一般会传递
NULL
。具体到该函数的实现时,就会找到对应的实现文件,那么其是
如何找到对应的实现文件的呢?这就需要回到第一步中来看看其是如何
注册的,比如
libx264
的编码器,查看其注册会发现
ff_libx264_encoder
结
构体的定义存在于
libx264.c
中,所以该
Codec
的生命周期方法就会委托
给该结构体对应的函数指针所指向的函数,
open
对应的就是
init
函数指
针所指向的函数,该函数里面就会调用具体的编码库的
API
,比如
libx264
这个
Codec
会调用
libx264
的编码库的
API
,而
LAME
这个
Codec
会
调用
LAME
的编码库的
API
,并且会以对应的
AVCodecContext
中的
priv_data
来填充对应第三方库所需要的私有参数,如果开发者没有对属
性
priv_data
填充值,那么就使用默认值。
4.avcodec_close
分析
如果理解了
avcodec_open
,那么对应的
close
就是一个逆过程,找到
对应的实现文件中的
close
函数指针所指向的函数,然后该函数会调用对
应第三方库的
API
来关闭掉对应的编码库。其实
FFmpeg
所做的事情就是
透明化所有的编解码库,用自己的封装来为开发者提供统一的接口。开
发者使用不同的编码库时,只需要指明要使用哪一个即可,这也充分体
现了面向对象编程中的封装特性,关于
FFmpeg
面向对象的特性后续还
会进一步讨论。
调用FFmpeg编码时用到的函数分析
1.avformat_alloc_output_context2
分析
该函数内部需要调用方法
avformat_alloc_context
来分配一个
AVFormatContext
结构体,当然最关键的还是根据上一步注册的
Muxer
和
Demuxer
部分(也就是封装格式部分)去找到对应的格式。有可能是
flv
格式、
MP4
格式、
mov
格式,甚至是
MP3
格式等,如果找不到对应的格
式(即在
configure
选项中没有打开这个格式的开关),那么这里会返回
找不到对应的格式的错误提示。在调用
API
的时候,可以使用
av_err2str
把返回整数类型的错误代码转换为肉眼可读的字符串,这在调试的时候
是一个比较有用的工具函数。该函数最终会将找出来的格式赋值给
AVFormatContext
类型的
oformat
。
2.avio_open2
分析
首先调用函数
ffurl_open
,构造出
URLContext
结构体,这个结构体
中包含了
URLProtocol
(需要去第一步
register_protocol
中已经注册的协
议链表中寻找);接着会调用
avio_alloc_context
方法,分配出
AVIOContext
结构体,并将上一步构造出来的
URLProtocol
传递进来;然
后把上一步分配出来
AVIOContext
结构体赋值给
AVFormatContext
的属
性,其实这就是图
3-2
表示的结构了。而该过程恰好是上面所分析的
avformat_open_input
函数的实现过程的一个逆过程。之前就提到过,编
码过程和解码过程从逻辑上来讲本来就是一个逆过程,所以在
FFmpeg
的实现过程中它们也是一个逆过程。
后面的步骤也都是解码的一个逆过程,解码过程中的
av_find_stream_info
对应到这里就是
avformat_new_stream
和
avformat_write_header
。
avformat_new_stream
函数会将音频流或者视频
流的信息填充好,分配出
AVStream
结构体,在音频流中分配声道、采
样率、表示格式、编码器等信息,在视频流中分配宽、高、帧率、表示
格式、编码器等信息;
avformat_write_header
函数与解码过程中的
read_header
恰好是一个逆过程,因此这里将不再介绍。接下来就是编码
的阶段了,开发者需要将手动封装好的
AVFrame
结构体,作为
avcodec_encode_video
方法的输入,将其编码成为
AVPacket
,然后调用
av_write_frame
方法输出到媒体文件中。而
av_write_frame
方法会将编码
后的
AVPacket
结构体作为
Muxer
中的
write_packet
生命周期方法的输入,
write_packet
函数会加上自己封装格式的头信息,然后调用协议层写到本
地文件或者网络服务器上。最后一步就是
av_write_trailer
,该函数有一
个非常大的坑,如果没有执行
write_header
操作,就直接执行
write_trailer
操作,程序会直接崩溃(即
Crash
掉),所以必须保证这两个函数成对
出现。
write_trailer
函数的实现会把没有输出的
AVPacket
全部丢给协议层
去做输出,然后会调用
Muxer
的
write_trailer
生命周期方法,对于不同的
格式写出的尾部也不尽相同,这里不再逐一介绍。
调用FFmpeg解码时用到的函数分析
1.avformat_open_input
分析
函数
avformat_open_input
会根据所提供的文件路径判断文件的格
式,其实就是通过这一步来决定使用的到底是哪一个
Demuxer
。举例来
说,如果是
flv
,那么
Demuxer
就会使用对应的
ff_flv_demuxer
,所以对应
的关键生命周期的方法
read_header
、
read_packet
、
read_seek
、
read_close
都会使用该
flv
的
Demuxer
中函数指针指定的函数。
read_header
函数会将
AVStream
结构体构造好,以便后续的步骤继续使用
AVStream
作为输入
参数。
2.avformat_find_stream_info
分析
这个函数非常重要,后续章节中将要介绍的如何在直播场景下的拉
流客户端中
“
秒开首屏
”
,就是与该函数分析的代码实现息息相关的,该
方法的作用就是把所有
Stream
的
MetaData
信息填充好。方法内部会先查
找对应的解码器,然后打开对应的解码器,紧接着会利用
Demuxer
中的
read_packet
函数读取一段数据进行解码,当然解码的数据越多,分析出
的流信息就会越准确,如果是本地资源,那么很快就可以得到非常准确
的信息了,但是对于网络资源来说,则会比较慢,因此该函数有几个参
数可以控制读取数据的长度,一个是
probe size
,一个是
max_analyze_duration
,还有一个是
fps_probe_size
,这三个参数共同控制
解码数据的长度,当然,如果配置这几个参数的值越小,那么这个函数
执行的时间就会越快,但是会导致
AVStream
结构体里面一些信息(视
频的宽、高、
fps
、编码类型等)不准确。
3.av_read_frame
分析
使用该方法读取出来的数据是
AVPacket
,在
FFmpeg
的早期版本中
开放给开发者的函数其实就是
av_read_packet
,但是需要开发者自己来
处理
AVPacket
中的数据不能被解码器完全处理完的情况,即需要把未处
理完的压缩数据缓存起来的问题。所以到了新版本的
FFmpeg
中,其提
供了该函数,用于处理此状况。该函数的实现首先会委托到
Demuxer
的
read_packet
方法中去,当然
read_packet
通过解复用层和协议层的处理之
后,会将数据返回到这里,在该函数中进行数据缓冲处理。前面曾说
过,对于音频流,一个
AVPacket
可能包含多个
AVFrame
,但是对于视频
流,一个
AVPacket
只包含一个
AVFrame
,该函数最终只会返回一个
AVPacket
结构体。
4.avcodec_decode
分析
该方法包含了两部分内容:一部分是解码视频,一部分是解码音
频。在上面的函数分析中,我们知道,解码是会委托给对应的解码器来
实施的,在打开解码器的时候就找到了对应解码器的实现,比如对于解
码
H264
来讲,会找到
ff_h264_decoder
,其中会有对应的生命周期函数的
实现,最重要的就是
init
、
decode
、
close
这三个方法,分别对应于打开
解码器、解码以及关闭解码器的操作,而解码过程就是调用
decode
方
法。
5.avformat_close_input
分析
该函数负责释放对应的资源,首先会调用对应的
Demuxer
中的生命
周期
read_close
方法,然后释放掉
AVFormatContext
,最后关闭文件或者
远程网络连接。