Das Herzstück der ExoPlayer-Bibliothek ist die Player
-Schnittstelle. Ein Player
bietet traditionelle Funktionen eines Mediaplayers auf oberster Ebene, z. B. die Möglichkeit, Medien zu puffern, abzuspielen, anzuhalten und zu suchen. Bei der Standardimplementierung ExoPlayer
werden nur wenige Annahmen über die Art der wiedergegebenen Medien, die Art und Weise, wie sie gespeichert und gerendert werden, getroffen. Daher gelten auch nur wenige Einschränkungen. Anstatt das Laden und Rendern von Medien direkt zu implementieren, delegieren ExoPlayer
-Implementierungen diese Aufgabe an Komponenten, die beim Erstellen eines Players oder beim Übergeben neuer Medienquellen an den Player eingefügt werden.
Komponenten, die allen ExoPlayer
-Implementierungen gemeinsam sind:
MediaSource
-Instanzen, die die abzuspielenden Medien definieren, die Medien laden und von denen die geladenen Medien gelesen werden können. EineMediaSource
-Instanz wird von einemMediaSource.Factory
im Player aus einerMediaItem
erstellt. Sie können auch über die Media Source Based Playlist API direkt an den Player übergeben werden.- Eine
MediaSource.Factory
-Instanz, die einMediaItem
in einMediaSource
konvertiert.MediaSource.Factory
wird beim Erstellen des Players eingefügt. Renderer
-Instanzen, die einzelne Komponenten der Medien rendern. Sie werden beim Erstellen des Players eingefügt.- Eine
TrackSelector
, die von derMediaSource
bereitgestellte Titel auswählt, die von allen verfügbarenRenderer
verwendet werden sollen. EinTrackSelector
wird beim Erstellen des Players eingefügt. - Ein
LoadControl
, das steuert, wann und wie viel Medien vomMediaSource
im Puffer gespeichert werden. EinLoadControl
wird beim Erstellen des Players eingefügt. - Eine
LivePlaybackSpeedControl
, die die Wiedergabegeschwindigkeit bei der Livewiedergabe steuert, damit der Player nah an einem konfigurierten Live-Offset bleibt. EinLivePlaybackSpeedControl
wird beim Erstellen des Players eingefügt.
Das Konzept, Komponenten einzuschleusen, die Teile der Playerfunktion implementieren, ist in der gesamten Bibliothek vorhanden. Die Standardimplementierungen einiger Komponenten delegieren Aufgaben an weitere injizierte Komponenten. So können viele Unterkomponenten einzeln durch benutzerdefinierte Implementierungen ersetzt werden.
Spieleranpassung
Im Folgenden findest du einige Beispiele für die Anpassung des Players durch Einfügen von Komponenten.
Netzwerkstack konfigurieren
Auf dieser Seite erfährst du, wie du den von ExoPlayer verwendeten Netzwerkstack anpassen kannst.
Aus dem Netzwerk geladene Daten im Cache speichern
Weitere Informationen finden Sie in den Anleitungen zum vorübergehenden On-the-fly-Caching und zum Herunterladen von Medien.
Serverinteraktionen anpassen
Einige Apps möchten möglicherweise HTTP-Anfragen und ‑Antworten abfangen. Du kannst beispielsweise benutzerdefinierte Anfrageheader einfügen, die Antwortheader des Servers lesen oder die URIs der Anfragen ändern. Deine App kann sich beispielsweise authentifizieren, indem sie beim Anfordern der Mediensegmente ein Token als Header einfügt.
Das folgende Beispiel zeigt, wie diese Verhaltensweisen implementiert werden, indem ein benutzerdefinierter DataSource.Factory
in den DefaultMediaSourceFactory
eingefügt wird:
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();
Im Code-Snippet oben enthält das eingefügte HttpDataSource
den Header "Header: Value"
in jeder HTTP-Anfrage. Dieses Verhalten ist für jede Interaktion mit einer HTTP-Quelle fest.
Für einen detaillierteren Ansatz können Sie mithilfe eines ResolvingDataSource
-Elements Just-in-Time-Verhalten einfügen. Das folgende Code-Snippet zeigt, wie Anfrageheader kurz vor der Interaktion mit einer HTTP-Quelle eingefügt werden:
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)));
Sie können auch einen ResolvingDataSource
verwenden, um Just-in-Time-Änderungen am URI vorzunehmen, wie im folgenden Snippet gezeigt:
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)));
Fehlerbehandlung anpassen
Durch die Implementierung einer benutzerdefinierten LoadErrorHandlingPolicy
können Apps anpassen, wie ExoPlayer auf Ladefehler reagiert. So kann es beispielsweise sein, dass eine App schnell fehlschlagen soll, anstatt viele Male wiederholt zu werden. Oder es kann sein, dass die Backoff-Logik angepasst werden soll, die steuert, wie lange der Player zwischen den einzelnen Versuchen wartet. Das folgende Snippet zeigt, wie eine benutzerdefinierte Back-off-Logik implementiert wird:
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();
Das Argument LoadErrorInfo
enthält weitere Informationen zur fehlgeschlagenen Ladeaktion, damit die Logik je nach Fehlertyp oder fehlgeschlagener Anfrage angepasst werden kann.
Extrahierungs-Flags anpassen
Mit Extractor-Flags kannst du anpassen, wie einzelne Formate aus progressiven Medien extrahiert werden. Sie können auf der DefaultExtractorsFactory
festgelegt werden, die der DefaultMediaSourceFactory
zur Verfügung gestellt wird. Im folgenden Beispiel wird ein Flag übergeben, das die indexbasierte Suche für MP3-Streams aktiviert.
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();
Suchen nach konstanter Bitrate aktivieren
Bei MP3-, ADTS- und AMR-Streams kannst du die ungefähre Suche mit einer Annahme einer konstanten Bitrate mit FLAG_ENABLE_CONSTANT_BITRATE_SEEKING
-Flags aktivieren.
Diese Flags können für einzelne Extraktoren mit den einzelnen DefaultExtractorsFactory.setXyzExtractorFlags
-Methoden wie oben beschrieben festgelegt werden. Wenn du die Suche nach konstanter Bitrate für alle unterstützten Extractor aktivieren möchtest, verwende DefaultExtractorsFactory.setConstantBitrateSeekingEnabled
.
Kotlin
val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)
Java
DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
Die ExtractorsFactory
kann dann wie oben beschrieben über DefaultMediaSourceFactory
eingefügt werden.
Asynchrone Pufferwarteschlange aktivieren
Die asynchrone Pufferwarteschlange ist eine Verbesserung in der Rendering-Pipeline von ExoPlayer. Dabei werden MediaCodec
-Instanzen im asynchronen Modus ausgeführt und zusätzliche Threads werden verwendet, um die Dekodierung und das Rendering von Daten zu planen. Wenn du sie aktivierst, können weniger Frames verloren gehen und Audioaussetzer reduziert werden.
Die asynchrone Pufferwarteschlange ist auf Geräten mit Android 12 (API-Level 31) und höher standardmäßig aktiviert und kann ab Android 6.0 (API-Level 23) manuell aktiviert werden. Du kannst die Funktion für bestimmte Geräte aktivieren, auf denen du Frame-Ausfälle oder Audioaussetzer feststellst, insbesondere beim Abspielen von mit digitalen Rechteverwaltung (DRM) geschützten Inhalten oder Inhalten mit hoher Framerate.
Im einfachsten Fall musst du dem Player einen DefaultRenderersFactory
einfügen. Gehe dazu so vor:
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();
Wenn Sie Renderer direkt instanziieren, übergeben Sie den Konstruktoren von MediaCodecVideoRenderer
und MediaCodecAudioRenderer
einen AsynchronousMediaCodecAdapter.Factory
.
Vorgänge mit ForwardingSimpleBasePlayer
anpassen
Sie können das Verhalten einer Player
-Instanz teilweise anpassen, indem Sie sie in eine Unterklasse von ForwardingSimpleBasePlayer
einbetten. Mit dieser Klasse können Sie bestimmte „Vorgänge“ abfangen, anstatt direkt Player
-Methoden implementieren zu müssen. So wird ein einheitliches Verhalten von beispielsweise play()
, pause()
und setPlayWhenReady(boolean)
sichergestellt. Außerdem wird sichergestellt, dass alle Statusänderungen korrekt an registrierte Player.Listener
-Instanzen weitergegeben werden. Für die meisten Anpassungsfälle sollte ForwardingSimpleBasePlayer
aufgrund dieser Konsistenzgarantien dem fehleranfälligeren ForwardingPlayer
vorgezogen werden.
So fügen Sie beispielsweise benutzerdefinierte Logik hinzu, wenn die Wiedergabe gestartet oder angehalten wird:
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); } }
So können Sie den Befehl SEEK_TO_NEXT
deaktivieren und dafür sorgen, dass Player.seekToNext
nicht ausgeführt wird:
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. }
MediaSource-Anpassung
In den Beispielen oben werden benutzerdefinierte Komponenten für die Wiedergabe aller MediaItem
-Objekte eingefügt, die an den Player übergeben werden. Wenn eine detaillierte Anpassung erforderlich ist, können benutzerdefinierte Komponenten auch in einzelne MediaSource
-Instanzen eingefügt werden, die dann direkt an den Player übergeben werden. Im folgenden Beispiel wird gezeigt, wie Sie eine ProgressiveMediaSource
so anpassen, dass benutzerdefinierte DataSource.Factory
, ExtractorsFactory
und LoadErrorHandlingPolicy
verwendet werden:
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));
Benutzerdefinierte Komponenten erstellen
Die Bibliothek bietet Standardimplementierungen der oben auf dieser Seite aufgeführten Komponenten für gängige Anwendungsfälle. Ein ExoPlayer
kann diese Komponenten verwenden, aber auch für benutzerdefinierte Implementierungen entwickelt werden, wenn nicht standardmäßiges Verhalten erforderlich ist. Beispiele für Anwendungsfälle für benutzerdefinierte Implementierungen:
Renderer
: Du kannst eine benutzerdefinierteRenderer
implementieren, um einen Medientyp zu verarbeiten, der von den Standardimplementierungen der Bibliothek nicht unterstützt wird.TrackSelector
: Mit einer benutzerdefiniertenTrackSelector
können App-Entwickler ändern, wie die von einemMediaSource
freigegebenen Tracks für die Nutzung durch die einzelnen verfügbarenRenderer
s ausgewählt werden.LoadControl
: Mit einer benutzerdefiniertenLoadControl
können App-Entwickler die Pufferrichtlinie des Players ändern.Extractor
: Wenn Sie ein Containerformat unterstützen müssen, das derzeit nicht von der Bibliothek unterstützt wird, sollten Sie eine benutzerdefinierteExtractor
-Klasse implementieren.MediaSource
: Die Implementierung einer benutzerdefiniertenMediaSource
-Klasse kann sinnvoll sein, wenn Sie Medienbeispiele erhalten möchten, die auf benutzerdefinierte Weise an Renderer gesendet werden, oder wenn Sie benutzerdefiniertesMediaSource
-Compositing-Verhalten implementieren möchten.MediaSource.Factory
: Mit einer benutzerdefiniertenMediaSource.Factory
kann in einer Anwendung die Erstellung einerMediaSource
aus einerMediaItem
angepasst werden.DataSource
: Das Upstream-Paket von ExoPlayer enthält bereits eine Reihe vonDataSource
-Implementierungen für verschiedene Anwendungsfälle. Sie können eine eigeneDataSource
-Klasse implementieren, um Daten auf andere Weise zu laden, z. B. über ein benutzerdefiniertes Protokoll, mit einem benutzerdefinierten HTTP-Stack oder aus einem benutzerdefinierten persistenten Cache.
Beim Erstellen benutzerdefinierter Komponenten empfehlen wir Folgendes:
- Wenn eine benutzerdefinierte Komponente Ereignisse an die App zurückgeben muss, empfehlen wir, dasselbe Modell wie bei vorhandenen ExoPlayer-Komponenten zu verwenden. Du kannst beispielsweise
EventDispatcher
-Klassen verwenden oder einenHandler
zusammen mit einem Listener an den Konstruktor der Komponente übergeben. - Wir empfehlen, für benutzerdefinierte Komponenten dasselbe Modell wie für vorhandene ExoPlayer-Komponenten zu verwenden, damit die App sie während der Wiedergabe neu konfigurieren kann. Dazu müssen benutzerdefinierte Komponenten
PlayerMessage.Target
implementieren und Konfigurationsänderungen über die MethodehandleMessage
empfangen. Der Anwendungscode sollte Konfigurationsänderungen übergeben, indem diecreateMessage
-Methode von ExoPlayer aufgerufen, die Nachricht konfiguriert und mitPlayerMessage.send
an die Komponente gesendet wird. Wenn Nachrichten an den Wiedergabe-Thread gesendet werden, werden sie in der Reihenfolge aller anderen Vorgänge ausgeführt, die auf dem Player ausgeführt werden.