Personalização

A interface Player é o núcleo da biblioteca ExoPlayer. Um Player expõe funcionalidades tradicionais de alto nível do player de mídia, como a capacidade de armazenar em buffer, reproduzir, pausar e buscar mídia. A implementação padrão ExoPlayer foi projetada para fazer poucas suposições sobre o tipo de mídia que está sendo reproduzida, como e onde ela é armazenada e como é renderizada, impondo poucas restrições. 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 origens de mídia são transmitidas para ele. Os componentes comuns a todas as implementações de ExoPlayer são:

  • Instâncias de 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ância de MediaSource é criada de um MediaItem por um MediaSource.Factory dentro do player. Eles também podem ser transmitidos diretamente ao player usando a API de playlist baseada em 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 inseridos quando o player é criado.
  • Um TrackSelector que seleciona as faixas fornecidas pelo MediaSource para 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 do vídeo durante as reproduções ao vivo para permitir que o player fique próximo de um deslocamento ao vivo configurado. Um LivePlaybackSpeedControl é 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 o trabalho para outros componentes injetados. Isso permite que muitos subcomponentes sejam substituídos individualmente por implementações configuradas de maneira personalizada.

Personalização do player

Alguns exemplos comuns de personalização do player com a injeção de componentes são descritos nas seções a seguir.

Como configurar a pilha de rede

Temos uma página sobre como personalizar a pilha de rede usada pelo ExoPlayer.

Armazenar em cache os dados carregados da rede

Consulte os guias sobre armazenamento em cache temporário em tempo real e download de mídia.

Como 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 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 anterior, o HttpDataSource injetado inclui o cabeçalho "Header: Value" em cada solicitação 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 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 realizar 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)));

Como personalizar o tratamento de erros

A implementação de um LoadErrorHandlingPolicy personalizado permite que os apps personalizem 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 jogador espera entre cada nova tentativa. O snippet a seguir mostra como implementar a lógica de espera 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();

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 as flags do extrator

As flags de extrator podem ser usadas para personalizar como os 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 flag que ativa a busca baseada em índice 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 de taxa de bits constante

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

Como ativar o enfileiramento de buffer assíncrono

A fila do buffer assíncrona é uma melhoria no pipeline de renderização do ExoPlayer, que opera instâncias de 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 os frames perdidos e as interrupções de áudio.

A fila de buffer assíncrona é ativada por padrão em dispositivos com o Android 12 (nível 31 da API) ou versões mais recentes e pode ser ativada manualmente a partir do Android 6.0 (nível 23 da API). Considere ativar o recurso para dispositivos específicos em que você observa frames descartados ou underruns de áudio, principalmente ao reproduzir conteúdo protegido por DRM ou com alta taxa de quadros.

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.

Como personalizar operações com ForwardingSimpleBasePlayer

É possível personalizar parte do comportamento de uma instância de Player ao envolvê-la em uma subclasse de ForwardingSimpleBasePlayer. Essa classe permite interceptar "operações" específicas, em vez de implementar diretamente os métodos Player. Isso garante um comportamento consistente, por exemplo, de play(), pause() e setPlayWhenReady(boolean). Ele também garante que todas as mudanças de estado sejam propagadas corretamente para as instâncias Player.Listener registradas. Para a maioria dos casos de uso de personalização, ForwardingSimpleBasePlayer deve ser preferível ao ForwardingPlayer, que é mais propenso 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

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

Ou para proibir o comando SEEK_TO_NEXT (e garantir que Player.seekToNext seja uma no-op):

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.
}

Personalização de MediaSource

Os exemplos anteriores 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 individuais de MediaSource, que podem ser transmitidas diretamente para o player. O exemplo a seguir 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 comportamentos não padrão forem necessários. Alguns casos de uso para implementações personalizadas são:

  • Renderer: talvez você queira implementar um Renderer 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 um TrackSelector personalizado permite que um desenvolvedor de apps mude a forma como as faixas expostas por um MediaSource são selecionadas para consumo por 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: se você precisar oferecer suporte a um formato de contêiner que não é compatível com a biblioteca, implemente uma classe Extractor personalizada.
  • MediaSource: implementar uma classe MediaSource personalizada pode ser adequado se você quiser receber amostras de mídia para alimentar renderizadores de uma maneira personalizada ou se quiser 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. Talvez você queira implementar sua própria classe DataSource para carregar dados de outra maneira, 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 ao app, recomendamos que você faça isso usando o mesmo modelo dos componentes atuais 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 dos componentes 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 a enviando ao componente usando PlayerMessage.send. O envio de mensagens para serem entregues na linha de execução de reprodução garante que elas sejam executadas em ordem com qualquer outra operação que esteja sendo realizada no player.