iOS視頻播放器之ZFPlayer剖析

引言
本文主要針對ZFPlayer的功能實現來剖析,以及總結一下你們遇到的問題和解決方案
首先ZFPlayer如今擁有的功能:

支持橫、豎屏切換,在全屏播放模式下還能夠鎖定屏幕方向
支持本地視頻、網絡視頻播放
支持在TableviewCell播放視頻
左側1/2位置上下滑動調節屏幕亮度(模擬器調不了亮度,請在真機調試)
右側1/2位置上下滑動調節音量(模擬器調不了音量,請在真機調試)
左右滑動調節播放進度
全屏狀態下拖動slider控制進度,顯示視頻的預覽圖
斷點下載功能
切換視頻分辨率、
ZFPlayer是對AVPlayer的封裝,有人會問它支持什麼格式的視頻播放,問這個問題的能夠自行搜索AVPlayer支持的格式。

跟AVPlayer聯繫密切的名詞:

Asset:AVAsset是抽象類,不能直接使用,其子類AVURLAsset能夠根據URL生成包含媒體信息的Asset對象。
AVPlayerItem:和媒體資源存在對應關係,管理媒體資源的信息和狀態。
AVPlayerLayer: CALayer的subclass,它主要用來在iOS中播放視頻內容
具體功能實現
一、經過一個網絡連接播放視頻
AVURLAsset *urlAsset = [AVURLAsset assetWithURL:videoURL];
// 初始化playerItem
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:urlAsset];
// 也可使用來初始化playerItem
// AVPlayerItem * playerItem = [AVPlayerItem playerItemWithURL:videoURL];

// 初始化Player
AVPlayer *player = [AVPlayer playerWithPlayerItem:self.playerItem];
// 初始化playerLayer
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
// 添加playerLayer到self.layer
[self.layer insertSublayer:self.playerLayer atIndex:0];
二、播放器的經常使用操做
播放:
[player play];
須要注意的是初始化完player以後不必定會立刻開始播放,須要等待player的狀態變爲ReadyToPlay纔會進行播放。
暫停:
[player pause];
三、播放多個items
這裏咱們有兩種方式能夠實現,一種是由你自行控制下一首歌曲的item,將其替換到當前播放的item

[player replaceCurrentItemWithPlayerItem:playerItem];
在iOS9後,AVPlayer的replaceCurrentItemWithPlayerItem方法在切換視頻時底層會調用信號量等待而後致使當前線程卡頓,若是在UITableViewCell中切換視頻播放使用這個方法,會致使當前線程凍結幾秒鐘。遇到這個坑還真很差在系統層面對它作什麼,後來找到的解決方法是在每次須要切換視頻時,需從新建立AVPlayer和AVPlayerItem。

另外一種可使用AVQueuePlayer播放多個items,AVQueuePlayer是AVPlayer的子類,能夠用一個數組來初始化一個AVQueuePlayer對象。代碼以下:

NSArray *items = <#An array of player items#>;
AVQueuePlayer *queuePlayer = [[AVQueuePlayer alloc] initWithItems:items];
和AVPlayer同樣,直接調用play方法來播放,queue player順序播放隊列中的item,若是想要跳過一個item,播放下一個item,能夠調用方法advanceToNextItem。

能夠對隊列進行插入和刪除操做,調用方法insertItem:afterItem:, removeItem:, 和 removeAllItems。正常狀況下當插入一個item以前,應該檢查是否能夠插入,經過使用canInsertItem:afterItem:方法,第二個參數傳nil,代碼以下:

AVPlayerItem *anItem = <#Get a player item#>;
if ([queuePlayer canInsertItem:anItem afterItem:nil]) {
   [queuePlayer insertItem:anItem afterItem:nil];
}
四、seekToTime指定從某一秒開始播放
可使用seekToTime:定位播放頭到指定的時間,以下代碼:

CMTime fiveSecondsIn = CMTimeMake(5, 1);
[player seekToTime:fiveSecondsIn];
seekTime:不能精肯定位,若是須要精肯定位,可使用seekToTime:toleranceBefore:toleranceAfter:,代碼以下:

CMTime fiveSecondsIn = CMTimeMake(5, 1);
[player seekToTime:fiveSecondsIn toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];
當tolerance=0的時候,framework須要進行大量解碼工做,比較耗性能,因此,只有當你必須使用的時候才用這個方法,好比開發一個複雜的多媒體編輯應用,這須要精確的控制。

關於重播什麼的就不用我多說了吧,點擊重播seekToTime:kCMTimeZero。還有關於下次播放的時候從上次離開的那個時間開始播放,你們都有思路啦吧,當離開當前視頻時候記錄播放到哪一秒了,下次點開直接seekToTime到那一秒開始播放就行了嘛。

五、監聽播放進度
使用addPeriodicTimeObserverForInterval:queue:usingBlock:來監聽播放器的進度
(1)方法傳入一個CMTime結構體,每到必定時間都會回調一次,包括開始和結束播放
(2)若是block裏面的操做耗時太長,下次不必定會收到回調,因此儘可能減小block的操做耗時
(3)方法會返回一個觀察者對象,當播放完畢時須要移除這個觀察者
添加觀察者:

id timeObserve = [player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
    float current = CMTimeGetSeconds(time);
    float total = CMTimeGetSeconds(songItem.duration);
    if (current) {
        weakSelf.progress = current / total;
        weakSelf.playTime = [NSString stringWithFormat:@"%.f",current];
        weakSelf.playDuration = [NSString stringWithFormat:@"%.2f",total];       
    }
}];
移除觀察者:

if (timeObserve) {
   [player removeTimeObserver:_timeObserve];
   timeObserve = nil;
 }
六、監聽改播放器狀態
[playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
播放器的三種狀態,當playerItem的狀態變爲AVPlayerItemStatusReadyToPlay纔會進行播放。

typedef NS_ENUM(NSInteger, AVPlayerItemStatus) {
   AVPlayerItemStatusUnknown,
   AVPlayerItemStatusReadyToPlay,
   AVPlayerItemStatusFailed
};
播放完了須要移除觀察者

[playerItem removeObserver:self forKeyPath:@"status"];
七、監聽緩衝進度
[playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
播放完了須要移除觀察者

[playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
八、監聽網絡緩衝狀態
// 緩衝區空了,須要等待數據
[playerItem addObserver:self forKeyPath:@"playbackBufferEmpty" options:NSKeyValueObservingOptionNew context:nil];
// 緩衝區有足夠數據能夠播放了
[playerItem addObserver:self forKeyPath:@"playbackLikelyToKeepUp" options:NSKeyValueObservingOptionNew context:nil];
播放完了須要移除觀察者

[playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty"];
[playerItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
九、監聽AVPlayer播放完成通知
監聽通知AVPlayerItemDidPlayToEndTimeNotification,來處理一些播放完的事情

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playerItemDidReachEnd:) name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
十、 系統音量相關
/**
 *  獲取系統音量
 */
- (void)configureVolume
{
    MPVolumeView *volumeView = [[MPVolumeView alloc] init];
    _volumeViewSlider = nil;
    for (UIView *view in [volumeView subviews]){
        if ([view.class.description isEqualToString:@"MPVolumeSlider"]){
            _volumeViewSlider = (UISlider *)view;
            break;
        }
    }

    // 使用這個category的應用不會隨着手機靜音鍵打開而靜音,可在手機靜音下播放聲音
    NSError *setCategoryError = nil;
    BOOL success = [[AVAudioSession sharedInstance]
                    setCategory: AVAudioSessionCategoryPlayback
                    error: &setCategoryError];

    if (!success) { /* handle the error in setCategoryError */ }

    // 監聽耳機插入和拔掉通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(audioRouteChangeListenerCallback:) name:AVAudioSessionRouteChangeNotification object:nil];
}

/**
 *  耳機插入、拔出事件
 */
- (void)audioRouteChangeListenerCallback:(NSNotification*)notification
{
    NSDictionary *interuptionDict = notification.userInfo;

    NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];

    switch (routeChangeReason) {

        case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
            // 耳機插入
            break;
        case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
        {
            // 耳機拔掉
            // 拔掉耳機繼續播放
            [self play];
        }

            break;

        case AVAudioSessionRouteChangeReasonCategoryChange:
            // called at start - also when other audio wants to play
            NSLog(@"AVAudioSessionRouteChangeReasonCategoryChange");
            break;
    }
}
設置系統音量

// 0 ... 1.0的數值, 1.0是最大的聲音.
self.volumeViewSlider.value = ...
十一、屏幕亮度相關
// 0 ... 1.0的數值, 1.0是最大的亮度.
[UIScreen mainScreen].brightness -= ...
十二、屏幕旋轉相關
蘋果手機除iPhone 4s(320*480)屏幕寬高比不是16:9外,其餘都爲16:9,因此橫豎屏能夠這樣實現,這裏必須使用autolayout,這裏提供兩種方法實現:

使用Xib或者Storyboard的話,必須把播放器view的寬高比設置成16:9,4s的話能夠單獨適配加約束(使用sizeClasses)
使用masonry,具體代碼以下:
[self.playerView mas_makeConstraints:^(MASConstraintMaker *make) {
      make.top.equalTo(self.view).offset(20);
      make.left.right.equalTo(self.view);
      // 注意此處,寬高比16:9優先級比1000低就行,在由於iPhone 4S寬高比不是16:9
      make.height.equalTo(self.playerView.mas_width).multipliedBy(9.0f/16.0f).with.priority(750);
  }];
關於屏幕旋轉能夠這樣強制讓屏幕轉屏,有人會問了,在我demo中爲啥能轉屏,而集成到本身項目中不能轉屏,我能夠明確的告訴你,是大家項目的橫屏給禁止掉了,你能夠看一下這裏是否打鉤啦:

設備方向
有人又會問了,咱們想實現這麼個需求,只有在播放器頁面支持橫屏,其餘頁面不支持橫屏。好了,那下邊我來告訴怎麼實現,首先上圖中的橫屏必須勾選,其次在你項目window的rootViewController中來實現兩個方法:

// 哪些頁面支持自動轉屏
- (BOOL)shouldAutorotate;
// viewcontroller支持哪些轉屏方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations;
這兩個方法看我註釋你就知道什麼意思啦,這兩個方法會不停的調用,不信你能夠打斷點試試,具體實現你去demo看看吧,在MainViewcontroller中。

下邊來講說強制屏幕旋轉,即便用戶的手機鎖定啦屏幕方法,調用這個方法照樣能夠旋轉:

/**
 *  強制屏幕轉屏
 *
 *  @param orientation 屏幕方向
 */
- (void)interfaceOrientation:(UIInterfaceOrientation)orientation
{
    // arc下
    if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {
        SEL selector             = NSSelectorFromString(@"setOrientation:");
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:selector]];
        [invocation setSelector:selector];
        [invocation setTarget:[UIDevice currentDevice]];
        int val                  = orientation;
        // 從2開始是由於0 1 兩個參數已經被selector和target佔用
        [invocation setArgument:&val atIndex:2];
        [invocation invoke];
    }
    /*
     // 非arc下
     if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {
     [[UIDevice currentDevice] performSelector:@selector(setOrientation:)
     withObject:@(orientation)];
     }

     // 直接調用這個方法通不過apple上架審覈
     [[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIInterfaceOrientationLandscapeRight] forKey:@"orientation"];
     */
}
監聽設備旋轉通知,來處理一些UI顯示問題

 [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
 [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(onDeviceOrientationChange)
                                                 name:UIDeviceOrientationDidChangeNotification
                                               object:nil
];


文/renzifeng(簡書做者)
原文連接:http://www.jianshu.com/p/5566077bb25f
著做權歸做者全部,轉載請聯繫做者得到受權,並標註「簡書做者」。
相關文章
相關標籤/搜索