用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];
}

你可能感兴趣的:(用AVFoundation做一个视频播放器(二))