Anpassung

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 Arbeit an Komponenten, die eingeschleust werden, wenn ein Player erstellt oder neue Medienquellen an den Player übergeben werden. Komponenten, die allen ExoPlayer-Implementierungen gemeinsam sind:

  • MediaSource-Instanzen, die Medien für die Wiedergabe definieren, die Medien laden und von denen die geladenen Medien gelesen werden können. Eine MediaSource-Instanz wird von einem MediaSource.Factory im Player aus einer MediaItem erstellt. Sie können auch über die Media Source Based Playlist API direkt an den Player übergeben werden.
  • Eine MediaSource.Factory-Instanz, die eine MediaItem in eine MediaSource 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 der MediaSource bereitgestellte Titel auswählt, die von allen verfügbaren Renderer verwendet werden sollen. Beim Erstellen des Players wird TrackSelector eingefügt.
  • Ein LoadControl, das steuert, wann und wie viel Medien vom MediaSource im Puffer gespeichert werden. Beim Erstellen des Players wird LoadControl eingefügt.
  • Ein LivePlaybackSpeedControl, das die Wiedergabegeschwindigkeit bei der Livewiedergabe steuert, damit der Player nah an einem konfigurierten Live-Offset bleibt. Ein LivePlaybackSpeedControl wird beim Erstellen des Players eingefügt.

Das Konzept, Komponenten einzufügen, 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 sind einige gängige Beispiele für die Anpassung des Players durch das Einfügen von Komponenten beschrieben.

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 einer ResolvingDataSource ein Just-in-Time-Verhalten einschleusen. 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 eines benutzerdefinierten LoadErrorHandlingPolicy können Apps anpassen, wie ExoPlayer auf Ladefehler reagiert. Beispielsweise kann es sein, dass eine Anwendung schnell ausfallen soll, anstatt viele Versuche zu wiederholen, oder die Backoff-Logik anpassen möchte, 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 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 dem DefaultExtractorsFactory festgelegt werden, das 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();

Konstante Bitratensuche 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);

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

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 es kommt seltener zu Audioaussetzern.

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. Erwägen Sie, die Funktion für bestimmte Geräte zu aktivieren, auf denen Sie verworfene Frames oder Audiounterläufe beobachten, insbesondere bei der Wiedergabe von 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.

Methodenaufrufe mit ForwardingPlayer abfangen

Sie können einen Teil des Verhaltens einer Player-Instanz anpassen, indem Sie sie in eine abgeleitete Klasse von ForwardingPlayer einschließen und Methoden überschreiben, um Folgendes zu tun:

  • Rufe die Parameter auf, bevor du sie an den Delegaten Player weitergibst.
  • Rufen Sie den Rückgabewert aus dem Delegaten Player ab, bevor Sie ihn zurückgeben.
  • Implementieren Sie die Methode vollständig neu.

Wenn Sie ForwardingPlayer-Methoden überschreiben, ist es wichtig, dass die Implementierung einheitlich bleibt und der Player-Benutzeroberfläche entspricht. Das gilt insbesondere für Methoden, die dasselbe oder ein ähnliches Verhalten haben sollen. Beispiel:

  • Wenn Sie jeden „play“-Vorgang überschreiben möchten, müssen Sie sowohl ForwardingPlayer.play als auch ForwardingPlayer.setPlayWhenReady überschreiben, da ein Aufrufer davon ausgeht, dass das Verhalten dieser Methoden bei playWhenReady = true identisch ist.
  • Wenn du das Increment für die Vorwärtssuche ändern möchtest, musst du sowohl ForwardingPlayer.seekForward überschreiben, um eine Suche mit dem benutzerdefinierten Increment durchzuführen, als auch ForwardingPlayer.getSeekForwardIncrement, um den richtigen benutzerdefinierten Wert an den Aufrufer zurückzugeben.
  • Wenn du festlegen möchtest, welche Player.Commands von einer Playerinstanz beworben werden, musst du sowohl Player.getAvailableCommands() als auch Player.isCommandAvailable() überschreiben und 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 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 direkt an den Player übergeben werden können. Im folgenden Beispiel wird gezeigt, wie eine ProgressiveMediaSource so angepasst wird, dass benutzerdefinierte DataSource.Factory-, ExtractorsFactory- und LoadErrorHandlingPolicy-Elemente 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. Einige Anwendungsfälle für benutzerdefinierte Implementierungen:

  • Renderer: Du kannst eine benutzerdefinierte Renderer implementieren, um einen Medientyp zu verarbeiten, der von den Standardimplementierungen der Bibliothek nicht unterstützt wird.
  • TrackSelector: Mit einer benutzerdefinierten TrackSelector können App-Entwickler ändern, wie die von einem MediaSource freigegebenen Tracks für die Nutzung durch die einzelnen verfügbaren Renderers ausgewählt werden.
  • LoadControl: Mit einer benutzerdefinierten LoadControl 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 benutzerdefinierte Extractor-Klasse implementieren.
  • MediaSource: Die Implementierung einer benutzerdefinierten MediaSource-Klasse kann sinnvoll sein, wenn Sie Medienbeispiele erhalten möchten, die auf benutzerdefinierte Weise an Renderer gesendet werden, oder wenn Sie benutzerdefiniertes MediaSource-Compositing-Verhalten implementieren möchten.
  • MediaSource.Factory: Mit einer benutzerdefinierten MediaSource.Factory kann in einer Anwendung die Erstellung einer MediaSource aus einer MediaItem angepasst werden.
  • 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, 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 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 die App sie während der Wiedergabe neu konfigurieren kann. Dazu sollten benutzerdefinierte Komponenten PlayerMessage.Target implementieren und Konfigurationsänderungen in der Methode handleMessage erhalten. Der Anwendungscode sollte Konfigurationsänderungen übergeben, indem die createMessage-Methode von ExoPlayer aufgerufen, die Nachricht konfiguriert und mit PlayerMessage.send an die Komponente gesendet wird. Das Senden von Nachrichten, die im Wiedergabe-Thread zugestellt werden sollen, stellt sicher, dass sie in der richtigen Reihenfolge mit allen anderen Vorgängen auf dem Player ausgeführt werden.