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. ZMediaItem
w odtwarzaczuMediaSource.Factory
tworzy się wystąpienieMediaSource
. Można je też przekazać bezpośrednio do odtwarzacza za pomocą interfejsu media source based playlist API.MediaSource.Factory
przekształcaMediaItem
wMediaSource
. 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 przezMediaSource
do odtworzenia przez każdą dostępnąRenderer
. Podczas tworzenia gracza dodawany jestTrackSelector
.LoadControl
, który określa, kiedyMediaSource
ma buforować więcej multimediów i ile multimediów ma być buforowanych. Gdy gracz zostanie utworzony, do gry zostanie wstrzyknięty elementLoadControl
.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 elementLivePlaybackSpeedControl
.
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żąco i pobierania 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 MediaCodecVideoRenderer
i MediaCodecAudioRenderer
.
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()
i 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ć niestandardowyRenderer
, aby obsłużyć typ multimediów, który nie jest obsługiwany przez domyślne implementacje udostępniane przez bibliotekę.TrackSelector
– wdrożenie niestandardowegoTrackSelector
pozwala deweloperowi aplikacji zmienić sposób, w jaki utwory udostępnione przezMediaSource
są wybierane do odtwarzania przez poszczególne dostępneRenderer
.LoadControl
– wdrożenie niestandardowegoLoadControl
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 zachowanieMediaSource
, możesz użyć niestandardowej klasyMediaSource
.MediaSource.Factory
– wdrożenie niestandardowegoMediaSource.Factory
pozwala aplikacji dostosować sposób tworzeniaMediaSource
zMediaItem
.DataSource
– pakiet upstream ExoPlayera zawiera już kilka implementacjiDataSource
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 obiektuHandler
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 metodyhandleMessage
. 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.