Dostosowywanie

Podstawą biblioteki ExoPlayer jest interfejs Player. Player pozwala na korzystanie z tradycyjnych funkcji odtwarzacza multimedialnego na wysokim poziomie, takich jak buforowanie multimediów, odtwarzanie, wstrzymywanie i przewijanie. Domyślna implementacja ExoPlayer zakłada pewne założenia (i w związku z tym nakłada pewne ograniczenia) dotyczące typu odtwarzanych multimediów, sposobu i miejsca ich przechowywania oraz sposobu ich renderowania. Zamiast implementować bezpośrednie wczytywanie i renderowanie multimediów, implementacje ExoPlayer powierzają tę pracę komponentom, które są wstrzykiwane podczas tworzenia odtwarzacza lub gdy nowe źródła multimediów są przekazywane do odtwarzacza. Komponenty wspólne dla wszystkich implementacji ExoPlayer:

  • MediaSource instancje, które definiują multimedia do odtworzenia, wczytują multimedia i z których można odczytać wczytane multimedia. Z MediaItem w odtwarzaczu MediaSource.Factory tworzy się wystąpienie MediaSource. Można je też przekazać bezpośrednio do odtwarzacza za pomocą interfejsu media source based playlist API.
  • MediaSource.Factory przekształca MediaItemMediaSource. Wartość MediaSource.Factory jest wstrzykiwana podczas tworzenia odtwarzacza.
  • Renderer instancje, które renderują poszczególne komponenty multimediów. Są one wstrzykiwane podczas tworzenia odtwarzacza.
  • TrackSelector, który wybiera utwory udostępnione przez MediaSource do odtworzenia przez każdą dostępną Renderer. Podczas tworzenia gracza dodawany jest TrackSelector.
  • LoadControl, który określa, kiedy MediaSource ma buforować więcej multimediów i ile multimediów ma być buforowanych. Gdy gracz zostanie utworzony, do gry zostanie wstrzyknięty element LoadControl.
  • LivePlaybackSpeedControl, który kontroluje szybkość odtwarzania podczas odtwarzania na żywo, aby odtwarzacz pozostawał w pobliżu skonfigurowanego opóźnienia na żywo. Podczas tworzenia odtwarzacza dodawany jest element LivePlaybackSpeedControl.

Koncepcja wstrzykiwania komponentów, które implementują funkcje dla graczy, jest obecna w całej bibliotece. Domyślne implementacje niektórych komponentów zlecają wykonanie zadania innym komponentom. Dzięki temu wiele podkomponentów można zastąpić poszczególnymi implementacjami skonfigurowanymi w niestandardowy sposób.

Dostosowywanie odtwarzacza

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

Konfigurowanie stosu sieciowego

Na stronie Dostosowywanie warstwy sieciowej używanej przez ExoPlayera znajdziesz informacje na ten temat.

Buforowanie danych wczytanych z sieci

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

Dostosowywanie interakcji z serwerem

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

Ten przykład pokazuje, jak zaimplementować te zachowania, wstrzykując niestandardową funkcję DataSource.Factory do funkcji 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 powyższym fragmencie kodu wstrzyknięty tag 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 zastosować bardziej szczegółowe podejście, możesz wstrzyknąć zachowanie typu just-in-time za pomocą funkcji ResolvingDataSource. Ten fragment kodu pokazuje, jak wstrzyknąć nagłówki żądań tuż przed nawiązaniem interakcji 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ć funkcji ResolvingDataSource do wprowadzania modyfikacji identyfikatora URI w czasie kompilacji, 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 pozwala aplikacjom dostosować sposób, w jaki ExoPlayer reaguje na błędy wczytywania. Aplikacja może na przykład szybko zakończyć działanie zamiast wielokrotnego powtarzania prób lub dostosować logikę wycofywania, która kontroluje czas oczekiwania gracza między kolejnymi próbami. Ten fragment kodu pokazuje, jak zaimplementować 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 umożliwić dostosowanie logiki na podstawie typu błędu lub nieudanej prośby.

Dostosowywanie flag ekstraktora

Flagi ekstraktora można używać do dostosowywania sposobu wyodrębniania poszczególnych formatów z multimediów progresywnych. Można je ustawić w DefaultExtractorsFactory, który jest udostępniany DefaultMediaSourceFactory. W tym przykładzie przekazywany jest parametr, który 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 wyszukiwania stałego bitrate’u

W przypadku strumieni MP3, ADTS i AMR możesz włączyć przybliżone wyszukiwanie za pomocą założenia stałej szybkości transmisji bitów z flagami FLAG_ENABLE_CONSTANT_BITRATE_SEEKING. Te flagi można ustawiać w przypadku poszczególnych ekstraktorów za pomocą metod DefaultExtractorsFactory.setXyzExtractorFlags opisanych powyżej. Aby włączyć wyszukiwanie stałego bitrate’u dla wszystkich ekstraktorów, które obsługują tę funkcję, użyj opcji DefaultExtractorsFactory.setConstantBitrateSeekingEnabled.

Kotlin

val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)

Java

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

Następnie możesz wstrzyknąć ExtractorsFactory za pomocą DefaultMediaSourceFactory w sposób opisany powyżej w sekcji poświęconej dostosowywaniu flag wyodrębniania.

Włączanie asynchronicznego kolejkowania buforów

Asynchroniczne kolejkowanie buforów to ulepszenie w pipeline’ie renderowania ExoPlayera, które obsługuje MediaCodec instancje w trybie asynchronicznym i korzysta z dodatkowych wątków do planowania dekodowania i renderowania danych. Włączenie tej opcji może zmniejszyć liczbę utraconej liczby klatek i niedostatecznego dźwięku.

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

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 instancjujesz bezpośrednio renderowanie, przekaż parametrAsynchronousMediaCodecAdapter.Factory do konstruktorów MediaCodecVideoRendererMediaCodecAudioRenderer.

Dostosowywanie operacji za pomocą ForwardingSimpleBasePlayer

Możesz dostosować niektóre zachowania instancji Player, owijając ją w podklasę ForwardingSimpleBasePlayer. Ta klasa umożliwia przechwytywanie określonych „operacji” zamiast bezpośredniego implementowania metod Player. Dzięki temu działanie funkcji takich jak play(), pause()setPlayWhenReady(boolean) będzie spójne. Zapewnia też, że wszystkie zmiany stanu są prawidłowo propagowane do zarejestrowanych instancji Player.Listener. W przypadku większości zastosowań związanych z dostosowywaniem ForwardingSimpleBasePlayer należy preferować przed bardziej podatnym na błędy ForwardingPlayer ze względu na gwarancję spójności.

Aby na przykład dodać logikę niestandardową podczas uruchamiania lub zatrzymywania odtwarzania:

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 polecenie Player.seekToNext jest poleceniem nieopcjonalnym):

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

W powyższych przykładach dodawane są niestandardowe komponenty do wykorzystania podczas odtwarzania wszystkich obiektówMediaItem przekazywanych do odtwarzacza. W przypadku dokładnej personalizacji możesz też wstrzyknąć niestandardowe komponenty do poszczególnych instancji MediaSource, które mogą być przekazywane bezpośrednio do odtwarzacza. Przykład poniżej pokazuje, jak dostosować element ProgressiveMediaSource, aby używać 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

Biblioteka udostępnia domyślne implementacje komponentów wymienionych u góry tej strony na potrzeby typowych zastosowań. ExoPlayer może używać tych komponentów, ale może też być zbudowany w taki sposób, aby używać implementacji niestandardowych, jeśli wymagane są niestandardowe zachowania. Oto kilka przykładowych zastosowań niestandardowych implementacji:

  • Renderer – możesz wdrożyć niestandardowy Renderer, aby obsłużyć typ multimediów, który nie jest obsługiwany przez domyślne implementacje udostępniane przez bibliotekę.
  • TrackSelector – wdrożenie niestandardowego TrackSelector pozwala deweloperowi aplikacji zmienić sposób, w jaki utwory udostępnione przez MediaSource są wybierane do odtwarzania przez poszczególne dostępne Renderer.
  • LoadControl – wdrożenie niestandardowego LoadControl pozwala deweloperowi aplikacji zmienić zasady buforowania odtwarzacza.
  • Extractor – jeśli potrzebujesz obsługi formatu kontenera, którego biblioteka nie obsługuje, zaimplementuj niestandardową klasę Extractor.
  • MediaSource – jeśli chcesz uzyskać próbki multimediów, które będą podawane do renderowania w niestandardowy sposób, lub jeśli chcesz zaimplementować niestandardowe zachowanie MediaSource, możesz użyć niestandardowej klasy MediaSource.
  • MediaSource.Factory – wdrożenie niestandardowego MediaSource.Factory pozwala aplikacji dostosować sposób tworzenia MediaSourceMediaItem.
  • DataSource – pakiet upstream ExoPlayera zawiera już kilka implementacji DataSource na potrzeby różnych zastosowań. Możesz zaimplementować własną klasę DataSource, aby wczytywać dane w inny sposób, np. za pomocą niestandardowego protokołu, za pomocą niestandardowego pakietu HTTP lub z niestandardowego trwałego 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 istniejące komponenty ExoPlayera, na przykład za pomocą klas EventDispatcher lub przekazania obiektu Handler wraz ze słuchaczem do konstruktora komponentu.
  • Zalecamy, aby komponenty niestandardowe korzystały z tego samego modelu co istniejące komponenty ExoPlayer, aby umożliwić aplikacji zmianę konfiguracji podczas odtwarzania. W tym celu komponenty niestandardowe powinny implementować interfejs PlayerMessage.Target i przekazywać zmiany konfiguracji w ramach metody handleMessage. Kod aplikacji powinien przekazywać zmiany konfiguracji, wywołując metodę createMessage w ExoPlayerze, konfigurując wiadomość i wysyłając ją do komponentu za pomocą PlayerMessage.send. Wysyłanie wiadomości do przekazania w wątku odtwarzania zapewnia, że są one wykonywane w kolejności z innymi operacjami wykonywanymi na odtwarzaczu.