媒體工作階段提供一種與音訊或視訊互動的方式
廣告。在 Media3 中,預設播放器是實作的 ExoPlayer
類別
Player
介面。將媒體工作階段連線至播放器,即可讓應用程式
通告媒體播放,並接收來自
外部來源。
指令可能來自實體按鈕,例如行動裝置上的播放按鈕 耳機或電視遙控器。他們也可能來自具有以下特性的用戶端應用程式 例如指示「暫停」。媒體 工作階段會將這些指令委派給媒體應用程式的播放器。
選擇媒體工作階段的時機
實作 MediaSession
後,使用者就能控製播放作業:
- 透過耳機使用。例如按鈕或觸控互動 使用者可在耳罩式耳機上播放或暫停媒體,或跳至下一集 播放歌曲或上一首曲目
- 與 Google 助理交談。常見的模式是說 Google,暫停"會暫停裝置正在播放的任何媒體。
- 透過 Wear OS 手錶。這樣一來,您就能更輕鬆存取 常用的播放控制項。
- 透過媒體控制選項。這個輪轉介面會顯示 執行媒體工作階段
- 使用電視。允許透過實體播放按鈕和平台播放等動作。 控制和電源管理 (例如電視、單件式環繞劇院或影音接收器) 或是輸入來源遭到切換,應用程式中的播放應該會停止)。
- 還有其他需要影響播放作業的外部處理程序。
這對許多用途來說十分實用。尤其是,你應該
在以下情況下使用 MediaSession
:
- 你正在串流播放長篇影片內容,例如電影或電視直播節目。
- 你正在串流播放長篇音訊內容,例如 Podcast 或音樂 播放清單
- 你正在建構 TV 應用程式。
不過,並非所有用途都適合 MediaSession
。建議您參考
請在下列情況只使用 Player
:
- 您刊登了短篇內容,也就是使用者參與和互動的管道 至關重要
- Google 不會提供單一影片,例如使用者正在捲動瀏覽清單 而且畫面上會同時顯示多部影片
- 你正在播放一次性的介紹或說明影片, 預期使用者會主動觀看
- 您的內容具有隱私權機密,您不希望外部程序: 存取媒體中繼資料 (例如瀏覽器的無痕模式)
如果您的用途與上述任一情況不符,請考慮您是否
當使用者不主動互動時,您的應用程式會繼續播放
與內容互動如果答案是肯定的,則建議您選擇
MediaSession
。如果答案為否,則建議使用 Player
。
建立媒體工作階段
媒體工作階段會與管理的播放器一起運作。您可以建構
使用 Context
和 Player
物件的媒體工作階段。您應建立和
視需要初始化媒體工作階段,例如 onStart()
或
Activity
或 Fragment
的 onResume()
生命週期方法,或 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 之間的互動。](https://developer.android.google.cn/static/guide/topics/media/images/backgroundcontrols.png?hl=zh-tw)
當控制器準備連線到媒體工作階段時,
onConnect()
敬上
方法。您可以使用 Google 提供的 ControllerInfo
決定是否要接受。
或拒絕
要求。如需查看接受連線要求的範例,請參閱宣告
可用的指令部分。
連線後,控制器便可傳送播放指令給工作階段。
然後將這些指令委派給玩家。播放與播放清單
自動處理 Player
介面中定義的指令
會很有幫助
其他回呼方法可讓您處理
自訂播放指令和
修改播放清單)。
這些回呼也包含 ControllerInfo
物件,因此您可以修改
您如何針對各控制器逐一回應各項要求
修改播放清單
媒體工作階段可以直接修改播放器的播放清單,詳情請參閱:
這個
播放清單的 ExoPlayer 指南。
如果符合任一條件,控管者也能修改播放清單
COMMAND_SET_MEDIA_ITEM
或 COMMAND_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.addListener
和 Player.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. } });