Anpassung

Den Kern der ExoPlayer-Bibliothek bildet die Player-Oberfläche. Ein Player bietet traditionelle allgemeine Mediaplayer-Funktionen wie die Möglichkeit, Medien zu puffern, abzuspielen, anzuhalten und zu suchen. Bei der Standardimplementierung ExoPlayer wird nur wenige Annahmen über die Art der Medien, die wiedergegeben werden, sowie darauf, wie und wo sie gespeichert und wie sie gerendert werden, vorgenommen (damit sind nur wenige Einschränkungen erforderlich). Anstatt das Laden und Rendern von Medien direkt zu implementieren, delegieren ExoPlayer-Implementierungen diese Arbeit an Komponenten, die beim Erstellen eines Players oder der Weitergabe neuer Medienquellen an den Player eingeschleust werden. Die folgenden Komponenten sind in allen ExoPlayer-Implementierungen gemein:

  • MediaSource-Instanzen, die die Medien definieren, die wiedergegeben werden sollen, die Medien laden und von denen die geladenen Medien gelesen werden können. Eine MediaSource-Instanz wird aus einem MediaItem von einem MediaSource.Factory im Player erstellt. Sie können auch mit der medienquellenbasierten Playlist API direkt an den Player übergeben werden.
  • Eine MediaSource.Factory-Instanz, die einen MediaItem in einen MediaSource konvertiert. MediaSource.Factory wird beim Erstellen des Players eingeschleust.
  • Renderer-Instanzen, die einzelne Komponenten der Medien rendern. Diese werden beim Erstellen des Players eingeschleust.
  • Ein TrackSelector, der von der MediaSource bereitgestellte Tracks auswählt, die von jedem verfügbaren Renderer genutzt werden sollen. Ein TrackSelector wird beim Erstellen des Players eingefügt.
  • Ein LoadControl, der steuert, wann MediaSource weitere Medien zwischenspeichert und wie viele Medien zwischengespeichert werden. Ein LoadControl wird beim Erstellen des Players eingeschleust.
  • Ein LivePlaybackSpeedControl, das die Wiedergabegeschwindigkeit bei Live-Wiedergaben steuert, damit der Player nah an einem konfigurierten Live-Offset bleiben kann. Ein LivePlaybackSpeedControl wird beim Erstellen des Players eingeschleust.

Das Konzept des Einfügens von Komponenten, die einzelne Playerfunktionen implementieren, ist überall in der Bibliothek zu finden. Bei den Standardimplementierungen einiger Komponenten wird die Arbeit an weitere eingeschleuste Komponenten delegieren. Dadurch können viele Unterkomponenten einzeln durch individuell konfigurierte Implementierungen ersetzt werden.

Player-Anpassung

Im Folgenden findest du einige allgemeine Beispiele für das Anpassen des Players durch das Einfügen von Komponenten.

Netzwerkstack konfigurieren

Auf unserer Seite erfahren Sie, wie Sie den von ExoPlayer verwendeten Netzwerk-Stack anpassen.

Aus dem Netzwerk geladene Daten im Cache speichern

Weitere Informationen finden Sie in den Anleitungen zum temporären Caching und zum Herunterladen von Medien.

Serverinteraktionen anpassen

Einige Apps möchten möglicherweise HTTP-Anfragen und -Antworten abfangen. Vielleicht möchten Sie benutzerdefinierte Anfrageheader einfügen, die Antwortheader des Servers lesen, die URIs der Anfrage ändern usw. Ihre App kann sich beispielsweise authentifizieren, indem sie beim Anfordern der Mediensegmente ein Token als Header einschleust.

Im folgenden Beispiel wird gezeigt, wie diese Verhaltensweisen implementiert werden, indem ein benutzerdefiniertes DataSource.Factory in das DefaultMediaSourceFactory-Element injiziert 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 obigen Code-Snippet enthält die eingefügte HttpDataSource den Header "Header: Value" in jeder HTTP-Anfrage. Dieses Verhalten wird bei jeder Interaktion mit einer HTTP-Quelle behoben.

Für einen detaillierteren Ansatz können Sie das Just-in-Time-Verhalten mit einer 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 bedarfsorientierte Ä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 anpassen, wie ExoPlayer auf Ladefehler reagiert. Beispielsweise kann es sein, dass eine Anwendung schnell ausfallen möchte, anstatt es mehrfach zu versuchen, oder die Backoff-Logik anpassen, die steuert, wie lange der Spieler zwischen den einzelnen Wiederholungen 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 Argument LoadErrorInfo enthält weitere Informationen zum fehlgeschlagenen Ladevorgang, um die Logik anhand des Fehlertyps oder der fehlgeschlagenen Anfrage anzupassen.

Extrahierer-Flags anpassen

Mit Extrahierer-Flags können Sie anpassen, wie einzelne Formate aus Progressive Media extrahiert werden. Sie können für das DefaultExtractorsFactory festgelegt werden, das der DefaultMediaSourceFactory zur Verfügung gestellt wird. Im folgenden Beispiel wird ein Flag übergeben, das die indexbasierte Suche nach MP3-Streams ermöglicht.

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 nach konstanter Bitrate aktivieren

Für MP3-, ADTS- und AMR-Streams können Sie die ungefähre Suche unter Verwendung 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. Verwenden Sie DefaultExtractorsFactory.setConstantBitrateSeekingEnabled, um die Suche mit konstanter Bitrate für alle Extraktoren zu aktivieren, die sie unterstützen.

Kotlin

val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)

Java

DefaultExtractorsFactory extractorsFactory =
    new DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true);

Die ExtractorsFactory kann dann über DefaultMediaSourceFactory eingeschleust werden, wie oben zum Anpassen der Extrahierer-Flags beschrieben.

Asynchrone Zwischenspeicherung von Warteschlange aktivieren

Die asynchrone Zwischenspeicherwarteschlange ist eine Verbesserung in der Renderingpipeline 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 diese Option aktivieren, können ausgelassene Frames und Audiountergänge reduziert werden.

Auf Geräten mit Android 12 (API-Level 31) und höher ist die asynchrone Zwischenspeicherwarteschlange standardmäßig aktiviert. Ab Android 6.0 (API-Ebene 23) kann sie manuell aktiviert werden. Du kannst die Funktion für bestimmte Geräte aktivieren, auf denen du abgebrochene Frames oder Audiountergänge feststellst, insbesondere wenn du DRM-geschützte Inhalte oder Inhalte mit hoher Framerate abspielst.

Im einfachsten Fall müssen Sie DefaultRenderersFactory so in den Player einschleusen:

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 einen AsynchronousMediaCodecAdapter.Factory an die Konstruktoren MediaCodecVideoRenderer und MediaCodecAudioRenderer.

Methodenaufrufe mit ForwardingPlayer abfangen

Sie können einen Teil des Verhaltens einer Player-Instanz anpassen, indem Sie sie in eine abgeleitete Klasse von ForwardingPlayer einbinden und Methoden überschreiben, um eine der folgenden Aktionen auszuführen:

  • Rufen Sie die Parameter auf, bevor Sie sie an den Delegat Player übergeben.
  • Greifen Sie vor der Rückgabe des Delegaten Player auf den Rückgabewert zu.
  • Implementieren Sie die Methode vollständig noch einmal.

Beim Überschreiben von ForwardingPlayer-Methoden muss dafür gesorgt werden, dass die Implementierung selbstkonsistent und mit der Player-Schnittstelle konform bleibt. Das gilt insbesondere, wenn es sich um Methoden handelt, die ein identisches oder ähnliches Verhalten haben sollen. Beispiele:

  • Wenn Sie jeden "play"-Vorgang überschreiben möchten, müssen Sie sowohl ForwardingPlayer.play als auch ForwardingPlayer.setPlayWhenReady überschreiben, da ein Aufrufer erwartet, dass das Verhalten dieser Methoden in playWhenReady = true identisch ist.
  • Wenn Sie das Vorwärts-Inkrement ändern möchten, müssen Sie ForwardingPlayer.seekForward überschreiben, um eine Suche mit Ihrem benutzerdefinierten Inkrement durchzuführen, und ForwardingPlayer.getSeekForwardIncrement, um das richtige benutzerdefinierte Inkrement an den Aufrufer zurückzumelden.
  • Wenn Sie steuern möchten, welche Player.Commands von einer Spielerinstanz angeboten werden, müssen Sie sowohl Player.getAvailableCommands() als auch Player.isCommandAvailable() überschreiben und auch den Player.Listener.onAvailableCommandsChanged()-Callback überwachen, um über Änderungen informiert zu werden, die vom zugrunde liegenden Player stammen.

MediaSource-Anpassung

In den Beispielen oben werden benutzerdefinierte Komponenten zur Verwendung während der Wiedergabe aller MediaItem-Objekte eingefügt, die an den Player übergeben werden. Wenn eine präzise Anpassung erforderlich ist, ist es auch möglich, benutzerdefinierte Komponenten in einzelne MediaSource-Instanzen einzufügen, die direkt an den Player übergeben werden können. Das folgende Beispiel zeigt, wie Sie ein ProgressiveMediaSource anpassen, um einen benutzerdefinierten 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 enthält Standardimplementierungen der oben auf dieser Seite aufgeführten Komponenten für häufige Anwendungsfälle. Ein ExoPlayer kann diese Komponenten verwenden, kann aber auch für benutzerdefinierte Implementierungen erstellt werden, wenn ein nicht standardmäßiges Verhalten erforderlich ist. Einige Anwendungsfälle für benutzerdefinierte Implementierungen sind:

  • Renderer: Sie können einen benutzerdefinierten Renderer für einen Medientyp implementieren, der von den Standardimplementierungen der Bibliothek nicht unterstützt wird.
  • TrackSelector: Die Implementierung eines benutzerdefinierten TrackSelector ermöglicht es einem App-Entwickler, die Art und Weise zu ändern, wie von einem MediaSource bereitgestellte Tracks für die Nutzung mit jedem der verfügbaren Renderers ausgewählt werden.
  • LoadControl: Die Implementierung eines benutzerdefinierten LoadControl ermöglicht einem App-Entwickler, die Pufferrichtlinie des Players zu ändern.
  • Extractor: Wenn Sie ein Containerformat unterstützen müssen, das derzeit nicht von der Bibliothek unterstützt wird, sollten Sie eine benutzerdefinierte Extractor-Klasse implementieren.
  • MediaSource: Die Implementierung einer benutzerdefinierten MediaSource-Klasse kann sinnvoll sein, wenn Sie Medienbeispiele auf benutzerdefinierte Weise an Renderer senden oder ein benutzerdefiniertes MediaSource-Compositing-Verhalten implementieren möchten.
  • MediaSource.Factory: Durch die Implementierung eines benutzerdefinierten MediaSource.Factory kann eine Anwendung anpassen, wie ein MediaSource aus einer MediaItem erstellt wird.
  • DataSource: Das Upstream-Paket von ExoPlayer enthält bereits eine Reihe von DataSource-Implementierungen für verschiedene Anwendungsfälle. Sie können eine eigene DataSource-Klasse implementieren, um Daten auf andere Weise zu laden, z. B. über ein benutzerdefiniertes Protokoll, einen benutzerdefinierten HTTP-Stack oder einen benutzerdefinierten nichtflüchtigen Cache.

Für das Erstellen benutzerdefinierter Komponenten empfehlen wir Folgendes:

  • Wenn eine benutzerdefinierte Komponente Ereignisse an die App zurückmelden muss, empfehlen wir, dafür dasselbe Modell wie bei den vorhandenen ExoPlayer-Komponenten zu verwenden. Sie können beispielsweise EventDispatcher-Klassen verwenden oder einen Handler 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 eine Neukonfiguration durch die App während der Wiedergabe möglich ist. Dazu sollten benutzerdefinierte Komponenten PlayerMessage.Target implementieren und Konfigurationsänderungen in der Methode handleMessage erhalten. Der Anwendungscode sollte Konfigurationsänderungen übergeben. Dazu ruft er die Methode createMessage von ExoPlayer auf, konfiguriert die Nachricht und sendet sie mithilfe von PlayerMessage.send an die Komponente. Durch das Senden von Nachrichten, die im Wiedergabe-Thread übermittelt werden sollen, wird sichergestellt, dass sie in der richtigen Reihenfolge mit allen anderen Vorgängen ausgeführt werden, die auf dem Player ausgeführt werden.