Das Herzstück der ExoPlayer-Bibliothek ist die Schnittstelle Player. Ein Player stellt herkömmliche allgemeine Mediaplayer-Funktionen bereit, z. B. die Möglichkeit, Medien zu puffern, abzuspielen, zu pausieren und zu suchen. Die Standardimplementierung ExoPlayer ist so konzipiert, dass sie nur wenige Annahmen über die Art der wiedergegebenen Medien, ihre Speicherung und Wiedergabe sowie ihren Speicherort trifft und daher nur wenige Einschränkungen auferlegt. Anstatt das Laden und Rendern von Medien direkt zu implementieren, delegieren ExoPlayer-Implementierungen diese Arbeit an Komponenten, die eingefügt werden, wenn ein Player erstellt wird oder wenn neue Medienquellen an den Player übergeben werden.
Komponenten, die in allen ExoPlayer-Implementierungen vorkommen:
MediaSource-Instanzen, die die abzuspielenden Medien definieren, die Medien laden und aus denen die geladenen Medien gelesen werden können. EineMediaSource-Instanz wird von einemMediaSource.Factoryim Player aus einemMediaItemerstellt. Sie können auch direkt mit der Playlist API auf Basis von Medienquellen an den Player übergeben werden.- Eine
MediaSource.Factory-Instanz, die einenMediaItemin einenMediaSourcekonvertiert. DerMediaSource.Factorywird eingefügt, wenn der Player erstellt wird. Renderer-Instanzen, die einzelne Komponenten der Medien rendern. Diese werden beim Erstellen des Players eingefügt.- Ein
TrackSelector, der von derMediaSourcebereitgestellte Tracks auswählt, die von jedem verfügbarenRendererverwendet werden sollen. EinTrackSelectorwird eingefügt, wenn der Player erstellt wird. - Ein
LoadControl, das steuert, wann derMediaSourcemehr Medien puffert und wie viele Medien gepuffert werden. Beim Erstellen des Players wird einLoadControleingefügt. - Ein
LivePlaybackSpeedControl, der die Wiedergabegeschwindigkeit während der Live-Wiedergabe steuert, damit der Player nahe an einem konfigurierten Live-Offset bleibt. EinLivePlaybackSpeedControlwird eingefügt, wenn der Player erstellt wird.
Das Konzept, Komponenten einzufügen, die Teile der Player-Funktionalität implementieren, ist in der gesamten Bibliothek vorhanden. Die Standardimplementierungen einiger Komponenten delegieren die Arbeit an weitere eingefügte Komponenten. So können viele Unterkomponenten einzeln durch Implementierungen ersetzt werden, die benutzerdefiniert konfiguriert sind.
Anpassung des Players
In den folgenden Abschnitten werden einige gängige Beispiele für die Anpassung des Players durch das Einfügen von Komponenten beschrieben.
Netzwerkstack konfigurieren
Wir haben eine Seite zum Anpassen des von ExoPlayer verwendeten Netzwerkstacks.
Aus dem Netzwerk geladene Daten im Cache speichern
Weitere Informationen finden Sie in den Leitfäden 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, die URIs der Anfragen ändern usw. Ihre App kann sich beispielsweise authentifizieren, indem sie beim Anfordern der Mediensegmente ein Token als Header einfügt.
Im folgenden Beispiel wird gezeigt, wie Sie diese Verhaltensweisen implementieren, indem Sie einen benutzerdefinierten DataSource.Factory in den 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 vorherigen 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 festgelegt.
Für einen detaillierteren Ansatz können Sie das Just-In-Time-Verhalten mithilfe eines ResolvingDataSource einfügen. Das folgende Code-Snippet zeigt, wie Anfrageheader unmittelbar 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 eines benutzerdefinierten LoadErrorHandlingPolicy können Apps die Reaktion von ExoPlayer auf Ladefehler anpassen. Beispielsweise kann es sinnvoll sein, dass eine App schnell fehlschlägt, anstatt viele Wiederholungsversuche zu unternehmen, oder dass die Backoff-Logik angepasst wird, die steuert, wie lange der Player zwischen den einzelnen Wiederholungsversuchen wartet. Das folgende Snippet zeigt, wie Sie eine benutzerdefinierte Backoff-Logik implementieren:
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();
Das Argument LoadErrorInfo enthält weitere Informationen zum fehlgeschlagenen Ladevorgang, um die Logik basierend auf dem Fehlertyp oder der fehlgeschlagenen Anfrage anzupassen.
Extraktor-Flags anpassen
Mit Extractor-Flags lässt sich anpassen, wie einzelne Formate aus progressiven Medien extrahiert werden. Sie können für das DefaultExtractorsFactory festgelegt werden, das für 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 kannst du die ungefähre Suche mit 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 festgelegt werden, wie oben beschrieben. Wenn du die Suche mit konstanter Bitrate für alle unterstützten Extraktoren aktivieren möchtest, verwende DefaultExtractorsFactory.setConstantBitrateSeekingEnabled.
Kotlin
val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)
Java
DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);
Die ExtractorsFactory kann dann mit DefaultMediaSourceFactory eingefügt werden, wie oben für das Anpassen von Extraktor-Flags beschrieben.
Asynchrones Einreihen von Puffern in die Warteschlange aktivieren
Die asynchrone Warteschlange ist eine Verbesserung der Rendering-Pipeline von ExoPlayer, die MediaCodec-Instanzen im asynchronen Modus ausführt und zusätzliche Threads verwendet, um die Decodierung und das Rendering von Daten zu planen. Wenn Sie sie aktivieren, können Sie die Anzahl der ausgelassenen Frames und Audio-Underruns reduzieren.
Die asynchrone Warteschlange ist auf Geräten mit Android 12 (API-Level 31) oder 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-Drops oder Audio-Underruns beobachtest, insbesondere bei der Wiedergabe von DRM-geschützten Inhalten oder Inhalten mit hoher Framerate.
Im einfachsten Fall müssen Sie einen DefaultRenderersFactory so in den Player einfü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 du Renderer direkt instanziierst, übergib 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 einbinden. Mit dieser Klasse können Sie bestimmte „Vorgänge“ abfangen, anstatt Player-Methoden direkt implementieren zu müssen. Dies sorgt für ein einheitliches Verhalten von z. B. play(), pause() und setPlayWhenReady(boolean). Außerdem wird sichergestellt, dass alle Statusänderungen korrekt an registrierte Player.Listener-Instanzen weitergegeben werden. Für die meisten Anpassungsanwendungsfälle sollte ForwardingSimpleBasePlayer aufgrund dieser Konsistenzgarantien dem fehleranfälligeren ForwardingPlayer vorgezogen werden.
So können Sie beispielsweise eine benutzerdefinierte Logik hinzufügen, 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
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); } }
Oder um den Befehl SEEK_TO_NEXT zu verbieten (und dafür zu sorgen, dass Player.seekToNext ein No-Op ist):
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. }
Anpassung von MediaSource
In den vorherigen Beispielen werden benutzerdefinierte Komponenten eingefügt, die während der Wiedergabe aller MediaItem-Objekte verwendet werden, die an den Player übergeben werden. Wenn eine detaillierte Anpassung erforderlich ist, können auch benutzerdefinierte Komponenten in einzelne MediaSource-Instanzen eingefügt werden, die direkt an den Player übergeben werden können. Im folgenden Beispiel wird gezeigt, wie Sie eine ProgressiveMediaSource so anpassen, dass sie eine benutzerdefinierte DataSource.Factory, ExtractorsFactory und LoadErrorHandlingPolicy verwendet:
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 so erstellt werden, dass benutzerdefinierte Implementierungen verwendet werden, wenn nicht standardmäßige Verhaltensweisen erforderlich sind. Hier sind einige Anwendungsfälle für benutzerdefinierte Implementierungen:
Renderer: Du kannst einen benutzerdefiniertenRendererimplementieren, um einen Medientyp zu verarbeiten, der von den Standardimplementierungen der Bibliothek nicht unterstützt wird.TrackSelector: Durch die Implementierung eines benutzerdefiniertenTrackSelectorkann ein App-Entwickler die Art und Weise ändern, wie von einemMediaSourcebereitgestellte Tracks für die Nutzung durch die einzelnen verfügbarenRenderers ausgewählt werden.LoadControl: Durch die Implementierung eines benutzerdefiniertenLoadControlkann ein App-Entwickler die Pufferrichtlinie des Players ändern.Extractor: Wenn du ein Containerformat unterstützen möchtest, das derzeit nicht von der Bibliothek unterstützt wird, kannst du eine benutzerdefinierteExtractor-Klasse implementieren.MediaSource: Die Implementierung einer benutzerdefiniertenMediaSource-Klasse kann sinnvoll sein, wenn Sie Medien-Samples auf benutzerdefinierte Weise an Renderer übergeben oder ein benutzerdefiniertesMediaSource-Compositing-Verhalten implementieren möchten.MediaSource.Factory: Durch die Implementierung eines benutzerdefiniertenMediaSource.Factorykann eine Anwendung die Art und Weise anpassen, wie einMediaSourceaus einemMediaItemerstellt wird.DataSource: Das Upstream-Paket von ExoPlayer enthält bereits eine Reihe vonDataSource-Implementierungen für verschiedene Anwendungsfälle. Du kannst 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 nichtflüchtigen Cache.
Beim Erstellen benutzerdefinierter Komponenten empfehlen wir Folgendes:
- Wenn eine benutzerdefinierte Komponente Ereignisse an die App melden muss, empfehlen wir, dasselbe Modell wie für vorhandene ExoPlayer-Komponenten zu verwenden, z. B.
EventDispatcher-Klassen oder die Übergabe einesHandlerzusammen mit einem Listener an den Konstruktor der Komponente. - Wir empfehlen, dass benutzerdefinierte Komponenten dasselbe Modell wie vorhandene ExoPlayer-Komponenten verwenden, damit die App während der Wiedergabe neu konfiguriert werden kann. Dazu sollten benutzerdefinierte Komponenten
PlayerMessage.Targetimplementieren und Konfigurationsänderungen in der MethodehandleMessageempfangen. Der Anwendungscode sollte Konfigurationsänderungen übergeben, indem er die MethodecreateMessagevon ExoPlayer aufruft, die Nachricht konfiguriert und sie mitPlayerMessage.sendan die Komponente sendet. Wenn du Nachrichten sendest, die im Wiedergabe-Thread zugestellt werden sollen, werden sie in der Reihenfolge mit allen anderen Vorgängen ausgeführt, die auf dem Player ausgeführt werden.