Eventos de player

As mudanças no estado do player (como início da reprodução, bufferização ou erros) acionam eventos enviados a instâncias registradas de Player.Listener. Esses eventos são representados por constantes inteiras e definidos por Player.Event e Player.Events.

Registrar um Player.Listener

Os eventos do player são informados a instâncias registradas de Player.Listener. Para registrar um listener e receber esses eventos:

Kotlin

// Add a listener to receive events from the player.
player.addListener(listener)

Java

// Add a listener to receive events from the player.
player.addListener(listener);

Se você estiver usando o Kotlin, também poderá usar as funções de extensão de suspensão fornecidas pelo módulo media3-common-ktx para detectar eventos usando corrotinas. Nesse caso, não será necessário registrar ou cancelar o registro de um Player.Listener explicitamente.

Detectar eventos de reprodução usando Player.Listener

Player.Listener tem métodos padrão vazios. Portanto, você só precisa implementar os métodos de seu interesse. Consulte o Javadoc para uma descrição completa dos métodos e quando eles são chamados. Alguns dos métodos mais importantes são descritos com mais detalhes abaixo.

Os listeners podem implementar callbacks de eventos individuais ou um callback genérico onEvents que é chamado depois que um ou mais eventos ocorrem juntos. Consulte Individual callbacks vs onEvents para uma explicação de qual deve ser preferido para diferentes casos de uso.

Mudanças no estado da reprodução

As mudanças no estado do player podem ser recebidas implementando onPlaybackStateChanged(@State int state) em um Player.Listener registrado. O player pode estar em um dos quatro estados de reprodução:

  • Player.STATE_IDLE: esse é o estado inicial, o estado quando o player é interrompido e quando a reprodução falha. O player vai manter apenas recursos limitados nesse estado.
  • Player.STATE_BUFFERING: o player não pode reproduzir imediatamente a partir da posição atual. Isso acontece principalmente porque mais dados precisam ser carregados.
  • Player.STATE_READY: o player pode reproduzir imediatamente a partir da posição atual.
  • Player.STATE_ENDED: o player terminou de reproduzir toda a mídia.

Além desses estados, o player tem uma flag playWhenReady para indicar a intenção do usuário de reproduzir. As mudanças nessa flag podem ser recebidas implementando onPlayWhenReadyChanged(playWhenReady, @PlayWhenReadyChangeReason int reason).

Um player está reproduzindo (ou seja, a posição está avançando e a mídia está sendo apresentada ao usuário) quando todas as três condições a seguir são atendidas:

  • O player está no estado Player.STATE_READY.
  • playWhenReady é true.
  • A reprodução não é suprimida por um motivo retornado por Player.getPlaybackSuppressionReason.

Em vez de verificar essas propriedades individualmente, Player.isPlaying pode ser chamado. As mudanças nesse estado podem ser recebidas implementando onIsPlayingChanged(boolean isPlaying):

Kotlin

player.addListener(
  object : Player.Listener {
    override fun onIsPlayingChanged(isPlaying: Boolean) {
      if (isPlaying) {
        // Active playback.
      } else {
        // Not playing because playback is paused, ended, suppressed, or the player
        // is buffering, stopped or failed. Check player.playWhenReady,
        // player.playbackState, player.playbackSuppressionReason and
        // player.playerError for details.
      }
    }
  }
)

Java

player.addListener(
    new Player.Listener() {
      @Override
      public void onIsPlayingChanged(boolean isPlaying) {
        if (isPlaying) {
          // Active playback.
        } else {
          // Not playing because playback is paused, ended, suppressed, or the player
          // is buffering, stopped or failed. Check player.getPlayWhenReady,
          // player.getPlaybackState, player.getPlaybackSuppressionReason and
          // player.getPlaybackError for details.
        }
      }
    });

Erros de reprodução

Os erros que causam falha na reprodução podem ser recebidos implementando onPlayerError(PlaybackException error) em um Player.Listener registrado. Quando ocorre uma falha, esse método é chamado imediatamente antes que o estado de reprodução faça a transição para Player.STATE_IDLE. As reproduções com falha ou interrompidas podem ser repetidas chamando ExoPlayer.prepare.

Algumas Player implementações transmitem instâncias de subclasses de PlaybackException para fornecer mais informações sobre a falha. Por exemplo, ExoPlayer transmite ExoPlaybackException, que tem type, rendererIndex, e outros campos específicos do ExoPlayer.

O exemplo a seguir mostra como detectar quando uma reprodução falhou devido a um problema de rede HTTP:

Kotlin

player.addListener(
  object : Player.Listener {
    override fun onPlayerError(error: PlaybackException) {
      val cause = error.cause
      if (cause is HttpDataSourceException) {
        // An HTTP error occurred.
        val httpError = cause
        // It's possible to find out more about the error both by casting and by querying
        // the cause.
        if (httpError is InvalidResponseCodeException) {
          // Cast to InvalidResponseCodeException and retrieve the response code, message
          // and headers.
        } else {
          // Try calling httpError.getCause() to retrieve the underlying cause, although
          // note that it may be null.
        }
      }
    }
  }
)

Java

player.addListener(
    new Player.Listener() {
      @Override
      public void onPlayerError(PlaybackException error) {
        @Nullable Throwable cause = error.getCause();
        if (cause instanceof HttpDataSourceException) {
          // An HTTP error occurred.
          HttpDataSourceException httpError = (HttpDataSourceException) cause;
          // It's possible to find out more about the error both by casting and by querying
          // the cause.
          if (httpError instanceof HttpDataSource.InvalidResponseCodeException) {
            // Cast to InvalidResponseCodeException and retrieve the response code, message
            // and headers.
          } else {
            // Try calling httpError.getCause() to retrieve the underlying cause, although
            // note that it may be null.
          }
        }
      }
    });

Transições de playlist

Sempre que o player muda para um novo item de mídia na playlist onMediaItemTransition(MediaItem mediaItem, @MediaItemTransitionReason int reason) é chamado em objetos Player.Listener registrados. O motivo indica se essa foi uma transição automática, uma busca (por exemplo, depois de chamar player.next()), uma repetição do mesmo item ou causada por uma mudança na playlist (por exemplo, se o item em reprodução for removido).

Metadados

Os metadados retornados de player.getCurrentMediaMetadata() podem mudar por vários motivos: transições de playlist, atualizações de metadados no stream ou atualização do MediaItem atual durante a reprodução.

Se você estiver interessado em mudanças de metadados, por exemplo, para atualizar uma interface que mostra o título atual, poderá detectar onMediaMetadataChanged.

Procurando

A chamada de métodos Player.seekTo resulta em uma série de callbacks para instâncias registradas de Player.Listener:

  1. onPositionDiscontinuity com reason=DISCONTINUITY_REASON_SEEK. Esse é o resultado direto da chamada de Player.seekTo. O callback tem campos PositionInfo para a posição antes e depois da busca.
  2. onPlaybackStateChanged com qualquer mudança de estado imediata relacionada à busca. Pode ser que não haja essa mudança.

Callbacks individuais versus onEvents

Os listeners podem escolher entre implementar callbacks individuais, como onIsPlayingChanged(boolean isPlaying), e o callback genérico onEvents(Player player, Events events). O callback genérico fornece acesso ao objeto Player e especifica o conjunto de events que ocorreram juntos. Esse callback é sempre chamado após os callbacks que correspondem aos eventos individuais.

Kotlin

override fun onEvents(player: Player, events: Player.Events) {
  if (
    events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED) ||
      events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)
  ) {
    uiModule.updateUi(player)
  }
}

Java

@Override
public void onEvents(Player player, Events events) {
  if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)
      || events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
    uiModule.updateUi(player);
  }
}

Os eventos individuais devem ser preferidos nos seguintes casos:

  • O listener está interessado nos motivos das mudanças. Por exemplo, os motivos fornecidos para onPlayWhenReadyChanged ou onMediaItemTransition.
  • O listener só age nos novos valores fornecidos por parâmetros de callback ou aciona algo mais que não depende dos parâmetros de callback.
  • A implementação do listener prefere uma indicação clara e legível do que acionou o evento no nome do método.
  • O listener informa a um sistema de análise que precisa saber sobre todos os eventos individuais e mudanças de estado.

O onEvents(Player player, Events events) genérico deve ser preferido nos seguintes casos:

  • O listener quer acionar a mesma lógica para vários eventos. Por exemplo, atualizar uma interface para onPlaybackStateChanged e onPlayWhenReadyChanged.
  • O listener precisa acessar o objeto Player para acionar outros eventos, por exemplo, buscar após uma transição de item de mídia.
  • O listener pretende usar vários valores de estado informados por callbacks separados ou em combinação com métodos getter Player. Por exemplo, usar Player.getCurrentWindowIndex() com a Timeline fornecida em onTimelineChanged só é seguro no onEvents callback.
  • O listener está interessado em saber se os eventos ocorreram logicamente juntos. Por exemplo, onPlaybackStateChanged para STATE_BUFFERING devido a uma transição de item de mídia.

Em alguns casos, os listeners podem precisar combinar os callbacks individuais com o callback genérico onEvents, por exemplo, para registrar motivos de mudança de item de mídia com onMediaItemTransition, mas só agir quando todas as mudanças de estado puderem ser usadas juntas em onEvents.

Detectar eventos de reprodução usando corrotinas

Como alternativa, você pode iniciar uma corrotina Kotlin usando Player.listenTo e especificar o Player.Event relevante:

As funções Player.listen e Player.listenTo podem ser chamadas de qualquer linha de execução, enquanto a lambda de callback é sempre invocada na linha de execução associada a Player.getApplicationLooper. Portanto, é seguro acessar métodos Player e propriedades de estado dentro da lambda de callback, mesmo que a corrotina tenha sido iniciada em uma linha de execução diferente.

Mudanças no estado da reprodução

coroutineScope.launch {
  player.listenTo(Player.EVENT_IS_PLAYING_CHANGED) {
    // `Player` is a receiver scope for this trailing lambda
    if (isPlaying) {
      // Active playback.
    } else {
      // Not playing.
    }
  }
}

Erros de reprodução

coroutineScope.launch {
  player.listenTo(Player.EVENT_PLAYER_ERROR) {
    val error = playerError ?: return@listenTo
    val cause = error.cause
    if (cause is HttpDataSourceException) {
      // An HTTP error occurred.
      if (cause is InvalidResponseCodeException) {
        // Retrieve the response code, message and headers
      } else {
        // Try calling cause.cause to retrieve the underlying cause
      }
    }
  }
}

Callbacks individuais versus onEvents

Ao detectar eventos do player em uma corrotina, você sempre vai fornecer a implementação do callback onEvents, em vez de um callback individual. Você pode escolher entre Player.listen e Player.listenTo, dependendo de quais eventos devem acionar uma invocação da lambda. No entanto, as funções são equivalentes:

ouvir

coroutineScope.launch {
  player.listen { events ->
    // `Player` is a receiver scope for this trailing lambda
    if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
      // Access the player state directly from the receiver
      updateUi(playbackState)
    }

    if (events.contains(Player.EVENT_PLAYER_ERROR)) {
      // Access the error directly from the player
      handleError(playerError)
    }
  }
}

listenTo

coroutineScope.launch {
  player.listenTo(Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYER_ERROR) { events ->
    // `Player` is a receiver scope for this trailing lambda
    if (events.contains(Player.EVENT_PLAYBACK_STATE_CHANGED)) {
      // Access the player state directly from the receiver
      updateUi(playbackState)
    }

    if (events.contains(Player.EVENT_PLAYER_ERROR)) {
      // Access the error directly from the player
      handleError(playerError)
    }
  }
}

Se você estiver interessado em vários tipos de eventos, poderá transmitir uma lista de eventos para Player.listenTo. A lambda será invocada sempre que um desses eventos ocorrer, e você poderá inspecionar o parâmetro Events para verificar quais eventos foram acionados:

coroutineScope.launch {
  player.listenTo(Player.EVENT_PLAYBACK_STATE_CHANGED, Player.EVENT_PLAYER_ERROR) { events ->
    // Unclear which event got triggered without querying `events` parameter
    // The following function will fire whenever either one is caught
    updateUiAndHandleError(playbackState, playerError)
  }
}

Como essas funções operam em onEvents, elas não fornecem acesso aos argumentos temporários transmitidos a callbacks individuais, como o motivo em onMediaItemTransition(..., int reason) ou o oldPosition em onPositionDiscontinuity(...). Se a lógica depender desses argumentos específicos (e eles não estiverem disponíveis como propriedades de estado no Player), você deve usar a interface Player.Listener padrão.

Usar AnalyticsListener

Ao usar ExoPlayer, um AnalyticsListener pode ser registrado no player chamando addAnalyticsListener. As implementações de AnalyticsListener podem detectar eventos detalhados que podem ser úteis para fins de análise e registro. Consulte a página de análise para mais detalhes.

Usar EventLogger

EventLogger é um AnalyticsListener fornecido diretamente pela biblioteca para fins de registro. Adicione EventLogger a um ExoPlayer para ativar o registro adicional útil com uma única linha:

Kotlin

player.addAnalyticsListener(EventLogger())

Java

player.addAnalyticsListener(new EventLogger());

Consulte a página de registro de depuração para mais detalhes.

Acionar eventos em posições de reprodução especificadas

Alguns casos de uso exigem o acionamento de eventos em posições de reprodução especificadas. Isso é compatível com PlayerMessage. Um PlayerMessage pode ser criado usando ExoPlayer.createMessage. A posição de reprodução em que ele deve ser executado pode ser definida usando PlayerMessage.setPosition. As mensagens são executadas na linha de execução de reprodução por padrão, mas isso pode ser personalizado usando PlayerMessage.setLooper. PlayerMessage.setDeleteAfterDelivery pode ser usado para controlar se a mensagem será executada sempre que a posição de reprodução especificada for encontrada (isso pode acontecer várias vezes devido aos modos de busca e repetição) ou apenas na primeira vez. Depois que o PlayerMessage for configurado, ele poderá ser programado usando PlayerMessage.send.

Kotlin

player
  .createMessage { messageType: Int, payload: Any? -> }
  .setLooper(Looper.getMainLooper())
  .setPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 120000)
  .setPayload(customPayloadData)
  .setDeleteAfterDelivery(false)
  .send()

Java

player
    .createMessage(
        (messageType, payload) -> {
          // Do something at the specified playback position.
        })
    .setLooper(Looper.getMainLooper())
    .setPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 120_000)
    .setPayload(customPayloadData)
    .setDeleteAfterDelivery(false)
    .send();