使用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;

播放器效果图

Demo地址

最近更新修改了很多地方的代码,主要是使用Masonry来重构了一下工具条,修复了一些bug,具体还请参考CLPlayer
如果喜欢,欢迎star。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,273评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,905评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,281评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,817评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,690评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,491评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,886评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,513评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,810评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,839评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,642评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,455评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,901评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,091评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,381评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,895评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,104评论 2 338

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,176评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,964评论 4 60
  • 挽归曲 『原创:阿斯先生』 『独白』:陌上花开,可缓缓归矣。 清风酒,明月下,轻弹琵琶笑吟花。...
    怕生厌喜阅读 186评论 2 3
  • 一觉醒来,热水器居然“罢工”了,这让我很是恼火。因为就我这头发,睡了一晚,必定如刺猬般乱七八糟,这形象,不洗澡不洗...
    coffeelooker阅读 484评论 0 0