優化播放器性能,我們首先要知道播放的完整流程,從播放的各個階段分析視頻播放存在的問題。
下面是播放的完整流程:
播放器加載一個網絡url,首先要進行網絡請求,網絡如何優化,涉及到網絡優化的方方面面。
網絡拉取回來數據之後,識別一下當前視頻的具體封裝格式,這個可以正式流式視頻,也可以是普通視頻,優化的手段有點不同。
識別到具體的封裝格式,按照封裝格式的要求,開始解析封裝格式,解析其中的音頻流、視頻流、字幕流等等。
音頻流要解碼成音頻原始數據,視頻流要解碼成視頻原始數據。
解碼過程中注意音視頻同步。
音頻播放,同時視頻開始渲染。
一、播放痛點
根據我們平時的開發實踐,我們總結出播放過程中常見的幾類問題:
播放失敗率高
播放首幀慢
播放卡頓
播放器占用CPU、內存過高
面對這些問題,我們急切需要知道兩方面的數據:
怎麼監控這些問題
怎麼解決這類問題
這兩個問題是有有遞進關係的,「怎麼監控這些問題」就是為了更好地「解決這類問題」。
二、監控手段
我們得知上面的痛點,在發生這些問題時,我們要收集相應的數據分析這類問題,不然開發者一頭霧水,不知道播放過程的數據信息,解決問題全靠運氣。
1、網絡加載監控
播放視頻首要的是網絡加載,網絡請求是一個複雜的過程,全鏈路的點太多,將全鏈路的所有點收集起來,可以在播放器中加上網絡的全鏈路監控:
這樣我們對網絡的整體加載情況有了全面的把握,發生網絡加載問題,也知道是哪個點出現了問題,分析解決問題有了更加全的數據。
2、播放器全鏈路監控
開篇就分析了播放器的完整流程,其實開發者也非常需要當前播放器的運行狀態:
播放器的工作狀態也可以拆解一下:
播放器發生狀態異常,開發者可以明確獲知播放器當前所處的狀態。
每個狀態都可能發生異常,發生異常都有具體的原因。利用播放器狀態、播放器出錯情況構建一個較為完善的播放監控體系。
3、播放器流暢度監控
播放卡頓,就是播放過程中發生loading,UI直接顯示轉圈,這對用戶體驗的損害是巨大的,用戶在不斷的吐槽中默默地卸載了我們的app。卡頓的主要原因是網絡狀況不好,很小的一部分原因是源的問題。
卡頓的次數
卡頓的時長
卡頓時的網速
單次播放平均卡頓次數和卡頓時間是我們衡量播放流暢度的重要指標。
如果是源的問題,例如出現播放視頻的時候,進度條在走,但是畫面不走,就是視頻解碼出現問題,但是又沒有出錯,只是解碼出的數據有問題。
解碼出的數據有問題,有兩種情況:原始數據就存在問題,這種情況下基本無法優化;另一種情況下是解碼線程異常。
MediaCodec發生異常
解碼線程異常錯誤
監控發生問題時系統codec的具體狀態,然後上報,便於分析問題。
三、播放成功率優化
播放失敗的原因很多,使用播放器播放視頻,最終都會在Player.onError回調中通知開發者播放失敗了,最多返回一個錯誤碼,對應一個播放錯誤。
總結而言,播放錯誤主要分為下面幾類:
網絡加載錯誤:網絡請求發生問題,可能是網絡請求的任何一個階段。
視頻格式識別錯誤:不支持當前的格式,或者當前格式識別出錯。
解碼出錯:不支持當前視頻、音頻解碼導致的出錯,或者系統codec異常導致的問題。
文件的IO異常:讀取緩存文件發生問題。
網絡加載錯誤一般要視情況而定,網絡超時要做好超時重試機制。
視頻格式支持使用ffmpeg能解決基本上所有的視頻格式的識別和處理工作。
MediaCodec解碼受到手機硬件的制約,解碼有時候會出錯,出錯可以切換到軟解碼。
四、播放性能優化
1、復用鏈接:
平時刷信息流視頻的時候,其實很多視頻的域名都是相同的,這些鏈接都是可以復用的,網絡建連的時間需要30ms到200ms不等,如果能復用鏈接,這部分的時間是可以節省下來的。
2、預加載
我們刷信息流的時候,還是會經常做預加載的的,但是通常預加載的做法是檔期實例化一個播放器player來播放視頻,再實例化一個player執行prepareAsync拉取數據,如果想預加載多個視頻的時候,就是實例化多個player執行prepareAsync,這樣做不妥。
一個播放器實例持有的數據非常大,player初始化的時候會初始化MediaCodec,MediaCodec對應底層的AVCodec,操作底層的/dev/codec-node,Android系統規定了系統最大持有的MediaCodec實例是16個,當然每個手機會有所不同,但總的來說不會有大的不同,就是MediaCodec的實例個數是有限的,不可能無限創建實例。
我們預加載多個播放器實例的時候,就會創建多個codec實例,超過codec實現限制,系統codec就無法正常工作,極易造成OOM或者ANR。
我們再平時解決問題的時候經常發現media.codec進程導致SystemServer卡死的。一般都是media.codec使用不當造成的。
那現在能否預加載不起播放器實例?
我們預加載的目的是為了請求視頻資源,其實只需要網路模塊就可以的。
本地代理可以實現將播放器的網絡模塊獨立出來:
播放器不直接和視頻源服務器交互,中間通過本地代理層交互。
本地代理層的網絡加載模塊是獨立於播放器的,可以是播放器發起請求,也可以是其他的外部調用發起請求。
最終setDataSource到播放器的url是一個http://127.0.0.1:port的請求,本地代理層會通過Socket向這個url中發送數據,播放器可以直接解析數據流。跟正常的播放流程完全一樣。
這樣我們可以全局持有一個播放器實例:既可以做到預加載,而且可以解決播放器占用資源過多的問題。一舉多得。
3、指定封裝格式和解碼格式:
針對一些視頻,我們已經明確知道它們的封裝格式和音視頻的編解碼格式,那我們就可以提前告知播放器這些信息,播放器直接使用特定的封裝格式去嗅探,直接起特定的解碼器去解碼。
例如信息流視頻基本上都是MP4的封裝格式,H264的視頻編碼,AAC的音頻編碼。
這樣我們可以節省嗅探和MediaCodec檢索的時間。
4、MP4視頻優化:
MP4格式的視頻解析出來如下:
其中moov中包含着MP4文件特有的屬性數據,mdat是具體的音頻和視頻數據,MP4格式規定,只有解析出moov數據之後,才能解析出mdat中具體的音頻和視頻數據。
但是moov有時候在mdat之前,有時間在mdat之後,如上,moov在mdat之前,那麼我們順序請求就沒有問題。
但是如果moov在mdat之後,我們順序請求就播放不出來,這時候需要起雙IO緩衝加載:一個從頭開始檢索moov,另一個從末尾檢索moov,雖找到moov,就可以先解析出moov,然後解析mdat,播放視頻了。
但是雙IO畢竟比較耗時,如果能在服務器提前將MP4視頻的moov移到mdat之前,就可以提升MP4的首幀。
5、流式視頻優化:
除了MP4視頻,還有一些流式點播的視頻,例如HLS格式,這些視頻是一個一個的ts分片組成的。針對這些視頻首幀的優化,我建議直接將前幾個ts的分片的數據壓縮,例如針對一個3s的ts視頻,原來的分辨率是1280 * 720,現在可以壓縮到320 * 180的大小,數據量大大降低,這樣首幀就能快速加載下來。
6、邊下邊播
我們在播放視頻的時候,最好能邊播放邊緩存到本地,這樣我二次打開這個視頻的時候,就可以不用請求了,直接復用本地的數據。
邊下邊播可以使用本地代理來完成。
7、video-id復用緩存
我們實行邊下邊播之後,二次打開可以復用緩存了,但是我們是根據視頻的url來復用的,現在信息流視頻的url經常變化的,即使是同一個視頻,半小時視頻url就發生了變化。
那這樣的復用效率不是很低嗎?
還好現在信息流都是傳過來一個video-id,這個video-id不會隨着視頻url變化而變化,只要是同一個視頻,video-id不會變化,那我們可以利用這個video-id來實現一次緩存,多次復用。
五、其他播放體驗優化建議
1、播放的時候出現丟幀
播放時丟幀主要是直播應用會出現,當出現播放丟幀的情況,服務器應該主動推流低碼率的流,防止客戶端出現丟幀嚴重甚至卡住不走的情況。
2、播放畫面出現鋸齒
播放視頻的時候出現鋸齒,這時候一般是兩種情況:
視頻清晰度較高,MediaCodec對搞清的視頻解碼支持地較弱,畫面細膩感不強。建議切換到軟解碼開始解碼,軟解碼使用CPU解碼,對細節的支持度很強,甚至8K的視頻都能很好的解析。
使用GLSurfaceView替換SurfaceView或者TextureView,GLSurfaceView通過OpenGL繪製紋理實現視頻細節的細膩繪製,對視頻畫面支持效果很好。
技術交流,歡迎加我微信:ezglumes ,拉你入技術交流群。
私信領取相關資料
推薦閱讀:
音視頻開發工作經驗分享 || 視頻版
OpenGL ES 學習資源分享
開通專輯 | 細數那些年寫過的技術文章專輯
NDK 學習進階免費視頻來了
你想要的音視頻開發資料庫來了
推薦幾個堪稱教科書級別的 Android 音視頻入門項目
覺得不錯,點個在看唄~
