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
即可(即URL
的 public 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提供的操作接口来间接影响其状态,而不能直接修改;
即使在内部,状态也应该有严格且准确的转换条件,我现在的做法是:
将状态设置为LXMAVPlayerStatusUnknown有三种情况,初始化时,reset时,或者KVO观察到playerItem的状态变为unKnown时;
将状态设置为LXMAVPlayerStatusStalling只有一种情况,即 playbackBufferEmpty由0变为1的时候;
将状态设置为LXMAVPlayerStatusReadyToPlay也只有一种情况,即KVO观察到playerItem的状态变为readToPlay时;
(这里还需要注意,测试的时候发现有时候APP从后台进入前台的时候,也触发了playerItem的KVO,change的新旧值都是readToPlay,这就有点坑了,可能会导致你暂停进入后台,回来前台却自动开始播放了,所以我加了一句判断,只有状态从unknown变为readToPlay时才赋值!)将状态设置为LXMAVPlayerStatusPlaying有两种情况,一是 playbackLikelyToKeepUp由0变为1的时候;
(这个设定可能跟其他的播放器稍微有点不一样,但就我的应用场景来说更合适一点,可以理解为真的在播放,缓冲或者没准备好都不算)
二是从暂停恢复到播放状态时(这个时候有可能playbackLikelyToKeepUp的状态一直都是1,所以相当于个一的特殊情况)将状态设置为LXMAVPlayerStatusPaused只有一种,就是调用pause方法的时候;所以这个pause状态一定是用户操作的暂停,而不是系统原因造成的暂停;
将状态设置为LXMAVPlayerStatusStopped有两种情况,一是调用stop方法的时候,二是player播放完的时候;这个感觉没太大用处,但是有一定要有;
将状态设置为LXMAVPlayerStatusFailed只有一种情况,即KVO观察到playerItem的状态变为failed时;
(理论上AVPlayer的文档还提到了一种情况, playbackBufferFull是true但是isPlaybackLikelyToKeepUp还是false,即缓存已经满了,但是缓存的这些内容还不够用来播放,我自己是没遇到这种情况,所以暂时没有处理,等遇到在说)
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状态这个设计貌似不是很合理。。。得考虑怎么优化一下了!!!
暂时总结到这些,等发现其他的再补充