AVPlayer播放網(wǎng)絡(luò)視頻踩坑記錄

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即可(即URLpublic 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)其他的再補充

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市画机,隨后出現(xiàn)的幾起案子冶伞,更是在濱河造成了極大的恐慌,老刑警劉巖色罚,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件碰缔,死亡現(xiàn)場離奇詭異,居然都是意外死亡戳护,警方通過查閱死者的電腦和手機金抡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進店門瀑焦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人梗肝,你說我怎么就攤上這事榛瓮。” “怎么了巫击?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵禀晓,是天一觀的道長。 經(jīng)常有香客問我坝锰,道長粹懒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任顷级,我火速辦了婚禮凫乖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘弓颈。我一直安慰自己帽芽,他們只是感情好,可當我...
    茶點故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布翔冀。 她就那樣靜靜地躺著导街,像睡著了一般。 火紅的嫁衣襯著肌膚如雪纤子。 梳的紋絲不亂的頭發(fā)上搬瑰,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天,我揣著相機與錄音计福,去河邊找鬼跌捆。 笑死,一個胖子當著我的面吹牛象颖,可吹牛的內(nèi)容都是我干的佩厚。 我是一名探鬼主播,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼说订,長吁一口氣:“原來是場噩夢啊……” “哼抄瓦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起陶冷,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤钙姊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后埂伦,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體煞额,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了膊毁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胀莹。...
    茶點故事閱讀 40,001評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖婚温,靈堂內(nèi)的尸體忽然破棺而出描焰,到底是詐尸還是另有隱情,我是刑警寧澤栅螟,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布荆秦,位于F島的核電站,受9級特大地震影響力图,放射性物質(zhì)發(fā)生泄漏步绸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一吃媒、第九天 我趴在偏房一處隱蔽的房頂上張望靡努。 院中可真熱鬧,春花似錦晓折、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至病梢,卻和暖如春若债,著一層夾襖步出監(jiān)牢的瞬間唠摹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留判呕,地道東北人。 一個月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓雌隅,卻偏偏與公主長得像窘问,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子隧期,可洞房花燭夜當晚...
    茶點故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內(nèi)容