iOS音樂播放器(上)

最近一直在做音頻播放器的改版重構(gòu),開發(fā)測試到最后發(fā)布用時差不多要一個月,功能上可能并不是很多,跟大多數(shù)音樂播放器一樣,但是要注意的細(xì)節(jié)倒是不少。今天將播放器整理一下,做個儲備。
播放器懸浮窗篇

播放器功能結(jié)構(gòu)

  • 從視圖上看整個播放器從上往下分為六個功能結(jié)構(gòu)
  • 音頻詳情:當(dāng)前音頻的詳情數(shù)據(jù)
  • 專欄詳情:當(dāng)前專欄的詳情
  • 倍速定時控制:倍速和定時列表選項(xiàng)
  • 控制條:進(jìn)度控制
  • 播放控制:音頻播放控制
  • 業(yè)務(wù)相關(guān):音頻列表、下載、點(diǎn)贊、進(jìn)文章頁

從功能結(jié)構(gòu)上劃分,可以對播放器的視圖做一個結(jié)構(gòu)拆分,這里是將播放控制和業(yè)務(wù)相關(guān)進(jìn)行了一個自定義view,可以簡單的降低了視圖的復(fù)雜度。

播放器數(shù)據(jù)結(jié)構(gòu)

開發(fā)模式采用了MVVM+RAC,無論是MVC還是MVVM、MVCS說白了都是數(shù)據(jù)流式架構(gòu),控制了數(shù)據(jù)的在代碼結(jié)構(gòu)中的走向,所以項(xiàng)目中只要能約定好一個固定的數(shù)據(jù)流的走向,分層結(jié)構(gòu)明確,就能在開發(fā)中降低耦合度,提升代碼質(zhì)量。
我們這里的VM是拆分成了store 和 service,store層負(fù)責(zé)基礎(chǔ)數(shù)據(jù)的管理,包括從網(wǎng)絡(luò)層和數(shù)據(jù)庫層上來的數(shù)據(jù),service負(fù)責(zé)從store層拿數(shù)據(jù),然后通過RAC提供給view層,所以播放器這里的view,只提供了一個入口函數(shù),外邊調(diào)用播放器,只需要傳遞專欄詳情和列表數(shù)據(jù)以及播放位置,而播放器的當(dāng)前播放數(shù)據(jù)都存放在service里。

//播放器的入口函數(shù),viewcontroller提供
- (void)addPlayList:(NSArray *)playList playIndex:(NSInteger)index detail:(NSDictionary *)detailColumn;

// 播放器的當(dāng)前播放數(shù)據(jù),service持有
@property (nonatomic, strong) Model                 currentAudioModel; //當(dāng)前播放的audio
@property (nonatomic, assign) NSInteger             currentAudioIndex; //當(dāng)前播放的位置
@property (nonatomic, strong) Model                 *detailRecord; //詳情
@property (nonatomic, strong) NSArray               *playList;//播放列表
@property (nonatomic, assign) PlayStatus            currentAudioPlayStatus; //當(dāng)前播放狀態(tài)
@property (nonatomic, strong) NSString              *playTime; //播放時間
@property (nonatomic, strong) NSNumber              *hadLiked; //是否點(diǎn)了贊
@property (nonatomic, strong) NSString              *audioSpeed;//當(dāng)前播放速度
@property (nonatomic, strong) RACSubject            *rac_shareIcon;

播放器核心控制

播放器開發(fā)通常采用的都是AVPlayer,功能豐富,自定制比較強(qiáng)。這里簡單的比較一下系統(tǒng)提供的音頻播放功能:

  • AudioToolbox.framework提供的系統(tǒng)聲音服務(wù)(System Sound Service),
    通常用來播放鈴聲,點(diǎn)擊聲音,推送聲
    音效播放時間不能超過30s
    數(shù)據(jù)必須是PCM或者IMA4格式
    音頻文件必須打包成.caf , .aif, .wav中的一種(這是官方的說法,實(shí)際測試一些.aac .mp3格式的也可以播放)
  • AVFoundation.framework中的AVAudioPlayer
    通過音頻的NSData或者本地音頻文件的url,來創(chuàng)建一個AVAudioPlayer實(shí)例,如加載本地的music.mp3的音頻
  • AVQueuePlayer
    AVPlayer的子類可以實(shí)現(xiàn)列表播放。因?yàn)槭顷犃泄芾恚詻]有提供上一首的方法,不滿足需求。
  • AVPlayer
    支持播放本地、分步下載、或在線流媒體音視頻,不僅可以播放音頻,配合AVPlayerLayer類可實(shí)現(xiàn)視頻播放。另外支持播放進(jìn)度監(jiān)聽。

AVPlayer的使用

  1. 設(shè)置播放器URL
    AVURLAsset *asset = [AVURLAsset URLAssetWithURL:playerUrl options:nil];
    AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:asset];
    self.player = [AVPlayer playerWithPlayerItem:item];
  1. 添加播放器監(jiān)聽
    這里監(jiān)聽的播放器器屬性包含了四個
  • status:我在AVPlayerItemStatusReadyToPlay的狀態(tài)下,取CMTimeGetSeconds(playerItem.duration)的值進(jìn)行了播放總時間的更新,因?yàn)楹笈_給的音頻總時間不一定是很準(zhǔn)確的,可能會丟失一點(diǎn)精度。
  • loadedTimeRanges:網(wǎng)絡(luò)音頻的的緩沖進(jìn)度,這里需要注意的是,本地數(shù)據(jù)不會調(diào)用,所以在設(shè)置的時候本地數(shù)據(jù)不能依賴這里
  • playbackBufferEmpty:緩沖是否為空,這個值為YES,視頻就會暫停,加載圈就會啟動
  • playbackLikelyToKeepUp:視頻是否可以正常播放,這個值為YES,加載圈就會消失,這里不包含本地音頻播放的情況
  • name:AVPlayerItemDidPlayToEndTimeNotification:播放結(jié)束通知
  • addPeriodicTimeObserverForInterval 播放進(jìn)度監(jiān)聽
    [self.player.currentItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    [self.player.currentItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    [self.player.currentItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
    [self.player.currentItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
//
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playFinished:) name:AVPlayerItemDidPlayToEndTimeNotification object:self.player.currentItem];
- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;

  1. 鎖屏狀態(tài)信息
//音樂鎖屏信息展示
- (void)setupLockScreenInfo
{
    // 1.獲取鎖屏中心
    MPNowPlayingInfoCenter *playingInfoCenter = [MPNowPlayingInfoCenter defaultCenter];
    
    //初始化一個存放音樂信息的字典
    NSMutableDictionary *playingInfoDict = [NSMutableDictionary dictionary];
    // 2、設(shè)置歌曲名
    [playingInfoDict setObject:self.currentModel.title forKey:MPMediaItemPropertyTitle];
    // 設(shè)置歌手名
    [playingInfoDict setObject:self.currentModel.artist forKey:MPMediaItemPropertyArtist];
    // 3設(shè)置封面的圖片
    MPMediaItemArtwork *artwork = [[MPMediaItemArtwork alloc] initWithImage:image];
    [playingInfoDict setObject:artwork forKey:MPMediaItemPropertyArtwork];
    // 4設(shè)置歌曲的總時長
    [playingInfoDict setObject:self.currentModel.time?:@0 forKey:MPMediaItemPropertyPlaybackDuration];
   //音樂信息賦值給獲取鎖屏中心的nowPlayingInfo屬性
    playingInfoCenter.nowPlayingInfo = playingInfoDict;
    // 5.開啟遠(yuǎn)程交互
    [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}

設(shè)備控制之AVAudioSession

只要是app使用到了音頻處理的相關(guān)硬件,就需要對AVAudioSession進(jìn)行設(shè)置,來管理多個APP對音頻硬件設(shè)備(麥克風(fēng),揚(yáng)聲器)的資源使用。
AUAvudioSession能做很多事:

  • 設(shè)置自己的APP是否和其他APP音頻同時存在,還是中斷其他APP聲音
  • 在手機(jī)調(diào)到靜音模式下,自己的APP音頻是否可以播放出聲音
  • 電話或者其他APP中斷自己APP的音頻的事件處理
  • 指定音頻輸入和輸出的設(shè)備(比如是聽筒輸出聲音,還是揚(yáng)聲器輸出聲音)
  • 是否支持錄音,錄音同時是否支持音頻播放
1. AVAudioSession的Categoary設(shè)置
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]

上述代碼是一般音樂播放器程序中的category,代表了只支持音頻播放,通過設(shè)置可以不支持與其他app進(jìn)行混音。iOS一共提供了七種category來進(jìn)行開發(fā)中的相應(yīng)配置,每種配置都提供了四種能力,

四種能力:

  • 是否打斷不支持混音播放的APP
  • 是否會響應(yīng)手機(jī)靜音鍵開關(guān)
  • 是否支持音頻錄制
  • 是否支持音頻播放

七種配置:
cateory
  • AVAudioSessionCategoryAmbient,只支持音頻播放。這個 Category,音頻會被靜音鍵和鎖屏鍵靜音。并且不會打斷其他應(yīng)用的音頻播放。
  • AVAudioSessionCategorySoloAmbient,這個是系統(tǒng)默認(rèn)使用的 Category,只支持音頻播放。音頻會被靜音鍵和鎖屏鍵靜音。和AVAudioSessionCategoryAmbient不同的是,這個會打斷其他應(yīng)用的音頻播放
  • AVAudioSessionCategoryPlayback,只支持音頻播放。你的音頻不會被靜音鍵和鎖屏鍵靜音。適用于音頻是主要功能的APP,像網(wǎng)易云這些音樂app,鎖屏后依然可以播放。

需要注意一下,選擇支持在靜音鍵切到靜音狀態(tài)以及鎖屏鍵切到鎖屏狀態(tài)下仍然可以播放音頻 Category 時,必須在應(yīng)用中開啟支持后臺音頻功能,詳見 UIBackgroundModes

  • AVAudioSessionCategoryRecord,只支持音頻錄制。不支持播放。
  • AVAudioSessionCategoryPlayAndRecord,支持音頻播放和錄制。音頻的輸入和輸出不需要同步進(jìn)行,也可以同步進(jìn)行。需要音頻通話類應(yīng)用,可以使用這個 Category。
  • AVAudioSessionCategoryAudioProcessing,只支持本地音頻編解碼處理。不支持播放和錄制。
  • AVAudioSessionCategoryMultiRoute,支持音頻播放和錄制。允許多條音頻流的同步輸入和輸出。(比如USB連接外部揚(yáng)聲器輸出音頻,藍(lán)牙耳機(jī)同時播放另一路音頻這種特殊需求)

我們也可以通過AVAudioSession的屬性來讀取當(dāng)前設(shè)備支持的Category,這樣可以保證設(shè)備兼容性。

@property(readonly) NSArray<NSString *> *availableCategories;
2. AVAudioSession的Mode和Options設(shè)置

上邊的Category是七種大類別,Mode和Options是小類別調(diào)整,一些情況下我們可能并不需要調(diào)整。

  1. Mode
    我們通過讀取下面這條屬性獲取當(dāng)前設(shè)備支持的Mode
@property(readonly) NSArray<NSString *> *availableModes;

iOS下有七種mode來定制我們的Category行為

Model
  • AVAudioSessionModeDefault,默認(rèn)模式,與所有的 Category 兼容
  • AVAudioSessionModeVoiceChat,適用于VoIP 類型的應(yīng)用。只能是 AVAudioSessionCategoryPlayAndRecord Category下。在這個模式系統(tǒng)會自動配置AVAudioSessionCategoryOptionAllowBluetooth 這個選項(xiàng)。系統(tǒng)會自動選擇最佳的內(nèi)置麥克風(fēng)組合支持語音聊天。
  • AVAudioSessionModeVideoChat,用于視頻聊天類型應(yīng)用,只能是 AVAudioSessionCategoryPlayAndRecord Category下。適在這個模式系統(tǒng)會自動配置 AVAudioSessionCategoryOptionAllowBluetooth 和 AVAudioSessionCategoryOptionDefaultToSpeaker 選項(xiàng)。系統(tǒng)會自動選擇最佳的內(nèi)置麥克風(fēng)組合支持視頻聊天。
  • AVAudioSessionModeGameChat,適用于游戲類應(yīng)用。使用 GKVoiceChat 對象的應(yīng)用會自動設(shè)置這個模式和AVAudioSessionCategoryPlayAndRecord Category。實(shí)際參數(shù)和AVAudioSessionModeVideoChat一致
  • AVAudioSessionModeVideoRecording,適用于使用攝像頭采集視頻的應(yīng)用。只能是 AVAudioSessionCategoryPlayAndRecord 和 - AVAudioSessionCategoryRecord 這兩個 Category下。這個模式搭配 AVCaptureSession API 結(jié)合來用可以更好地控制音視頻的輸入輸出路徑。(例如,設(shè)置 automaticallyConfiguresApplicationAudioSession 屬性,系統(tǒng)會自動選擇最佳輸出路徑。
  • AVAudioSessionModeMeasurement,最小化系統(tǒng)。只用于 AVAudioSessionCategoryPlayAndRecord、AVAudioSessionCategoryRecord、AVAudioSessionCategoryPlayback 這幾種 Category。
  • AVAudioSessionModeMoviePlayback,適用于播放視頻的應(yīng)用。只用于 AVAudioSessionCategoryPlayback 這個Category。
  1. Options
    使用options去微調(diào)Category行為,如下表


    Options

    調(diào)優(yōu)我們的Category
    通過Category和合適的Mode和Options的搭配我們可以調(diào)優(yōu)出我們的效果,下面舉兩個應(yīng)用場景:
    用過高德地圖的都知道,在后臺播放QQ音樂的時候,如果導(dǎo)航語音出來,QQ音樂不會停止,而是被智能壓低和混音,等導(dǎo)航語音播報完后,QQ音樂正常播放,這里我們需要后臺播放音樂,所以Category使用AVAudioSessionCategoryPlayback,需要混音和智能壓低其他APP音量,所以O(shè)ptions選用 AVAudioSessionCategoryOptionMixWithOthers和AVAudioSessionCategoryOptionDuckOthers
    代碼示例如下

 BOOL isSuccess = [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback 
withOptions:AVAudioSessionCategoryOptionMixWithOthers | AVAudioSessionCategoryOptionDuckOthers 
error:&setCategoryError];

又或者我希望AVAudioSessionCategoryPlayAndRecord這個Category默認(rèn)的音頻由揚(yáng)聲器播放,那么可以調(diào)用這個接口去調(diào)整Category

- (BOOL)setCategory:(NSString *)category 
withOptions:(AVAudioSessionCategoryOptions)options 
error:(NSError **)outError

通過選擇合適和Category,mode和options,就可以調(diào)優(yōu)音頻的輸入輸出,來滿足日常開發(fā)需求(需要注意的是Category,mode,option是搭配使用的,而不是簡單組合,也就是說某種Category支持某些mode和option,從上面的表中也可以看出這一點(diǎn))

3. 播放器中斷處理

其他APP或者電話會中斷我們的APP音頻,所以相應(yīng)的我們要做出處理。
我們可以通過監(jiān)聽AVAudioSessionInterruptionNotification這個key獲取音頻中斷事件
回調(diào)回來Userinfo有鍵值

AVAudioSessionInterruptionTypeKey:
取值A(chǔ)VAudioSessionInterruptionTypeBegan表示中斷開始
取值A(chǔ)VAudioSessionInterruptionTypeEnded表示中斷結(jié)束

中斷開始:我們需要做的是保存好播放狀態(tài),上下文,更新用戶界面等
中斷結(jié)束:我們要做的是恢復(fù)好狀態(tài)和上下文,更新用戶界面,根據(jù)需求準(zhǔn)備好之后選擇是否激活我們session。
選擇不同的音頻播放技術(shù),處理中斷方式也有差別,具體如下:

  • System Sound Services:使用 System Sound Services 播發(fā)音頻,系統(tǒng)會自動處理,不受APP控制,當(dāng)中斷發(fā)生時,音頻播放會靜音,當(dāng)中斷結(jié)束后,音頻播放會恢復(fù)。
  • AV Foundation framework:AVAudioPlayer 類和 AVAudioRecorder 類提供了中斷開始和結(jié)束的 Delegate 回調(diào)方法來處理中斷。中斷發(fā)生,系統(tǒng)會自動停止播放,需要做的是記錄播放時間等狀態(tài),更新用戶界面,等中斷結(jié)束后,再次調(diào)用播放方法,系統(tǒng)會自動激活session。
  • Audio Queue Services, I/O audio unit:使用aduio unit這些技術(shù)需要處理中斷,需要做的是記錄播放或者錄制的位置,中斷結(jié)束后自己恢復(fù)audio session。
  • OpenAL:使用 OpenAL 播放時,同樣需要自己監(jiān)聽中斷。管理 OpenAL上下文,用戶中斷結(jié)束后恢復(fù)audio session。

需要注意的是:

  • 有中斷開始事件,不一定對應(yīng)有中斷結(jié)束事件,所以需要在用戶進(jìn)入前臺,點(diǎn)擊UI操作的時候,需要保存好播放狀態(tài)和對Audio Session管理,以便不影響APP的音頻功能。
  • 接受到中斷結(jié)束時候,需要判斷是否需要進(jìn)行繼續(xù)播放,未放置在之前沒有播放時候也接受到了中斷結(jié)束事件,所以需要增加之前是否播放的判斷。
  • 音頻資源競爭上,一定是電話優(yōu)先。
 [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleInterreption:)
                                                     name:AVAudioSessionInterruptionNotification
                                                   object:nil];
- (void)handleInterreption:(NSNotification *)notificaiton
{
    DDLogDebug(@"handleInterreption:%@", notificaiton.userInfo);
    
    AVAudioSessionInterruptionType interuptionType = [notificaiton.userInfo[AVAudioSessionInterruptionTypeKey] intValue];
    switch (interuptionType) {
        case AVAudioSessionInterruptionTypeBegan: {
            DDLogDebug(@"收到中斷,停止音頻播放");
            [self pause];
            break;
        }
        case AVAudioSessionInterruptionTypeEnded: {
            DDLogDebug(@"系統(tǒng)中斷結(jié)束");
            AVAudioSessionInterruptionOptions options = [notificaiton.userInfo[AVAudioSessionInterruptionOptionKey] intValue];
            NSError *error = nil;
            if (options == AVAudioSessionInterruptionOptionShouldResume) {
                [self play:nil];
            }
            if (error) {
                DDLogDebug(@"AVAudioSessionInterruptionOptionShouldResume失敗:%@",[error localizedDescription]);
            }
            break;
        }
    }
}

3. AVAudioSession之 setActive

setActive方法有一下幾個功能:

  • 設(shè)置在靜音模式下播放
  • 設(shè)置之后可以使app拿到第一響應(yīng)者,同時會取消別的音樂播放器播放狀態(tài)
  • 取消設(shè)置時候設(shè)置withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation,可以放棄相應(yīng),使對方收到中斷通知,對方app可以繼續(xù)播放。

這里有一個問題,在app剛啟動時候,設(shè)置active為yes,然后立刻鎖屏,app會有幾率收到中斷通知,所以我們上邊提到,收到中斷喚醒時候需要判斷是否可以進(jìn)行播放。

耳機(jī)監(jiān)聽

[[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(audioSessionRouteChange:)
                                                 name:AVAudioSessionRouteChangeNotification
                                               object:nil];
- (void)audioSessionRouteChange:(NSNotification *)notify
{
    NSDictionary *interuptionDict = notify.userInfo;
    
    NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
    switch (routeChangeReason) {
        case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
            DDLogVerbose(@"耳機(jī)插入");
            // 繼續(xù)播放音頻,什么也不用做
            break;
        case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
        {
            DDLogVerbose(@"耳機(jī)拔出");
            // 拔出耳機(jī)時系統(tǒng)會自動暫停你正在播放的音頻,因此只需要改變UI為暫停狀態(tài)即可
            if ([GKTAudioManager sharedManager].currentAudioPlayStatus == GKTPlayStatusPlaying) {
                [[GKTAudioManager sharedManager] pause];
            }
        }
            break;
        default:
            break;
    }
}

遠(yuǎn)程控制,耳機(jī)和鎖屏狀態(tài)

//鎖屏界面開啟和監(jiān)控遠(yuǎn)程控制事件
+ (void)configRemoteCommandCenter{    /**/
    //遠(yuǎn)程控制命令中心 iOS 7.1 之后  詳情看官方文檔:https://developer.apple.com/documentation/mediaplayer/mpremotecommandcenter
    
    MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
    //   MPFeedbackCommand對象反映了當(dāng)前App所播放的反饋狀態(tài). MPRemoteCommandCenter對象提供feedback對象用于對媒體文件進(jìn)行喜歡, 不喜歡, 標(biāo)記的操作. 效果類似于網(wǎng)易云音樂鎖屏?xí)r的效果
    
    //添加喜歡按鈕
    //    MPFeedbackCommand *likeCommand = commandCenter.likeCommand;
    //    likeCommand.enabled = YES;
    //    likeCommand.localizedTitle = @"喜歡";
    //    [likeCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {        NSLog(@"喜歡");        return MPRemoteCommandHandlerStatusSuccess;
    //    }];    //添加不喜歡按鈕,假裝是“上一首”
    //    MPFeedbackCommand *dislikeCommand = commandCenter.dislikeCommand;
    //    dislikeCommand.enabled = YES;
    //    dislikeCommand.localizedTitle = @"上一首";
    //    [dislikeCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
    //        NSLog(@"上一首");
    //        [[GKTAudioManager sharedManager] previous:^(id error, id result) {
    //
    //        }];
    //        return MPRemoteCommandHandlerStatusSuccess;
    //    }];
    
    [commandCenter.previousTrackCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
        NSLog(@"上一首");
        [[GKTAudioManager sharedManager] previous];
        return MPRemoteCommandHandlerStatusSuccess;
    }];
    //標(biāo)記
    //    MPFeedbackCommand *bookmarkCommand = commandCenter.bookmarkCommand;
    //    bookmarkCommand.enabled = YES;
    //    bookmarkCommand.localizedTitle = @"標(biāo)記";
    //    [bookmarkCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {        NSLog(@"標(biāo)記");        return MPRemoteCommandHandlerStatusSuccess;
    //    }];
    //    commandCenter.togglePlayPauseCommand 耳機(jī)線控的暫停/播放
    
    commandCenter.togglePlayPauseCommand.enabled = YES;
    [commandCenter.togglePlayPauseCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
        if ([GKTAudioManager sharedManager].currentAudioPlayStatus == GKTPlayStatusPlaying) {
            [[GKTAudioManager sharedManager] pause];
        } else {
            [[GKTAudioManager sharedManager] play:^(id error, id result) {
                
            }];
        }
        return MPRemoteCommandHandlerStatusSuccess;
    }];
    
    [commandCenter.pauseCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
        [[GKTAudioManager sharedManager] pause];
        return MPRemoteCommandHandlerStatusSuccess;
    }];
    [commandCenter.playCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
        [[GKTAudioManager sharedManager] play:^(id error, id result) {
            
        }];
        //        [self.player setRate:self.rateValue];
        return MPRemoteCommandHandlerStatusSuccess;
    }];    //    [commandCenter.previousTrackCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {
    //        NSLog(@"上一首");
    //        return MPRemoteCommandHandlerStatusSuccess;
    //    }];
    
    [commandCenter.nextTrackCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {        NSLog(@"下一首");
        [[GKTAudioManager sharedManager] next:^(id error, id result) {
            
        }];
        //        [self.player setRate:self.rateValue];
        
        return MPRemoteCommandHandlerStatusSuccess;
    }];
    //快進(jìn)
    //    MPSkipIntervalCommand *skipBackwardIntervalCommand = commandCenter.skipForwardCommand;
    //    skipBackwardIntervalCommand.preferredIntervals = @[@(54)];
    //    skipBackwardIntervalCommand.enabled = YES;
    //    [skipBackwardIntervalCommand addTarget:self action:@selector(skipBackwardEvent:)];
    
    //在控制臺拖動進(jìn)度條調(diào)節(jié)進(jìn)度(仿QQ音樂的效果)
    [commandCenter.changePlaybackPositionCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent * _Nonnull event) {        CMTime totlaTime = [GKTAudioManager sharedManager].player.currentItem.duration;
        MPChangePlaybackPositionCommandEvent * playbackPositionEvent = (MPChangePlaybackPositionCommandEvent *)event;
        @try {
            [[GKTAudioManager sharedManager].player seekToTime:CMTimeMake(totlaTime.value*playbackPositionEvent.positionTime/CMTimeGetSeconds(totlaTime), totlaTime.timescale) completionHandler:^(BOOL finished) {
            }];
        } @catch (NSException *exception) {} @finally {}
        return MPRemoteCommandHandlerStatusSuccess;
    }];
}

-(void)skipBackwardEvent: (MPSkipIntervalCommandEvent *)skipEvent
{    NSLog(@"快進(jìn)了 %f秒", skipEvent.interval);
}

總結(jié):
播放器的總結(jié)大概就到這里,在開發(fā)的過程中,我們可能更關(guān)注一個基礎(chǔ)功能的開發(fā),但更要注重細(xì)節(jié)上東西,比如上邊提到的AVAudioSession對 setActive的設(shè)置,大概率在發(fā)布之前,大家更注重的是功能上的完整性,而推到了線上,用戶在使用時候,對細(xì)節(jié)的感知更明顯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容