Anpassung

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, abzuspielen, zu pausieren und zu suchen. Die Standardimplementierung ExoPlayer geht von wenigen Annahmen aus und unterliegt daher nur wenigen Einschränkungen in Bezug auf die Art der wiedergegebenen Medien, wie und wo sie gespeichert werden und wie sie gerendert werden. Anstatt das Laden und Rendern von Media direkt zu implementieren, delegieren ExoPlayer-Implementierungen diese Aufgabe an Komponenten, die eingefügt werden, wenn ein Player erstellt wird oder wenn dem Player neue Media-Quellen übergeben 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. Eine MediaSource-Instanz wird von einem MediaSource.Factory im Player aus einem MediaItem erstellt. Sie können auch direkt über die Playlist API für Media-Quellen an den Player übergeben werden.
  • Eine MediaSource.Factory-Instanz, die einen MediaItem in einen MediaSource konvertiert. Die MediaSource.Factory wird beim Erstellen des Players eingefügt.
  • Renderer-Instanzen, die einzelne Komponenten der Media rendern. Sie werden beim Erstellen des Players eingefügt.
  • Ein TrackSelector, das Tracks auswählt, die vom MediaSource für die Nutzung durch die einzelnen verfügbaren Renderer bereitgestellt werden. Ein TrackSelector wird eingefügt, wenn der Player erstellt wird.
  • Ein LoadControl, das steuert, wann und wie viele Medien vom MediaSource gepuffert werden. Ein LoadControl 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. Ein LivePlaybackSpeedControl 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 Media.

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 das 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 das Just-in-Time-Verhalten mit einem ResolvingDataSource einfügen. Das folgende Code-Snippet zeigt, wie Anfrageheader direkt 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 die Reaktion von ExoPlayer auf Ladefehler anpassen. 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: 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 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 Medien 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 Zwischenspeichern von Pufferinhalten 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

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 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

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.
}

MediaSource-Anpassung

In den obigen Beispielen werden benutzerdefinierte Komponenten eingefügt, die bei der Wiedergabe aller MediaItem-Objekte verwendet werden, 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, 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 benutzerdefinierte Renderer implementieren, um einen Medientyp zu verarbeiten, der von den Standardimplementierungen der Bibliothek nicht unterstützt wird.
  • TrackSelector: Durch die Implementierung eines benutzerdefinierten TrackSelector kann ein App-Entwickler ändern, wie von einem MediaSource bereitgestellte Tracks für die Nutzung durch die einzelnen verfügbaren Renderers ausgewählt werden.
  • LoadControl: Durch die Implementierung eines benutzerdefinierten LoadControl 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 benutzerdefinierte Extractor-Klasse implementieren.
  • MediaSource: Die Implementierung einer benutzerdefinierten MediaSource-Klasse kann sinnvoll sein, wenn Sie Media-Samples auf benutzerdefinierte Weise an Renderer übergeben oder ein benutzerdefiniertes MediaSource-Compositing-Verhalten implementieren möchten.
  • MediaSource.Factory: Durch die Implementierung eines benutzerdefinierten MediaSource.Factory kann eine Anwendung die Art und Weise anpassen, in der ein MediaSource aus einem MediaItem erstellt wird.
  • DataSource: Das Upstream-Paket von ExoPlayer enthält bereits eine Reihe von DataSource-Implementierungen für verschiedene Anwendungsfälle. Möglicherweise möchten Sie Ihre 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ückmelden muss, empfehlen wir, dass Sie dazu dasselbe Modell wie bei vorhandenen ExoPlayer-Komponenten verwenden, z. B. EventDispatcher-Klassen oder das Übergeben eines Handler 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 Methode handleMessage empfangen. Der Anwendungscode sollte Konfigurationsänderungen durch Aufrufen der createMessage-Methode von ExoPlayer übergeben, die Nachricht konfigurieren und sie mit PlayerMessage.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.