Dostosowywanie

Podstawą biblioteki ExoPlayer jest interfejs Player. Player udostępnia tradycyjne funkcje odtwarzacza multimediów wysokiego poziomu, takie jak buforowanie multimediów, odtwarzanie, wstrzymywanie i przewijanie. Domyślna implementacja ExoPlayer zakłada niewiele założeń (a tym samym nakłada niewiele ograniczeń) dotyczących typu odtwarzanych multimediów, sposobu i miejsca ich przechowywania oraz sposobu ich renderowania. Zamiast bezpośrednio implementować wczytywanie i renderowanie multimediów,ExoPlayer implementacje przekazują to zadanie komponentom, które są wstrzykiwane podczas tworzenia odtwarzacza lub gdy do odtwarzacza przekazywane są nowe źródła multimediów. Komponenty wspólne dla wszystkich implementacji ExoPlayer to:

  • MediaSource instancje, które określają multimedia do odtworzenia, wczytują multimedia i z których można odczytać wczytane multimedia. Wystąpienie MediaSource jest tworzone z MediaItem przez MediaSource.Factory w odtwarzaczu. Można je też przekazywać bezpośrednio do odtwarzacza za pomocą interfejsu API listy odtwarzania opartej na źródle multimediów.
  • MediaSource.Factory instancji, która przekształca MediaItemMediaSource. Wartość MediaSource.Factory jest wstawiana podczas tworzenia odtwarzacza.
  • Renderer instancje, które renderują poszczególne komponenty multimediów; Są one wstrzykiwane podczas tworzenia odtwarzacza.
  • TrackSelector, który wybiera ścieżki dostarczane przez MediaSource, aby były wykorzystywane przez każdy dostępny Renderer. Po utworzeniu odtwarzacza wstrzykiwany jest TrackSelector.
  • LoadControl, która określa, kiedy MediaSource buforuje więcej multimediów i ile multimediów jest buforowanych. Gdy odtwarzacz jest tworzony, wstawiany jest element LoadControl.
  • LivePlaybackSpeedControl, która kontroluje szybkość odtwarzania podczas odtwarzania na żywo, aby odtwarzacz mógł utrzymać skonfigurowane przesunięcie na żywo. Gdy odtwarzacz jest tworzony, wstrzykiwany jest element A LivePlaybackSpeedControl.

Koncepcja wstrzykiwania komponentów implementujących fragmenty funkcjonalności odtwarzacza jest obecna w całej bibliotece. Domyślne implementacje niektórych komponentów przekazują pracę do kolejnych wstrzykiwanych komponentów. Umożliwia to indywidualną wymianę wielu podzespołów na implementacje skonfigurowane w niestandardowy sposób.

Dostosowywanie gracza

Poniżej znajdziesz kilka typowych przykładów dostosowywania odtwarzacza przez wstrzykiwanie komponentów.

Konfigurowanie stosu sieciowego

Mamy stronę poświęconą dostosowywaniu stosu sieciowego używanego przez ExoPlayera.

Buforowanie danych wczytanych z sieci

Zapoznaj się z przewodnikami dotyczącymi tymczasowego buforowania w lociepobierania multimediów.

Dostosowywanie interakcji z serwerem

Niektóre aplikacje mogą przechwytywać żądania i odpowiedzi HTTP. Możesz wstawiać niestandardowe nagłówki żądań, odczytywać nagłówki odpowiedzi serwera, modyfikować adresy URI żądań itp. Na przykład aplikacja może uwierzytelniać się, wstawiając token jako nagłówek podczas żądania segmentów multimediów.

Ten przykład pokazuje, jak zaimplementować te zachowania, wstrzykując niestandardowy obiekt DataSource.Factory do obiektu 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();

We fragmencie kodu powyżej wstrzyknięty kod HttpDataSource zawiera nagłówek "Header: Value" w każdym żądaniu HTTP. To zachowanie jest stałe w przypadku każdej interakcji ze źródłem HTTP.

Aby uzyskać bardziej szczegółowe podejście, możesz wstrzyknąć zachowanie w odpowiednim momencie za pomocą elementu ResolvingDataSource. Poniższy fragment kodu pokazuje, jak wstawić nagłówki żądań tuż przed interakcją ze źródłem 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)));

Możesz też użyć znaku ResolvingDataSource, aby wprowadzać modyfikacje identyfikatora URI w odpowiednim momencie, jak pokazano w tym fragmencie kodu:

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

Dostosowywanie obsługi błędów

Wdrożenie niestandardowego LoadErrorHandlingPolicy umożliwia aplikacjom dostosowywanie sposobu, w jaki ExoPlayer reaguje na błędy ładowania. Na przykład aplikacja może chcieć szybko zakończyć działanie zamiast wielokrotnie ponawiać próbę lub dostosować logikę wycofywania, która kontroluje, jak długo odtwarzacz czeka między kolejnymi próbami. Poniższy fragment kodu pokazuje, jak wdrożyć niestandardową logikę ponawiania:

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

Argument LoadErrorInfo zawiera więcej informacji o nieudanym wczytaniu, aby dostosować logikę na podstawie typu błędu lub nieudanego żądania.

Dostosowywanie flag ekstraktora

Flagi ekstraktora można wykorzystać do dostosowania sposobu wyodrębniania poszczególnych formatów z mediów progresywnych. Można je ustawić na DefaultExtractorsFactory, który jest udostępniany DefaultMediaSourceFactory. W przykładzie poniżej przekazywana jest flaga, która włącza wyszukiwanie w strumieniach MP3 na podstawie indeksu.

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

Włączanie wyszukiwania przy stałej szybkości transmisji

W przypadku strumieni MP3, ADTS i AMR możesz włączyć przybliżone wyszukiwanie, zakładając stałą szybkość transmisji bitów za pomocą flagi FLAG_ENABLE_CONSTANT_BITRATE_SEEKING. Te flagi można ustawiać dla poszczególnych ekstraktorów za pomocą metod DefaultExtractorsFactory.setXyzExtractorFlags opisanych powyżej. Aby włączyć wyszukiwanie ze stałą szybkością transmisji we wszystkich ekstraktorach, które je obsługują, użyj parametru DefaultExtractorsFactory.setConstantBitrateSeekingEnabled.

Kotlin

val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)

Java

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

ExtractorsFactory można następnie wstawić za pomocą DefaultMediaSourceFactory, jak opisano powyżej w przypadku dostosowywania flag ekstraktora.

Włączanie asynchronicznego kolejkowania buforów

Asynchroniczne kolejkowanie buforów to ulepszenie w potoku renderowania ExoPlayera, które działa w MediaCodec instancjach w trybie asynchronicznym i wykorzystuje dodatkowe wątki do planowania dekodowania i renderowania danych. Włączenie tej opcji może zmniejszyć liczbę pominiętych klatek i niedoborów dźwięku.

Asynchroniczne kolejkowanie buforów jest domyślnie włączone na urządzeniach z Androidem 12 (poziom interfejsu API 31) i nowszym. Można je włączyć ręcznie od Androida 6.0 (poziom interfejsu API 23). Rozważ włączenie tej funkcji na urządzeniach, na których występują utracone klatki lub niedobory dźwięku, zwłaszcza podczas odtwarzania treści chronionych DRM lub treści o wysokiej liczbie klatek na sekundę.

W najprostszym przypadku musisz wstawić DefaultRenderersFactory do odtwarzacza w ten sposób:

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

Jeśli tworzysz instancje modułów renderujących bezpośrednio, przekaż wartość new DefaultMediaCodecAdapter.Factory(context).forceEnableAsynchronous() do konstruktorów MediaCodecVideoRendererMediaCodecAudioRenderer.

Dostosowywanie operacji za pomocą ForwardingSimpleBasePlayer

Niektóre zachowania instancji Player możesz dostosować, umieszczając ją w podklasie ForwardingSimpleBasePlayer. Ta klasa umożliwia przechwytywanie określonych „operacji”, zamiast bezpośredniego implementowania metod Player. Zapewnia to spójne działanie np. play(), pause()setPlayWhenReady(boolean). Zapewnia też prawidłowe przekazywanie wszystkich zmian stanu do zarejestrowanych instancji Player.Listener. W większości przypadków dostosowywania ForwardingSimpleBasePlayer jest preferowane od bardziej podatnego na błędy ForwardingPlayer ze względu na gwarancje spójności.

Aby na przykład dodać niestandardową logikę, gdy odtwarzanie jest rozpoczynane lub zatrzymywane:

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

Aby zablokować polecenie SEEK_TO_NEXT (i upewnić się, że Player.seekToNext nie wykonuje żadnej operacji):

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

Dostosowywanie MediaSource

Powyższe przykłady wstawiają dostosowane komponenty do użycia podczas odtwarzania wszystkich obiektów przekazywanych do odtwarzacza.MediaItem W przypadku, gdy wymagane jest precyzyjne dostosowanie, można też wstawiać niestandardowe komponenty do poszczególnych instancji MediaSource, które można przekazywać bezpośrednio do odtwarzacza. Poniższy przykład pokazuje, jak dostosować element ProgressiveMediaSource, aby używać niestandardowych elementów DataSource.Factory, ExtractorsFactoryLoadErrorHandlingPolicy:

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

Tworzenie komponentów niestandardowych

Biblioteka udostępnia domyślne implementacje komponentów wymienionych u góry tej strony w przypadku typowych zastosowań. ExoPlayer może używać tych komponentów, ale może też być zbudowany tak, aby korzystać z niestandardowych implementacji, jeśli wymagane są niestandardowe zachowania. Przykłady zastosowań niestandardowych implementacji:

  • Renderer – możesz wdrożyć niestandardowy element Renderer, aby obsługiwać typ multimediów, który nie jest obsługiwany przez domyślne implementacje udostępniane przez bibliotekę.
  • TrackSelector – wdrożenie niestandardowego TrackSelector umożliwia deweloperowi aplikacji zmianę sposobu wybierania ścieżek udostępnianych przez MediaSource do odtwarzania przez poszczególne dostępne Renderer.
  • LoadControl – wdrożenie niestandardowego LoadControl umożliwia deweloperowi aplikacji zmianę zasad buforowania odtwarzacza.
  • Extractor – Jeśli musisz obsługiwać format kontenera, który nie jest obecnie obsługiwany przez bibliotekę, rozważ wdrożenie niestandardowej klasy Extractor.
  • MediaSource – Zaimplementowanie niestandardowej klasy MediaSource może być odpowiednie, jeśli chcesz uzyskiwać próbki multimediów do przekazywania do modułów renderujących w niestandardowy sposób lub jeśli chcesz zaimplementować niestandardowe zachowanie związane z komponowaniem MediaSource.
  • MediaSource.Factory – wdrożenie niestandardowego elementu MediaSource.Factory umożliwia aplikacji dostosowanie sposobu tworzenia elementu MediaSource z elementu MediaItem.
  • DataSource – pakiet nadrzędny ExoPlayera zawiera już kilka implementacji DataSource dla różnych przypadków użycia. Możesz zaimplementować własną klasę DataSource, aby wczytywać dane w inny sposób, np. za pomocą protokołu niestandardowego, stosu HTTP lub niestandardowej pamięci podręcznej.

Podczas tworzenia komponentów niestandardowych zalecamy wykonanie tych czynności:

  • Jeśli komponent niestandardowy musi zgłaszać zdarzenia do aplikacji, zalecamy używanie tego samego modelu co w przypadku istniejących komponentów ExoPlayera, np. klas EventDispatcher lub przekazywanie Handler wraz z odbiornikiem do konstruktora komponentu.
  • Zalecamy, aby komponenty niestandardowe korzystały z tego samego modelu co istniejące komponenty ExoPlayera, co umożliwi aplikacji zmianę konfiguracji podczas odtwarzania. Aby to zrobić, komponenty niestandardowe powinny implementować interfejs PlayerMessage.Target i otrzymywać zmiany konfiguracji w metodzie handleMessage. Kod aplikacji powinien przekazywać zmiany konfiguracji, wywołując metodę createMessage ExoPlayera, konfigurując wiadomość i wysyłając ją do komponentu za pomocą PlayerMessage.send. Wysyłanie wiadomości do wątku odtwarzania zapewnia, że są one wykonywane w kolejności z innymi operacjami wykonywanymi na odtwarzaczu.