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 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. 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 ein MediaItem in ein 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. Ein TrackSelector wird beim Erstellen des Players eingefügt.
  • Ein LoadControl, das steuert, wann und wie viel Medien vom MediaSource im Puffer gespeichert werden. Ein LoadControl 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. Ein LivePlaybackSpeedControl 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 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 müssen benutzerdefinierte Komponenten PlayerMessage.Target implementieren und Konfigurationsänderungen über die Methode handleMessage empfangen. 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. 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.