iOS AVPlayer的那些坑

盧三發表於2017-12-28

這次主要是總結和記錄下視訊播放遇到的坑,視訊播放採用的是AVPlayer這個控制元件,語法大致如下:

    NSURL * url = [NSURL fileURLWithPath:@"視訊地址"];
    AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:url];
    self.player = [AVPlayer playerWithPlayerItem:playerItem];
    [playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
    self.player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
    self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
    self.playerLayer.videoGravity     = AVLayerVideoGravityResizeAspect;
    self.playerLayer.frame = self.view.bounds;
    [self.view.layer addSublayer:self.playerLayer];
複製程式碼

一般說來,這裡要監聽AVPlayerItem的status屬性:

 *
 *AVPlayerItem的三種狀態
 *AVPlayerItemStatusUnknown,
 *AVPlayerItemStatusReadyToPlay,
 *AVPlayerItemStatusFailed
 */
複製程式碼

如果是AVPlayerStatusFailed說明視訊載入失敗,這時可以通過self.player.error.description屬性來找出具體的原因。 問題一:當status變為AVPlayerStatusReadyToPlay後,我們呼叫play方法真的就能保證視訊正常播放嗎?

眾所周知,AVPlayer支援的視訊、音訊格式非常廣泛,拋開那些無法正常編解碼的情況,在某些情況下其可能就是無法正常播放。AVPlayer在進行播放時,會預先解碼一些內容,而此時如果我們的App使用CPU過多,I/O讀寫過多時,有可能導致視訊播放聲/畫不同步,這點尤其在iPhone4上面表現更為明顯,使用者反饋iOS9.3.2的系統上也很明顯。而如果是發生在AVPlayer初始化解碼視訊的時候,有可能導致視訊直接無法播放,這時,我們再呼叫play或者seekToTime:方法都無法正常播放。建議不要在CPU或者I/O很頻繁的情況下使用AVPlayer,例如剛登入App載入各種資料的情況下,可以等App預熱以後再使用。

問題二:當rate屬性的值大於0後,真的就在播放視訊了嗎?

當然不是。當發生上面所講的情況時,我列印了當前的rate情況,是大於0的,但是頁面上顯示的情況卻還是什麼也沒有。有時候我們如果想要在視訊一播放的時候去做一些事情,例如設定一下播放器的背景色,如果我們僅僅是監聽這個rate可能無法100%保證有效,而如果我們真的要監聽這種情況的話,有一個取巧的方法:

 id _timerObserver = [self.player addBoundaryTimeObserverForTimes:@[[NSValue valueWithCMTime:CMTimeMake(1, 30)]] queue:dispatch_get_main_queue()
                                                       usingBlock:^{
        //do something
    }];
複製程式碼

另外如果不需要監聽播放進度的時候可以調下面的方法:

[self.player removeTimeObserver:_timerObserver];
複製程式碼

問題三:AVPlayer前後臺播放

當我們切換到後臺後,這時AVPlayer通常會自動暫停,當然如果設定了後臺播放音訊的話,是可以在後臺繼續播放聲音的,正如蘋果自己的WWDC這個App一樣。這個功能在我的另一篇文章iOS AVPlayer之後臺連續播放視訊中解決了這個問題。

問題四:音訊通道的搶佔引起的無法播放視訊問題

這個問題下週我會另開一篇部落格專門講述,今兒就此略過。

問題五:其它App播放聲音打斷

如果使用者當時在後臺聽音樂,如QQ音樂,或者喜馬拉雅這些App,這個時候播放視訊後,其會被我們打斷,當我們不再播放視訊的時候,自然需要繼續這些後臺聲音的播放。

首先,我們需要先向裝置註冊啟用聲音打斷AudioSessionSetActive(YES);,當然我們也可以通過 [AVAudioSession sharedInstance].otherAudioPlaying;這個方法來判斷還有沒有其它業務的聲音在播放。 當我們播放完視訊後,需要恢復其它業務或App的聲音,這時我們可以在退到後臺的事件中呼叫如下方法:

- (void)applicationDidEnterBackground:(UIApplication *)application {

    NSError *error =nil;
    AVAudioSession *session = [AVAudioSession sharedInstance];
    
    // [session setCategory:AVAudioSessionCategoryPlayback error:nil];
    BOOL isSuccess = [session setActive:NO withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation error:&error];
    
    if (!isSuccess) {
        
        NSLog(@"__%@",error);
        
    }else{
        
        
        NSLog(@"成功了");
    }
    
    
}
複製程式碼

問題六:在使用者插入和拔出耳機時,導致視訊暫停,解決方法如下

 //耳機插入和拔掉通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioRouteChangeListenerCallback:) name:AVAudioSessionRouteChangeNotification object:[AVAudioSession sharedInstance]];
    
    //耳機插入、拔出事件
- (void)audioRouteChangeListenerCallback:(NSNotification*)notification {
    NSDictionary *interuptionDict = notification.userInfo;
    
    NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
    
    switch (routeChangeReason) {
            
        case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
            
            break;
            
        case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
        {
            //判斷為耳機介面
            AVAudioSessionRouteDescription *previousRoute =interuptionDict[AVAudioSessionRouteChangePreviousRouteKey];
            
            AVAudioSessionPortDescription *previousOutput =previousRoute.outputs[0];
            NSString *portType =previousOutput.portType;
            
            if ([portType isEqualToString:AVAudioSessionPortHeadphones]) {
                // 拔掉耳機繼續播放
                if (self.playing) {
                    
                    [self.player play];
                }
            }
            
    }
            break;
            
        case AVAudioSessionRouteChangeReasonCategoryChange:
            // called at start - also when other audio wants to play
            
            break;
    }
}

    
複製程式碼

問題七:打電話等中斷事件

//中斷的通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];
    //中斷事件
- (void)handleInterruption:(NSNotification *)notification{
    
    NSDictionary *info = notification.userInfo;
        //一箇中斷狀態型別
        AVAudioSessionInterruptionType type =[info[AVAudioSessionInterruptionTypeKey] integerValue];
    
        //判斷開始中斷還是中斷已經結束
        if (type == AVAudioSessionInterruptionTypeBegan) {
            //停止播放
            [self.player pause];
            
        }else {
            //如果中斷結束會附帶一個KEY值,表明是否應該恢復音訊
            AVAudioSessionInterruptionOptions options =[info[AVAudioSessionInterruptionOptionKey] integerValue];
            if (options == AVAudioSessionInterruptionOptionShouldResume) {
                //恢復播放
                [self.player play];
            }
            
 }
    
}
    
複製程式碼

小提示:如果不起作用,請檢查退到後臺事件中有什麼其它的操作沒。因為電話來時,會呼叫退到後臺的事件。

問題七:記憶體洩露問題 當我們釋放一個正在播放的視訊時,需要先呼叫pause方法,如果由於某些原因,例如切前後臺時,導致又呼叫了play方法,那麼有可能會hold不住記憶體空間而導致記憶體洩漏。其實更靠譜的方法是,還要移除player載入的資源。

總的來說,AVPlayer能滿足一般的需求,雖然坑不少。最後,再來推銷一下我自己封裝的LYAVPlayer,簡單方便,支援cocoa pods,只需幾行程式碼即可完成播放:

         LYAVPlayerView *playerView =[LYAVPlayerView alloc]init];         
         playerView.frame =CGRectMake(0, 64, ScreenWidth,200);
         playerView.delegate =self;//設定代理
         [self.view addSubview:playerView];
         [playerView setURL:[NSURL URLWithString:VideoURL]];//設定播放的URL
         [playerView play];//開始播放
複製程式碼

工程中pod 'LYAVPlayer','~> 1.0.1'即可使用。有什麼問題請Issues我。

相關文章