iOS播放遠(yuǎn)程網(wǎng)絡(luò)音樂的核心技術(shù)點

播放遠(yuǎn)程網(wǎng)絡(luò)音樂.jpg

一、前言

這兩天做了個小項目涉及到了遠(yuǎn)程音樂播放,因為第一次做這種音樂項目,邊查資料邊做,其中涉及到主要技術(shù)點有:

  • 如何播放遠(yuǎn)程網(wǎng)絡(luò)音樂
  • 如何切換當(dāng)前正在播放中的音樂資源
  • 如何監(jiān)聽音樂播放的各種狀態(tài)(播放器狀態(tài)、播放的進度、緩沖的進度,播放完成)
  • 如何手動操控播放進度
  • 如何在后臺模式或者鎖屏情況下正常播放音樂
  • 如何在鎖屏模式下顯示音樂播放信息和遠(yuǎn)程操控音樂

如果您對一塊技術(shù)點有興趣或者正在尋找相關(guān)資料,那么本篇或許能提供一些參考或啟發(fā)。

二、 網(wǎng)絡(luò)音樂播放的核心技術(shù)點

根據(jù)自己的經(jīng)驗和查了一些音樂播放的相關(guān)資料,最簡單和最易上手的的技術(shù)方案我想應(yīng)該是采用ios系統(tǒng)自帶的AVFoundation框架。

我們知道AVFoundation框架是蘋果專門為多媒體打造的一個庫,這個庫非常強大,專門用來處理音視頻等復(fù)雜的多媒體技術(shù),而本篇要講的所有技術(shù)點就是基于AVFoundation框架中的一個類——AVPlayer。

那么AVPlayer是什么?

你可以把他看成是一個已經(jīng)封裝好的播放器,它的作用是用來播放遠(yuǎn)程的或本地的視頻和音頻。因為本地的音視頻的播放比較簡單,這里就不做講述,本編主要是講遠(yuǎn)程音樂播放,因為都是基于AVPlayer同一套API,所以掌握遠(yuǎn)程音樂播放其實就是相當(dāng)于掌握遠(yuǎn)程視頻播放。好了廢話就不多說了,下面開始上菜。

1、導(dǎo)入AVFoundation框架,創(chuàng)建AVPlayer播放器

-(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;
}

此處懶加載創(chuàng)建,讓播放器成為控制器的全局屬性,注意需要強引用,否則回收釋放掉了就無法播放。

2、播放或停止音樂

    //開始播放
    [self.player play];
     //停止播放
    [self.player pause];

這個沒什么好講的,只要調(diào)用AVPlayer的兩個實例方法

3、切換當(dāng)前正在播放中的音樂資源

//創(chuàng)建需要播放的AVPlayerItem
 AVPlayerItem *item = [[AVPlayerItem alloc] initWithURL:[NSURL URLWithString:model.url]];
 //替換當(dāng)前音樂資源  
 [self.player replaceCurrentItemWithPlayerItem:item];

這個可以用于歌曲的切換,如上一首、下一首。

4、通過KVO監(jiān)聽播放器的狀態(tài)



 [self.player.currentItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
 

拿到播放器的currentItem,注冊當(dāng)前對象為觀察者,監(jiān)聽它的status屬性。status屬性是AVPlayerItemStatus類型,它是一個枚舉類型,如下:

typedef NS_ENUM(NSInteger, AVPlayerItemStatus) {
    AVPlayerItemStatusUnknown,//未知狀態(tài)
    AVPlayerItemStatusReadyToPlay,//準(zhǔn)備播放
    AVPlayerItemStatusFailed//加載失敗
};

當(dāng)status屬性值發(fā)生改變時,就會觸發(fā)觀察者方法的回調(diào),如下:

//觀察者回調(diào)
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
   //注意這里查看的是self.player.status屬性
    if ([keyPath isEqualToString:@"status"]) {
        switch (self.player.status) {
            case AVPlayerStatusUnknown:
            {
                NSLog(@"未知轉(zhuǎn)態(tài)");
            }
                break;
            case AVPlayerStatusReadyToPlay:
            {
                NSLog(@"準(zhǔn)備播放");
            }
                break;
            case AVPlayerStatusFailed:
            {
                NSLog(@"加載失敗");
            }
                break;
                 default:
                break;
        }
   }
}

當(dāng) self.player.status == AVPlayerStatusReadyToPlay時,音樂就會開始正常播放,另外兩種狀態(tài)音樂是無法播放的,可以在上面方法相應(yīng)狀態(tài)里給出提示。這里需要特別強調(diào)一點的是觀察者監(jiān)聽的對象是self.player.currentItem,而不是self.player,而當(dāng)監(jiān)聽的屬性發(fā)生改變時,觀察者回調(diào)的方法里需要查看的是self.player.status。當(dāng)然,你也可以不這么干,但是我嘗試過好幾次,不這么干的后果是無法監(jiān)聽到self.player.status屬性的改變。

當(dāng)音樂播放完成,或者切換下一首歌曲時,請務(wù)必記得移除觀察者,否則會crash。操作如下:

//移除觀察者
 [self.player.currentItem removeObserver:self forKeyPath:@"status"];

5、監(jiān)聽音樂的緩沖進度

這個也是通過KVO監(jiān)聽播放器當(dāng)前播放的音樂資源AVPlayerItemloadedTimeRanges屬性。我們先看監(jiān)聽,如下:

//KVO監(jiān)聽音樂緩沖狀態(tài)
[self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];

當(dāng)loadedTimeRanges屬性發(fā)生改變時,回調(diào)如下:

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)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這個屬性是一個數(shù)組,里面裝的是本次緩沖的時間范圍,這個范圍是用一個結(jié)構(gòu)體CMTimeRange表示,當(dāng)然在oc中結(jié)構(gòu)體是不能直接存放數(shù)組的,所以它被包裝成了oc對象NSValue。

我們來看下這個結(jié)構(gòu)體:

typedef struct
{
    CMTime            start;        
    CMTime            duration;    
} CMTimeRange;

start表示本次緩沖時間的起點,duratin表示本次緩沖持續(xù)的時間范圍,具體詳細(xì)的計算方法可以看上面方法的實現(xiàn)。

當(dāng)音樂播放完成,或者切換下一首歌曲時,請務(wù)必記得移除觀察者,否則會crash。操作如下:

[self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];

6、監(jiān)聽音樂播放的進度

這個不是通過KVO了,AVPlayer專門提供了下面這個api用來監(jiān)聽播放的進度:

/**
 監(jiān)聽音樂播放進度

 @param interval 監(jiān)聽的時間間隔,用來設(shè)置多長時間回調(diào)一次
 @param queue    隊列,一般傳主隊列
 @param block    回調(diào)的block,會把當(dāng)前的播放時間傳遞過來

 @return 監(jiān)聽的對象
 */
- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;

操作如下:

 __weak typeof(self) weakSelf = self;
    self.timeObserver =  [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
        //當(dāng)前播放的時間
        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里面拿到當(dāng)前播放時間,根據(jù)總時間計算出當(dāng)前播放所占的時間比例,最后更新播放進度條。這里又涉及到了一個數(shù)據(jù)類類型CMTime,它也是一個結(jié)構(gòu)體,用來作為時間的格式,定義如下:

   typedef struct
     CMTimeValue    value;        
     CMTimeScale    timescale;    
     CMTimeFlags    flags;        
     CMTimeEpoch    epoch;        
   } CMTime;

CMTime是以分?jǐn)?shù)的形式表示時間,value表示分子,timescale表示分母,flags是位掩碼,表示時間的指定狀態(tài)。所以我們要獲得時間的秒數(shù)需要分子除以分母。當(dāng)然你還可以用下面這個函數(shù)來獲取時間的秒數(shù):

Float64 CMTimeGetSeconds(CMTime time)

最后,當(dāng)音樂播放完成或者切換音樂時,依然需要移除監(jiān)聽:

if (self.timeObserver) {
        [self.player removeTimeObserver:self.timeObserver];
        self.timeObserver = nil;
    }

7、手動超控(移動滑塊)播放進度

這是一個播放音視頻很常見的功能,所以強大的AVPlayer理所當(dāng)然的提供了幾個api,下面只講述其中最簡單的一個:

/**
 定位播放時間

 @param time 指定的播放時間
 */
- (void)seekToTime:(CMTime)time;

具體使用如下:

//移動滑塊調(diào)整播放進度
- (IBAction)playSliderValueChange:(UISlider *)sender
{
    //根據(jù)值計算時間
    float time = sender.value * CMTimeGetSeconds(self.player.currentItem.duration);
    //跳轉(zhuǎn)到當(dāng)前指定時間
    [self.player seekToTime:CMTimeMake(time, 1)];
}

8、監(jiān)聽音樂播放完成

一般音視頻播放完成時我們或多或少的都要處理一些業(yè)務(wù),比如循環(huán)播放,播完退出界面等等。下面看下如何監(jiān)聽AVPlayer的播放完成。

//給AVPlayerItem添加播放完成通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:_player.currentItem];

這里是采用注冊監(jiān)聽AVPlayerItemDidPlayToEndTimeNotification通知,當(dāng)AVPlayer一播放完成時,便會發(fā)出這個通知,我們收到通知后進行處理即可

9、設(shè)置音樂后臺播放

我們知道運行在ios系統(tǒng)下的程序一旦進入后臺就會處于休眠狀態(tài),程序停止運行了,也就播放不了什么音樂了。但是有一些特定功能的app還是處于可以后臺運行的,比如音樂類型的app正處于這個范疇。但是,并不是說你在應(yīng)用中播放音樂就能后臺高枕無憂的運行了,你依然需要做如下幾步操作:

(1)開啟后臺模式

target ->capabilities-> Background modes ->打開開關(guān) ->勾選第一個選項


設(shè)置后臺播放模式

(2)程序啟動時設(shè)置音頻會話

   //一般在方法:application: didFinishLaunchingWithOptions:設(shè)置
   //獲取音頻會話
    AVAudioSession *session = [AVAudioSession sharedInstance];
    //設(shè)置類型是播放。
    [session setCategory:AVAudioSessionCategoryPlayback error:nil];
    //激活音頻會話。
    [session setActive:YES error:nil];

以上兩步設(shè)置無誤,程序進入后臺模式,便可以進行音樂播放

10、如何設(shè)置音樂鎖頻信息

我們看百度音樂鎖頻時,也依然能在屏幕上展示歌曲的信息,以及切換歌曲等。下面看看這個功能是如何實現(xiàn)的:

//音樂鎖屏信息展示
- (void)setupLockScreenInfo
{
    // 1.獲取鎖屏中心
    MPNowPlayingInfoCenter *playingInfoCenter = [MPNowPlayingInfoCenter defaultCenter];
    
   //初始化一個存放音樂信息的字典
    NSMutableDictionary *playingInfoDict = [NSMutableDictionary dictionary];
    // 2、設(shè)置歌曲名
    if (self.currentModel.name) {
        [playingInfoDict setObject:self.currentModel.name forKey:MPMediaItemPropertyAlbumTitle];
    }
    // 設(shè)置歌手名
    if (self.currentModel.artist) {
        [playingInfoDict setObject:self.currentModel.artist forKey:MPMediaItemPropertyArtist];
    }
    // 3設(shè)置封面的圖片
    UIImage *image = [self getMusicImageWithMusicId:self.currentModel];
    if (image) {
        MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:image];
        [playingInfoDict setObject:artwork forKey:MPMediaItemPropertyArtwork];
    }
    
    // 4設(shè)置歌曲的總時長
    [playingInfoDict setObject:self.currentModel.detailDuration forKey:MPMediaItemPropertyPlaybackDuration];
    
    //音樂信息賦值給獲取鎖屏中心的nowPlayingInfo屬性
    playingInfoCenter.nowPlayingInfo = playingInfoDict;
    
    // 5.開啟遠(yuǎn)程交互,只有開啟這個才能進行遠(yuǎn)程操控
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}

這里設(shè)置圖片時需要注意下,異步加載網(wǎng)絡(luò)圖片后再設(shè)置是無效的,所以圖片信息最好是先請求下來后再進行設(shè)置。

遠(yuǎn)程超控的回調(diào)如下:

//監(jiān)聽遠(yuǎn)程交互方法
- (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;
    }
}

三、總結(jié)

最后,畫了一張圖總結(jié)下播放遠(yuǎn)程網(wǎng)絡(luò)音樂的流程:

遠(yuǎn)程音樂播放流程.png

根據(jù)QQ音樂的界面做了個小demo,下面是demo的真機前臺和后臺播放的運行效果:

前臺播放
后臺播放

附上github下載地址:https://github.com/yedexiong/MsuicPlayDemo.git

四、結(jié)束語

播放遠(yuǎn)程網(wǎng)絡(luò)音樂的核心技術(shù)點基本上已經(jīng)寫完,當(dāng)然AVPlayer還有很多強大的功能沒有寫出來,有興趣的可以進一步挖掘。寫到這里已經(jīng)疲倦至極,如果喜歡的可以點贊和關(guān)注,后續(xù)會持續(xù)更新一些精彩的技術(shù)點。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內(nèi)容