ExoPlayer 支援多種播放數據分析需求。最終,分析功能會收集、解讀、匯總及摘要播放資料。這類資料可用於裝置上 (例如記錄、偵錯或做為日後播放決策的依據),或回報給伺服器,以便監控所有裝置的播放情形。
數據分析系統通常需要先收集事件,然後進一步處理,才能產生有意義的資料:
- 事件收集:在
ExoPlayer執行個體上註冊AnalyticsListener即可完成。已註冊的數據分析事件監聽器會在播放器使用期間,接收發生的事件。每個事件都會與播放清單中對應的媒體項目,以及播放位置和時間戳記中繼資料建立關聯。 - 事件處理:部分數據分析系統會將原始事件上傳至伺服器,並在伺服器端處理所有事件。您也可以在裝置上處理事件,這樣做可能更簡單,或可減少需要上傳的資訊量。ExoPlayer 提供
PlaybackStatsListener,可讓您執行下列處理步驟:- 事件解讀:為了用於分析,事件必須在單一播放情境中解讀。舉例來說,播放器狀態變更為
STATE_BUFFERING的原始事件,可能對應到初始緩衝、重新緩衝,或跳轉後發生的緩衝。 - 狀態追蹤:這個步驟會將事件轉換為計數器。舉例來說,狀態變更事件可以轉換為計數器,追蹤每個播放狀態所花費的時間。結果會是單一播放的基本分析資料值組合。
- 匯總:這個步驟會合併多個播放作業的數據分析資料,通常是加總計數器。
- 計算摘要指標:許多最實用的指標都是計算平均值,或以其他方式結合基本 Analytics 資料值。摘要指標可針對單一或多個播放計算。
- 事件解讀:為了用於分析,事件必須在單一播放情境中解讀。舉例來說,播放器狀態變更為
使用 AnalyticsListener 收集事件
播放器的原始播放事件會回報給 AnalyticsListener 實作。您可以新增自己的事件監聽器,並只覆寫您感興趣的方法:
Kotlin
exoPlayer.addAnalyticsListener( object : AnalyticsListener { override fun onPlaybackStateChanged(eventTime: EventTime, @Player.State state: Int) {} override fun onDroppedVideoFrames( eventTime: EventTime, droppedFrames: Int, elapsedMs: Long, ) {} } )
Java
exoPlayer.addAnalyticsListener( new AnalyticsListener() { @Override public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) {} @Override public void onDroppedVideoFrames( EventTime eventTime, int droppedFrames, long elapsedMs) {} });
傳遞至每個回呼的 EventTime 會將事件與播放清單中的媒體項目建立關聯,以及播放位置和時間戳記中繼資料:
realtimeMs::事件的實際時間。timeline、windowIndex和mediaPeriodId:定義播放清單,以及事件所屬的播放清單項目。mediaPeriodId包含選用的額外資訊,例如指出事件是否屬於項目中的廣告。eventPlaybackPositionMs:事件發生時,項目中的播放位置。currentTimeline、currentWindowIndex、currentMediaPeriodId和currentPlaybackPositionMs:如前所述,但適用於目前播放的項目。目前播放的項目可能與事件所屬的項目不同,例如事件對應到下一個要播放項目的預先緩衝處理。
使用 PlaybackStatsListener 處理事件
PlaybackStatsListener 是實作裝置端事件處理的 AnalyticsListener。這項工具會計算 PlaybackStats,並提供計數器和衍生指標,包括:
- 摘要指標,例如總播放時間。
- 自動調整播放畫質指標,例如平均影片解析度。
- 算繪品質指標,例如影格遺失率。
- 資源用量指標,例如透過網路讀取的位元組數。
如需可用計數和衍生指標的完整清單,請參閱 PlaybackStats Javadoc。
PlaybackStatsListener 會為播放清單中的每個媒體項目,以及這些項目中插入的每個用戶端廣告,分別計算 PlaybackStats。您可以向 PlaybackStatsListener 提供回呼,以便在播放結束時收到通知,並使用傳遞至回呼的 EventTime 識別結束的播放。您可以匯總多次播放的數據分析資料。您也可以隨時使用 PlaybackStatsListener.getPlaybackStats() 查詢目前播放工作階段的 PlaybackStats。
Kotlin
exoPlayer.addAnalyticsListener( PlaybackStatsListener(/* keepHistory= */ true) { eventTime: EventTime?, playbackStats: PlaybackStats? -> // Analytics data for the session started at `eventTime` is ready. } )
Java
exoPlayer.addAnalyticsListener( new PlaybackStatsListener( /* keepHistory= */ true, (eventTime, playbackStats) -> { // Analytics data for the session started at `eventTime` is ready. }));
PlaybackStatsListener 的建構函式可讓您選擇保留已處理事件的完整記錄。請注意,視播放長度和事件數量而定,這可能會產生未知的記憶體負擔。因此,只有在需要存取已處理事件的完整記錄,而不只是最終數據分析資料時,才應啟用這項功能。
請注意,PlaybackStats 會使用一組擴充狀態,不只指出媒體狀態,還會指出使用者意圖,以及更詳細的資訊,例如播放中斷或結束的原因:
| 播放狀態 | 使用者播放意圖 | 無意播放 |
|---|---|---|
| 播放前 | JOINING_FOREGROUND |
NOT_STARTED、JOINING_BACKGROUND |
| 正在播放 | PLAYING |
|
| 播放中斷 | BUFFERING、SEEKING |
PAUSED,PAUSED_BUFFERING,SUPPRESSED,SUPPRESSED_BUFFERING,INTERRUPTED_BY_AD |
| 結束狀態 | ENDED、STOPPED、FAILED、ABANDONED |
使用者播放意圖的重要性,在於區分使用者主動等待播放繼續的時間,以及被動等待時間。舉例來說,PlaybackStats.getTotalWaitTimeMs 會傳回在 JOINING_FOREGROUND、BUFFERING 和 SEEKING 狀態下花費的總時間,但不包括播放暫停的時間。同樣地,PlaybackStats.getTotalPlayAndWaitTimeMs 會傳回使用者意圖玩遊戲的總時間,也就是總主動等待時間和處於 PLAYING 狀態的總時間。
已處理及解讀的事件
您可以使用 PlaybackStatsListener 搭配 keepHistory=true,記錄已處理和解讀的事件。產生的 PlaybackStats 會包含下列事件清單:
playbackStateHistory:已排序的擴充播放狀態清單,以及開始套用這些狀態的EventTime。您也可以使用PlaybackStats.getPlaybackStateAtTime查詢指定時鐘時間的狀態。mediaTimeHistory:時鐘時間和媒體時間組合的記錄,可讓您重建媒體在何時播放哪些部分。您也可以使用PlaybackStats.getMediaTimeMsAtRealtimeMs查詢指定時鐘時間的播放位置。videoFormatHistory和audioFormatHistory:播放期間使用的影片和音訊格式已排序的清單,以及開始使用的EventTime。fatalErrorHistory和nonFatalErrorHistory:致命和非致命錯誤的排序清單,以及發生錯誤時的EventTime。重大錯誤會導致播放作業結束,而非重大錯誤可能可以復原。
單次播放數據分析資料
如果您使用 PlaybackStatsListener,即使搭配 keepHistory=false,系統也會自動收集這項資料。最終值是您可以在 PlaybackStats Javadoc 中找到的公開欄位,以及 getPlaybackStateDurationMs 傳回的播放狀態持續時間。為方便起見,您也可以使用 getTotalPlayTimeMs 和 getTotalWaitTimeMs 等方法,傳回特定播放狀態組合的時間長度。
Kotlin
Log.d( "DEBUG", "Playback summary: " + "play time = " + playbackStats.totalPlayTimeMs + ", rebuffers = " + playbackStats.totalRebufferCount, )
Java
Log.d( "DEBUG", "Playback summary: " + "play time = " + playbackStats.getTotalPlayTimeMs() + ", rebuffers = " + playbackStats.totalRebufferCount);
匯總多個播放作業的數據分析資料
您可以呼叫 PlaybackStats.merge,將多個 PlaybackStats 結合在一起。產生的 PlaybackStats 將包含所有合併播放的匯總資料。請注意,這份報表不會包含個別播放事件的記錄,因為這些事件無法匯總。
PlaybackStatsListener.getCombinedPlaybackStats 可用於匯總 PlaybackStatsListener 存續期間收集的所有數據分析資料。
計算摘要指標
除了基本數據分析資料外,PlaybackStats 還提供多種方法來計算摘要指標。
Kotlin
Log.d( "DEBUG", "Additional calculated summary metrics: " + "average video bitrate = " + playbackStats.meanVideoFormatBitrate + ", mean time between rebuffers = " + playbackStats.meanTimeBetweenRebuffers, )
Java
Log.d( "DEBUG", "Additional calculated summary metrics: " + "average video bitrate = " + playbackStats.getMeanVideoFormatBitrate() + ", mean time between rebuffers = " + playbackStats.getMeanTimeBetweenRebuffers());
進階主題
將數據分析資料與播放中繼資料建立關聯
收集個別播放的數據分析資料時,您可能需要將播放數據分析資料與播放媒體的中繼資料建立關聯。
建議使用 MediaItem.Builder.setTag 設定媒體專屬中繼資料。媒體標記是原始事件和 PlaybackStats 完成時回報的 EventTime 的一部分,因此在處理對應的數據分析資料時,可以擷取該標記:
Kotlin
PlaybackStatsListener(/* keepHistory= */ false) { eventTime: EventTime, playbackStats: PlaybackStats -> val mediaTag = eventTime.timeline .getWindow(eventTime.windowIndex, Timeline.Window()) .mediaItem .localConfiguration ?.tag // Report playbackStats with mediaTag metadata. }
Java
new PlaybackStatsListener( /* keepHistory= */ false, (eventTime, playbackStats) -> { Object mediaTag = eventTime.timeline.getWindow(eventTime.windowIndex, new Timeline.Window()) .mediaItem .localConfiguration .tag; // Report playbackStats with mediaTag metadata. });
回報自訂數據分析事件
如需在數據分析資料中加入自訂事件,請將這些事件儲存在自己的資料結構中,稍後再與回報的 PlaybackStats 合併。如有需要,您可以擴充 DefaultAnalyticsCollector,為自訂事件產生 EventTime 執行個體,並傳送至已註冊的事件監聽器,如以下範例所示。
Kotlin
@OptIn(UnstableApi::class) private interface ExtendedListener : AnalyticsListener { fun onCustomEvent(eventTime: EventTime) } @OptIn(UnstableApi::class) private class ExtendedCollector : DefaultAnalyticsCollector(Clock.DEFAULT) { fun customEvent() { val eventTime = super.generateCurrentPlayerMediaPeriodEventTime() super.sendEvent(eventTime, CUSTOM_EVENT_ID) { listener: AnalyticsListener -> if (listener is ExtendedListener) { listener.onCustomEvent(eventTime) } } } } @OptIn(UnstableApi::class) fun useExtendedAnalyticsCollector(context: Context) { // Usage - Setup and listener registration. val player = ExoPlayer.Builder(context).setAnalyticsCollector(ExtendedCollector()).build() player.addAnalyticsListener( object : ExtendedListener { override fun onCustomEvent(eventTime: EventTime) { // Save custom event for analytics data. } } ) // Usage - Triggering the custom event. (player.analyticsCollector as ExtendedCollector).customEvent() }
Java
@OptIn(markerClass = UnstableApi.class) private interface ExtendedListener extends AnalyticsListener { void onCustomEvent(EventTime eventTime); } @OptIn(markerClass = UnstableApi.class) private static class ExtendedCollector extends DefaultAnalyticsCollector { public ExtendedCollector() { super(Clock.DEFAULT); } public void customEvent() { AnalyticsListener.EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); sendEvent( eventTime, CUSTOM_EVENT_ID, listener -> { if (listener instanceof ExtendedListener) { ((ExtendedListener) listener).onCustomEvent(eventTime); } }); } } @OptIn(markerClass = UnstableApi.class) public static void useExtendedAnalyticsCollector(Context context) { // Usage - Setup and listener registration. ExoPlayer player = new ExoPlayer.Builder(context).setAnalyticsCollector(new ExtendedCollector()).build(); player.addAnalyticsListener( (ExtendedListener) eventTime -> { // Save custom event for analytics data. }); // Usage - Triggering the custom event. ((ExtendedCollector) player.getAnalyticsCollector()).customEvent(); }
追蹤功能旗標和初期測試模式
進行 A/B 測試或評估新 Media3 最佳化措施的影響時,您可以將播放數據分析資料與有效的功能旗標和 Canary 模式建立關聯。
您可以使用 Flags.getEnabledOverrides 和 Flags.getDisabledOverrides 查詢 Flags.getLogString 或檢查特定旗標覆寫,在自訂數據分析酬載或工作階段中繼資料中加入旗標狀態。