媒體工作階段提供與音訊或影片播放器互動的通用方式。在 Media3 中,預設的播放器是 ExoPlayer
類別,可實作 Player
介面。將媒體工作階段連結至播放器,可讓應用程式在外部宣傳媒體播放內容,並接收來自外部來源的播放指令。
指令可能來自實體按鈕,例如耳機或電視遙控器上的播放按鈕。這些指令也可能來自具有媒體控制器的用戶端應用程式,例如指示 Google 助理「暫停」。媒體工作階段會將這些指令委派給媒體應用程式的播放器。
選擇媒體工作階段的時機
實作 MediaSession
時,您可以讓使用者控制播放作業:
- 透過耳機。使用者通常可透過按鈕或觸控互動操作耳機,播放或暫停媒體,或切換至下一首或上一首曲目。
- 透過語音與 Google 助理交談。常見的使用模式是說出「OK Google,暫停」,即可暫停裝置上目前正在播放的任何媒體。
- 透過 Wear OS 手錶。這樣一來,使用者在手機上播放內容時,就能更輕鬆地存取最常見的播放控制選項。
- 透過媒體控制選項。這個輪轉介面會顯示每個執行中的媒體工作階段控制項。
- 在電視上。允許使用實體播放按鈕、平台播放控制項和電源管理功能執行動作 (例如,如果電視、單件式環繞劇院或影音接收器關閉或切換輸入來源,應用程式應停止播放)。
- 以及任何其他需要影響播放作業的外部程序。
這對許多用途來說都很實用。特別是當下列情況發生時,您應考慮使用 MediaSession
:
- 你正在串流播放長篇影片內容,例如電影或電視直播。
- 你串流播放長篇音訊內容,例如 Podcast 或音樂播放清單。
- 您正在建構電視應用程式。
不過,並非所有用途都適合使用 MediaSession
。您可能會在下列情況下只使用 Player
:
- 您正在顯示短片內容,因此不需要外部控制項或背景播放功能。
- 沒有單一活動影片,例如使用者正在捲動清單,且畫面上同時顯示多部影片。
- 您正在播放一次性介紹或說明影片,且希望使用者能主動觀看,而不需要外部播放控制選項。
- 您的內容涉及隱私權,且您不希望外部程序存取媒體中繼資料 (例如瀏覽器的無痕模式)。
如果您的用途不符合上述任何情況,請考慮在使用者未積極與內容互動時,是否允許應用程式繼續播放內容。如果答案是肯定的,您可能要選擇 MediaSession
。如果答案為否,您可能需要改用 Player
。
建立媒體工作階段
媒體工作階段會與其管理的播放器一同存在。您可以使用 Context
和 Player
物件建立媒體工作階段。您應在需要時建立及初始化媒體工作階段,例如 Activity
或 Fragment
的 onStart()
或 onResume()
生命週期方法,或是擁有媒體工作階段及其相關播放器的 Service
的 onCreate()
方法。
如要建立媒體工作階段,請初始化 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 程式庫會使用播放器的狀態自動更新媒體工作階段。因此,您不需要手動處理從玩家到工作階段的對應作業。
這與平台媒體工作階段不同,因為您需要在播放器本身之外,另外建立及維護 PlaybackState
,例如用於指出任何錯誤。
專屬工作階段 ID
根據預設,MediaSession.Builder
會建立工作階段,並以空字串做為工作階段 ID。如果應用程式只想建立單一工作階段例項 (這是最常見的情況),這就足夠了。
如果應用程式想同時管理多個工作階段執行個體,就必須確保每個工作階段的工作階段 ID 皆不重複。使用 MediaSession.Builder.setId(String id)
建構工作階段時,可以設定工作階段 ID。
如果您看到 IllegalStateException
導致應用程式異常終止,並顯示錯誤訊息 IllegalStateException: Session ID must be unique. ID=
,則表示在先前建立的具有相同 ID 的例項釋出前,可能已意外建立工作階段。為避免因程式設計錯誤而導致工作階段外洩,系統會偵測這類情況,並透過擲回例外狀況來發出通知。
將控制權授予其他用戶端
媒體工作階段是控制播放的關鍵。可讓您將來自外部來源的指令,轉送至負責播放媒體的播放器。這些來源可以是實體按鈕,例如耳機或電視遙控器上的播放按鈕,或是間接指令,例如指示 Google 助理「暫停」。同樣地,您可能也想授予 Android 系統存取權,以便使用通知和鎖定螢幕控制項,或是授予 Wear OS 手錶存取權,以便透過錶面控制播放功能。外部用戶端可以使用媒體控制器,向媒體應用程式發出播放指令。這些指令會由媒體工作階段接收,最終將指令委派給媒體播放器。

當控制器即將連線至媒體工作階段時,系統會呼叫 onConnect()
方法。您可以使用提供的 ControllerInfo
,決定是否要接受或拒絕要求。請參閱「宣告自訂指令」一節中接受連線要求的範例。
連線後,控制器就能將播放指令傳送至工作階段。然後,工作階段會將這些指令委派給播放器。播放和播放清單指令會在 Player
介面中定義,並由工作階段自動處理。
其他回呼方法可讓您處理自訂指令和修改播放清單等要求。這些回呼同樣包含 ControllerInfo
物件,因此您可以依控制器修改對每項要求的回應方式。
修改播放清單
如ExoPlayer 播放清單指南所述,媒體工作階段可以直接修改播放器的播放清單。如果控制器可使用 COMMAND_SET_MEDIA_ITEM
或 COMMAND_CHANGE_MEDIA_ITEMS
,控制器也能修改播放清單。
將新項目加入播放清單時,播放器通常需要 MediaItem
例項,並附上已定義的 URI,才能播放這些項目。根據預設,如果新新增的項目已定義 URI,就會自動轉送至 player.addMediaItem
等播放器方法。
如要自訂新增至播放器的 MediaItem
例項,您可以覆寫 onAddMediaItems()
。如要支援不含定義 URI 的控制器,就必須執行這個步驟。相反地,MediaItem
通常會設定一或多個下列欄位,用來描述要求的媒體:
MediaItem.id
:用於識別媒體的一般 ID。MediaItem.RequestMetadata.mediaUri
:可能會使用自訂結構描述的請求 URI,且不一定能由播放器直接播放。MediaItem.RequestMetadata.searchQuery
:文字搜尋查詢,例如來自 Google 助理的查詢。MediaItem.MediaMetadata
:結構化中繼資料,例如「title」或「artist」。
如要為全新播放清單提供更多自訂選項,您可以另外覆寫 onSetMediaItems()
,藉此定義播放清單中的開始項目和位置。舉例來說,您可以將單一要求的項目擴展至整個播放清單,並指示播放器從原本要求的項目索引開始播放。您可以在工作階段示範應用程式中,找到使用這項功能的 onSetMediaItems()
實作範例。
管理媒體按鈕偏好設定
每個控制器 (例如系統 UI、Android Auto 或 Wear OS) 都可以自行決定要向使用者顯示哪些按鈕。如要指出要向使用者顯示哪些播放控制項,您可以在 MediaSession
上指定媒體按鈕偏好設定。這些偏好設定包含 CommandButton
例項的排序清單,每個例項都會定義使用者介面中按鈕的偏好設定。
定義指令按鈕
CommandButton
例項用於定義媒體按鈕偏好設定。每個按鈕都會定義所需 UI 元素的三個層面:
- 圖示:定義視覺外觀。建立
CommandButton.Builder
時,圖示必須設為預先定義的常數之一。請注意,這並非實際的點陣圖或圖片資源。通用常數可協助控制器選擇適當的資源,以便在其 UI 中提供一致的外觀和感受。如果沒有任何預先定義的圖示常數可用於您的用途,您可以改用setCustomIconResId
。 - 指令:定義使用者與按鈕互動時觸發的動作。您可以使用
setPlayerCommand
做為Player.Command
,也可以使用setSessionCommand
做為預先定義或自訂的SessionCommand
。 - Slot:定義按鈕應放在控制器 UI 中的哪個位置。這個欄位為選用欄位,會根據圖示和指令自動設定。舉例來說,您可以指定按鈕應顯示在 UI 的「前進」導覽區域,而非預設的「溢位」區域。
Kotlin
val button = CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15) .setSessionCommand(SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY)) .setSlots(CommandButton.SLOT_FORWARD) .build()
Java
CommandButton button = new CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15) .setSessionCommand(new SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY)) .setSlots(CommandButton.SLOT_FORWARD) .build();
解析媒體按鈕偏好設定後,系統會套用下列演算法:
- 針對媒體按鈕偏好設定中的每個
CommandButton
,將按鈕放在第一個可用且允許的插槽中。 - 如果中央、向前和向後格中沒有任何按鈕,請為這個格新增預設按鈕。
您可以使用 CommandButton.DisplayConstraints
產生預覽畫面,瞭解如何根據 UI 顯示限制解決媒體按鈕偏好設定。
設定媒體按鈕偏好設定
設定媒體按鈕偏好設定最簡單的方法,就是在建構 MediaSession
時定義清單。您也可以覆寫 MediaSession.Callback.onConnect
,為每個已連結的控制器自訂媒體按鈕偏好設定。
Kotlin
val mediaSession = MediaSession.Builder(context, player) .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton)) .build()
Java
MediaSession mediaSession = new MediaSession.Builder(context, player) .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton)) .build();
在使用者互動後更新媒體按鈕偏好設定
處理與播放器的互動後,您可能需要更新控制器 UI 中顯示的按鈕。典型例子是切換按鈕,在觸發與此按鈕相關聯的動作後,會變更圖示和動作。如要更新媒體按鈕偏好設定,您可以使用 MediaSession.setMediaButtonPreferences
更新所有控制器或特定控制器的偏好設定:
Kotlin
// Handle "favoritesButton" action, replace by opposite button mediaSession.setMediaButtonPreferences( ImmutableList.of(likeButton, removeFromFavoritesButton))
Java
// Handle "favoritesButton" action, replace by opposite button mediaSession.setMediaButtonPreferences( ImmutableList.of(likeButton, removeFromFavoritesButton));
新增自訂指令並自訂預設行為
可用的播放器指令可透過自訂指令擴充,也可以攔截傳入的播放器指令和媒體按鈕,以便變更預設行為。
宣告及處理自訂指令
媒體應用程式可以定義自訂指令,例如可用於媒體按鈕偏好設定。舉例來說,您可能會想實作按鈕,讓使用者將媒體項目儲存至最愛項目清單。MediaController
會傳送自訂指令,而 MediaSession.Callback
會接收這些指令。
如要定義自訂指令,您必須覆寫 MediaSession.Callback.onConnect()
,為每個已連結的控制器設定可用的自訂指令。
Kotlin
private 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 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
屬性,追蹤提出要求的媒體控制器。這可讓您針對來自系統、您自己的應用程式或其他用戶端應用程式的特定指令,調整應用程式的回應行為。
自訂預設玩家指令
所有預設指令和狀態處理都會委派至 MediaSession
上的 Player
。如要自訂 Player
介面中定義的指令行為 (例如 play()
或 seekToNext()
),請先將 Player
包裝在 ForwardingSimpleBasePlayer
中,再傳遞至 MediaSession
:
Kotlin
val player = (logic to build a Player instance) val forwardingPlayer = object : ForwardingSimpleBasePlayer(player) { // Customizations } val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()
Java
ExoPlayer player = (logic to build a Player instance) ForwardingSimpleBasePlayer forwardingPlayer = new ForwardingSimpleBasePlayer(player) { // Customizations }; MediaSession mediaSession = new MediaSession.Builder(context, forwardingPlayer).build();
如要進一步瞭解 ForwardingSimpleBasePlayer
,請參閱 ExoPlayer 指南的「自訂化」一節。
找出玩家指令的請求控制器
當 MediaController
發出對 Player
方法的呼叫時,您可以使用 MediaSession.controllerForCurrentRequest
識別來源,並取得目前要求的 ControllerInfo
:
Kotlin
class CallerAwarePlayer(player: Player) : ForwardingSimpleBasePlayer(player) { override fun handleSeek( mediaItemIndex: Int, positionMs: Long, seekCommand: Int, ): ListenableFuture<*> { Log.d( "caller", "seek operation from package ${session.controllerForCurrentRequest?.packageName}", ) return super.handleSeek(mediaItemIndex, positionMs, seekCommand) } }
Java
public class CallerAwarePlayer extends ForwardingSimpleBasePlayer { public CallerAwarePlayer(Player player) { super(player); } @Override protected ListenableFuture<?> handleSeek( int mediaItemIndex, long positionMs, int seekCommand) { Log.d( "caller", "seek operation from package: " + session.getControllerForCurrentRequest().getPackageName()); return super.handleSeek(mediaItemIndex, positionMs, seekCommand); } }
自訂媒體按鈕處理方式
媒體按鈕是 Android 裝置和其他周邊裝置上的硬體按鈕,例如藍牙耳機上的播放/暫停按鈕。Media3 會在媒體按鈕事件到達工作階段時為您處理這些事件,並在工作階段播放器上呼叫適當的 Player
方法。
建議您在對應的 Player
方法中處理所有傳入的媒體按鈕事件。如要處理更進階的用途,您可以在 MediaSession.Callback.onMediaButtonEvent(Intent)
中攔截媒體按鈕事件。
錯誤處理和回報
工作階段會發出並回報給控制器的錯誤有兩種。嚴重錯誤會回報會中斷播放的會話方塊播放器技術播放失敗。發生嚴重錯誤時,系統會自動向控制器回報錯誤。非致命錯誤是指非技術或政策錯誤,不會中斷播放,且會由應用程式手動傳送至控制器。
嚴重播放錯誤
播放器會將嚴重播放錯誤回報至工作階段,然後再回報至控制器,以便透過 Player.Listener.onPlayerError(PlaybackException)
和 Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException)
呼叫。
在這種情況下,播放狀態會轉換為 STATE_IDLE
,而 MediaController.getPlaybackError()
會傳回導致轉換的 PlaybackException
。控制器可以檢查 PlayerException.errorCode
,取得錯誤原因的相關資訊。
為了達到互通性,系統會將嚴重錯誤複製到平台工作階段,方法是將其狀態轉換為 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();
轉發播放器可使用 ForwardingSimpleBasePlayer
攔截錯誤,並自訂錯誤代碼、訊息或額外內容。同樣地,您也可以產生原始播放器中不存在的新錯誤:
Kotlin
class ErrorForwardingPlayer (private val context: Context, player: Player) : ForwardingSimpleBasePlayer(player) { override fun getState(): State { var state = super.getState() if (state.playerError != null) { state = state.buildUpon() .setPlayerError(customizePlaybackException(state.playerError!!)) .build() } return state } 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) } 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) } }
Java
class ErrorForwardingPlayer extends ForwardingSimpleBasePlayer { private final Context context; public ErrorForwardingPlayer(Context context, Player player) { super(player); this.context = context; } @Override protected State getState() { State state = super.getState(); if (state.playerError != null) { state = state.buildUpon() .setPlayerError(customizePlaybackException(state.playerError)) .build(); } return state; } private PlaybackException customizePlaybackException(PlaybackException error) { 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; 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); } }
非致命錯誤
應用程式可以將非技術例外狀況所導致的非致命錯誤傳送至所有或特定控制器:
Kotlin
val sessionError = SessionError( SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, context.getString(R.string.error_message_authentication_expired), ) // Option 1: Sending a nonfatal error to all controllers. mediaSession.sendError(sessionError) // Option 2: Sending a nonfatal error to the media notification controller only // 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)); // Option 1: Sending a nonfatal error to all controllers. mediaSession.sendError(sessionError); // Option 2: Sending a nonfatal error to the media notification controller only // 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); }
當非重大錯誤傳送至媒體通知控制器時,錯誤代碼和錯誤訊息會複製至平台媒體工作階段,而 PlaybackState.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. } });