26
2017
09

遇到的AVPlayer的那些坑

遇到的AVPlayer的那些坑

最近做了一个短视频相关的功,主要是列表页,页面里面都是mp4的视频,需求是同一时间内,只能有一个视频在播放,视频播放器内部的功能也很简单,包含展示播放时间和总时长、全屏功能、进度条拖拽、播放、暂停功能。想要自定义播放器,那肯定就是要用到AVPlayer了,简单的用法有太多教程可以看,这里就不再赘述,主要是来说说都遇到了哪些坑。

调用了[self.player play]时,就能保证视频正常播放么?

首先当status变为了AVPlayerStatusReadyToPlay后,我们就可以调用[self.player play]方法来播放视频了。但并不是调用了play方法之后就真的在播放了。在性能差的机器上表现的尤为明显,开始的时候会显示黑屏。原因是AVPlayer在进行播放的时候,会预先解码一些内容,但在这个时候系统就已经告诉我们可以播放了,其实并不是真正的在播放,可能黑屏之后一两秒之后,就会自动播放了。

如何精准的跳转到某一时刻?

在应用进入后台的时候,我们需要记录当前的播放进度,并且停止播放视频,等到用户回到我们的app之后,继续播放。这里就会遇到一个问题,在这种情况下,根据视频的总时长的不同,会有不同情况的不一致,差值在-5~5秒之间。也就是在回到应用继续播放时,不一定是在压入后台时候的那个时间点。同样在进度条拖拽到最右侧的时候,也会倒退回去几秒钟,如何处理呢?

其实主要的方法是使用系统API的问题,系统提供了定位到某一时刻的API如下:

[self.player seekToTime:self.lastPlayTime toleranceBefore: toleranceAfter: completionHandler:];

如果需要精准定位,那么把toleranceBefore:toleranceAfter:的参数都设置为kCMTimeZero即可。
所以在进入后台返回的时候就可以通过一下代码进行处理

    @try
    {
        DEF_WEAKSELF;
        [self.playerseekToTime:self.lastPlayTimetoleranceBefore:kCMTimeZerotoleranceAfter:kCMTimeZerocompletionHandler:^(BOOL finished) {
            if (finished)
                [weakSelf.playerplay];
        }];
    }
    @catch (NSException * exception) {
        [self.playerplay];
    }

因为压入后台的时候已经暂停了,所以在seekToTime:完成之后,调用play方法。增加trycatch是为了当seekToTime:出现异常时也不至于崩溃,可以让播放器继续播放。

判断当前的播放器的状态是播放、暂停、还是缓冲中?

我想你一定看过,通过rate来判断视频是否在播放。rate是表示视频播放的速度,当rate为1.0时,也就是正常的播放速度,如果rate是1.25、1.5、2.0时,播放就是分别的倍数,也就是像B站类似的效果。当暂停的时候rate为0,但是并不代表rate为1是就是在播放。在iOS10的SDK中提供了新的方法来判断是否处于播放状态,一个枚举类型的变量timeControlStatus

typedef NS_ENUM(NSInteger, AVPlayerTimeControlStatus) {
    AVPlayerTimeControlStatusPaused,
    AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate,
    AVPlayerTimeControlStatusPlaying
} NS_ENUM_AVAILABLE(10_12, 10_0);

看到这个就知道了暂停,等待还是播放状态。
有时候我们如果想要在视频一播放的时候去做一些事情,例如设置一下播放器的背景色,如果我们仅仅是监听这个rate可能无法100%保证有效,而如果我们真的要监听这种情况的话,有一个取巧的方法

self.playTimeObserver = [self.playeraddPeriodicTimeObserverForInterval:CMTimeMake(1, 1) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
    if ([[[UIDevicecurrentDevice] systemVersion] floatValue] >= 9.9)
    {
        if (weakSelf.player.timeControlStatus == AVPlayerTimeControlStatusPlaying)
            //  do something
    }
    else
    {
        if (weakSelf.player.rate == 1)
            //  do something
    }
}];

这样通过增加监听来判断,并且判断了当前的状态是否是播放状态。

监听了isPlaybackBufferEmpty并且值为0,那么播放器就不会播放了么?

答案是否定的,在网络状态不太好的情况下,如果视频正在加载中,并且自动缓冲,此时不调用pause方法,播放器就可能在一段时间内收到多次isPlaybackBufferEmpty改变的消息,并且有的时候为1,有的时候为0,这个时候进度条就会开始鬼畜。那如果要解决这个问题呢?

        //isPlaybackBufferEmpty这个属性不准,所以检查缓冲的时间
        __blockBOOL isBufferEmpty = YES;
        NSArray * timeRangeArray = self.playerItem.loadedTimeRanges;
        CMTime currentTime = self.playerItem.currentTime;
        [timeRangeArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            CMTimeRange aTimeRange = [[timeRangeArray objectAtIndex:0] CMTimeRangeValue];
            if(CMTimeRangeContainsTime(aTimeRange, currentTime) && CMTimeGetSeconds(aTimeRange.duration) > 0.1)
                *stop = YES, isBufferEmpty = NO;
        }];

像上面写的,可以通过判断当前的播放时间是否在缓冲的区域之内来判断,如果不在,那肯定说明,还没有缓冲到。

其它App播放声音打断问题

如果用户当时在后台听音乐,如QQ音乐,或者喜马拉雅这些App,这个时候播放视频后,其会被我们打断,当我们不再播放视频的时候,自然需要继续这些后台声音的播放。

首先,我们需要先向设备注册激活声音打断AudioSessionSetActive(YES);,当然我们也可以通过[AVAudioSession sharedInstance].otherAudioPlaying;这个方法来判断还有没有其它业务的声音在播放。

当我们播放完视频后,需要恢复其它业务或App的声音,这时我们可以调用如下方法:

OSStatus ret = AudioSessionSetActiveWithFlags(NO, kAudioSessionSetActiveFlag_NotifyOthersOnDeactivation);

App不支持横屏,如何让播放器支持全屏模式?

这个问题其实很头疼,但是也不是没有解决方法。首先全屏的时候,播放器肯定会从列表中移除,并且加载到当前window上的某一个view中来展示。这样我们就可以旋转这个view,达到横屏的效果。

    //播放器所在控制器不支持旋转,采用旋转view的方式实现
    if (direction == UIInterfaceOrientationLandscapeLeft)
    {
        [UIViewanimateWithDuration:0.25animations:^{
            self.transform = CGAffineTransformMakeRotation(M_PI / 2);
        }];
        [[UIApplicationsharedApplication] setStatusBarOrientation:UIInterfaceOrientationLandscapeRightanimated:NO];
    }
    elseif (direction == UIInterfaceOrientationLandscapeRight)
    {
        [UIViewanimateWithDuration:0.25animations:^{
            self.transform = CGAffineTransformMakeRotation(-M_PI / 2);
        }];
        [[UIApplicationsharedApplication] setStatusBarOrientation:UIInterfaceOrientationLandscapeLeftanimated:NO];
    }
    self.frame = CGRectMake(0, 0, kAppHeight, kAPPWidth);

当要恢复时,把播放器view移除,并且加入到之前的那个cell中,再旋转回来,并且设置frame。

//还原
[UIViewanimateWithDuration:0.25animations:^{
    self.transform = CGAffineTransformMakeRotation(0);
}];
self.frame = _originFrame;
//还原到原有父类上
[_fatherViewaddSubview:self];

这里还有一个需要注意的问题,如果要在这个时候弹框,也就是UIAlertController。会发现alert还是竖屏的,这个时候就需要把UIAlertController的view同样旋转。以为这样就完事了么?不,在点击按钮的时候,会发现UIAlertController会旋转回竖屏之后才消失。这个时候需要swizzle一下UIAlertController的viewWillDisappear函数,在函数中先将UIAlertController的view隐藏。

播放完毕之后,如何释放AVPlayer?

释放AVPlayer之前需要remote监听的NSNotificationKVO,并且切换playercurrentItemnil,之后将playerLayerplayerItem置为空。

[_playerreplaceCurrentItemWithPlayerItem:nil];
[_playerItemremoveObserver:selfforKeyPath:status];
[_playerItemremoveObserver:selfforKeyPath:loadedTimeRanges];
[_playerItemremoveObserver:selfforKeyPath:playbackBufferEmpty];
[_playerItemremoveObserver:selfforKeyPath:playbackLikelyToKeepUp];
[_playerremoveTimeObserver:self.playTimeObserver];
[[NSNotificationCenterdefaultCenter] removeObserver:self];
self.playTimeObserver = nil;
_playerLayer = nil;
_playerItem = nil;

以上就是目前遇到的并且解决的问题,系统挖的坑很多,所以我们必须要自己填坑才能让播放器做的很完美。很多时候没必要把思路局限在某些demo中,很多东西还是需要自己尝试之后,才能得到自己最想要的答案。所以不要怕麻烦,你会学到很多东西。这篇文章也给那些准备做视频播放的同学们,少踩一些坑。

上一篇:Android逐帧动画——让图片动起来 下一篇:关于启动一个IOS程序的详细流程图