• iOS开发--XLVideoPlayer——基于AVFoundation自定义的视频播放器


    本文聊点关于最近写的这个自定义播放器。支持UITableViewCell上小屏、全屏播放,手动及屏幕旋转切换,包括右下角的小窗悬停播放,不依赖于视图控制器和第三方,尽量的让使用起来更简单,具体代码详情请戳Github,先看看效果如何!

    1121012-97f1d83cb7470a06.gif

    1121012-1832f4a772839eb0.gif

    这是基于AVFoundation下自定义的一个播放器,先简单介绍几个用到的类。

    介绍:

    • AVPlayer:可以理解为播放器对象,灵活性好,可以高度化的自定义UI,但它本身不能显示视频,显示需要另一个类AVPlayerLayer来显示,继承于CALayer,下面是摘自官方的一段介绍

    AVPlayer works equally well with local and remote media files.

    You can display the visual content of items played by an instance of AVPlayer in a CoreAnimation layer of class AVPlayerLayer.

    You can observe the status of a player using key-value observing.

    主要是说它支持本地/网络媒体播放,需要CoreAnimation下的AVPlayerLayer来显示视频,我们可以通过KVO监听player的播放状态。

    • AVPlayerItem:存有相关媒体信息的类,一个视频资源对应一个AVPlayerItem对象,当你需要循环播放多个视频资源时也需创建多个AVPlayerItem对象。建议大家可以多看看官方的英文文档解释(题外话)。

    An AVPlayerItem represents the presentation state of an asset that’s played by an AVPlayer object, and lets you observe that state.

    • AVAsset:主要用于获取多媒体信息,可以理解为一个抽象类,不能直接使用,操作针对它的子类AVURLAsset,根据你视频的url创建一个包含视频媒体信息的AVURLAsset对象。

    • CMTime:还会用到这个媒体时间相关的类,如有不明白可以看之前一个帖子的解释。

    层级关系:

    基 于以上几个类就能实现视频的基本功能了,例如暂停、播放,快进、后退、显示播放/缓冲进度。然后UI层面,层级很简单,XLVideoPlayer继承于 UIView,上面我们说到显示视频需要AVPlayerLayer,我们将AVPlayerLayer加到view的layer上。

    1121012-f86b3e428e604109.jpg

    下面贴出主要的代码,初始化AVPlayer对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    - (AVPlayerLayer *)playerLayer {
        if (!_playerLayer) {
            _playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
            _playerLayer.backgroundColor = kPlayerBackgroundColor;
            _playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;//视频填充模式
        }
        return _playerLayer;
    }
     
    - (AVPlayer *)player{
        if (!_player) {
            AVPlayerItem *playerItem = [self getAVPlayItem];
            self.playerItem = playerItem;
            _player = [AVPlayer playerWithPlayerItem:playerItem];
     
            [self addProgressObserver];
     
            [self addObserverToPlayerItem:playerItem];
        }
        return _player;
    }
     
    //initialize AVPlayerItem
    - (AVPlayerItem *)getAVPlayItem{
     
        NSAssert(self.videoUrl != nil, @"必须先传入视频url!!!");
     
        if ([self.videoUrl rangeOfString:@"http"].location != NSNotFound) {
            AVPlayerItem *playerItem=[AVPlayerItem playerItemWithURL:[NSURL URLWithString:[self.videoUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]];
            return playerItem;
        }else{
            AVAsset *movieAsset  = [[AVURLAsset alloc]initWithURL:[NSURL fileURLWithPath:self.videoUrl] options:nil];
            AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:movieAsset];
            return playerItem;
        }
    }

    同时我们注册KVO,监控视频播放过程,这可以获取视频的播放进度。AVPlayer有一个属性currentItem是AVPlayerItem类型,表示当前播放的视频对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    #pragma mark - monitor video playing course
     
    -(void)addProgressObserver{
     
        //get current playerItem object
        AVPlayerItem *playerItem = self.player.currentItem;
        __weak typeof(self) weakSelf = self;
     
        //Set once per second
        [self.player addPeriodicTimeObserverForInterval:CMTimeMake(1.0, 1.0) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
     
            float current = CMTimeGetSeconds(time);
            float total = CMTimeGetSeconds([playerItem duration]);
            weakSelf.progressLabel.text = [weakSelf timeFormatted:current];
            if (current) {
    //            NSLog(@"%f", current / total);
                weakSelf.slider.value = current / total;
     
                if (weakSelf.slider.value == 1) {      //complete block
                    if (weakSelf.completedPlayingBlock) {
                        weakSelf.completedPlayingBlock(weakSelf);
                    }else {       //finish and loop playback
                        weakSelf.playOrPauseBtn.selected = NO;
                        [weakSelf showOrHidenBar];
                        CMTime currentCMTime = CMTimeMake(0, 1);
                        [weakSelf.player seekToTime:currentCMTime completionHandler:^(BOOL finished) {
                            weakSelf.slider.value = 0.0f;
                        }];
                    }
                }
            }
        }];
    }

    以 及监听AVPlayerItem对象的status/loadedTimeRanges属性变化,status对应播放状 态,loadedTimeRanges网络缓冲状态,当loadedTimeRanges的改变时,每缓冲一部分数据就会更新此属性,可以获得本次缓冲加 载的视频范围(包含起始时间、本次网络加载时长)

    1
    2
    3
    4
    5
    6
    7
    #pragma mark - PlayerItem (status,loadedTimeRanges)
    -(void)addObserverToPlayerItem:(AVPlayerItem *)playerItem{
        //监控状态属性,注意AVPlayer也有一个status属性,通过监控它的status也可以获得播放状态
        [playerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
        //network loading progress
        [playerItem addObserver:self forKeyPath:@"loadedTimeRanges" options:NSKeyValueObservingOptionNew context:nil];
    }

    在这获取视频的总时长,网络的视频缓冲进度,做相应的显示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
        AVPlayerItem *playerItem = object;
        if ([keyPath isEqualToString:@"status"]) {
            AVPlayerStatus status = [[change objectForKey:@"new"] intValue];
            if(status == AVPlayerStatusReadyToPlay){
                self.totalDuration = CMTimeGetSeconds(playerItem.duration);
                self.totalDurationLabel.text = [self timeFormatted:self.totalDuration];
            }
        }else if([keyPath isEqualToString:@"loadedTimeRanges"]){
            NSArray *array = playerItem.loadedTimeRanges;
            CMTimeRange timeRange = [array.firstObject CMTimeRangeValue];//本次缓冲时间范围
            float startSeconds = CMTimeGetSeconds(timeRange.start);
            float durationSeconds = CMTimeGetSeconds(timeRange.duration);
            NSTimeInterval totalBuffer = startSeconds + durationSeconds;//缓冲总长度
            self.slider.middleValue = totalBuffer / CMTimeGetSeconds(playerItem.duration);
    //        NSLog(@"totalBuffer:%.2f",totalBuffer);
            //remove loading animation
            if (self.slider.middleValue <= self.slider.value) {
                self.activityIndicatorView.center = self.center;
                [self addSubview:self.activityIndicatorView];
                [self.activityIndicatorView startAnimating];
            }else {
                [self.activityIndicatorView removeFromSuperview];
            }
        }
    }

    下面这部分是定位视频的某个位置播放,也就是快进后退。

    这里需要注意的是在用户拖拽slider的过程中需要先暂停,否则手动改变进度和播放的进度会有冲突,用户拖拽完毕再去播放视频。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    - (void)finishChange {
        _inOperation = NO;
        [self hiden];
        CMTime currentCMTime = CMTimeMake(self.slider.value * self.totalDuration, 1);
        [self.player seekToTime:currentCMTime completionHandler:^(BOOL finished) {
            [self.player play];
            self.playOrPauseBtn.selected = YES;
        }];
    }

    关于屏幕旋转

    这 部分还是遇到一些坑,可以看到并没有在plist文件设置工程支持横屏,所有都是通过强制旋转屏幕实现,在用户旋转屏幕的通知或者点击事件中调用强制旋转 的代码。会发现当你旋转屏幕时,其实UITableView和其他控件是不会随屏幕一起旋转的,强制旋转涉及到iOS8+和之前的系统的问题,当我们调用 之前的时,在iOS7和iOS8+的效果是不一样的,我从网上摘了来两个图。

    1
    [[UIApplication sharedApplication] setStatusOrientation:XX]

    第 一张图iOS 7的,第二张图是iOS 8+,很明显我们发现iOS7当你调用这个方法UIscreen和UIWindow一起转过来了,而iOS8后UIScreen并没有转过来,这样就会导 致调用这个方法在iOS8+会存在部分区域点击无响应,因为它超出UIScreen的那部分范围,而且我在测试过程中还发现用这种方法旋转在点击Home 键再次进入程序会导致屏幕错位。

    1121012-b6e641de187b2482.jpg

    1121012-293506d254fec468.jpg

    怎么办呢!后面又找到这个方法:

    1
    [[UIDevice currentDevice]setOrientation:UIInterfaceOrientationPortrait];

    但是现在苹果已经将该方法私有化了,直接pass掉。之后在stackoverflow做了些尝试,找到现在用的这个方法,它并没有把系统的status bar旋转过来。

    1
    2
    NSNumber *value = [NSNumber numberWithInt:UIInterfaceOrientationLandscapeRight];
    [[UIDevice currentDevice] setValue:value forKey:@"orientation"];

    之后还查了一些相关的东西,有兴趣大家可以看看:

    详解UICoordinateSpace和UIScreen在iOS 8上的坐标问题

    屏幕旋转学习笔记

    写一个播放器还需要注意很多细节,只能根据需求一步步的完善,这里只能说一些需要关注的点。

  • 相关阅读:
    MVC NonAction属性
    未将对象引用设置到对象的实例
    回调函数callback
    Json详解
    浅谈HTTP中Get与Post的区别
    JQuery $.ajax()方法详解
    C#中Const和Readonly的区别
    全面解释StringBuilder、StringBuffer和String的关系
    基本数据类型的包装类和随机数
    枚举类的使用
  • 原文地址:https://www.cnblogs.com/Samboo/p/5393823.html
Copyright © 2020-2023  润新知