雲音樂 Android 視頻「無縫」播放實現總結

圖片來源:bz.zzzmh.cn/前端

本文做者:王永亮git

在網易雲音樂 8.0 改版中,接到一個播放中的視頻能夠點擊「小窗」按鈕收起到 mini 播放條中繼續播放的需求,剛接到這個需求時心裏是崩潰的,要知道網易雲音樂的 mini 播放條是一個可能會出如今 App 中的任何 Activity 上的 View,在不一樣 Activity 之間跳轉時,如何能保證視頻能夠從一個 Activity 「無縫」轉移到另外一個 Activity 呢?github

MediaPlayer 換綁

通常簡單的視頻播放功能我會使用系統自帶的 VideoView,只需幾行代碼就可讓視頻播放起來,系統自帶的 VideoView 繼承自 SurfaceView,而且將 MediaPlayer 的具體調用,包括 Surface 和 MediaPlayer 的綁定封裝在裏面,這樣封裝的優點是簡單易用,可是也存在一些問題,SurfaceView 和 MediaPlayer 徹底綁定在一塊兒,一個 MediaPlayer 只能對應一個 SurfaceView,而小窗播放想作到的是 MediaPlayer 和 SurfaceView 能夠一對多,在頁面切換時 MediaPlayer 能夠綁定新的 SurfaceView,就像一臺電腦對應多個顯示器。咱們的視頻播放框架很好的解決了這個問題,以下圖所示:緩存

視頻播放架構

因爲 App 中有一些對視頻作動畫的場景,因此框架中使用的是 TextureView,TextureView 和 MediaPlayer 使用 AIDL 進行通訊,以下圖所示:markdown

aidl通訊架構

從上面兩圖能夠看出,視頻播放框架中把全部的 MediaPlayer 放到了一個單獨的 Video 進程的緩存池中來管理,正在使用的放在 Active 的池子中,閒置在 idle 池子中,閒置的 MediaPlayer 超過上限時會被回收,啓動新頁面時 VideoView 能夠從 Video 進程的池子中獲取閒置的 MediaPlayer,其餘進程中的 VideoView 經過 AIDL 同 Video 進程中的 MediaPlayer 通訊。架構

這種架構使不一樣 Activity 中的 VideoView 能夠很方便的替換其綁定的 MediaPlayer,因爲播放能力都在 MediaPlayer 中,因此在 MediaPlayer 同 TextureView 解綁時並不會致使播放的中斷,新頁面啓動時,只要將正在播放的 MediaPlayer 同 TextureView 從新綁定,新的頁面就能馬上展現播放中的畫面了。實際上視頻播放框架最先並非服務於無縫播放的場景,設計最先是出於如下緣由:app

  1. 自研的 MediaPlayer 在上線早期穩定性並無那麼好,使用多進程能夠防止播放器異常影響主進程其餘功能
  2. 普通 VideoView 只能支持一個 TextureView 一個 MediaPlayer,而視頻播放優化須要額外的視頻播放器實現視頻預加載的能力
  3. 減小主進程內存佔用,避免視頻播放對音頻播放等重要業務產生影響
  4. 播放器複用減小對象建立

因此綜合以上需求咱們設計了這套 MediaPlayer 和 TextureView 隔離的方案,若是是比較簡單場景也能夠考慮使用單例持有 MediaPlayer,因爲這套方案已經很好的將 MediaPlayer 和 TextureView 隔離,因此咱們只須要經過給 MediaPlayer 池子增長一些獲取被複用播放器的方法就能夠很容易的支持 VideoView 和 MediaPlayer的換綁,從得到無縫播放的效果了。框架

具體的換綁 MediaPlayer 流程以下圖:ide

在原有 Activity 中,若是播放器是要被複用的,咱們會將播放器的惟一 id 和正在播放的資源 id 保存在一個全局位置,以此做爲播放器可複用的標誌。在新頁面啓動時,新頁面的 VideoView 被建立,在新頁面中會調用 VideoView 的 setDataSource 設置要播放的內容,setDataSource 會根據當前播放的內容和保存的全局播放器 id 在播放器池子中從新找到原來正在播放的播放器,並將 Surface 經過 AIDL 發送給被複用的 MediaPlayer 從新綁定,這樣在不打斷當前播放的狀況下,視頻播放的畫面就無縫被轉移到新的 Activity 中了。其中要注意的一個知識點是Surface自己就是支持跨進程傳遞的:函數

public class Surface implements Parcelable
複製代碼

另外這個方案中使用 MediaPlayer 對象的 hashCode 做爲播放器的惟一 id,若是使用這個方案,你們也能夠結合本身的狀況設計惟一id。

換綁方案的核心是 MediaPlayer 同 VideoView 的從新綁定,從新綁定只須要作到下面兩步:

  1. 使用當前頁面中定義的 onPrepare、onPause 等回調設置給播放器替換原有回調
  2. 從新綁定 Surface,須要注意的是有時 SurfaceTexture 並非馬上就能準備好,沒準備好時能夠在 onSurfaceAvailable 中從新綁定 SurfaceTexture。

這個方案基本上能知足絕大部分的無縫播放需求,不過也並不是沒有缺點,這個方案主要有如下幾個問題:

  1. 在視頻暫停時 MediaPlayer 從新綁定 TextureView,Surface 上會沒有內容,這種狀況能夠先使用視頻封面覆蓋 TextureView,等從新播放時再移除封面。
  2. 原來的方案設計中將 AudioFocus 的獲取封裝在了 VideoView 中,在新的頁面使用 MediaPlayer 開始播放時,因爲原來持有 MediaPlayer 頁面還繼續持有着 MediaPlayer 的引用,因此會由於 AudioFocus 搶佔而調用 MediaPlayer 的 pause 方法,從而形成在新的頁面播放也會暫停,解決方案是將 AudioFocus 的監聽放在 Video 進程中的 MediaPlayer 中,你們在使用這個方案時也能夠注意下有沒有相似問題。
  3. 原來播放器使用完成會跟隨頁面銷燬而被回收,在複用場景是不能回收的,這時要注意避免播放器泄漏。

終實現效果以下圖:

「假」頁面切換方案

在換綁方案以外,網易雲音樂中也有一些其餘的無縫播放方案實現,首先介紹一種實現比較簡單,也是在網易雲中比較早使用的一種方案,「假」頁面切換方案,由名字能夠知道,這種方案不是真正的在 Activity 之間進行跳轉,而是利用 TextureView 能夠像普通 View 同樣移動、作動畫的特性,利用過渡動畫,讓效果看起來像是從一個頁面跳轉到了另外一個頁面,效果以下圖所示:

在網易雲音樂的視頻 Feed 流中,視頻播放時,點擊熱區能夠在不暫停播放的狀況下「展開」到播放詳情頁,具體的實現方法是將視頻播放的 View 放在 Fragment 中,Fragment 的 Container 放在整個 ViewTree 的最頂層,點擊播放時,將視頻播放 Fragment 移動到須要展現視頻的位置並開始播放,須要點擊進入詳情頁時,只須要對視頻播放的 Fragment 作平移和縮放動畫,在視頻播放的 Fragment 下方再添加評論等其餘的 Fragment。這裏能夠參考 Android 原生的 VideoView 的封裝思想來實現:

public class VideoView extends SurfaceView
        implements MediaPlayerControl, SubtitleController.Anchor {
複製代碼

參考 VideoView 源碼能夠將 SurfaceView 替換爲 TextureView ,再對應處理下 onSurfaceTextureAvailable 等回調便可。這種方案應用仍是比較普遍的,好比京東、淘寶等的商品詳情的介紹視頻。這種方案雖然簡單可是侷限也比較大,只能解決在同一個 Activity 中的場景,若是需求是在不一樣 Activity 中無縫播放切換這個方案就沒法知足了。

不一樣頁面間播放器 Seek 方案

實現跨 Activity 場景無縫播放的另外一個方案是打開新的頁面時,在新的頁面中使用新播放器從新打開資源,並根據原來保存的進度從新 seek 後再續播,這種方案其實並不能保證真正的「無縫」播放,畢竟 Activity 啓動也要消耗一兩百毫秒的時間,不過這個方案最大的優點是一些老邏輯進行不多的更改就能夠支持無縫播放功能,好比在一些不是很重要的頁面中,視頻播放功能可能已經存在而且播放邏輯耦合了很重的業務邏輯,這時 seek 方案就比較合適了。

這個方案雖然簡單可是也有一些須要注意的地方:

  1. 緩存複用提高體驗。播放在線視頻時可使用播放緩存複用來提高用戶體驗,視頻播放緩存能夠採用 url 代理下載的方式實現,通常作法是啓動 Local Http Server 將視頻播放的請求代理到本地 server,在本地 server 中將視頻文件存儲到指定的位置,這也是視頻播放中比較經常使用的方案,緩存功能的能夠參考 AndroidVideoCache
  2. 系統提供的 MediaPlayer 只能 Seek 到關鍵幀的問題。使用系統播放器從新打開視頻資源 seek 時,有時會沒法 seek 到原來播放的位置,甚至會直接跳轉到視頻播放的開始或者結束位置,視頻越短壓縮率越高就會越明顯,這就是關鍵幀的問題。關鍵幀問題的解決方案是能夠是基於 MediaCodec 本身實現播放器,能夠參考或直接使用Google開源的 ExoPlayer

關鍵幀被稱爲 I 幀,能夠被看作是一幀沒有壓縮過的畫面,解碼的時候無需依賴其餘幀,關鍵幀之間還存在 B 幀和 P 幀這樣的壓縮幀,須要依賴其餘幀才能解碼出完整的畫面,兩個關鍵幀之間的間隔被稱爲一個 GOP,在 GOP 內的幀系統播放器是沒辦法直接 seek 的。

4. View 跨 Activity 複用方案

View 跨 Activity 複用是指手動使用 ApplicationContext 建立須要被複用的 View,而且使用單例 Manager 持有該 View,添加刪除可複用 View 能夠統一在 Activity 生命週期函數中實現,示例代碼以下:

object Manager : ActivityLifecycleCallbacks
    override fun onActivityStarted(activity: Activity) {
        ...
        removePlayerBarFromWindow(activity)
        addPlayerBarToWindow(activity)
    }

    override fun onActivityPaused(activity: Activity) {
        ...
        if (activity.isFinishing && getMiniPlayerBarParentContext() == activity) {
            removePlayerBarFromWindow(activity, true)
        }
    }
複製代碼
private fun getPlayerBar(activityBase: Activity): MiniPlayerBar {
        synchronized(this) {
            if (miniPlayerBar == null) {
                miniPlayerBar = MiniPlayerBar(activityBase.applicationContext)
            }
            ...
            return miniPlayerBar!!
        }
    }
複製代碼

理論上這是一種更加靈活的方案,使用 Application 做爲 View 的 Context 也不用擔憂泄漏問題,不過因爲在此次小窗的需求中涉及到老的頁面和新頁面的播放器複用,在不少場景下並非一個統一的播放View,因此沒有采用這種方案,不過這個方案在網易雲音樂的音街 App 的 mini 播放條上已經被使用,有興趣的小夥伴也能夠嘗試下。

總結

以上是網易雲音樂中一些無縫播放的方案的總結,主要介紹了一下網易雲音樂中幾種無縫播放能力的實現思路,給你們方案選型作參考,若是有其餘的方案也歡迎交流。網易雲音樂中的方案是從簡單到複雜逐漸演進而來,隨着需求不斷迭代變成今天的樣子,我的理解設計方案時不用過度的追求大而全,適合當前場景的纔是最好的,好的架構不只要靠好的設計,也要靠不斷的改進優化。

本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe(at)corp.netease.com!

相關文章
相關標籤/搜索