產品最新消息

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

閱讀時間:9 分鐘
查看 Mayuri Khinvasara Khabya 的個人資料
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 或影片格式的錯誤率最高?(剖析 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 播放功能互動。

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 和播放器共用。我們將在即將推出的 Media3 1.9 版本中,提供使用 LoadControl 有效控制預先載入的功能。

preloadManagerBuilder.setLoadControl(customLoadControl)

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

preloadManagerBuilder.setTrackSelectorFactory(customTrackSelectorFactory)

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

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 中的預設元件,例如 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 設定達到這種精細程度。

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

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

結論與後續行動

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

重點回顧:

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

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

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

歡迎提供意見。期待收到您的回覆。

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

繼續閱讀