Dostosowywanie

Sercem biblioteki ExoPlayer jest interfejs Player. Player eksponuje funkcje tradycyjnego odtwarzacza multimediów wysokiego poziomu, takie jak możliwość buforowania, odtwarzania, wstrzymywania i przeszukiwania. Domyślna implementacja ExoPlayer wprowadza niewielką wartość w odniesieniu do typu odtwarzanych multimediów, sposobu i miejsca ich przechowywania oraz sposobu ich renderowania (dlatego wprowadza niewielkie ograniczenia). Zamiast bezpośrednio wczytywać i renderować multimedia, implementacje ExoPlayer przekazują te zadania do komponentów wstrzykiwanych podczas tworzenia odtwarzacza lub przekazywania do niego nowych źródeł multimediów. Komponenty wspólne dla wszystkich implementacji ExoPlayer to:

  • MediaSource instancji, które określają multimedia do odtwarzania oraz wczytują multimedia i z których można je odczytywać. 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 opartego na źródle multimediów.
  • Instancje MediaSource.Factory, które konwertują dane z MediaItem na MediaSource. Podczas tworzenia odtwarzacza jest wstrzykiwany MediaSource.Factory.
  • Renderer do renderowania poszczególnych komponentów multimediów. Są one wstrzykiwane podczas tworzenia odtwarzacza.
  • Zasób TrackSelector, który wybiera ścieżki udostępnione przez obiekt MediaSource do wykorzystania przez każdy dostępny obiekt Renderer. Podczas tworzenia odtwarzacza jest wstrzykiwany TrackSelector.
  • Element LoadControl, który określa, kiedy MediaSource będzie buforować więcej multimediów i ile multimediów ma być buforowane. Podczas tworzenia odtwarzacza jest wstrzykiwany LoadControl.
  • Element LivePlaybackSpeedControl, który steruje szybkością odtwarzania podczas odtwarzania na żywo, aby odtwarzacz pozostawał w pobliżu skonfigurowanego przesunięcia transmisji na żywo. Podczas tworzenia odtwarzacza jest dodawany LivePlaybackSpeedControl.

Koncepcja wstrzykiwania komponentów implementujących poszczególne funkcje odtwarzacza jest obecna w całej bibliotece. Domyślne implementacje niektórych komponentów delegują pracę do dalszych wstrzykiwanych komponentów. Dzięki temu wiele komponentów podrzędnych można zastępować pojedynczo implementacjami skonfigurowanymi w niestandardowy sposób.

Personalizacja odtwarzacza

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

Konfigurowanie stosu sieciowego

Zapoznaj się ze stroną poświęconą dostosowywaniu stosu sieciowego używanego przez ExoPlayer.

Buforuję dane wczytane z sieci

Zapoznaj się z przewodnikami na temat tymczasowego buforowania w czasie rzeczywistym i pobierania multimediów.

Dostosowywanie interakcji z serwerem

Niektóre aplikacje mogą chcieć przechwytywać żądania i odpowiedzi HTTP. Może być konieczne wstawianie niestandardowych nagłówków żądań, odczytywanie nagłówków odpowiedzi serwera, modyfikowanie identyfikatorów URI żądań itp. Aplikacja może na przykład uwierzytelnić się, wstawiając token jako nagłówek przy wysyłaniu żądania segmentów multimediów.

Ten przykład pokazuje, jak wdrożyć te zachowania przez wstrzyknięcie niestandardowego elementu DataSource.Factory w interfejsie 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 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 informacje, możesz wstrzyknąć zdarzenie „w odpowiednim czasie” za pomocą metody ResolvingDataSource. Ten 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ć polecenia ResolvingDataSource, aby na bieżąco modyfikować identyfikator URI, jak pokazano w tym fragmencie:

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 elementu LoadErrorHandlingPolicy pozwala aplikacjom dostosowywać sposób, w jaki ExoPlayer reaguje na błędy wczytywania. Na przykład aplikacja może chcieć szybko ponosić błędy, zamiast ponawiać wielokrotne próby, albo zdecydować o dostosowaniu mechanizmu wycofywania, który określa czas oczekiwania między kolejnymi próbami. Ten fragment kodu pokazuje, jak wdrożyć niestandardową logikę wycofywania:

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 modułu wyodrębniania

Flagi wyodrębniania można używać do dostosowywania sposobu wyodrębniania poszczególnych formatów z multimediów progresywnych. Można je ustawić w elemencie DefaultExtractorsFactory udostępnianym przez interfejs DefaultMediaSourceFactory. Poniższy przykład przekazuje flagę, 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();

Umożliwianie wyszukiwania ze stałą szybkością transmisji

W przypadku strumieni MP3, ADTS i AMR możesz włączyć przybliżone przewijanie, korzystając z 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 modułów wyodrębniania za pomocą poszczególnych metod DefaultExtractorsFactory.setXyzExtractorFlags, jak opisano powyżej. Aby włączyć wyszukiwanie ze stałą szybkością transmisji bitów we wszystkich modułach wyodrębniania, które ją obsługują, użyj DefaultExtractorsFactory.setConstantBitrateSeekingEnabled.

Kotlin

val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)

Java

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

Następnie można wstrzykiwać ExtractorsFactory za pomocą DefaultMediaSourceFactory w sposób opisany powyżej w celu dostosowania flag modułu wyodrębniania.

Włączanie asynchronicznego kolejkowania bufora

Asynchroniczne kolejkowanie bufora to udoskonalenie potoku renderowania ExoPlayera, które obsługuje instancje MediaCodec w trybie asynchronicznym oraz wykorzystuje dodatkowe wątki do planowania dekodowania i renderowania danych. Włączenie go może zmniejszyć liczbę pomijanych klatek i zaniżenia w dźwięku.

Asynchroniczne kolejki buforowania są domyślnie włączone na urządzeniach z Androidem 12 (poziom interfejsu API 31) i nowszym. Można je włączyć ręcznie na urządzeniach z Androidem 6.0 (poziom API 23). Rozważ włączenie tej funkcji na określonych urządzeniach, na których zauważysz pominięte klatki lub zacinanie się dźwięku, zwłaszcza w przypadku treści z zabezpieczeniami DRM lub dużą liczbą klatek.

W najprostszym przypadku musisz wstawić do odtwarzacza element DefaultRenderersFactory 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 bezpośrednio instancje mechanizmu renderowania, przekaż instruktorowi AsynchronousMediaCodecAdapter.Factory do konstruktorów MediaCodecVideoRenderer i MediaCodecAudioRenderer.

Przechwytywanie wywołań metody za pomocą funkcji ForwardingPlayer

Możesz dostosować niektóre działanie instancji Player, pakując ją w podklasę ForwardingPlayer i zastępując metody. Dzięki temu da się wykonać dowolną z tych czynności:

  • Parametry dostępu przed przekazaniem ich do przedstawiciela Player.
  • Uzyskaj dostęp do wartości zwracanej przez przedstawiciela Player, zanim ją zwrócisz.
  • Całkowicie zaimplementuj metodę ponownie.

W przypadku zastępowania metod ForwardingPlayer ważne jest, aby zapewnić ich samospójność i zgodność z interfejsem Player, zwłaszcza w przypadku metod, które mają działać w sposób identyczny lub powiązany. Na przykład:

  • Jeśli chcesz zastąpić każdą operację „odtwarzanie”, musisz zastąpić zarówno metodę ForwardingPlayer.play, jak i ForwardingPlayer.setPlayWhenReady, ponieważ wywołujący będzie oczekiwać działania tych metod, gdy metoda playWhenReady = true będzie taka sama.
  • Jeśli chcesz zmienić przyrost wartości przewijania, musisz zastąpić oba: ForwardingPlayer.seekForward, aby wykonać wyszukiwanie z niestandardowym przyrostem, oraz ForwardingPlayer.getSeekForwardIncrement, by zgłosić poprawny i niestandardowy przyrost z powrotem do elementu wywołującego.
  • Jeśli chcesz kontrolować, jakie elementy Player.Commands są rozgłaszane przez wystąpienie odtwarzacza, musisz zastąpić wartości Player.getAvailableCommands() i Player.isCommandAvailable() oraz nasłuchiwać wywołania zwrotnego Player.Listener.onAvailableCommandsChanged(), aby otrzymywać powiadomienia o zmianach wprowadzanych w odtwarzaczu.

Dostosowanie MediaSource

W powyższych przykładach wstrzyknięte są niestandardowe komponenty, które będą używane podczas odtwarzania wszystkich obiektów MediaItem przekazywanych do odtwarzacza. Jeśli wymagane jest szczegółowe dostosowanie, można też wstrzykiwać niestandardowe komponenty w poszczególnych instancjach MediaSource, które można przekazywać bezpośrednio do odtwarzacza. Przykład poniżej pokazuje, jak dostosować ProgressiveMediaSource, aby użyć niestandardowych elementów 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

W bibliotece znajdziesz domyślne implementacje komponentów wymienionych u góry strony przeznaczone do typowych zastosowań. Element ExoPlayer może korzystać z tych komponentów, ale można też w niej korzystać z implementacji niestandardowych, jeśli wymagane są zachowania niestandardowe. Przykłady implementacji niestandardowych:

  • Renderer – możesz zaimplementować niestandardowy Renderer do obsługi typu multimediów, który nie jest obsługiwany przez domyślne implementacje udostępnione przez bibliotekę.
  • TrackSelector – wdrożenie niestandardowego elementu TrackSelector umożliwia deweloperowi aplikacji zmianę sposobu, w jaki ścieżki wyświetlane przez element MediaSource są wybierane do wykorzystania przez poszczególne dostępne właściwości Renderer.
  • LoadControl – zaimplementowanie niestandardowego elementu LoadControl umożliwia deweloperowi aplikacji zmianę zasady buforowania odtwarzacza.
  • Extractor – jeśli chcesz obsługiwać format kontenera, który nie jest obecnie obsługiwany przez bibliotekę, rozważ wdrożenie niestandardowej klasy Extractor.
  • MediaSource – implementacja niestandardowej klasy MediaSource może być odpowiednia, jeśli chcesz, aby próbki multimediów były przesyłane do mechanizmów renderowania w niestandardowy sposób lub chcesz wdrożyć niestandardowe działanie komponowania MediaSource.
  • MediaSource.Factory – wdrożenie niestandardowego elementu MediaSource.Factory umożliwia aplikacji dostosowanie sposobu tworzenia MediaSource na podstawie MediaItem.
  • DataSource – pakiet nadrzędny platformy ExoPlayer zawiera już wiele implementacji DataSource do różnych zastosowań. Możesz zaimplementować własną klasę DataSource, aby wczytywać dane w inny sposób, np. za pomocą niestandardowego protokołu, niestandardowego stosu HTTP lub z niestandardowej stałej pamięci podręcznej.

Podczas tworzenia komponentów niestandardowych zalecamy te działania:

  • Jeśli komponent niestandardowy musi zgłaszać zdarzenia z powrotem do aplikacji, zalecamy użycie tego samego modelu co w przypadku dotychczasowych komponentów ExoPlayer, np. za pomocą klas EventDispatcher lub przekazania obiektu Handler razem z odbiornikiem do konstruktora komponentu.
  • Zalecaliśmy, aby komponenty niestandardowe korzystały z tego samego modelu co istniejące komponenty ExoPlayer, co umożliwi ponowną konfigurację przez aplikację podczas odtwarzania. W tym celu komponenty niestandardowe powinny implementować atrybut PlayerMessage.Target i odbierać zmiany konfiguracji w metodzie handleMessage. Kod aplikacji powinien przekazywać zmiany w konfiguracji przez wywołanie metody createMessage platformy ExoPlayer, skonfigurowanie wiadomości i wysłanie jej do komponentu za pomocą PlayerMessage.send. Wysyłanie wiadomości do dostarczenia w wątku odtwarzania gwarantuje, że będą one wykonywane w tej samej kolejności, co inne operacje wykonywane w odtwarzaczu.