AVPlayer播放网络视频踩坑记录

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提供的操作接口来间接影响其状态,而不能直接修改;
即使在内部,状态也应该有严格且准确的转换条件,我现在的做法是:

  • 将状态设置为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状态这个设计貌似不是很合理。。。得考虑怎么优化一下了!!!

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容