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, wiederzugeben, zu pausieren und zu suchen. Die Standardimplementierung ExoPlayer geht von wenigen Annahmen über die Art der wiedergegebenen Medien, die Art und den Ort ihrer Speicherung sowie die Art ihrer Darstellung aus und unterliegt daher nur wenigen Einschränkungen. Anstatt das Laden und Rendern von Media direkt zu implementieren, delegieren ExoPlayer-Implementierungen diese Aufgabe an Komponenten, die beim Erstellen eines Players oder beim Übergeben neuer Media-Quellen an den Player eingefügt 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 MediaItem durch ein MediaSource.Factory im Player erstellt. Sie können auch direkt über die Playlist API basierend auf der Media-Quelle an den Player übergeben werden.
  • Eine MediaSource.Factory-Instanz, die ein MediaItem in ein 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.
  • Eine TrackSelector, mit der Titel ausgewählt werden, die von der MediaSource bereitgestellt werden und von jeder verfügbaren Renderer verwendet werden können. Ein TrackSelector wird eingefügt, wenn der Player erstellt wird.
  • Ein LoadControl, der steuert, wann MediaSource mehr Medien puffert und wie viele Medien 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 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 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 der 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 Just-in-Time-Verhalten mit einem ResolvingDataSource 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 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 anpassen, wie ExoPlayer auf Ladefehler reagiert. 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: 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 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 Media 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 Einreihen von Puffern 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

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

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 für die Wiedergabe aller MediaItem-Objekte eingefügt, 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, kann 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.