作者:shelin(簡書)
本文聊點關於最近寫的這個自定義播放器。支持UITableViewCell上小屏、全屏播放,手動及屏幕旋轉切換,包括右下角的小窗懸停播放,不依賴於視圖控制器和第三方,盡量的讓使用起來更簡單,具體代碼詳情請戳Github,先看看效果如何!
這是基於AVFoundation下自定義的一個播放器,先簡單介紹幾個用到的類。
介紹:
AVPlayer:可以理解為播放器對象,靈活性好,可以高度化的自定義UI,但它本身不能顯示視頻,顯示需要另一個類AVPlayerLayer來顯示,繼承於CALayer,下面是摘自官方的一段介紹:
AVPlayer works equally well with local and remote media files.
You can display the visual content of items played by an instance of AVPlayer in a CoreAnimation layer of class AVPlayerLayer.
You can observe the status of a player using key-value observing.
主要是說它支持本地/網絡媒體播放,需要CoreAnimation下的AVPlayerLayer來顯示視頻,我們可以通過KVO監聽player的播放狀態。
AVPlayerItem:存有相關媒體信息的類,一個視頻資源對應一個AVPlayerItem對象,當你需要循環播放多個視頻資源時也需創建多個AVPlayerItem對象。建議大家可以多看看官方的英文文檔解釋(題外話)。
An AVPlayerItem represents the presentation state of an asset that’s played by an AVPlayer object, and lets you observe that state.
AVAsset:主要用於獲取多媒體信息,可以理解為一個抽象類,不能直接使用,操作針對它的子類AVURLAsset,根據你視頻的url創建一個包含視頻媒體信息的AVURLAsset對象。
CMTime:還會用到這個媒體時間相關的類,如有不明白可以看之前一個帖子的解釋。
層級關系:
基於以上幾個類就能實現視頻的基本功能了,例如暫停、播放,快進、後退、顯示播放/緩沖進度。然後UI層面,層級很簡單,XLVideoPlayer繼承於UIView,上面我們說到顯示視頻需要AVPlayerLayer,我們將AVPlayerLayer加到view的layer上。
下面貼出主要的代碼,初始化AVPlayer對象
- (AVPlayerLayer *)playerLayer { if (!_playerLayer) { _playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player]; _playerLayer.backgroundColor = kPlayerBackgroundColor; _playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;//視頻填充模式 } return _playerLayer; } - (AVPlayer *)player{ if (!_player) { AVPlayerItem *playerItem = [self getAVPlayItem]; self.playerItem = playerItem; _player = [AVPlayer playerWithPlayerItem:playerItem]; [self addProgressObserver]; [self addObserverToPlayerItem:playerItem]; } return _player; } //initialize AVPlayerItem - (AVPlayerItem *)getAVPlayItem{ NSAssert(self.videoUrl != nil, @"必須先傳入視頻url!!!"); if ([self.videoUrl rangeOfString:@"http"].location != NSNotFound) { AVPlayerItem *playerItem=[AVPlayerItem playerItemWithURL:[NSURL URLWithString:[self.videoUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]]; return playerItem; }else{ AVAsset *movieAsset = [[AVURLAsset alloc]initWithURL:[NSURL fileURLWithPath:self.videoUrl] options:nil]; AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:movieAsset]; return playerItem; } }
同時我們注冊KVO,監控視頻播放過程,這可以獲取視頻的播放進度。AVPlayer有一個屬性currentItem是AVPlayerItem類型,表示當前播放的視頻對象。
#pragma mark - monitor video playing course -(void)addProgressObserver{ //get current playerItem object AVPlayerItem *playerItem = self.player.currentItem; __weak typeof(self) weakSelf = self; //Set once per second [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { float current = CMTimeGetSeconds(time); float total = CMTimeGetSeconds([playerItem duration]); weakSelf.progressLabel.text = [weakSelf timeFormatted:current]; if (current) { // NSLog(@"%f", current / total); weakSelf.slider.value = current / total; if (weakSelf.slider.value == 1) { //complete block if (weakSelf.completedPlayingBlock) { weakSelf.completedPlayingBlock(weakSelf); }else { //finish and loop playback weakSelf.playOrPauseBtn.selected = NO; [weakSelf showOrHidenBar]; CMTime currentCMTime = CMTimeMake(0, 1); [weakSelf.player seekToTime:currentCMTime completionHandler:^(BOOL finished) { weakSelf.slider.value = 0.0f; }]; } } } }]; }
以及監聽AVPlayerItem對象的status/loadedTimeRanges屬性變化,status對應播放狀態,loadedTimeRanges網絡緩沖狀態,當loadedTimeRanges的改變時,每緩沖一部分數據就會更新此屬性,可以獲得本次緩沖加載的視頻范圍(包含起始時間、本次網絡加載時長)
#pragma mark - PlayerItem (status,loadedTimeRanges) -(void)addObserverToPlayerItem:(AVPlayerItem *)playerItem{ //監控狀態屬性,注意AVPlayer也有一個status屬性,通過監控它的status也可以獲得播放狀態 [playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil]; //network loading progress [playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil]; }
在這獲取視頻的總時長,網絡的視頻緩沖進度,做相應的顯示。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{ AVPlayerItem *playerItem = object; if ([keyPath isEqualToString:@"status"]) { AVPlayerStatus status = [[change objectForKey:@"new"] intValue]; if(status == AVPlayerStatusReadyToPlay){ self.totalDuration = CMTimeGetSeconds(playerItem.duration); self.totalDurationLabel.text = [self timeFormatted:self.totalDuration]; } }else if([keyPath isEqualToString:@"loadedTimeRanges"]){ NSArray *array = playerItem.loadedTimeRanges; CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];//本次緩沖時間范圍 float startSeconds = CMTimeGetSeconds(timeRange.start); float durationSeconds = CMTimeGetSeconds(timeRange.duration); NSTimeInterval totalBuffer = startSeconds + durationSeconds;//緩沖總長度 self.slider.middleValue = totalBuffer / CMTimeGetSeconds(playerItem.duration); // NSLog(@"totalBuffer:%.2f",totalBuffer); //remove loading animation if (self.slider.middleValue <= self.slider.value) { self.activityIndicatorView.center = self.center; [self addSubview:self.activityIndicatorView]; [self.activityIndicatorView startAnimating]; }else { [self.activityIndicatorView removeFromSuperview]; } } }
下面這部分是定位視頻的某個位置播放,也就是快進後退。
這裡需要注意的是在用戶拖拽slider的過程中需要先暫停,否則手動改變進度和播放的進度會有沖突,用戶拖拽完畢再去播放視頻。
- (void)finishChange { _inOperation = NO; [self hiden]; CMTime currentCMTime = CMTimeMake(self.slider.value * self.totalDuration, 1); [self.player seekToTime:currentCMTime completionHandler:^(BOOL finished) { [self.player play]; self.playOrPauseBtn.selected = YES; }]; }
關於屏幕旋轉
這部分還是遇到一些坑,可以看到並沒有在plist文件設置工程支持橫屏,所有都是通過強制旋轉屏幕實現,在用戶旋轉屏幕的通知或者點擊事件中調用強制旋轉的代碼。會發現當你旋轉屏幕時,其實UITableView和其他控件是不會隨屏幕一起旋轉的,強制旋轉涉及到iOS8+和之前的系統的問題,當我們調用之前的時,在iOS7和iOS8+的效果是不一樣的,我從網上摘了來兩個圖。
[[UIApplication sharedApplication] setStatusOrientation:XX]
第一張圖iOS 7的,第二張圖是iOS 8+,很明顯我們發現iOS7當你調用這個方法UIscreen和UIWindow一起轉過來了,而iOS8後UIScreen並沒有轉過來,這樣就會導致調用這個方法在iOS8+會存在部分區域點擊無響應,因為它超出UIScreen的那部分范圍,而且我在測試過程中還發現用這種方法旋轉在點擊Home鍵再次進入程序會導致屏幕錯位。
怎麼辦呢!後面又找到這個方法:
[[UIDevice currentDevice]setOrientation:UIInterfaceOrientationPortrait];
但是現在蘋果已經將該方法私有化了,直接pass掉。之後在stackoverflow做了些嘗試,找到現在用的這個方法,它並沒有把系統的status bar旋轉過來。
NSNumber *value = [NSNumber numberWithInt:UIInterfaceOrientationLandscapeRight]; [[UIDevice currentDevice] setValue:value forKey:@"orientation"];
之後還查了一些相關的東西,有興趣大家可以看看:
詳解UICoordinateSpace和UIScreen在iOS 8上的坐標問題
屏幕旋轉學習筆記
寫一個播放器還需要注意很多細節,只能根據需求一步步的完善,這裡只能說一些需要關注的點。如果大家覺得不錯希望可以在點擊右上角Star,謝謝支持!