歡迎閱讀 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 播放功能互動。
使用共用 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,用於分配記憶體緩衝區。隨著這些物件累積,應用程式的堆積空間可能會耗盡。解決方法是使用您可能已經熟悉的演算法,稱為滑動視窗。滑動視窗模式會在記憶體中保留一組小型、可管理的項目,這些項目在邏輯上與使用者目前在動態消息中的位置相鄰。使用者捲動畫面時,這個受管理項目的「視窗」會隨之滑動,新增進入檢視畫面的新項目,並移除現在距離較遠的項目。
實作滑動視窗模式
請務必瞭解,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 部分:使用預先載入的媒體進行快取
將資料預先載入記憶體可立即提升效能,但可能需要權衡取捨。一旦應用程式關閉或預先載入的媒體從管理工具中移除,資料就會消失。如要達到更持久的最佳化效果,可以結合預先載入和磁碟快取。這項功能正在積極開發中,預計幾個月後推出。
歡迎提供意見。期待收到您的回覆。
敬請期待,並立即加快影片播放速度!🚀
-
產品最新消息在現今以媒體為主的應用程式中,提供流暢不中斷的播放體驗,是打造優質使用者體驗的關鍵。使用者希望影片能立即開始播放,且過程流暢不中斷。
Mayuri Khinvasara Khabya • 閱讀時間:8 分鐘 -
產品最新消息今天,我們在 Android 開發人員高峰會上宣布,將廣泛提供多項工具和 API,旨在消除日常摩擦,讓您更靈活地建構出色的 Android 應用程式。
Matthew McCullough • 閱讀時間:4 分鐘 -
產品最新消息Android Studio Rabbit 2 穩定版已推出,可供您在正式環境中使用。此版本包含多項重要功能,可提升開發效率,例如「自備代理」、全新 XR 模擬器功能、LeakCanary 更新、全新代理技能管理工具、AI 程式碼審查等。
Matthew Warner • 閱讀時間:3 分鐘
每週透過電子郵件接收 Android 開發人員的最新深入分析。