Das Herzstück der ExoPlayer-Bibliothek ist die Player
-Schnittstelle. Ein Player
bietet herkömmliche Media Player-Funktionen auf hoher Ebene, z. B. die Möglichkeit, Medien zu puffern, wiederzugeben, zu pausieren und zu suchen. Die Standardimplementierung ExoPlayer
geht von wenigen Annahmen über die Art der wiedergegebenen Medien, die Art und den Ort ihrer Speicherung sowie die Art ihrer Darstellung aus und unterliegt daher nur wenigen Einschränkungen. Anstatt das Laden und Rendern von Media direkt zu implementieren, delegieren ExoPlayer
-Implementierungen diese Aufgabe an Komponenten, die beim Erstellen eines Players oder beim Übergeben neuer Media-Quellen an den Player eingefügt werden.
Die folgenden Komponenten sind für alle ExoPlayer
-Implementierungen gleich:
MediaSource
-Instanzen, die abzuspielende Medien definieren, die Medien laden und aus denen die geladenen Medien gelesen werden können. EineMediaSource
-Instanz wird von einemMediaItem
durch einMediaSource.Factory
im Player erstellt. Sie können auch direkt über die Playlist API basierend auf der Media-Quelle an den Player übergeben werden.- Eine
MediaSource.Factory
-Instanz, die einMediaItem
in einMediaSource
konvertiert. DieMediaSource.Factory
wird beim Erstellen des Players eingefügt. Renderer
-Instanzen, die einzelne Komponenten der Media rendern. Sie werden beim Erstellen des Players eingefügt.- Eine
TrackSelector
, mit der Titel ausgewählt werden, die von derMediaSource
bereitgestellt werden und von jeder verfügbarenRenderer
verwendet werden können. EinTrackSelector
wird eingefügt, wenn der Player erstellt wird. - Ein
LoadControl
, der steuert, wannMediaSource
mehr Medien puffert und wie viele Medien gepuffert werden. EinLoadControl
wird eingefügt, wenn der Player erstellt wird. - Eine
LivePlaybackSpeedControl
, die die Wiedergabegeschwindigkeit bei Live-Wiedergaben steuert, damit der Player in der Nähe eines konfigurierten Live-Offsets bleibt. EinLivePlaybackSpeedControl
wird beim Erstellen des Players eingefügt.
Das Konzept des Einfügens von Komponenten, die Teile der Player-Funktionalität implementieren, ist in der gesamten Bibliothek vorhanden. Bei den Standardimplementierungen einiger Komponenten wird die Arbeit an weitere eingefügte Komponenten delegiert. So können viele Unterkomponenten einzeln durch Implementierungen ersetzt werden, die benutzerdefiniert konfiguriert sind.
Videoplayer-Anpassung
Im Folgenden finden Sie einige gängige Beispiele für die Anpassung des Players durch Einfügen von Komponenten.
Netzwerk-Stack konfigurieren
Hier finden Sie Informationen zum Anpassen des von ExoPlayer verwendeten Netzwerk-Stacks.
Aus dem Netzwerk geladene Daten im Cache speichern
Weitere Informationen finden Sie in den Anleitungen zum temporären On-the-fly-Caching und zum Herunterladen von Medien.
Serverinteraktionen anpassen
Einige Apps möchten möglicherweise HTTP-Anfragen und ‑Antworten abfangen. Möglicherweise möchten Sie benutzerdefinierte Anfrageheader einfügen, die Antwortheader des Servers lesen oder die URIs der Anfragen ändern. Ihre App kann sich beispielsweise authentifizieren, indem sie beim Anfordern der Media-Segmente ein Token als Header einfügt.
Im folgenden Beispiel wird gezeigt, wie Sie diese Verhaltensweisen implementieren, indem Sie ein benutzerdefiniertes DataSource.Factory
in das DefaultMediaSourceFactory
einfügen:
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 obigen Code-Snippet enthält der eingefügte HttpDataSource
den Header "Header: Value"
in jeder HTTP-Anfrage. Dieses Verhalten ist für jede Interaktion mit einer HTTP-Quelle behoben.
Für einen detaillierteren Ansatz können Sie Just-in-Time-Verhalten mit einem ResolvingDataSource
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 ein ResolvingDataSource
verwenden, um den URI in Echtzeit zu ändern, 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 eines benutzerdefinierten LoadErrorHandlingPolicy
können Apps anpassen, wie ExoPlayer auf Ladefehler reagiert. Eine App kann beispielsweise schnell fehlschlagen, anstatt es viele Male zu versuchen, oder die Backoff-Logik anpassen, die steuert, wie lange der Player zwischen den einzelnen Wiederholungsversuchen wartet. Das folgende Snippet zeigt, wie eine benutzerdefinierte Backoff-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 LoadErrorInfo
-Argument enthält weitere Informationen zum fehlgeschlagenen Ladevorgang, um die Logik basierend auf dem Fehlertyp oder der fehlgeschlagenen Anfrage anzupassen.
Extractor-Flags anpassen
Mit Extraktor-Flags können Sie anpassen, wie einzelne Formate aus progressiven Media extrahiert werden. Sie können für die DefaultExtractorsFactory
festgelegt werden, die für die DefaultMediaSourceFactory
bereitgestellt 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();
Suche mit konstanter Bitrate aktivieren
Bei MP3-, ADTS- und AMR-Streams können Sie 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 oben beschriebenen individuellen DefaultExtractorsFactory.setXyzExtractorFlags
-Methoden festgelegt werden. Wenn Sie die Suche mit konstanter Bitrate für alle Extraktoren aktivieren möchten, die sie unterstützen, verwenden Sie DefaultExtractorsFactory.setConstantBitrateSeekingEnabled
.
Kotlin
val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)
Java
DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
Die ExtractorsFactory
kann dann über DefaultMediaSourceFactory
eingefügt werden, wie oben für das Anpassen von Extraktor-Flags beschrieben.
Asynchrones Einreihen von Puffern aktivieren
Die asynchrone Pufferwarteschlange ist eine Verbesserung in der Rendering-Pipeline von ExoPlayer, bei der MediaCodec
-Instanzen im asynchronen Modus ausgeführt werden und zusätzliche Threads zum Planen der Decodierung und des Renderings von Daten verwendet werden. Wenn Sie diese Option aktivieren, können Sie die Anzahl der ausgelassenen Frames und Audio-Underruns reduzieren.
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. Aktivieren Sie die Funktion für bestimmte Geräte, auf denen Sie Frame-Drops oder Audio-Underruns beobachten, insbesondere bei der Wiedergabe von DRM-geschützten Inhalten oder Inhalten mit hoher Framerate.
Im einfachsten Fall müssen Sie dem Player ein DefaultRenderersFactory
so hinzufügen:
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 new DefaultMediaCodecAdapter.Factory(context).forceEnableAsynchronous()
an die Konstruktoren MediaCodecVideoRenderer
und MediaCodecAudioRenderer
.
Vorgänge mit ForwardingSimpleBasePlayer
anpassen
Sie können das Verhalten einer Player
-Instanz anpassen, indem Sie sie in eine Unterklasse von ForwardingSimpleBasePlayer
einfügen. Mit dieser Klasse können Sie bestimmte „Vorgänge“ abfangen, anstatt Player
-Methoden direkt implementieren zu müssen. So wird ein einheitliches Verhalten von beispielsweise play()
, pause()
und setPlayWhenReady(boolean)
gewährleistet. Außerdem wird so dafür gesorgt, dass alle Statusänderungen korrekt an registrierte Player.Listener
-Instanzen weitergegeben werden. Für die meisten Anwendungsfälle zur Anpassung sollte ForwardingSimpleBasePlayer
aufgrund dieser Konsistenzgarantien dem fehleranfälligeren ForwardingPlayer
vorgezogen werden.
Wenn Sie beispielsweise benutzerdefinierte Logik hinzufügen möchten, wenn die Wiedergabe gestartet oder beendet 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
keine Auswirkungen hat):
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 obigen Beispielen 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 Sie auch benutzerdefinierte Komponenten in einzelne MediaSource
-Instanzen einfügen, die direkt an den Player übergeben werden können. Das folgende Beispiel zeigt, wie Sie ein ProgressiveMediaSource
anpassen, um ein benutzerdefiniertes DataSource.Factory
, ExtractorsFactory
und LoadErrorHandlingPolicy
zu verwenden:
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, kann aber auch so erstellt werden, dass benutzerdefinierte Implementierungen verwendet werden, wenn nicht standardmäßige Verhaltensweisen erforderlich sind. Beispiele für Anwendungsfälle für benutzerdefinierte Implementierungen:
Renderer
: Möglicherweise möchten Sie eine benutzerdefinierteRenderer
implementieren, um einen Medientyp zu verarbeiten, der von den Standardimplementierungen der Bibliothek nicht unterstützt wird.TrackSelector
: Durch die Implementierung eines benutzerdefiniertenTrackSelector
kann ein App-Entwickler ändern, wie von einemMediaSource
bereitgestellte Tracks für die Nutzung durch die einzelnen verfügbarenRenderer
s ausgewählt werden.LoadControl
: Durch die Implementierung eines benutzerdefiniertenLoadControl
kann ein App-Entwickler die Pufferungsrichtlinie 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 Media-Samples auf benutzerdefinierte Weise an Renderer übergeben oder ein benutzerdefiniertesMediaSource
-Compositing-Verhalten implementieren möchten.MediaSource.Factory
: Durch die Implementierung eines benutzerdefiniertenMediaSource.Factory
kann eine Anwendung die Art und Weise anpassen, in der einMediaSource
aus einemMediaItem
erstellt wird.DataSource
: Das Upstream-Paket von ExoPlayer enthält bereits eine Reihe vonDataSource
-Implementierungen für verschiedene Anwendungsfälle. Möglicherweise möchten Sie Ihre 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ückmelden muss, empfehlen wir, dass Sie dazu dasselbe Modell wie bei vorhandenen ExoPlayer-Komponenten verwenden, z. B.
EventDispatcher
-Klassen oder das Übergeben einesHandler
zusammen mit einem Listener an den Konstruktor der Komponente. - Wir empfehlen, dass benutzerdefinierte Komponenten dasselbe Modell wie vorhandene ExoPlayer-Komponenten verwenden, damit sie während der Wiedergabe von der App neu konfiguriert werden können. Dazu sollten benutzerdefinierte Komponenten
PlayerMessage.Target
implementieren und Konfigurationsänderungen in der MethodehandleMessage
empfangen. Der Anwendungscode sollte Konfigurationsänderungen durch Aufrufen dercreateMessage
-Methode von ExoPlayer übergeben, die Nachricht konfigurieren und sie mitPlayerMessage.send
an die Komponente senden. Wenn Sie Nachrichten senden, die im Wiedergabethread zugestellt werden sollen, werden sie in der richtigen Reihenfolge mit allen anderen Vorgängen ausgeführt, die für den Player ausgeführt werden.