Événements du joueur

Les modifications apportées à l'état du lecteur (par exemple, le démarrage de la lecture, la mise en mémoire tampon ou les erreurs) déclenchent des événements qui sont envoyés aux instances Player.Listener enregistrées. Ces événements sont représentés par des constantes entières et sont définis par Player.Event et Player.Events.

Enregistrer un Player.Listener

Les événements du lecteur sont signalés aux instances Player.Listener enregistrées. Pour enregistrer un écouteur afin de recevoir ces événements :

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 vous utilisez Kotlin, vous pouvez également utiliser les fonctions d'extension de suspension fournies par le module media3-common-ktx pour écouter les événements à l'aide de coroutines. Dans ce cas, vous n'aurez pas besoin d'enregistrer ni de désenregistrer explicitement un Player.Listener.

Écouter les événements de lecture à l'aide de Player.Listener

Player.Listener comporte des méthodes par défaut vides. Vous n'avez donc besoin d'implémenter que les méthodes qui vous intéressent. Consultez la documentation Javadoc pour obtenir une description complète des méthodes et savoir quand elles sont appelées. Certaines des méthodes les plus importantes sont décrites plus en détail ci-dessous.

Les écouteurs peuvent choisir d'implémenter des rappels d'événements individuels ou un rappel onEvents générique qui est appelé après qu'un ou plusieurs événements se sont produits ensemble. Consultez la section Individual callbacks vs onEvents pour savoir quelle option privilégier pour différents cas d'utilisation.

Changements d'état de la lecture

Les modifications de l'état du lecteur peuvent être reçues en implémentant onPlaybackStateChanged(@State int state) dans un Player.Listener enregistré. Les états de lecture du lecteur sont au nombre de quatre :

  • Player.STATE_IDLE: il s'agit de l'état initial, de l'état lorsque le lecteur est arrêté et de l'état lorsque la lecture a échoué. Dans cet état, le lecteur ne détient que des ressources limitées.
  • Player.STATE_BUFFERING: le lecteur ne peut pas lire immédiatement à partir de sa position actuelle. Cela se produit principalement parce que davantage de données doivent être chargées.
  • Player.STATE_READY: le lecteur peut démarrer immédiatement la lecture depuis la position actuelle.
  • Player.STATE_ENDED : le lecteur a terminé la lecture de tous les contenus multimédias.

En plus de ces états, le lecteur dispose d'un indicateur playWhenReady pour indiquer l'intention de l'utilisateur de lire. Les modifications apportées à cet indicateur peuvent être reçues en implémentant onPlayWhenReadyChanged(playWhenReady, @PlayWhenReadyChangeReason int reason).

Un lecteur est en cours de lecture (c'est-à-dire que sa position avance et que le contenu multimédia est présenté à l'utilisateur) lorsque les trois conditions suivantes sont remplies :

  • Le lecteur est dans l'état Player.STATE_READY.
  • playWhenReady est true.
  • La lecture n'est pas supprimée pour une raison renvoyée par Player.getPlaybackSuppressionReason.

Au lieu de devoir vérifier ces propriétés individuellement, vous pouvez appeler Player.isPlaying. Les modifications apportées à cet état peuvent être reçues en implémentant 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.
        }
      }
    });

Erreurs de lecture

Les erreurs qui entraînent l'échec de la lecture peuvent être reçues en implémentant onPlayerError(PlaybackException error) dans un Player.Listener enregistré. En cas d'échec, cette méthode est appelée immédiatement avant que l'état de lecture ne passe à Player.STATE_IDLE. Vous pouvez réessayer les lectures ayant échoué ou ayant été arrêtées en appelant ExoPlayer.prepare.

Notez que certaines Player implémentations transmettent des instances de sous-classes de PlaybackException pour fournir des informations supplémentaires sur l'échec. Par exemple, ExoPlayer transmet ExoPlaybackException, qui comporte type, rendererIndex, et d'autres champs spécifiques à ExoPlayer.

L'exemple suivant montre comment détecter l'échec d'une lecture en raison d'un problème de réseau 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.
          }
        }
      }
    });

Transitions de playlists

Chaque fois que le lecteur passe à un nouvel élément multimédia dans la playlist onMediaItemTransition(MediaItem mediaItem, @MediaItemTransitionReason int reason) est appelé sur les objets Player.Listener enregistrés. La raison indique s'il s'agit d'une transition automatique, d'une recherche (par exemple, après avoir appelé player.next()), d'une répétition du même élément ou d'une modification de la playlist (par exemple, si l'élément en cours de lecture est supprimé).

Métadonnées

Les métadonnées renvoyées par player.getCurrentMediaMetadata() peuvent changer pour de nombreuses raisons : transitions de playlists, mises à jour des métadonnées InStream ou mise à jour de l'élément MediaItem actuel en cours de lecture.

Si les modifications des métadonnées vous intéressent, par exemple pour mettre à jour une interface utilisateur qui affiche le titre actuel, vous pouvez écouter onMediaMetadataChanged.

Recherche…

L'appel des méthodes Player.seekTo entraîne une série de rappels aux instances Player.Listener enregistrées :

  1. onPositionDiscontinuity avec reason=DISCONTINUITY_REASON_SEEK. Il s'agit du résultat direct de l'appel de Player.seekTo. Le rappel comporte des champs PositionInfo pour la position avant et après la recherche.
  2. onPlaybackStateChanged avec toute modification d'état immédiate liée à la recherche. Notez qu'il peut ne pas y avoir de modification.

Rappels individuels par rapport à onEvents

Les écouteurs peuvent choisir d'implémenter des rappels individuels tels que onIsPlayingChanged(boolean isPlaying), et le rappel générique onEvents(Player player, Events events). Le rappel générique permet d'accéder à l'objet Player et spécifie l'ensemble des events qui se sont produits ensemble. Ce rappel est toujours appelé après les rappels qui correspondent aux événements individuels.

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

Les événements individuels doivent être privilégiés dans les cas suivants :

  • L'écouteur s'intéresse aux raisons des modifications. Par exemple, les raisons fournies pour onPlayWhenReadyChanged ou onMediaItemTransition.
  • L'écouteur n'agit que sur les nouvelles valeurs fournies via les paramètres de rappel ou déclenche autre chose qui ne dépend pas des paramètres de rappel.
  • L'implémentation de l'écouteur préfère une indication claire et lisible de ce qui a déclenché l'événement dans le nom de la méthode.
  • L'écouteur signale à un système d'analyse qui doit connaître tous les événements individuels et les changements d'état.

Le rappel générique onEvents(Player player, Events events) doit être privilégié dans les cas suivants :

  • L'écouteur souhaite déclencher la même logique pour plusieurs événements. Par exemple, la mise à jour d'une interface utilisateur pour onPlaybackStateChanged et onPlayWhenReadyChanged.
  • L'écouteur doit accéder à l'objet Player pour déclencher d'autres événements, par exemple une recherche après une transition d'élément multimédia.
  • L'écouteur a l'intention d'utiliser plusieurs valeurs d'état signalées via des rappels distincts, ou en combinaison avec des méthodes getter Player. Par exemple, l'utilisation de Player.getCurrentWindowIndex() avec le Timeline fourni dans onTimelineChanged n'est sécurisée que depuis le onEvents rappel.
  • L'écouteur souhaite savoir si les événements se sont produits ensemble de manière logique. Par exemple, onPlaybackStateChanged à STATE_BUFFERING en raison d'une transition d'élément multimédia.

Dans certains cas, les écouteurs peuvent avoir besoin de combiner les rappels individuels avec le rappel générique onEvents, par exemple pour enregistrer les raisons de modification des éléments multimédias avec onMediaItemTransition, mais n'agir qu'une fois que toutes les modifications d'état peuvent être utilisées ensemble dans onEvents.

Écouter les événements de lecture à l'aide de coroutines

Vous pouvez également lancer une coroutine Kotlin à l'aide de Player.listenTo et spécifier le Player.Event approprié :

Notez que Player.listen et Player.listenTo peuvent être appelés à partir de n'importe quel thread, tandis que le lambda de rappel est toujours appelé sur le thread associé à Player.getApplicationLooper. Par conséquent, il est sûr d'accéder aux méthodes Player et aux propriétés d'état dans le lambda de rappel, même si la coroutine a été lancée sur un autre thread.

Changements d'état de la lecture

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

Erreurs de lecture

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

Rappels individuels par rapport à onEvents

Lorsque vous écoutez des événements de lecteur dans une coroutine, vous fournissez toujours l'implémentation du rappel onEvents, par opposition à un rappel individuel. Vous pouvez choisir entre Player.listen et Player.listenTo, en fonction des événements qui doivent déclencher un appel de votre lambda. Les fonctions sont par ailleurs équivalentes :

écouter

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 plusieurs types d'événements vous intéressent, vous pouvez transmettre une liste d'événements à Player.listenTo. Votre lambda sera appelé chaque fois que l'un de ces événements se produira, et vous pourrez inspecter le paramètre Events pour vérifier quels événements ont réellement été déclenchés :

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

Étant donné que ces fonctions fonctionnent sur onEvents, elles ne permettent pas d'accéder aux arguments transitoires transmis aux rappels individuels, tels que la raison dans onMediaItemTransition(..., int reason) ou oldPosition dans onPositionDiscontinuity(...). Si votre logique repose sur ces arguments spécifiques (et qu'ils ne sont pas disponibles en tant que propriétés d'état sur le Player), vous devez utiliser l'interface Player.Listener standard.

Utiliser AnalyticsListener

Lorsque vous utilisez ExoPlayer, un AnalyticsListener peut être enregistré auprès du lecteur en appelant addAnalyticsListener. Les implémentations AnalyticsListener peuvent écouter des événements détaillés qui peuvent être utiles à des fins d'analyse et de journalisation. Pour en savoir plus, consultez la page sur l'analyse.

Utiliser EventLogger

EventLogger est un AnalyticsListener fourni directement par la bibliothèque à des fins de journalisation. Ajoutez EventLogger à un ExoPlayer pour activer une journalisation supplémentaire utile en une seule ligne :

Kotlin

player.addAnalyticsListener(EventLogger())

Java

player.addAnalyticsListener(new EventLogger());

Pour en savoir plus, consultez la page sur la journalisation de débogage.

Déclencher des événements à des positions de lecture spécifiées

Certains cas d'utilisation nécessitent de déclencher des événements à des positions de lecture spécifiées. Cette fonctionnalité est compatible avec PlayerMessage. Un PlayerMessage peut être créé à l'aide de ExoPlayer.createMessage. La position de lecture à laquelle il doit être exécuté peut être définie à l'aide de PlayerMessage.setPosition. Les messages sont exécutés sur le thread de lecture par défaut, mais ce comportement peut être personnalisé à l'aide de PlayerMessage.setLooper. PlayerMessage.setDeleteAfterDelivery permet de contrôler si le message sera exécuté chaque fois que la position de lecture spécifiée est rencontrée (cela peut se produire plusieurs fois en raison des modes de recherche et de répétition) ou uniquement la première fois. Une fois le PlayerMessage configuré, il peut être planifié à l'aide de 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();