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. InstancjaMediaSourcejest tworzona zMediaItemprzezMediaSource.Factoryw 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 konwertujeMediaItemnaMediaSource. ObiektMediaSource.Factoryjest wstrzykiwany podczas tworzenia odtwarzacza. Rendererinstancji, które renderują poszczególne komponenty multimediów. Są one wstrzykiwane podczas tworzenia odtwarzacza.TrackSelector, który wybiera ścieżki dostarczane przezMediaSourcedo wykorzystania przez każdy dostępnyRenderer. Podczas tworzenia odtwarzacza jest wstrzykiwany elementTrackSelector.LoadControl, który określa, kiedyMediaSourcebuforuje więcej multimediów i ile multimediów jest buforowanych. Podczas tworzenia odtwarzacza wstrzykiwany jest obiektLoadControl.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 elementLivePlaybackSpeedControl.
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ć niestandardowyRenderer, aby obsługiwać typ multimediów, który nie jest obsługiwany przez domyślne implementacje udostępniane przez bibliotekę.TrackSelector– implementacja niestandardowego elementuTrackSelectorumożliwia deweloperowi aplikacji zmianę sposobu, w jaki ścieżki udostępniane przez elementMediaSourcesą wybierane do wykorzystania przez poszczególne dostępne elementyRenderer.LoadControl– wdrożenie niestandardowegoLoadControlpozwala 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 klasyExtractor.MediaSource– wdrożenie niestandardowej klasyMediaSourcemoż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 kompozycjiMediaSource.MediaSource.Factory– implementacja niestandardowego interfejsuMediaSource.Factoryumożliwia aplikacji dostosowanie sposobu tworzenia obiektuMediaSourcez obiektuMediaItem.DataSource– pakiet nadrzędny ExoPlayera zawiera już wiele implementacjiDataSourcedla 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
EventDispatcherlub przekazaniaHandlerwraz 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.Targeti odbierać zmiany konfiguracji w metodziehandleMessage. Kod aplikacji powinien przekazywać zmiany konfiguracji, wywołując metodęcreateMessageExoPlayera, konfigurując wiadomość i wysyłając ją do komponentu za pomocą metodyPlayerMessage.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.