Personalização

A interface Player está no centro da biblioteca do ExoPlayer. Um Player expõe a funcionalidade tradicional do player de mídia de alto nível, como a capacidade de armazenar mídia em buffer, reproduzir, pausar e procurar. A implementação padrão ExoPlayer foi projetada para fazer poucas suposições sobre (e, portanto, impor poucas restrições) sobre o tipo de mídia 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 que são injetados quando um player é criado ou quando novas fontes de mídia são transmitidas ao player. Os componentes comuns a todas as implementações de ExoPlayer são:

  • Instâncias de MediaSource que definem a mídia a ser reproduzida, carrega a mídia e a mídia carregada pode ser lida. Uma instância MediaSource é criada com base em um MediaItem por um MediaSource.Factory no player. Eles também podem ser transmitidos diretamente ao player usando a API de playlist baseada na fonte de mídia.
  • Uma instância MediaSource.Factory que converte um MediaItem em um MediaSource. O MediaSource.Factory é injetado quando o player é criado.
  • Instâncias de Renderer que renderizam componentes individuais da mídia. Eles são injetados quando o player é criado.
  • Um TrackSelector que seleciona faixas fornecidas pelo MediaSource a serem consumidas por cada Renderer disponível. Um TrackSelector é injetado quando o player é criado.
  • Um LoadControl que controla quando o MediaSource armazena mais mídia em buffer e quanta mídia é armazenada em buffer. Um LoadControl é injetado quando o player é criado.
  • Um LivePlaybackSpeedControl que controla a velocidade de reprodução durante reproduções ao vivo para permitir que o player fique próximo a um deslocamento ao vivo configurado. Uma LivePlaybackSpeedControl é injetada 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 o 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 a injeção de componentes.

Como configurar a pilha de rede

Temos uma página sobre como personalizar a pilha de rede usada pelo ExoPlayer (em inglês).

Armazenamento em cache dos dados carregados da rede

Consulte os guias sobre armazenamento em cache temporário e dinâmico e como fazer o download de mídia.

Como personalizar interações do servidor

Alguns apps podem querer interceptar solicitações e respostas HTTP. Você pode querer 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 um cabeçalho ao solicitar os segmentos de mídia.

O exemplo abaixo 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 origem HTTP.

Para uma abordagem mais granular, é possível injetar o comportamento just-in-time usando um ResolvingDataSource. O snippet de código a seguir mostra como injetar cabeçalhos de solicitação logo 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)));

Você também pode usar um ResolvingDataSource para realizar modificações no momento certo do URI, conforme mostrado no snippet abaixo:

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ção do tratamento de erros

A implementação de um LoadErrorHandlingPolicy personalizado permite que os apps personalizem a forma 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 jogador 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.

Como personalizar sinalizações do extrator

As flags do extrator podem ser usadas para personalizar como formatos individuais são extraídos da mídia progressiva. Eles podem ser definidos no DefaultExtractorsFactory fornecido ao DefaultMediaSourceFactory. O exemplo a seguir transmite uma sinalização que permite a busca baseada em índices para streams 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();

Ativar a busca constante da taxa de bits

Para streams de MP3, ADTS e AMR, é possível ativar a busca aproximada usando uma suposição de taxa de bits constante com sinalizações FLAG_ENABLE_CONSTANT_BITRATE_SEEKING. Essas sinalizações podem ser definidas para extratores individuais usando os métodos DefaultExtractorsFactory.setXyzExtractorFlags individuais, conforme descrito acima. Para ativar a busca constante da taxa de bits 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 por DefaultMediaSourceFactory, conforme descrito para personalizar as sinalizações do extrator acima.

Como ativar o enfileiramento do buffer assíncrono

A fila de buffer assíncrono é uma melhoria no pipeline de renderização do ExoPlayer, que opera instâncias de MediaCodec no modo assíncrono e usa linhas de execução extras para programar a decodificação e a renderização de dados. A ativação pode reduzir a queda de frames e underruns de áudio.

O enfileiramento do buffer assíncrono é ativado por padrão em dispositivos com o Android 12 (nível 31 da API) e versões mais recentes e pode ser ativado manualmente a partir do Android 6.0 (nível 23 da API). Ative o recurso para dispositivos específicos em que você observa queda de frames ou underruns de áudio, principalmente ao reproduzir conteúdo protegido por DRM ou com frame rate alta.

No caso mais simples, você precisa injetar um DefaultRenderersFactory no jogador 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 um AsynchronousMediaCodecAdapter.Factory para os construtores MediaCodecVideoRenderer e MediaCodecAudioRenderer.

Como interceptar chamadas de método com ForwardingPlayer

É possível personalizar parte do comportamento de uma instância de Player envolvendo-a em uma subclasse de ForwardingPlayer e modificando métodos para realizar um dos seguintes procedimentos:

  • Acesse os parâmetros antes de transmiti-los para o Player delegado.
  • Acesse o valor de retorno do Player delegado antes de retorná-lo.
  • Implemente o método novamente por completo.

Ao substituir métodos ForwardingPlayer, é importante garantir que a implementação permaneça autoconsistente e compatível com a interface Player, especialmente ao lidar com métodos que têm comportamento idêntico ou relacionado. Por exemplo:

  • Se você quiser substituir todas as operações "reproduzir", será necessário substituir ForwardingPlayer.play e ForwardingPlayer.setPlayWhenReady, porque o autor da chamada espera que o comportamento desses métodos seja idêntico quando playWhenReady = true.
  • Se você quiser mudar o incremento de avançar, será necessário substituir ForwardingPlayer.seekForward para realizar uma busca com o incremento personalizado e ForwardingPlayer.getSeekForwardIncrement para informar o incremento personalizado correto de volta ao autor da chamada.
  • Se você quiser controlar quais Player.Commands são anunciadas por uma instância de jogador, substitua Player.getAvailableCommands() e Player.isCommandAvailable(), além de detectar o callback Player.Listener.onAvailableCommandsChanged() para receber notificações sobre mudanças vindas do player.

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 uma personalização detalhada é necessária, também é possível injetar componentes personalizados em instâncias MediaSource individuais, 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 oferece implementações padrão dos componentes listados na parte de cima desta página para casos de uso comuns. Uma ExoPlayer pode usar esses componentes, mas também pode ser criada para usar implementações personalizadas se comportamentos não padrão forem necessários. Alguns casos de uso para implementações personalizadas são:

  • Renderer: implemente um Renderer personalizado para processar um tipo de mídia que não tem suporte das implementações padrão fornecidas pela biblioteca.
  • TrackSelector: implementar um TrackSelector personalizado permite que um desenvolvedor de apps mude a maneira como as faixas expostas por um MediaSource são selecionadas para consumo em cada um dos Renderers disponíveis.
  • LoadControl: a implementação de um LoadControl personalizado permite que um desenvolvedor de apps mude a política de armazenamento em buffer do player.
  • Extractor: caso você precise oferecer suporte a um formato de contêiner que não tem suporte atual da biblioteca, implemente uma classe Extractor personalizada.
  • MediaSource: a implementação de uma classe MediaSource personalizada pode ser adequada caso você queira receber amostras de mídia para alimentar os renderizadores de maneira personalizada ou implementar um comportamento de composição MediaSource personalizado.
  • MediaSource.Factory: a implementação de um MediaSource.Factory personalizado permite que um aplicativo personalize a maneira como um MediaSource é criado com base em um MediaItem.
  • DataSource: o pacote upstream do ExoPlayer já contém várias implementações de DataSource para diferentes casos de uso. É possível implementar sua própria classe DataSource para carregar dados de outra forma, como por meio de um protocolo personalizado, usando uma pilha HTTP personalizada ou de um cache permanente personalizado.

Ao criar componentes personalizados, recomendamos o seguinte:

  • Se um componente personalizado precisar relatar eventos ao app, recomendamos que você faça isso usando o mesmo modelo dos componentes existentes do ExoPlayer, por exemplo, usando classes EventDispatcher ou transmitindo um Handler com um listener para o construtor do componente.
  • Recomendamos que os componentes personalizados usem o mesmo modelo que os componentes existentes do ExoPlayer para permitir a reconfiguração pelo app durante a reprodução. Para fazer isso, os componentes personalizados precisam implementar PlayerMessage.Target e receber mudanças de configuração no método handleMessage. O código do aplicativo precisa transmitir as mudanças de configuração chamando o método createMessage do ExoPlayer, configurando a mensagem e enviando-a ao componente usando PlayerMessage.send. O envio de mensagens a serem entregues na linha de execução de reprodução garante que elas sejam executadas em ordem com qualquer outra operação que seja realizada no player.