自訂

ExoPlayer 程式庫的核心是 Player 介面。Player 會公開傳統的高階媒體播放器功能,例如緩衝處理媒體、播放、暫停和跳轉。預設實作 ExoPlayer 的設計,對播放的媒體類型、儲存方式和位置,以及轉譯方式,幾乎沒有假設 (因此幾乎沒有限制)。ExoPlayer 實作項目不會直接載入及算繪媒體,而是將這項工作委派給在建立播放器或將新媒體來源傳遞至播放器時插入的元件。所有 ExoPlayer 實作項目通用的元件包括:

  • MediaSource 執行個體,用於定義要播放的媒體、載入媒體,以及讀取載入的媒體。MediaSource 執行個體是由播放器內的 MediaSource.Factory 從 MediaItem 建立。也可以使用媒體來源播放清單 API,直接傳遞至播放器。
  • MediaSource.Factory 例項,可將 MediaItem 轉換為 MediaSource。系統會在建立播放器時插入 MediaSource.Factory。
  • Renderer 執行個體,用於轉譯媒體的個別元件。這些會在建立播放器時插入。
  • TrackSelector,用於選取 MediaSource 提供的曲目,供每個可用的 Renderer 使用。建立播放器時,系統會插入 TrackSelector。
  • LoadControl,用於控制 MediaSource 緩衝處理更多媒體的時間,以及緩衝處理的媒體量。建立播放器時,系統會插入 LoadControl。
  • LivePlaybackSpeedControl,用於控制直播播放期間的播放速度,讓播放器盡可能接近設定的直播偏移量。建立播放器時,系統會插入 LivePlaybackSpeedControl。

整個程式庫都採用插入元件的概念,用來實作播放器功能。部分元件的預設實作會將工作委派給進一步插入的元件。這樣一來,許多子元件就能個別替換為以自訂方式設定的實作項目。

自訂播放器

以下各節將說明一些常見的例子,介紹如何透過插入元件來自訂播放器。

設定網路堆疊

我們有一頁說明如何自訂 ExoPlayer 使用的網路堆疊。

快取從網路載入的資料

請參閱暫時即時快取和下載媒體指南。

自訂伺服器互動

部分應用程式可能會想攔截 HTTP 要求和回應。您可能需要插入自訂要求標頭、讀取伺服器的回應標頭、修改要求的 URI 等。舉例來說,應用程式在要求媒體片段時,可能會插入權杖做為標頭,藉此驗證自身身分。

以下範例示範如何將自訂 DataSource.Factory 插入 DefaultMediaSourceFactory,以實作這些行為:

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();

在上述程式碼片段中,插入的 HttpDataSource 會在每個 HTTP 要求中加入 "Header: Value" 標頭。每次與 HTTP 來源互動時,這項行為都是固定的。

如要採用更精細的做法,可以使用 ResolvingDataSource 插入即時行為。下列程式碼片段說明如何在與 HTTP 來源互動前,插入要求標頭:

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

您也可以使用 ResolvingDataSource 即時修改 URI,如以下程式碼片段所示:

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

自訂錯誤處理

實作自訂 LoadErrorHandlingPolicy 可讓應用程式自訂 ExoPlayer 對載入錯誤的反應方式。舉例來說,應用程式可能想快速失敗,而不是多次重試,或想自訂輪詢邏輯,控制播放器在每次重試之間的等待時間。下列程式碼片段說明如何實作自訂輪詢邏輯:

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();

LoadErrorInfo 引數包含更多載入失敗的相關資訊,可根據錯誤類型或失敗的要求自訂邏輯。

自訂擷取器標記

擷取器旗標可用來自訂從漸進式媒體擷取個別格式的方式。您可以在提供給 DefaultMediaSourceFactory 的 DefaultExtractorsFactory 上設定這些屬性。以下範例會傳遞標記,為 MP3 串流啟用索引式跳轉功能。

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();

啟用固定位元率搜尋

如果是 MP3、ADTS 和 AMR 串流,您可以使用 FLAG_ENABLE_CONSTANT_BITRATE_SEEKING 旗標,透過固定位元率假設啟用近似跳轉功能。您可以使用前述的個別 DefaultExtractorsFactory.setXyzExtractorFlags 方法,為個別擷取器設定這些標記。如要為所有支援的擷取器啟用固定位元率跳轉功能,請使用 DefaultExtractorsFactory.setConstantBitrateSeekingEnabled。

Kotlin

val extractorsFactory = DefaultExtractorsFactory().setConstantBitrateSeekingEnabled(true)

Java

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

接著,您可以按照先前自訂擷取器旗標的說明,使用 DefaultMediaSourceFactory 插入 ExtractorsFactory。

啟用非同步緩衝區佇列

非同步緩衝區佇列是 ExoPlayer 轉譯管道中的一項強化功能,可在「非同步模式」下操作 MediaCodec 執行個體,並使用額外的執行緒排定資料解碼和轉譯作業。啟用這項功能可減少影格遺失和音訊欠載。

搭載 Android 12 (API 級別 31) 以上版本的裝置預設會啟用非同步緩衝區佇列,Android 6.0 (API 級別 23) 以上版本則可手動啟用。如果發現特定裝置有影格遺失或音訊不足的問題,建議啟用這項功能,特別是在播放受 DRM 保護或高影格率內容時。

在最簡單的情況下,您需要將 DefaultRenderersFactory 插入播放器,如下所示:

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();

如果您直接例項化轉譯器,請將 new DefaultMediaCodecAdapter.Factory(context).forceEnableAsynchronous() 傳遞至 MediaCodecVideoRenderer 和 MediaCodecAudioRenderer 建構函式。

使用 ForwardingSimpleBasePlayer 自訂作業

您可以將 Player 執行個體包裝在 ForwardingSimpleBasePlayer 的子類別中,藉此自訂部分行為。這個類別可讓您攔截特定「作業」,而不必直接實作 Player 方法。這可確保 play()、pause() 和 setPlayWhenReady(boolean) 等項目的行為一致。此外,這項機制也能確保所有狀態變更都能正確傳播至已註冊的 Player.Listener 執行個體。在大多數自訂用途中,ForwardingSimpleBasePlayer 應優先於更容易出錯的 ForwardingPlayer,因為前者可保證一致性。

舉例來說,如要在播放開始或停止時新增自訂邏輯,請按照下列步驟操作:

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);
  }
}

或禁止 SEEK_TO_NEXT 指令 (並確保 Player.seekToNext 為無作業):

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

上述範例會插入自訂元件,用於播放傳遞至播放器的所有 MediaItem 物件。如需精細自訂,也可以將自訂元件插入個別 MediaSource 執行個體,並直接傳遞至播放器。以下範例說明如何自訂 ProgressiveMediaSource,以使用自訂 DataSource.Factory、ExtractorsFactory 和 LoadErrorHandlingPolicy:

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

建立自訂元件

這個程式庫提供本頁頂端所列元件的預設實作,適用於常見用途。ExoPlayer 可以使用這些元件,但如果需要非標準行為,也可以建構自訂實作。自訂導入方式的用途包括:

  • Renderer:您可能需要導入自訂 Renderer,處理程式庫提供的預設導入項目不支援的媒體類型。
  • TrackSelector:實作自訂 TrackSelector 可讓應用程式開發人員變更 MediaSource 公開的曲目選取方式,以便每個可用的 Renderer 使用。
  • LoadControl:實作自訂 LoadControl 可讓應用程式開發人員變更播放器的緩衝政策。
  • Extractor:如需支援程式庫目前不支援的容器格式,建議您實作自訂 Extractor 類別。
  • MediaSource:如要以自訂方式取得媒體樣本並提供給轉譯器,或想實作自訂 MediaSource 複合行為,建議實作自訂 MediaSource 類別。
  • MediaSource.Factory:實作自訂 MediaSource.Factory 可讓應用程式自訂從 MediaItem 建立 MediaSource 的方式。
  • DataSource:ExoPlayer 的上游套件已包含多個適用於不同用途的 DataSource 實作。您可能需要實作自己的 DataSource 類別,以其他方式載入資料,例如透過自訂通訊協定、使用自訂 HTTP 堆疊,或從自訂永久快取載入。

建構自訂元件時,建議您採取下列做法:

  • 如果自訂元件需要向應用程式回報事件,建議您使用與現有 ExoPlayer 元件相同的模型,例如使用 EventDispatcher 類別,或將 Handler 與事件監聽器一併傳遞至元件的建構函式。
  • 建議自訂元件使用與現有 ExoPlayer 元件相同的模型,以便應用程式在播放期間重新設定。為此,自訂元件應實作 PlayerMessage.Target,並在 handleMessage 方法中接收設定變更。應用程式程式碼應呼叫 ExoPlayer 的 createMessage 方法、設定訊息,並使用 PlayerMessage.send 將訊息傳送至元件,藉此傳遞設定變更。傳送要在播放執行緒上傳送的訊息,可確保訊息會與在播放器上執行的任何其他作業依序執行。