Analytics

ExoPlayer 支援各種播放分析需求。最終,數據分析的目的是收集、解讀、匯總及摘要播放資料。這項資料可用於裝置上 (例如記錄、偵錯或做為日後播放決策的依據),也可以回報給伺服器,監控所有裝置的播放情形。

分析系統通常需要先收集事件,然後進一步處理這些事件,使其具有意義:

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

使用 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:事件的實際時間。
  • timelinewindowIndexmediaPeriodId:定義播放清單,以及事件所屬播放清單中的項目。mediaPeriodId 包含選用的額外資訊,例如指出事件是否屬於項目中的廣告。
  • eventPlaybackPositionMs:事件發生時,項目中的播放位置。
  • currentTimelinecurrentWindowIndexcurrentMediaPeriodIdcurrentPlaybackPositionMs:與上述相同,但適用於目前播放的項目。目前播放的項目可能與事件所屬的項目不同,例如事件對應於下一個要播放項目的預先緩衝。

使用 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_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 等方法,可傳回特定播放狀態組合的持續時間。

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生命週期內收集的所有數據分析資料匯總檢視畫面。

計算摘要指標

除了基本的 Analytics 資料,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());

進階主題

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

收集個別播放的 Analytics 資料時,您可能希望將播放 Analytics 資料與所播放媒體的中繼資料建立關聯。

建議使用 MediaItem.Builder.setTag 設定媒體專屬中繼資料。 媒體標記是針對原始事件回報的 EventTime 一部分,且會在 PlaybackStats 完成時回報,因此處理相應的 Analytics 資料時,可以輕鬆擷取:

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

回報自訂數據分析事件

如要將自訂事件加入 Analytics 資料,您必須將這些事件儲存在自己的資料結構中,並在稍後與回報的 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();
}