Eventos del jugador

Los cambios en el estado del reproductor (como el inicio de la reproducción, el almacenamiento en búfer o los errores) activan eventos que se envían a las instancias de Player.Listener registradas. Estos eventos se representan con constantes de números enteros y se definen con Player.Event y Player.Events.

Registra un Player.Listener

Los eventos del reproductor se informan a las instancias de Player.Listener registradas. Para registrar un objeto de escucha que reciba esos eventos, haz lo siguiente:

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

Si usas Kotlin, también puedes utilizar las funciones de extensión de suspensión que proporciona el módulo media3-common-ktx para escuchar eventos con corrutinas. En ese caso, no necesitarás registrar ni cancelar el registro de un Player.Listener de forma explícita.

Escucha eventos de reproducción con Player.Listener

Player.Listener tiene métodos predeterminados vacíos, por lo que solo necesitas implementar los métodos que te interesen. Consulta el Javadoc para obtener una descripción completa de los métodos y cuándo se los llama. Algunos de los métodos más importantes se describen con más detalle más adelante.

Los objetos de escucha pueden elegir entre implementar devoluciones de llamada de eventos individuales o una devolución de llamada onEvents genérica que se llama después de que se producen uno o más eventos juntos. Consulta Individual callbacks vs onEvents para obtener una explicación de cuál se debe preferir para diferentes casos de uso.

Cambios en el estado de reproducción

Los cambios en el estado del reproductor se pueden recibir implementando onPlaybackStateChanged(@State int state) en un Player.Listener registrado. El reproductor puede estar en uno de los siguientes cuatro estados de reproducción:

  • Player.STATE_IDLE: Es el estado inicial, el estado cuando se detiene el reproductor y cuando falla la reproducción. El reproductor solo tendrá recursos limitados en este estado.
  • Player.STATE_BUFFERING: El reproductor no puede reproducir de inmediato desde su posición actual. Esto sucede principalmente porque se deben cargar más datos.
  • Player.STATE_READY: El reproductor puede reproducir de inmediato desde su posición actual.
  • Player.STATE_ENDED: El reproductor terminó de reproducir todo el contenido multimedia.

Además de estos estados, el reproductor tiene una marca playWhenReady para indicar la intención del usuario de reproducir contenido. Los cambios en este parámetro se pueden recibir implementando onPlayWhenReadyChanged(playWhenReady, @PlayWhenReadyChangeReason int reason).

Un reproductor está reproduciendo contenido (es decir, su posición avanza y se presenta contenido multimedia al usuario) cuando se cumplen las tres condiciones siguientes:

  • El reproductor está en el estado Player.STATE_READY.
  • playWhenReady es true.
  • La reproducción no se suprime por un motivo que devuelve Player.getPlaybackSuppressionReason.

En lugar de tener que verificar estas propiedades de forma individual, se puede llamar a Player.isPlaying. Para recibir los cambios en este estado, implementa 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.
        }
      }
    });

Errores de reproducción

Los errores que provocan que falle la reproducción se pueden recibir implementando onPlayerError(PlaybackException error) en un Player.Listener registrado. Cuando se produce un error, se llama a este método inmediatamente antes de que el estado de reproducción cambie a Player.STATE_IDLE. Las reproducciones fallidas o detenidas se pueden reintentar llamando a ExoPlayer.prepare.

Ten en cuenta que algunas implementaciones de Player pasan instancias de subclases de PlaybackException para proporcionar información adicional sobre la falla. Por ejemplo, ExoPlayer pasa ExoPlaybackException, que tiene type, rendererIndex y otros campos específicos de ExoPlayer.

En el siguiente ejemplo, se muestra cómo detectar cuándo falló una reproducción debido a un problema de red 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.
          }
        }
      }
    });

Transiciones de playlists

Cada vez que el reproductor cambia a un nuevo elemento multimedia de la playlist, se llama a onMediaItemTransition(MediaItem mediaItem, @MediaItemTransitionReason int reason) en los objetos Player.Listener registrados. El motivo indica si se trató de una transición automática, una búsqueda (por ejemplo, después de llamar a player.next()), una repetición del mismo elemento o un cambio en la playlist (por ejemplo, si se quita el elemento que se está reproduciendo).

Metadatos

Los metadatos que se muestran desde player.getCurrentMediaMetadata() pueden cambiar por muchos motivos: transiciones de playlists, actualizaciones de metadatos en la transmisión o actualización del MediaItem actual durante la reproducción.

Si te interesan los cambios en los metadatos, por ejemplo, para actualizar una IU que muestra el título actual, puedes escuchar onMediaMetadataChanged.

Buscando

Cuando se llama a los métodos Player.seekTo, se genera una serie de devoluciones de llamada a las instancias de Player.Listener registradas:

  1. onPositionDiscontinuity con reason=DISCONTINUITY_REASON_SEEK. Este es el resultado directo de llamar a Player.seekTo. La devolución de llamada tiene campos PositionInfo para la posición antes y después de la búsqueda.
  2. onPlaybackStateChanged con cualquier cambio de estado inmediato relacionado con la búsqueda. Ten en cuenta que es posible que no haya un cambio de este tipo.

Devoluciones de llamada individuales frente a onEvents

Los objetos de escucha pueden elegir entre implementar devoluciones de llamada individuales, como onIsPlayingChanged(boolean isPlaying), y la devolución de llamada genérica onEvents(Player player, Events events). La devolución de llamada genérica proporciona acceso al objeto Player y especifica el conjunto de events que se produjeron juntos. Esta devolución de llamada siempre se llama después de las devoluciones de llamada que corresponden a los eventos individuales.

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

Se deben preferir los eventos individuales en los siguientes casos:

  • El oyente se interesa por los motivos de los cambios. Por ejemplo, los motivos proporcionados para onPlayWhenReadyChanged o onMediaItemTransition.
  • El objeto de escucha solo actúa en los valores nuevos que se proporcionan a través de los parámetros de devolución de llamada o activa otra acción que no depende de estos parámetros.
  • La implementación del objeto de escucha prefiere una indicación clara y legible de lo que activó el evento en el nombre del método.
  • El objeto de escucha informa a un sistema de estadísticas que necesita conocer todos los eventos individuales y los cambios de estado.

Se debe preferir el onEvents(Player player, Events events) genérico en los siguientes casos:

  • El objeto de escucha quiere activar la misma lógica para varios eventos. Por ejemplo, actualizar una IU para onPlaybackStateChanged y onPlayWhenReadyChanged.
  • El objeto de escucha necesita acceder al objeto Player para activar más eventos, por ejemplo, buscar después de una transición de elemento multimedia.
  • El objeto de escucha tiene la intención de usar varios valores de estado que se informan a través de devoluciones de llamada independientes juntas o en combinación con los métodos get de Player. Por ejemplo, usar Player.getCurrentWindowIndex() con el Timeline proporcionado en onTimelineChanged solo es seguro desde la devolución de llamada onEvents.
  • El objeto de escucha está interesado en saber si los eventos ocurrieron juntos de forma lógica. Por ejemplo, de onPlaybackStateChanged a STATE_BUFFERING debido a una transición de elemento multimedia.

En algunos casos, es posible que los objetos de escucha deban combinar las devoluciones de llamada individuales con la devolución de llamada genérica onEvents, por ejemplo, para registrar los motivos de cambio de elementos multimedia con onMediaItemTransition, pero solo actuar una vez que todos los cambios de estado se puedan usar juntos en onEvents.

Escucha eventos de reproducción con corrutinas

Como alternativa, puedes iniciar una corrutina de Kotlin con Player.listenTo y especificar el Player.Event correspondiente:

Ten en cuenta que se puede llamar a Player.listen y Player.listenTo desde cualquier subproceso, mientras que la lambda de devolución de llamada siempre se invoca en el subproceso asociado con Player.getApplicationLooper. Por lo tanto, es seguro acceder a los métodos y las propiedades de estado de Player dentro de la expresión lambda de devolución de llamada, incluso si la corrutina se inició en un subproceso diferente.

Cambios en el estado de reproducción

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

Errores de reproducción

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

Devoluciones de llamada individuales frente a onEvents

Cuando escuches eventos del reproductor dentro de una corrutina, siempre proporcionarás la implementación para la devolución de llamada onEvents, en lugar de una devolución de llamada individual. Puedes elegir entre Player.listen y Player.listenTo, según los eventos que deban activar una invocación de tu lambda. Sin embargo, las funciones son equivalentes:

escuchar

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

Si te interesan varios tipos de eventos, puedes pasar una lista de eventos a Player.listenTo. Se invocará tu expresión lambda cada vez que ocurra cualquiera de esos eventos, y podrás inspeccionar el parámetro Events para verificar qué eventos se activaron realmente:

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

Debido a que estas funciones operan en onEvents, no proporcionan acceso a los argumentos transitorios que se pasan a devoluciones de llamada individuales, como el motivo en onMediaItemTransition(..., int reason) o el oldPosition en onPositionDiscontinuity(...). Si tu lógica se basa en estos argumentos específicos (y no están disponibles como propiedades de estado en Player), debes usar la interfaz estándar de Player.Listener.

Usar AnalyticsListener

Cuando se usa ExoPlayer, se puede registrar un AnalyticsListener con el reproductor llamando a addAnalyticsListener. Las implementaciones de AnalyticsListener pueden escuchar eventos detallados que pueden ser útiles para fines de análisis y registro. Consulta la página de estadísticas para obtener más detalles.

Usar EventLogger

EventLogger es un AnalyticsListener que proporciona directamente la biblioteca para fines de registro. Agrega EventLogger a un ExoPlayer para habilitar un registro adicional útil con una sola línea:

Kotlin

player.addAnalyticsListener(EventLogger())

Java

player.addAnalyticsListener(new EventLogger());

Consulta la página de registro de depuración para obtener más detalles.

Activa eventos en posiciones de reproducción especificadas

En algunos casos de uso, se deben activar eventos en posiciones de reproducción específicas. Esto se admite con PlayerMessage. Se puede crear un PlayerMessage con ExoPlayer.createMessage. La posición de reproducción en la que se debe ejecutar se puede establecer con PlayerMessage.setPosition. De forma predeterminada, los mensajes se ejecutan en el subproceso de reproducción, pero esto se puede personalizar con PlayerMessage.setLooper. PlayerMessage.setDeleteAfterDelivery se puede usar para controlar si el mensaje se ejecutará cada vez que se encuentre la posición de reproducción especificada (esto puede suceder varias veces debido a los modos de búsqueda y repetición) o solo la primera vez. Una vez que se configura el PlayerMessage, se puede programar con 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();