В вашем приложении необходимо объявить MediaBrowserService с помощью intent-filter в манифесте. Вы можете выбрать собственное имя службы. В следующем примере выбранное имя службы — MediaPlaybackService .
<service android:name=".MediaPlaybackService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
Инициализация медиасессии
Когда сервис получает вызов метода обратного вызова жизненного цикла onCreate() он должен выполнить следующие действия:
- Создайте и инициализируйте медиасессию.
- Установить функцию обратного вызова для медиасессии
- Установите токен сеанса мультимедиа.
Следующий код onCreate() демонстрирует эти шаги:
Котлин
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() может выглядеть следующим образом:
Котлин
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), который определяет, какие подключения разрешены, или перечисляет, какие подключения должны быть запрещены. Пример реализации ACL, разрешающего определенные подключения, можно найти в классе PackageValidator в примере приложения Universal Android Music Player .
Следует рассмотреть возможность предоставления различных иерархий контента в зависимости от типа клиента, выполняющего запрос. В частности, Android Auto ограничивает взаимодействие пользователей с аудиоприложениями. Для получения дополнительной информации см. раздел «Воспроизведение аудио для Auto» . Вы можете посмотреть clientPackageName во время подключения, чтобы определить тип клиента, и получить разные BrowserRoot в зависимости от клиента (или rootHints если таковые имеются).
Передача контента с помощью onLoadChildren()
После подключения клиент может перемещаться по иерархии контента, многократно вызывая метод MediaBrowserCompat.subscribe() для создания локального представления пользовательского интерфейса. Метод subscribe() отправляет обратный вызов onLoadChildren() в службу, которая возвращает список объектов MediaBrowser.MediaItem .
Каждый элемент MediaItem имеет уникальный идентификатор, представляющий собой непрозрачный токен. Когда клиент хочет открыть подменю или воспроизвести элемент, он передает этот идентификатор. Ваш сервис отвечает за сопоставление идентификатора с соответствующим узлом меню или элементом контента.
Простая реализация метода onLoadChildren() может выглядеть следующим образом:
Котлин
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 , работающий в другом Activity, подключается к MediaBrowserService , он привязывает Activity к службе, делая службу привязанной (но не запущенной). Это поведение по умолчанию заложено в класс MediaBrowserServiceCompat .
Сервис, который только привязан (но не запущен), уничтожается, когда все его клиенты отключаются. Если в этот момент происходит отключение вашего пользовательского интерфейса, сервис уничтожается. Это не проблема, если вы еще не воспроизводили музыку. Однако, когда начинается воспроизведение, пользователь, вероятно, ожидает продолжить прослушивание даже после переключения приложений. Вы не хотите уничтожать плеер при отключении пользовательского интерфейса для работы с другим приложением.
Поэтому необходимо убедиться, что служба запущена при начале воспроизведения, вызвав startService() . Запущенную службу необходимо явно остановить, независимо от того, привязана она или нет. Это гарантирует, что ваш плеер продолжит работу, даже если управляющая активность пользовательского интерфейса отключится.
Чтобы остановить запущенную службу, вызовите Context.stopService() или stopSelf() . Система остановит и уничтожит службу как можно скорее. Однако, если один или несколько клиентов все еще привязаны к службе, вызов для остановки службы будет отложен до тех пор, пока все ее клиенты не отвяжутся.
Жизненный цикл MediaBrowserService контролируется способом его создания, количеством подключенных к нему клиентов и вызовами, которые он получает от обратных вызовов медиасессий. Вкратце:
- Сервис создается при его запуске в ответ на нажатие медиакнопки или при привязке к нему активности (после подключения через
MediaBrowser). - Функция обратного вызова
onPlay()медиасессии должна включать код, вызывающийstartService(). Это гарантирует запуск и продолжение работы службы даже после отмены привязки всех связанных с ней действийMediaBrowserв пользовательском интерфейсе. - Функция обратного вызова
onStop()должна вызыватьstopSelf(). Если служба была запущена, это её останавливает. Кроме того, служба уничтожается, если к ней не привязаны никакие действия. В противном случае служба остаётся привязанной до тех пор, пока все её действия не будут отвязаны. (Если последующий вызовstartService()будет получен до уничтожения службы, ожидающая остановка будет отменена.)
Следующая блок-схема демонстрирует, как управляется жизненный цикл сервиса. Переменная-счетчик отслеживает количество подключенных клиентов:

Использование уведомлений MediaStyle с фоновой службой
Когда служба воспроизводит контент, она должна работать на переднем плане. Это позволяет системе понять, что служба выполняет полезную функцию и не должна быть завершена, если в системе не хватает памяти. Служба, работающая на переднем плане, должна отображать уведомление, чтобы пользователь знал о ней и мог при необходимости управлять ею. Функция обратного вызова onPlay() должна переводить службу на передний план. (Обратите внимание, что это особое значение слова «передний план». Хотя Android считает службу работающей на переднем плане для целей управления процессами, для пользователя плеер воспроизводит контент в фоновом режиме, в то время как на экране отображается другое приложение, работающее на переднем плане.)
Когда служба работает в фоновом режиме, она должна отображать уведомление , в идеале содержащее один или несколько элементов управления транспортом. Уведомление также должно включать полезную информацию из метаданных сессии.
Создайте и отобразите уведомление, когда плеер начнет воспроизведение. Лучше всего это сделать внутри метода MediaSessionCompat.Callback.onPlay() .
В приведенном ниже примере используется класс NotificationCompat.MediaStyle , разработанный для медиаприложений. Он демонстрирует, как создать уведомление, отображающее метаданные и элементы управления воспроизведением. Удобный метод getController() позволяет создать медиаконтроллер непосредственно из вашей медиасессии.
Котлин
// 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(), чтобы добавить до 3 действий, которые будут отображаться в стандартном 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, предоставляли доступ к определенному действию с фильтром намерений для проверки возможности просмотра веб-страниц.
Добавьте этот конкретный фильтр намерений в экспортируемую активность в вашем файле 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>