媒體工作階段提供與音訊或影片播放器互動的通用方式。在 Media3 中,預設播放器是 ExoPlayer
類別,可實作 Player
介面。將媒體工作階段連結至播放器,應用程式就能在外部宣傳媒體播放功能,並接收外部來源的播放指令。
指令可能來自實體按鈕,例如耳機或電視遙控器上的播放按鈕。也可能來自有媒體控制器的用戶端應用程式,例如指示 Google 助理「暫停」。媒體工作階段會將這些指令委派給媒體應用程式的播放器。
選擇媒體工作階段的時機
實作 MediaSession
後,使用者就能控制播放:
- 透過耳機。使用者通常可以透過耳機上的按鈕或觸控操作,播放或暫停媒體,或是跳到上一首或下一首曲目。
- 與 Google 助理交談。常見的模式是說出「Ok Google,暫停」,暫停裝置目前播放的任何媒體。
- 透過 Wear OS 手錶。方便使用者在手機上播放內容時,輕鬆存取最常用的播放控制選項。
- 透過媒體控制項。這個輪播介面會顯示每個執行中媒體工作階段的控制選項。
- 在電視上。可透過實體播放按鈕、平台播放控制項和電源管理功能執行動作 (例如,如果電視、單件式環繞劇院或影音接收器關閉或切換輸入,應用程式應停止播放)。
- 透過 Android Auto 媒體控制項。確保駕駛人行車安全。
- 以及任何其他需要影響播放作業的外部程序。
這項功能非常適合許多用途。特別是當您遇到以下情況時,強烈建議使用 MediaSession
:
- 你正在串流播放長篇影片內容,例如電影或電視直播。
- 你正在串流播放長篇音訊內容,例如 Podcast 或音樂播放清單。
- 您要建構 TV 應用程式。
不過,並非所有用途都適合使用 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
:結構化中繼資料,例如「標題」或「藝人」。
如要進一步自訂全新播放清單,可以額外覆寫 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 指南的「自訂」一節。
識別播放器指令的請求控制器
如果對 Player
方法的呼叫是由 MediaController
發起,您可以透過 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. } });