В манифесте приложения должен быть объявлен класс MediaBrowserService с фильтром intent. Вы можете выбрать собственное название сервиса. В примере ниже выбрано название MediaPlaybackService.
<service android:name=".MediaPlaybackService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
Инициализация сеанса воспроизведения медиаконтента
Когда сервис получает метод обратного вызова жизненного цикла onCreate(), он должен выполнить следующие действия:
- Создайте и инициализируйте медиасеанс.
- Настройте обратный вызов сеанса воспроизведения медиаконтента
- Как настроить токен сеанса мультимедиа
Ниже приведен пример кода на onCreate(), в котором выполняются эти действия.
Kotlin
private const val MY_MEDIA_ROOT_ID = "media_root_id" private const val MY_EMPTY_MEDIA_ROOT_ID = "empty_root_id" class MediaPlaybackService : MediaBrowserServiceCompat() { private var mediaSession: MediaSessionCompat? = null private lateinit var stateBuilder: PlaybackStateCompat.Builder override fun onCreate() { super.onCreate() // Create a MediaSessionCompat mediaSession = MediaSessionCompat(baseContext, LOG_TAG).apply { // Enable callbacks from MediaButtons and TransportControls setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS ) // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player stateBuilder = PlaybackStateCompat.Builder() .setActions(PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PLAY_PAUSE ) setPlaybackState(stateBuilder.build()) // MySessionCallback() has methods that handle callbacks from a media controller setCallback(MySessionCallback()) // Set the session's token so that client activities can communicate with it. setSessionToken(sessionToken) } } }
Java
public class MediaPlaybackService extends MediaBrowserServiceCompat { private static final String MY_MEDIA_ROOT_ID = "media_root_id"; private static final String MY_EMPTY_MEDIA_ROOT_ID = "empty_root_id"; private MediaSessionCompat mediaSession; private PlaybackStateCompat.Builder stateBuilder; @Override public void onCreate() { super.onCreate(); // Create a MediaSessionCompat mediaSession = new MediaSessionCompat(context, LOG_TAG); // Enable callbacks from MediaButtons and TransportControls mediaSession.setFlags( MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); // Set an initial PlaybackState with ACTION_PLAY, so media buttons can start the player stateBuilder = new PlaybackStateCompat.Builder() .setActions( PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PLAY_PAUSE); mediaSession.setPlaybackState(stateBuilder.build()); // MySessionCallback() has methods that handle callbacks from a media controller mediaSession.setCallback(new MySessionCallback()); // Set the session's token so that client activities can communicate with it. setSessionToken(mediaSession.getSessionToken()); } }
Управление подключениями клиентов
В MediaBrowserService есть два метода, которые обрабатывают подключения клиентов:
onGetRoot() управляет доступом к сервису, а
onLoadChildren()
позволяет клиенту создавать и показывать меню иерархии контента MediaBrowserService.
Управление подключениями клиентов с помощью onGetRoot()
Метод onGetRoot() возвращает корневой узел иерархии контента. Если метод возвращает null, подключение отклоняется.
Чтобы клиенты могли подключаться к вашему сервису и просматривать его медиаконтент, метод onGetRoot() должен возвращать ненулевой объект BrowserRoot, который является корневым идентификатором, представляющим иерархию вашего контента.
Чтобы клиенты могли подключаться к MediaSession без просмотра, onGetRoot() по-прежнему должен возвращать ненулевой BrowserRoot, но идентификатор корневого каталога должен представлять пустую иерархию контента.
Типичная реализация onGetRoot() может выглядеть следующим образом:
Kotlin
override fun onGetRoot( clientPackageName: String, clientUid: Int, rootHints: Bundle? ): MediaBrowserServiceCompat.BrowserRoot { // (Optional) Control the level of access for the specified package name. // You'll need to write your own logic to do this. return if (allowBrowsing(clientPackageName, clientUid)) { // Returns a root ID that clients can use with onLoadChildren() to retrieve // the content hierarchy. MediaBrowserServiceCompat.BrowserRoot(MY_MEDIA_ROOT_ID, null) } else { // Clients can connect, but this BrowserRoot is an empty hierarchy // so onLoadChildren returns nothing. This disables the ability to browse for content. MediaBrowserServiceCompat.BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null) } }
Java
@Override public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) { // (Optional) Control the level of access for the specified package name. // You'll need to write your own logic to do this. if (allowBrowsing(clientPackageName, clientUid)) { // Returns a root ID that clients can use with onLoadChildren() to retrieve // the content hierarchy. return new BrowserRoot(MY_MEDIA_ROOT_ID, null); } else { // Clients can connect, but this BrowserRoot is an empty hierarchy // so onLoadChildren returns nothing. This disables the ability to browse for content. return new BrowserRoot(MY_EMPTY_MEDIA_ROOT_ID, null); } }
В некоторых случаях вам может потребоваться контролировать, кто может подключаться к вашему MediaBrowserService. Один из способов – использовать список контроля доступа (ACL), в котором указано, какие подключения разрешены, или, наоборот, перечислены запрещенные подключения. Пример реализации списка управления доступом, разрешающего определенные подключения, можно найти в классе PackageValidator в примере приложения Universal Android Music Player.
Рекомендуем использовать разные иерархии контента в зависимости от типа клиента, выполняющего запрос. В частности, Android Auto ограничивает
взаимодействие пользователей с аудиоприложениями. Подробнее о воспроизведении аудио в Android Auto… Вы можете посмотреть на clientPackageName во время подключения, чтобы определить тип клиента, и вернуть разные BrowserRoot в зависимости от клиента (или rootHints, если они есть).
Передача контента в onLoadChildren()
После подключения клиент может перемещаться по иерархии контента, выполняя повторные вызовы MediaBrowserCompat.subscribe(), чтобы создать локальное представление пользовательского интерфейса. Метод subscribe() отправляет обратный вызов onLoadChildren() в сервис, который возвращает список объектов MediaBrowser.MediaItem.
У каждого объекта MediaItem есть уникальный идентификатор, который представляет собой непрозрачный токен. Когда клиент хочет открыть подменю или воспроизвести объект, он передает идентификатор. Ваш сервис должен сопоставить идентификатор с нужным узлом меню или элементом контента.
Простая реализация onLoadChildren() может выглядеть следующим образом:
Kotlin
override fun onLoadChildren( parentMediaId: String, result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>> ) { // Browsing not allowed if (MY_EMPTY_MEDIA_ROOT_ID == parentMediaId) { result.sendResult(null) return } // Assume for example that the music catalog is already loaded/cached. val mediaItems = emptyList<MediaBrowserCompat.MediaItem>() // Check if this is the root menu: if (MY_MEDIA_ROOT_ID == parentMediaId) { // Build the MediaItem objects for the top level, // and put them in the mediaItems list... } else { // Examine the passed parentMediaId to see which submenu we're at, // and put the children of that menu in the mediaItems list... } result.sendResult(mediaItems) }
Java
@Override public void onLoadChildren(final String parentMediaId, final Result<List<MediaItem>> result) { // Browsing not allowed if (TextUtils.equals(MY_EMPTY_MEDIA_ROOT_ID, parentMediaId)) { result.sendResult(null); return; } // Assume for example that the music catalog is already loaded/cached. List<MediaItem> mediaItems = new ArrayList<>(); // Check if this is the root menu: if (MY_MEDIA_ROOT_ID.equals(parentMediaId)) { // Build the MediaItem objects for the top level, // and put them in the mediaItems list... } else { // Examine the passed parentMediaId to see which submenu we're at, // and put the children of that menu in the mediaItems list... } result.sendResult(mediaItems); }
Примечание. Объекты MediaItem, передаваемые MediaBrowserService, не должны содержать растровые изображения значков. Вместо этого используйте Uri, вызывая setIconUri() при создании MediaDescription для каждого товара.
Пример реализации onLoadChildren() можно найти в примере приложения Universal Android Music Player.
Жизненный цикл сервиса просмотра медиаконтента
Поведение сервиса Android зависит от того, запущен ли он или привязан к одному или нескольким клиентам. После создания сервис можно запустить, привязать или сделать и то, и другое. Во всех этих состояниях сервис полностью функционален и может выполнять работу, для которой он предназначен. Разница заключается в том, как долго сервис будет существовать. Привязанная служба не уничтожается, пока все ее привязанные клиенты не отвяжутся. Запущенный сервис можно явно остановить и уничтожить (при условии, что он больше не привязан ни к каким клиентам).
Когда компонент MediaBrowser, выполняющийся в другом действии, подключается к MediaBrowserService, он связывает действие с сервисом, делая сервис связанным (но не запущенным). Такой режим работы по умолчанию встроен в класс MediaBrowserServiceCompat.
Сервис, который только привязан (и не запущен), уничтожается, когда все его клиенты отменяют привязку. Если в этот момент связь с интерфейсом прервется, сервис будет удален. Если вы ещё не слушали музыку, это нормально. Однако когда воспроизведение начинается, пользователь, вероятно, ожидает, что оно продолжится даже после переключения приложений. Не нужно удалять проигрыватель, когда вы отменяете привязку интерфейса для работы с другим приложением.
Поэтому при начале воспроизведения необходимо запустить сервис, вызвав метод startService(). Запущенный сервис должен быть явно остановлен, независимо от того, привязан он или нет. Это гарантирует, что проигрыватель продолжит работать, даже если управляющее действие пользовательского интерфейса будет отменено.
Чтобы остановить запущенный сервис, вызовите Context.stopService() или stopSelf(). Система останавливает и удаляет сервис как можно скорее. Однако если один или несколько клиентов все ещё привязаны к сервису, вызов для остановки сервиса откладывается до тех пор, пока все его клиенты не будут отвязаны.
Жизненный цикл MediaBrowserService зависит от того, как он создан, от количества связанных с ним клиентов и от вызовов, которые он получает от обратных вызовов сеанса мультимедиа. Подведем итоги:
- Сервис создается, когда он запускается в ответ на нажатие кнопки мультимедиа или когда с ним связывается действие (после подключения через
MediaBrowser). - Обратный вызов
onPlay()сеанса воспроизведения должен включать код, который вызываетstartService(). Это гарантирует, что сервис запустится и продолжит работать, даже если все связанные с ним действия пользовательского интерфейсаMediaBrowserбудут отменены. - Обратный вызов
onStop()должен вызыватьstopSelf(). Если служба была запущена, она будет остановлена. Кроме того, сервис удаляется, если с ним не связаны никакие действия. В противном случае сервис остается привязанным, пока не будут отвязаны все его действия. Если до завершения работы сервиса поступит последующий вызовstartService(), ожидающая остановка будет отменена.
На следующей блок-схеме показано, как управляется жизненный цикл сервиса. Счетчик переменных отслеживает количество связанных клиентов:

Как использовать уведомления MediaStyle с активной службой
Когда сервис воспроизводит контент, он должен работать в активном режиме. Это позволяет системе понять, что сервис выполняет полезную функцию и его не следует отключать, если в системе мало памяти. Активная служба должна показывать уведомление, чтобы пользователь знал о ней и мог управлять ею. Обратный вызов onPlay() должен перевести службу в активный режим. Обратите внимание, что в данном случае термин "активный режим" имеет особое значение. Android считает, что служба работает в активном режиме, но для пользователя проигрыватель работает в фоновом режиме, а на экране видно другое приложение.
Когда служба работает в активном режиме, она должна показывать уведомление, желательно с одним или несколькими элементами управления. Уведомление также должно содержать полезную информацию из метаданных сеанса.
Создайте и покажите уведомление, когда проигрыватель начнет воспроизведение. Лучше всего сделать это в методе MediaSessionCompat.Callback.onPlay().
В примере ниже используется NotificationCompat.MediaStyle, предназначенный для мультимедийных приложений. В нем показано, как создать уведомление, в котором отображаются метаданные и элементы управления транспортом. Удобный метод
getController()
позволяет создать контроллер мультимедиа непосредственно из сеанса мультимедиа.
Kotlin
// Given a media session and its context (usually the component containing the session) // Create a NotificationCompat.Builder // Get the session's metadata val controller = mediaSession.controller val mediaMetadata = controller.metadata val description = mediaMetadata.description val builder = NotificationCompat.Builder(context, channelId).apply { // Add the metadata for the currently playing track setContentTitle(description.title) setContentText(description.subtitle) setSubText(description.description) setLargeIcon(description.iconBitmap) // Enable launching the player by clicking the notification setContentIntent(controller.sessionActivity) // Stop the service when the notification is swiped away setDeleteIntent( MediaButtonReceiver.buildMediaButtonPendingIntent( context, PlaybackStateCompat.ACTION_STOP ) ) // Make the transport controls visible on the lockscreen setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // Add an app icon and set its accent color // Be careful about the color setSmallIcon(R.drawable.notification_icon) color = ContextCompat.getColor(context, R.color.primaryDark) // Add a pause button addAction( NotificationCompat.Action( R.drawable.pause, getString(R.string.pause), MediaButtonReceiver.buildMediaButtonPendingIntent( context, PlaybackStateCompat.ACTION_PLAY_PAUSE ) ) ) // Take advantage of MediaStyle features setStyle(android.support.v4.media.app.NotificationCompat.MediaStyle() .setMediaSession(mediaSession.sessionToken) .setShowActionsInCompactView(0) // Add a cancel button .setShowCancelButton(true) .setCancelButtonIntent( MediaButtonReceiver.buildMediaButtonPendingIntent( context, PlaybackStateCompat.ACTION_STOP ) ) ) } // Display the notification and place the service in the foreground startForeground(id, builder.build())
Java
// Given a media session and its context (usually the component containing the session) // Create a NotificationCompat.Builder // Get the session's metadata MediaControllerCompat controller = mediaSession.getController(); MediaMetadataCompat mediaMetadata = controller.getMetadata(); MediaDescriptionCompat description = mediaMetadata.getDescription(); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId); builder // Add the metadata for the currently playing track .setContentTitle(description.getTitle()) .setContentText(description.getSubtitle()) .setSubText(description.getDescription()) .setLargeIcon(description.getIconBitmap()) // Enable launching the player by clicking the notification .setContentIntent(controller.getSessionActivity()) // Stop the service when the notification is swiped away .setDeleteIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_STOP)) // Make the transport controls visible on the lockscreen .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // Add an app icon and set its accent color // Be careful about the color .setSmallIcon(R.drawable.notification_icon) .setColor(ContextCompat.getColor(context, R.color.primaryDark)) // Add a pause button .addAction(new NotificationCompat.Action( R.drawable.pause, getString(R.string.pause), MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE))) // Take advantage of MediaStyle features .setStyle(new MediaStyle() .setMediaSession(mediaSession.getSessionToken()) .setShowActionsInCompactView(0) // Add a cancel button .setShowCancelButton(true) .setCancelButtonIntent(MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_STOP))); // Display the notification and place the service in the foreground startForeground(id, builder.build());
При использовании уведомлений MediaStyle учитывайте поведение следующих настроек NotificationCompat:
- При использовании
setContentIntent()сервис запускается автоматически при нажатии на уведомление. Это очень удобно. - В "небезопасных" ситуациях, например на заблокированном экране, по умолчанию используется значение
VISIBILITY_PRIVATE. Скорее всего, вы хотите видеть элементы управления воспроизведением на заблокированном экране, поэтому лучше использоватьVISIBILITY_PUBLIC. - Будьте внимательны при выборе цвета фона. В обычном уведомлении на устройстве с Android 5.0 или более поздней версии цвет применяется только к фону маленького значка приложения. В уведомлениях MediaStyle до Android 7.0 цвет используется для всего фона уведомления. Проверьте цвет фона. Не используйте слишком яркие или флуоресцентные цвета.
Эти настройки доступны только при использовании NotificationCompat.MediaStyle:
- Используйте
setMediaSession(), чтобы связать уведомление с сеансом. Это позволяет сторонним приложениям и дополнительным устройствам получать доступ к сеансу и управлять им. - Используйте
setShowActionsInCompactView(), чтобы добавить до трех действий, которые будут показываться в стандартном представлении contentView уведомления. (в данном случае – кнопка паузы). - В Android 5.0 (уровень API 21) и более поздних версиях вы можете смахнуть уведомление, чтобы остановить проигрыватель, когда сервис больше не работает в активном режиме. В более ранних версиях это сделать нельзя. Чтобы пользователи могли убрать уведомление и остановить воспроизведение в версиях до Android 5.0 (уровень API 21), вы можете добавить кнопку отмены в правом верхнем углу уведомления, вызвав
setShowCancelButton(true)иsetCancelButtonIntent().
При добавлении кнопок паузы и отмены вам потребуется PendingIntent для прикрепления к действию воспроизведения. Метод MediaButtonReceiver.buildMediaButtonPendingIntent() преобразует действие PlaybackState в PendingIntent.
Включить просмотр медиаконтента по AVRCP
Помимо специальных приложений, таких как Android Auto, уровень Bluetooth системы также действует как клиент для вашего MediaBrowserService, чтобы упростить беспроводной удаленный просмотр каталога (AVRCP).
В Android 16 и Android 17 приложения, которые не используют Media3, должны предоставлять определенный объект activity с фильтром интентов, чтобы пройти проверку для просмотра.
Добавьте этот фильтр интентов в экспортированный объект activity в файле AndroidManifest.xml. Обратите внимание, что CATEGORY_DEFAULT намеренно опущен, чтобы ваше приложение не появлялось в общих меню "Открыть с помощью" для локальных аудиофайлов:
<activity
android:name=".BluetoothValidationActivity"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay"
android:excludeFromRecents="true"
android:noHistory="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="content" />
<data android:host="media" />
<!-- Specific path check used by Bluetooth stack for validation -->
<data android:pathPrefix="/internal/audio/media/" />
<data android:mimeType="audio/*" />
</intent-filter>
</activity>