Eventi giocatore

Le modifiche allo stato del player (ad esempio l'avvio della riproduzione, il buffering o gli errori) attivano eventi che vengono inviati alle istanze Player.Listener registrate. Questi eventi sono rappresentati da costanti intere e sono definiti da Player.Event e Player.Events.

Registrare un Player.Listener

Gli eventi del player vengono segnalati alle istanze Player.Listener registrate. Per registrare un listener per ricevere questi eventi:

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 utilizzi Kotlin, puoi anche utilizzare le funzioni di estensione di sospensione fornite dal modulo media3-common-ktx per ascoltare gli eventi utilizzando le coroutine. In questo caso, non dovrai registrare o annullare la registrazione di un Player.Listener in modo esplicito.

Ascoltare gli eventi di riproduzione utilizzando Player.Listener

Player.Listener ha metodi predefiniti vuoti, quindi devi implementare solo i metodi di tuo interesse. Consulta il Javadoc per una descrizione completa dei metodi e di quando vengono chiamati. Alcuni dei metodi più importanti sono descritti in modo più dettagliato di seguito.

I listener possono scegliere di implementare i singoli callback degli eventi o un callback onEvents generico chiamato dopo che si sono verificati uno o più eventi contemporaneamente. Consulta la sezione Individual callbacks vs onEvents per una spiegazione di quale dovrebbe essere preferito per i diversi casi d'uso.

Modifiche dello stato di riproduzione

Le modifiche allo stato del player possono essere ricevute implementando onPlaybackStateChanged(@State int state) in un Player.Listener registrato. Il player può trovarsi in uno dei quattro stati di riproduzione:

  • Player.STATE_IDLE: questo è lo stato iniziale, lo stato in cui il player è arrestato e quando la riproduzione non è riuscita. In questo stato, il player manterrà solo risorse limitate.
  • Player.STATE_BUFFERING: il player non è in grado di riprodurre immediatamente dalla posizione corrente. Questo accade principalmente perché è necessario caricare altri dati.
  • Player.STATE_READY: il player è in grado di riprodurre immediatamente dalla posizione corrente.
  • Player.STATE_ENDED: il player ha terminato la riproduzione di tutti i contenuti multimediali.

Oltre a questi stati, il player ha un flag playWhenReady per indicare l'intenzione dell'utente di riprodurre. Le modifiche a questo flag possono essere ricevute implementando onPlayWhenReadyChanged(playWhenReady, @PlayWhenReadyChangeReason int reason).

Un player è in riproduzione (ovvero la sua posizione sta avanzando e i contenuti multimediali vengono presentati all'utente) quando sono soddisfatte tutte e tre le seguenti condizioni:

  • Il player è nello stato Player.STATE_READY
  • playWhenReady è true
  • La riproduzione non viene soppressa per un motivo restituito da Player.getPlaybackSuppressionReason

Anziché dover controllare queste proprietà singolarmente, è possibile chiamare Player.isPlaying. Le modifiche a questo stato possono essere ricevute 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.
        }
      }
    });

Errori di riproduzione

Gli errori che causano il fallimento della riproduzione possono essere ricevuti implementando onPlayerError(PlaybackException error) in un Player.Listener registrato. Quando si verifica un errore, questo metodo viene chiamato immediatamente prima che lo stato di riproduzione passi a Player.STATE_IDLE. È possibile riprovare a riprodurre le riproduzioni non riuscite o interrotte chiamando ExoPlayer.prepare.

Tieni presente che alcune Player implementazioni passano istanze di sottoclassi di PlaybackException per fornire ulteriori informazioni sull'errore. Ad esempio, ExoPlayer passa ExoPlaybackException, che ha type, rendererIndex, e altri campi specifici di ExoPlayer.

L'esempio seguente mostra come rilevare quando una riproduzione non è riuscita a causa di un problema di rete 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.
          }
        }
      }
    });

Transizioni delle playlist

Ogni volta che il player passa a un nuovo elemento multimediale nella playlist onMediaItemTransition(MediaItem mediaItem, @MediaItemTransitionReason int reason) viene chiamato sugli oggetti Player.Listener registrati. Il motivo indica se si è trattato di una transizione automatica, di una ricerca (ad esempio dopo aver chiamato player.next()), di una ripetizione dello stesso elemento o causata da una modifica della playlist (ad esempio, se l'elemento in riproduzione corrente viene rimosso).

Metadati

I metadati restituiti da player.getCurrentMediaMetadata() possono cambiare per molti motivi: transizioni delle playlist, aggiornamenti dei metadati in-stream o aggiornamento dell'elemento MediaItem corrente durante la riproduzione.

Se ti interessano le modifiche ai metadati, ad esempio per aggiornare un'interfaccia utente che mostra il titolo corrente, puoi ascoltare onMediaMetadataChanged.

Attivazione dello spostamento in corso

La chiamata ai metodi Player.seekTo genera una serie di callback alle istanze Player.Listener registrate:

  1. onPositionDiscontinuity con reason=DISCONTINUITY_REASON_SEEK. Questo è il risultato diretto della chiamata a Player.seekTo. Il callback ha campi PositionInfo per la posizione prima e dopo la ricerca.
  2. onPlaybackStateChanged con qualsiasi modifica immediata dello stato correlata alla ricerca. Tieni presente che potrebbe non esserci una modifica di questo tipo.

Callback individuali rispetto a onEvents

I listener possono scegliere tra l'implementazione di callback individuali come onIsPlayingChanged(boolean isPlaying), e il callback generico onEvents(Player player, Events events). Il callback generico fornisce l'accesso all'oggetto Player e specifica l'insieme di events che si sono verificati contemporaneamente. Questo callback viene sempre chiamato dopo i callback che corrispondono ai singoli eventi.

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

I singoli eventi devono essere preferiti nei seguenti casi:

  • Il listener è interessato ai motivi delle modifiche. Ad esempio, i motivi forniti per onPlayWhenReadyChanged o onMediaItemTransition.
  • Il listener agisce solo sui nuovi valori forniti tramite i parametri di callback o attiva qualcos'altro che non dipende dai parametri di callback.
  • L'implementazione del listener preferisce un'indicazione chiara e leggibile di ciò che ha attivato l'evento nel nome del metodo.
  • Il listener genera report per un sistema di analisi che deve conoscere tutti i singoli eventi e le modifiche dello stato.

Il callback generico onEvents(Player player, Events events) deve essere preferito nei seguenti casi:

  • Il listener vuole attivare la stessa logica per più eventi. Ad esempio, aggiornare un'interfaccia utente sia per onPlaybackStateChanged sia per onPlayWhenReadyChanged.
  • Il listener deve accedere all'oggetto Player per attivare altri eventi, ad esempio la ricerca dopo una transizione di un elemento multimediale.
  • Il listener intende utilizzare più valori di stato segnalati tramite callback separati o in combinazione con i metodi getter Player. Ad esempio, l'utilizzo di Player.getCurrentWindowIndex() con Timeline fornito in onTimelineChanged è sicuro solo all'interno del callback onEvents.
  • Il listener è interessato a sapere se gli eventi si sono verificati logicamente insieme. Ad esempio, onPlaybackStateChanged a STATE_BUFFERING a causa di una transizione di un elemento multimediale.

In alcuni casi, i listener potrebbero dover combinare i singoli callback con il callback generico onEvents, ad esempio per registrare i motivi della modifica dell'elemento multimediale con onMediaItemTransition, ma agire solo quando tutti i cambiamenti di stato possono essere utilizzati insieme in onEvents.

Ascoltare gli eventi di riproduzione utilizzando le coroutine

In alternativa, puoi avviare una coroutine Kotlin utilizzando Player.listenTo e specificare il Player.Event pertinente:

Tieni presente che Player.listen e Player.listenTo possono essere chiamati da qualsiasi thread, mentre la lambda di callback viene sempre richiamata sul thread associato a Player.getApplicationLooper. Pertanto, è sicuro accedere ai metodi Player e alle proprietà di stato all'interno della lambda di callback anche se la coroutine è stata avviata su un thread diverso.

Modifiche dello stato di riproduzione

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

Errori di riproduzione

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

Callback individuali rispetto a onEvents

Quando ascolti gli eventi del player all'interno di una coroutine, fornisci sempre l'implementazione per il callback onEvents, anziché un callback individuale. Puoi scegliere tra Player.listen e Player.listenTo, a seconda degli eventi che devono attivare una chiamata della lambda. Tuttavia, le funzioni sono equivalenti:

ascolta

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

ascolta

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 ti interessano più tipi di eventi, puoi passare un elenco di eventi a Player.listenTo. La lambda verrà richiamata ogni volta che si verifica uno di questi eventi e puoi esaminare il parametro Events per verificare quali eventi sono stati effettivamente attivati:

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

Poiché queste funzioni operano su onEvents, non forniscono l'accesso agli argomenti temporanei passati ai singoli callback, come il motivo in onMediaItemTransition(..., int reason) o oldPosition in onPositionDiscontinuity(...). Se la tua logica si basa su questi argomenti specifici (e non sono disponibili come proprietà di stato in Player), devi utilizzare l'interfaccia Player.Listener standard.

Utilizzare AnalyticsListener

Quando utilizzi ExoPlayer, puoi registrare un AnalyticsListener con il player chiamando addAnalyticsListener. Le implementazioni di AnalyticsListener sono in grado di ascoltare eventi dettagliati che possono essere utili per l'analisi e la registrazione. Per maggiori dettagli, consulta la pagina relativa all'analisi.

Utilizzare EventLogger

EventLogger è un AnalyticsListener fornito direttamente dalla libreria per la registrazione. Aggiungi EventLogger a un ExoPlayer per attivare la registrazione aggiuntiva utile con una sola riga:

Kotlin

player.addAnalyticsListener(EventLogger())

Java

player.addAnalyticsListener(new EventLogger());

Per maggiori dettagli, consulta la pagina relativa alla registrazione di debug.

Attivare gli eventi in posizioni di riproduzione specifiche

Alcuni casi d'uso richiedono l'attivazione di eventi in posizioni di riproduzione specifiche. Questa funzionalità è supportata tramite PlayerMessage. È possibile creare un PlayerMessage utilizzando ExoPlayer.createMessage. La posizione di riproduzione in cui deve essere eseguita può essere impostata utilizzando PlayerMessage.setPosition. Per impostazione predefinita, i messaggi vengono eseguiti sul thread di riproduzione, ma questa impostazione può essere personalizzata utilizzando PlayerMessage.setLooper. Puoi utilizzare PlayerMessage.setDeleteAfterDelivery per controllare se il messaggio verrà eseguito ogni volta che viene rilevata la posizione di riproduzione specificata (ciò può accadere più volte a causa delle modalità di ricerca e ripetizione) o solo la prima volta. Una volta configurato PlayerMessage, può essere pianificato utilizzando 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();