Personnalisation

L'interface Player est au cœur de la bibliothèque ExoPlayer. Un Player expose les fonctionnalités traditionnelles de haut niveau du lecteur multimédia, telles que la mise en mémoire tampon, la lecture, la pause et la recherche. L'implémentation par défaut ExoPlayer est conçue pour faire peu d'hypothèses (et donc imposer peu de restrictions) sur le type de contenu multimédia lu, sur la façon et l'endroit où il est stocké, et sur la façon dont il est rendu. Plutôt que d'implémenter directement le chargement et le rendu des contenus multimédias, les implémentations ExoPlayer délèguent ce travail à des composants qui sont injectés lorsqu'un lecteur est créé ou lorsque de nouvelles sources multimédias sont transmises au lecteur. Voici les composants communs à toutes les implémentations ExoPlayer :

  • Instances MediaSource qui définissent le contenu multimédia à lire, le chargent et à partir desquelles le contenu multimédia chargé peut être lu. Une instance MediaSource est créée à partir d'un MediaItem par un MediaSource.Factory dans le lecteur. Ils peuvent également être transmis directement au lecteur à l'aide de l'API de playlist basée sur la source multimédia.
  • Instance MediaSource.Factory qui convertit un MediaItem en MediaSource. MediaSource.Factory est injecté lorsque le lecteur est créé.
  • Instances Renderer qui affichent des composants individuels du contenu multimédia. Elles sont injectées lors de la création du lecteur.
  • Un TrackSelector qui sélectionne les pistes fournies par le MediaSource pour qu'elles soient utilisées par chaque Renderer disponible. Un TrackSelector est injecté lors de la création du lecteur.
  • LoadControl qui contrôle le moment où MediaSource met en mémoire tampon davantage de contenus multimédias et la quantité de contenus multimédias mis en mémoire tampon. Un LoadControl est injecté lorsque le lecteur est créé.
  • Un LivePlaybackSpeedControl qui contrôle la vitesse de lecture pendant les lectures en direct pour permettre au lecteur de rester proche d'un décalage en direct configuré. Un LivePlaybackSpeedControl est injecté lorsque le lecteur est créé.

Le concept d'injection de composants qui implémentent des éléments de fonctionnalité du lecteur est présent dans toute la bibliothèque. Les implémentations par défaut de certains composants délèguent le travail à d'autres composants injectés. Cela permet de remplacer individuellement de nombreux sous-composants par des implémentations configurées de manière personnalisée.

Personnalisation du lecteur

Vous trouverez ci-dessous quelques exemples courants de personnalisation du lecteur en injectant des composants.

Configurer la pile réseau

Nous avons une page sur la personnalisation de la pile réseau utilisée par ExoPlayer.

Mise en cache des données chargées à partir du réseau

Consultez les guides sur la mise en cache temporaire à la volée et le téléchargement de contenus multimédias.

Personnaliser les interactions avec le serveur

Certaines applications peuvent vouloir intercepter les requêtes et les réponses HTTP. Vous pouvez injecter des en-têtes de requête personnalisés, lire les en-têtes de réponse du serveur, modifier les URI des requêtes, etc. Par exemple, votre application peut s'authentifier en injectant un jeton en tant qu'en-tête lorsqu'elle demande les segments multimédias.

L'exemple suivant montre comment implémenter ces comportements en injectant un DataSource.Factory personnalisé dans le DefaultMediaSourceFactory :

Kotlin

val dataSourceFactory =
  DataSource.Factory {
    val dataSource = httpDataSourceFactory.createDataSource()
    // Set a custom authentication request header.
    dataSource.setRequestProperty("Header", "Value")
    dataSource
  }
val player =
  ExoPlayer.Builder(context)
    .setMediaSourceFactory(
      DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory)
    )
    .build()

Java

DataSource.Factory dataSourceFactory =
    () -> {
      HttpDataSource dataSource = httpDataSourceFactory.createDataSource();
      // Set a custom authentication request header.
      dataSource.setRequestProperty("Header", "Value");
      return dataSource;
    };

ExoPlayer player =
    new ExoPlayer.Builder(context)
        .setMediaSourceFactory(
            new DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory))
        .build();

Dans l'extrait de code ci-dessus, l'HttpDataSource injecté inclut l'en-tête "Header: Value" dans chaque requête HTTP. Ce comportement est corrigé pour chaque interaction avec une source HTTP.

Pour une approche plus précise, vous pouvez injecter un comportement juste à temps à l'aide d'un ResolvingDataSource. L'extrait de code suivant montre comment injecter des en-têtes de requête juste avant d'interagir avec une source HTTP :

Kotlin

val dataSourceFactory: DataSource.Factory =
  ResolvingDataSource.Factory(httpDataSourceFactory) { dataSpec: DataSpec ->
    // Provide just-in-time request headers.
    dataSpec.withRequestHeaders(getCustomHeaders(dataSpec.uri))
  }

Java

    DataSource.Factory dataSourceFactory =
        new ResolvingDataSource.Factory(
            httpDataSourceFactory,
            // Provide just-in-time request headers.
            dataSpec -> dataSpec.withRequestHeaders(getCustomHeaders(dataSpec.uri)));

Vous pouvez également utiliser un ResolvingDataSource pour effectuer des modifications juste à temps de l'URI, comme indiqué dans l'extrait suivant :

Kotlin

val dataSourceFactory: DataSource.Factory =
  ResolvingDataSource.Factory(httpDataSourceFactory) { dataSpec: DataSpec ->
    // Provide just-in-time URI resolution logic.
    dataSpec.withUri(resolveUri(dataSpec.uri))
  }

Java

DataSource.Factory dataSourceFactory =
    new ResolvingDataSource.Factory(
        httpDataSourceFactory,
        // Provide just-in-time URI resolution logic.
        dataSpec -> dataSpec.withUri(resolveUri(dataSpec.uri)));

Personnaliser la gestion des erreurs

L'implémentation d'un LoadErrorHandlingPolicy personnalisé permet aux applications de personnaliser la façon dont ExoPlayer réagit aux erreurs de chargement. Par exemple, une application peut vouloir échouer rapidement au lieu de réessayer plusieurs fois, ou vouloir personnaliser la logique de backoff qui contrôle la durée d'attente du joueur entre chaque tentative. L'extrait suivant montre comment implémenter une logique de backoff personnalisée :

Kotlin

val loadErrorHandlingPolicy: LoadErrorHandlingPolicy =
  object : DefaultLoadErrorHandlingPolicy() {
    override fun getRetryDelayMsFor(loadErrorInfo: LoadErrorInfo): Long {
      // Implement custom back-off logic here.
      return 0
    }
  }
val player =
  ExoPlayer.Builder(context)
    .setMediaSourceFactory(
      DefaultMediaSourceFactory(context).setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
    )
    .build()

Java

LoadErrorHandlingPolicy loadErrorHandlingPolicy =
    new DefaultLoadErrorHandlingPolicy() {
      @Override
      public long getRetryDelayMsFor(LoadErrorInfo loadErrorInfo) {
        // Implement custom back-off logic here.
        return 0;
      }
    };

ExoPlayer player =
    new ExoPlayer.Builder(context)
        .setMediaSourceFactory(
            new DefaultMediaSourceFactory(context)
                .setLoadErrorHandlingPolicy(loadErrorHandlingPolicy))
        .build();

L'argument LoadErrorInfo contient plus d'informations sur l'échec du chargement pour personnaliser la logique en fonction du type d'erreur ou de la requête ayant échoué.

Personnaliser les indicateurs de l'extracteur

Les indicateurs d'extraction peuvent être utilisés pour personnaliser la façon dont les formats individuels sont extraits des contenus multimédias progressifs. Elles peuvent être définies sur le DefaultExtractorsFactory fourni au DefaultMediaSourceFactory. L'exemple suivant transmet un indicateur qui permet la recherche basée sur l'index pour les flux MP3.

Kotlin

val extractorsFactory =
  DefaultExtractorsFactory().setMp3ExtractorFlags(Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING)
val player =
  ExoPlayer.Builder(context)
    .setMediaSourceFactory(DefaultMediaSourceFactory(context, extractorsFactory))
    .build()

Java

DefaultExtractorsFactory extractorsFactory =
    new DefaultExtractorsFactory().setMp3ExtractorFlags(Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING);

ExoPlayer player =
    new ExoPlayer.Builder(context)
        .setMediaSourceFactory(new DefaultMediaSourceFactory(context, extractorsFactory))
        .build();

Activer la recherche à débit constant

Pour les flux MP3, ADTS et AMR, vous pouvez activer la recherche approximative à l'aide d'une hypothèse de débit constant avec les indicateurs FLAG_ENABLE_CONSTANT_BITRATE_SEEKING. Ces indicateurs peuvent être définis pour des extracteurs individuels à l'aide des méthodes DefaultExtractorsFactory.setXyzExtractorFlags individuelles, comme décrit ci-dessus. Pour activer la recherche à débit constant pour tous les extracteurs qui la prennent en charge, utilisez DefaultExtractorsFactory.setConstantBitrateSeekingEnabled.

Kotlin

val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)

Java

DefaultExtractorsFactory extractorsFactory =
    new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);

Le ExtractorsFactory peut ensuite être injecté via DefaultMediaSourceFactory comme décrit ci-dessus pour la personnalisation des indicateurs d'extraction.

Activer la mise en file d'attente asynchrone des tampons

La mise en file d'attente asynchrone des tampons est une amélioration du pipeline de rendu d'ExoPlayer, qui exécute des instances MediaCodec en mode asynchrone et utilise des threads supplémentaires pour planifier le décodage et le rendu des données. L'activation de cette option peut réduire les pertes d'images et les sous-dépassements audio.

La mise en file d'attente asynchrone des tampons est activée par défaut sur les appareils équipés d'Android 12 (niveau d'API 31) ou version ultérieure. Elle peut être activée manuellement à partir d'Android 6.0 (niveau d'API 23). Envisagez d'activer la fonctionnalité pour des appareils spécifiques sur lesquels vous constatez des pertes d'images ou des sous-dépassements audio, en particulier lorsque vous lisez du contenu protégé par DRM ou à fréquence d'images élevée.

Dans le cas le plus simple, vous devez injecter un DefaultRenderersFactory au lecteur comme suit :

Kotlin

val renderersFactory = 
  DefaultRenderersFactory(context).forceEnableMediaCodecAsynchronousQueueing()
val exoPlayer = ExoPlayer.Builder(context, renderersFactory).build()

Java

DefaultRenderersFactory renderersFactory =
    new DefaultRenderersFactory(context).forceEnableMediaCodecAsynchronousQueueing();
ExoPlayer exoPlayer = new ExoPlayer.Builder(context, renderersFactory).build();

Si vous instanciez directement des renderers, transmettez new DefaultMediaCodecAdapter.Factory(context).forceEnableAsynchronous() aux constructeurs MediaCodecVideoRenderer et MediaCodecAudioRenderer.

Personnaliser les opérations avec ForwardingSimpleBasePlayer

Vous pouvez personnaliser certains comportements d'une instance Player en l'encapsulant dans une sous-classe de ForwardingSimpleBasePlayer. Cette classe vous permet d'intercepter des "opérations" spécifiques, plutôt que d'avoir à implémenter directement les méthodes Player. Cela garantit un comportement cohérent de, par exemple, play(), pause() et setPlayWhenReady(boolean). Il garantit également que tous les changements d'état sont correctement propagés aux instances Player.Listener enregistrées. Pour la plupart des cas d'utilisation de la personnalisation, il est préférable d'utiliser ForwardingSimpleBasePlayer plutôt que ForwardingPlayer, qui est plus sujette aux erreurs, en raison de ces garanties de cohérence.

Par exemple, pour ajouter une logique personnalisée lorsque la lecture est lancée ou arrêtée :

Kotlin

class PlayerWithCustomPlay(player: Player) : ForwardingSimpleBasePlayer(player) {
  override fun handleSetPlayWhenReady(playWhenReady: Boolean): ListenableFuture<*> {
    // Add custom logic
    return super.handleSetPlayWhenReady(playWhenReady)
  }
}

Java

class PlayerWithCustomPlay extends ForwardingSimpleBasePlayer {

  public PlayerWithCustomPlay(Player player) {
    super(player);
  }

  @Override
  protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) {
    // Add custom logic
    return super.handleSetPlayWhenReady(playWhenReady);
  }
}

Ou, pour interdire la commande SEEK_TO_NEXT (et s'assurer que Player.seekToNext est une opération sans effet) :

Kotlin

class PlayerWithoutSeekToNext(player: Player) : ForwardingSimpleBasePlayer(player) {
  override fun getState(): State {
    val state = super.getState()
    return state
      .buildUpon()
      .setAvailableCommands(
        state.availableCommands.buildUpon().remove(COMMAND_SEEK_TO_NEXT).build()
      )
      .build()
  }

  // We don't need to override handleSeek, because it is guaranteed not to be called for
  // COMMAND_SEEK_TO_NEXT since we've marked that command unavailable.
}

Java

class PlayerWithoutSeekToNext extends ForwardingSimpleBasePlayer {

  public PlayerWithoutSeekToNext(Player player) {
    super(player);
  }

  @Override
  protected State getState() {
    State state = super.getState();
    return state
        .buildUpon()
        .setAvailableCommands(
            state.availableCommands.buildUpon().remove(COMMAND_SEEK_TO_NEXT).build())
        .build();
  }

  // We don't need to override handleSeek, because it is guaranteed not to be called for
  // COMMAND_SEEK_TO_NEXT since we've marked that command unavailable.
}

Personnalisation de MediaSource

Les exemples ci-dessus injectent des composants personnalisés à utiliser lors de la lecture de tous les objets MediaItem transmis au lecteur. Lorsque vous avez besoin d'une personnalisation précise, vous pouvez également injecter des composants personnalisés dans des instances MediaSource individuelles, qui peuvent être transmises directement au lecteur. L'exemple ci-dessous montre comment personnaliser un ProgressiveMediaSource pour utiliser un DataSource.Factory, un ExtractorsFactory et un LoadErrorHandlingPolicy personnalisés :

Kotlin

val mediaSource =
  ProgressiveMediaSource.Factory(customDataSourceFactory, customExtractorsFactory)
    .setLoadErrorHandlingPolicy(customLoadErrorHandlingPolicy)
    .createMediaSource(MediaItem.fromUri(streamUri))

Java

ProgressiveMediaSource mediaSource =
    new ProgressiveMediaSource.Factory(customDataSourceFactory, customExtractorsFactory)
        .setLoadErrorHandlingPolicy(customLoadErrorHandlingPolicy)
        .createMediaSource(MediaItem.fromUri(streamUri));

Créer des composants personnalisés

La bibliothèque fournit des implémentations par défaut des composants listés en haut de cette page pour les cas d'utilisation courants. Un ExoPlayer peut utiliser ces composants, mais peut également être conçu pour utiliser des implémentations personnalisées si des comportements non standards sont requis. Voici quelques cas d'utilisation des implémentations personnalisées :

  • Renderer : vous pouvez implémenter un Renderer personnalisé pour gérer un type de support non compatible avec les implémentations par défaut fournies par la bibliothèque.
  • TrackSelector : l'implémentation d'un TrackSelector personnalisé permet à un développeur d'application de modifier la façon dont les pistes exposées par un MediaSource sont sélectionnées pour être utilisées par chacun des Renderer disponibles.
  • LoadControl : l'implémentation d'un LoadControl personnalisé permet à un développeur d'application de modifier la stratégie de mise en mémoire tampon du lecteur.
  • Extractor : si vous devez prendre en charge un format de conteneur qui n'est pas actuellement compatible avec la bibliothèque, envisagez d'implémenter une classe Extractor personnalisée.
  • MediaSource : l'implémentation d'une classe MediaSource personnalisée peut être appropriée si vous souhaitez obtenir des exemples de contenus multimédias à fournir aux renderers de manière personnalisée, ou si vous souhaitez implémenter un comportement de composition MediaSource personnalisé.
  • MediaSource.Factory : l'implémentation d'un MediaSource.Factory personnalisé permet à une application de personnaliser la façon dont un MediaSource est créé à partir d'un MediaItem.
  • DataSource : le package en amont d'ExoPlayer contient déjà un certain nombre d'implémentations DataSource pour différents cas d'utilisation. Vous pouvez implémenter votre propre classe DataSource pour charger des données d'une autre manière, par exemple via un protocole personnalisé, à l'aide d'une pile HTTP personnalisée ou à partir d'un cache persistant personnalisé.

Lorsque vous créez des composants personnalisés, nous vous recommandons de procéder comme suit :

  • Si un composant personnalisé doit renvoyer des événements à l'application, nous vous recommandons de le faire en utilisant le même modèle que les composants ExoPlayer existants, par exemple en utilisant des classes EventDispatcher ou en transmettant un Handler avec un écouteur au constructeur du composant.
  • Nous vous recommandons d'utiliser le même modèle pour les composants personnalisés que pour les composants ExoPlayer existants afin de permettre à l'application de les reconfigurer pendant la lecture. Pour ce faire, les composants personnalisés doivent implémenter PlayerMessage.Target et recevoir les modifications de configuration dans la méthode handleMessage. Le code de l'application doit transmettre les modifications de configuration en appelant la méthode createMessage d'ExoPlayer, en configurant le message et en l'envoyant au composant à l'aide de PlayerMessage.send. L'envoi de messages à diffuser sur le thread de lecture garantit qu'ils sont exécutés dans l'ordre avec toutes les autres opérations effectuées sur le lecteur.