2019年1月16日更新:
13, 想了很久player狀態(tài)定義的問題,現(xiàn)在感覺AVFoundation的AVPlayerItemStatus的定義是對的,即AVPlayerItemStatus跟player的status其實不是同一個東西,不應(yīng)該統(tǒng)一到一起找岖;
AVPlayerItemStatus表示的是這個item是否可以播放,它只有unknown敛滋,readToPlay许布,fail三種狀態(tài),針對的是這個item的可用性绎晃;而播放器的status可能有unknown, playing蜜唾,stalling,paused, stopped, failed,等狀態(tài)箕昭,針對的是播放器灵妨,是在item可用的前提下才有意義的,播放器當然也可能有fail的情況落竹,但這個fail跟item的fail不一樣。
從這個意義上來說货抄,AVPlayerItemStatus應(yīng)該是播放器狀態(tài)內(nèi)部的一個狀態(tài)述召,比如播放器播放失敗了,可能是itemStatus是failed蟹地,也可能是其他原因积暖。
所以把status定義里的readToPlay狀態(tài)刪除掉了,單獨作為player的一個只讀的屬性怪与,并單獨給出變?yōu)閞eatyToPlay的回調(diào)(最新代碼還是在這里:https://github.com/Phelthas/LXMPlayer )夺刑。這樣的話也兼容了下面14的問題。
14,播放本地視頻跟播放網(wǎng)絡(luò)視頻稍微有點不一樣
按照現(xiàn)在的封裝遍愿,播放本地視頻只需要將視頻的本地地址URL傳給assetURL
即可(即URL
的 public init(fileURLWithPath path: String)
返回的地址)存淫,但kvo觸發(fā)的流程不太一樣:
1)首先是觀察到kAVPlayerItemPlaybackBufferEmpty的變化,從1變?yōu)?沼填,說有緩存到內(nèi)容了桅咆,已經(jīng)有l(wèi)oadedTimeRanges了,但這時候還不一定能播放坞笙,因為數(shù)據(jù)可能還不夠播放岩饼;
2)然后是kAVPlayerItemPlaybackLikelyToKeepUp,從0變到1薛夜,說明可以播放了籍茧,這時候會自動開始播放
3)然后是kAVPlayerItemStatus的變化,從0變?yōu)?梯澜,即變?yōu)閞eadyToPlay
即不同于網(wǎng)絡(luò)播放的場景寞冯,播放本地視頻時,是先觀察到playing開始腊徙,kAVPlayerItemStatus才變?yōu)閞eadyToPlay的简十。
15,保存到本地的視頻如果沒有后綴撬腾,AVPlayer會識別不了螟蝙,AVPlayerItemStatus的狀態(tài)會變?yōu)椤癆VPlayerItemStatusFailed”,所以在保存的時候民傻,必須把原來的后綴也保存下來胰默。
以下是原文
AVPlayer可以用來直接播放網(wǎng)絡(luò)上的視頻,只要設(shè)置一個AVURLAsset就行漓踢,
但在播放的過程中牵署,需要時刻注意playerItem的狀態(tài),一般是用KVO來觀察playerItem的幾個屬性喧半,
主要包括status
,playbackBufferEmpty
,playbackLikelyToKeepUp
等奴迅。
在觀察到這些值的變化時,執(zhí)行的操作一般來說也是大同小異挺据,狀態(tài)判斷的代碼基本頁是一樣取具,所以如果每個地方都寫一套KVO的代碼的話就太麻煩了,
比較好的解決辦法是將AVPlayer再封裝一層扁耐,用block回調(diào)或者delegate的方式來通知外部狀態(tài)的變化暇检。
我簡單封裝了一層LXMPlayerView,(代碼:https://github.com/Phelthas/LXMPlayer )可以在一定程度上簡化代碼結(jié)構(gòu)婉称,這里記錄一下工程中遇到的問題及解決方案:
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蜜暑,一個簡單播放流程中各個狀態(tài)的變化
1)打斷點觀察铐姚,當調(diào)用play方法的時候,首先會觀察到kAVPlayerRate的變化肛捍,從0變到1;但這時候并沒有畫面隐绵,因為還沒有任何數(shù)據(jù);
2)然后開始loading拙毫,稍后就會觀察到kAVPlayerItemPlaybackBufferEmpty的變化依许,從1變?yōu)?,說有緩存到內(nèi)容了缀蹄,已經(jīng)有l(wèi)oadedTimeRanges了峭跳,但這時候還不一定能播放,因為數(shù)據(jù)可能還不夠播放缺前;
3)然后是kAVPlayerItemPlaybackLikelyToKeepUp的變化蛀醉,新舊值都是0,這時候還沒什么用衅码,因為本來就還沒開始播放拯刁;
4)然后是kAVPlayerItemStatus的變化,從0變?yōu)?逝段,即變?yōu)閞eadyToPlay
5)然后是kAVPlayerItemPlaybackLikelyToKeepUp垛玻,從0變到1,說明可以播放了奶躯,這時候會自動開始播放
3帚桩,考慮到上面的這些狀態(tài)變化,所以定義了playerView的status枚舉
typedef NS_ENUM(NSInteger, LXMAVPlayerStatus) {
LXMAVPlayerStatusUnknown = 0,
LXMAVPlayerStatusStalling,
LXMAVPlayerStatusReadyToPlay,
LXMAVPlayerStatusPlaying,
LXMAVPlayerStatusPaused,
LXMAVPlayerStatusStopped,
LXMAVPlayerStatusFailed,
};
按我的理解嘹黔,這些狀態(tài)應(yīng)該就是playerView完整的狀態(tài)機账嚎,即playerView會且僅會處于上面其中一種狀態(tài)。
并且這些狀態(tài)是playerView的內(nèi)部狀態(tài)儡蔓,對外部來說是只讀的醉锄,外部只能通過playerView提供的操作接口來間接影響其狀態(tài),而不能直接修改浙值;
即使在內(nèi)部,狀態(tài)也應(yīng)該有嚴格且準確的轉(zhuǎn)換條件檩小,我現(xiàn)在的做法是:
將狀態(tài)設(shè)置為LXMAVPlayerStatusUnknown有三種情況开呐,初始化時,reset時,或者KVO觀察到playerItem的狀態(tài)變?yōu)閡nKnown時筐付;
將狀態(tài)設(shè)置為LXMAVPlayerStatusStalling只有一種情況卵惦,即 playbackBufferEmpty由0變?yōu)?的時候;
將狀態(tài)設(shè)置為LXMAVPlayerStatusReadyToPlay也只有一種情況瓦戚,即KVO觀察到playerItem的狀態(tài)變?yōu)閞eadToPlay時沮尿;
(這里還需要注意,測試的時候發(fā)現(xiàn)有時候APP從后臺進入前臺的時候较解,也觸發(fā)了playerItem的KVO畜疾,change的新舊值都是readToPlay,這就有點坑了印衔,可能會導(dǎo)致你暫停進入后臺啡捶,回來前臺卻自動開始播放了,所以我加了一句判斷奸焙,只有狀態(tài)從unknown變?yōu)閞eadToPlay時才賦值O故睢)將狀態(tài)設(shè)置為LXMAVPlayerStatusPlaying有兩種情況,一是 playbackLikelyToKeepUp由0變?yōu)?的時候与帆;
(這個設(shè)定可能跟其他的播放器稍微有點不一樣了赌,但就我的應(yīng)用場景來說更合適一點,可以理解為真的在播放玄糟,緩沖或者沒準備好都不算)
二是從暫臀鹚恢復(fù)到播放狀態(tài)時(這個時候有可能playbackLikelyToKeepUp的狀態(tài)一直都是1,所以相當于個一的特殊情況)將狀態(tài)設(shè)置為LXMAVPlayerStatusPaused只有一種茶凳,就是調(diào)用pause方法的時候嫂拴;所以這個pause狀態(tài)一定是用戶操作的暫停,而不是系統(tǒng)原因造成的暫停贮喧;
將狀態(tài)設(shè)置為LXMAVPlayerStatusStopped有兩種情況筒狠,一是調(diào)用stop方法的時候,二是player播放完的時候箱沦;這個感覺沒太大用處辩恼,但是有一定要有;
將狀態(tài)設(shè)置為LXMAVPlayerStatusFailed只有一種情況谓形,即KVO觀察到playerItem的狀態(tài)變?yōu)閒ailed時灶伊;
(理論上AVPlayer的文檔還提到了一種情況, playbackBufferFull是true但是isPlaybackLikelyToKeepUp還是false寒跳,即緩存已經(jīng)滿了聘萨,但是緩存的這些內(nèi)容還不夠用來播放,我自己是沒遇到這種情況童太,所以暫時沒有處理米辐,等遇到在說)
4胸完,playerItem的rate
rate就是在player調(diào)用play的時候變?yōu)?,調(diào)用pause的時候變?yōu)?翘贮,它的值不根據(jù)卡不卡變化赊窥,它應(yīng)該是用來決定當load到新數(shù)據(jù)是要不要繼續(xù)播放。所以我感覺rate是沒有必要用KVO觀察狸页。當然如果要做倍率播放或者慢速播放锨能,那估計會用到,到時候再處理芍耘。
5址遇,監(jiān)聽APP進入前臺或者后臺
如果APP沒有申請后臺播放權(quán)限,那APP進入后臺的時候齿穗,AVPlayer就會被暫停傲隶,重新進入前臺之后會繼續(xù)播放(有時候不會開始播放。窃页。跺株。)。
這個有點不好控制脖卖,因為如果用戶是暫停了進入后臺的乒省,這種情況下回到前臺肯定還是需要是暫停狀態(tài)。
這里我參考了其他播放器的做法畦木,添加了一個 statusBeforeBackground屬性袖扛,用來記錄APP進入后臺之前的播放狀態(tài),
然后監(jiān)聽 UIApplicationWillResignActiveNotification和 UIApplicationDidBecomeActiveNotification兩個通知十籍,
在通知的回調(diào)中修改statusBeforBackground的狀態(tài)蛆封;
當進入后臺時,只有當statusBeforBackground是unknown的時候勾栗,才會記錄當前播放狀態(tài)惨篱,然后暫停;
當進入前臺時围俘,只有當statusBeforBackground記錄到的狀態(tài)是playing || stalling || readToPlay時砸讳,才會繼續(xù)播放,并將statusBeforBackground重置為unknown界牡。
這里這么寫簿寂,主要是因為,APP在前臺時拉下通知欄宿亡,會讓APP進入inactive狀態(tài)常遂,這時候不知道為什么和通知會被觸發(fā)兩次,狀態(tài)有點混亂挽荠,所以只能暫時這么特殊處理下烈钞,
如果有什么更好的解決辦法泊碑,再優(yōu)化。毯欣。。
6臭脓,監(jiān)聽網(wǎng)絡(luò)狀態(tài)變化
一般來說酗钞,網(wǎng)絡(luò)狀態(tài)從wifi變?yōu)榉涓C網(wǎng)絡(luò)的時候,要暫停播放器来累,這個應(yīng)該由播放器外部來控制砚作。
但測試的時候發(fā)現(xiàn)了一種特殊情況:正在播放的時候把APP切到后臺,關(guān)掉網(wǎng)絡(luò)嘹锁,再切回APP葫录,播放器會暫停一下,再繼續(xù)播放领猾。米同。。
這是因為上面監(jiān)聽APP進入后臺的機制摔竿,進入后臺的時候記錄到的statusBeforBackground是playing面粮,所以返回前臺時觸發(fā)UIApplicationDidBecomeActiveNotification通知,會再調(diào)用play方法继低,通知觸發(fā)的時機是在外部調(diào)用暫停之后的熬苍!
所以我這里監(jiān)聽了一下網(wǎng)絡(luò)狀態(tài)的變化,當網(wǎng)絡(luò)狀態(tài)變化為非wifi時袁翁,將statusBeforBackground設(shè)置為paused柴底。
理論上,如果外部調(diào)用暫停方法的時候粱胜,將statusBeforBackground重置為unknown也是可以的柄驻。但這樣又要多判斷一下是用戶暫停還是通知造成的暫停,
我也不確定那種方式更好年柠,暫時用監(jiān)聽網(wǎng)絡(luò)變化的方法了凿歼。。冗恨。
7答憔,內(nèi)存管理
AVPlayer必須要有一個類強引用一下,否則它不知道什么時候就釋放掉了掀抹,這樣會導(dǎo)致kvo沒有取消觀察者之類的crash虐拓。
這個PlayerView也是如此,測試的時候出現(xiàn)過playerView的View還在(因為已經(jīng)添加到其他view上),但palyerView本身卻被釋放掉的bug傲武,千萬注意蓉驹!
2018年11月29日更新:
8城榛,內(nèi)存管理之AVPlayerLayer
AVPlayerLayer會retain其相關(guān)的AVPlayer,所以釋放的時候态兴,必須主動將AVPlayerView的player設(shè)置為nil狠持,否則即使player被設(shè)置為nil了,player還是不會釋放(因為還有其他地方強引用嘛)瞻润。這個問題坑了我好久喘垂,需特別注意一下!
9绍撞,seek方法的問題
統(tǒng)計到如下錯誤
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的狀態(tài)還沒有變成readyToPlay之前傻铣,seek方法是肯定會報錯章贞!當狀態(tài)變成readyToPlay之后,如果seek的time是非法的非洲,也會報錯鸭限,所以在seek之前就需要加兩個判斷。
readToPlay的判斷只能用kvo觀察AVPlayerItem的方式來做怪蔑,加個內(nèi)部變量就好里覆,
判斷seek的time是否合法,系統(tǒng)提供了函數(shù):
if (CMTIME_IS_INDEFINITE(time) || CMTIME_IS_INVALID(time)) {
return;
}
10, 切換視頻清晰度缆瓣,界面可能會閃一下的問題
切換清晰度其實就是換個url喧枷,然后從剛剛的進度繼續(xù)開始播。這就需要保存當前播放進度弓坞,等切換的playerItem的狀態(tài)變?yōu)閞eadToPlay的時候隧甚,seek到這個時間點開始播放。界面會閃渡冻,大概率是因為seek之前戚扳,播放器是處于play狀態(tài)的,所以playerItem會直接從0開始播放族吻,而seek方法是異步的帽借,所以在從指定時間點播放之前可能已經(jīng)播了一點點,seek完成之后直接開始播放指定時間的內(nèi)容超歌,造成界面閃一下砍艾。
正確的做法是:在seek之前,暫停視頻的播放巍举,在seek完成的回調(diào)中再繼續(xù)播放脆荷。
11,seek導(dǎo)致播放狀態(tài)不對的問題
因為上面10的原因,可能需要在readToPlay的時候直接seek到某一時間點蜓谋,而我之前寫的邏輯是player的狀態(tài)只有在有限的情況下才會變梦皮,所以這里可能會導(dǎo)致player的狀態(tài)一直保持在readToPlay而沒有切換到playing。桃焕。剑肯。這個問題比較坑,暫時沒想到什么特別好的解決辦法覆旭,現(xiàn)在暫時hardCode解決:在seek方法之后加了個判斷退子,如果原來狀態(tài)是readToPlay,那seek之后型将,會設(shè)置為playing。
從stop狀態(tài)seek問題同上荐虐,暫時也是hardCode解決
12七兜,暫停時,網(wǎng)絡(luò)加載異步回調(diào)導(dǎo)致player狀態(tài)變化的問題
這個也比較坑福扬,因為網(wǎng)絡(luò)加載是異步回調(diào)的腕铸,所以用戶手動點了暫停之后,可能過了幾秒鐘下載了新的內(nèi)容回來铛碑,kvo會觀察到playbackLikelyToKeepUp
變化狠裹,這時候按理說不應(yīng)該修改播放器的狀態(tài)。汽烦。涛菠。
從11,12的問題來看撇吞,用kvo來確定player狀態(tài)這個設(shè)計貌似不是很合理俗冻。。牍颈。得考慮怎么優(yōu)化一下了F !煮岁!
暫時總結(jié)到這些讥蔽,等發(fā)現(xiàn)其他的再補充