Analytics

ExoPlayer は、幅広い再生分析のニーズに対応しています。最終的に、分析とは、再生からのデータを収集、解釈、集計、要約することです。このデータは、デバイス上で(ロギング、デバッグ、今後の再生に関する決定に役立てるなど)使用することも、すべてのデバイスでの再生をモニタリングするためにサーバーに報告することもできます。

通常、分析システムでは、まずイベントを収集し、さらに処理して意味のあるものにする必要があります。

  • イベント収集: ExoPlayer インスタンスに AnalyticsListener を登録することで実行できます。登録された分析リスナーは、プレーヤーの使用中にイベントが発生すると、そのイベントを受信します。各イベントは、再生リスト内の対応するメディア アイテム、再生位置、タイムスタンプ メタデータに関連付けられています。
  • イベント処理: 一部の分析システムでは、未加工のイベントをサーバーにアップロードし、すべてのイベント処理をサーバーサイドで行います。デバイスでイベントを処理することもできます。その方が簡単であったり、アップロードする必要がある情報の量を減らせたりする場合があります。ExoPlayer は PlaybackStatsListener を提供します。これにより、次の処理ステップを実行できます。
    1. イベントの解釈: 分析に役立つように、イベントは 1 回の再生のコンテキストで解釈する必要があります。たとえば、プレーヤーの状態が STATE_BUFFERING に変化した未加工のイベントは、初期バッファリング、再バッファリング、シーク後に発生するバッファリングに対応している可能性があります。
    2. 状態のトラッキング: このステップでは、イベントがカウンタに変換されます。たとえば、状態変更イベントを、各再生状態で費やされた時間をトラッキングするカウンタに変換できます。結果は、1 回の再生に関する基本的な分析データ値のセットです。
    3. 集計: このステップでは、通常はカウンタを加算することで、複数の再生にわたる分析データを結合します。
    4. 概要指標の計算: 最も有用な指標の多くは、平均値を計算したり、基本的な分析データの値を他の方法で組み合わせたりするものです。概要指標は、1 回または複数回の再生に対して計算できます。

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.getTotalWaitTimeMsJOINING_FOREGROUNDBUFFERINGSEEKING の各状態での合計時間を返しますが、再生が一時停止された時間は返しません。同様に、PlaybackStats.getTotalPlayAndWaitTimeMs は、ユーザーが再生を意図した合計時間(合計アクティブ待機時間と PLAYING 状態の合計時間)を返します。

処理および解釈されたイベント

処理および解釈されたイベントは、keepHistory=truePlaybackStatsListener を使用して記録できます。結果の PlaybackStats には、次のイベント リストが含まれます。

  • playbackStateHistory: 拡張再生状態と、それらが適用され始めた EventTime の順序付きリスト。PlaybackStats.getPlaybackStateAtTime を使用して、特定の壁時計の時刻の状態を検索することもできます。
  • mediaTimeHistory: 壁時計の時間とメディア時間のペアの履歴。メディアのどの部分がどの時間に再生されたかを再構築できます。また、PlaybackStats.getMediaTimeMsAtRealtimeMs を使用して、特定の時刻の再生位置を検索することもできます。
  • videoFormatHistoryaudioFormatHistory: 再生中に使用された動画と音声のフォーマットの順序付きリスト。EventTime は、それらのフォーマットが使用され始めた時点です。
  • fatalErrorHistorynonFatalErrorHistory: 発生した EventTime を含む、致命的エラーと致命的でないエラーの順序付きリスト。致命的なエラーは再生が終了したエラーで、致命的でないエラーは回復可能だった可能性があります。

1 回のみ再生の分析データ

keepHistory=false を使用している場合でも、PlaybackStatsListener を使用すると、このデータは自動的に収集されます。最終的な値は、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 のライフタイムで収集されたすべての分析データの集計ビューを取得できます。

計算された概要指標

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