En el centro 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 en búfer contenido multimedia, reproducir, pausar y buscar. La implementación predeterminada ExoPlayer
está diseñada para hacer pocas suposiciones (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 la renderización de contenido multimedia directamente, las implementaciones de ExoPlayer
delegan este trabajo a 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. UnMediaSource.Factory
dentro del reproductor crea una instancia deMediaSource
a partir de unMediaItem
. También se pueden pasar directamente al reproductor con la API de playlist basada en fuentes de medios. - Es una instancia de
MediaSource.Factory
que convierte unMediaItem
en unMediaSource
. El objetoMediaSource.Factory
se inyecta cuando se crea el reproductor. - Son instancias
Renderer
que renderizan componentes individuales del contenido multimedia. Estos se insertan cuando se crea el reproductor. - Un
TrackSelector
que selecciona los segmentos proporcionados por elMediaSource
para que los consuma cadaRenderer
disponible. Se inyecta unTrackSelector
cuando se crea el reproductor. - Un
LoadControl
que controla cuándo elMediaSource
almacena en búfer más contenido multimedia y cuánto contenido multimedia se almacena en búfer. Se inyecta unLoadControl
cuando se crea el reproductor. - Un
LivePlaybackSpeedControl
que controla la velocidad de reproducción durante las repeticiones en vivo para permitir que el reproductor se mantenga cerca de un desplazamiento en vivo configurado. Se inyecta unLivePlaybackSpeedControl
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 individualmente por implementaciones configuradas de forma personalizada.
Personalización del jugador
A continuación, se describen algunos ejemplos comunes de personalización del reproductor a través de la inserción de componentes.
Cómo configurar la pila de red
Tenemos una página sobre la personalización de la pila de red que usa ExoPlayer.
Almacenamiento en caché de los datos cargados desde la red
Consulta las guías sobre el almacenamiento en caché temporal sobre la marcha y la descarga de contenido multimedia.
Cómo personalizar las interacciones del servidor
Es posible que algunas apps deseen interceptar solicitudes y respuestas HTTP. Es posible que desees insertar encabezados de solicitud personalizados, leer los encabezados de respuesta del servidor, modificar los URI de las solicitudes, etcétera. Por ejemplo, tu app puede autenticarse insertando un token como encabezado cuando solicita los segmentos de medios.
En el siguiente ejemplo, se muestra cómo implementar estos comportamientos inyectando 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 se corrige para cada interacción con una fuente HTTP.
Para un enfoque más detallado, puedes insertar un comportamiento 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 del URI justo a tiempo, 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)));
Cómo personalizar el control 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, una app puede querer fallar rápido en lugar de reintentar muchas veces, o puede querer 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 una lógica de reintento 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 fallida para personalizar la lógica según el tipo de error o la solicitud fallida.
Cómo personalizar las marcas del extractor
Las marcas del extractor se pueden usar para personalizar la forma en que se extraen los formatos individuales de los medios progresivos. Se pueden configurar 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();
Cómo habilitar la búsqueda de velocidad de bits constante
En el caso de las transmisiones MP3, ADTS y AMR, puedes habilitar la búsqueda aproximada con una suposición de tasa de bits constante con 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 velocidad 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 ExtractorsFactory
a través de DefaultMediaSourceFactory
, como se describió anteriormente para personalizar las marcas del extractor.
Cómo habilitar la puesta en cola asíncrona de búferes
El encolamiento asíncrono 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 la pérdida de fotogramas y los subdesbordamientos de audio.
El encolamiento asíncrono de búferes está habilitado de forma predeterminada en dispositivos con Android 12 (nivel de API 31) y versiones posteriores, y se puede habilitar manualmente a partir de Android 6.0 (nivel de API 23). Considera habilitar la función para dispositivos específicos en los que observes fotogramas descartados o subejecuciones de audio, en especial cuando reproduzcas contenido protegido por DRM o con una alta velocidad de fotogramas.
En el caso más simple, debes inyectar 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
.
Cómo personalizar operaciones con ForwardingSimpleBasePlayer
Puedes personalizar parte del comportamiento de una instancia de Player
envolviéndola en 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 de, por ejemplo, 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
a ForwardingPlayer
, que es más propenso a errores debido a estas garantías de coherencia.
Por ejemplo, para agregar lógica personalizada cuando se inicia o 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
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 inhabilitar el comando SEEK_TO_NEXT
(y garantizar que Player.seekToNext
no realice ninguna operación):
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. }
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 de MediaSource
individuales, 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));
Cómo crear componentes personalizados
La biblioteca proporciona implementaciones predeterminadas de los componentes que se enumeran 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ándares. Estos son algunos casos de uso de las implementaciones personalizadas:
Renderer
: Es posible que desees implementar unRenderer
personalizado para controlar un tipo de medio que no admiten las implementaciones predeterminadas que proporciona la biblioteca.TrackSelector
: La implementación de unTrackSelector
personalizado permite que un desarrollador de apps cambie la forma en que cada uno de losRenderer
s disponibles selecciona las pistas expuestas por unMediaSource
para su consumo.LoadControl
: La implementación de unLoadControl
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 claseExtractor
personalizada.MediaSource
: Implementar una claseMediaSource
personalizada puede ser adecuado si deseas obtener muestras de medios para alimentar a los renderizadores de una manera personalizada o si deseas implementar un comportamiento de composiciónMediaSource
personalizado.MediaSource.Factory
: La implementación de unMediaSource.Factory
personalizado permite que una aplicación personalice la forma en que se crea unMediaSource
a partir de unMediaItem
.DataSource
: El paquete upstream de ExoPlayer ya contiene varias implementaciones deDataSource
para diferentes casos de uso. Es posible que desees implementar tu propia claseDataSource
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 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, usando clases
EventDispatcher
o pasando unHandler
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 reconfigure durante la reproducción. Para ello, los componentes personalizados deben implementar
PlayerMessage.Target
y recibir cambios de configuración en el métodohandleMessage
. El código de la aplicación debe pasar los cambios de configuración llamando al métodocreateMessage
de ExoPlayer, configurando el mensaje y enviándolo al componente conPlayerMessage.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.