產品新訊

提升媒體播放體驗:深入瞭解 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)

從監聽器擷取洞察資訊

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

  • 預先載入成功率為何?(onCompleted 事件與預先載入嘗試總次數的比率)
  • 哪些 CDN 或影片格式的錯誤率最高?(透過剖析 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 就能在預先載入或播放期間,根據目前的網路狀況和緩衝區狀態,選取最高品質的音軌。接著,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。它會檢查這個藍圖,瞭解最終播放器支援的影片、音訊和文字格式。這樣一來,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 共用這些元件。透過 buildExoPlayer 的 DefaultPreloadManager.Builder 建構 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 收集 Analytics 洞察資料,並實作完善的錯誤處理機制。
  • 請務必使用單一 DefaultPreloadManager.Builder 建立管理員和播放器執行個體,確保重要元件共用。
  • 主動管理 add() 和 remove() 呼叫,實作滑動視窗模式,避免發生 OutOfMemoryError。
  • 使用 TargetPreloadStatusControl 建立智慧分層預先載入策略,在效能和資源耗用之間取得平衡。

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

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

歡迎分享意見回饋,我們很想聽聽你的想法。

敬請期待,並搶先體驗更快的影片播放速度!🚀

撰寫者:

繼續閱讀