谈一谈音频播放器的实现
转载链接:
@微博
目前音频市场主流的 APP 都支持边下边播。我好奇它们是如何实现的,所以对其中的「得到」和「喜马拉雅」进行了逆向。发现「得到」的音频服务基于 ijkplayer
开源库封装了一个 DDAudioPlayer
类;而「喜马拉雅」的语音播放器是基于 TheAmazingAudioEngine
库封装的。这两个库的描述如下:
ijkplayer: Android/iOS video player based on FFmpeg n3.4, with MediaCodec, VideoToolbox support.
TheAmazingAudioEngine: Core Audio, Cordially: A sophisticated framework for iOS audio applications, built so you don't have to.
设计实现一款音频播放器并没有那么简单,所以这两款 APP 都没有自己从头实现。不过在做音频播放的时候,如果选择一个坑比较多的第三方库,无疑会给项目带来很高的风险和维护成本。所以对于一个项目,建议还是不要使用第三方库实现主要功能。那么,如果是自己从头实现一个音频播放器,究竟应该如何设计呢?
本文主要通过开源库 FreeStreamer
来学习如何设计一个音频播放器,以及它的架构及设计方案。
FreeStreamer的两层结构
FreeStreamer
从整体结构上来讲,可以分为两层:基于 C++ 的底层,用以实现播放器核心的功能,包括数据获取、缓存、播放等;基于底层的 Objective-C 封装,支持单播,多播,预先加载下一个等功能,让我们能够更加方便的使用。
下面我们来看看这两层的具体实现。
基于C++的底层
FreeStreamer
的底层是用 C++ 实现的,它基于 AudioQueue
来实现边下边播,支持音频的在线播放和缓冲,性能消耗比较小,可定制性比较高。它的整体架构如下:
audio_stream
从上面的整体架构来说,音频播放服务的关键点在于 audio_stream
,可以说它是整个框架的灵魂,所有的类最终都为 audio_stream
服务。audio_stream
类似于一个管理器,用来统筹播放和获取数据,以及缓存数据。它通过 file_stream
从本地获取音频流或者 http_stream
从网络获取音频流,然后将数据发送给 audio_queue
来播放。
audio_queue
audio_queue
主要负责播放音频,底层直接使用的是 AudioQueue
。市面上大多数音频播放器都是基于 AudioQueue
实现的,或者更底层的 AudioUnit
。另外有些应用是基于 AVPlayer
,并实现自己的边下边播功能。类似于唱吧的设计方案,可以看这篇文章读懂「 唱吧KTVHTTPCache 」设计思想。关于 AudioQueue
的更多内容将在后续的文章详细介绍。
input_stream
˚ 主要用来规范各个接口的设计,给 ˚ 播放的音频流可以通过不同的渠道获取:本地的缓存数据或者网络请求的数据。这样统一接口后,负责数据的提供者(file_stream
,http_stream
)实现 input_stream
提供的纯虚函数,而 audio_stream
持有一个 input_stream
实例即可。经过 audio_stream
处理后的音频数据,会交给 AudioQueue
进行播放。
input_stream
是一个抽象类,定义了一系列纯虚函数,主要用来组织一个继承层次结构,并由它提供一个公共的根。
// 音频的 content-type
virtual CFStringRef contentType() = 0;
virtual size_t contentLength() = 0;
// 打开一个音频流
virtual bool open() = 0;
virtual bool open(Input_Stream_Position& position) = 0;
virtual void close() = 0;
virtual void setScheduledInRunLoop(bool scheduledInRunLoop) = 0;
// 音频地址
virtual void setUrl(CFURLRef url) = 0;
Input_Stream_Delegate
主要用来回调音频流数据状态。
// 开始读取音频流
virtual void streamIsReadyRead() = 0;
// 获取到新的数据流
virtual void streamHasBytesAvailable(UInt8 *data, UInt32 numBytes) = 0;
// 读取音频流介绍
virtual void streamEndEncountered() = 0;
// 发生错误
virtual void streamErrorOccurred(CFStringRef errorDesc) = 0;
caching_stream
caching_stream
更像一个管理类,用来操作为 AudioQueue
提供音频流的源,也就是说它通过 URL 来利用 Input_Stream
读取音频流。它为 audio_stream
提供服务。它主要用来缓存音频流,当从网络中加载到数据后,会缓存到本地。缓存的 key 是由音频的 URL 生成的 hash 字符串。同时会保存一个 .metadata
的缓存文件,用来存储由 id3_parser
解析出来的音频额外信息,比如音频的名字、作者等信息。caching_stream
读取音频数据时,首先判断 m_metaDataUrl
和 m_fileUrl
是否本地都存在,如果存在的话,直接从 File_Stream
中读取数据;否则会从 http_stream
中读取数据。
它的原理如下:
利用构造函数
Caching_Stream(Input_Stream *target)
生成一个Caching_Stream
对象,比如:Caching_Stream *cache = new Caching_Stream(new HTTP_Stream())
;播放每个音频的时候,会设置
audio_stream
的音频的 URL,如果 URL 的 schema 为 file,将生成一个File_Stream
对象。否则会判断是否开启了缓存策略,如果开启将生成一个Caching_Stream
对象,否则生成一个HTTP_Stream
对象。
if (HTTP_Stream::canHandleUrl(url)) {
// schema 不是 file://
Stream_Configuration *config = Stream_Configuration::configuration();
if (config->cacheEnabled) {
Caching_Stream *cache = new Caching_Stream(new HTTP_Stream());
m_inputStream = cache;
} else {
m_inputStream = new HTTP_Stream();
}
} else if (File_Stream::canHandleUrl(url)) {
m_inputStream = new File_Stream();
}
- 当音频播放的时候,会调用 audio_stream 的 open() 方法,这时 Caching_Stream 会判断是读取本地音频还是网络音频;
bool Caching_Stream::open()
{
bool status;
if (CFURLResourceIsReachable(m_metaDataUrl, NULL) &&
CFURLResourceIsReachable(m_fileUrl, NULL)) {
status = m_fileStream->open();
} else {
status = m_target->open();
}
}
file_stream
file_stream
的作用是读取本地缓存好的音频数据,主要使用 CFReadStream
读取本地的音频流,并使用 ID3_Parser
解析音频的 metaData
(音频的基本信息,比如名称、作者等)。当读取到数据后,会通过代理 Input_Stream_Delegate
通知到 audio_stream
以播放音频。它继承自 Input_Stream
。所以理解这个类主要是掌握如何使用 CFReadStream
读取本地数据。在顶层(使用 OC)可以使用 NSInpuStream
。
每个 Caching_Stream
会有一个成员变量 file_stream
,用来读取本地音频数据。
file_stream
工作原理:
当有本地缓存时,通过它来读取本地的音频文件,通过
setUrl(CFURLRef url)
设置本地音频的路径;通过 URL 生成一个
CFReadStreamRef m_readStream
来读取音频流数据,读取到数据后通过Input_Stream_Delegate
传递给Caching_Stream
;实现
Input_Stream
所定义的虚函数,比如contentType()
等。
http_stream
http_stream
主要用来读取网络中的音频数据,继承自 Input_Stream
。获取音频流时,如果本地没有缓存过,就会发起 HTTP 请求以获取数据。主要使用 CFNetwork
进行网络请求。这里的请求主要通过 CFReadStreamCreateForHTTPRequest
生成一个 CFReadStream
,然后从网络中读取数据,读取到数据后回调到 audio_stream
供 AudioQueue
播放。这里的套路其实和 file_stream
的套路一样。
在 Stream_Configuration
中可以设置 predefinedHttpHeaderValues
和 predefinedHttpParamsValues
来定义每次 HTTP 请求的默认头和参数。
这里实现了 ICY 协议,它主要用来请求音频的基本信息。如果在HTTP header
中添加了 Icy-MetaData = "1"
,请求音频流的时候会拉取音频的基本信息。如果服务器实现了这种协议,那么音频的相关信息完全可以放到这里。当然我们是不需要这些信息的,这里可以优化。
Shoutcast - ICY
You first need to understand how shoutcast works. It’s very simple protocol for streaming audio content built using HTTP. It was at first called “I Can Yell” and you therefore see a lot of icy tags in the HTTP header; we will also refer to it as the ICY protocol.
[{’icy-notice’, "This stream requires Winamp .."},
{’icy-name’, "Virgin Radio ..."},
{’icy-genre’, "Adult Pop Rock"},
{’icy-url’, "http://www.virginradio.co.uk/"},
{’content-type’, "audio/mpeg"},
{’icy-pub’, "1"},
{’icy-metaint’ "8192"},
{’icy-br’, "128"}]
file_output
file_output
主要用来把 HTTP 请求回来的数据缓存到本地,使用 CFWriteStream
写入本地文件中。每个 Audio_Stream
会有一个成员变量 m_fileOutput
用来保存音频数据。
File_Output(CFURLRef fileURL)
构造函数,负责生成一个 m_writeStream
,用来实时写入音频流到文件中;
当有数据时使用 m_writeStream
写入文件,此时如果有特殊需求可以在这个地方对音频流作处理;
CFIndex File_Output::write(const UInt8 *buffer, CFIndex bufferLength)
{
return CFWriteStreamWrite(m_writeStream, buffer, bufferLength);
}
d3_parser
id3_parser
主要负责解析音频流中的多媒体信息,比如音频的名称、作者等信息。MP3 文件开头或结尾中包含音乐标题、专辑、归属等信息的数据块。
基于底层的封装
直接调用 audio_stream
对于 Objective-C 开发人员来说并不太友好,所以 FreeStreamer
在 audio_stream
的基础上又封装了一层:FSAudioController
。我们来看看这一层主要的类:
FSAudioController
当使用的时候,直接使用 FSAudioController
播放音频,它支持单播、多播、调整音量大小、速率、快进、快退、预先加载下一个等功能。而它的本质就是管理多个 FSAudioStream
。
FSAudioStream
给予 audio_stream
的进一步封装,供 FSAudioController
使用,每个音频需要对应一个 FSAudioStream
。
FSCacheObject
是音频的缓存对象,负责对播放过的音频做缓存处理。如果开启缓存策略,那么每次 FSAudioStream
释放的时候会检查缓存的音频是否已经超出缓存设定的大小,如果超出限制,将按照先进先出的顺序来清除缓存。这里比较耗费性能,而且在主线程,需要优化。
FSCheckContentTypeRequest
请求音频的 contentType
,相当于每次播放音频时需要向服务器发送一个 HEAD 请求,确认是否为音频格式,当然即使 contentType
不正确,也会试图播放音频。
FSPlaylistItem
播放音频的 Model
,提供音频数据。
总结
以上就是整个 FreeStreamer
的设计,从设计上来看,有以下优点:
有一种面向协议的感觉,比如设计 Model 的时候,我更喜欢使用协议而不是继承;
耦合度底,最底层用 C++ 写的,对上层做一个很好的隔离;
通过阅读 FreeStreamer
的设计,我们可以了解设计一款在线播放器所需要到的技术点,以及架构。回到本文的中心思想,设计一款播放器其实关键点在于:
掌握
AudioQueue
播放音频的套路,当获取到音频流处理后交给AudioQueue
播放;如果支持缓存策略,播放音频前需要判断本地是否已经缓存过音频,如果有缓存直接读取本地音频流;
读取服务端的音频流是基于 HTTP 来读取音频流;
不管从哪里读取音频流,它的回调接口保持一致,比如
HTTP_steam
和File_stream
获取到数据后都会通过streamHasBytesAvailable(UInt8 *data, UInt32 numBytes)
来告诉Cache_stream
。
参考
Shoutcast - ICY https://people.kth.se/~johanmon/dse/casty.pdf