視音訊播放

weixin_34019929發表於2018-02-28

視音訊播放可以說是視訊開發中比較簡單的了,只要建立幾個物件,監聽一些屬性值即可完成一個視音訊播放器。

我們需要了解下面幾個類:

  • AVPlayer
  • AVPlayerLayer
  • AVPlayerItem

AVPlayer是播放器物件,用來控制視音訊的播放和暫停,還可以用來跳轉進度;
AVPlayerLayers用於展示視訊,AVPlayer需要在AVPlayerLayer上才能展示;
AVPlayerItem是資源管理器,用於載入資源;

播放器的建立

- (void)initializedPlayerWith:(NSURL *)url {
    AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:url];    //資源管理器
    
    if (_player.currentItem != nil) {
        [self removeObserverFromPlayerItem:_player.currentItem];    //移除之前的監聽
        [_player replaceCurrentItemWithPlayerItem:playerItem];  //切換資源
        [self pause];
    }else {
        _player = [AVPlayer playerWithPlayerItem:playerItem];
    }
    
    [_playerLayer removeFromSuperlayer];
    _playerLayer = nil;
    
    if (_containerView != nil) {
        _playerLayer = [AVPlayerLayer playerLayerWithPlayer:_player];
        _playerLayer.videoGravity = _videoGravity;    //視訊填充模式
        _playerLayer.frame = _containerView.bounds;
        [_containerView.layer addSublayer:_playerLayer];
    }
    
    //監聽播放器
    [self addObserverFromPlayerItem:playerItem];
    [self addProgressNotification];
    [self addPlayerFinishNotification];
}

當AVPlayerLayer父檢視的frame發生了變化,記得及時修改AVPlayerLayer的frame
其他的程式碼很簡單,關於監聽的函式下面會講

緩衝、播放、暫停和跳轉進度

緩衝
在播放器建立之後,會自動進行緩衝

//快取
- (void)buffer {
    if (_playerLayer == nil && self.fileUrl) {
        [self initializedPlayerWith:self.fileUrl];
    }
}

播放

- (void)play {
    [self buffer];
    if (_player && _player.rate == 0) {
        [_player play];
    }
}

暫停

- (void)pause {
    if (_player && _player.rate != 0) {
        [_player pause];
    }
}

跳轉進度
這裡value取值範圍是0-1,
說明一點,被註釋掉的跳轉函式seekToTime只能跳轉大於1秒的進度,要實現精確跳轉,使用後面那個

- (void)jumpProgressWith:(CGFloat)value {
    if (_playerLayer == nil) return;
    CMTime time = CMTimeMakeWithSeconds(value * CMTimeGetSeconds(_player.currentItem.duration), _player.currentItem.currentTime.timescale);
    [self jumpProgressWithTime:time];
}

- (void)jumpProgressWithTime:(CMTime)time {
    if (_playerLayer == nil) return;
//    [_player seekToTime:time];    //該方法無法進行精確的跳轉,只能跳轉大於1秒後的進度
    [self.player seekToTime:time toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero];   //這樣才可以進行1秒內的精確跳轉
}

視訊填充模式

  • AVLayerVideoGravityResizeAspect 預設,按視訊比例顯示,直到寬或高佔滿,未達到的地方顯示父檢視
  • AVLayerVideoGravityResizeAspectFill 按原比例顯示視訊,直到兩邊螢幕佔滿,但視訊部分內容可能無法顯示
  • AVLayerVideoGravityResize 按父檢視尺寸顯示,可能與原視訊比例不同
AVPlayerLayer.videoGravity

播放器狀態監聽

在建立播放器物件後,通過監聽資源管理器AVPlayerItem的屬性,可以得知視音訊的總長度緩衝進度

//開始監聽
- (void)addObserverFromPlayerItem:(AVPlayerItem *)playerItem {
    [playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];    //開始或暫停
    [playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];  //快取進度
}
//監聽回撥
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    AVPlayerItem *playerItem = object;
    if ([keyPath isEqualToString:@"status"]) {
        AVPlayerStatus status = [[change valueForKey:@"new"] integerValue];
        if (status ==AVPlayerStatusReadyToPlay) {
            CGFloat totalTime = CMTimeGetSeconds(playerItem.duration);
            NSLog(@"正在播放,視訊總長度為 %.2f",totalTime);
        }
    }else if ([keyPath isEqualToString:@"loadedTimeRanges"]) {
        NSArray *array = playerItem.loadedTimeRanges;
        //本次緩衝時間範圍
        CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];
        CGFloat startSecond = CMTimeGetSeconds(timeRange.start);
        CGFloat durationSecond = CMTimeGetSeconds(timeRange.duration);
        CGFloat totalTime = CMTimeGetSeconds(playerItem.duration);
        //緩衝總長度
        NSTimeInterval totalBuffer = startSecond + durationSecond;
        CGFloat bufferProgress = totalBuffer / totalTime;
        if (_bufferBlock) {
            _bufferBlock(bufferProgress);
        }
    }
}
//別忘了移除監聽
- (void)removeObserverFromPlayerItem:(AVPlayerItem *)playerItem {
    [playerItem removeObserver:self forKeyPath:@"status"];
    [playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
}

播放進度

//監聽播放進度
- (void)addProgressNotification {
    AVPlayerItem *playerItem = _player.currentItem;
    if (playerItem == nil) return;
    
    id playProgressObserver; //播放進度監聽物件
    
    //先移除上一個視訊的監聽
    if (playProgressObserver) {
        [_player removeTimeObserver:playProgressObserver];
    }
    
    //每秒監聽一次播放進度
    __weak typeof(self) weekSelf = self;
    /*
     CMTimeMake(value,timeScale):
     value表示第幾幀,timeScale表示幀率,即每秒多少幀
     CMTimeMake(1,10):第一幀,幀率為每秒10幀,轉換為時間公式:value/timeScale,即1/10=0.1,表示在視訊的0.1秒時刻
     CMTimeMakeWithSeconds的第一個引數可以使float,其他都一樣,不過因為這個比較好用,所以我一般用這個
     */
    playProgressObserver = [_player addPeriodicTimeObserverForInterval:CMTimeMake(playerProgressTime, playerProgressTime) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
        CGFloat currentTime = CMTimeGetSeconds(time);
        CGFloat totalTime = CMTimeGetSeconds(playerItem.duration);
        
        if (currentTime) {
            CGFloat playProgress = currentTime / totalTime;
            if (weekSelf.progressBlock) {
                weekSelf.progressBlock(playProgress,currentTime,totalTime);
            }
        }
    }];
}

關於CMTime的使用建議先去百度瞭解一下,對視訊開發的理解會很有好處

播放完成回撥

- (void)addPlayerFinishNotification {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playFinishNotification) name:AVPlayerItemDidPlayToEndTimeNotification object:_player.currentItem];
}

現在我們完成了視音訊的播放,使用本地/網路資源去試試吧。

GitHub連結

相關文章