Analytics

ExoPlayer 支援各種播放分析需求。最終,Analytics 主要是收集、解讀、匯總播放次數以及匯總播放資料。這些資料可以在裝置上使用,例如記錄、偵錯,或用於日後的播放決策,或將這類資料回報給伺服器,讓它監控所有裝置的播放次數。

分析系統通常需要先收集事件,然後再進一步處理,才能讓事件產生意義:

  • 事件收集:只要在 ExoPlayer 例項上註冊 AnalyticsListener,即可完成這項操作。註冊的分析事件監聽器會在使用者使用播放器時接收事件。每個事件都會與播放清單中的對應媒體項目、播放位置和時間戳記中繼資料建立關聯。
  • 事件處理:部分數據分析系統會將原始事件上傳至伺服器,並在伺服器端執行所有事件處理作業。您也可以在裝置上處理事件,這樣做可能會比較簡單,或可減少需要上傳的資訊量。ExoPlayer 提供 PlaybackStatsListener,可讓您執行下列處理步驟:
    1. 事件解讀:事件必須在單一播放環境中解譯,才能用於數據分析。舉例來說,當播放器狀態變更為 STATE_BUFFERING 的原始事件,可能會對應到初始緩衝、重新緩衝,或是在搜尋後發生的緩衝。
    2. 狀態追蹤:這個步驟會將事件轉換為計數器。舉例來說,狀態變更事件可轉換為計數器,用於追蹤在各播放狀態中花費的時間。結果是單一播放作業的基本數據分析資料值組。
    3. 匯總:這個步驟會合併多個播放請求的數據分析資料,通常是透過新增計數器。
    4. 計算摘要指標:許多最實用的指標會以其他方式計算平均值或結合基本分析資料值。您可以為單一或多個播放作業計算摘要指標。

使用 AnalyticsListener 收集事件

系統會將播放器的原始播放事件回報給 AnalyticsListener 實作。您可以輕鬆新增自己的事件監聽器,並只覆寫您感興趣的方法:

KotlinJava
exoPlayer.addAnalyticsListener(
 
object : AnalyticsListener {
   
override fun onPlaybackStateChanged(
      eventTime
: EventTime, @Player.State state: Int
   
) {}

   
override fun onDroppedVideoFrames(
      eventTime
: EventTime,
      droppedFrames
: Int,
      elapsedMs
: Long,
   
) {}
 
}
)
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:事件的實際時數。
  • timelinewindowIndexmediaPeriodId:定義事件所屬的播放清單和播放清單中的項目。mediaPeriodId 包含選用的額外資訊,例如指出事件是否屬於項目中的廣告。
  • eventPlaybackPositionMs:事件發生時,項目中的播放位置。
  • currentTimelinecurrentWindowIndexcurrentMediaPeriodIdcurrentPlaybackPositionMs:與上述相同,但適用於目前播放的項目。目前播放的項目可能與事件所屬的項目不同,例如如果事件對應的是下一個要播放的項目的預先緩衝處理。

使用 PlaybackStatsListener 處理事件

PlaybackStatsListener 是實作裝置端事件處理作業的 AnalyticsListener。它會使用計數器和衍生指標計算 PlaybackStats,包括:

  • 摘要指標,例如總播放時間。
  • 自適應播放品質指標,例如平均影片解析度。
  • 算繪品質指標,例如影格遺漏率。
  • 資源用量指標,例如透過網路讀取的位元組數。

如需可用計數和衍生指標的完整清單,請參閱 PlaybackStats Javadoc

PlaybackStatsListener 會為播放清單中的每個媒體項目計算個別的 PlaybackStats,以及插入這些項目中的每個用戶端廣告。您可以提供對 PlaybackStatsListener 的回呼,接收已播放完畢的通知,並使用傳遞至回呼的 EventTime 來找出播放完成的播放動作。您可以針對多個播放作業匯總數據分析資料。您也可以隨時使用 PlaybackStatsListener.getPlaybackStats() 查詢目前播放會話程式的 PlaybackStats

KotlinJava
exoPlayer.addAnalyticsListener(
 
PlaybackStatsListener(/* keepHistory= */ true) {
    eventTime
: EventTime?,
    playbackStats
: PlaybackStats?,
   
-> // Analytics data for the session started at `eventTime` is ready.
 
}
)
exoPlayer.addAnalyticsListener(
   
new PlaybackStatsListener(
       
/* keepHistory= */ true,
       
(eventTime, playbackStats) -> {
         
// Analytics data for the session started at `eventTime` is ready.
       
}));

PlaybackStatsListener 的建構函式提供保留已處理事件完整記錄的選項。請注意,這可能會產生未知的記憶體開銷,具體取決於播放長度和事件數量。因此,只有在需要存取已處理事件的完整記錄,而非最終的分析資料時,才應開啟這項功能。

請注意,PlaybackStats 不僅使用一組延伸的狀態來指出媒體狀態,還會指出使用者想播放的意圖,以及更多詳細資訊 (例如播放中斷或結束的原因):

播放狀態 使用者想玩遊戲 沒有意圖播放
播放前 JOINING_FOREGROUND NOT_STARTEDJOINING_BACKGROUND
正在播放 PLAYING
播放中斷 BUFFERINGSEEKING PAUSEDPAUSED_BUFFERINGSUPPRESSEDSUPPRESSED_BUFFERINGINTERRUPTED_BY_AD
結束狀態 ENDEDSTOPPEDFAILEDABANDONED

使用者播放意圖非常重要,因為這有助於區分使用者主動等待播放內容繼續播放的時間,以及被動等待的時間。舉例來說,PlaybackStats.getTotalWaitTimeMs 會傳回在 JOINING_FOREGROUNDBUFFERINGSEEKING 狀態中所花費的總時間,但不會傳回暫停播放的時間。同樣地,PlaybackStats.getTotalPlayAndWaitTimeMs 會傳回使用者有意播放的總時間,也就是總的有效等待時間,以及在 PLAYING 狀態中花費的總時間。

已處理及解讀的事件

您可以使用 PlaybackStatsListener 搭配 keepHistory=true,記錄已處理及解讀的事件。產生的 PlaybackStats 會包含下列事件清單:

  • playbackStateHistory:延長播放狀態的排序清單,以及開始套用的 EventTime。您也可以使用 PlaybackStats.getPlaybackStateAtTime 查詢特定時鐘時間的狀態。
  • mediaTimeHistory:實際時間與媒體時間組合的記錄,可讓您重新建構當時播放媒體的哪些部分。您也可以使用 PlaybackStats.getMediaTimeMsAtRealtimeMs 查詢特定實際時鐘時間的播放位置。
  • videoFormatHistoryaudioFormatHistory:播放期間使用的影片和音訊格式排序清單,以及開始使用的 EventTime
  • fatalErrorHistorynonFatalErrorHistory:發生與 EventTime 相關的嚴重和一般錯誤排序清單。嚴重錯誤會導致播放作業結束,而非嚴重錯誤則可能可以復原。

單次播放數據分析資料

如果使用 PlaybackStatsListener (包括 keepHistory=false),系統會自動收集這項資料。最終值是您可以在 PlaybackStats Javadoc 中找到的公開欄位,以及 getPlaybackStateDurationMs 傳回的播放狀態時間長度。為方便起見,您還會發現 getTotalPlayTimeMsgetTotalWaitTimeMs 等方法,可傳回特定播放狀態組合的時間長度。

KotlinJava
Log.d(
 
"DEBUG",
 
"Playback summary: " +
   
"play time = " +
    playbackStats
.totalPlayTimeMs +
   
", rebuffers = " +
    playbackStats
.totalRebufferCount
)
Log.d(
   
"DEBUG",
   
"Playback summary: "
       
+ "play time = "
       
+ playbackStats.getTotalPlayTimeMs()
       
+ ", rebuffers = "
       
+ playbackStats.totalRebufferCount);

PlaybackStats

匯總多個播放的數據分析資料

您可以呼叫 PlaybackStats.merge,將多個 PlaybackStats 組合在一起。產生的 PlaybackStats 會包含所有合併播放內容的匯總資料。請注意,由於無法匯總個別播放事件,因此不會納入個別播放事件的歷史記錄。

PlaybackStatsListener.getCombinedPlaybackStats 可用於取得 PlaybackStatsListener 生命週期內收集到的所有數據分析資料的匯總檢視畫面。

計算的摘要指標

除了基本分析資料之外,PlaybackStats 還提供許多方法來計算摘要指標。

KotlinJava
Log.d(
 
"DEBUG",
 
"Additional calculated summary metrics: " +
   
"average video bitrate = " +
    playbackStats
.meanVideoFormatBitrate +
   
", mean time between rebuffers = " +
    playbackStats
.meanTimeBetweenRebuffers
)
Log.d(
   
"DEBUG",
   
"Additional calculated summary metrics: "
       
+ "average video bitrate = "
       
+ playbackStats.getMeanVideoFormatBitrate()
       
+ ", mean time between rebuffers = "
       
+ playbackStats.getMeanTimeBetweenRebuffers());

進階主題

將數據分析資料與播放中繼資料建立關聯

收集個別播放內容的數據分析資料時,您可能會將播放數據分析資料與播放媒體的中繼資料建立關聯。

建議您使用 MediaItem.Builder.setTag 設定媒體專屬的中繼資料。媒體標記包含在原始事件回報的 EventTime 中,以及 PlaybackStats 完成後,因此在處理對應的數據分析資料時,可以輕鬆擷取該代碼:

KotlinJava
PlaybackStatsListener(/* keepHistory= */ false) {
  eventTime
: EventTime,
  playbackStats
: PlaybackStats ->
 
val mediaTag =
    eventTime
.timeline
     
.getWindow(eventTime.windowIndex, Timeline.Window())
     
.mediaItem
     
.localConfiguration
     
?.tag
   
// Report playbackStats with mediaTag metadata.
}
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 例項,並傳送至已註冊的事件監聽器,如以下範例所示。

KotlinJava
private interface ExtendedListener : AnalyticsListener {
 
fun onCustomEvent(eventTime: EventTime)
}

private class ExtendedCollector : DefaultAnalyticsCollector(Clock.DEFAULT) {
 
fun customEvent() {
   
val eventTime = generateCurrentPlayerMediaPeriodEventTime()
    sendEvent
(eventTime, CUSTOM_EVENT_ID) { listener: AnalyticsListener ->
     
if (listener is ExtendedListener) {
        listener
.onCustomEvent(eventTime)
     
}
   
}
 
}
}

// 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()
private interface ExtendedListener extends AnalyticsListener {
 
void onCustomEvent(EventTime eventTime);
}

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);
         
}
       
});
 
}
}

// 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();