转载

谈一谈音频播放器的实现

谈一谈音频播放器的实现

阅读本文需要一些 C++ 的知识,如果不了解 C++ 语法,可以查看 @Lefe_x 的微博,《iOS 开发者应该掌握些 C++ 知识》。

目前音频市场主流的 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

input_stream 主要用来规范各个接口的设计,给 AudioQueue 播放的音频流可以通过不同的渠道获取:本地的缓存数据或者网络请求的数据。这样统一接口后,负责数据的提供者(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);
}

id3_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

正文到此结束
Loading...