使用 MediaSession 控制及放送廣告

媒體工作階段提供一種與音訊或視訊互動的方式 廣告。在 Media3 中,預設播放器是實作的 ExoPlayer 類別 Player 介面。將媒體工作階段連線至播放器,即可讓應用程式 通告媒體播放,並接收來自 外部來源。

指令可能來自實體按鈕,例如行動裝置上的播放按鈕 耳機或電視遙控器。他們也可能來自具有以下特性的用戶端應用程式 例如指示「暫停」。媒體 工作階段會將這些指令委派給媒體應用程式的播放器。

選擇媒體工作階段的時機

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

  • 透過耳機使用。例如按鈕或觸控互動 使用者可在耳罩式耳機上播放或暫停媒體,或跳至下一集 播放歌曲或上一首曲目
  • Google 助理交談。常見的模式是說 Google,暫停"會暫停裝置正在播放的任何媒體。
  • 透過 Wear OS 手錶。這樣一來,您就能更輕鬆存取 常用的播放控制項。
  • 透過媒體控制選項。這個輪轉介面會顯示 執行媒體工作階段
  • 使用電視。允許透過實體播放按鈕和平台播放等動作。 控制和電源管理 (例如電視、單件式環繞劇院或影音接收器) 或是輸入來源遭到切換,應用程式中的播放應該會停止)。
  • 還有其他需要影響播放作業的外部處理程序。

這對許多用途來說十分實用。尤其是,你應該 在以下情況下使用 MediaSession

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

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

  • 您刊登了短篇內容,也就是使用者參與和互動的管道 至關重要
  • Google 不會提供單一影片,例如使用者正在捲動瀏覽清單 而且畫面上會同時顯示多部影片
  • 你正在播放一次性的介紹或說明影片, 預期使用者會主動觀看
  • 您的內容具有隱私權機密,您不希望外部程序: 存取媒體中繼資料 (例如瀏覽器的無痕模式)

如果您的用途與上述任一情況不符,請考慮您是否 當使用者不主動互動時,您的應用程式會繼續播放 與內容互動如果答案是肯定的,則建議您選擇 MediaSession。如果答案為否,則建議使用 Player

建立媒體工作階段

媒體工作階段會與管理的播放器一起運作。您可以建構 使用 ContextPlayer 物件的媒體工作階段。您應建立和 視需要初始化媒體工作階段,例如 onStart()ActivityFragmentonResume() 生命週期方法,或 onCreate() 擁有媒體工作階段及相關播放器的 Service 方法。

如要建立媒體工作階段,請初始化 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 不重複工作階段 ID 在使用 MediaSession.Builder.setId(String id) 建構工作階段時進行設定。

如果您發現「IllegalStateException」導致應用程式當機並出現錯誤 傳送訊息給「IllegalStateException: Session ID must be unique. ID=」,表示 工作階段可能在前一個設定檔建立前 已發布 ID 相同的執行個體。為避免工作階段因 程式設計錯誤,可以藉由擲回 例外狀況。

將控制權授予其他用戶端

媒體工作階段是控製播放的關鍵。這項功能可讓您 發出指令,以便執行 媒體。這些來源可以是實體按鈕,例如 頭戴式耳機或電視遙控器,或間接命令 (例如說出「暫停」) 。同樣地,建議您授予 Android 應用程式的存取權 通知和螢幕鎖定控制項,或是 Wear OS ,你就能透過錶面控制音訊播放。外部用戶端 使用媒體控制器向媒體應用程式發出播放指令。這些 所接收的指令,進而將指令委派給 媒體播放器中。

圖表:展示 MediaSession 和 MediaController 之間的互動。
圖 1:媒體控制器協助傳送 新增至媒體工作階段的指令。

當控制器準備連線到媒體工作階段時, onConnect()敬上 方法。您可以使用 Google 提供的 ControllerInfo 決定是否要接受。 或拒絕 要求。如需查看接受連線要求的範例,請參閱宣告 可用的指令部分。

連線後,控制器便可傳送播放指令給工作階段。 然後將這些指令委派給玩家。播放與播放清單 自動處理 Player 介面中定義的指令 會很有幫助

其他回呼方法可讓您處理 自訂播放指令修改播放清單)。 這些回呼也包含 ControllerInfo 物件,因此您可以修改 您如何針對各控制器逐一回應各項要求

修改播放清單

媒體工作階段可以直接修改播放器的播放清單,詳情請參閱: 這個 播放清單的 ExoPlayer 指南。 如果符合任一條件,控管者也能修改播放清單 COMMAND_SET_MEDIA_ITEMCOMMAND_CHANGE_MEDIA_ITEMS 控制器可用

在播放清單中新增項目時,播放器通常需要使用 MediaItem 以及含有 定義的 URI 以便播放根據預設,系統會自動轉寄新增的項目 也可以針對 player.addMediaItem 等玩家方法定義 URI。

如要自訂新增至播放器的 MediaItem 例項,可以 覆寫 onAddMediaItems()。 如要支援要求媒體的控制器 但不含已定義的 URI相反地,MediaItem 通常有 以下一或多個欄位集描述所請求的媒體:

  • MediaItem.id:用於識別媒體的一般 ID。
  • MediaItem.RequestMetadata.mediaUri:可能使用自訂值的要求 URI 而且播放器不一定可以直接播放。
  • MediaItem.RequestMetadata.searchQuery:文字搜尋查詢,例如 。
  • MediaItem.MediaMetadata:結構化中繼資料,例如「title」或是「artist」的歌

如需更多全新播放清單的自訂選項,您可以 額外覆寫 onSetMediaItems()敬上 可讓您定義起始項目和播放清單中的位置。例如: 可以將單一要求的項目展開至整個播放清單,並指示 的播放器,從原本要求的項目索引開始著手。A 罩杯 onSetMediaItems() 的實作範例 您可以在工作階段試用版應用程式中找到這項功能。

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

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

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

向用戶端應用程式指明您要顯示哪些播放控制項 設定工作階段的自訂版面配置 使用 App Engine 的 onCreate() 方法建構 MediaSession 課程中也會快速介紹 Memorystore 這是 Google Cloud 的全代管 Redis 服務

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

您可以使用 也就是 MediaSession.ControllerInfo 物件的 packageName 屬性 傳入 Callback 方法。這樣一來,您就能根據自己的需求 回應特定指令的行為 (如果該指令來自系統、 或其他用戶端應用程式

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

在處理自訂指令或任何其他與播放器的互動後, 您可能會想更新控制器 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 細節

錯誤處理與回報

工作階段會發出兩種錯誤,並回報給控制器。 嚴重錯誤回報工作階段的技術播放錯誤 中斷播放的播放器。已向控制器回報嚴重錯誤 並自動同步。一般錯誤不屬於技術或政策 不會中斷播放,並由 應用程式。

嚴重播放錯誤

播放器會回報工作階段嚴重的播放錯誤, 向控制器回報 「Player.Listener.onPlayerError(PlaybackException)」和 Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException)

在這種情況下,播放狀態會轉換為 STATE_IDLE,並 MediaController.getPlaybackError() 會傳回造成該錯誤的 PlaybackException 轉場效果控制器可以檢查 PlayerException.errorCode,取得 錯誤原因的相關資訊。

基於互通性,系統會將嚴重錯誤複製到 PlaybackStateCompat 並藉由將狀態轉換為 STATE_ERROR 和 錯誤代碼和訊息。詳情請參閱 PlaybackException

自訂嚴重錯誤

為向使用者提供有意義的本地化資訊、錯誤代碼、 錯誤訊息和重大播放錯誤的相關錯誤,可自訂 使用 ForwardingPlayer 建構工作階段:

Kotlin

val forwardingPlayer = ErrorForwardingPlayer(player)
val session = MediaSession.Builder(context, forwardingPlayer).build()

Java

Player forwardingPlayer = new ErrorForwardingPlayer(player);
MediaSession session =
    new MediaSession.Builder(context, forwardingPlayer).build();

轉送播放器會向實際的玩家註冊 Player.Listener 並攔截回報錯誤的回呼。自訂 PlaybackException 會委派給 已登錄到轉送播放器上。為使這個過程順利進行,轉送播放器 覆寫 Player.addListenerPlayer.removeListener,即可使用 可以傳送自訂錯誤代碼、訊息或額外項目的接聽程式:

Kotlin

class ErrorForwardingPlayer(private val context: Context, player: Player) :
  ForwardingPlayer(player) {

  private val listeners: MutableList<Player.Listener> = mutableListOf()

  private var customizedPlaybackException: PlaybackException? = null

  init {
    player.addListener(ErrorCustomizationListener())
  }

  override fun addListener(listener: Player.Listener) {
    listeners.add(listener)
  }

  override fun removeListener(listener: Player.Listener) {
    listeners.remove(listener)
  }

  override fun getPlayerError(): PlaybackException? {
    return customizedPlaybackException
  }

  private inner class ErrorCustomizationListener : Player.Listener {

    override fun onPlayerErrorChanged(error: PlaybackException?) {
      customizedPlaybackException = error?.let { customizePlaybackException(it) }
      listeners.forEach { it.onPlayerErrorChanged(customizedPlaybackException) }
    }

    override fun onPlayerError(error: PlaybackException) {
      listeners.forEach { it.onPlayerError(customizedPlaybackException!!) }
    }

    private fun customizePlaybackException(
      error: PlaybackException,
    ): PlaybackException {
      val buttonLabel: String
      val errorMessage: String
      when (error.errorCode) {
        PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> {
          buttonLabel = context.getString(R.string.err_button_label_restart_stream)
          errorMessage = context.getString(R.string.err_msg_behind_live_window)
        }
        // Apps can customize further error messages by adding more branches.
        else -> {
          buttonLabel = context.getString(R.string.err_button_label_ok)
          errorMessage = context.getString(R.string.err_message_default)
        }
      }
      val extras = Bundle()
      extras.putString("button_label", buttonLabel)
      return PlaybackException(errorMessage, error.cause, error.errorCode, extras)
    }

    override fun onEvents(player: Player, events: Player.Events) {
      listeners.forEach {
        it.onEvents(player, events)
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

Java

private static class ErrorForwardingPlayer extends ForwardingPlayer {

  private final Context context;
  private List<Player.Listener> listeners;
  @Nullable private PlaybackException customizedPlaybackException;

  public ErrorForwardingPlayer(Context context, Player player) {
    super(player);
    this.context = context;
    listeners = new ArrayList<>();
    player.addListener(new ErrorCustomizationListener());
  }

  @Override
  public void addListener(Player.Listener listener) {
    listeners.add(listener);
  }

  @Override
  public void removeListener(Player.Listener listener) {
    listeners.remove(listener);
  }

  @Nullable
  @Override
  public PlaybackException getPlayerError() {
    return customizedPlaybackException;
  }

  private class ErrorCustomizationListener implements Listener {

    @Override
    public void onPlayerErrorChanged(@Nullable PlaybackException error) {
      customizedPlaybackException =
          error != null ? customizePlaybackException(error, context) : null;
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerErrorChanged(customizedPlaybackException);
      }
    }

    @Override
    public void onPlayerError(PlaybackException error) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onPlayerError(checkNotNull(customizedPlaybackException));
      }
    }

    private PlaybackException customizePlaybackException(
        PlaybackException error, Context context) {
      String buttonLabel;
      String errorMessage;
      switch (error.errorCode) {
        case PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW:
          buttonLabel = context.getString(R.string.err_button_label_restart_stream);
          errorMessage = context.getString(R.string.err_msg_behind_live_window);
          break;
        // Apps can customize further error messages by adding more case statements.
        default:
          buttonLabel = context.getString(R.string.err_button_label_ok);
          errorMessage = context.getString(R.string.err_message_default);
          break;
      }
      Bundle extras = new Bundle();
      extras.putString("button_label", buttonLabel);
      return new PlaybackException(errorMessage, error.getCause(), error.errorCode, extras);
    }

    @Override
    public void onEvents(Player player, Events events) {
      for (int i = 0; i < listeners.size(); i++) {
        listeners.get(i).onEvents(player, events);
      }
    }
    // Delegate all other callbacks to all listeners without changing arguments like onEvents.
  }
}

一般錯誤

「不是」源自技術例外狀況的重大錯誤都可傳送 應用程式傳送給所有或特定控制器:

Kotlin

val sessionError = SessionError(
  SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
  context.getString(R.string.error_message_authentication_expired),
)

// Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError)

// Interoperability: Sending a nonfatal error to the media notification controller to set the
// error code and error message in the playback state of the platform media session.
mediaSession.mediaNotificationControllerInfo?.let {
  mediaSession.sendError(it, sessionError)
}

Java

SessionError sessionError = new SessionError(
    SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
    context.getString(R.string.error_message_authentication_expired));

// Sending a nonfatal error to all controllers.
mediaSession.sendError(sessionError);

// Interoperability: Sending a nonfatal error to the media notification controller to set the
// error code and error message in the playback state of the platform media session.
ControllerInfo mediaNotificationControllerInfo =
    mediaSession.getMediaNotificationControllerInfo();
if (mediaNotificationControllerInfo != null) {
  mediaSession.sendError(mediaNotificationControllerInfo, sessionError);
}

系統會將傳送到媒體通知控制器的一般錯誤複製到 平台工作階段的 PlaybackStateCompat。如此一來,就只有錯誤代碼和 並據此將錯誤訊息設為 PlaybackStateCompat,而 PlaybackStateCompat.state 未變更為 STATE_ERROR

接收一般錯誤

MediaController 在實作時收到非重大錯誤 MediaController.Listener.onError

Kotlin

val future = MediaController.Builder(context, sessionToken)
  .setListener(object : MediaController.Listener {
    override fun onError(controller: MediaController, sessionError: SessionError) {
      // Handle nonfatal error.
    }
  })
  .buildAsync()

Java

MediaController.Builder future =
    new MediaController.Builder(context, sessionToken)
        .setListener(
            new MediaController.Listener() {
              @Override
              public void onError(MediaController controller, SessionError sessionError) {
                // Handle nonfatal error.
              }
            });