Analytics

O ExoPlayer atende a uma ampla variedade de necessidades de análise de dados de reprodução. Em resumo, a análise coleta, interpreta, agrega e resume dados de reproduções. Esses dados podem ser usados no dispositivo, por exemplo, para registro, depuração ou para informar decisões futuras de reprodução, ou podem ser enviados a um servidor para monitorar as reproduções em todos os dispositivos.

Um sistema de análise geralmente precisa coletar eventos primeiro e depois processá-los para que sejam significativos:

  • Coleta de eventos: isso pode ser feito registrando um AnalyticsListener em uma instância ExoPlayer. Os listeners de análise registrados recebem eventos à medida que eles ocorrem durante o uso do player. Cada evento é associado ao item de mídia correspondente na playlist, bem como à posição de reprodução e aos metadados de carimbo de data/hora.
  • Processamento de eventos: alguns sistemas de análise enviam eventos brutos para um servidor, com todo o processamento realizado no lado do servidor. Também é possível processar eventos no dispositivo, o que pode ser mais simples ou reduzir a quantidade de informações que precisam ser enviadas. O ExoPlayer fornece PlaybackStatsListener, que permite realizar as seguintes etapas de processamento:
    1. Interpretação de eventos: para serem úteis para fins de análise, os eventos precisam ser interpretados no contexto de uma única reprodução. Por exemplo, o evento bruto de uma mudança de estado do player para STATE_BUFFERING pode corresponder ao buffer inicial, a um novo buffer ou ao buffer que acontece após uma busca.
    2. Rastreamento de estado: esta etapa converte eventos em contadores. Por exemplo, os eventos de mudança de estado podem ser convertidos em contadores que rastreiam quanto tempo é gasto em cada estado de reprodução. O resultado é um conjunto básico de valores de dados de análise para uma única reprodução.
    3. Agregação: combina os dados de análise em várias reproduções, geralmente somando contadores.
    4. Cálculo de métricas de resumo: muitas das métricas mais úteis são aquelas que calculam médias ou combinam os valores básicos de dados do Google Analytics de outras maneiras. As métricas de resumo podem ser calculadas para uma ou várias reproduções.

Coleta de eventos com AnalyticsListener

Os eventos de reprodução brutos do player são informados às implementações do AnalyticsListener. É fácil adicionar seu próprio listener e substituir apenas os métodos de seu interesse:

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

O EventTime transmitido a cada callback associa o evento a um item de mídia na playlist, bem como a posição de reprodução e os metadados de carimbo de data/hora:

  • realtimeMs: o horário do relógio de parede do evento.
  • timeline, windowIndex e mediaPeriodId: definem a playlist e o item nela a que o evento pertence. O mediaPeriodId contém informações adicionais opcionais, por exemplo, indicando se o evento pertence a um anúncio no item.
  • eventPlaybackPositionMs: a posição de reprodução no item quando o evento ocorreu.
  • currentTimeline, currentWindowIndex, currentMediaPeriodId e currentPlaybackPositionMs: como acima, mas para o item em reprodução no momento. O item em reprodução no momento pode ser diferente do item a que o evento pertence, por exemplo, se o evento corresponder ao pré-buffer do próximo item a ser reproduzido.

Processamento de eventos com PlaybackStatsListener

PlaybackStatsListener é um AnalyticsListener que implementa o processamento de eventos no dispositivo. Ele calcula PlaybackStats com contadores e métricas derivadas, incluindo:

  • Métricas de resumo, por exemplo, o tempo total de reprodução.
  • Métricas de qualidade de reprodução adaptável, como a resolução média do vídeo.
  • Métricas de qualidade de renderização, por exemplo, a taxa de frames descartados.
  • Métricas de uso de recursos, por exemplo, o número de bytes lidos na rede.

Confira a lista completa de contagens e métricas derivadas disponíveis no Javadoc do PlaybackStats.

O PlaybackStatsListener calcula PlaybackStats separados para cada item de mídia na playlist e também para cada anúncio do lado do cliente inserido nesses itens. Você pode fornecer um callback para PlaybackStatsListener e receber informações sobre as reproduções concluídas. Use o EventTime transmitido ao callback para identificar qual reprodução foi concluída. É possível agregar os dados de análise para várias reproduções. Também é possível consultar o PlaybackStats da sessão de reprodução atual a qualquer momento usando 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.
        }));

O construtor de PlaybackStatsListener oferece a opção de manter o histórico completo de eventos processados. Isso pode gerar uma sobrecarga de memória desconhecida, dependendo da duração da reprodução e do número de eventos. Portanto, ative essa opção apenas se precisar acessar o histórico completo de eventos processados, e não apenas os dados de análise finais.

O PlaybackStats usa um conjunto estendido de estados para indicar não apenas o estado da mídia, mas também a intenção do usuário de reproduzir e informações mais detalhadas, como o motivo da interrupção ou do fim da reprodução:

Estado da reprodução Intenção do usuário de jogar Sem intenção de jogar
Antes da exibição JOINING_FOREGROUND NOT_STARTED, JOINING_BACKGROUND
Reprodução ativa PLAYING
Reprodução interrompida BUFFERING, SEEKING PAUSED, PAUSED_BUFFERING, SUPPRESSED, SUPPRESSED_BUFFERING, INTERRUPTED_BY_AD
Estados finais ENDED, STOPPED, FAILED, ABANDONED

A intenção do usuário de jogar é importante para distinguir os momentos em que ele estava esperando ativamente a continuação da reprodução dos tempos de espera passivos. Por exemplo, PlaybackStats.getTotalWaitTimeMs retorna o tempo total gasto nos estados JOINING_FOREGROUND, BUFFERING e SEEKING, mas não o tempo em que a reprodução foi pausada. Da mesma forma, PlaybackStats.getTotalPlayAndWaitTimeMs vai retornar o tempo total com uma intenção do usuário de jogar, ou seja, o tempo total de espera ativa e o tempo total gasto no estado PLAYING.

Eventos processados e interpretados

É possível gravar eventos processados e interpretados usando PlaybackStatsListener com keepHistory=true. O PlaybackStats resultante vai conter as seguintes listas de eventos:

  • playbackStateHistory: uma lista ordenada de estados de reprodução estendida com o EventTime em que eles começaram a ser aplicados. Também é possível usar PlaybackStats.getPlaybackStateAtTime para pesquisar o estado em um determinado horário do relógio de parede.
  • mediaTimeHistory: um histórico de pares de tempo de relógio de parede e tempo de mídia que permite reconstruir quais partes da mídia foram reproduzidas em qual momento. Também é possível usar PlaybackStats.getMediaTimeMsAtRealtimeMs para pesquisar a posição de reprodução em um determinado horário.
  • videoFormatHistory e audioFormatHistory: listas ordenadas de formatos de vídeo e áudio usados durante a reprodução com o EventTime em que eles começaram a ser usados.
  • fatalErrorHistory e nonFatalErrorHistory: listas ordenadas de erros fatais e não fatais com o EventTime em que ocorreram. Os erros fatais são aqueles que encerraram a reprodução, enquanto os não fatais podem ter sido recuperáveis.

Dados de análise de reprodução única

Esses dados são coletados automaticamente se você usa o PlaybackStatsListener, mesmo com o keepHistory=false. Os valores finais são os campos públicos que podem ser encontrados no Javadoc do PlaybackStats e as durações do estado de reprodução retornadas por getPlaybackStateDurationMs. Para sua conveniência, você também encontra métodos como getTotalPlayTimeMs e getTotalWaitTimeMs que retornam a duração de combinações específicas de estados de reprodução.

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

Agregar dados de análise de várias reproduções

Você pode combinar vários PlaybackStats chamando PlaybackStats.merge. O PlaybackStats resultante vai conter os dados agregados de todas as reproduções combinadas. Ele não contém o histórico de eventos de reprodução individuais, já que não é possível agregá-los.

O PlaybackStatsListener.getCombinedPlaybackStats pode ser usado para ter uma visão agregada de todos os dados de análise coletados durante o ciclo de vida de um PlaybackStatsListener.

Métricas de resumo calculadas

Além dos dados básicos de análise, o PlaybackStats oferece muitos métodos para calcular métricas de resumo.

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

Temas avançados

Como associar dados de análise a metadados de reprodução

Ao coletar dados de análise de reproduções individuais, talvez você queira associar esses dados a metadados sobre a mídia que está sendo reproduzida.

É recomendável definir metadados específicos da mídia com MediaItem.Builder.setTag. A tag de mídia faz parte do EventTime informado para eventos brutos e quando os PlaybackStats são concluídos. Assim, ela pode ser facilmente recuperada ao processar os dados de análise correspondentes:

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

Relatórios de eventos de análise personalizada

Se você precisar adicionar eventos personalizados aos dados de análise, salve esses eventos na sua própria estrutura de dados e combine-os com o PlaybackStats informado mais tarde. Se ajudar, estenda DefaultAnalyticsCollector para gerar instâncias EventTime para seus eventos personalizados e envie-as aos listeners já registrados, conforme mostrado no exemplo a seguir.

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