音频

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 ,最后关闭文件或者
远程网络连接。
 
 
 
 
 
 
 
 
 
 
 
 
 
 

相关文章: