使用AVPlayer自定義支援全屏的播放器(一)

JmoVxia發表於2018-01-22

使用AVPlayer自定義支援全屏的播放器(一)

前言

最近在專案中,遇到了視訊播放的需求,直接使用系統封裝的播放器太過於簡單,不能很好的滿足專案要求,於是花時間研究了一下,使用AVPlayer來自定義播放器。     本視訊播放器主要自定義了帶緩衝顯示的進度條,可以拖動調節視訊播放進度的播放條,具有當前播放時間和總時間的Label,全屏播放功能,定時消失的工具條。播放器已經封裝到UIView中,支援自動旋轉切換全屏,支援UITableView

主要功能

1.帶緩衝顯示的進度條

在自定義的時候,主要是需要計算當前進度和監聽緩衝的進度,細節方面需要注意進度顏色,進度為0的時候要設定為透明色,緩衝完成的時候需要設定顏色,不然全屏切換就會導致緩衝完成的進度條顏色消失。

  • 自定義進度條的程式碼

#pragma mark - 建立UIProgressView
- (void)createProgress
{
    CGFloat width;
    if (_isFullScreen == NO)
    {
        width = self.frame.size.width;
    }
    else
    {
        width = self.frame.size.height;
    }
    _progress                = [[UIProgressView alloc]init];
    _progress.frame          = CGRectMake(_startButton.right + Padding, 0, width - 80 - Padding - _startButton.right - Padding - Padding, Padding);
    _progress.centerY        = _bottomView.height/2.0;
    //進度條顏色
    _progress.trackTintColor = ProgressColor;
    
    // 計算緩衝進度
    NSTimeInterval timeInterval = [self availableDuration];
    CMTime duration             = _playerItem.duration;
    CGFloat totalDuration       = CMTimeGetSeconds(duration);
    [_progress setProgress:timeInterval / totalDuration animated:NO];
    
    CGFloat time  = round(timeInterval);
    CGFloat total = round(totalDuration);
    
    //確保都是number
    if (isnan(time) == 0 && isnan(total) == 0)
    {
        if (time == total)
        {
            //緩衝進度顏色
            _progress.progressTintColor = ProgressTintColor;
        }
        else
        {
            //緩衝進度顏色
            _progress.progressTintColor = [UIColor clearColor];
        }
    }
    else
    {
        //緩衝進度顏色
        _progress.progressTintColor = [UIColor clearColor];
    }
    [_bottomView addSubview:_progress];
}
複製程式碼
  • 緩衝進度計算和監聽程式碼

#pragma mark - 快取條監聽
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"loadedTimeRanges"])
    {
        // 計算緩衝進度
        NSTimeInterval timeInterval = [self availableDuration];
        CMTime duration             = _playerItem.duration;
        CGFloat totalDuration       = CMTimeGetSeconds(duration);
        [_progress setProgress:timeInterval / totalDuration animated:NO];
        
        //設定快取進度顏色
        _progress.progressTintColor = ProgressTintColor;
    }
}
複製程式碼

2.可以拖動調節視訊播放進度的播放條

這裡主要需要注意的是建立的播放條需要比進度條稍微長一點,這樣才可以看到滑塊從開始到最後走完整個進度條。播放條最好單獨新建一個繼承自UISlider的控制元件,因為進度條和播放條的大小很可能不能完美的重合在一起,這樣看起來就會有2條線條,很不美觀,內部程式碼將其預設長度和起點重新佈局。

  • 播放條控制元件內部程式碼

這裡重寫- (CGRect)trackRectForBounds:(CGRect)bounds方法,才能改變播放條的大小。

// 控制slider的寬和高,這個方法才是真正的改變slider滑道的高的
- (CGRect)trackRectForBounds:(CGRect)bounds
{
    [super trackRectForBounds:bounds];
    return CGRectMake(-2, (self.frame.size.height - 2.6)/2.0, CGRectGetWidth(bounds) + 4, 2.6);
}
複製程式碼
  • 建立播放條程式碼
#pragma mark - 建立UISlider
- (void)createSlider
{
    _slider         = [[Slider alloc]init];
    _slider.frame   = CGRectMake(_progress.x, 0, _progress.width, ViewHeight);
    _slider.centerY = _bottomView.height/2.0;
    [_bottomView addSubview:_slider];
    
    //自定義滑塊大小
    UIImage *image     = [UIImage imageNamed:@"round"];
    //改變滑塊大小
    UIImage *tempImage = [image OriginImage:image scaleToSize:CGSizeMake( SliderSize, SliderSize)];
    //改變滑塊顏色
    UIImage *newImage  = [tempImage imageWithTintColor:SliderColor];
    [_slider setThumbImage:newImage forState:UIControlStateNormal];
    
    //開始拖拽
    [_slider addTarget:self
                action:@selector(processSliderStartDragAction:)
      forControlEvents:UIControlEventTouchDown];
    //拖拽中
    [_slider addTarget:self
                action:@selector(sliderValueChangedAction:)
      forControlEvents:UIControlEventValueChanged];
    //結束拖拽
    [_slider addTarget:self
                action:@selector(processSliderEndDragAction:)
      forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside];
    
    //左邊顏色
    _slider.minimumTrackTintColor = PlayFinishColor;
    //右邊顏色
    _slider.maximumTrackTintColor = [UIColor clearColor];
}
複製程式碼
  • 拖動播放條程式碼

#pragma mark - 拖動進度條
//開始
- (void)processSliderStartDragAction:(UISlider *)slider
{
    //暫停
    [self pausePlay];
    [_timer invalidate];
}
//結束
- (void)processSliderEndDragAction:(UISlider *)slider
{
    //繼續播放
    [self playVideo];
    _timer = [NSTimer scheduledTimerWithTimeInterval:DisappearTime
                                              target:self
                                            selector:@selector(disappear)
                                            userInfo:nil
                                             repeats:NO];
}
//拖拽中
- (void)sliderValueChangedAction:(UISlider *)slider
{
    //計算出拖動的當前秒數
    CGFloat total           = (CGFloat)_playerItem.duration.value / _playerItem.duration.timescale;
    NSInteger dragedSeconds = floorf(total * slider.value);
    //轉換成CMTime才能給player來控制播放進度
    CMTime dragedCMTime     = CMTimeMake(dragedSeconds, 1);
    [_player seekToTime:dragedCMTime];
}
複製程式碼

3.具有當前播放時間和總時間的Label

建立時間顯示Label的時候,我們需要建立一個定時器,每秒執行一下程式碼,來實現動態改變Label上的時間顯示。

  • Label建立程式碼
#pragma mark - 建立播放時間
- (void)createCurrentTimeLabel
{
    _currentTimeLabel           = [[UILabel alloc]init];
    _currentTimeLabel.frame     = CGRectMake(0, 0, 80, Padding);
    _currentTimeLabel.centerY   = _progress.centerY;
    _currentTimeLabel.right     = _backView.right - Padding;
    _currentTimeLabel.textColor = [UIColor whiteColor];
    _currentTimeLabel.font      = [UIFont systemFontOfSize:12];
    _currentTimeLabel.text      = @"00:00/00:00";
    [_bottomView addSubview:_currentTimeLabel];
}
複製程式碼
  • Label上面定時器的定時事件
#pragma mark - 計時器事件
- (void)timeStack
{
    if (_playerItem.duration.timescale != 0)
    {
        //總共時長
        _slider.maximumValue = 1;
        //當前進度
        _slider.value        = CMTimeGetSeconds([_playerItem currentTime]) / (_playerItem.duration.value / _playerItem.duration.timescale);
        //當前時長進度progress
        NSInteger proMin     = (NSInteger)CMTimeGetSeconds([_player currentTime]) / 60;//當前秒
        NSInteger proSec     = (NSInteger)CMTimeGetSeconds([_player currentTime]) % 60;//當前分鐘
        //duration 總時長
        NSInteger durMin     = (NSInteger)_playerItem.duration.value / _playerItem.duration.timescale / 60;//總秒
        NSInteger durSec     = (NSInteger)_playerItem.duration.value / _playerItem.duration.timescale % 60;//總分鐘
        self.currentTimeLabel.text = [NSString stringWithFormat:@"%02ld:%02ld / %02ld:%02ld", (long)proMin, proSec, durMin, durSec];
    }
    //開始播放停止轉子
    if (_player.status == AVPlayerStatusReadyToPlay)
    {
        [_activity stopAnimating];
    }
    else
    {
        [_activity startAnimating];
    }
    
}
複製程式碼

4.全屏播放功能

上面都是一些基本功能,最重要的還是全屏功能的實現。全屏功能這裡多說一下,由於我將播放器封裝到一個UIView裡邊,導致在做全屏的時候出現了一些問題。因為播放器被封裝起來了,全屏的時候,播放器的大小就很可能超出父類控制元件的大小範圍,造成了超出部分點選事件無法獲取,最開始打算重寫父類-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event方法,但是想到這樣做就沒有達到封裝的目的,於是改變了一下思路,在全屏的時候,將播放器新增到Window上,這樣播放器就不會超出父類的範圍大小,小屏的時候將播放器從Window上還原到原有的父類上。

  • 全屏程式碼     全屏的適配採用的是遍歷刪除原有控制元件,重新佈局建立全屏控制元件的方法實現。
#pragma mark - 全屏按鈕響應事件
- (void)maxAction:(UIButton *)button
{
    if (_isFullScreen == NO)
    {
        [self fullScreenWithDirection:Letf];
    }
    else
    {
        [self originalscreen];
    }
}
#pragma mark - 全屏
- (void)fullScreenWithDirection:(Direction)direction
{
    //記錄播放器父類
    _fatherView = self.superview;
    
    _isFullScreen = YES;

    //取消定時消失
    [_timer invalidate];
    [self setStatusBarHidden:YES];
    //新增到Window上
    [self.window addSubview:self];
    
    if (direction == Letf)
    {
        [UIView animateWithDuration:0.25 animations:^{
            self.transform = CGAffineTransformMakeRotation(M_PI / 2);
        }];
    }
    else
    {
        [UIView animateWithDuration:0.25 animations:^{
            self.transform = CGAffineTransformMakeRotation( - M_PI / 2);
        }];
    }
    
    self.frame         = CGRectMake(0, 0, ScreenWidth, ScreenHeight);
    _playerLayer.frame = CGRectMake(0, 0, ScreenHeight, ScreenWidth);
    
    //刪除原有控制元件
    [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
    //建立全屏UI
    [self creatUI];
}
#pragma mark - 原始大小
- (void)originalscreen
{
    _isFullScreen = NO;
    
    //取消定時消失
    [_timer invalidate];
    [self setStatusBarHidden:NO];

    [UIView animateWithDuration:0.25 animations:^{
        //還原大小
        self.transform = CGAffineTransformMakeRotation(0);
    }];
    
    self.frame = _customFarme;
    _playerLayer.frame = CGRectMake(0, 0, _customFarme.size.width, _customFarme.size.height);
    //還原到原有父類上
    [_fatherView addSubview:self];
    
    //刪除
    [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
    //建立小屏UI
    [self creatUI];
}

複製程式碼
  • 建立播放器UI的程式碼
#pragma mark - 建立播放器UI
- (void)creatUI
{
    //最上面的View
    _backView                 = [[UIView alloc]init];
    _backView.frame           = CGRectMake(0, _playerLayer.frame.origin.y, _playerLayer.frame.size.width, _playerLayer.frame.size.height);
    _backView.backgroundColor = [UIColor clearColor];
    [self addSubview:_backView];
    
    //頂部View條
    _topView                 = [[UIView alloc]init];
    _topView.frame           = CGRectMake(0, 0, _backView.width, ViewHeight);
    _topView.backgroundColor = [UIColor colorWithRed:0.00000f green:0.00000f blue:0.00000f alpha:0.50000f];
    [_backView addSubview:_topView];
    
    //底部View條
    _bottomView                 = [[UIView alloc] init];
    _bottomView.frame           = CGRectMake(0, _backView.height - ViewHeight, _backView.width, ViewHeight);
    _bottomView.backgroundColor = [UIColor colorWithRed:0.00000f green:0.00000f blue:0.00000f alpha:0.50000f];
    [_backView addSubview:_bottomView];
    
    // 監聽loadedTimeRanges屬性
    [_playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    
    //建立播放按鈕
    [self createButton];
    //建立進度條
    [self createProgress];
    //建立播放條
    [self createSlider];
    //建立時間Label
    [self createCurrentTimeLabel];
    //建立返回按鈕
    [self createBackButton];
    //建立全屏按鈕
    [self createMaxButton];
    //建立點選手勢
    [self createGesture];
    
    //計時器,迴圈執行
    [NSTimer scheduledTimerWithTimeInterval:1.0f
                                     target:self
                                   selector:@selector(timeStack)
                                   userInfo:nil
                                    repeats:YES];
    //定時器,工具條消失
    _timer = [NSTimer scheduledTimerWithTimeInterval:DisappearTime
                                              target:self
                                            selector:@selector(disappear)
                                            userInfo:nil
                                             repeats:NO];
    
}

複製程式碼

5.定時消失的工具條

如果工具條是顯示狀態,不點選視訊,預設一段時間後,自動隱藏工具條,點選視訊,直接隱藏工具條;如果工具條是隱藏狀態,點選視訊,就讓工具條顯示。功能說起來很簡單,最開始的時候,我使用GCD延遲程式碼實現,但是當點選讓工具條顯示,然後再次點選讓工具條消失,多點幾下你會發現你的定時消失時間不對。這裡我們需要注意的是,當你再次點選的時候需要取消上一次的延遲執行程式碼,才能夠讓下一次點選的時候,延遲程式碼正確執行。這裡採用定時器來實現,因為定時器可以取消延遲執行的程式碼。

  • 點選視訊的程式碼
#pragma mark - 輕拍方法
- (void)tapAction:(UITapGestureRecognizer *)tap
{
    //取消定時消失
    [_timer invalidate];
    if (_backView.alpha == 1)
    {
        [UIView animateWithDuration:0.5 animations:^{
            _backView.alpha = 0;
        }];
    }
    else if (_backView.alpha == 0)
    {
        //新增定時消失
        _timer = [NSTimer scheduledTimerWithTimeInterval:DisappearTime
                                                  target:self
                                                selector:@selector(disappear)
                                                userInfo:nil
                                                 repeats:NO];
        
        [UIView animateWithDuration:0.5 animations:^{
            _backView.alpha = 1;
        }];
    }
}

複製程式碼

介面與用法

這裡是寫給懶人看的,對播放器做了一下簡單的封裝,留了幾個常用的介面,方便使用。

  • 介面

/**視訊url*/
@property (nonatomic,strong) NSURL *url;
/**旋轉自動全屏,預設Yes*/
@property (nonatomic,assign) BOOL autoFullScreen;
/**重複播放,預設No*/
@property (nonatomic,assign) BOOL repeatPlay;
/**是否支援橫屏,預設No*/
@property (nonatomic,assign) BOOL isLandscape;
/**播放*/
- (void)playVideo;
/**暫停*/
- (void)pausePlay;
/**返回按鈕回撥方法*/
- (void)backButton:(BackButtonBlock) backButton;
/**播放完成回撥*/
- (void)endPlay:(EndBolck) end;
/**銷燬播放器*/
- (void)destroyPlayer;
/**
 根據播放器所在位置計算是否滑出螢幕,

 @param tableView Cell所在tableView
 @param cell 播放器所在Cell
 @param beyond 滑出後的回撥
 */
- (void)calculateWith:(UITableView *)tableView cell:(UITableViewCell *)cell beyond:(BeyondBlock) beyond;
複製程式碼
  • 使用方法

直接使用cocoapods匯入,pod 'CLPlayer'

  • 具體使用程式碼
CLPlayerView *playerView = [[CLPlayerView alloc] initWithFrame:CGRectMake(0, 90, ScreenWidth, 300)];
[self.view addSubview:playerView];
//根據旋轉自動支援全屏,預設支援
//    playerView.autoFullScreen = NO;
//重複播放,預設不播放
//    playerView.repeatPlay     = YES;
//如果播放器所在頁面支援橫屏,需要設定為Yes,不支援不需要設定(預設不支援)
//    playerView.isLandscape    = YES;

//視訊地址
playerView.url = [NSURL URLWithString:@"http://wvideo.spriteapp.cn/video/2016/0215/56c1809735217_wpd.mp4"];

//播放
[playerView playVideo];

//返回按鈕點選事件回撥
[playerView backButton:^(UIButton *button) {
    NSLog(@"返回按鈕被點選");
}];

//播放完成回撥
[playerView endPlay:^{
    //銷燬播放器
    [playerView destroyPlayer];
    playerView = nil;
    NSLog(@"播放完成");
}];
複製程式碼

說明

UIImage+TintColor是用來渲染圖片顏色的分類,由於缺少圖片資源,所以採用其他顏色圖片渲染成自己需要的顏色;UIImage+ScaleToSize這個分類是用來改變圖片尺寸大小的,因為播放條中的滑塊不能直接改變大小,所以通過改變圖片尺寸大小來控制滑塊大小;UIView+SetRect是用於適配的分類。

總結

在自定義播放器的時候,需要注意的細節太多,這裡就不一一細說了,更多細節請看Demo,Demo中有很詳細的註釋。考慮到大部分APP不支援橫屏,播放器預設是不支援橫屏的,如果需要支援橫屏(勾選了支援左右方向),建立播放器的時候,寫上這句程式碼playerView.isLandscape    = YES;

使用AVPlayer自定義支援全屏的播放器(一)

播放器效果圖

使用AVPlayer自定義支援全屏的播放器(一)

Demo地址

最近更新修改了很多地方的程式碼,主要是使用Masonry來重構了一下工具條,修復了一些bug,具體還請參考CLPlayer 如果喜歡,歡迎star。

相關文章