產品新訊

提升媒體播放體驗:深入瞭解 Media3 的 PreloadManager - 第 2 部分

9 分鐘閱讀
Mayuri Khinvasara Khabya
開發人員關係工程師

歡迎收看媒體預先載入系列影片的第二部,瞭解如何使用 Media3 預先載入媒體。本系列文章旨在引導您在 Android 應用程式中,建構高回應速度、低延遲的媒體體驗。

  • 第 1 部分:透過 Media3 介紹預先載入涵蓋了基本概念。我們探討了簡單播放清單的 PreloadConfiguration,以及動態使用者介面的更強大 DefaultPreloadManager 之間的差異。您已瞭解如何實作基本 API 生命週期:使用 add() 新增媒體、使用 getMediaSource() 擷取準備好的 MediaSource、使用 setCurrentPlayingIndex() 和 invalidate() 管理優先順序,以及使用 remove() 和 release() 釋出資源。
  • 第 2 部分 (本文):在本網誌中,我們將探討 DefaultPreloadManager 的進階功能。我們將說明如何使用 PreloadManagerListener 取得深入分析、實作可部署於正式環境的最佳做法 (例如與 ExoPlayer 共用核心元件),以及掌握滑動視窗模式,有效管理記憶體。
  • 第 3 部分:本系列文章的最後一部分將深入探討如何整合 PreloadManager 與持續性磁碟快取,讓您透過資源管理減少資料消耗量,並提供流暢的體驗。

如果您是 Media3 預先載入功能的新手,強烈建議先閱讀第 1 部分,再繼續操作。如果您已準備好進階功能,請繼續瞭解如何提升媒體播放實作成效。

監聽:使用 PreloadManagerListener 擷取分析資料

應用程式開發人員在正式版中推出功能時,也想瞭解並擷取背後的數據分析資料。如何確定預先載入策略在實際環境中有效?如要回答這個問題,需要成功率、失敗次數和效能資料。PreloadManagerListener 介面是收集這項資料的主要機制。

PreloadManagerListener 提供兩個重要回呼,可深入瞭解預先載入程序和狀態。

  • onCompleted(MediaItem mediaItem):成功完成預先載入要求時,系統會叫用這個回呼,如 TargetPreloadStatusControl 所定義。
  • onError(PreloadException error):這個回呼可用於偵錯和監控。預先載入失敗時,系統會叫用此方法,並提供相關聯的例外狀況。

您可以透過單一方法呼叫註冊事件監聽器,如以下程式碼範例所示:

val preloadManagerListener = object : PreloadManagerListener {
    override fun onCompleted(mediaItem: MediaItem) {
        // Log success for analytics. 
        Log.d("PreloadAnalytics", "Preload completed for $mediaItem")
    }

    override fun onError( preloadError: PreloadException) {
        // Log the specific error for debugging and monitoring.
        Log.e("PreloadAnalytics", "Preload error ", preloadError)
    }
}

preloadManager.addListener(preloadManagerListener)

從監聽器擷取洞察資訊

這些事件監聽器回呼可連結至您的數據分析管道。將這些事件轉送至分析引擎,即可解答下列重要問題:

  • 預先載入成功率為何?(onCompleted 事件與預先載入嘗試總數的比率)
  • 哪些 CDN 或影片格式的錯誤率最高?(By parsing the exceptions from onError)
  • 預先載入錯誤率為何?(onError 事件與預先載入嘗試總數的比率)

這項資料可提供預先載入策略的量化意見回饋,讓您進行 A/B 測試,並根據資料改善使用者體驗。這項資料可進一步協助您智慧微調預先載入時間長度、預先載入的影片數量,以及分配的緩衝區。

除了偵錯之外:使用 onError 進行 UI 安全回溯

預先載入失敗是使用者即將發生緩衝事件的強烈指標。onError 回呼可讓您做出反應。您可以調整 UI,而不只是記錄錯誤。舉例來說,如果即將播放的影片無法預先載入,應用程式可能會在下一次滑動時停用自動播放功能,要求使用者輕觸螢幕開始播放。

此外,檢查 PreloadException 型別可定義更智慧的重試策略。應用程式可以根據錯誤訊息或 HTTP 狀態碼,選擇立即從管理工具中移除失敗的來源。因此,必須從 UI 串流中移除該項目,以免載入問題影響使用者體驗。您也可以從 PreloadException 取得更精細的資料,例如 HttpDataSourceException,進一步探究錯誤。進一步瞭解如何排解 ExoPlayer 問題

好友系統:為什麼必須與 ExoPlayer 共用元件?

DefaultPreloadManager 和 ExoPlayer 的設計宗旨是互相搭配運作。為確保穩定性和效率,這些應用程式必須共用多個核心元件。如果這些元件各自運作,且彼此之間沒有協調,可能會影響執行緒安全性和播放器上預先載入曲目的可用性,因為我們必須確保預先載入的曲目會在正確的播放器上播放。個別元件也可能爭奪有限的資源 (例如網路頻寬和記憶體),導致效能下降。生命週期中重要的一環是妥善處置,建議的處置順序是先釋出 PreloadManager,再釋出 ExoPlayer。

DefaultPreloadManager.Builder 的設計宗旨是簡化這項共用作業,並提供 API 來例項化 PreloadManager 和連結的播放器例項。讓我們看看為何必須共用 BandwidthMeter、LoadControl、TrackSelector、Looper 等元件。查看視覺化表示法,瞭解這些元件如何與 ExoPlayer Playback 互動。

preloadManager2.png

使用共用的 BandwidthMeter 避免頻寬衝突

BandwidthMeter 會根據歷來傳輸速率,預估可用的網路頻寬。如果 PreloadManager 和播放器使用不同的執行個體,兩者不會知道彼此的網路活動,這可能會導致失敗情況。舉例來說,假設使用者正在觀看影片,但網路連線品質變差,預先載入的 MediaSource 同時啟動了未來影片的積極下載作業。預先載入 MediaSource 的活動會耗用作用中播放器所需的頻寬,導致目前的影片停滯。播放期間發生停頓是嚴重的使用者體驗失敗。

只要共用單一 BandwidthMeter,TrackSelector 就能在預先載入或播放期間,根據目前的網路狀況和緩衝區狀態,選取最高品質的軌。然後做出智慧決策,保護正在播放的內容並確保流暢體驗。

preloadManagerBuilder.setBandwidthMeter(customBandwidthMeter)

確保與 ExoPlayer 的共用 LoadControl、TrackSelector、Renderer 元件保持一致

  • LoadControl:這個元件會決定緩衝政策,例如開始播放前要緩衝多少資料,以及何時開始或停止載入更多資料。共用 LoadControl 可確保播放器和 PreloadManager 的記憶體用量,會受到預先載入和主動播放媒體的單一協調緩衝策略引導,避免資源爭用。您必須根據預先載入的項目數量和時間長度,妥善分配緩衝區空間,確保一致性。發生爭用時,播放器會優先播放畫面上顯示的目前項目。使用共用的 LoadControl 時,只要為預先載入分配的目標緩衝區位元組尚未達到上限,預先載入管理工具就會繼續預先載入,不會等到載入播放完成。

注意:在最新版本 Media3 (1.8) 中共用 LoadControl,可確保 Allocator 能正確與 PreloadManager 和播放器共用。使用 LoadControl 有效控制預先載入功能,將在日後推出的 Media3 1.9 版本中提供。

preloadManagerBuilder.setLoadControl(customLoadControl)

  • TrackSelector:這個元件負責選取要載入及播放的音軌 (例如特定解析度的影片、特定語言的音訊)。分享可確保預先載入的音軌與播放器使用的音軌相同。這樣可避免浪費資源,因為預先載入 480p 影片軌後,播放器會立即捨棄該軌,並在播放時擷取 720p 軌。< br />預先載入管理工具「不應」與播放器共用 TrackSelector 的相同例項。他們應改用不同的 TrackSelector 執行個體,但實作方式相同。因此,我們在 DefaultPreloadManager.Builder 中設定的是 TrackSelectorFactory,而不是 TrackSelector。

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

  • Renderer:這個元件負責瞭解播放器的功能,不必建立完整的 Renderer。並根據這份藍圖,判斷最終播放器支援哪些影片、音訊和文字格式。這樣一來,播放器就能智慧選取並下載相容的媒體軌,避免浪費頻寬下載無法播放的內容。

preloadManagerBuilder.setRenderersFactory(customRenderersFactory)

進一步瞭解 ExoPlayer 元件

黃金法則:使用通用的 Playback Looper 掌控一切

建立播放器時,您可以傳遞 Looper,明確指定可存取 ExoPlayer 執行個體的執行緒。可以使用 Player.getApplicationLooper 查詢必須存取播放器的執行緒 Looper。在播放器和 PreloadManager 之間維護共用的 Looper,可確保這些共用媒體物件的所有作業都會序列化至單一執行緒的訊息佇列。這有助於減少並行錯誤。

PreloadManager 與播放器之間的所有互動,以及要載入或預先載入的媒體來源,都必須在同一個播放執行緒中進行。為了確保執行緒安全,必須共用 Looper,因此我們必須在 PreloadManager 和播放器之間共用 PlaybackLooper。

PreloadManager 會在背景準備有狀態的 MediaSource 物件。當 UI 程式碼呼叫 player.setMediaSource(mediaSource) 時,您會將這個複雜的具狀態物件從預先載入的 MediaSource 交給播放器。在此情境中,整個 PreloadMediaSource 會從管理工具移至播放器。所有這些互動和交接都應在同一個 PlaybackLooper 上進行。

如果 PreloadManager 和 ExoPlayer 在不同執行緒上運作,可能會發生競爭狀況。在播放器的執行緒嘗試從 MediaSource 讀取資料的當下,PreloadManager 的執行緒可能會修改 MediaSource 的內部狀態 (例如將新資料寫入緩衝區)。這會導致無法預測的行為,以及難以偵錯的 IllegalStateException。

preloadManagerBuilder.setPreloadLooper(playbackLooper)

讓我們看看如何在設定中,於 ExoPlayer 和 DefaultPreloadManager 之間共用上述所有元件。

val preloadManagerBuilder =
DefaultPreloadManager.Builder(context, targetPreloadStatusControl)

// Optional - Share components between ExoPlayer and DefaultPreloadManager
preloadManagerBuilder
     .setBandwidthMeter(customBandwidthMeter)
     .setLoadControl(customLoadControl)
     .setMediaSourceFactory(customMediaSourceFactory)
     .setTrackSelectorFactory(customTrackSelectorFactory)
     .setRenderersFactory(customRenderersFactory)
     .setPreloadLooper(playbackLooper)

val preloadManager = val preloadManagerBuilder.build()

提示:如果您在 ExoPlayer 中使用 Default 元件 (例如 DefaultLoadControl 等),就不必明確與 DefaultPreloadManager 共用這些元件。透過 DefaultPreloadManager.Builder 的 buildExoPlayer 建構 ExoPlayer 執行個體時,如果使用預設實作項目和預設設定,這些元件會自動互相參照。但如果您使用自訂元件或自訂設定,請透過上述 API明確通知 DefaultPreloadManager。

可供正式環境使用的預先載入功能:滑動視窗模式

在動態動態饋給中,使用者可以捲動瀏覽幾乎無限量的內容。如果您持續將影片新增至 DefaultPreloadManager,但沒有相應的移除策略,就難免會導致 OutOfMemoryError。每個預先載入的 MediaSource 都會保留 SampleQueue,用於分配記憶體緩衝區。如果這類物件累積過多,可能會耗盡應用程式的堆積空間。解決方法是您可能已熟悉的演算法,稱為滑動視窗。滑動視窗模式會在記憶體中保留一小組可管理項目,這些項目在動態消息中與使用者目前的位置邏輯上相鄰。使用者捲動畫面時,這個受管理項目的「視窗」也會隨之滑動,加入新顯示的項目,並移除現在距離較遠的項目。

slidingwindow.png

實作滑動視窗模式

請務必瞭解,PreloadManager 不提供內建的 setWindowSize() 方法。滑動視窗是一種設計模式,開發人員必須使用原始的 add() 和 remove() 方法實作。您的應用程式邏輯必須將捲動或頁面變更等 UI 事件,連結至這些 API 呼叫。如要取得相關程式碼參考資料,我們在 socialite 範例中實作了這個滑動視窗模式,其中也包含模仿滑動視窗的 PreloadManagerWrapper

當項目不太可能很快出現在使用者的觀看內容中時,請記得在實作中加入 preloadManager.remove(mediaItem)。如果無法移除不再靠近使用者的項目,是預先載入實作項目發生記憶體問題的主要原因。remove() 呼叫可確保釋出資源,協助您將應用程式的記憶體用量維持在一定範圍內,並保持穩定。

使用 TargetPreloadStatusControl 微調分類預先載入策略

定義要預先載入的內容 (視窗中的項目) 後,我們就能為每個項目套用明確定義的預先載入策略。我們已在第 1 部分中,瞭解如何透過 TargetPreloadStatusControl 設定達成這個精細程度。

回想一下,與位置 +/- 4 的項目相比,位置 +/- 1 的項目更有可能播放。您可以為使用者最可能觀看的項目分配更多資源 (網路、CPU、記憶體),這會根據鄰近程度建立「預先載入」策略,這是平衡即時播放與有效資源用量的關鍵。

如先前章節所述,您可以透過 PreloadManagerListener 使用 Analytics 資料,決定預先載入時間長度策略。

結論與後續步驟

您現在已具備進階知識,可使用 Media3 的 DefaultPreloadManager 快速建構穩定且資源效率高的媒體動態饋給。

重點回顧:

  • 使用 PreloadManagerListener 收集數據分析洞察資料,並導入完善的錯誤處理機制。
  • 請務必使用單一 DefaultPreloadManager.Builder 建立管理員和播放器執行個體,確保重要元件共用。
  • 主動管理 add() 和 remove() 呼叫,實作滑動視窗模式,避免發生 OutOfMemoryError。
  • 使用 TargetPreloadStatusControl 建立智慧分層預先載入策略,在效能和資源耗用之間取得平衡。

第 3 部分的後續內容:使用預先載入的媒體進行快取

將資料預先載入記憶體可立即提升效能,但可能需要付出代價。應用程式關閉或預先載入的媒體從管理員中移除後,資料就會消失。如要達到更持久的最佳化程度,我們可以將預先載入與磁碟快取結合。這項功能仍在積極開發中,預計在幾個月內推出。

有任何意見想分享嗎?期待收到您的回覆。

敬請期待,並快去加快影片播放速度吧!🚀

撰寫者:

繼續閱讀