Podstawą biblioteki ExoPlayer jest interfejs Player
. Player
udostępnia tradycyjne funkcje odtwarzacza multimediów wysokiego poziomu, takie jak buforowanie multimediów, odtwarzanie, wstrzymywanie i przewijanie. Domyślna implementacja ExoPlayer
zakłada niewiele założeń (a tym samym nakłada niewiele ograniczeń) dotyczących typu odtwarzanych multimediów, sposobu i miejsca ich przechowywania oraz sposobu ich renderowania. Zamiast bezpośrednio implementować wczytywanie i renderowanie multimediów,ExoPlayer
implementacje przekazują to zadanie komponentom, które są wstrzykiwane podczas tworzenia odtwarzacza lub gdy do odtwarzacza przekazywane są nowe źródła multimediów.
Komponenty wspólne dla wszystkich implementacji ExoPlayer
to:
MediaSource
instancje, które określają multimedia do odtworzenia, wczytują multimedia i z których można odczytać wczytane multimedia. WystąpienieMediaSource
jest tworzone zMediaItem
przezMediaSource.Factory
w odtwarzaczu. Można je też przekazywać bezpośrednio do odtwarzacza za pomocą interfejsu API listy odtwarzania opartej na źródle multimediów.MediaSource.Factory
instancji, która przekształcaMediaItem
wMediaSource
. WartośćMediaSource.Factory
jest wstawiana podczas tworzenia odtwarzacza.Renderer
instancje, które renderują poszczególne komponenty multimediów; Są one wstrzykiwane podczas tworzenia odtwarzacza.TrackSelector
, który wybiera ścieżki dostarczane przezMediaSource
, aby były wykorzystywane przez każdy dostępnyRenderer
. Po utworzeniu odtwarzacza wstrzykiwany jestTrackSelector
.LoadControl
, która określa, kiedyMediaSource
buforuje więcej multimediów i ile multimediów jest buforowanych. Gdy odtwarzacz jest tworzony, wstawiany jest elementLoadControl
.LivePlaybackSpeedControl
, która kontroluje szybkość odtwarzania podczas odtwarzania na żywo, aby odtwarzacz mógł utrzymać skonfigurowane przesunięcie na żywo. Gdy odtwarzacz jest tworzony, wstrzykiwany jest element ALivePlaybackSpeedControl
.
Koncepcja wstrzykiwania komponentów implementujących fragmenty funkcjonalności odtwarzacza jest obecna w całej bibliotece. Domyślne implementacje niektórych komponentów przekazują pracę do kolejnych wstrzykiwanych komponentów. Umożliwia to indywidualną wymianę wielu podzespołów na implementacje skonfigurowane w niestandardowy sposób.
Dostosowywanie gracza
Poniżej znajdziesz kilka typowych przykładów dostosowywania odtwarzacza przez wstrzykiwanie komponentów.
Konfigurowanie stosu sieciowego
Mamy stronę poświęconą dostosowywaniu stosu sieciowego używanego przez ExoPlayera.
Buforowanie danych wczytanych z sieci
Zapoznaj się z przewodnikami dotyczącymi tymczasowego buforowania w locie i pobierania multimediów.
Dostosowywanie interakcji z serwerem
Niektóre aplikacje mogą przechwytywać żądania i odpowiedzi HTTP. Możesz wstawiać niestandardowe nagłówki żądań, odczytywać nagłówki odpowiedzi serwera, modyfikować adresy URI żądań itp. Na przykład aplikacja może uwierzytelniać się, wstawiając token jako nagłówek podczas żądania segmentów multimediów.
Ten przykład pokazuje, jak zaimplementować te zachowania, wstrzykując niestandardowy obiekt DataSource.Factory
do obiektu 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 kod 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 w odpowiednim momencie za pomocą elementu ResolvingDataSource
. Poniższy fragment kodu pokazuje, jak wstawić nagłówki żądań 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ć znaku ResolvingDataSource
, aby wprowadzać modyfikacje identyfikatora URI w odpowiednim momencie, 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
umożliwia aplikacjom dostosowywanie sposobu, w jaki ExoPlayer reaguje na błędy ładowania. Na przykład aplikacja może chcieć szybko zakończyć działanie zamiast wielokrotnie ponawiać próbę lub dostosować logikę wycofywania, która kontroluje, jak długo odtwarzacz czeka między kolejnymi próbami. Poniższy fragment kodu pokazuje, jak wdrożyć niestandardową logikę ponawiania:
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 ekstraktora
Flagi ekstraktora można wykorzystać do dostosowania sposobu wyodrębniania poszczególnych formatów z mediów progresywnych. Można je ustawić na DefaultExtractorsFactory
, który jest udostępniany DefaultMediaSourceFactory
. W przykładzie poniżej przekazywana jest flaga, która włącza wyszukiwanie w strumieniach MP3 na podstawie indeksu.
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 przy stałej szybkości transmisji
W przypadku strumieni MP3, ADTS i AMR możesz włączyć przybliżone wyszukiwanie, zakładając stałą szybkość transmisji bitów za pomocą flagi FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
.
Te flagi można ustawiać dla poszczególnych ekstraktorów za pomocą metod DefaultExtractorsFactory.setXyzExtractorFlags
opisanych powyżej. Aby włączyć wyszukiwanie ze stałą szybkością transmisji we wszystkich ekstraktorach, które je obsługują, użyj parametru DefaultExtractorsFactory.setConstantBitrateSeekingEnabled
.
Kotlin
val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)
Java
DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
ExtractorsFactory
można następnie wstawić za pomocą DefaultMediaSourceFactory
, jak opisano powyżej w przypadku dostosowywania flag ekstraktora.
Włączanie asynchronicznego kolejkowania buforów
Asynchroniczne kolejkowanie buforów to ulepszenie w potoku renderowania ExoPlayera, które działa w MediaCodec
instancjach w trybie asynchronicznym i wykorzystuje dodatkowe wątki do planowania dekodowania i renderowania danych. Włączenie tej opcji może zmniejszyć liczbę pominiętych klatek i niedoborów dźwięku.
Asynchroniczne kolejkowanie buforów jest domyślnie włączone na urządzeniach z Androidem 12 (poziom interfejsu API 31) i nowszym. Można je włączyć ręcznie od Androida 6.0 (poziom interfejsu API 23). Rozważ włączenie tej funkcji na urządzeniach, na których występują utracone klatki lub niedobory dźwięku, zwłaszcza podczas odtwarzania treści chronionych DRM lub treści o wysokiej liczbie klatek na sekundę.
W najprostszym przypadku musisz wstawić 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ż wartość
new DefaultMediaCodecAdapter.Factory(context).forceEnableAsynchronous()
do konstruktorów
MediaCodecVideoRenderer
i MediaCodecAudioRenderer
.
Dostosowywanie operacji za pomocą ForwardingSimpleBasePlayer
Niektóre zachowania instancji Player
możesz dostosować, umieszczając ją w podklasie ForwardingSimpleBasePlayer
. Ta klasa umożliwia przechwytywanie określonych „operacji”, zamiast 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 ForwardingSimpleBasePlayer
jest preferowane od bardziej podatnego na błędy ForwardingPlayer
ze względu na gwarancje spójności.
Aby 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
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 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
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
Powyższe przykłady wstawiają dostosowane komponenty do użycia podczas odtwarzania wszystkich obiektów przekazywanych do odtwarzacza.MediaItem
W przypadku, gdy wymagane jest precyzyjne dostosowanie, można też wstawiać niestandardowe komponenty do poszczególnych instancji MediaSource
, które można przekazywać bezpośrednio do odtwarzacza. Poniższy przykład 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 w przypadku typowych zastosowań. ExoPlayer
może używać tych komponentów, ale może też być zbudowany tak, aby korzystać z niestandardowych implementacji, jeśli wymagane są niestandardowe zachowania. Przykłady zastosowań niestandardowych implementacji:
Renderer
– możesz wdrożyć niestandardowy elementRenderer
, aby obsługiwać typ multimediów, który nie jest obsługiwany przez domyślne implementacje udostępniane przez bibliotekę.TrackSelector
– wdrożenie niestandardowegoTrackSelector
umożliwia deweloperowi aplikacji zmianę sposobu wybierania ścieżek udostępnianych przezMediaSource
do odtwarzania przez poszczególne dostępneRenderer
.LoadControl
– wdrożenie niestandardowegoLoadControl
umożliwia deweloperowi aplikacji zmianę zasad buforowania odtwarzacza.Extractor
– Jeśli musisz obsługiwać format kontenera, który nie jest obecnie obsługiwany przez bibliotekę, rozważ wdrożenie niestandardowej klasyExtractor
.MediaSource
– Zaimplementowanie niestandardowej klasyMediaSource
może być odpowiednie, jeśli chcesz uzyskiwać próbki multimediów do przekazywania do modułów renderujących w niestandardowy sposób lub jeśli chcesz zaimplementować niestandardowe zachowanie związane z komponowaniemMediaSource
.MediaSource.Factory
– wdrożenie niestandardowego elementuMediaSource.Factory
umożliwia aplikacji dostosowanie sposobu tworzenia elementuMediaSource
z elementuMediaItem
.DataSource
– pakiet nadrzędny ExoPlayera zawiera już kilka implementacjiDataSource
dla różnych przypadków użycia. Możesz zaimplementować własną klasęDataSource
, aby wczytywać dane w inny sposób, np. za pomocą protokołu niestandardowego, stosu HTTP lub niestandardowej pamięci podręcznej.
Podczas tworzenia komponentów niestandardowych zalecamy wykonanie tych czynności:
- Jeśli komponent niestandardowy musi zgłaszać zdarzenia do aplikacji, zalecamy używanie tego samego modelu co w przypadku istniejących komponentów ExoPlayera, np. klas
EventDispatcher
lub przekazywanieHandler
wraz z odbiornikiem do konstruktora komponentu. - Zalecamy, aby komponenty niestandardowe korzystały z tego samego modelu co istniejące komponenty ExoPlayera, co umożliwi aplikacji zmianę konfiguracji podczas odtwarzania. Aby to zrobić, komponenty niestandardowe powinny implementować interfejs
PlayerMessage.Target
i otrzymywać zmiany konfiguracji w metodziehandleMessage
. Kod aplikacji powinien przekazywać zmiany konfiguracji, wywołując metodęcreateMessage
ExoPlayera, konfigurując wiadomość i wysyłając ją do komponentu za pomocąPlayerMessage.send
. Wysyłanie wiadomości do wątku odtwarzania zapewnia, że są one wykonywane w kolejności z innymi operacjami wykonywanymi na odtwarzaczu.