No centro da biblioteca ExoPlayer está a interface Player
. Um Player
expõe a funcionalidade tradicional de player de mídia de alto nível, como a capacidade de
armazenar mídia em buffer, reproduzir, pausar e buscar. A implementação padrão ExoPlayer
foi
criada para fazer poucas suposições sobre (e, portanto, impor poucas restrições a)
o tipo de mídia que está sendo reproduzida, como e onde ela é armazenada e como ela é
renderizada. Em vez de implementar o carregamento e a renderização de mídia diretamente,
as implementações de ExoPlayer
delegam esse trabalho a componentes injetados
quando um player é criado ou quando novas fontes de mídia são transmitidas a ele.
Os componentes comuns a todas as implementações de ExoPlayer
são:
- Instâncias
MediaSource
que definem a mídia a ser reproduzida, carregam a mídia e de onde a mídia carregada pode ser lida. Uma instânciaMediaSource
é criada de umMediaItem
por umMediaSource.Factory
dentro do player. Eles também podem ser transmitidos diretamente ao player usando a API de lista de reprodução baseada em fonte de mídia. - Uma instância de
MediaSource.Factory
que converte umMediaItem
em umMediaSource
. OMediaSource.Factory
é injetado quando o player é criado. - Instâncias
Renderer
que renderizam componentes individuais da mídia. Eles são injetados quando o player é criado. - Um
TrackSelector
que seleciona faixas fornecidas peloMediaSource
para serem consumidas por cadaRenderer
disponível. UmTrackSelector
é injetado quando o player é criado. - Um
LoadControl
que controla quando oMediaSource
armazena mais mídia em buffer e a quantidade de mídia armazenada em buffer. UmLoadControl
é injetado quando o player é criado. - Um
LivePlaybackSpeedControl
que controla a velocidade de reprodução durante as reproduções ao vivo para permitir que o player fique próximo a um deslocamento ao vivo configurado. UmLivePlaybackSpeedControl
é injetado quando o player é criado.
O conceito de injetar componentes que implementam partes da funcionalidade do player está presente em toda a biblioteca. As implementações padrão de alguns componentes delegam trabalho a outros componentes injetados. Isso permite que muitos subcomponentes sejam substituídos individualmente por implementações configuradas de maneira personalizada.
Personalização do player
Confira abaixo alguns exemplos comuns de personalização do player com injeção de componentes.
Como configurar a pilha de rede
Temos uma página sobre como personalizar a pilha de rede usada pelo ExoPlayer.
Armazenamento em cache de dados carregados da rede
Consulte os guias para armazenamento temporário em cache instantâneo e download de mídia.
Personalizar interações com o servidor
Alguns apps podem querer interceptar solicitações e respostas HTTP. Talvez você queira injetar cabeçalhos de solicitação personalizados, ler os cabeçalhos de resposta do servidor, modificar os URIs das solicitações etc. Por exemplo, seu app pode se autenticar injetando um token como cabeçalho ao solicitar os segmentos de mídia.
O exemplo a seguir demonstra como implementar esses comportamentos injetando um DataSource.Factory
personalizado no 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();
No snippet de código acima, o HttpDataSource
injetado inclui o cabeçalho
"Header: Value"
em todas as solicitações HTTP. Esse comportamento é corrigido para cada interação com uma fonte HTTP.
Para uma abordagem mais granular, injete o comportamento just-in-time usando um
ResolvingDataSource
. O snippet de código a seguir mostra como injetar
cabeçalhos de solicitação pouco antes de interagir com uma fonte 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)));
Também é possível usar um ResolvingDataSource
para fazer
modificações just-in-time do URI, conforme mostrado no snippet a seguir:
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)));
Personalizar o tratamento de erros
Ao implementar um LoadErrorHandlingPolicy
personalizado, os apps podem personalizar a
maneira como o ExoPlayer reage a erros de carregamento. Por exemplo, um app pode querer falhar rapidamente
em vez de tentar várias vezes ou personalizar a lógica de espera que
controla quanto tempo o player espera entre cada nova tentativa. O snippet a seguir
mostra como implementar uma lógica de espera 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();
O argumento LoadErrorInfo
contém mais informações sobre o carregamento com falha para personalizar a lógica com base no tipo de erro ou na solicitação com falha.
Personalizar flags do extrator
As flags do extrator podem ser usadas para personalizar a extração de formatos individuais
de mídia progressiva. Eles podem ser definidos no DefaultExtractorsFactory
fornecido ao DefaultMediaSourceFactory
. O exemplo a seguir transmite uma flag
que ativa a busca baseada em índice para streams 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();
Como ativar a busca de taxa de bits constante
Para streams MP3, ADTS e AMR, é possível ativar a busca aproximada usando uma
taxa de bits constante com flags FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
.
Essas flags podem ser definidas para extratores individuais usando os métodos DefaultExtractorsFactory.setXyzExtractorFlags
individuais, conforme descrito acima. Para
ativar a busca de taxa de bits constante para todos os extratores compatíveis, use
DefaultExtractorsFactory.setConstantBitrateSeekingEnabled
.
Kotlin
val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)
Java
DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
O ExtractorsFactory
pode ser injetado via DefaultMediaSourceFactory
, conforme
descrito para personalizar flags de extrator acima.
Como ativar o enfileiramento assíncrono de buffers
O enfileiramento assíncrono de buffers é uma melhoria no pipeline de renderização do ExoPlayer, que opera instâncias MediaCodec
no modo assíncrono e usa outras linhas de execução para programar a decodificação e a renderização de dados. Ativar essa opção
pode reduzir a perda de frames e o subfluxo de áudio.
O enfileiramento assíncrono de buffers é ativado por padrão em dispositivos com Android 12 (nível 31 da API) e versões mais recentes. Ele pode ser ativado manualmente a partir do Android 6.0 (nível 23 da API). Considere ativar o recurso em dispositivos específicos em que você observa quedas de frames ou subexecuções de áudio, principalmente ao reproduzir conteúdo protegido por DRM ou com alta taxa de frames.
No caso mais simples, é necessário injetar um DefaultRenderersFactory
no
player da seguinte maneira:
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();
Se você estiver instanciando renderizadores diretamente, transmita
new DefaultMediaCodecAdapter.Factory(context).forceEnableAsynchronous()
para os construtores
MediaCodecVideoRenderer
e MediaCodecAudioRenderer
.
Personalização de operações com ForwardingSimpleBasePlayer
É possível personalizar parte do comportamento de uma instância Player
ao encapsulá-la em
uma subclasse de ForwardingSimpleBasePlayer
. Essa classe permite interceptar
operações específicas, em vez de precisar implementar diretamente métodos
Player
. Isso garante um comportamento consistente de, por exemplo, play()
, pause()
e setPlayWhenReady(boolean)
. Ele também garante que todas as mudanças de estado sejam propagadas corretamente para instâncias Player.Listener
registradas. Na maioria dos casos de uso de personalização, ForwardingSimpleBasePlayer
é preferível a ForwardingPlayer
, que é mais propensa a erros, devido a essas garantias de consistência.
Por exemplo, para adicionar uma lógica personalizada quando a reprodução é iniciada ou interrompida:
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 para não permitir o comando SEEK_TO_NEXT
(e garantir que Player.seekToNext
seja uma
operação nula):
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. }
Personalização do MediaSource
Os exemplos acima injetam componentes personalizados para uso durante a reprodução de todos os objetos
MediaItem
transmitidos ao player. Quando é necessária uma personalização refinada, também é possível injetar componentes personalizados em instâncias individuais de MediaSource
, que podem ser transmitidas diretamente ao player. O exemplo
abaixo mostra como personalizar um ProgressiveMediaSource
para usar um
DataSource.Factory
, ExtractorsFactory
e 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));
Como criar componentes personalizados
A biblioteca fornece implementações padrão dos componentes listados na parte de cima
desta página para casos de uso comuns. Um ExoPlayer
pode usar esses componentes, mas
também pode ser criado para usar implementações personalizadas se forem necessários
comportamentos não padrão. Alguns casos de uso para implementações personalizadas são:
Renderer
: talvez você queira implementar umRenderer
personalizado para processar um tipo de mídia não compatível com as implementações padrão fornecidas pela biblioteca.TrackSelector
: a implementação de umTrackSelector
personalizado permite que um desenvolvedor de apps mude a forma como as faixas expostas por umMediaSource
são selecionadas para consumo por cada um dosRenderer
s disponíveis.LoadControl
: a implementação de umLoadControl
personalizado permite que um desenvolvedor de apps mude a política de buffer do player.Extractor
: se você precisar oferecer suporte a um formato de contêiner que não é compatível com a biblioteca, considere implementar uma classeExtractor
personalizada.MediaSource
: implementar uma classeMediaSource
personalizada pode ser adequado se você quiser receber amostras de mídia para alimentar renderizadores de maneira personalizada ou se quiser implementar um comportamento de composiçãoMediaSource
personalizado.MediaSource.Factory
: a implementação de umMediaSource.Factory
personalizado permite que um aplicativo personalize a maneira como umMediaSource
é criado de umMediaItem
.DataSource
: o pacote upstream do ExoPlayer já contém várias implementações deDataSource
para diferentes casos de uso. Talvez você queira implementar sua própria classeDataSource
para carregar dados de outra forma, como por um protocolo personalizado, usando uma pilha HTTP personalizada ou de um cache persistente personalizado.
Ao criar componentes personalizados, recomendamos o seguinte:
- Se um componente personalizado precisar informar eventos de volta ao app, recomendamos
que você faça isso usando o mesmo modelo dos componentes do ExoPlayer, por
exemplo, usando classes
EventDispatcher
ou transmitindo umHandler
junto com um listener ao construtor do componente. - Recomendamos que os componentes personalizados usem o mesmo modelo dos componentes
do ExoPlayer para permitir a reconfiguração pelo app durante a reprodução. Para isso, os componentes personalizados precisam implementar
PlayerMessage.Target
e receber mudanças de configuração no métodohandleMessage
. O código do aplicativo precisa transmitir mudanças de configuração chamando o métodocreateMessage
do ExoPlayer, configurando a mensagem e enviando-a ao componente usandoPlayerMessage.send
. O envio de mensagens para serem entregues na linha de execução da reprodução garante que elas sejam executadas em ordem com qualquer outra operação que esteja sendo realizada no player.