Votre application doit déclarer le MediaBrowserService avec un filtre d'intent dans son fichier manifeste. Vous pouvez choisir votre propre nom de service. Dans l'exemple suivant, le nom de service choisi est MediaPlaybackService.
<service android:name=".MediaPlaybackService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
Initialiser la session multimédia
Lorsque le service reçoit la méthode de rappel de cycle de vie onCreate(), il doit effectuer les étapes suivantes :
- Créer et initialiser la session multimédia
- Définir le rappel de session multimédia
- Définir le jeton de session multimédia
Le code onCreate() suivant illustre ces étapes :
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()); } }
Gérer les connexions client
Un MediaBrowserService comporte deux méthodes qui gèrent les connexions client : onGetRoot() contrôle l'accès au service, et onLoadChildren() permet à un client de créer et d'afficher un menu de la hiérarchie de contenu du MediaBrowserService.
Contrôler les connexions client avec onGetRoot()
La méthode onGetRoot() renvoie le nœud racine de la hiérarchie de contenu. Si la méthode renvoie la valeur nulle, la connexion est refusée.
Pour autoriser les clients à se connecter à votre service et à parcourir son contenu multimédia, onGetRoot() doit renvoyer un BrowserRoot non nul, qui est un ID racine représentant votre hiérarchie de contenu.
Pour autoriser les clients à se connecter à votre MediaSession sans parcourir le contenu, onGetRoot() doit toujours renvoyer un BrowserRoot non nul, mais l'ID racine doit représenter une hiérarchie de contenu vide.
Une implémentation typique de onGetRoot() peut se présenter comme suit :
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); } }
Dans certains cas, vous pouvez contrôler qui peut se connecter à votre MediaBrowserService. Pour ce faire, vous pouvez utiliser une liste de contrôle d'accès (ACL) qui spécifie les connexions autorisées ou, à l'inverse, celles qui doivent être interdites. Pour obtenir un exemple d'implémentation d'une LCA qui autorise des connexions spécifiques, consultez la classe PackageValidator dans l'application exemple Universal Android Music Player.
Vous devez envisager de fournir différentes hiérarchies de contenu en fonction du type de client qui effectue la requête. En particulier, Android Auto limite la façon dont les utilisateurs interagissent avec les applications audio. Pour en savoir plus, consultez la section Lire de l'audio pour
Auto. Vous pouvez examiner le clientPackageName au moment de la connexion pour déterminer le type de client et renvoyer un BrowserRoot différent en fonction du client (ou des rootHints, le cas échéant).
Communiquer du contenu avec onLoadChildren()
Une fois le client connecté, il peut parcourir la hiérarchie de contenu en effectuant des appels répétés à MediaBrowserCompat.subscribe() pour créer une représentation locale de l'UI. La méthode subscribe() envoie le rappel
onLoadChildren() au service, qui renvoie une liste d'MediaBrowser.MediaItem
objets.
Chaque MediaItem possède une chaîne d'ID unique, qui est un jeton opaque. Lorsqu'un client souhaite ouvrir un sous-menu ou lire un élément, il transmet l'ID. Votre service est responsable de l'association de l'ID au nœud de menu ou à l'élément de contenu approprié.
Une implémentation simple de onLoadChildren() peut se présenter comme suit :
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); }
Remarque : Les objets MediaItem fournis par le MediaBrowserService
ne doivent pas contenir de bitmaps d'icônes. Utilisez plutôt un Uri en appelant
setIconUri()
lorsque vous créez le MediaDescription pour chaque élément.
Pour obtenir un exemple d'implémentation de onLoadChildren(), consultez l'application exemple Universal
Android Music Player.
Cycle de vie du service de navigateur multimédia
Le comportement d'un service Android dépend de s'il est démarré ou lié à un ou plusieurs clients. Une fois un service créé, il peut être démarré, lié ou les deux. Dans tous ces états, il est entièrement fonctionnel et peut effectuer le travail pour lequel il est conçu. La différence réside dans la durée de vie du service. Un service lié n'est pas détruit tant que tous ses clients liés ne sont pas dissociés. Un service démarré peut être arrêté et détruit explicitement (en supposant qu'il n'est plus lié à aucun client).
Lorsqu'un MediaBrowser s'exécutant dans une autre activité se connecte à un
MediaBrowserService, il lie l'activité au service, ce qui rend le service
lié (mais pas démarré). Ce comportement par défaut est intégré à la classe MediaBrowserServiceCompat.
Un service qui n'est que lié (et non démarré) est détruit lorsque tous ses clients sont dissociés. Si votre activité d'UI se déconnecte à ce stade, le service est détruit. Ce n'est pas un problème si vous n'avez pas encore écouté de musique. Toutefois, lorsque la lecture commence, l'utilisateur s'attend probablement à continuer à écouter même après avoir changé d'application. Vous ne voulez pas détruire le lecteur lorsque vous dissociez l'UI pour travailler avec une autre application.
Pour cette raison, vous devez vous assurer que le service est démarré lorsqu'il commence à lire en appelant startService(). Un service démarré doit être arrêté explicitement, qu'il soit lié ou non. Cela garantit que votre lecteur continue de fonctionner même si l'activité d'UI de contrôle est dissociée.
Pour arrêter un service démarré, appelez Context.stopService() ou stopSelf(). Le système arrête et détruit le service dès que possible. Toutefois, si un ou plusieurs clients sont toujours liés au service, l'appel visant à arrêter le service est retardé jusqu'à ce que tous ses clients soient dissociés.
Le cycle de vie du MediaBrowserService est contrôlé par la façon dont il est créé, le nombre de clients qui y sont liés et les appels qu'il reçoit des rappels de session multimédia. En résumé :
- Le service est créé lorsqu'il est démarré en réponse à un bouton multimédia ou lorsqu'une activité y est liée (après connexion via son
MediaBrowser). - Le rappel
onPlay()de la session multimédia doit inclure du code qui appellestartService(). Cela garantit que le service démarre et continue de s'exécuter, même lorsque toutes les activités d'UIMediaBrowserqui y sont liées sont dissociées. - Le rappel
onStop()doit appelerstopSelf(). Si le service a été démarré, il est arrêté. De plus, le service est détruit si aucune activité n'y est liée. Sinon, le service reste lié jusqu'à ce que toutes ses activités soient dissociées. (Si un appelstartService()ultérieur est reçu avant la destruction du service, l'arrêt en attente est annulé.)
L'organigramme suivant montre comment le cycle de vie d'un service est géré. Le compteur de variables suit le nombre de clients liés :

Utiliser les notifications MediaStyle avec un service de premier plan
Lorsqu'un service est en cours de lecture, il doit s'exécuter au premier plan. Cela permet au système de savoir que le service exécute une fonction utile et ne doit pas être arrêté si le système manque de mémoire. Un service de premier plan doit afficher une notification pour que l'utilisateur en soit informé et puisse éventuellement le contrôler. Le rappel onPlay() doit placer le service au premier plan. (Notez qu'il s'agit d'une signification spéciale de "premier plan". Bien qu'Android considère le service au premier plan à des fins de gestion des processus, pour l'utilisateur, le lecteur est en cours de lecture en arrière-plan tandis qu'une autre application est visible au "premier plan" à l'écran.)
Lorsqu'un service s'exécute au premier plan, il doit afficher une notification, idéalement avec une ou plusieurs commandes de transport. La notification doit également inclure des informations utiles provenant des métadonnées de la session.
Créez et affichez la notification lorsque le lecteur commence la lecture. Le meilleur endroit pour cela est la méthode MediaSessionCompat.Callback.onPlay().
L'exemple ci-dessous utilise NotificationCompat.MediaStyle, qui est conçu pour les applications multimédias. Il montre comment créer une notification qui affiche des métadonnées et des commandes de transport. La méthode pratique getController() vous permet de créer un contrôleur multimédia directement à partir de votre session multimédia.
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());
Lorsque vous utilisez des notifications MediaStyle, tenez compte du comportement de ces paramètres NotificationCompat :
- Lorsque vous utilisez
setContentIntent(), votre service démarre automatiquement lorsque vous cliquez sur la notification, ce qui est une fonctionnalité pratique. - Dans une situation "non fiable" comme l'écran de verrouillage, la visibilité par défaut du contenu des notifications est
VISIBILITY_PRIVATE. Vous souhaitez probablement voir les commandes de transport sur l'écran de verrouillage. Vous devez donc utiliserVISIBILITY_PUBLIC. - Soyez prudent lorsque vous définissez la couleur d'arrière-plan. Dans une notification ordinaire sous Android 5.0 ou version ultérieure, la couleur n'est appliquée qu'à l'arrière-plan de la petite icône d'application. Toutefois, pour les notifications MediaStyle antérieures à Android 7.0, la couleur est utilisée pour l'ensemble de l'arrière-plan de la notification. Testez votre couleur d'arrière-plan. Choisissez des couleurs douces pour les yeux et évitez les couleurs extrêmement vives ou fluorescentes.
Ces paramètres ne sont disponibles que lorsque vous utilisez NotificationCompat.MediaStyle :
- Utilisez
setMediaSession()pour associer la notification à votre session. Cela permet aux applications tierces et aux appareils associés d'accéder à la session et de la contrôler. - Utilisez
setShowActionsInCompactView()pour ajouter jusqu'à trois actions à afficher dans la contentView de taille standard de la notification. (Ici, le bouton de pause est spécifié.) - Sous Android 5.0 (niveau d'API 21) et versions ultérieures, vous pouvez balayer une notification pour arrêter le lecteur une fois que le service ne s'exécute plus au premier plan. Vous ne pouvez pas le faire dans les versions antérieures. Pour permettre aux utilisateurs de supprimer la notification et d'arrêter la lecture avant Android 5.0 (niveau d'API 21), vous pouvez ajouter un bouton d'annulation en haut à droite de la notification en appelant
setShowCancelButton(true)etsetCancelButtonIntent().
Lorsque vous ajoutez les boutons de pause et d'annulation, vous avez besoin d'un PendingIntent à associer à l'action de lecture. La méthode MediaButtonReceiver.buildMediaButtonPendingIntent() permet de convertir une action PlaybackState en PendingIntent.
Activer la navigation multimédia AVRCP
En plus des applications personnalisées telles qu'Android Auto, la couche Bluetooth du système agit également comme client de votre MediaBrowserService pour faciliter la navigation à distance sans fil dans le catalogue (AVRCP).
Sous Android 16 et Android 17, la plate-forme exige que les applications n'utilisant pas Media3 exposent une activité spécifique avec un filtre d'intent pour être validées pour la navigation.
Ajoutez ce filtre d'intent spécifique à une activité exportée dans votre AndroidManifest.xml. Notez que CATEGORY_DEFAULT est intentionnellement omis pour empêcher votre application d'apparaître dans les menus génériques "Ouvrir avec" pour les fichiers audio locaux :
<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>