Personalización

En el núcleo de la biblioteca de ExoPlayer, se encuentra la interfaz Player. Un Player expone la funcionalidad tradicional de alto nivel del reproductor multimedia, como la capacidad de almacenar contenido multimedia en el búfer, reproducirlo, pausarlo y buscarlo. La implementación predeterminada ExoPlayer está diseñada para hacer pocas suposiciones sobre el tipo de contenido multimedia que se reproduce, cómo y dónde se almacena, y cómo se renderiza (y, por lo tanto, imponer pocas restricciones). En lugar de implementar la carga y la renderización de contenido multimedia directamente, las implementaciones de ExoPlayer delegan este trabajo a los componentes que se insertan cuando se crea un reproductor o cuando se pasan nuevas fuentes de contenido multimedia al reproductor. Los componentes comunes a todas las implementaciones de ExoPlayer son los siguientes:

  • Instancias de MediaSource que definen el contenido multimedia que se reproducirá, cargan el contenido multimedia y desde las que se puede leer el contenido multimedia cargado. Una instancia de MediaSource se crea a partir de un MediaItem por un MediaSource.Factory dentro del reproductor. También se pueden pasar directamente al reproductor con la API de playlist basada en fuentes de contenido multimedia.
  • Es una instancia de MediaSource.Factory que convierte un MediaItem en un MediaSource. El MediaSource.Factory se inserta cuando se crea el reproductor.
  • Instancias de Renderer que renderizan componentes individuales del contenido multimedia Se insertan cuando se crea el reproductor.
  • Un TrackSelector que selecciona las pistas que proporciona el MediaSource para que las consuma cada Renderer disponible. Se inserta un TrackSelector cuando se crea el reproductor.
  • Un LoadControl que controla cuándo el MediaSource almacena en búfer más contenido multimedia y cuánto contenido se almacena en búfer Se inserta un LoadControl cuando se crea el reproductor.
  • Un LivePlaybackSpeedControl que controla la velocidad de reproducción durante las reproducciones en vivo para permitir que el reproductor se mantenga cerca de un desplazamiento en vivo configurado. Se inserta un LivePlaybackSpeedControl cuando se crea el reproductor.

El concepto de insertar componentes que implementan partes de la funcionalidad del reproductor está presente en toda la biblioteca. Las implementaciones predeterminadas de algunos componentes delegan el trabajo a otros componentes insertados. Esto permite que muchos subcomponentes se reemplacen de forma individual por implementaciones que se configuran de manera personalizada.

Personalización del reproductor

En las siguientes secciones, se describen algunos ejemplos comunes de personalización del reproductor mediante la inserción de componentes.

Configura la pila de red

Tenemos una página sobre cómo personalizar la pila de red que usa ExoPlayer.

Cómo almacenar en caché los datos cargados desde la red

Consulta las guías para el almacenamiento en caché temporal sobre la marcha y la descarga de contenido multimedia.

Personaliza las interacciones con el servidor

Es posible que algunas apps quieran interceptar solicitudes y respuestas HTTP. Es posible que quieras insertar encabezados de la solicitud personalizados, leer los encabezados de respuesta del servidor, modificar los URI de las solicitudes, etc. Por ejemplo, tu app puede autenticarse insertando un token como encabezado cuando solicita los segmentos de contenido multimedia.

En el siguiente ejemplo, se muestra cómo implementar estos comportamientos insertando un DataSource.Factory personalizado en el 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();

En el fragmento de código anterior, el HttpDataSource insertado incluye el encabezado "Header: Value" en cada solicitud HTTP. Este comportamiento es fijo para cada interacción con una fuente HTTP.

Para un enfoque más detallado, puedes insertar un comportamiento de tipo justo a tiempo con un ResolvingDataSource. En el siguiente fragmento de código, se muestra cómo insertar encabezados de solicitud justo antes de interactuar con una fuente 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)));

También puedes usar un ResolvingDataSource para realizar modificaciones de último momento en el URI, como se muestra en el siguiente fragmento:

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

Personaliza el manejo de errores

Implementar un LoadErrorHandlingPolicy personalizado permite que las apps personalicen la forma en que ExoPlayer reacciona a los errores de carga. Por ejemplo, es posible que una app deba fallar con rapidez en lugar de reintentar muchas veces, o que deba personalizar la lógica de retirada que controla cuánto tiempo espera el reproductor entre cada reintento. En el siguiente fragmento, se muestra cómo implementar la lógica de retirada personalizada:

Kotlin

val loadErrorHandlingPolicy: LoadErrorHandlingPolicy =
  object : DefaultLoadErrorHandlingPolicy() {
    override fun getRetryDelayMsFor(
      loadErrorInfo: LoadErrorHandlingPolicy.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(LoadErrorHandlingPolicy.LoadErrorInfo loadErrorInfo) {
        // Implement custom back-off logic here.
        return 0;
      }
    };

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

El argumento LoadErrorInfo contiene más información sobre la carga fallida para personalizar la lógica según el tipo de error o la solicitud fallida.

Personaliza las marcas del extractor

Las marcas de extractor se pueden usar para personalizar la forma en que se extraen los formatos individuales del contenido multimedia progresivo. Se pueden establecer en el DefaultExtractorsFactory que se proporciona al DefaultMediaSourceFactory. En el siguiente ejemplo, se pasa una marca que habilita la búsqueda basada en índices para las transmisiones de 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();

Habilita la búsqueda de tasa de bits constante

Para las transmisiones de MP3, ADTS y AMR, puedes habilitar la búsqueda aproximada con una suposición de tasa de bits constante con las marcas FLAG_ENABLE_CONSTANT_BITRATE_SEEKING. Estas marcas se pueden establecer para extractores individuales con los métodos DefaultExtractorsFactory.setXyzExtractorFlags individuales, como se describió anteriormente. Para habilitar la búsqueda de tasa de bits constante en todos los extractores que la admiten, usa DefaultExtractorsFactory.setConstantBitrateSeekingEnabled.

Kotlin

val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)

Java

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

Luego, se puede insertar el ExtractorsFactory con DefaultMediaSourceFactory, como se describió antes para personalizar las marcas del extractor.

Habilita la puesta en cola asíncrona de búferes

La puesta en cola asíncrona de búferes es una mejora en la canalización de renderización de ExoPlayer, que opera instancias de MediaCodec en modo asíncrono y usa subprocesos adicionales para programar la decodificación y la renderización de datos. Si la habilitas, puedes reducir los fotogramas perdidos y los subdesbordamientos de audio.

La puesta en cola asíncrona de búferes está habilitada de forma predeterminada en los dispositivos que ejecutan Android 12 (nivel de API 31) o versiones posteriores, y se puede habilitar de forma manual a partir de Android 6.0 (nivel de API 23). Considera habilitar la función para dispositivos específicos en los que observes fotogramas perdidos o insuficiencias de audio, en especial cuando se reproduce contenido protegido por DRM o con una alta velocidad de fotogramas.

En el caso más simple, debes insertar un DefaultRenderersFactory en el reproductor de la siguiente manera:

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 creas instancias de renderizadores directamente, pasa new DefaultMediaCodecAdapter.Factory(context).forceEnableAsynchronous() a los constructores MediaCodecVideoRenderer y MediaCodecAudioRenderer.

Personaliza las operaciones con ForwardingSimpleBasePlayer

Puedes personalizar parte del comportamiento de una instancia de Player si la unes a una subclase de ForwardingSimpleBasePlayer. Esta clase te permite interceptar "operaciones" específicas, en lugar de tener que implementar directamente métodos Player. Esto garantiza un comportamiento coherente, por ejemplo, de play(), pause() y setPlayWhenReady(boolean). También garantiza que todos los cambios de estado se propaguen correctamente a las instancias de Player.Listener registradas. Para la mayoría de los casos de uso de personalización, se debe preferir ForwardingSimpleBasePlayer en lugar de ForwardingPlayer, que es más propenso a errores, debido a estas garantías de coherencia.

Por ejemplo, para agregar una lógica personalizada cuando se inicia o se detiene la reproducción, haz lo siguiente:

Kotlin

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

Java

public static final class PlayerWithCustomPlay extends ForwardingSimpleBasePlayer {

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

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

O bien, para no permitir el comando SEEK_TO_NEXT (y garantizar que Player.seekToNext no realice ninguna operación), haz lo siguiente:

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

public static final 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.
}

Personalización de MediaSource

En los ejemplos anteriores, se insertan componentes personalizados para usarlos durante la reproducción de todos los objetos MediaItem que se pasan al reproductor. Cuando se requiere una personalización detallada, también es posible insertar componentes personalizados en instancias individuales de MediaSource, que se pueden pasar directamente al reproductor. En el siguiente ejemplo, se muestra cómo personalizar un ProgressiveMediaSource para usar un DataSource.Factory, un ExtractorsFactory y un LoadErrorHandlingPolicy personalizados:

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

Crea componentes personalizados

La biblioteca proporciona implementaciones predeterminadas de los componentes que se indican en la parte superior de esta página para casos de uso comunes. Un ExoPlayer puede usar estos componentes, pero también se puede compilar para usar implementaciones personalizadas si se requieren comportamientos no estándar. Estos son algunos casos de uso de las implementaciones personalizadas:

  • Renderer: Es posible que quieras implementar un Renderer personalizado para controlar un tipo de contenido multimedia que no admiten las implementaciones predeterminadas que proporciona la biblioteca.
  • TrackSelector: Implementar un TrackSelector personalizado permite que un desarrollador de apps cambie la forma en que se seleccionan las pistas expuestas por un MediaSource para que las consuma cada uno de los Renderer disponibles.
  • LoadControl: Implementar un LoadControl personalizado permite que un desarrollador de apps cambie la política de almacenamiento en búfer del reproductor.
  • Extractor: Si necesitas admitir un formato de contenedor que la biblioteca no admite actualmente, considera implementar una clase Extractor personalizada.
  • MediaSource: Implementar una clase MediaSource personalizada puede ser adecuado si quieres obtener muestras de contenido multimedia para alimentar los renderizadores de una manera personalizada o si quieres implementar un comportamiento de composición de MediaSource personalizado.
  • MediaSource.Factory: Implementar un MediaSource.Factory personalizado permite que una aplicación personalice la forma en que se crea un MediaSource a partir de un MediaItem.
  • DataSource: El paquete upstream de ExoPlayer ya contiene varias implementaciones de DataSource para diferentes casos de uso. Te recomendamos implementar tu propia clase DataSource para cargar datos de otra manera, como a través de un protocolo personalizado, con una pila HTTP personalizada o desde una caché persistente personalizada.

Cuando compiles componentes personalizados, te recomendamos lo siguiente:

  • Si un componente personalizado necesita informar eventos a la app, te recomendamos que lo hagas con el mismo modelo que los componentes existentes de ExoPlayer, por ejemplo, con clases EventDispatcher o pasando un Handler junto con un objeto de escucha al constructor del componente.
  • Recomendamos que los componentes personalizados usen el mismo modelo que los componentes existentes de ExoPlayer para permitir que la app los vuelva a configurar durante la reproducción. Para ello, los componentes personalizados deben implementar PlayerMessage.Target y recibir cambios de configuración en el método handleMessage. El código de la aplicación debe pasar los cambios de configuración llamando al método createMessage de ExoPlayer, configurando el mensaje y enviándolo al componente con PlayerMessage.send. Enviar mensajes para que se entreguen en el subproceso de reproducción garantiza que se ejecuten en orden con cualquier otra operación que se realice en el reproductor.