媒體工作階段提供一種與音訊或視訊互動的方式
廣告。在 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 ,你就能透過錶面控制音訊播放。外部用戶端 使用媒體控制器向媒體應用程式發出播放指令。這些 所接收的指令,進而將指令委派給 媒體播放器中。
,瞭解如何調查及移除這項存取權。當控制器準備連線到媒體工作階段時,
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. } });