用AVFoundation做一个视频播放器(二)

  本文主要是对视频播放器项目的介绍。本文主要介绍视频控制和多个视频的切换以及全屏播放。

1.播放控制

  播放控制,主要的难点在于处理AVPlayerLayer的层级问题。为了保证AVPlayerLayer永远在控制层下面,我们可以把控制层(控制层是用来控制视频的暂停、播放、播放进度和缓冲进度等)加入AVPlayerLayer,作为AVPlayerLayer的子视图。但是AVPlayerLayer是CALayer,不能进行用户交互。所以我们要自定义UIView,修改他的layer,作为显示视图。
  制作完的效果如下:


视频播放.PNG

  View一共有四层,controlView -> presentView -> containerView -> controller.view,。controlView在最上,上文介绍用途了;presentView是用来展示视频的,它的layer就是AVPlayerLayer;containerView作为controlView和presentView的父视图。

@interface FHPlayerPresentView : UIView

@property (nonatomic, strong) AVPlayer *player;
/// default is AVLayerVideoGravityResizeAspect.
@property (nonatomic, strong) AVLayerVideoGravity videoGravity;

@end

@implementation FHPlayerPresentView

/*
 + (Class)layerClass;
 - (AVPlayerLayer *)avLayer;
 - (void)setPlayer:(AVPlayer *)player;
 这三个方法相当于下面的方法
 [self.layer addSublayer:avPlayerLayer];
 **/

// 重写+layerClass方法使得在创建的时候能返回一个不同的图层子类。UIView会在初始化的时候调用+layerClass方法,然后用它的返回类型来创建宿主图层
+ (Class)layerClass 
{
    return [AVPlayerLayer class];
}

- (AVPlayerLayer *)avLayer
 {
    return (AVPlayerLayer *)self.layer;
}

- (instancetype)initWithFrame:(CGRect)frame 
{
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor blackColor];
    }
    return self;
}

- (void)setPlayer:(AVPlayer *)player 
{
    if (player == _player) return;
    self.avLayer.player = player;
}

- (void)setVideoGravity:(AVLayerVideoGravity)videoGravity
 {
    if (videoGravity == self.videoGravity) return;
    [self avLayer].videoGravity = videoGravity;
}

- (AVLayerVideoGravity)videoGravity
 {
    return [self avLayer].videoGravity;
}

@end

  为什么是FHPlayerPresentView不是直接操作AVPlayerLayer呢?因为AVPlayerLayer的层级没办法控制(反正我是没找着方法),当切换视频的时候controlView就要在AVPlayerLayer下面了,controlView上面的控制按钮就不能用了。因为当切换视频的时候,首先是移除AVPlayerLayer,再添加新的,而 controlView一直没有变化。所已我把AVPlayerLayer放在FHPlayerPresentView中,操作FHPlayerPresentView的层级就很简单了。
  另外,我们需要设置一个属性,来记录视频是否暂停,是否是播放了。

// 是否正在播放
@property (nonatomic, assign) BOOL playing;
// 控制视频播放
- (void)controlAction:(UIButton *)button
{
    // 如果视频正在播放,暂停;否则,播放。
    if (self.playing)
    {
        // 暂停
        [self.player pause];
    }
    else
    {
        //播放
        [self play];
    }
    
    // 记录播放的状态
    self.playing = !self.playing;
    
    // 修改button的图标
    NSString *imageName = self.playing ? @"pause" : @"play";
    [self.controlView.playBtn setImage:[UIImage imageNamed:imageName] forState:UIControlStateNormal];
}

   下面的这些代码就相当于[self.containerView.layer addSublayer:self.avLayer];

FHPlayerPresentView *presentView = [[FHPlayerPresentView alloc] initWithFrame:self.containerView.bounds];
    [self.containerView addSubview:presentView];
    presentView.player = self.player;
    self.presentView = presentView;
    
    [self.containerView insertSubview:self.controlView aboveSubview:self.presentView];
2.切换视频

  切换视频需要我们多次创建AVPlayer及其相关对象,创建之后一定要释放,否则就会发生内存泄漏,且不会有提示。

- (IBAction)playTheNext:(id)sender 
{
    _currentIndex++;
    
    if (_currentIndex < 0 || self.dataSource.count == 0)
   {
        return;
    }
    
    if ( _currentIndex >= self.dataSource.count) 
    {
        _currentIndex = 0;
    }
    
    // 停止播放视频
    [self stop];
    [self initPlayer];
}

我的思路是移除原来的视频播放器,在初始化一个新的。苹果还提供了一个切换视频的方法:

- (void)replaceCurrentItemWithPlayerItem:(nullable AVPlayerItem *)item;

但是这个方法在某些版本上会引发异常,所以我就没用。

// 停止播放视频
- (void)stop
{
    // 暂停播放视频
    [self.player pause];
    // 记录视频的播放状态
     self.playing = NO;
    
    // 移除观察者
    [self.player removeTimeObserver:_timeObserver];
    // 一定要取消player的当前PlayerItem,负责会造成内存泄漏,且没有提示
    // 多次切换PlayerItem就会崩溃
    [self.player replaceCurrentItemWithPlayerItem:nil];
    _timeObserver = nil;
    [[NSNotificationCenter defaultCenter] removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
    _itmePlaybackEndObserver = nil;
    // 移除KVO
    // 必须先移除KVO,在释放playerItem,否则多初始化几次播放器,就会崩溃,而且没有错误日志。
    [self removeObserver];
    
    // 释放视频相关对象
    self.player = nil;
    self.playerItem = nil;
    self.asset = nil;
    [self.playerLayer removeFromSuperlayer];
    
}

  视频播放器,一定要把观察者也一并移除,不然会一直存在,这样会造成大量的内存损耗,但是重复添加并不会引起崩溃。移除KVO的时候,我用了try - catch,因为重复移除是会引起崩溃的。
  [self.player replaceCurrentItemWithPlayerItem:nil];这行代码的意思释放AVPlayer持有的AVPlayerItem,一定要执行,否则会发生内存泄漏,切没哟提示。如果不添加,多次切换,大约10次以上后,就会发生崩溃。

- (void)removeObserver
{
    // 防止删除不存在的观察者,崩溃
    @try{
        [self.playerItem removeObserver:self forKeyPath:@"status"];
        [self.playerItem removeObserver:self forKeyPath:@"presentationSize"];
        [self.playerItem removeObserver:self forKeyPath:@"loadedTimeRanges"];
        [self.playerItem removeObserver:self forKeyPath:@"playbackLikelyToKeepUp"];
        [self.playerItem removeObserver:self forKeyPath:@"playbackBufferEmpty"];
    } @catch(NSException *e){
        NSLog(@"failed to remove observer");
    }
}
3.全屏播放

  全屏播放是每一个视屏播放器的标配。现在一般是用户点击按钮,进行竖屏和全屏的切换。

- (void)fullScreanAction:(UIButton *)button
{
    [self changeInterfaceOrientation:self.isFullScreen ? UIInterfaceOrientationPortrait : UIInterfaceOrientationLandscapeRight];
}

// 旋转屏幕,interfaceOrientation要旋转的方向
- (void)changeInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
    // 父视图
    UIView *superView = nil;
    // 旋转的角度,默认值是恢复原来的样式
    CGAffineTransform  transform = CGAffineTransformIdentity;
    
    // 竖屏 -> 横屏
    if (interfaceOrientation == UIInterfaceOrientationLandscapeLeft || interfaceOrientation == UIInterfaceOrientationLandscapeRight) {
        // 父视图是keyWindow
        superView = [[UIApplication sharedApplication] keyWindow];
        
        // HOME键在左边,逆时针旋转90°
        if (interfaceOrientation == UIInterfaceOrientationLandscapeLeft) {
            transform = CGAffineTransformMakeRotation(-M_PI_2);
            
        }else if(interfaceOrientation == UIInterfaceOrientationLandscapeRight){
            // HOME键在右边,顺时针旋转90°
            transform = CGAffineTransformMakeRotation(M_PI_2);
        }
        // 记录界面的状态
        self.isFullScreen = YES;
        
    }else{
        // 横屏 -> 竖屏
        superView = self.containerView;
        transform = CGAffineTransformIdentity;
        self.isFullScreen = NO;
    }
    
    [superView addSubview:self.presentView];
    
    // 修改界面的方向
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    /*
     * 设置- (BOOL)shouldAutorotate{return NO;}才有效
     */
    [UIApplication sharedApplication].statusBarOrientation = interfaceOrientation;
#pragma clang diagnostic pop
    // 标记界面的方向需要更改
    [self setNeedsStatusBarAppearanceUpdate];
    
    // 旋转动画
    [UIView animateWithDuration:0.25 animations:^{
        // 旋转
        self.presentView.transform = transform;
        [UIView animateWithDuration:0.25 animations:^{
            // 修改尺寸
            self.presentView.frame = superView.bounds;
        }];
    }  completion:^(BOOL finished) {
        // 修改控制视图的约束
        [self updateControlViewConstraint];
    }];
}

- (void)updateControlViewConstraint
{
    // 当屏幕旋转后,屏幕的长宽也发生了变化,现在长的值变为了原来的宽的值
    if (self.isFullScreen)
    {
        CGFloat width = self.presentView.bounds.size.width;
        CGFloat height = self.presentView.bounds.size.height;
        self.controlView.frame = CGRectMake(0, height - 40, width, 40);
    }
    else
    {
        CGFloat width = SCREEN_WIDTH;
        CGFloat height = SCREEN_WIDTH / 7 * 4;
        self.controlView.frame = CGRectMake(0, height - 40, width, 40);
    }
    
    // 如果不执行下面的两个方法, 上面的设置无效
    // 标记更新约束
    [self.controlView setNeedsUpdateConstraints];
    // 更新约束
    [self.controlView updateConstraintsIfNeeded];
}

  修改手机的statusbar的方向的核心方法是:[UIApplication sharedApplication].statusBarOrientation = interfaceOrientation;;但是发现有时候发现这样设置无效,那是因为还需要添加下面的代码。

- (BOOL)shouldAutorotate
{
    return NO;
}

  有时候这样设置了可能仍然无效。如果window.rootViewController是一个容器视图,例如UINavigationController,UITabBarController,默认走的是容器视图下面的方法,我们要设置成走对应视图的对应方法。以UINavigationController为例。

// 是否支持屏幕旋转
- (BOOL)shouldAutorotate {
    return [self.topViewController shouldAutorotate];
}

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

推荐阅读更多精彩内容