Analytics

ExoPlayer는 다양한 재생 분석 요구사항을 지원합니다. 궁극적으로 분석은 재생에서 데이터를 수집, 해석, 집계, 요약하는 것입니다. 이 데이터는 기기에서(예: 로깅, 디버깅, 향후 재생 결정에 사용) 사용하거나 서버에 보고하여 모든 기기에서 재생을 모니터링할 수 있습니다.

분석 시스템은 일반적으로 먼저 이벤트를 수집한 다음 이를 추가로 처리하여 의미 있는 정보로 만들어야 합니다.

  • 이벤트 수집: ExoPlayer 인스턴스에 AnalyticsListener를 등록하여 이 작업을 실행할 수 있습니다. 등록된 분석 리스너는 플레이어 사용 중에 이벤트가 발생하면 이벤트를 수신합니다. 각 이벤트는 재생목록의 해당 미디어 항목과 재생 위치 및 타임스탬프 메타데이터와 연결됩니다.
  • 이벤트 처리: 일부 분석 시스템은 원시 이벤트를 서버에 업로드하며 모든 이벤트 처리는 서버 측에서 실행됩니다. 기기에서 이벤트를 처리할 수도 있으며 이렇게 하면 더 간단하거나 업로드해야 하는 정보의 양이 줄어들 수 있습니다. ExoPlayer는 다음 처리 단계를 실행할 수 있는 PlaybackStatsListener를 제공합니다.
    1. 이벤트 해석: 분석 목적으로 유용하려면 단일 재생의 컨텍스트에서 이벤트를 해석해야 합니다. 예를 들어 플레이어 상태가 STATE_BUFFERING로 변경되는 원시 이벤트는 초기 버퍼링, 리버퍼링 또는 탐색 후 발생하는 버퍼링에 해당할 수 있습니다.
    2. 상태 추적: 이 단계에서는 이벤트를 카운터로 변환합니다. 예를 들어 상태 변경 이벤트는 각 재생 상태에서 소비된 시간을 추적하는 카운터로 변환할 수 있습니다. 결과는 단일 재생에 대한 기본 분석 데이터 값 집합입니다.
    3. 집계: 이 단계에서는 일반적으로 카운터를 합산하여 여러 재생의 분석 데이터를 결합합니다.
    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,
    ) {}
  }
)

자바

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

자바

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.getTotalWaitTimeMsJOINING_FOREGROUND, BUFFERING, SEEKING 상태에서 소비된 총 시간을 반환하지만 재생이 일시중지된 시간은 반환하지 않습니다. 마찬가지로 PlaybackStats.getTotalPlayAndWaitTimeMs는 사용자가 재생하려는 의도를 가진 총 시간, 즉 총 활성 대기 시간과 PLAYING 상태에서 보낸 총 시간을 반환합니다.

처리되고 해석된 이벤트

keepHistory=true와 함께 PlaybackStatsListener를 사용하여 처리되고 해석된 이벤트를 기록할 수 있습니다. 결과 PlaybackStats에는 다음 이벤트 목록이 포함됩니다.

  • playbackStateHistory: 적용이 시작된 EventTime가 포함된 확장 재생 상태의 정렬된 목록입니다. PlaybackStats.getPlaybackStateAtTime를 사용하여 특정 시계 시간의 상태를 조회할 수도 있습니다.
  • mediaTimeHistory: 벽시계 시간과 미디어 시간 쌍의 기록으로, 미디어의 어느 부분이 언제 재생되었는지 재구성할 수 있습니다. PlaybackStats.getMediaTimeMsAtRealtimeMs을 사용하여 지정된 실제 시간의 재생 위치를 조회할 수도 있습니다.
  • videoFormatHistoryaudioFormatHistory: 재생 중에 사용된 동영상 및 오디오 형식의 순서가 지정된 목록과 사용이 시작된 EventTime입니다.
  • fatalErrorHistorynonFatalErrorHistory: 발생한 EventTime와 함께 치명적 오류와 치명적이지 않은 오류의 순서가 지정된 목록입니다. 심각한 오류는 재생이 종료된 오류이고 심각하지 않은 오류는 복구 가능했을 수 있습니다.

한 번만 재생 분석 데이터

keepHistory=false을 사용하더라도 PlaybackStatsListener를 사용하면 이 데이터가 자동으로 수집됩니다. 최종 값은 PlaybackStats Javadoc에서 확인할 수 있는 공개 필드와 getPlaybackStateDurationMs에서 반환된 재생 상태 지속 시간입니다. 편의를 위해 특정 재생 상태 조합의 지속 시간을 반환하는 getTotalPlayTimeMsgetTotalWaitTimeMs과 같은 메서드도 있습니다.

Kotlin

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.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,
)

자바

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

자바

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