Personalización

En el centro de la biblioteca de ExoPlayer, se encuentra la interfaz Player. Un Player expone la funcionalidad tradicional de reproductor multimedia de alto nivel, como la capacidad de almacenar contenido en el búfer, reproducir, pausar y buscar. La implementación predeterminada ExoPlayer está diseñada para hacer algunas suposiciones sobre (y, por lo tanto, imponer pocas restricciones sobre) el tipo de contenido multimedia que se reproduce, cómo y dónde se almacena y cómo se renderiza. En lugar de implementar la carga y el procesamiento 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 le pasan nuevas fuentes de contenido multimedia. Los componentes comunes de todas las implementaciones de ExoPlayer son los siguientes:

  • Instancias de MediaSource que definen el contenido multimedia que se reproducirá, lo cargan y desde donde 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 playlists basadas en fuentes multimedia.
  • Una instancia de MediaSource.Factory que convierte un MediaItem en un MediaSource. El MediaSource.Factory se inserta cuando se crea el reproductor.
  • Instancias Renderer que renderizan componentes individuales del contenido multimedia Estos se insertan cuando se crea el reproductor.
  • Un TrackSelector que selecciona pistas proporcionadas por MediaSource para que las consuma cada Renderer disponible. Se inserta un TrackSelector cuando se crea el reproductor.
  • Un LoadControl que controla cuándo MediaSource almacena más contenido en el búfer y cuánto contenido multimedia se almacena en el búfer. Se inserta un LoadControl cuando se crea el jugador.
  • Un objeto 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 inyectar componentes que implementan partes de la funcionalidad del jugador está presente en toda la biblioteca. Las implementaciones predeterminadas de algunos componentes delegan trabajo a otros componentes inyectados. Esto permite reemplazar muchos subcomponentes de forma individual por implementaciones configuradas de forma personalizada.

Personalización del reproductor

A continuación, se describen algunos ejemplos comunes de cómo personalizar el 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.

Almacena datos en caché cargados desde la red

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

Personaliza las interacciones del servidor

Es posible que algunas apps quieran interceptar solicitudes y respuestas HTTP. Es posible que desees insertar encabezados de solicitud personalizados, leer los encabezados de respuesta del servidor, modificar los URIs de las solicitudes, etc. Por ejemplo, tu app puede autenticarse mediante la inserción de un token como encabezado cuando se solicitan los segmentos de medios.

En el siguiente ejemplo, se muestra cómo implementar estos comportamientos mediante la inserción de un DataSource.Factory personalizado en 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 corregido para cada interacción con una fuente HTTP.

Si deseas un enfoque más detallado, puedes insertar un comportamiento justo a tiempo mediante 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 justo a tiempo del 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

La implementación de 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 quiera fallar rápidamente en lugar de reintentarlo varias veces, o bien personalizar la lógica de retirada que controla el tiempo que espera el jugador 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: 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();

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

Personaliza las marcas del extractor

Las marcas de extracción se pueden usar para personalizar cómo se extraen los formatos individuales de los medios progresivos. Se pueden configurar en el DefaultExtractorsFactory que se proporciona a DefaultMediaSourceFactory. En el siguiente ejemplo, se pasa una marca que habilita la búsqueda basada en índices para transmisiones 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();

Cómo habilitar la búsqueda con tasa de bits constante

Para las transmisiones de MP3, ADTS y AMR, puedes habilitar la búsqueda aproximada mediante una suposición de tasa de bits constante con marcas FLAG_ENABLE_CONSTANT_BITRATE_SEEKING. Estas marcas se pueden configurar para extractores individuales con los métodos individuales de DefaultExtractorsFactory.setXyzExtractorFlags como se describió antes. Si quieres habilitar la búsqueda de tasa de bits constante para todos los extractores compatibles, usa DefaultExtractorsFactory.setConstantBitrateSeekingEnabled.

Kotlin

val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)

Java

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

Luego, ExtractorsFactory se puede insertar a través de DefaultMediaSourceFactory como se describió anteriormente para personalizar las marcas del extractor.

Habilita las colas de búfer asíncronas

La cola de búfer asíncrona es una mejora en la canalización de renderización de ExoPlayer, que opera instancias MediaCodec en modo asíncrono y usa subprocesos adicionales para programar la decodificación y la renderización de datos. Su habilitación puede reducir la pérdida de fotogramas y los subdesbordamientos de audio.

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

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 quieres crear instancias de procesadores de forma directa, pasa un AsynchronousMediaCodecAdapter.Factory a los constructores MediaCodecVideoRenderer y MediaCodecAudioRenderer.

Interceptación de llamadas de método con ForwardingPlayer

Puedes personalizar parte del comportamiento de una instancia de Player uniéndola a una subclase de ForwardingPlayer y anulando los métodos para realizar cualquiera de las siguientes acciones:

  • Accede a los parámetros antes de pasarlos al delegado Player.
  • Accede al valor que se muestra del delegado Player antes de mostrarlo.
  • Vuelve a implementar el método por completo.

Cuando se anulan métodos ForwardingPlayer, es importante garantizar que la implementación siga siendo autocoherente y cumpla con la interfaz Player, en especial cuando se trata de métodos que deben tener un comportamiento idéntico o relacionado. Por ejemplo:

  • Si deseas anular todas las operaciones de "play", debes anular ForwardingPlayer.play y ForwardingPlayer.setPlayWhenReady, ya que un llamador esperará que el comportamiento de estos métodos sea idéntico cuando playWhenReady = true.
  • Si deseas cambiar el incremento de búsqueda hacia adelante, debes anular ForwardingPlayer.seekForward para realizar una búsqueda con tu incremento personalizado y ForwardingPlayer.getSeekForwardIncrement para informar el incremento personalizado correcto al emisor.
  • Si deseas controlar qué Player.Commands anuncia una instancia del jugador, debes anular Player.getAvailableCommands() y Player.isCommandAvailable(), y escuchar la devolución de llamada Player.Listener.onAvailableCommandsChanged() para recibir notificaciones sobre los cambios provenientes del reproductor subyacente.

Personalización de MediaSource

En los ejemplos anteriores, se insertan componentes personalizados para usar 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 DataSource.Factory, ExtractorsFactory y 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 enumerados 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 para las implementaciones personalizadas:

  • Renderer: Te recomendamos que implementes un Renderer personalizado para controlar un tipo de medio que no es compatible con las implementaciones predeterminadas que proporciona la biblioteca.
  • TrackSelector: La implementación de un TrackSelector personalizado permite a un desarrollador de apps cambiar la forma en que se seleccionan los segmentos expuestos por un MediaSource para el consumo de cada uno de los Renderer disponibles.
  • LoadControl: La implementación de 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 por el momento, considera implementar una clase Extractor personalizada.
  • MediaSource: Implementar una clase MediaSource personalizada puede ser apropiado si deseas obtener muestras de contenido multimedia para enviar a los procesadores de forma personalizada o si deseas implementar un comportamiento de composición personalizado de MediaSource.
  • MediaSource.Factory: La implementación de 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 ascendente de ExoPlayer ya contiene varias implementaciones de DataSource para diferentes casos de uso. Es posible que quieras implementar tu propia clase DataSource para cargar datos de otra manera, por ejemplo, a través de un protocolo personalizado, con una pila HTTP personalizada o desde una caché persistente personalizada.

Cuando crees 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 pasar 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 la 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. El envío de 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.