Элементы управления мультимедиа в Android расположены рядом с быстрыми настройками. Сеансы из нескольких приложений организованы в пролистываемую карусель. В карусели сеансы перечислены в следующем порядке:
- Потоки, воспроизводимые локально на телефоне
- Удаленные потоки, например обнаруженные на внешних устройствах или в сеансах трансляции.
- Предыдущие возобновляемые сеансы в том порядке, в котором они проводились в последний раз.
Начиная с Android 13 (уровень API 33), чтобы гарантировать пользователям доступ к богатому набору элементов управления мультимедиа для приложений, воспроизводящих мультимедиа, кнопки действий на элементах управления мультимедиа получаются из состояния Player
.
Таким образом, вы можете предоставить согласованный набор элементов управления мультимедиа и более совершенный опыт управления мультимедиа на разных устройствах.
На рис. 1 показан пример того, как это выглядит на телефоне и планшете соответственно.
Система отображает до пяти кнопок действий в зависимости от состояния Player
, как описано в следующей таблице. В компактном режиме отображаются только первые три слота действий. Это соответствует тому, как элементы управления мультимедиа отображаются на других платформах Android, таких как Auto, Assistant и 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 недоступна, а пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. | Обычай | |
Дополнительные возможности сеанса включают true логическое значение для ключа EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_PREV . | Пустой | |
3 | Доступна команда проигрывателя COMMAND_SEEK_TO_NEXT или COMMAND_SEEK_TO_NEXT_MEDIA_ITEM . | Следующий |
Ни команда игрока COMMAND_SEEK_TO_NEXT , ни COMMAND_SEEK_TO_NEXT_MEDIA_ITEM недоступны, а пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. | Обычай | |
Дополнительные возможности сеанса включают true логическое значение для ключа EXTRAS_KEY_SLOT_RESERVATION_SEEK_TO_NEXT . | Пустой | |
4 | Пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. | Обычай |
5 | Пользовательская команда из пользовательского макета, которая еще не была размещена, доступна для заполнения слота. | Обычай |
Пользовательские команды размещаются в том порядке, в котором они были добавлены в пользовательский макет.
Настройте командные кнопки
Чтобы настроить системные элементы управления мультимедиа с помощью Jetpack Media3 , вы можете соответствующим образом установить собственный макет сеанса и доступные команды контроллеров при реализации MediaSessionService
:
В
onCreate()
создайтеMediaSession
и определите собственный макет командных кнопок.В
MediaSession.Callback.onConnect()
авторизуйте контроллеры, определив их доступные команды, включая пользовательские команды , вConnectionResult
.В
MediaSession.Callback.onCustomCommand()
ответьте на пользовательскую команду, выбранную пользователем.
Котлин
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) } } }
Ява
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
делегирует эти команды проигрывателю. Команды, определенные в интерфейсе Player
Media3, автоматически обрабатываются мультимедийным сеансом.
Инструкции о том, как реагировать на пользовательскую команду, см. в разделе Добавление пользовательских команд .
Поведение до версии Android 13
В целях обратной совместимости системный пользовательский интерфейс по-прежнему предоставляет альтернативный макет, в котором используются действия уведомлений для приложений, которые не обновляются до Android 13 или не включают информацию PlaybackState
. Кнопки действий извлекаются из списка Notification.Action
, прикрепленного к уведомлению MediaStyle
. Система отображает до пяти действий в том порядке, в котором они были добавлены. В компактном режиме отображается до трех кнопок, определяемых значениями, переданными в setShowActionsInCompactView()
.
Пользовательские действия размещаются в том порядке, в котором они были добавлены в PlaybackState
.
В следующем примере кода показано, как добавить действия в уведомление MediaStyle:
Котлин
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()
Ява
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-интерфейсов мультимедиа
В этом разделе объясняется, как интегрироваться с системными элементами управления мультимедиа с помощью устаревших API MediaCompat.
Система получает следующую информацию из MediaMetadata
объекта MediaSession
и отображает ее, когда она доступна:
-
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 включают true логическое значение для ключа SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV . | |
3 | Следующий | Действия PlaybackState включают ACTION_SKIP_TO_NEXT . |
Обычай | Действия PlaybackState не включают ACTION_SKIP_TO_NEXT , а настраиваемые действия PlaybackState включают настраиваемое действие, которое еще не было размещено. | |
Пустой | Дополнительные возможности PlaybackState включают true логическое значение для ключа SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT . | |
4 | Обычай | Настраиваемые действия PlaybackState включают настраиваемое действие, которое еще не было размещено. |
5 | Обычай | Настраиваемые действия PlaybackState включают настраиваемое действие, которое еще не было размещено. |
Добавьте стандартные действия
В следующих примерах кода показано, как добавить стандартные и настраиваемые действия PlaybackState
.
Для воспроизведения, паузы, предыдущего и следующего задайте эти действия в PlaybackState
для сеанса мультимедиа.
Котлин
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)
Ява
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
, а вместо этого добавьте в сеанс дополнительные функции:
Котлин
session.setExtras(Bundle().apply { putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true) putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true) })
Ява
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
. Эти действия показаны в порядке их добавления.
Котлин
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)
Ява
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
. Вам необходимо зарегистрировать обратный вызов, который сможет правильно реагировать на эти события.
Котлин
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)
Ява
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); } } };
Возобновление работы СМИ
Чтобы приложение проигрывателя отображалось в области настроек быстрых настроек, необходимо создать уведомление MediaStyle
с действительным токеном MediaSession
.
Чтобы отобразить заголовок уведомления MediaStyle, используйте NotificationBuilder.setContentTitle()
.
Чтобы отобразить значок бренда для медиаплеера, используйте NotificationBuilder.setSmallIcon()
.
Чтобы поддерживать возобновление воспроизведения, приложения должны реализовать MediaBrowserService
и MediaSession
. Ваш MediaSession
должен реализовать обратный вызов onPlay()
.
Реализация MediaBrowserService
После загрузки устройства система ищет пять последних использованных мультимедийных приложений и предоставляет элементы управления, которые можно использовать для возобновления воспроизведения из каждого приложения.
Система пытается связаться с вашим MediaBrowserService
с помощью соединения из SystemUI. Ваше приложение должно разрешать такие соединения, иначе оно не сможет поддерживать возобновление воспроизведения.
Подключения из SystemUI можно идентифицировать и проверить с помощью имени пакета com.android.systemui
и подписи. SystemUI подписан подписью платформы. Пример проверки подписи платформы можно найти в приложении UAMP .
Чтобы поддерживать возобновление воспроизведения, ваш MediaBrowserService
должен реализовать следующее поведение:
onGetRoot()
должен быстро возвращать ненулевой корень. Другая сложная логика должна обрабатываться вonLoadChildren()
Когда
onLoadChildren()
вызывается для идентификатора корневого носителя, результат должен содержать дочерний элемент FLAG_PLAYABLE .MediaBrowserService
должен возвращать последний воспроизведенный элемент мультимедиа при получении запроса EXTRA_RECENT . Возвращаемое значение должно быть реальным элементом мультимедиа, а не универсальной функцией.MediaBrowserService
должен предоставить соответствующее MediaDescription с непустым заголовком и подзаголовком . Он также должен установить URI значка или растровое изображение значка .
Следующие примеры кода иллюстрируют, как реализовать onGetRoot()
.
Котлин
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)
Ява
@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); }