iOS之开发配置iOS之性能优化iOS之报错上架填坑

AVPlayer播放网络视频踩坑记录

2018-09-30  本文已影响797人  Phelthas

2019年1月16日更新:

13, 想了很久player状态定义的问题,现在感觉AVFoundation的AVPlayerItemStatus的定义是对的,即AVPlayerItemStatus跟player的status其实不是同一个东西,不应该统一到一起;
AVPlayerItemStatus表示的是这个item是否可以播放,它只有unknown,readToPlay,fail三种状态,针对的是这个item的可用性;而播放器的status可能有unknown, playing,stalling,paused, stopped, failed,等状态,针对的是播放器,是在item可用的前提下才有意义的,播放器当然也可能有fail的情况,但这个fail跟item的fail不一样。
从这个意义上来说,AVPlayerItemStatus应该是播放器状态内部的一个状态,比如播放器播放失败了,可能是itemStatus是failed,也可能是其他原因。
所以把status定义里的readToPlay状态删除掉了,单独作为player的一个只读的属性,并单独给出变为reatyToPlay的回调(最新代码还是在这里:https://github.com/Phelthas/LXMPlayer )。这样的话也兼容了下面14的问题。

14,播放本地视频跟播放网络视频稍微有点不一样
按照现在的封装,播放本地视频只需要将视频的本地地址URL传给assetURL即可(即URLpublic init(fileURLWithPath path: String) 返回的地址),但kvo触发的流程不太一样:

1)首先是观察到kAVPlayerItemPlaybackBufferEmpty的变化,从1变为0,说有缓存到内容了,已经有loadedTimeRanges了,但这时候还不一定能播放,因为数据可能还不够播放;
2)然后是kAVPlayerItemPlaybackLikelyToKeepUp,从0变到1,说明可以播放了,这时候会自动开始播放
3)然后是kAVPlayerItemStatus的变化,从0变为1,即变为readyToPlay

即不同于网络播放的场景,播放本地视频时,是先观察到playing开始,kAVPlayerItemStatus才变为readyToPlay的。

15,保存到本地的视频如果没有后缀,AVPlayer会识别不了,AVPlayerItemStatus的状态会变为“AVPlayerItemStatusFailed”,所以在保存的时候,必须把原来的后缀也保存下来。

以下是原文


AVPlayer可以用来直接播放网络上的视频,只要设置一个AVURLAsset就行,
但在播放的过程中,需要时刻注意playerItem的状态,一般是用KVO来观察playerItem的几个属性,
主要包括status,playbackBufferEmpty,playbackLikelyToKeepUp等。
在观察到这些值的变化时,执行的操作一般来说也是大同小异,状态判断的代码基本页是一样,所以如果每个地方都写一套KVO的代码的话就太麻烦了,
比较好的解决办法是将AVPlayer再封装一层,用block回调或者delegate的方式来通知外部状态的变化。
我简单封装了一层LXMPlayerView,(代码:https://github.com/Phelthas/LXMPlayer )可以在一定程度上简化代码结构,这里记录一下工程中遇到的问题及解决方案:

1, 首先是参考了别人的代码,继承UIView作为一个playerView,然后重载layerClass方法,将View的layer变成一个AVPlayerLayer。

+ (Class)layerClass {
    return [AVPlayerLayer class];
}

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

这样做的好处是,layer的大小会自动跟着view的大小变化,而view可以用autoLayout,就不用在layoutSubview里面手动更新layer的大小了。

2,一个简单播放流程中各个状态的变化

 1)打断点观察,当调用play方法的时候,首先会观察到kAVPlayerRate的变化,从0变到1;但这时候并没有画面,因为还没有任何数据;

 2)然后开始loading,稍后就会观察到kAVPlayerItemPlaybackBufferEmpty的变化,从1变为0,说有缓存到内容了,已经有loadedTimeRanges了,但这时候还不一定能播放,因为数据可能还不够播放;

 3)然后是kAVPlayerItemPlaybackLikelyToKeepUp的变化,新旧值都是0,这时候还没什么用,因为本来就还没开始播放;

 4)然后是kAVPlayerItemStatus的变化,从0变为1,即变为readyToPlay

 5)然后是kAVPlayerItemPlaybackLikelyToKeepUp,从0变到1,说明可以播放了,这时候会自动开始播放

3,考虑到上面的这些状态变化,所以定义了playerView的status枚举

typedef NS_ENUM(NSInteger, LXMAVPlayerStatus) {
    LXMAVPlayerStatusUnknown = 0,
    LXMAVPlayerStatusStalling,
    LXMAVPlayerStatusReadyToPlay,
    LXMAVPlayerStatusPlaying,
    LXMAVPlayerStatusPaused,
    LXMAVPlayerStatusStopped,
    LXMAVPlayerStatusFailed,
};

按我的理解,这些状态应该就是playerView完整的状态机,即playerView会且仅会处于上面其中一种状态。
并且这些状态是playerView的内部状态,对外部来说是只读的,外部只能通过playerView提供的操作接口来间接影响其状态,而不能直接修改;
即使在内部,状态也应该有严格且准确的转换条件,我现在的做法是:

4,playerItem的rate

rate就是在player调用play的时候变为1,调用pause的时候变为0,它的值不根据卡不卡变化,它应该是用来决定当load到新数据是要不要继续播放。所以我感觉rate是没有必要用KVO观察。当然如果要做倍率播放或者慢速播放,那估计会用到,到时候再处理。

5,监听APP进入前台或者后台

如果APP没有申请后台播放权限,那APP进入后台的时候,AVPlayer就会被暂停,重新进入前台之后会继续播放(有时候不会开始播放。。。)。
这个有点不好控制,因为如果用户是暂停了进入后台的,这种情况下回到前台肯定还是需要是暂停状态。
这里我参考了其他播放器的做法,添加了一个 statusBeforeBackground属性,用来记录APP进入后台之前的播放状态,
然后监听 UIApplicationWillResignActiveNotification和 UIApplicationDidBecomeActiveNotification两个通知,
在通知的回调中修改statusBeforBackground的状态;

当进入后台时,只有当statusBeforBackground是unknown的时候,才会记录当前播放状态,然后暂停;

当进入前台时,只有当statusBeforBackground记录到的状态是playing || stalling || readToPlay时,才会继续播放,并将statusBeforBackground重置为unknown。

这里这么写,主要是因为,APP在前台时拉下通知栏,会让APP进入inactive状态,这时候不知道为什么和通知会被触发两次,状态有点混乱,所以只能暂时这么特殊处理下,

如果有什么更好的解决办法,再优化。。。

6,监听网络状态变化

一般来说,网络状态从wifi变为蜂窝网络的时候,要暂停播放器,这个应该由播放器外部来控制。
但测试的时候发现了一种特殊情况:正在播放的时候把APP切到后台,关掉网络,再切回APP,播放器会暂停一下,再继续播放。。。
这是因为上面监听APP进入后台的机制,进入后台的时候记录到的statusBeforBackground是playing,所以返回前台时触发UIApplicationDidBecomeActiveNotification通知,会再调用play方法,通知触发的时机是在外部调用暂停之后的!
所以我这里监听了一下网络状态的变化,当网络状态变化为非wifi时,将statusBeforBackground设置为paused。
理论上,如果外部调用暂停方法的时候,将statusBeforBackground重置为unknown也是可以的。但这样又要多判断一下是用户暂停还是通知造成的暂停,

我也不确定那种方式更好,暂时用监听网络变化的方法了。。。

7,内存管理

AVPlayer必须要有一个类强引用一下,否则它不知道什么时候就释放掉了,这样会导致kvo没有取消观察者之类的crash。
这个PlayerView也是如此,测试的时候出现过playerView的View还在(因为已经添加到其他view上),但palyerView本身却被释放掉的bug,千万注意!

2018年11月29日更新:

8,内存管理之AVPlayerLayer
AVPlayerLayer会retain其相关的AVPlayer,所以释放的时候,必须主动将AVPlayerView的player设置为nil,否则即使player被设置为nil了,player还是不会释放(因为还有其他地方强引用嘛)。这个问题坑了我好久,需特别注意一下!

9,seek方法的问题
统计到如下错误
1)AVPlayerItem cannot service a seek request with a completion handler until its status is AVPlayerItemStatusReadyToPlay。
2)Seeking is not possible to time {INDEFINITE}。
即当AVPlayerItem的状态还没有变成readyToPlay之前,seek方法是肯定会报错!当状态变成readyToPlay之后,如果seek的time是非法的,也会报错,所以在seek之前就需要加两个判断。
readToPlay的判断只能用kvo观察AVPlayerItem的方式来做,加个内部变量就好,
判断seek的time是否合法,系统提供了函数:

if (CMTIME_IS_INDEFINITE(time) || CMTIME_IS_INVALID(time)) {
        return;
}

10, 切换视频清晰度,界面可能会闪一下的问题
切换清晰度其实就是换个url,然后从刚刚的进度继续开始播。这就需要保存当前播放进度,等切换的playerItem的状态变为readToPlay的时候,seek到这个时间点开始播放。界面会闪,大概率是因为seek之前,播放器是处于play状态的,所以playerItem会直接从0开始播放,而seek方法是异步的,所以在从指定时间点播放之前可能已经播了一点点,seek完成之后直接开始播放指定时间的内容,造成界面闪一下。
正确的做法是:在seek之前,暂停视频的播放,在seek完成的回调中再继续播放。

11,seek导致播放状态不对的问题
因为上面10的原因,可能需要在readToPlay的时候直接seek到某一时间点,而我之前写的逻辑是player的状态只有在有限的情况下才会变,所以这里可能会导致player的状态一直保持在readToPlay而没有切换到playing。。。这个问题比较坑,暂时没想到什么特别好的解决办法,现在暂时hardCode解决:在seek方法之后加了个判断,如果原来状态是readToPlay,那seek之后,会设置为playing。
从stop状态seek问题同上,暂时也是hardCode解决

12,暂停时,网络加载异步回调导致player状态变化的问题
这个也比较坑,因为网络加载是异步回调的,所以用户手动点了暂停之后,可能过了几秒钟下载了新的内容回来,kvo会观察到playbackLikelyToKeepUp 变化,这时候按理说不应该修改播放器的状态。。。

从11,12的问题来看,用kvo来确定player状态这个设计貌似不是很合理。。。得考虑怎么优化一下了!!!

暂时总结到这些,等发现其他的再补充

上一篇下一篇

猜你喜欢

热点阅读