在iOS上,播放音頻一般使用AVAudioPlayer進(jìn)行音頻播放,但是AVAudioPlayer并不支持流媒體播放,換言之,AVAudioPlayer只能播放本地音頻,當(dāng)遇到網(wǎng)絡(luò)音頻的時(shí)候,都是先下載到整個(gè)音頻文件,然后再播放(微信語音就是先下載再播放),但是有些使用場(chǎng)景要求音頻使用流媒體播放,提高用戶體驗(yàn),像音樂軟件,在線音頻教育軟件的一些音頻課程都要求流媒體播放。
項(xiàng)目git地址
1 開發(fā)初衷
目前開源的流媒體播放器有很多,例如AudioStreamer、DOUAudioStreamer,但是這兩個(gè)開源庫(kù)或多或少都有點(diǎn)瑕疵,并且都是在很久以前開發(fā)的,并不滿足我的要求,所以決定自己開發(fā)一個(gè)流媒體播放器,并回饋一下開源社區(qū)。
2 KVAudioStreamer介紹
KVAudioStreamer采用AudioToolBox框架開發(fā),使用C接口開發(fā)將更容易自主定制,當(dāng)然,相對(duì)的也增加了開發(fā)難度。KVAudioStreamer內(nèi)部代碼結(jié)構(gòu)清晰,由于開源時(shí)間晚于AudioStreamer和DOUAudioStreamer,所以使用的API都是最新的。
2.1 AudioQueue介紹
我使用的是AudioQueue進(jìn)行音頻播放,AudioToolBox播放音頻主要涉及到以下API(僅僅列出,連參數(shù)都沒有放上去):
//AudioFileStreamID,用于音頻數(shù)據(jù)解析
extern OSStatus AudioFileStreamOpen (); //打開文件流,獲取文件流id
extern OSStatus AudioFileStreamParseBytes(); //解析數(shù)據(jù),將會(huì)傳入一個(gè)C語言方法,獲取解析結(jié)果
extern OSStatus AudioFileStreamClose(); //關(guān)閉文件流
extern OSStatus AudioFileStreamGetProperty(); //獲取文件信息
//AudioQueueBufferRef,用于音頻緩存數(shù)據(jù)存儲(chǔ)
extern OSStatus AudioQueueEnqueueBuffer(); //放進(jìn)音頻隊(duì)列
extern OSStatus AudioQueueFreeBuffer(); //釋放緩存區(qū)數(shù)據(jù)
//AudioQueueRef,用于音頻播放
extern OSStatus AudioQueueStart(); //開始播放
extern OSStatus AudioQueuePause(); //暫停播放
extern OSStatus AudioQueueStop(); //停止播放
extern OSStatus AudioQueueDispose(); //釋放音頻隊(duì)列
//以下兩個(gè)方法配合使用,來控制播放速率
extern OSStatus AudioQueueSetProperty(); //設(shè)置屬性
extern OSStatus AudioQueueSetParameter(); //設(shè)置參數(shù)
AudioQueue的播放流程如下所示:
從上圖可以看出,AudioQueue播放音頻是一種生產(chǎn)者-消費(fèi)者模式,所以KVAudioStreamer也采用生產(chǎn)者-消費(fèi)者的設(shè)計(jì)模式進(jìn)行框架搭建,內(nèi)部代碼邏輯清晰,充分解耦,方便開發(fā)者學(xué)習(xí)以及修改(這一點(diǎn)自認(rèn)為優(yōu)于AudioStreamer和DOUAudioStreamer)。
下圖是KVAudioStreamer的代碼結(jié)構(gòu),我已經(jīng)將實(shí)現(xiàn)代碼抽象成生產(chǎn)者和消費(fèi)者:
本篇文章僅為KVAudioStreamer的介紹文檔,關(guān)于AudioToolBox的使用問題將會(huì)在往后另一篇文章進(jìn)行說明,造輪子的過程是痛并快樂著的,踩過無數(shù)的坑,在填坑的過程中也在不斷成長(zhǎng),文章最后將會(huì)貼出當(dāng)時(shí)學(xué)習(xí)AudioToolBox的參考文章,同時(shí)也感謝這些作者的付出。
2.2 功能介紹
KVAudioStreamer擁有以下功能:
- 支持多種音頻格式(mp3、flac、wav、m4a...);
- 支持緩存功能;
- 支持定點(diǎn)播放;
- 多倍率播放。
KVAudioStreamer支持多種音頻格式,經(jīng)測(cè)試,目前音頻格式中僅ape格式文件無法播放,另外對(duì)m4a音頻文件只能做到流播放,無法使用seek操作,后續(xù)將會(huì)研究如何解決,如果開發(fā)者不需要播放m4a文件,那么KVAudioStreamer會(huì)是一個(gè)不錯(cuò)的選擇。
支持緩存功能,針對(duì)網(wǎng)絡(luò)文件,在完整緩存完畢將會(huì)通過代理事件通知開發(fā)者緩存成功,攜帶文件路徑供開發(fā)者下一步操作(注:僅在完整緩存后才會(huì)自動(dòng)緩存,如果播放網(wǎng)絡(luò)文件時(shí)還未緩存成功就使用了seek操作,那么就不算完整緩存,因?yàn)閮?nèi)部使用了斷點(diǎn)下載,如果seek后便無法保證文件的完整性,如果文件已經(jīng)完整緩存成功,重復(fù)seek不會(huì)產(chǎn)生重復(fù)的網(wǎng)絡(luò)請(qǐng)求,幫助用戶節(jié)省流量)。
定點(diǎn)播放也是KVAudioStreamer的一大特色,支持從音頻的某個(gè)位置開始播放,用于播放位置記憶功能。
多倍率播放,這也是音頻播放的一個(gè)常用功能,建議區(qū)間(0,5),其實(shí)2倍速度播放,出來的聲音就已經(jīng)很鬼畜了。
3 如何集成
該項(xiàng)目已提交到github開源社區(qū),并且提供cocoapod功能,可以直接通過git clone進(jìn)行項(xiàng)目下載,里面包含一個(gè)完整的demo演示,demo里面同時(shí)提供了音頻后臺(tái)播放和鎖屏控制的解決方案。
3.1 git地址
3.2 cocoapod集成
使用以下pod命令集成
pod 'KVAudioStreamer', ' 1.0.0'
4 如何使用
KVAudioStreamer的API設(shè)計(jì)遵從命名規(guī)范,堅(jiān)持一切從簡(jiǎn)的設(shè)計(jì)原則,所以使用簡(jiǎn)單,上手快速。
4.1 初始化
self.streamer = [[KVAudioStreamer alloc] init];
self.streamer.delegate = self;
self.streamer.cacheEnable = YES; //開啟緩存功能
//設(shè)置httpheader,音樂資源在阿里云OSS開啟了防盜鏈,需要在這里設(shè)置referer,如果沒有防盜鏈,那么不需要設(shè)置
self.streamer.httpHeaders = @{@"Referer" : @"kevinrefer"};
4.2 設(shè)置音頻路徑
[self.streamer resetAudioURL:self.filepath]; //音頻路徑需遵從以下規(guī)則
KVAudioStreamer通過音頻路徑來進(jìn)行本地以及網(wǎng)絡(luò)文件的區(qū)分,所以務(wù)必遵從該規(guī)則:如果是本地文件,需以file://
開頭,網(wǎng)絡(luò)文件需以http
開頭,如果音頻資源是https,開發(fā)者可以自行修改http請(qǐng)求文件中的代碼,KVAudioStreamer使用NSURLSession
作為網(wǎng)絡(luò)請(qǐng)求框架,處理網(wǎng)絡(luò)請(qǐng)求的代碼全部封裝在這里,無需改動(dòng)其他代碼:
4.3 播放控制
- 播放
[self.streamer play];
- 定點(diǎn)播放
[self.streamer playAtTime:60];
- 暫停
[self.streamer pause];
- seek
[self.streamer seekToTime:60];
- 停止
[self.streamer stop];
- 設(shè)置音量
self.streamer.volume = 0.5;
- 設(shè)置倍速
self.streamer.playRate = 0.5;
4.4 代理通知
KVAudioStreamer使用代理事件進(jìn)行事件通知,總共有六個(gè)代理方法。
- 播放狀態(tài)改變通知,將會(huì)在這個(gè)代理方法里面接收到流媒體播放過程的各種狀態(tài)變化。
- (void)audioStreamer:(KVAudioStreamer*)streamer playStatusChange:(KVAudioStreamerPlayStatus)status;
所有的狀態(tài),如下所示:
typedef NS_ENUM(NSInteger, KVAudioStreamerPlayStatus) {
KVAudioStreamerPlayStatusIdle, //閑置狀態(tài)
KVAudioStreamerPlayStatusBuffering, //緩沖中
KVAudioStreamerPlayStatusPlaying, //播放
KVAudioStreamerPlayStatusPause, //暫停
KVAudioStreamerPlayStatusFinish, //完成播放
KVAudioStreamerPlayStatusStop //停止
};
- 音頻時(shí)長(zhǎng)改變通知,KVAudioStreamer內(nèi)部計(jì)算時(shí)長(zhǎng)使用了三種方法,只有一種能夠拿到確切的時(shí)長(zhǎng),如果獲取不到將會(huì)使用另外兩種方法進(jìn)行計(jì)算,得出的為近似的音頻時(shí)長(zhǎng)。
- (void)audioStreamer:(KVAudioStreamer *)streamer durationChange:(float)duration;
近似時(shí)長(zhǎng)通知,注意:該方法有可能調(diào)用多次。
- (void)audioStreamer:(KVAudioStreamer *)streamer estimateDurationChange:(float)estimateDuration;
- 播放進(jìn)度通知,內(nèi)部使用定時(shí)器監(jiān)聽播放進(jìn)度。
- (void)audioStreamer:(KVAudioStreamer *)streamer playAtTime:(long)location;
- 緩存完成通知,如果開啟了緩存功能,并且文件完整緩存成功,將會(huì)回調(diào)這個(gè)方法,返回YES,將會(huì)刪除該緩存文件。
- (BOOL)audioStreamer:(KVAudioStreamer *)streamer cacheCompleteWithRelativePath:(NSString*)relativePath cachepath:(NSString*)cachepath;
- 錯(cuò)誤通知,內(nèi)部報(bào)錯(cuò)將會(huì)回調(diào)該方法。
- (void)audioStreamer:(KVAudioStreamer *)streamer didFailWithErrorType:(KVAudioStreamerErrorType)errorType msg:(NSString*)msg error:(NSError*)error
4.5 使用注意事項(xiàng)
由于KVAudioStreamer使用了定時(shí)器進(jìn)行播放時(shí)長(zhǎng)監(jiān)聽,所以在適當(dāng)(不使用)的時(shí)候手動(dòng)釋放流媒體播放器。
- (void)dealloc {
[self.streamer releaseStreamer]; //釋放流媒體
self.streamer = nil;
}
5 寫在最后
造輪子的確很辛苦,過程中遇到了很多問題,撓破頭皮才一一解決,不過最后還是沒能解決m4a文件的播放seek問題,等待以后有空閑時(shí)間再慢慢研究。
KVAudioStreamer使用到的核心技術(shù):
- AudioToolBox框架
- GCD串行隊(duì)列,音頻數(shù)據(jù)解析都在串行隊(duì)列中順序執(zhí)行
- 線程鎖(pthread_mutex_t),用于解決多線程資源共享問題
- 線程條件變量(pthread_cond_t),由于音頻數(shù)據(jù)的解析后是在子線程連續(xù)填充緩存區(qū)的,在AudioQueue還未播放完成時(shí)緩存區(qū)是無法使用的,線程就必須等待,所以使用了條件變量進(jìn)行線程的等待以及喚醒,避免過多的CPU資源占用
以下文章為本人在學(xué)習(xí)AudioToolBox時(shí)的參考文章,當(dāng)然,里面或多或少有些坑,再次感謝這些作者的付出,往后有時(shí)間將會(huì)寫一篇文章完整講解AudioToolBox的使用。
http://www.cocoachina.com/ios/20170721/19969.html
http://www.lxweimin.com/p/05b6e9bc4060
http://blog.csdn.net/cairo123/article/details/53839980