Analytics

ExoPlayer поддерживает широкий спектр потребностей в аналитике воспроизведения. В конечном счете, аналитика заключается в сборе, интерпретации, агрегировании и обобщении данных о воспроизведениях. Эти данные можно использовать на устройстве, например для ведения журнала, отладки или принятия решений о воспроизведении в будущем, или передавать на сервер для отслеживания воспроизведения на всех устройствах.

Система аналитики сначала собирает события, а затем обрабатывает их, чтобы сделать их значимыми:

  • Сбор событий. Для этого нужно зарегистрировать AnalyticsListener в экземпляре ExoPlayer. Зарегистрированные прослушиватели аналитики получают события по мере их возникновения во время использования проигрывателя. Каждое событие связано с соответствующим объектом в плейлисте, а также с метаданными о позиции воспроизведения и временной метке.
  • Обработка событий. Некоторые системы аналитики загружают необработанные события на сервер, где и происходит их обработка. События также можно обрабатывать на устройстве. Это может быть проще или позволит сократить объем загружаемой информации. В 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,
    ) {}
  }
)

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, переданный обратному вызову, чтобы определить, какое воспроизведение завершено. Вы можете объединить данные аналитики для нескольких воспроизведений. Вы также можете в любой момент запросить у PlaybackStats текущий сеанс воспроизведения с помощью PlaybackStatsListener.getPlaybackStats().

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, вызвав метод PlaybackStats.merge. В полученном объекте 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. Тег медиаконтента является частью EventTime, который передается для необработанных событий и по завершении PlaybackStats. Поэтому его можно получить при обработке соответствующих аналитических данных:

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

Отслеживание флагов функций и режим Canary

При проведении A/B-тестирования или оценке влияния новых оптимизаций Media3 вы можете сопоставить данные аналитики воспроизведения с активными флагами функций и режимом Canary.

Вы можете запрашивать Flags.getLogString или проверять переопределения определенных флагов с помощью Flags.getEnabledOverrides и Flags.getDisabledOverrides, чтобы включать состояния флагов в полезную нагрузку пользовательской аналитики или метаданные сеанса.