AVPlayer封装

说明

基于AVPlayer和MVP模式封装的一个视频播放控制器,支持全屏,暂停播放,进度条拖动。

Demo地址

AVPlayer框架介绍

AVPlay既可以用来播放音频也可以用来播放视频,AVPlay在播放音频方面可以直接用来播放网络上的音频。在使用AVPlay的时候我们需要导入AVFoundation.framework框架,再引入头文件#import<AVFoundation/AVFoundation.h>。

主要包括下面几个类

1.AVPlayer:播放器类
2.AVPlayerItem:播放单元类,即一个播放源
3.AVPlayerLayer:播放界面

使用时,需要先根据NSURL生成一个播放源,[AVPlayerItem playerItemWithURL:],再根据这个播放源获得一个播放器对象,[AVPlayer playerWithPlayerItem:];,此时播放器已经准备完成,但还需要根据AVPlayer生成一个AVPlayerLayer,设置frame,再加入到superView.layer中,[AVPlayerLayer playerLayerWithPlayer:]; self.playerLayer.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.width*0.6); [self.layer addSublayer:self.playerLayer];

此时一个简单的播放器就已经配置完成。

暂停播放

AVPlayer有一个rate属性,可以根据这个属性来判断当前是否在播放,rate == 0.f为暂停,反之视频播放。

AVPlayerItemStatus

可以对AVPlayerItem设置kvo,监听视频源是否可播放,系统给了三种状态,如下:

typedef NS_ENUM(NSInteger, AVPlayerItemStatus) {
    AVPlayerItemStatusUnknown,
    AVPlayerItemStatusReadyToPlay,
    AVPlayerItemStatusFailed
};

设置KVO监听:

[self.playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if ([keyPath isEqualToString:@"status"]) {
        AVPlayerItemStatus status = [change[NSKeyValueChangeNewKey] intValue];
        if (status == AVPlayerItemStatusReadyToPlay) {
            isReadyToPlay = YES;
            [self.player play];
        }else{
            //预留
            isReadyToPlay = NO;
        }
        [self.controlView controlItemStatus:status playItem:object];
    }
}
全屏操作

Demo中给出的思路是:

1.首先将当前竖屏状态下的播放器的view的frame保存下来,方便退出全屏时,布局;
2.然后新建一个全屏展示View的控制器,重写该控制器的@property(nonatomic, readonly) UIInterfaceOrientation preferredInterfaceOrientationForPresentation NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;,强制让该控制器旋转;
3.将当前根控制器present到上述的全屏控制器,在completion:回调中,做个简单的动画过渡一下,然后再将承载AVPlayerLayer的view的frame改成横屏状态,然后再修改AVPlayerLayer的frame;

退出全屏:

1.将当前全屏控制器dismiss;
2.再dismiss的成功回调中,设置View的frame为进入全屏前保存的frame;
3.再将AVPlayerLayer的frame修改。

代码如下:

#pragma mark - 进入全屏和退出全屏的动画和present处理
- (void)enterFullScreen:(BOOL)rightOrLeft{
    playViewBeforeRect = _playerView.frame;
    playViewBeforeCenter = _playerView.center;
    
    TBZAVFullViewController *vc = [[TBZAVFullViewController alloc] init];
    vc.type = rightOrLeft;
    self.fullVC = vc;
    
    __weak TBZAVPlayerViewController *weakSelf = self;
    
    [self.navigationController presentViewController:vc animated:false completion:^{
        [UIView animateWithDuration:0.25 animations:^{
            weakSelf.playerView.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height);
        } completion:^(BOOL finished) {
            [weakSelf.playerView enterFull];
            [weakSelf.fullVC.view addSubview:weakSelf.playerView];
            [UIApplication.sharedApplication.keyWindow insertSubview:UIApplication.sharedApplication.keyWindow.rootViewController.view belowSubview:vc.view.superview];
            
            self->isFull = YES;
        }];
    }];
}

- (void)exitFullScreen{
    __weak TBZAVPlayerViewController *weakSelf = self;
    [self.fullVC dismissViewControllerAnimated:false completion:^{
        [UIView animateWithDuration:0.25 animations:^{
            weakSelf.playerView.frame = self->playViewBeforeRect;
        } completion:^(BOOL finished) {
            [weakSelf.playerView exitFull];
            [weakSelf.view addSubview:weakSelf.playerView];
            
            self->isFull = NO;
        }];
    }];
}
播放进度

主要就是需要对AVPlayer添加监听,且注意需要释放该方法返回的对象。AVPlayerItem有两个属性,currentTime和duration,这两个对象都是CMTime类,可以用CMTimeGetSeconds(CMTime t);得到一个float指,秒数。也就是CMTimeGetSeconds(item.currentTime)可以得到当前播放到第几秒,CMTimeGetSeconds(item.duration)可以得到当前视频的总时长。

/*!
    @method         addPeriodicTimeObserverForInterval:queue:usingBlock:
    @abstract       Requests invocation of a block during playback to report changing time.
    @param          interval
      The interval of invocation of the block during normal playback, according to progress of the current time of the player.
    @param          queue
      The serial queue onto which block should be enqueued.  If you pass NULL, the main queue (obtained using dispatch_get_main_queue()) will be used.  Passing a
      concurrent queue to this method will result in undefined behavior.
    @param          block
      The block to be invoked periodically.
    @result
      An object conforming to the NSObject protocol.  You must retain this returned value as long as you want the time observer to be invoked by the player.
      Pass this object to -removeTimeObserver: to cancel time observation.
    @discussion     The block is invoked periodically at the interval specified, interpreted according to the timeline of the current item.
                    The block is also invoked whenever time jumps and whenever playback starts or stops.
                    If the interval corresponds to a very short interval in real time, the player may invoke the block less frequently
                    than requested. Even so, the player will invoke the block sufficiently often for the client to update indications
                    of the current time appropriately in its end-user interface.
                    Each call to -addPeriodicTimeObserverForInterval:queue:usingBlock: should be paired with a corresponding call to -removeTimeObserver:.
                    Releasing the observer object without a call to -removeTimeObserver: will result in undefined behavior.
*/
- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;

/*!
    @method         removeTimeObserver:
    @abstract       Cancels a previously registered time observer.
    @param          observer
      An object returned by a previous call to -addPeriodicTimeObserverForInterval:queue:usingBlock: or -addBoundaryTimeObserverForTimes:queue:usingBlock:.
    @discussion     Upon return, the caller is guaranteed that no new time observer blocks will begin executing.  Depending on the calling thread and the queue
                    used to add the time observer, an in-flight block may continue to execute after this method returns.  You can guarantee synchronous time 
                    observer removal by enqueuing the call to -removeTimeObserver: on that queue.  Alternatively, call dispatch_sync(queue, ^{}) after
                    -removeTimeObserver: to wait for any in-flight blocks to finish executing.
                    -removeTimeObserver: should be used to explicitly cancel each time observer added using -addPeriodicTimeObserverForInterval:queue:usingBlock:
                    and -addBoundaryTimeObserverForTimes:queue:usingBlock:.
*/
- (void)removeTimeObserver:(id)observer;

- (id)addPeriodicTimeObserverForInterval:(CMTime)interval queue:(nullable dispatch_queue_t)queue usingBlock:(void (^)(CMTime time))block;其实就是一个Timer,每隔1秒执行block,可以设置常驻子线程,如果设为NULL,就是在主线程。
主要使用如下:

    __weak AVPlayer *weakAVPlayer = self.player;
    __weak TBZAVPlayerView *weakSelf = self;
    //监听播放进度,需要再destory方法中,释放timeObserve
    self.timeObserver = [self.player addPeriodicTimeObserverForInterval:CMTimeMakeWithSeconds(1, NSEC_PER_SEC) queue:NULL usingBlock:^(CMTime time) {
        CGFloat progress = CMTimeGetSeconds(weakAVPlayer.currentItem.currentTime) / CMTimeGetSeconds(weakAVPlayer.currentItem.duration);
        if (progress == 1.0f) {
            //视频播放完毕
            if (weakSelf.delegate && [weakSelf.delegate respondsToSelector:@selector(playEnd)]) {
                [weakSelf.delegate playEnd];
            }
        }else{
            [weakSelf.controlView controlPlayItem:weakAVPlayer.currentItem];
        }
    }];

- (void)destroy{
    if (self.player || self.playerItem || self.playerLayer) {
        [self.player pause];
        if (self.timeObserver) {
            [self.player removeTimeObserver:self.timeObserver];
        }
        [self.playerItem removeObserver:self forKeyPath:@"status"];
        self.playerItem = nil;
        self.player = nil;
        [self.playerLayer removeFromSuperlayer];
    }
}

总结

1.当视频源切换了之后,需要将当前视频源添加的监听都remove掉,重新给新的视频源添加监听;
2.全屏跟退出全屏,主要是注意AVPlayerLayer的布局,不会跟着superLayer的变动而变动,需要手动再设置一遍;

具体可以结合Demo来看。

Demo下载


觉得有用,请帮忙点亮红心


Better Late Than Never!
努力是为了当机会来临时不会错失机会。
共勉!

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