你可以在 Android 的「快速設定」附近找到媒體控制項,工作階段來源: 滑動式輪轉介面會一次排列多個應用程式。輪轉介面會列出工作階段 順序:
- 在手機上播放串流內容
- 遠端串流,例如在外部裝置或投放工作階段偵測到的內容
- 先前可續傳的工作階段 (按照上次遊玩順序排列)
自 Android 13 (API 級別 33) 起,請確保使用者能夠存取
用於播放媒體的應用程式媒體控制項、媒體控制項上的動作按鈕組合
衍生自 Player
狀態
如此一來,就能呈現一致的媒體控制項組合,以及強化細節 跨裝置的媒體控制體驗
圖 1 舉例說明此應用程式在手機和平板電腦上的外觀。 。
系統會根據 Player
狀態顯示最多五個動作按鈕,如
使用。在密集模式下,系統只會顯示前三個動作
版位。這與媒體控制項在其他 Google 服務中的呈現方式一致
Android 平台,例如 Auto、Google 助理和 Wear OS。
版位 | 條件 | 動作 |
---|---|---|
1 |
playWhenReady 敬上
為 false 或目前的播放程序
狀態為 STATE_ENDED 。
|
播放 |
playWhenReady 為 true,且目前的播放狀態為 STATE_BUFFERING 。
|
載入旋轉圖示 | |
playWhenReady 為 true,且目前的播放狀態為 STATE_READY 。 |
暫停 | |
2 | 可以使用播放器指令 COMMAND_SEEK_TO_PREVIOUS 或 COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM 。 |
上一頁 |
無論是玩家指令 COMMAND_SEEK_TO_PREVIOUS 或 COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM ,系統都不會使用。此外,您也可以透過自訂版面配置中尚未放置的自訂指令來填滿版位。 |
自訂 | |
(Media3 尚未支援) PlaybackState extras 鍵包含鍵 EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV 的 true 布林值。 |
空白 | |
3 | 可以使用播放器指令 COMMAND_SEEK_TO_NEXT 或 COMMAND_SEEK_TO_NEXT_MEDIA_ITEM 。 |
繼續 |
無論是玩家指令 COMMAND_SEEK_TO_NEXT 或 COMMAND_SEEK_TO_NEXT_MEDIA_ITEM ,系統都不會使用。此外,您也可以透過自訂版面配置中尚未放置的自訂指令來填滿版位。 |
自訂 | |
(Media3 尚未支援) PlaybackState extras 鍵包含鍵 EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT 的 true 布林值。 |
空白 | |
4 | 可在尚未放置的自訂版面配置中填入自訂指令來填滿版位。 | 自訂 |
5 | 可在尚未放置的自訂版面配置中填入自訂指令來填滿版位。 | 自訂 |
自訂指令的排列順序取決於加入 自訂版面配置
自訂指令按鈕
如要透過 Jetpack Media3 自訂系統媒體控制項,請按照下列步驟操作:
您可以設定工作階段的自訂版面配置和
(在實作 MediaSessionService
時):
在
onCreate()
中建構MediaSession
並定義自訂版面配置 指令按鈕在
MediaSession.Callback.onConnect()
, 藉由定義可用的指令來授權控制器,包括 自訂指令 在ConnectionResult
中。在
MediaSession.Callback.onCustomCommand()
, 回應使用者選取的自訂指令。
Kotlin
class PlaybackService : MediaSessionService() { private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY) private var mediaSession: MediaSession? = null override fun onCreate() { super.onCreate() val favoriteButton = CommandButton.Builder() .setDisplayName("Save to favorites") .setIconResId(R.drawable.favorite_icon) .setSessionCommand(customCommandFavorites) .build() val player = ExoPlayer.Builder(this).build() // Build the session with a custom layout. mediaSession = MediaSession.Builder(this, player) .setCallback(MyCallback()) .setCustomLayout(ImmutableList.of(favoriteButton)) .build() } private inner class MyCallback : MediaSession.Callback { override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo ): ConnectionResult { // Set available player and session commands. return AcceptedResultBuilder(session) .setAvailablePlayerCommands( ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() .remove(COMMAND_SEEK_TO_NEXT) .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .remove(COMMAND_SEEK_TO_PREVIOUS) .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) .build() ) .setAvailableSessionCommands( ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() .add(customCommandFavorites) .build() ) .build() } override fun onCustomCommand( session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle ): ListenableFuture{ if (customCommand.customAction == ACTION_FAVORITES) { // Do custom logic here saveToFavorites(session.player.currentMediaItem) return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } return super.onCustomCommand(session, controller, customCommand, args) } } }
Java
public class PlaybackService extends MediaSessionService { private static final SessionCommand CUSTOM_COMMAND_FAVORITES = new SessionCommand("ACTION_FAVORITES", Bundle.EMPTY); @Nullable private MediaSession mediaSession; public void onCreate() { super.onCreate(); CommandButton favoriteButton = new CommandButton.Builder() .setDisplayName("Save to favorites") .setIconResId(R.drawable.favorite_icon) .setSessionCommand(CUSTOM_COMMAND_FAVORITES) .build(); Player player = new ExoPlayer.Builder(this).build(); // Build the session with a custom layout. mediaSession = new MediaSession.Builder(this, player) .setCallback(new MyCallback()) .setCustomLayout(ImmutableList.of(favoriteButton)) .build(); } private static class MyCallback implements MediaSession.Callback { @Override public ConnectionResult onConnect( MediaSession session, MediaSession.ControllerInfo controller) { // Set available player and session commands. return new AcceptedResultBuilder(session) .setAvailablePlayerCommands( ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() .remove(COMMAND_SEEK_TO_NEXT) .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .remove(COMMAND_SEEK_TO_PREVIOUS) .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) .build()) .setAvailableSessionCommands( ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() .add(CUSTOM_COMMAND_FAVORITES) .build()) .build(); } public ListenableFutureonCustomCommand( MediaSession session, MediaSession.ControllerInfo controller, SessionCommand customCommand, Bundle args) { if (customCommand.customAction.equals(CUSTOM_COMMAND_FAVORITES.customAction)) { // Do custom logic here saveToFavorites(session.getPlayer().getCurrentMediaItem()); return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS)); } return MediaSession.Callback.super.onCustomCommand( session, controller, customCommand, args); } } }
進一步瞭解如何設定 MediaSession
,以便將
系統可以連結到你的媒體應用程式,請參閱
將控制權授予其他用戶端。
使用 Jetpack Media3 時,當您實作 MediaSession
時,PlaybackState
會自動與媒體播放器保持最新狀態。同樣地,
實作 MediaSessionService
,程式庫會自動發布
MediaStyle
通知
並且保持最新狀態
回應動作按鈕
當使用者輕觸系統媒體控制項中的動作按鈕時,系統的
MediaController
會將播放指令傳送至 MediaSession
。
接著,MediaSession
會將這些指令委派給玩家。指令
在 Media3 的 Player
中定義的
介面會自動處理媒體介面
會很有幫助
請參閱新增自訂指令 ,瞭解如何回應自訂指令。
Android 13 以下版本的行為
為顧及回溯相容性,系統 UI 會持續提供替代版面配置
針對未更新至 Android 13 的應用程式使用通知動作。
或不含PlaybackState
資訊動作按鈕是
衍生自 MediaStyle
附加的 Notification.Action
清單
通知。系統會按照動作順序顯示最多五個動作
已新增 。在精簡模式中,系統最多會顯示三個按鈕,
傳入 setShowActionsInCompactView()
的值。
自訂動作的排列順序取決於加入
PlaybackState
。
以下程式碼範例說明如何在 MediaStyle 中新增動作 通知:
Kotlin
import androidx.core.app.NotificationCompat import androidx.media3.session.MediaStyleNotificationHelper var notification = NotificationCompat.Builder(context, CHANNEL_ID) // Show controls on lock screen even when user hides sensitive content. .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSmallIcon(R.drawable.ic_stat_player) // Add media control buttons that invoke intents in your media service .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0 .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1 .addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2 // Apply the media style template .setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession) .setShowActionsInCompactView(1 /* #1: pause button */)) .setContentTitle("Wonderful music") .setContentText("My Awesome Band") .setLargeIcon(albumArtBitmap) .build()
Java
import androidx.core.app.NotificationCompat; import androidx.media3.session.MediaStyleNotificationHelper; NotificationCompat.Builder notification = new NotificationCompat.Builder(context, CHANNEL_ID) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setSmallIcon(R.drawable.ic_stat_player) .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) .addAction(R.drawable.ic_next, "Next", nextPendingIntent) .setStyle(new MediaStyleNotificationHelper.MediaStyle(mediaSession) .setShowActionsInCompactView(1 /* #1: pause button */)) .setContentTitle("Wonderful music") .setContentText("My Awesome Band") .setLargeIcon(albumArtBitmap) .build();
支援媒體繼續播放功能
使用者可透過媒體恢復功能,透過輪轉介面重新啟動先前的工作階段。 不必啟動應用程式開始播放時,使用者會與影片互動 播放媒體控制項的方式
你可以使用「設定」應用程式 開啟或關閉繼續播放功能 > 媒體選項。使用者也可以透過下列方式存取「設定」: 滑動展開的輪轉介面後,輕觸畫面上顯示的齒輪圖示。
Media3 提供的 API 可更輕鬆地支援媒體繼續作業。詳情請參閱 使用 Media3 繼續播放內容 ,瞭解如何實作這項功能。
使用舊版媒體 API
本節說明如何使用 舊版 MediaCompat API
系統會從
MediaSession
的 MediaMetadata
,並在可用時顯示:
METADATA_KEY_ALBUM_ART_URI
METADATA_KEY_TITLE
METADATA_KEY_DISPLAY_TITLE
METADATA_KEY_ARTIST
METADATA_KEY_DURATION
(如果未指定時間長度,但未設定跳轉滑桿 顯示進度)
為確保能取得有效且正確的媒體控制通知,
設定 METADATA_KEY_TITLE
或 METADATA_KEY_DISPLAY_TITLE
的值
中繼資料。
媒體播放器會顯示目前播放的經過時間
媒體,以及對應至 MediaSession
的跳轉滑桿
PlaybackState
。
媒體播放器會顯示目前正在播放媒體的進度,以及
對應至 MediaSession
PlaybackState
的跳轉滑桿。跳轉滑桿
使用者可以變更位置並顯示媒體的經過時間
項目。如要啟用跳轉滑桿,
PlaybackState.Builder#setActions
且包含 ACTION_SEEK_TO
。
版位 | 動作 | 條件 |
---|---|---|
1 | 播放 |
PlaybackState 目前的狀態為下列其中一種:
|
載入旋轉圖示 |
PlaybackState 目前的狀態為下列其中一種:
|
|
暫停 | PlaybackState 目前的狀態並非以上皆是。 |
|
2 | 上一頁 | PlaybackState 動作包含 ACTION_SKIP_TO_PREVIOUS 。 |
自訂 | PlaybackState 動作不含 ACTION_SKIP_TO_PREVIOUS ,且 PlaybackState 個自訂動作包含尚未放置的自訂動作。 |
|
空白 | PlaybackState 「extras」包含鍵 SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV 的 true 布林值。 |
|
3 | 繼續 | PlaybackState 動作包含 ACTION_SKIP_TO_NEXT 。 |
自訂 | PlaybackState 動作不含 ACTION_SKIP_TO_NEXT ,且 PlaybackState 個自訂動作包含尚未放置的自訂動作。 |
|
空白 | PlaybackState 「extras」包含鍵 SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT 的 true 布林值。 |
|
4 | 自訂 | PlaybackState 自訂動作包含尚未加入的自訂動作。 |
5 | 自訂 | PlaybackState 自訂動作包含尚未加入的自訂動作。 |
新增標準動作
以下程式碼範例說明如何新增 PlaybackState
標準和
自訂動作
如要播放、暫停、上一首和下一集,請在以下位置設定這些動作:
媒體工作階段的 PlaybackState
。
Kotlin
val session = MediaSessionCompat(context, TAG) val playbackStateBuilder = PlaybackStateCompat.Builder() val style = NotificationCompat.MediaStyle() // For this example, the media is currently paused: val state = PlaybackStateCompat.STATE_PAUSED val position = 0L val playbackSpeed = 1f playbackStateBuilder.setState(state, position, playbackSpeed) // And the user can play, skip to next or previous, and seek val stateActions = PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PLAY_PAUSE or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or PlaybackStateCompat.ACTION_SKIP_TO_NEXT or PlaybackStateCompat.ACTION_SEEK_TO // adding the seek action enables seeking with the seekbar playbackStateBuilder.setActions(stateActions) // ... do more setup here ... session.setPlaybackState(playbackStateBuilder.build()) style.setMediaSession(session.sessionToken) notificationBuilder.setStyle(style)
Java
MediaSessionCompat session = new MediaSessionCompat(context, TAG); PlaybackStateCompat.Builder playbackStateBuilder = new PlaybackStateCompat.Builder(); NotificationCompat.MediaStyle style = new NotificationCompat.MediaStyle(); // For this example, the media is currently paused: int state = PlaybackStateCompat.STATE_PAUSED; long position = 0L; float playbackSpeed = 1f; playbackStateBuilder.setState(state, position, playbackSpeed); // And the user can play, skip to next or previous, and seek long stateActions = PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SEEK_TO; // adding this enables the seekbar thumb playbackStateBuilder.setActions(stateActions); // ... do more setup here ... session.setPlaybackState(playbackStateBuilder.build()); style.setMediaSession(session.getSessionToken()); notificationBuilder.setStyle(style);
如果在上一個或下一個版位中沒有任何按鈕,請勿新增
ACTION_SKIP_TO_PREVIOUS
或 ACTION_SKIP_TO_NEXT
,然後再將額外項目新增至
這個工作階段:
Kotlin
session.setExtras(Bundle().apply { putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true) putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true) })
Java
Bundle extras = new Bundle(); extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true); extras.putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true); session.setExtras(extras);
新增自訂動作
如要在媒體控制項上顯示其他動作,您可以建立
PlaybackStateCompat.CustomAction
敬上
請改為將其新增至 PlaybackState
這些動作會顯示在
新增順序
Kotlin
val customAction = PlaybackStateCompat.CustomAction.Builder( "com.example.MY_CUSTOM_ACTION", // action ID "Custom Action", // title - used as content description for the button R.drawable.ic_custom_action ).build() playbackStateBuilder.addCustomAction(customAction)
Java
PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction.Builder( "com.example.MY_CUSTOM_ACTION", // action ID "Custom Action", // title - used as content description for the button R.drawable.ic_custom_action ).build(); playbackStateBuilder.addCustomAction(customAction);
回應 PlaybackState 動作
使用者輕觸按鈕時,SystemUI 會使用
MediaController.TransportControls
敬上
將指令傳回 MediaSession
。您必須註冊回呼
能妥善回應這些事件
Kotlin
val callback = object: MediaSession.Callback() { override fun onPlay() { // start playback } override fun onPause() { // pause playback } override fun onSkipToPrevious() { // skip to previous } override fun onSkipToNext() { // skip to next } override fun onSeekTo(pos: Long) { // jump to position in track } override fun onCustomAction(action: String, extras: Bundle?) { when (action) { CUSTOM_ACTION_1 -> doCustomAction1(extras) CUSTOM_ACTION_2 -> doCustomAction2(extras) else -> { Log.w(TAG, "Unknown custom action $action") } } } } session.setCallback(callback)
Java
MediaSession.Callback callback = new MediaSession.Callback() { @Override public void onPlay() { // start playback } @Override public void onPause() { // pause playback } @Override public void onSkipToPrevious() { // skip to previous } @Override public void onSkipToNext() { // skip to next } @Override public void onSeekTo(long pos) { // jump to position in track } @Override public void onCustomAction(String action, Bundle extras) { if (action.equals(CUSTOM_ACTION_1)) { doCustomAction1(extras); } else if (action.equals(CUSTOM_ACTION_2)) { doCustomAction2(extras); } else { Log.w(TAG, "Unknown custom action " + action); } } };
繼續播放媒體
如要讓播放器應用程式出現在快速設定區:
您必須使用有效的 MediaSession
權杖建立 MediaStyle
通知。
如要顯示 MediaStyle 通知的標題,請使用
NotificationBuilder.setContentTitle()
。
如要顯示媒體播放器的品牌圖示,請使用
NotificationBuilder.setSmallIcon()
。
如要支援繼續播放功能,應用程式必須實作 MediaBrowserService
和 MediaSession
。MediaSession
必須實作 onPlay()
回呼。
MediaBrowserService
實作
裝置啟動後,系統會尋找最近使用的五個媒體 應用程式,並提供控制選項,可用於從各個應用程式重新開始播放。
系統會嘗試透過以下連線與「MediaBrowserService
」聯絡:
SystemUI。您的應用程式必須允許這類連線,否則無法支援
繼續播放。
可以使用套件名稱來識別和驗證 SystemUI 的連線
com.android.systemui
和簽章。SystemUI 是透過平台簽署
簽章。以下舉例說明如何檢查平台簽章
位於 UAMP 應用程式中。
為了支援繼續播放功能,MediaBrowserService
必須
實作這些行為
onGetRoot()
必須快速傳回非空值的根層級。其他複雜邏輯應 在「onLoadChildren()
」中處理時間 根媒體 ID 上呼叫了
onLoadChildren()
,結果必須包含 FLAG_PLAYABLE 孩子。MediaBrowserService
應傳回最近播放的媒體項目, 他們會收到 EXTRA_RECENT 。傳回的值應為實際媒體項目,而非一般媒體項目 函式。「
MediaBrowserService
」必須提供適當的 MediaDescription 含有非空白 title 和 副標題。 也應該設定 圖示 URI 或 「點陣圖」圖示。
以下程式碼範例說明如何實作 onGetRoot()
。
Kotlin
override fun onGetRoot( clientPackageName: String, clientUid: Int, rootHints: Bundle? ): BrowserRoot? { ... // Verify that the specified package is SystemUI. You'll need to write your // own logic to do this. if (isSystem(clientPackageName, clientUid)) { rootHints?.let { if (it.getBoolean(BrowserRoot.EXTRA_RECENT)) { // Return a tree with a single playable media item for resumption. val extras = Bundle().apply { putBoolean(BrowserRoot.EXTRA_RECENT, true) } return BrowserRoot(MY_RECENTS_ROOT_ID, extras) } } // You can return your normal tree if the EXTRA_RECENT flag is not present. return BrowserRoot(MY_MEDIA_ROOT_ID, null) } // Return an empty tree to disallow browsing. return BrowserRoot(MY_EMPTY_ROOT_ID, null)
Java
@Override public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { ... // Verify that the specified package is SystemUI. You'll need to write your // own logic to do this. if (isSystem(clientPackageName, clientUid)) { if (rootHints != null) { if (rootHints.getBoolean(BrowserRoot.EXTRA_RECENT)) { // Return a tree with a single playable media item for resumption. Bundle extras = new Bundle(); extras.putBoolean(BrowserRoot.EXTRA_RECENT, true); return new BrowserRoot(MY_RECENTS_ROOT_ID, extras); } } // You can return your normal tree if the EXTRA_RECENT flag is not present. return new BrowserRoot(MY_MEDIA_ROOT_ID, null); } // Return an empty tree to disallow browsing. return new BrowserRoot(MY_EMPTY_ROOT_ID, null); }