Dostosowywanie

Podstawą biblioteki ExoPlayer jest interfejs Player. Player udostępnia tradycyjne funkcje odtwarzacza multimedialnego wysokiego poziomu, takie jak buforowanie multimediów, odtwarzanie, wstrzymywanie i wyszukiwanie. Domyślna implementacja ExoPlayer została zaprojektowana tak, aby nie zakładać zbyt wiele o typie odtwarzanych multimediów, sposobie i miejscu ich przechowywania oraz sposobie ich renderowania (a tym samym nie nakładać zbyt wielu ograniczeń). Zamiast implementować wczytywanie i renderowanie multimediów bezpośrednio, implementacje ExoPlayer przekazują to zadanie komponentom, które są wstrzykiwane podczas tworzenia odtwarzacza lub przekazywania do niego nowych źródeł multimediów. Komponenty wspólne dla wszystkich implementacji ExoPlayer to:

  • Instancje MediaSource, które określają multimedia do odtworzenia, wczytują je i z których można odczytać wczytane multimedia. Instancja MediaSource jest tworzona z MediaItem przez MediaSource.Factory w odtwarzaczu. Można je też przekazywać bezpośrednio do odtwarzacza za pomocą interfejsu API playlisty opartej na źródle multimediów.
  • Instancja MediaSource.Factory, która konwertuje MediaItem na MediaSource. Obiekt MediaSource.Factory jest wstrzykiwany podczas tworzenia odtwarzacza.
  • Renderer instancji, które renderują poszczególne komponenty multimediów. Są one wstrzykiwane podczas tworzenia odtwarzacza.
  • TrackSelector, który wybiera ścieżki dostarczane przez MediaSource do wykorzystania przez każdy dostępny Renderer. Podczas tworzenia odtwarzacza jest wstrzykiwany element TrackSelector.
  • LoadControl, który określa, kiedy MediaSource buforuje więcej multimediów i ile multimediów jest buforowanych. Podczas tworzenia odtwarzacza wstrzykiwany jest obiekt LoadControl.
  • LivePlaybackSpeedControl, który kontroluje szybkość odtwarzania podczas odtwarzania na żywo, aby odtwarzacz mógł pozostać blisko skonfigurowanego przesunięcia na żywo. Podczas tworzenia odtwarzacza wstrzykiwany jest element LivePlaybackSpeedControl.

Koncepcja wstrzykiwania komponentów, które implementują elementy funkcji odtwarzacza, jest obecna w całej bibliotece. Domyślne implementacje niektórych komponentów przekazują pracę do kolejnych wstrzykniętych komponentów. Dzięki temu wiele podkomponentów można zastąpić indywidualnie implementacjami skonfigurowanymi w niestandardowy sposób.

Dostosowywanie odtwarzacza

W sekcjach poniżej opisujemy kilka typowych przykładów dostosowywania odtwarzacza przez wstrzykiwanie komponentów.

Konfigurowanie stosu sieciowego

Mamy stronę o dostosowywaniu stosu sieciowego używanego przez ExoPlayer.

Zapisywanie w pamięci podręcznej danych wczytanych z sieci

Zapoznaj się z przewodnikami dotyczącymi tymczasowego buforowania na bieżąco i pobierania multimediów.

Dostosowywanie interakcji z serwerem

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

Poniższy przykład pokazuje, jak wdrożyć te zachowania przez wstrzyknięcie niestandardowego elementu DataSource.Factory do 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();

W poprzednim fragmencie kodu wstrzyknięty obiekt 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 „dokładnie na czas” za pomocą ResolvingDataSource. Poniższy fragment kodu pokazuje, jak wstrzyknąć nagłówki żądania 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ć ResolvingDataSource, aby wprowadzić modyfikacje identyfikatora URI w czasie rzeczywistym, 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

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

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

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

Dostosowywanie flag ekstraktora

Flagi ekstraktora mogą służyć do dostosowywania sposobu wyodrębniania poszczególnych formatów z multimediów progresywnych. Można je ustawić w obiekcie DefaultExtractorsFactory, który jest przekazywany do metody DefaultMediaSourceFactory. W poniższym przykładzie przekazywana jest flaga, która umożliwia wyszukiwanie na podstawie indeksu w przypadku strumieni 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();

Włączanie przewijania przy stałej szybkości transmisji bitów

W przypadku strumieni MP3, ADTS i AMR możesz włączyć przybliżone przewijanie, używając założenia o stałej szybkości transmisji bitów z flagami FLAG_ENABLE_CONSTANT_BITRATE_SEEKING. Te flagi można ustawić dla poszczególnych ekstraktorów za pomocą metod DefaultExtractorsFactory.setXyzExtractorFlags, jak opisano wcześniej. Aby włączyć wyszukiwanie stałej szybkości transmisji bitów we wszystkich ekstraktorach, które je obsługują, użyj DefaultExtractorsFactory.setConstantBitrateSeekingEnabled.

Kotlin

val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)

Java

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

Następnie można wstrzyknąć ExtractorsFactory za pomocą DefaultMediaSourceFactory, jak opisano wcześniej w sekcji dotyczącej dostosowywania flag ekstraktora.

Włączanie asynchronicznego kolejkowania bufora

Asynchroniczne kolejkowanie bufora to ulepszenie w potoku renderowania ExoPlayera, które obsługuje instancje MediaCodec w trybie asynchronicznym i używa dodatkowych wątków do planowania dekodowania i renderowania danych. Włączenie tej opcji może zmniejszyć liczbę pominiętych klatek i niedomiarów dźwięku.

Asynchroniczne kolejkowanie bufora jest domyślnie włączone na urządzeniach z Androidem 12 (poziom interfejsu API 31) lub nowszym. Można je też włączyć ręcznie na urządzeniach z Androidem 6.0 (poziom interfejsu API 23) lub nowszym. Rozważ włączenie tej funkcji na konkretnych urządzeniach, na których zauważasz gubienie klatek lub niedobór danych audio, zwłaszcza podczas odtwarzania treści chronionych przez DRM lub treści o wysokiej liczbie klatek.

W najprostszym przypadku musisz wstrzyknąć 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ż new DefaultMediaCodecAdapter.Factory(context).forceEnableAsynchronous() do konstruktorów MediaCodecVideoRenderer i MediaCodecAudioRenderer.

Dostosowywanie operacji za pomocą ForwardingSimpleBasePlayer

Możesz dostosować niektóre zachowania instancji Player, opakowując ją w podklasę ForwardingSimpleBasePlayer. Ta klasa umożliwia przechwytywanie określonych „operacji” bez konieczności bezpośredniego implementowania metod Player. Zapewnia to spójne działanie np. play(), pause() i setPlayWhenReady(boolean). Zapewnia też prawidłowe przekazywanie wszystkich zmian stanu do zarejestrowanych instancji Player.Listener. W większości przypadków dostosowywania ze względu na gwarancję spójności lepiej jest używać ForwardingSimpleBasePlayer niż bardziej podatnego na błędy ForwardingPlayer.

Możesz 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

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

Aby nie zezwalać na 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

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

Dostosowywanie MediaSource

W powyższych przykładach wstrzykiwane są niestandardowe komponenty do użycia podczas odtwarzania wszystkich obiektów MediaItem przekazywanych do odtwarzacza. Jeśli wymagane jest szczegółowe dostosowanie, można też wstrzyknąć dostosowane komponenty do poszczególnych instancji MediaSource, które można przekazać bezpośrednio do odtwarzacza. Poniższy przykład pokazuje, jak dostosować ProgressiveMediaSource, aby używać niestandardowych DataSource.Factory, ExtractorsFactory i LoadErrorHandlingPolicy:

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 używać niestandardowych implementacji, jeśli wymagane są niestandardowe zachowania. Oto kilka przypadków użycia niestandardowych implementacji:

  • Renderer – możesz zaimplementować niestandardowy Renderer, aby obsługiwać typ multimediów, który nie jest obsługiwany przez domyślne implementacje udostępniane przez bibliotekę.
  • TrackSelector – implementacja niestandardowego elementu TrackSelector umożliwia deweloperowi aplikacji zmianę sposobu, w jaki ścieżki udostępniane przez element MediaSource są wybierane do wykorzystania przez poszczególne dostępne elementy Renderer.
  • LoadControl – wdrożenie niestandardowego LoadControl pozwala deweloperowi aplikacji zmienić zasadę buforowania odtwarzacza.
  • Extractor – jeśli chcesz obsługiwać format kontenera, który nie jest obecnie obsługiwany przez bibliotekę, rozważ zaimplementowanie niestandardowej klasy Extractor.
  • MediaSource – wdrożenie niestandardowej klasy MediaSource może być odpowiednie, jeśli chcesz uzyskać próbki multimediów do przekazania do modułów renderujących w niestandardowy sposób lub jeśli chcesz wdrożyć niestandardowe zachowanie kompozycji MediaSource.
  • MediaSource.Factory – implementacja niestandardowego interfejsu MediaSource.Factory umożliwia aplikacji dostosowanie sposobu tworzenia obiektu MediaSource z obiektu MediaItem.
  • DataSource – pakiet nadrzędny ExoPlayera zawiera już wiele 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ą niestandardowego protokołu, niestandardowego stosu HTTP lub niestandardowej trwałej pamięci podręcznej.

Podczas tworzenia komponentów niestandardowych zalecamy:

  • Jeśli komponent niestandardowy musi zgłaszać zdarzenia do aplikacji, zalecamy użycie tego samego modelu co w przypadku istniejących komponentów ExoPlayer, na przykład klas EventDispatcher lub przekazania Handler wraz z detektorem do konstruktora komponentu.
  • Zalecamy, aby komponenty niestandardowe używały tego samego modelu co istniejące komponenty ExoPlayer, aby umożliwić ponowną konfigurację przez aplikację podczas odtwarzania. Aby to zrobić, komponenty niestandardowe powinny implementować interfejs PlayerMessage.Target i odbierać 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ą metody PlayerMessage.send. Wysyłanie wiadomości do dostarczenia w wątku odtwarzania gwarantuje, że będą one wykonywane w kolejności z innymi operacjami wykonywanymi w odtwarzaczu.