Android我還可以相信你多少系列文章二之音視頻播放

音頻視頻播放在現在的應用裡面很常見,傳統應用發展到一定階段多少會引入音視頻資源,特別是現在短視頻被看作下一個增長爆發點,和之相關的創業層出不窮,作為開發者如何進行音視頻技術選型非常關鍵

MediaPlayer和VideoView給我們提供了非常方便的播放音視頻的能力,幾乎不需要要寫幾行代碼就可以完成。我們也可以使用MediaPlayer結合SurfaceView或者TextureView來實現視頻播放,本質和VideoView是一樣的,不過有更多的靈活性。

正因為封裝性太強,意味著定製化變弱。MediaPlayer提供的setDataSource方法支持http,file,content等協議,但仍然無法應對複雜的需求。所以更靈活的AudioTrack的出現,可以讓我們直接傳送解碼後的byte[]給他,帶來的問題就是自己要做解碼。解碼不是件簡單的事情,往往我們利用MediaCodec(Android4.1增加)或者外部解碼庫(比如ffmpeg)來實現。自己來實現解碼要特別注意不要丟失了硬體加速,音頻軟解碼還好,視頻解碼軟解碼對CPU壓力會大很多。

在做音視頻業務的時候,經常會遇到這樣幾個問題需要設置代理,或者邊播邊緩存,緩存加密,失敗重試,網路優化等等

因為我們無法干涉MediaPlayer的網路請求部分,所以一般會將原始的播放地址xxx.com/playurl轉換成本機代理地址http://127.0.0.1:port?url=htt...,這樣MediaPlayer就會來請求本機port埠上面起的一個代理服務,在這個代理端可以做很多優化邏輯,比如給真正發往服務端的請求加上代理;將請求到的數據寫入磁碟緩存,這個代理端可以根據磁碟緩存來按需請求服務端(使用http的Range參數);還有一些失敗重試等網路優化手段。這個代理層還有個特別的意義甚至可以接管webview裡面的audio和video標籤請求。

這種實現方式在實際運行中偶爾會出現本機代理無法啟動的情況,原因是Socket無法bind到指定埠,往往我們會在bind的時候指定讓系統來分配一個可用埠,所以這種失敗情況很有可能是root手機或者一些安全管理軟體禁用了許可權。

特別再說下邊播邊緩存的實現,緩存文件允許空洞,每個緩存文件配備另外一個內容索引文件,MediaPlayer本身會根據解碼情況發出多個帶Range的請求,根據內容索引文件來確定當前請求從文件哪個位置讀,接下去多少位元組從文件讀,多少位元組從網路讀,網路讀的部分同時寫迴文件以保證下次請求可以復用,這樣就實現了一個邊播邊緩存的邏輯,甚至我們還可以給本地緩存文件進行加密。同時這個緩存文件的載入百分比可以用來做UI界面上面的緩衝進度,監控下載速度進行網路請求優化。

2.MediaPlayer的Looper。新手往往可能不關心MediaPlayer的實現,打開它的構造器前面幾行代碼我們就會看到他默認使用的是當前線程的Looper,如果當前線程不是個Looper線程則使用MainLooper。這一點比較重要,因為我們知道即使MediaPlayer運行在Service裡面,實際上還在跑在主線程,這樣的結果導致後續所有的MediaPlayer回調操作都跑在主線程,這可能是隱藏的一個定時炸彈。

更優雅的設計我們建議將MediaPlayer的回調和主動操作(stop,reset等操作)都放入work線程,操作的串列化是種最簡單的設計,也是最有效的設計。大概的代碼形式是這樣的:

MediaPlayer在PlayHandlerThread裡面初始化,就保證了他裡面使用的Looper也是這個PlayHandlerThread的,這樣回調就都會在這個線程觸發,同時我們也在這個線程裡面做setDataSource等主動操作。

3.視頻播放本質上也是用MediaPlayer實現的,所以讀取數據上面沒有特別差異。現在比較熱的小視頻需要顯示在列表頁面支持滾動播放一個視頻,點擊在新頁面繼續觀看,一般採用MediaPlayer+TextureView來實現,MediaPlayer可以採用全局定義唯一一個,只是不同時刻把內容綁定顯示在不同的TextureView上而已。

4.MediaPlayer最大的問題還是在於其兼容性。從我們的經驗來看可能會有這些問題:音頻格式支持不全(ape,wma等原生系統不支持),未緩衝完不開始播放,播放過程中突然沒有聲音,播放存在跳幀,mediaserver died;視頻播放只有聲音沒有畫面,視頻格式兼容性差無法播放等。這些問題在系統基礎上基本無法解決。

最頭疼的問題是MediaPlayer返回的errorcode很多都是廠家擴展出來的,文檔上面提供的幾個值基本也是表意不清到底什麼問題。這給排查問題帶來很大麻煩。最最頭疼的是MediaPlayer的EventHandler裡面處理異常直接導致程序崩潰,比如像這樣:

11-04 13:43:08.966: E/AndroidRuntime(26482): java.lang.RuntimeException: failure code: -3211-04 13:43:08.966: E/AndroidRuntime(26482): at android.media.MediaPlayer.invoke(MediaPlayer.java:664)11-04 13:43:08.966: E/AndroidRuntime(26482): at android.media.MediaPlayer.getInbandTrackInfo(MediaPlayer.java:1692)11-04 13:43:08.966: E/AndroidRuntime(26482): at android.media.MediaPlayer.scanInternalSubtitleTracks(MediaPlayer.java:1851)11-04 13:43:08.966: E/AndroidRuntime(26482): at android.media.MediaPlayer.access$600(MediaPlayer.java:529)11-04 13:43:08.966: E/AndroidRuntime(26482): at android.media.MediaPlayer$EventHandler.handleMessage(MediaPlayer.java:2198)11-04 13:43:08.966: E/AndroidRuntime(26482): at android.os.Handler.dispatchMessage(Handler.java:102)11-04 13:43:08.966: E/AndroidRuntime(26482): at android.os.Looper.loop(Looper.java:137)11-04 13:43:08.966: E/AndroidRuntime(26482): at android.app.ActivityThread.main(ActivityThread.java:4998)11-04 13:43:08.966: E/AndroidRuntime(26482): at java.lang.reflect.Method.invokeNative(Native Method)11-04 13:43:08.966: E/AndroidRuntime(26482): at java.lang.reflect.Method.invoke(Method.java:515)11-04 13:43:08.966: E/AndroidRuntime(26482): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:777)11-04 13:43:08.966: E/AndroidRuntime(26482): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:593)11-04 13:43:08.966: E/AndroidRuntime(26482): at dalvik.system.NativeStart.main(Native Method)

除了反射替換MediaPlayer裡面的EventHandler來抓住異常,其他沒啥特別好的辦法。

遇到這麼多問題開發者只能另投他路。市面上採用自解碼的方案也很多,比較主流的是使用MediaCodec和ffmpeg,ffmpeg更是因為MediaCodec版本限制原因,加上本來就聞名遐邇,被很多開發者青睞。主流的音視頻播放器大部分都是在這個上面進行改造的。

ExoPlayer:https://github.com/google/Exo... ,作為google在MediaCodec的封裝也是不錯的推薦,相比自己要去抽取ffmpeg代碼進行android適配編譯來得容易得多

ffmepg:當然也有一些現成的實現:https://github.com/search?o=d... ,最出名的當是ijkplayer,嗶哩嗶哩出品,跨平台還有彈幕。做視頻彈幕真是開箱即用。ffmpeg功能強大,唯一的缺點就是軟解碼,這也是他兼容性好的原因,我們知道硬解碼依賴各個廠家硬體實現兼容性自然就下降了。

在使用自解碼的時候,我們建議將自己的MediaPlayer封裝成Android高版本上面添加的介面一樣:

/** * Sets the data source (MediaDataSource) to use. * * @param dataSource the MediaDataSource for the media you want to play * @throws IllegalStateException if it is called in an invalid state * @throws IllegalArgumentException if dataSource is not a valid MediaDataSource */public void setDataSource(MediaDataSource dataSource) throws IllegalArgumentException, IllegalStateException { _setDataSource(dataSource);}

這樣做的好處是所有實現都對MediaPlayer透明,我們只需要定義好MediaDataSource介面,後面只需要專註於實現就可以了,比如HttpDataSource,FileDataSource,MemoryDataSource等。

或許自解碼會引入更多的不確定性,但是這一步遲早都要邁出去。推薦小型app或者需求不強的產品使用系統解碼,在我上面提到的一些解決思路上進行改進應該能滿足絕大部分場景。而那些音視頻作為主業務的產品則不得不面對自解碼來提高兼容性。

於是我們又在造輪子了;)

更多文章請關注微信公眾號:anzhuozhimei


推薦閱讀:

Android音頻模塊啟動流程分析
荔枝FM產品分析
Android Audio BSP工程師需要清楚的基本知識點
【IC.TECH】第二期 | 為什麼CD的採樣率是44.1KHz?
知識服務+付費+音頻,開啟內容生產新的產業級機遇,知識經濟10年千億級市場規模可期

TAG:Android | 音頻 | 視頻 |