最近做了一个短视频相关的功,主要是列表页,页面里面都是mp4的视频,需求是同一时间内,只能有一个视频在播放,视频播放器内部的功能也很简单,包含展示播放时间和总时长、全屏功能、进度条拖拽、播放、暂停功能。想要自定义播放器,那肯定就是要用到AVPlayer了,简单的用法有太多教程可以看,这里就不再赘述,主要是来说说都遇到了哪些坑。
首先当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
方法。增加try
、catch
是为了当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
}
}];
这样通过增加监听来判断,并且判断了当前的状态是否是播放状态。
答案是否定的,在网络状态不太好的情况下,如果视频正在加载中,并且自动缓冲,此时不调用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;
}];
像上面写的,可以通过判断当前的播放时间是否在缓冲的区域之内来判断,如果不在,那肯定说明,还没有缓冲到。
如果用户当时在后台听音乐,如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之前需要remote监听的NSNotification
和KVO
,并且切换player
的currentItem
到nil
,之后将playerLayer
和playerItem
置为空。
[_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中,很多东西还是需要自己尝试之后,才能得到自己最想要的答案。所以不要怕麻烦,你会学到很多东西。这篇文章也给那些准备做视频播放的同学们,少踩一些坑。