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

前言

最近在項目中,遇到了視頻播放的需求,直接使用系統(tǒng)封裝的播放器太過于簡單,不能很好的滿足項目要求,于是花時間研究了一下,使用AVPlayer來自定義播放器。
? ? 本視頻播放器主要自定義了帶緩沖顯示的進(jìn)度條,可以拖動調(diào)節(jié)視頻播放進(jìn)度的播放條,具有當(dāng)前播放時間和總時間的Label,全屏播放功能,定時消失的工具條。播放器已經(jīng)封裝到UIView中,支持自動旋轉(zhuǎn)切換全屏,支持UITableView。

主要功能

1.帶緩沖顯示的進(jìn)度條

在自定義的時候,主要是需要計算當(dāng)前進(jìn)度和監(jiān)聽緩沖的進(jìn)度,細(xì)節(jié)方面需要注意進(jìn)度顏色,進(jìn)度為0的時候要設(shè)置為透明色,緩沖完成的時候需要設(shè)置顏色,不然全屏切換就會導(dǎo)致緩沖完成的進(jìn)度條顏色消失。

  • 自定義進(jìn)度條的代碼

#pragma mark - 創(chuàng)建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;
    //進(jìn)度條顏色
    _progress.trackTintColor = ProgressColor;
    
    // 計算緩沖進(jìn)度
    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)
        {
            //緩沖進(jìn)度顏色
            _progress.progressTintColor = ProgressTintColor;
        }
        else
        {
            //緩沖進(jìn)度顏色
            _progress.progressTintColor = [UIColor clearColor];
        }
    }
    else
    {
        //緩沖進(jìn)度顏色
        _progress.progressTintColor = [UIColor clearColor];
    }
    [_bottomView addSubview:_progress];
}
  • 緩沖進(jìn)度計算和監(jiān)聽代碼

#pragma mark - 緩存條監(jiān)聽
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"loadedTimeRanges"])
    {
        // 計算緩沖進(jìn)度
        NSTimeInterval timeInterval = [self availableDuration];
        CMTime duration             = _playerItem.duration;
        CGFloat totalDuration       = CMTimeGetSeconds(duration);
        [_progress setProgress:timeInterval / totalDuration animated:NO];
        
        //設(shè)置緩存進(jìn)度顏色
        _progress.progressTintColor = ProgressTintColor;
    }
}

2.可以拖動調(diào)節(jié)視頻播放進(jìn)度的播放條

這里主要需要注意的是創(chuàng)建的播放條需要比進(jìn)度條稍微長一點(diǎn),這樣才可以看到滑塊從開始到最后走完整個進(jìn)度條。播放條最好單獨(dú)新建一個繼承自UISlider的控件,因為進(jìn)度條和播放條的大小很可能不能完美的重合在一起,這樣看起來就會有2條線條,很不美觀,內(nèi)部代碼將其默認(rèn)長度和起點(diǎn)重新布局。

  • 播放條控件內(nèi)部代碼
    ? ? 這里重寫- (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);
}
  • 創(chuàng)建播放條代碼
#pragma mark - 創(chuàng)建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];
    //結(jié)束拖拽
    [_slider addTarget:self
                action:@selector(processSliderEndDragAction:)
      forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside];
    
    //左邊顏色
    _slider.minimumTrackTintColor = PlayFinishColor;
    //右邊顏色
    _slider.maximumTrackTintColor = [UIColor clearColor];
}
  • 拖動播放條代碼

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

3.具有當(dāng)前播放時間和總時間的Label

創(chuàng)建時間顯示Label的時候,我們需要創(chuàng)建一個定時器,每秒執(zhí)行一下代碼,來實現(xiàn)動態(tài)改變Label上的時間顯示。

  • Label創(chuàng)建代碼
#pragma mark - 創(chuàng)建播放時間
- (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;
        //當(dāng)前進(jìn)度
        _slider.value        = CMTimeGetSeconds([_playerItem currentTime]) / (_playerItem.duration.value / _playerItem.duration.timescale);
        //當(dāng)前時長進(jìn)度progress
        NSInteger proMin     = (NSInteger)CMTimeGetSeconds([_player currentTime]) / 60;//當(dāng)前秒
        NSInteger proSec     = (NSInteger)CMTimeGetSeconds([_player currentTime]) % 60;//當(dāng)前分鐘
        //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];
    }
    //開始播放停止轉(zhuǎn)子
    if (_player.status == AVPlayerStatusReadyToPlay)
    {
        [_activity stopAnimating];
    }
    else
    {
        [_activity startAnimating];
    }
    
}

4.全屏播放功能

上面都是一些基本功能,最重要的還是全屏功能的實現(xiàn)。全屏功能這里多說一下,由于我將播放器封裝到一個UIView里邊,導(dǎo)致在做全屏的時候出現(xiàn)了一些問題。因為播放器被封裝起來了,全屏的時候,播放器的大小就很可能超出父類控件的大小范圍,造成了超出部分點(diǎn)擊事件無法獲取,最開始打算重寫父類-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event方法,但是想到這樣做就沒有達(dá)到封裝的目的,于是改變了一下思路,在全屏的時候,將播放器添加到Window上,這樣播放器就不會超出父類的范圍大小,小屏的時候?qū)⒉シ牌鲝?code>Window上還原到原有的父類上。

  • 全屏代碼
    ? ? 全屏的適配采用的是遍歷刪除原有控件,重新布局創(chuàng)建全屏控件的方法實現(xiàn)。
#pragma mark - 全屏按鈕響應(yīng)事件
- (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)];
    //創(chuàng)建全屏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)];
    //創(chuàng)建小屏UI
    [self creatUI];
}

  • 創(chuàng)建播放器UI的代碼
#pragma mark - 創(chuàng)建播放器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];
    
    // 監(jiān)聽loadedTimeRanges屬性
    [_playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    
    //創(chuàng)建播放按鈕
    [self createButton];
    //創(chuàng)建進(jìn)度條
    [self createProgress];
    //創(chuàng)建播放條
    [self createSlider];
    //創(chuàng)建時間Label
    [self createCurrentTimeLabel];
    //創(chuàng)建返回按鈕
    [self createBackButton];
    //創(chuàng)建全屏按鈕
    [self createMaxButton];
    //創(chuàng)建點(diǎn)擊手勢
    [self createGesture];
    
    //計時器,循環(huán)執(zhí)行
    [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.定時消失的工具條

如果工具條是顯示狀態(tài),不點(diǎn)擊視頻,默認(rèn)一段時間后,自動隱藏工具條,點(diǎn)擊視頻,直接隱藏工具條;如果工具條是隱藏狀態(tài),點(diǎn)擊視頻,就讓工具條顯示。功能說起來很簡單,最開始的時候,我使用GCD延遲代碼實現(xiàn),但是當(dāng)點(diǎn)擊讓工具條顯示,然后再次點(diǎn)擊讓工具條消失,多點(diǎn)幾下你會發(fā)現(xiàn)你的定時消失時間不對。這里我們需要注意的是,當(dāng)你再次點(diǎn)擊的時候需要取消上一次的延遲執(zhí)行代碼,才能夠讓下一次點(diǎn)擊的時候,延遲代碼正確執(zhí)行。這里采用定時器來實現(xiàn),因為定時器可以取消延遲執(zhí)行的代碼。

  • 點(diǎn)擊視頻的代碼
#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;
/**旋轉(zhuǎn)自動全屏,默認(rèn)Yes*/
@property (nonatomic,assign) BOOL autoFullScreen;
/**重復(fù)播放,默認(rèn)No*/
@property (nonatomic,assign) BOOL repeatPlay;
/**是否支持橫屏,默認(rèn)No*/
@property (nonatomic,assign) BOOL isLandscape;
/**播放*/
- (void)playVideo;
/**暫停*/
- (void)pausePlay;
/**返回按鈕回調(diào)方法*/
- (void)backButton:(BackButtonBlock) backButton;
/**播放完成回調(diào)*/
- (void)endPlay:(EndBolck) end;
/**銷毀播放器*/
- (void)destroyPlayer;
/**
 根據(jù)播放器所在位置計算是否滑出屏幕,

 @param tableView Cell所在tableView
 @param cell 播放器所在Cell
 @param beyond 滑出后的回調(diào)
 */
- (void)calculateWith:(UITableView *)tableView cell:(UITableViewCell *)cell beyond:(BeyondBlock) beyond;
  • 使用方法

直接使用cocoapods導(dǎo)入,pod 'CLPlayer'

  • 具體使用代碼
CLPlayerView *playerView = [[CLPlayerView alloc] initWithFrame:CGRectMake(0, 90, ScreenWidth, 300)];
[self.view addSubview:playerView];
//根據(jù)旋轉(zhuǎn)自動支持全屏,默認(rèn)支持
//    playerView.autoFullScreen = NO;
//重復(fù)播放,默認(rèn)不播放
//    playerView.repeatPlay     = YES;
//如果播放器所在頁面支持橫屏,需要設(shè)置為Yes,不支持不需要設(shè)置(默認(rèn)不支持)
//    playerView.isLandscape    = YES;

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

//播放
[playerView playVideo];

//返回按鈕點(diǎn)擊事件回調(diào)
[playerView backButton:^(UIButton *button) {
    NSLog(@"返回按鈕被點(diǎn)擊");
}];

//播放完成回調(diào)
[playerView endPlay:^{
    //銷毀播放器
    [playerView destroyPlayer];
    playerView = nil;
    NSLog(@"播放完成");
}];

說明

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

總結(jié)

在自定義播放器的時候,需要注意的細(xì)節(jié)太多,這里就不一一細(xì)說了,更多細(xì)節(jié)請看Demo,Demo中有很詳細(xì)的注釋。考慮到大部分APP不支持橫屏,播放器默認(rèn)是不支持橫屏的,如果需要支持橫屏(勾選了支持左右方向),創(chuàng)建播放器的時候,寫上這句代碼playerView.isLandscape = YES;。

播放器效果圖

Demo地址

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,359評論 25 708
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,245評論 4 61
  • 挽歸曲 『原創(chuàng):阿斯先生』 『獨(dú)白』:陌上花開,可緩緩歸矣。 清風(fēng)酒,明月下,輕彈琵琶笑吟花。...
    怕生厭喜閱讀 211評論 2 3
  • 一覺醒來,熱水器居然“罷工”了,這讓我很是惱火。因為就我這頭發(fā),睡了一晚,必定如刺猬般亂七八糟,這形象,不洗澡不洗...
    coffeelooker閱讀 509評論 0 0