In der Manifestdatei Ihrer App muss das MediaBrowserService mit einem Intent-Filter deklariert werden. Sie können einen eigenen Dienstnamen auswählen. Im folgenden Beispiel ist der ausgewählte Dienstname MediaPlaybackService.
<service android:name=".MediaPlaybackService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
Media Session initialisieren
Wenn der Dienst die onCreate()-Lifecycle-Callback-Methode empfängt, sollte er die folgenden Schritte ausführen:
- Media Session erstellen und initialisieren
- Callback für Mediensitzung festlegen
- Sitzungstoken für Medien festlegen
Der folgende onCreate()-Code veranschaulicht diese Schritte:
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()); } }
Clientverbindungen verwalten
Ein MediaBrowserService hat zwei Methoden, die Clientverbindungen verarbeiten:
onGetRoot() steuert den Zugriff auf den Dienst und onLoadChildren() ermöglicht es einem Client, ein Menü der Inhaltshierarchie des MediaBrowserService zu erstellen und anzuzeigen.
Clientverbindungen mit onGetRoot() steuern
Die Methode onGetRoot() gibt den Stammknoten der Inhaltshierarchie zurück. Wenn die Methode „null“ zurückgibt, wird die Verbindung abgelehnt.
Damit Clients eine Verbindung zu Ihrem Dienst herstellen und seine Medieninhalte durchsuchen können, muss onGetRoot() einen BrowserRoot zurückgeben, der nicht null ist. Dies ist eine Stamm-ID, die Ihre Inhaltshierarchie darstellt.
Damit Clients eine Verbindung zu deiner MediaSession herstellen können, ohne zu browsen, muss onGetRoot() weiterhin einen BrowserRoot zurückgeben, der nicht null ist. Die Stamm-ID sollte jedoch eine leere Inhaltshierarchie darstellen.
Eine typische Implementierung von onGetRoot() könnte so aussehen:
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); } }
In einigen Fällen möchten Sie möglicherweise steuern, wer sich mit Ihrem MediaBrowserService verbinden kann. Eine Möglichkeit besteht darin, eine Zugriffssteuerungsliste (Access Control List, ACL) zu verwenden, in der angegeben wird, welche Verbindungen zulässig sind oder welche Verbindungen verboten werden sollen. Ein Beispiel für die Implementierung einer ACL, die bestimmte Verbindungen zulässt, finden Sie in der Klasse PackageValidator in der Beispiel-App Universal Android Music Player.
Je nachdem, welcher Client die Anfrage stellt, sollten Sie unterschiedliche Inhaltshierarchien bereitstellen. Insbesondere schränkt Android Auto ein, wie Nutzer mit Audio-Apps interagieren. Weitere Informationen finden Sie unter Audio für Auto abspielen. Sie können den clientPackageName zum Zeitpunkt der Verbindung prüfen, um den Clienttyp zu ermitteln, und je nach Client einen anderen BrowserRoot zurückgeben (oder rootHints, falls vorhanden).
Inhalte mit onLoadChildren() teilen
Nachdem der Client eine Verbindung hergestellt hat, kann er die Inhaltshierarchie durchlaufen, indem er wiederholt MediaBrowserCompat.subscribe() aufruft, um eine lokale Darstellung der Benutzeroberfläche zu erstellen. Die Methode subscribe() sendet den Callback onLoadChildren() an den Dienst, der eine Liste von MediaBrowser.MediaItem-Objekten zurückgibt.
Jedes MediaItem hat einen eindeutigen ID-String, der ein intransparentes Token ist. Wenn ein Client ein Untermenü öffnen oder ein Element abspielen möchte, übergibt er die ID. Ihr Dienst ist dafür verantwortlich, die ID dem entsprechenden Menüknoten oder Inhaltselement zuzuordnen.
Eine einfache Implementierung von onLoadChildren() könnte so aussehen:
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); }
Hinweis : Von MediaBrowserService bereitgestellte MediaItem-Objekte sollten keine Symbol-Bitmaps enthalten. Verwenden Sie stattdessen ein Uri, indem Sie setIconUri() aufrufen, wenn Sie das MediaDescription für jedes Element erstellen.
Ein Beispiel für die Implementierung von onLoadChildren() finden Sie in der Beispiel-App Universal Android Music Player.
Lebenszyklus des Medienbrowser-Dienstes
Das Verhalten eines Android-Dienstes hängt davon ab, ob er gestartet oder an einen oder mehrere Clients gebunden ist. Nachdem ein Dienst erstellt wurde, kann er gestartet, gebunden oder beides werden. In all diesen Status ist der Dienst voll funktionsfähig und kann die Aufgaben ausführen, für die er entwickelt wurde. Der Unterschied besteht darin, wie lange der Dienst verfügbar sein wird. Ein gebundener Dienst wird erst zerstört, wenn alle gebundenen Clients die Bindung aufheben. Ein gestarteter Dienst kann explizit beendet und zerstört werden, sofern er nicht mehr an Clients gebunden ist.
Wenn ein MediaBrowser, das in einer anderen Aktivität ausgeführt wird, eine Verbindung zu einem MediaBrowserService herstellt, wird die Aktivität an den Dienst gebunden. Der Dienst ist dann gebunden (aber nicht gestartet). Dieses Standardverhalten ist in die Klasse MediaBrowserServiceCompat integriert.
Ein Dienst, der nur gebunden (und nicht gestartet) ist, wird zerstört, wenn alle Clients die Bindung aufheben. Wenn die UI-Aktivität an diesem Punkt getrennt wird, wird der Dienst beendet. Das ist kein Problem, wenn Sie noch keine Musik abgespielt haben. Wenn die Wiedergabe jedoch gestartet wird, erwartet der Nutzer wahrscheinlich, dass er auch nach dem Wechseln der App weiter zuhören kann. Sie möchten den Player nicht zerstören, wenn Sie die Benutzeroberfläche entbinden, um mit einer anderen App zu arbeiten.
Aus diesem Grund müssen Sie dafür sorgen, dass der Dienst gestartet wird, wenn die Wiedergabe durch Aufrufen von startService() beginnt. Ein gestarteter Dienst muss explizit beendet werden, unabhängig davon, ob er gebunden ist oder nicht. So wird sichergestellt, dass der Player weiterhin funktioniert, auch wenn die Aktivität der steuernden Benutzeroberfläche entbunden wird.
Rufen Sie Context.stopService() oder stopSelf() auf, um einen gestarteten Dienst zu beenden. Das System beendet und löscht den Dienst so schnell wie möglich. Wenn jedoch ein oder mehrere Clients noch an den Dienst gebunden sind, wird der Aufruf zum Beenden des Dienstes verzögert, bis alle Clients die Bindung aufheben.
Der Lebenszyklus von MediaBrowserService wird durch die Art und Weise gesteuert, wie er erstellt wird, die Anzahl der Clients, die daran gebunden sind, und die Aufrufe, die er von Media-Sitzungs-Callbacks empfängt. Zusammenfassung:
- Der Dienst wird erstellt, wenn er als Reaktion auf eine Media-Schaltfläche gestartet wird oder wenn eine Aktivität daran gebunden wird (nachdem die Verbindung über das
MediaBrowserhergestellt wurde). - Der Media-Sitzungs-Callback
onPlay()sollte Code enthalten, derstartService()aufruft. So wird sichergestellt, dass der Dienst gestartet wird und weiter ausgeführt wird, auch wenn alle daran gebundenenMediaBrowser-Aktivitäten der Benutzeroberfläche entbunden werden. - Im
onStop()-Callback solltestopSelf()aufgerufen werden. Wenn der Dienst gestartet wurde, wird er dadurch beendet. Außerdem wird der Dienst beendet, wenn keine Aktivitäten daran gebunden sind. Andernfalls bleibt der Dienst gebunden, bis alle seine Aktivitäten entbunden werden. Wenn ein nachfolgenderstartService()-Aufruf empfangen wird, bevor der Dienst beendet wird, wird der ausstehende Stopp abgebrochen.
Das folgende Flussdiagramm zeigt, wie der Lebenszyklus eines Dienstes verwaltet wird. Der Variablencounter erfasst die Anzahl der verknüpften Clients:

MediaStyle-Benachrichtigungen mit einem Dienst im Vordergrund verwenden
Wenn ein Dienst Medien wiedergibt, sollte er im Vordergrund ausgeführt werden. So weiß das System, dass der Dienst eine nützliche Funktion ausführt und nicht beendet werden sollte, wenn der Arbeitsspeicher des Systems knapp wird. Ein Dienst im Vordergrund muss eine Benachrichtigung anzeigen, damit der Nutzer darüber informiert ist und ihn optional steuern kann. Der onPlay()-Callback sollte den Dienst in den Vordergrund stellen. Beachten Sie, dass dies eine spezielle Bedeutung von „Vordergrund“ ist. Während Android den Dienst für die Prozessverwaltung als Dienst im Vordergrund betrachtet, wird der Player für den Nutzer im Hintergrund wiedergegeben, während eine andere App auf dem Bildschirm im „Vordergrund“ sichtbar ist.)
Wenn ein Dienst im Vordergrund ausgeführt wird, muss er eine Benachrichtigung anzeigen, idealerweise mit einer oder mehreren Transportsteuerungen. Die Benachrichtigung sollte auch nützliche Informationen aus den Metadaten der Sitzung enthalten.
Erstellen und zeigen Sie die Benachrichtigung an, wenn die Wiedergabe beginnt. Am besten tun Sie das in der Methode MediaSessionCompat.Callback.onPlay().
Im folgenden Beispiel wird NotificationCompat.MediaStyle verwendet, das für Media-Apps entwickelt wurde. Es wird gezeigt, wie eine Benachrichtigung erstellt wird, in der Metadaten und Transportsteuerungen angezeigt werden. Mit der Convenience-Methode getController() können Sie eine Mediensteuerung direkt über Ihre Mediensitzung erstellen.
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());
Beachten Sie bei der Verwendung von MediaStyle-Benachrichtigungen das Verhalten der folgenden NotificationCompat-Einstellungen:
- Wenn Sie
setContentIntent()verwenden, wird Ihr Dienst automatisch gestartet, wenn auf die Benachrichtigung geklickt wird. - In einer „nicht vertrauenswürdigen“ Situation wie dem Sperrbildschirm ist die Standardsichtbarkeit für Benachrichtigungsinhalte
VISIBILITY_PRIVATE. Wahrscheinlich möchtest du die Transportsteuerung auf dem Sperrbildschirm sehen. In diesem Fall istVISIBILITY_PUBLICdie richtige Option. - Seien Sie vorsichtig, wenn Sie die Hintergrundfarbe festlegen. Bei einer normalen Benachrichtigung in Android 5.0 oder höher wird die Farbe nur auf den Hintergrund des kleinen App-Symbols angewendet. Bei MediaStyle-Benachrichtigungen vor Android 7.0 wird die Farbe jedoch für den gesamten Benachrichtigungshintergrund verwendet. Hintergrundfarbe testen Achten Sie auf eine angenehme Darstellung und vermeiden Sie extrem helle oder fluoreszierende Farben.
Diese Einstellungen sind nur verfügbar, wenn Sie NotificationCompat.MediaStyle verwenden:
- Verwenden Sie
setMediaSession(), um die Benachrichtigung Ihrer Sitzung zuzuordnen. Dadurch können Drittanbieter-Apps und Companion-Geräte auf die Sitzung zugreifen und sie steuern. - Mit
setShowActionsInCompactView()können Sie bis zu drei Aktionen hinzufügen, die in der contentView-Ansicht der Benachrichtigung in Standardgröße angezeigt werden. (Hier wird der Pause-Button angegeben.) - Unter Android 5.0 (API-Level 21) und höher können Sie eine Benachrichtigung wegwischen, um die Wiedergabe zu beenden, sobald der Dienst nicht mehr im Vordergrund ausgeführt wird. In früheren Versionen ist das nicht möglich. Damit Nutzer die Benachrichtigung entfernen und die Wiedergabe vor Android 5.0 (API-Ebene 21) beenden können, können Sie eine Schaltfläche zum Abbrechen in der oberen rechten Ecke der Benachrichtigung hinzufügen, indem Sie
setShowCancelButton(true)undsetCancelButtonIntent()aufrufen.
Wenn Sie die Schaltflächen zum Pausieren und Abbrechen hinzufügen, benötigen Sie ein PendingIntent, das Sie an die Wiedergabeaktion anhängen können. Die Methode MediaButtonReceiver.buildMediaButtonPendingIntent() wandelt eine PlaybackState-Aktion in einen PendingIntent um.
AVRCP-Mediendurchsuchen aktivieren
Zusätzlich zu benutzerdefinierten Apps wie Android Auto fungiert die Bluetooth-Ebene des Systems auch als Client für Ihr MediaBrowserService, um das drahtlose Durchsuchen des Katalogs per Fernbedienung (AVRCP) zu ermöglichen.
Unter Android 16 und Android 17 ist es erforderlich, dass Apps, die Media3 nicht verwenden, eine bestimmte Aktivität mit einem Intent-Filter zur Verfügung stellen, die für das Browsen validiert werden muss.
Fügen Sie diesen spezifischen Intent-Filter einer exportierten Aktivität in Ihrem AndroidManifest.xml hinzu. Beachten Sie, dass CATEGORY_DEFAULT absichtlich weggelassen wird, damit Ihre App nicht in allgemeinen „Öffnen mit“-Menüs für lokale Audiodateien angezeigt wird:
<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>