使用 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:結構化中繼資料,例如「title」或「演出者」。

如需全新播放清單的更多自訂選項,也可以覆寫 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 細節。