使用AVPlayer+AFNetworking封装一个带有缓存逻辑的音频播放器
音頻播放一直是一個最常用的功能,不管是否是以該功能為主業務的app,音頻播放都可以作為一個模塊存在于其中。
一般來說,在普通的業務需求中,很少會遇到直接播放本地資源文件的情況,基本都是給一個資源鏈接再播放。
那么,在一個以音頻播放為核心功能的app中,相關延伸的功能也就必須實現:
- 邊緩沖邊播放。
- 播放進度控制。
- 緩存機制(即播放過一遍的鏈接再次播放時無需再次請求資源)
- 后臺播放以及遠程控制響應(從鎖屏和控制中心進行的播放控制)
那么為何使用AVPlayer作為播放框架,而不使用市面上其他的第三方封裝的流媒體播放框架(AudloStreamer, freeStreamer等)呢?這里說明一下,我個人在工作中遇到的坑。
AudloStreamer是一款歷史久遠的流媒體播放框架,基于蘋果的AudioToolbox封裝,功能強大,不僅實現了播放,緩沖,緩存等必備機制,還可提供一些音頻數據處理的相關功能,可謂是十分強大。而在我負責的項目中原本使用的也是該框架。此框架作為核心音頻播放框架使用,一直表現穩定,并無任何bug出現。但是,iOS13的出現,徹底動搖了此框架的地位。
眾所周知,iOS13出現的初期,可謂是烽煙四起,各大app頻頻出現被無故殺后臺的現象,但知道真相的用戶畢竟是少數,多數用戶會把現象歸結于程序本身,認為是程序自身出了bug,才導致的后臺無故退出。當然蘋果后來也對這種瘋狂殺后臺的機制做了調整(估計是收到了大量開發者的抗議),但某些機制并未作出調整。
比如,我負責的這個項目也出現了崩潰率暴漲的現象(因iOS殺后臺是被系統級的watchDog強行結束進程,因此會被崩潰監聽的框架認為是異常退出),而用戶并不知道退出的真正原因,只從現象上進行反饋,比如:“為什么我播著播著就停了?”,“為什么從后臺打開就重啟了?”,“為什么后臺播放停止了我在鎖屏上點什么都沒反應?”等等等等。這一系列反饋的根本原因都是因為我這個項目在后臺播放時,莫名其妙的被系統看門狗干掉了。
在這個現象出現的初期,我實在一頭霧水,因為崩潰的線程很奇怪,那是一個網絡請求使用的線程,而這個網絡請求使用的框架更是一個遠古的框架,著名的ASIHTTPRequset。而在此之前,整個后臺播放的流程運行到此處時并未出現問題,而且崩潰位置被記錄到了框架代碼內部。由于該框架早已無人維護,因此也無法向框架作者尋求幫助,于是只能自己開始研究。
過程就不多說了,直接說結果。經過一系列的嘗試,最終問題鎖定在了播放框架上,也就是AudloStreamer。我們都知道,如果想讓你的app在后臺播放音頻,那必須要先啟動系統的音頻播放通道AVAudioSession。當然,這在之前肯定也是做了的,否則早就出問題了。但是,在iOS13中,當我這個項目退到后臺播放時,只要播放結束,下一個音頻開始播放之后程序就會被殺死。然而,我嘗試使用蘋果自己封裝的AVAudioPlayer循環播放一個本地音頻文件時,卻可以正常播放。
這也就解釋了為什么之前的崩潰會發生在網絡層。請求發出了,等待響應的時候app直接被殺死了,當然是網絡請求的進程會報錯。所以我得出結論,如果使用蘋果封裝的音頻播放框架,則程序可以正常存活在后臺,如果使用自己封裝的播放框架,則程序就不能正常在后臺存活。
這就是我選擇了AVPlayer的主要原因。一個以音頻播放為核心功能的app,如果不能正常在后臺持續播放那還玩個錘子。而且,AVPlayer的封裝度很高,使用方便,可提供一切播放器所需的數據支持(除了緩存文件)。
好了,說了這么多,也該上代碼了。
在使用之前,我們需要知道幾個關鍵類:
- AVPlayer(播放器類) :播放器主體,控制播放狀態,播放進度,以及播放源
- AVPlayerItem(播放源):可播放的資源,包含各種資源信息,以及資源的狀態
- CMTime(播放源中使用的時間相關結構體)? :AVPlayer中所有時間相關的屬性都是該結構體
- AVAudioSession(系統的音頻通道):這個都知道
- MPRemoteCommandCenter(遠程控制命令中心):這個理論上也都知道
在實際工作中,會因為業務需求不同,我們所用的數據結構也不同,但最終,我們都會用到的一定有資源鏈接。所以在demo中,我定義的也只有鏈接,其他屬性可根據自己需要再補充。
那么,我們從我們得到了資源鏈接之后開始。
有了資源鏈接,我們需要先創建播放源。
/// 創建播放源 /// @param url 鏈接 - (AVPlayerItem *)getAVPlayItemWithURL:(NSURL *)url{AVPlayerItem *playerItem = [[AVPlayerItem alloc] initWithURL:url];if (@available(iOS 10.0, *)) {playerItem.preferredForwardBufferDuration = 1;}return playerItem; }然后我們可以選擇播放器的創建思路,如果你只需要播放一次音頻,可直接用播放源創建播放器,當然也可直接用資源鏈接直接創建,但如果你需要監聽播放時的各種屬性,就需要播放源了。
AVPlayer *player = [AVPlayer playerWithPlayerItem:playerItem];如果你需要的是播放音頻列表,那懶加載創建一個播放器是更優選擇。然后使用創建的播放源替換當前播放源
/// 懶加載生成player - (AVPlayer *)player{if(!_player){_player = [[AVPlayer alloc] init];//添加音頻播放結束時的通知監聽[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_playEOF) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];}return _player; }/// 替換播放源 /// @param url 鏈接 - (void)resetPlayItemWithURL:(NSURL *)url{//移除正在當前播放源的監聽[self removeObserversFromPlayerItem];//創建新播放源AVPlayerItem *playerItem = [self getAVPlayItemWithURL:url];//替換播放源[self.player replaceCurrentItemWithPlayerItem:playerItem];if (@available(iOS 10.0, *)) {self.player.automaticallyWaitsToMinimizeStalling = NO;}//給新播放源加監聽[self addObserverToPlayerItem:playerItem]; }給當前播放源添加監聽事件,在添加之前,需移除上一個播放源的監聽。
/// 給播放源添加監聽 /// @param playerItem 播放源 - (void)addObserverToPlayerItem:(AVPlayerItem *)playerItem{//監聽字段@"status":播放源狀態[playerItem addObserver:selfforKeyPath:@"status"options:NSKeyValueObservingOptionNewcontext:nil];//監聽字段@"playbackBufferEmpty":緩沖區為空[playerItem addObserver:selfforKeyPath:@"playbackBufferEmpty"options:NSKeyValueObservingOptionNewcontext:nil];//監聽字段@"playbackLikelyToKeepUp":播放似乎可繼續進行[playerItem addObserver:selfforKeyPath:@"playbackLikelyToKeepUp"options:NSKeyValueObservingOptionNewcontext:nil];//監聽字段@"loadedTimeRanges":已加載的時間區間[playerItem addObserver:selfforKeyPath:@"loadedTimeRanges"options:NSKeyValueObservingOptionNewcontext:nil]; }/// 移除播放源監聽 - (void)removeObserversFromPlayerItem {if (self.player.currentItem){[self.player.currentItem removeObserver:self forKeyPath:@"status"];[self.player.currentItem removeObserver:self forKeyPath:@"playbackBufferEmpty"];[self.player.currentItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];[self.player.currentItem removeObserver:self forKeyPath:@"loadedTimeRanges"];} }完成這些之后,其實已經可以播放音頻了,其實如果只想播放,直接用鏈接創建播放器,然后play就可以了,所以說AVPlayer的封裝度真的很高。
然后,我們需要實現監聽方法。
#pragma mark- -KVO 監聽實現- (void)observeValueForKeyPath:(NSString *)keyPathofObject:(id)objectchange:(NSDictionary *)changecontext:(void *)context {if (object == [self.player currentItem]) {//播放源狀態if ([keyPath isEqualToString:@"status"]) {AVPlayerStatus status = [[change objectForKey:NSKeyValueChangeNewKey] integerValue];switch (status) {case AVPlayerStatusUnknown:{//未知}break;case AVPlayerStatusReadyToPlay:{[self.player play];}break;//失敗時,停止case AVPlayerStatusFailed:{NSLog(@"播放失敗:%@",self.player.currentItem.error);[self.player stop];}break;default:break;}} else if ([keyPath isEqualToString:@"playbackBufferEmpty"] && self.player.currentItem.playbackBufferEmpty) {//緩沖區為空時,播放狀態為加載中,并暫停播放器[self.player pause];} else if ([keyPath isEqualToString:@"playbackLikelyToKeepUp"]) {//播放源可持續播放,按需進行操作} else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {//當前緩沖區間發生變化時,讀取已緩沖的進度,并通知進度條更新UICMTime playerDuration = [[self.player currentItem] duration];//判斷當前播放源的整體播放區間是否合法if (!CMTIME_IS_INVALID(playerDuration)) {//獲取已緩存的時間NSTimeInterval *bufferTime = (NSTimeInterval)[self availableDuration];//發送通知給UI層更新緩沖進度。}}} }// 返回當前已緩存的時長 - (float)availableDuration {NSArray *loadedTimeRanges = [[self.player currentItem] loadedTimeRanges];if ([loadedTimeRanges count] > 0) {CMTimeRange timeRange = [[loadedTimeRanges objectAtIndex:0] CMTimeRangeValue];float startSeconds = CMTimeGetSeconds(timeRange.start);float durationSeconds = CMTimeGetSeconds(timeRange.duration);return (startSeconds + durationSeconds);} else {return 0.0f;} }還有一些播放器需要的屬性需要單獨獲取,并通過通知的方式通知UI層刷新UI,具體可通過demo查看
到此,一個可以邊緩沖邊播放的播放器就完成了。
如果還想實現緩存機制,那還需要做如下操作。
先說明一點,其實有很多第三方庫可以進行邊緩存邊播放,并且將player本身的緩沖進度替換為緩存進度,實現的核心思想是以AVURLAsset創建播放源,設置它的resourceLoader的delegate,然后通過代練方法重寫AVPlayer自己封裝的緩沖機制。方法固然好,我也進行了嘗試,但播放源總是報錯,因為我這個項目使用的鏈接并非正常的鏈接,通過AVAssetResourceLoaderDelegate的代理方法返回的鏈接均不能再次使用,因此,這個辦法被廢棄。
之后,我便又生一計,既然AVPlayer可以自己邊緩沖邊播放,那我只需要單獨實現一個緩存機制,然后在播放前判斷本地是否有緩存文件不就得了嗎。于是,AFNetworking便派上用場。
在設置播放鏈接之前,我先對本地是否有完整的緩存文件進行判斷,如沒有或文件不完整,則一遍開始播放,一遍去下載文件進行緩存。下載方式如下:
//緩存音頻原始數據 - (void)requestRawAudioDataWithURL:(NSURL *)url {//先取消上一次的下載任務if (_cacheDownloadTask && _cacheDownloadTask.state == NSURLSessionTaskStateRunning) {[_cacheDownloadTask cancel];_cacheDownloadTask = nil;}//創建任務_cacheDownloadTask = [self.cacheDownloadManager downloadTaskWithRequest:[NSURLRequest requestWithURL:url] progress:^(NSProgress * _Nonnull downloadProgress) {//下載進度,可按需求對UI層進行刷新NSLog(@"下載進度:%.0f%", downloadProgress.fractionCompleted * 100);} destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) {//保存完整文件的長度if (response.expectedContentLength > 0) {//TODO: 需要自己創建本地緩存,記錄對應音頻文件的總長度,已用于判斷文件是否完整。}NSURL *path = //TODO: 需要自己定義本地存儲路徑return path;} completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {NSLog(@"下載完成");}];[_cacheDownloadTask resume]; }對應下載路徑的操作,具體可以參考demo。
緩存之前先取消上次的緩存是為了節約內存資源,否則一旦用戶快速切歌,就會導致所有的音頻都要進行緩存。
當然,有緩存,就得有清空緩存,否則你的app會越來越大,最終被用戶放棄。
先這樣吧。
總結
以上是生活随笔為你收集整理的使用AVPlayer+AFNetworking封装一个带有缓存逻辑的音频播放器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 运筹学 知识点总结(三)
- 下一篇: 计算机硬件毕业论文题目,最新计算机硬件论