Элементы управления мультимедиа в 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 | В настройках кнопок мультимедиа содержится пользовательская кнопка для CommandButton.SLOT_BACK | Обычай |
Доступны команды проигрывателя COMMAND_SEEK_TO_PREVIOUS или COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM . | Предыдущий | |
| Ни пользовательская кнопка, ни одна из перечисленных команд недоступны. | Пустой | |
| 3 | В настройках кнопок мультимедиа содержится пользовательская кнопка для CommandButton.SLOT_FORWARD | Обычай |
Доступны команды проигрывателя COMMAND_SEEK_TO_NEXT или COMMAND_SEEK_TO_NEXT_MEDIA_ITEM . | Следующий | |
| Ни пользовательская кнопка, ни одна из перечисленных команд недоступны. | Пустой | |
| 4 | В настройках кнопок мультимедиа содержится пользовательская кнопка для CommandButton.SLOT_OVERFLOW , которая еще не размещена. | Обычай |
| 5 | В настройках кнопок мультимедиа содержится пользовательская кнопка для CommandButton.SLOT_OVERFLOW , которая еще не размещена. | Обычай |
Пользовательские кнопки переполнения размещаются в том порядке, в котором они были добавлены в настройки кнопок мультимедиа.
Настройка кнопок управления
Для настройки управления мультимедиа в Jetpack Media3 можно задать параметры кнопок управления мультимедиа для сессии и доступные команды контроллеров.
Создайте объект
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(CommandButton.ICON_HEART_UNFILLED) .setDisplayName("Save to favorites") .setSessionCommand(customCommandFavorites) .build() val player = ExoPlayer.Builder(this).build() // Build the session with a custom layout. mediaSession = MediaSession.Builder(this, player) .setCallback(MyCallback()) .setMediaButtonPreferences(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) .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(CommandButton.ICON_HEART_UNFILLED) .setDisplayName("Save to favorites") .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()) .setMediaButtonPreferences(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) .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, обрабатываются медиасессией автоматически.
Инструкции по добавлению пользовательских команд см. в разделе «Добавление пользовательских команд».
Поддержите возобновление работы СМИ.
Функция возобновления воспроизведения позволяет пользователям перезапускать предыдущие сеансы из карусели без необходимости запуска приложения. После начала воспроизведения пользователь взаимодействует с элементами управления воспроизведением обычным способом.
Функцию возобновления воспроизведения можно включить и выключить с помощью приложения «Настройки», в разделе « Звук» > «Медиа» . Пользователь также может получить доступ к настройкам, нажав на значок шестеренки, который появляется после свайпа по развернутой карусели.
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)
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 , а вместо этого добавьте дополнительные параметры в сессию:
Котлин
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 . Эти действия будут отображаться в порядке их добавления.
Котлин
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 . Вам необходимо зарегистрировать функцию обратного вызова, которая сможет корректно реагировать на эти события.
Котлин
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); } } };
Возобновление работы СМИ
Чтобы ваше приложение-плеер отображалось в области быстрых настроек, необходимо создать уведомление 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)
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); }
Поведение до 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()
Java
import androidx.core.app.NotificationCompat; import androidx.media3.session.MediaStyleNotificationHelper; NotificationCompat.Builder notification = new 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(new MediaStyleNotificationHelper.MediaStyle(mediaSession) .setShowActionsInCompactView(1 /* #1: pause button */)) .setContentTitle("Wonderful music") .setContentText("My Awesome Band") .setLargeIcon(albumArtBitmap) .build();