Anpassung

Das Herzstück der ExoPlayer-Bibliothek ist die Schnittstelle Player. Ein Player stellt herkömmliche allgemeine Mediaplayer-Funktionen bereit, z. B. die Möglichkeit, Medien zu puffern, abzuspielen, zu pausieren und zu suchen. Die Standardimplementierung ExoPlayer ist so konzipiert, dass sie nur wenige Annahmen über die Art der wiedergegebenen Medien, ihre Speicherung und Wiedergabe sowie ihren Speicherort trifft und daher nur wenige Einschränkungen auferlegt. Anstatt das Laden und Rendern von Medien direkt zu implementieren, delegieren ExoPlayer-Implementierungen diese Arbeit an Komponenten, die eingefügt werden, wenn ein Player erstellt wird oder wenn neue Medienquellen an den Player übergeben werden. Komponenten, die in allen ExoPlayer-Implementierungen vorkommen:

  • MediaSource-Instanzen, die die abzuspielenden 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 mit der Playlist API auf Basis von Medienquellen an den Player übergeben werden.
  • Eine MediaSource.Factory-Instanz, die einen MediaItem in einen MediaSource konvertiert. Der MediaSource.Factory wird eingefügt, wenn der Player erstellt wird.
  • Renderer-Instanzen, die einzelne Komponenten der Medien rendern. Diese werden beim Erstellen des Players eingefügt.
  • Ein TrackSelector, der von der MediaSource bereitgestellte Tracks auswählt, die von jedem verfügbaren Renderer verwendet werden sollen. Ein TrackSelector wird eingefügt, wenn der Player erstellt wird.
  • Ein LoadControl, das steuert, wann der MediaSource mehr Medien puffert und wie viele Medien gepuffert werden. Beim Erstellen des Players wird ein LoadControl eingefügt.
  • Ein LivePlaybackSpeedControl, der die Wiedergabegeschwindigkeit während der Live-Wiedergabe steuert, damit der Player nahe an einem konfigurierten Live-Offset bleibt. Ein LivePlaybackSpeedControl wird eingefügt, wenn der Player erstellt wird.

Das Konzept, Komponenten einzufügen, die Teile der Player-Funktionalität implementieren, ist in der gesamten Bibliothek vorhanden. Die Standardimplementierungen einiger Komponenten delegieren die Arbeit an weitere eingefügte Komponenten. So können viele Unterkomponenten einzeln durch Implementierungen ersetzt werden, die benutzerdefiniert konfiguriert sind.

Anpassung des Players

In den folgenden Abschnitten werden einige gängige Beispiele für die Anpassung des Players durch das Einfügen von Komponenten beschrieben.

Netzwerkstack konfigurieren

Wir haben eine Seite zum Anpassen des von ExoPlayer verwendeten Netzwerkstacks.

Aus dem Netzwerk geladene Daten im Cache speichern

Weitere Informationen finden Sie in den Leitfäden 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, die URIs der Anfragen ändern usw. Ihre App kann sich beispielsweise authentifizieren, indem sie beim Anfordern der Mediensegmente ein Token als Header einfügt.

Im folgenden Beispiel wird gezeigt, wie Sie diese Verhaltensweisen implementieren, indem Sie einen benutzerdefinierten DataSource.Factory in den 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 vorherigen 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 festgelegt.

Für einen detaillierteren Ansatz können Sie das Just-In-Time-Verhalten mithilfe eines 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 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 die Reaktion von ExoPlayer auf Ladefehler anpassen. Beispielsweise kann es sinnvoll sein, dass eine App schnell fehlschlägt, anstatt viele Wiederholungsversuche zu unternehmen, oder dass die Backoff-Logik angepasst wird, die steuert, wie lange der Player zwischen den einzelnen Wiederholungsversuchen wartet. Das folgende Snippet zeigt, wie Sie eine benutzerdefinierte Backoff-Logik implementieren:

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 Argument LoadErrorInfo enthält weitere Informationen zum fehlgeschlagenen Ladevorgang, um die Logik basierend auf dem Fehlertyp oder der fehlgeschlagenen Anfrage anzupassen.

Extraktor-Flags anpassen

Mit Extractor-Flags lässt sich anpassen, wie einzelne Formate aus progressiven Medien extrahiert werden. Sie können für das DefaultExtractorsFactory festgelegt werden, das für 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 kannst du die ungefähre Suche mit 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. Wenn du die Suche mit konstanter Bitrate für alle unterstützten Extraktoren aktivieren möchtest, verwende DefaultExtractorsFactory.setConstantBitrateSeekingEnabled.

Kotlin

val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)

Java

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

Die ExtractorsFactory kann dann mit DefaultMediaSourceFactory eingefügt werden, wie oben für das Anpassen von Extraktor-Flags beschrieben.

Asynchrones Einreihen von Puffern in die Warteschlange aktivieren

Die asynchrone Warteschlange ist eine Verbesserung der Rendering-Pipeline 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 sie aktivieren, können Sie die Anzahl der ausgelassenen Frames und Audio-Underruns reduzieren.

Die asynchrone Warteschlange ist auf Geräten mit Android 12 (API-Level 31) oder 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-Drops oder Audio-Underruns beobachtest, insbesondere bei der Wiedergabe von DRM-geschützten Inhalten oder Inhalten mit hoher Framerate.

Im einfachsten Fall müssen Sie einen DefaultRenderersFactory so in den Player einfü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 du Renderer direkt instanziierst, übergib 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 einbinden. Mit dieser Klasse können Sie bestimmte „Vorgänge“ abfangen, anstatt Player-Methoden direkt implementieren zu müssen. Dies sorgt für ein einheitliches Verhalten von z. B. play(), pause() und setPlayWhenReady(boolean). Außerdem wird sichergestellt, dass alle Statusänderungen korrekt an registrierte Player.Listener-Instanzen weitergegeben werden. Für die meisten Anpassungsanwendungsfälle sollte ForwardingSimpleBasePlayer aufgrund dieser Konsistenzgarantien dem fehleranfälligeren ForwardingPlayer vorgezogen werden.

So können Sie beispielsweise eine benutzerdefinierte Logik hinzufügen, 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 ein No-Op ist):

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

Anpassung von MediaSource

In den vorherigen Beispielen werden benutzerdefinierte Komponenten eingefügt, die während der Wiedergabe aller MediaItem-Objekte verwendet werden, die an den Player übergeben werden. Wenn eine detaillierte Anpassung erforderlich ist, können auch benutzerdefinierte Komponenten in einzelne MediaSource-Instanzen eingefügt werden, die direkt an den Player übergeben werden können. Im folgenden Beispiel wird gezeigt, wie Sie eine ProgressiveMediaSource so anpassen, dass sie eine benutzerdefinierte DataSource.Factory, ExtractorsFactory und LoadErrorHandlingPolicy verwendet:

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. Hier sind einige Anwendungsfälle für benutzerdefinierte Implementierungen:

  • Renderer: Du kannst einen benutzerdefinierten 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 die Art und Weise ä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 Pufferrichtlinie des Players ändern.
  • Extractor: Wenn du ein Containerformat unterstützen möchtest, das derzeit nicht von der Bibliothek unterstützt wird, kannst du eine benutzerdefinierte Extractor-Klasse implementieren.
  • MediaSource: Die Implementierung einer benutzerdefinierten MediaSource-Klasse kann sinnvoll sein, wenn Sie Medien-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, wie 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. Du kannst 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 nichtflüchtigen Cache.

Beim Erstellen benutzerdefinierter Komponenten empfehlen wir Folgendes:

  • Wenn eine benutzerdefinierte Komponente Ereignisse an die App melden muss, empfehlen wir, dasselbe Modell wie für vorhandene ExoPlayer-Komponenten zu verwenden, z. B. EventDispatcher-Klassen oder die Übergabe eines Handler zusammen mit einem Listener an den Konstruktor der Komponente.
  • Wir empfehlen, dass benutzerdefinierte Komponenten dasselbe Modell wie vorhandene ExoPlayer-Komponenten verwenden, damit die App während der Wiedergabe neu konfiguriert werden kann. Dazu sollten benutzerdefinierte Komponenten PlayerMessage.Target implementieren und Konfigurationsänderungen in der Methode handleMessage empfangen. Der Anwendungscode sollte Konfigurationsänderungen übergeben, indem er die Methode createMessage von ExoPlayer aufruft, die Nachricht konfiguriert und sie mit PlayerMessage.send an die Komponente sendet. Wenn du Nachrichten sendest, die im Wiedergabe-Thread zugestellt werden sollen, werden sie in der Reihenfolge mit allen anderen Vorgängen ausgeführt, die auf dem Player ausgeführt werden.