使用 MediaSession 控制及放送廣告

媒體工作階段提供了一種與音訊或影片播放器互動的通用方式。在 Media3 中,預設播放器是實作 Player 介面的 ExoPlayer 類別。將媒體工作階段連結至播放器,可讓應用程式對外宣傳媒體播放,並從外部來源接收播放指令。

指令可能源自實體按鈕,例如耳機或電視遙控器上的播放按鈕。也可能來自具有媒體控制器的用戶端應用程式,例如向 Google 助理下達指示「暫停」。媒體工作階段會將這些指令委派給媒體應用程式的播放器。

選擇媒體工作階段的時機

實作 MediaSession 後,使用者就能控製播放功能:

  • 透過耳機。使用者通常會在耳機上執行按鈕或觸控互動,以播放或暫停媒體,或前往下一首或上一首曲目。
  • Google 助理交談。常見的模式是說出「Ok Google,暫停」,暫停裝置上目前正在播放的任何媒體。
  • 使用 Wear OS 手錶。這樣一來,在手機上播放內容時,就能輕鬆存取最常用的播放控制項。
  • 透過媒體控制項。這個輪轉介面顯示每個執行中媒體工作階段的控制項。
  • 使用電視。允許具有實體播放按鈕、平台播放控制項和電源管理動作執行。舉例來說,如果電視、單件式環繞劇院或影音接收器關閉,或輸入來源遭到切換,應用程式應會停止播放。
  • 以及需要影響播放的任何其他外部程序。

這在許多用途上都很適合。請特別注意,在以下情況時,我們強烈建議您考慮使用 MediaSession

  • 您串流播放的是長篇影片內容,例如電影或電視直播。
  • 您串流播放的是長篇音訊內容,例如 Podcast 或音樂播放清單。
  • 您正在建構 TV 應用程式

不過,MediaSession 並非所有用途都適合使用。在下列情況下,建議您只使用 Player

  • 您正在展示短篇內容,這類內容的使用者互動和互動非常重要。
  • 沒有單一作用中的影片 (例如使用者捲動瀏覽清單,且螢幕同時顯示多部影片)。
  • 您正在播放一次性簡介或說明影片,預期使用者會積極觀看。
  • 您的內容注重隱私權,而且您不希望外部程序存取媒體中繼資料 (例如瀏覽器中的無痕模式)

如果您的用途不符合上述任一條件,請評估當使用者未積極與內容互動時,您是否允許應用程式繼續播放。如果答案為「是」,建議選擇 MediaSession。如果答案為否,建議您改用 Player

建立媒體工作階段

媒體工作階段會與管理的播放器同時運作。您可以使用 ContextPlayer 物件建構媒體工作階段。您應在需要時建立並初始化媒體工作階段,例如 ActivityFragmentonStart()onResume() 生命週期方法,或擁有媒體工作階段和相關播放器的 ServiceonCreate() 方法。

如要建立媒體工作階段,請初始化 Player 並提供給 MediaSession.Builder,如下所示:

Kotlin

val player = ExoPlayer.Builder(context).build()
val mediaSession = MediaSession.Builder(context, player).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();
MediaSession mediaSession = new MediaSession.Builder(context, player).build();

自動狀態處理

Media3 程式庫會根據播放器的狀態自動更新媒體工作階段。因此,您不需要手動處理玩家與工作階段的對應關係。

這個方法與舊方法不同,您必須在舊方法之外獨立建立及維護 PlaybackStateCompat,例如用來表示任何錯誤。

不重複工作階段 ID

根據預設,MediaSession.Builder 會建立以空白字串做為工作階段 ID 的工作階段。如果應用程式打算只建立單一工作階段例項 (這是最常見的情況),這項做法就足夠了。

如果應用程式想要同時管理多個工作階段執行個體,應用程式必須確保每個工作階段的工作階段 ID 不會重複。使用 MediaSession.Builder.setId(String id) 建構工作階段時,可以設定工作階段 ID。

如果 IllegalStateException 異常終止應用程式並顯示錯誤訊息 IllegalStateException: Session ID must be unique. ID=,可能是因為在先前建立且具有相同 ID 的執行個體之前,意外建立工作階段。為避免工作階段因程式設計錯誤外洩,系統會藉由擲回例外狀況來偵測並通知這類情況。

將控制權授予其他用戶端

媒體工作階段是控製播放的關鍵。可讓您將外部來源的指令轉送至負責播放媒體的播放器。這些來源可以是實體按鈕 (例如耳機或電視遙控器上的播放按鈕),或是向 Google 助理下達指示的間接指令。同樣地,建議您授予 Android 系統的存取權,以便取得通知和螢幕鎖定控制項,或是授予 Wear OS 手錶的存取權,以便透過錶面控製播放。外部用戶端可以使用媒體控制器向您的媒體應用程式發出播放指令。這些內容會由您的媒體工作階段接收,最終將指令委派給媒體播放器。

展示 MediaSession 與 MediaController 之間互動的圖表。
圖 1:媒體控制器可協助將來自外部來源的指令傳送至媒體工作階段。

當控制器即將連線至媒體工作階段時,系統會呼叫 onConnect() 方法。您可以使用系統提供的 ControllerInfo 決定是否要接受拒絕要求。如需接受連線要求的範例,請參閱「宣告可用的指令」一節。

連線後,控制器就能傳送播放指令到工作階段。然後,工作階段會將這些指令委派給玩家。工作階段會自動處理 Player 介面中定義的播放和播放清單指令。

其他回呼方法可處理例如,針對自訂播放指令修改播放清單的要求。這些回呼同樣包含 ControllerInfo 物件,以便修改您回應每個控制器的方式。

修改播放清單

播放清單的 ExoPlayer 指南所述,媒體工作階段可以直接修改播放器的播放清單。如果控制器可以使用 COMMAND_SET_MEDIA_ITEMCOMMAND_CHANGE_MEDIA_ITEMS,控制器也可以修改播放清單。

將新項目新增至播放清單時,玩家通常需要使用已定義 URIMediaItem 執行個體才能播放項目。根據預設,如果新新增的項目已定義 URI,就會自動轉送至 player.addMediaItem 等玩家方法。

如要自訂新增至玩家的 MediaItem 例項,可以覆寫 onAddMediaItems()。如要支援在沒有定義 URI 的情況下要求媒體的控制器,就需要執行這個步驟。不過,MediaItem 通常會設定下列一或多個欄位,說明要求的媒體:

  • MediaItem.id:識別媒體的一般 ID。
  • MediaItem.RequestMetadata.mediaUri:可能使用自訂結構定義,且玩家不一定能直接播放的要求 URI。
  • MediaItem.RequestMetadata.searchQuery:文字搜尋查詢,例如 Google 助理。
  • MediaItem.MediaMetadata:結構化中繼資料,例如「名稱」或「演出者」。

對於全新播放清單的更多自訂選項,您可以另外覆寫 onSetMediaItems(),這可讓您定義起始項目和播放清單中的位置。舉例來說,您可以將單一要求的項目展開至整個播放清單,然後指示玩家從原始要求項目的索引開始。您可以在工作階段示範應用程式中找到含有這項功能的 onSetMediaItems() 實作範例

管理自訂版面配置和自訂指令

以下各節說明如何向用戶端應用程式通告自訂指令按鈕的自訂版面配置,並授權控制器傳送自訂指令。

定義工作階段的自訂版面配置

如要指示用戶端應用程式要向使用者顯示哪些播放控制項,請在服務的 onCreate() 方法中建構 MediaSession 時,設定工作階段的自訂版面配置

Kotlin

override fun onCreate() {
  super.onCreate()

  val likeButton = CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build()
  val favoriteButton = CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(SessionCommand(SAVE_TO_FAVORITES, Bundle()))
    .build()

  session =
    MediaSession.Builder(this, player)
      .setCallback(CustomMediaSessionCallback())
      .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
      .build()
}

Java

@Override
public void onCreate() {
  super.onCreate();

  CommandButton likeButton = new CommandButton.Builder()
    .setDisplayName("Like")
    .setIconResId(R.drawable.like_icon)
    .setSessionCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING))
    .build();
  CommandButton favoriteButton = new CommandButton.Builder()
    .setDisplayName("Save to favorites")
    .setIconResId(R.drawable.favorite_icon)
    .setSessionCommand(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
    .build();

  Player player = new ExoPlayer.Builder(this).build();
  mediaSession =
      new MediaSession.Builder(this, player)
          .setCallback(new CustomMediaSessionCallback())
          .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
          .build();
}

宣告可用的播放器和自訂指令

媒體應用程式可以定義自訂指令,以便用於自訂版面配置的執行個體。舉例來說,您可能會想要實作按鈕,讓使用者將媒體項目儲存至收藏項目清單。MediaController 會傳送自訂指令,MediaSession.Callback 會接收這些指令。

您可以在 MediaController 連線至媒體工作階段時,定義可以使用哪些自訂工作階段指令。做法是覆寫 MediaSession.Callback.onConnect()。在 onConnect 回呼方法中透過 MediaController 接受連線要求時,設定並傳回一組可用指令:

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  override fun onConnect(
    session: MediaSession,
    controller: MediaSession.ControllerInfo
  ): MediaSession.ConnectionResult {
    val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
        .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY))
        .build()
    return AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build()
  }
}

Java

class CustomMediaSessionCallback implements MediaSession.Callback {
  // Configure commands available to the controller in onConnect()
  @Override
  public ConnectionResult onConnect(
    MediaSession session,
    ControllerInfo controller) {
    SessionCommands sessionCommands =
        ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
            .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle()))
            .build();
    return new AcceptedResultBuilder(session)
        .setAvailableSessionCommands(sessionCommands)
        .build();
  }
}

如要透過 MediaController 接收自訂指令要求,請覆寫 Callback 中的 onCustomCommand() 方法。

Kotlin

private inner class CustomMediaSessionCallback: MediaSession.Callback {
  ...
  override fun onCustomCommand(
    session: MediaSession,
    controller: MediaSession.ControllerInfo,
    customCommand: SessionCommand,
    args: Bundle
  ): ListenableFuture<SessionResult> {
    if (customCommand.customAction == SAVE_TO_FAVORITES) {
      // Do custom logic here
      saveToFavorites(session.player.currentMediaItem)
      return Futures.immediateFuture(
        SessionResult(SessionResult.RESULT_SUCCESS)
      )
    }
    ...
  }
}

Java

class CustomMediaSessionCallback implements MediaSession.Callback {
  ...
  @Override
  public ListenableFuture<SessionResult> onCustomCommand(
    MediaSession session, 
    ControllerInfo controller,
    SessionCommand customCommand,
    Bundle args
  ) {
    if(customCommand.customAction.equals(SAVE_TO_FAVORITES)) {
      // Do custom logic here
      saveToFavorites(session.getPlayer().getCurrentMediaItem());
      return Futures.immediateFuture(
        new SessionResult(SessionResult.RESULT_SUCCESS)
      );
    }
    ...
  }
}

您可以透過傳入 Callback 方法的 MediaSession.ControllerInfo 物件 packageName 屬性,追蹤哪個媒體控制器正在提出要求。這樣做可讓您針對來自系統、您自己的應用程式或其他用戶端應用程式的特定指令,調整應用程式行為,以回應特定指令。

在使用者互動後更新自訂版面配置

處理自訂指令或任何其他與播放器互動後,您可能會想要更新控制器 UI 中顯示的版面配置。典型的例子是切換按鈕,會在觸發與此按鈕相關的動作後變更圖示。如要更新版面配置,您可以使用 MediaSession.setCustomLayout

Kotlin

val removeFromFavoritesButton = CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(SessionCommand(REMOVE_FROM_FAVORITES, Bundle()))
  .build()
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

CommandButton removeFromFavoritesButton = new CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(new SessionCommand(REMOVE_FROM_FAVORITES, new Bundle()))
  .build();
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton));

自訂播放指令行為

如要自訂 Player 介面中定義的指令行為 (例如 play()seekToNext()),請將 Player 納入 ForwardingPlayer 中。

Kotlin

val player = ExoPlayer.Builder(context).build()

val forwardingPlayer = object : ForwardingPlayer(player) {
  override fun play() {
    // Add custom logic
    super.play()
  }

  override fun setPlayWhenReady(playWhenReady: Boolean) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady)
  }
}

val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()

Java

ExoPlayer player = new ExoPlayer.Builder(context).build();

ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player) {
  @Override
  public void play() {
    // Add custom logic
    super.play();
  }

  @Override
  public void setPlayWhenReady(boolean playWhenReady) {
    // Add custom logic
    super.setPlayWhenReady(playWhenReady);
  }
};

MediaSession mediaSession = 
  new MediaSession.Builder(context, forwardingPlayer).build();

如要進一步瞭解 ForwardingPlayer,請參閱有關自訂的 ExoPlayer 指南。

找出玩家命令的請求控制器

如果呼叫 Player 方法是由 MediaController 發出,您可以使用 MediaSession.controllerForCurrentRequest 識別來源來源,並針對目前要求取得 ControllerInfo

Kotlin

class CallerAwareForwardingPlayer(player: Player) :
  ForwardingPlayer(player) {

  override fun seekToNext() {
    Log.d(
      "caller",
      "seekToNext called from package ${session.controllerForCurrentRequest?.packageName}"
    )
    super.seekToNext()
  }
}

Java

public class CallerAwareForwardingPlayer extends ForwardingPlayer {
  public CallerAwareForwardingPlayer(Player player) {
    super(player);
  }

  @Override
  public void seekToNext() {
    Log.d(
        "caller",
        "seekToNext called from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    super.seekToNext();
  }
}

回應媒體按鈕事件

媒體按鈕是 Android 裝置和其他週邊裝置的硬體按鈕,例如藍牙耳機的播放/暫停按鈕。Media3 會在使用者抵達工作階段時處理媒體按鈕事件,並在工作階段播放器上呼叫適當的 Player 方法。

應用程式可以覆寫 MediaSession.Callback.onMediaButtonEvent(Intent),覆寫預設行為。在這種情況下,應用程式可以/需要自行處理所有 API 細節。