一、前言
這兩天做了個小專案涉及到了遠端音樂播放,因為第一次做這種音樂專案,邊查資料邊做,其中涉及到主要技術點有:
- 如何播放遠端網路音樂
- 如何切換當前正在播放中的音樂資源
- 如何監聽音樂播放的各種狀態(播放器狀態、播放的進度、緩衝的進度,播放完成)
- 如何手動操控播放進度
- 如何在後臺模式或者鎖屏情況下正常播放音樂
- 如何在鎖屏模式下顯示音樂播放資訊和遠端操控音樂
如果您對一塊技術點有興趣或者正在尋找相關資料,那麼本篇或許能提供一些參考或啟發。
二、 網路音樂播放的核心技術點
根據自己的經驗和查了一些音樂播放的相關資料,最簡單和最易上手的的技術方案我想應該是採用ios系統自帶的AVFoundation框架。
我們知道AVFoundation框架是蘋果專門為多媒體打造的一個庫,這個庫非常強大,專門用來處理音視訊等複雜的多媒體技術,而本篇要講的所有技術點就是基於AVFoundation框架中的一個類——AVPlayer。
那麼AVPlayer是什麼?
你可以把他看成是一個已經封裝好的播放器,它的作用是用來播放遠端的或本地的視訊和音訊。因為本地的音視訊的播放比較簡單,這裡就不做講述,本編主要是講遠端音樂播放,因為都是基於AVPlayer同一套API,所以掌握遠端音樂播放其實就是相當於掌握遠端視訊播放。好了廢話就不多說了,下面開始上菜。
1、匯入AVFoundation框架,建立AVPlayer播放器
1 2 3 4 5 6 7 8 9 10 11 |
-(AVPlayer *)player { if (_player == nil) { // AVPlayerItem是一個包裝音樂資源的類,初始化時可以傳入一個音樂的url AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:@"http://xxxxxxxx"]]; //通過AVPlayerItem初始化player _player = [[AVPlayer alloc] initWithPlayerItem:item]; } return _player; } |
此處懶載入建立,讓播放器成為控制器的全域性屬性,注意需要強引用,否則回收釋放掉了就無法播放。
2、播放或停止音樂
1 2 3 4 |
//開始播放 [self.player play]; //停止播放 [self.player pause]; |
這個沒什麼好講的,只要呼叫AVPlayer的兩個例項方法
3、切換當前正在播放中的音樂資源
1 2 3 4 |
//建立需要播放的AVPlayerItem AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:model.url]]; //替換當前音樂資源 [self.player replaceCurrentItemWithPlayerItem:item]; |
這個可以用於歌曲的切換,如上一首、下一首。
4、通過KVO監聽播放器的狀態
1 |
[self.player.currentItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil]; |
拿到播放器的currentItem
,註冊當前物件為觀察者,監聽它的status
屬性。status
屬性是AVPlayerItemStatus
型別,它是一個列舉型別,如下:
1 2 3 4 5 |
typedef NS_ENUM(NSInteger, AVPlayerItemStatus) { AVPlayerItemStatusUnknown,//未知狀態 AVPlayerItemStatusReadyToPlay,//準備播放 AVPlayerItemStatusFailed//載入失敗 }; |
當status屬性值發生改變時,就會觸發觀察者方法的回撥,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
//觀察者回撥 -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { //注意這裡檢視的是self.player.status屬性 if ([keyPath isEqualToString:@"status"]) { switch (self.player.status) { case AVPlayerStatusUnknown: { NSLog(@"未知轉態"); } break; case AVPlayerStatusReadyToPlay: { NSLog(@"準備播放"); } break; case AVPlayerStatusFailed: { NSLog(@"載入失敗"); } break; default: break; } } } |
當 self.player.status == AVPlayerStatusReadyToPlay
時,音樂就會開始正常播放,另外兩種狀態音樂是無法播放的,可以在上面方法相應狀態裡給出提示。這裡需要特別強調一點的是觀察者監聽的物件是self.player.currentItem
,而不是self.player
,而當監聽的屬性發生改變時,觀察者回撥的方法裡需要檢視的是self.player.status
。當然,你也可以不這麼幹,但是我嘗試過好幾次,不這麼幹的後果是無法監聽到self.player.status
屬性的改變。
當音樂播放完成,或者切換下一首歌曲時,請務必記得移除觀察者,否則會crash。操作如下:
1 2 |
//移除觀察者 [self.player.currentItem removeObserver:self forKeyPath:@"status"]; |
5、監聽音樂的緩衝進度
這個也是通過KVO監聽播放器當前播放的音樂資源AVPlayerItem
的loadedTimeRanges
屬性。我們先看監聽,如下:
1 2 |
//KVO監聽音樂緩衝狀態 [self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil]; |
當loadedTimeRanges
屬性發生改變時,回撥如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"loadedTimeRanges"]) { NSArray * timeRanges = self.player.currentItem.loadedTimeRanges; //本次緩衝的時間範圍 CMTimeRange timeRange = [timeRanges.firstObject CMTimeRangeValue]; //緩衝總長度 NSTimeInterval totalLoadTime = CMTimeGetSeconds(timeRange.start) + CMTimeGetSeconds(timeRange.duration); //音樂的總時間 NSTimeInterval duration = CMTimeGetSeconds(self.player.currentItem.duration); //計算緩衝百分比例 NSTimeInterval scale = totalLoadTime/duration; //更新緩衝進度條 self.loadTimeProgress.progress = scale; } } |
loadedTimeRanges這個屬性是一個陣列,裡面裝的是本次緩衝的時間範圍,這個範圍是用一個結構體CMTimeRange
表示,當然在oc中結構體是不能直接存放陣列的,所以它被包裝成了oc物件NSValue
。
我們來看下這個結構體:
1 2 3 4 5 |
typedef struct { CMTime start; CMTime duration; } CMTimeRange; |
start表示本次緩衝時間的起點,duratin表示本次緩衝持續的時間範圍,具體詳細的計算方法可以看上面方法的實現。
當音樂播放完成,或者切換下一首歌曲時,請務必記得移除觀察者,否則會crash。操作如下:
1 |
[self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil]; |
6、監聽音樂播放的進度
這個不是通過KVO了,AVPlayer專門提供了下面這個api用來監聽播放的進度:
1 2 3 4 5 6 7 8 9 10 |
/** 監聽音樂播放進度 @param interval 監聽的時間間隔,用來設定多長時間回撥一次 @param queue 佇列,一般傳主佇列 @param block 回撥的block,會把當前的播放時間傳遞過來 @return 監聽的物件 */ - (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block; |
操作如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
__weak typeof(self) weakSelf = self; self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { //當前播放的時間 float current = CMTimeGetSeconds(time); //總時間 float total = CMTimeGetSeconds(item.duration); if (current) { float progress = current / total; //更新播放進度條 weakSelf.playSlider.value = progress; weakSelf.currentTime.text = [weakSelf timeFormatted:current]; } }]; |
我們可以這個block裡面拿到當前播放時間,根據總時間計算出當前播放所佔的時間比例,最後更新播放進度條。這裡又涉及到了一個資料類型別CMTime,它也是一個結構體,用來作為時間的格式,定義如下:
1 2 3 4 5 6 |
typedef struct CMTimeValue value; CMTimeScale timescale; CMTimeFlags flags; CMTimeEpoch epoch; } CMTime; |
CMTime是以分數的形式表示時間,value表示分子,timescale表示分母,flags是位掩碼,表示時間的指定狀態。所以我們要獲得時間的秒數需要分子除以分母。當然你還可以用下面這個函式來獲取時間的秒數:
1 |
Float64 CMTimeGetSeconds(CMTime time) |
最後,當音樂播放完成或者切換音樂時,依然需要移除監聽:
1 2 3 4 |
if (self.timeObserver) { [self.player removeTimeObserver:self.timeObserver]; self.timeObserver = nil; } |
7、手動超控(移動滑塊)播放進度
這是一個播放音視訊很常見的功能,所以強大的AVPlayer理所當然的提供了幾個api,下面只講述其中最簡單的一個:
1 2 3 4 5 6 |
/** 定位播放時間 @param time 指定的播放時間 */ - (void)seekToTime:(CMTime)time; |
具體使用如下:
1 2 3 4 5 6 7 8 |
//移動滑塊調整播放進度 - (IBAction)playSliderValueChange:(UISlider *)sender { //根據值計算時間 float time = sender.value * CMTimeGetSeconds(self.player.currentItem.duration); //跳轉到當前指定時間 [self.player seekToTime:CMTimeMake(time, 1)]; } |
8、監聽音樂播放完成
一般音視訊播放完成時我們或多或少的都要處理一些業務,比如迴圈播放,播完退出介面等等。下面看下如何監聽AVPlayer的播放完成。
1 2 |
//給AVPlayerItem新增播放完成通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:_player.currentItem]; |
這裡是採用註冊監聽AVPlayerItemDidPlayToEndTimeNotification
通知,當AVPlayer一播放完成時,便會發出這個通知,我們收到通知後進行處理即可
9、設定音樂後臺播放
我們知道執行在ios系統下的程式一旦進入後臺就會處於休眠狀態,程式停止執行了,也就播放不了什麼音樂了。但是有一些特定功能的app還是處於可以後臺執行的,比如音樂型別的app正處於這個範疇。但是,並不是說你在應用中播放音樂就能後臺高枕無憂的執行了,你依然需要做如下幾步操作:
(1)開啟後臺模式
target ->capabilities-> Background modes ->開啟開關 ->勾選第一個選項
(2)程式啟動時設定音訊會話
1 2 3 4 5 6 7 |
//一般在方法:application: didFinishLaunchingWithOptions:設定 //獲取音訊會話 AVAudioSession *session = [AVAudioSession sharedInstance]; //設定型別是播放。 [session setCategory:AVAudioSessionCategoryPlayback error:nil]; //啟用音訊會話。 [session setActive:YES error:nil]; |
以上兩步設定無誤,程式進入後臺模式,便可以進行音樂播放
10、如何設定音樂鎖頻資訊
我們看百度音樂鎖頻時,也依然能在螢幕上展示歌曲的資訊,以及切換歌曲等。下面看看這個功能是如何實現的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
//音樂鎖屏資訊展示 - (void)setupLockScreenInfo { // 1.獲取鎖屏中心 MPNowPlayingInfoCenter *playingInfoCenter = [MPNowPlayingInfoCenter defaultCenter]; //初始化一個存放音樂資訊的字典 NSMutableDictionary *playingInfoDict = [NSMutableDictionary dictionary]; // 2、設定歌曲名 if (self.currentModel.name) { [playingInfoDict setObject:self.currentModel.name forKey:MPMediaItemPropertyAlbumTitle]; } // 設定歌手名 if (self.currentModel.artist) { [playingInfoDict setObject:self.currentModel.artist forKey:MPMediaItemPropertyArtist]; } // 3設定封面的圖片 UIImage *image = [self getMusicImageWithMusicId:self.currentModel]; if (image) { MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:image]; [playingInfoDict setObject:artwork forKey:MPMediaItemPropertyArtwork]; } // 4設定歌曲的總時長 [playingInfoDict setObject:self.currentModel.detailDuration forKey:MPMediaItemPropertyPlaybackDuration]; //音樂資訊賦值給獲取鎖屏中心的nowPlayingInfo屬性 playingInfoCenter.nowPlayingInfo = playingInfoDict; // 5.開啟遠端互動,只有開啟這個才能進行遠端操控 [[UIApplication sharedApplication] beginReceivingRemoteControlEvents]; } |
這裡設定圖片時需要注意下,非同步載入網路圖片後再設定是無效的,所以圖片資訊最好是先請求下來後再進行設定。
遠端超控的回撥如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
//監聽遠端互動方法 - (void)remoteControlReceivedWithEvent:(UIEvent *)event { switch (event.subtype) { //播放 case UIEventSubtypeRemoteControlPlay:{ [self.player play]; } break; //停止 case UIEventSubtypeRemoteControlPause:{ [self.player pause]; } break; //下一首 case UIEventSubtypeRemoteControlNextTrack: [self nextBtnAction:nil]; break; //上一首 case UIEventSubtypeRemoteControlPreviousTrack: [self lastBtnAction:nil]; break; default: break; } } |
三、總結
最後,畫了一張圖總結下播放遠端網路音樂的流程:
根據QQ音樂的介面做了個小demo,下面是demo的真機前臺和後臺播放的執行效果:
附上github下載地址:https://github.com/yedexiong/MsuicPlayDemo.git
四、結束語
播放遠端網路音樂的核心技術點基本上已經寫完,當然AVPlayer還有很多強大的功能沒有寫出來,有興趣的可以進一步挖掘。寫到這裡已經疲倦至極,如果喜歡的可以點贊和關注,後續會持續更新一些精彩的技術點。