Android 中的媒体控件位于“快捷设置”附近。来自多个应用的会话排列在一个可滑动的轮播界面中。轮播界面会列出会话 顺序如下:
- 在手机本地播放的会话流
- 远程会话流,例如在外部设备上检测到的会话或投射会话
- 可继续播放的以前的会话(按上次播放的顺序排列)
从 Android 13(API 级别 33)开始,为了确保用户可以访问丰富的
一组媒体控件,适用于播放媒体的应用、媒体控件上的操作按钮
衍生自 Player
状态。
这样,您就可以呈现一组一致的媒体控件, 跨设备媒体控制体验。
图 1 显示了其在手机和平板电脑设备上的显示效果示例。 。
<ph type="x-smartling-placeholder">根据 Player
状态,系统最多会显示五个操作按钮:
如下表所示。在紧凑模式下,只有前三项操作
。这与其他媒体控件的
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 extra 包含键 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 extra 包含键 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 之前的行为
为实现向后兼容性,系统界面继续提供备用布局
针对未更新为以 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 extra 包含键 SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV 的 true 布尔值。 |
|
3 | 下一首 | PlaybackState 操作包括 ACTION_SKIP_TO_NEXT 。 |
自定义 | PlaybackState 操作不包含 ACTION_SKIP_TO_NEXT ,而 PlaybackState 自定义操作包含尚未执行的自定义操作。 |
|
未连接 | PlaybackState extra 包含键 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
实现
设备启动后,系统会查找最近使用过的五个媒体应用,并提供可用于在每个应用中重新开始播放媒体内容的控件。
系统会尝试通过来自 SystemUI 的连接与您的 MediaBrowserService
联系。您的应用必须允许此类连接,否则无法支持“继续播放媒体内容”功能。
您可以使用软件包名称 com.android.systemui
和签名来标识和验证来自 SystemUI 的连接。SystemUI 使用平台签名进行签名。您可以在 UAMP 应用中找到有关如何检查平台签名的示例。
为了支持“继续播放媒体内容”功能,MediaBrowserService
必须实现以下行为:
onGetRoot()
必须快速返回非 null 的根。其他复杂逻辑应在onLoadChildren()
中处理对根媒体 ID 调用
onLoadChildren()
时,结果必须包含 FLAG_PLAYABLE 子项。收到 EXTRA_RECENT 查询时,
MediaBrowserService
应返回最近播放的媒体项。返回的值应为实际媒体项而非通用函数。MediaBrowserService
必须提供适当的 MediaDescription 以及非空标题和副标题。它还应设置一个图标 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); }