產品新聞

提升媒體播放體驗:深入瞭解 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 或影片格式的錯誤率最高?(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)

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

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 在不同的執行緒上運作,可能會發生競爭狀況。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 設定達到這種精細程度。

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

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

結論與後續行動

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

重點回顧:

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

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

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

你是否想分享任何意見?期待收到您的回覆。

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

繼續閱讀