Tu app debe declarar el objeto MediaBrowserService
con un filtro de intents en su manifiesto. Puedes elegir tu propio nombre de servicio; en el siguiente ejemplo, es "MediaPlaybackService".
<service android:name=".MediaPlaybackService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
Nota: La implementación recomendada de MediaBrowserService
es MediaBrowserServiceCompat
.
que se define en el
biblioteca de compatibilidad de media-compat.
En esta página, el término "MediaBrowserService" se refiere a una instancia de
de MediaBrowserServiceCompat
.
Cómo inicializar la sesión multimedia
Cuando el servicio recibe el método de devolución de llamada del ciclo de vida de onCreate()
, debe realizar estos pasos:
- Crear e inicializar la sesión multimedia
- Establecer la devolución de llamada de la sesión multimedia
- Establecer el token de la sesión multimedia
En el siguiente código de onCreate()
, se demuestran los pasos:
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()); } }
Administra las conexiones de clientes
Una MediaBrowserService
tiene dos métodos que controlan las conexiones de clientes:
onGetRoot()
controles
acceso al servicio
onLoadChildren()
Permite que un cliente compile y muestre un menú de la jerarquía de contenido de MediaBrowserService
.
Cómo controlar las conexiones de clientes con onGetRoot()
El método onGetRoot()
muestra el nodo raíz de la jerarquía de contenido. Si el botón
muestra un valor nulo, y se rechaza la conexión.
Para permitir que los clientes se conecten a tu servicio y exploren su contenido multimedia, onGetRoot() debe mostrar un valor de BrowserRoot no nulo, que es un ID raíz que representa tu jerarquía de contenido.
Para permitir que los clientes se conecten a tu MediaSession sin explorar, onGetRoot() debe mostrar un valor de BrowserRoot no nulo, pero el ID raíz debe representar un jerarquía de contenido vacía.
Una implementación típica de onGetRoot()
podría verse de esta manera:
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); } }
En algunos casos, tal vez quieras
controlar quién se puede conectar
a tu MediaBrowserService
. Una forma es usar una Lista de control de acceso (LCA)
que especifique qué conexiones están permitidas o que, de forma alternativa, enumera
qué conexiones deberían prohibirse. Ejemplo de cómo implementar una LCA
que permite conexiones específicas, consulta la
PackageValidator
en el Universal Android Music Player
app de ejemplo.
Procura proporcionar diferentes jerarquías de contenido según
qué tipo de cliente realiza la consulta. En particular, Android Auto limita
los usuarios interactúan con apps de audio. Para obtener más información, consulta Cómo reproducir audio de
Automático: Tú
puede observar el clientPackageName
en el momento de la conexión para determinar el cliente
tipo y muestra un BrowserRoot
diferente según el cliente (o rootHints
si corresponde).
Cómo enviar contenido con onLoadChildren()
Luego de que el cliente se conecta, puede desviar la jerarquía de contenido realizando llamadas repetidas a MediaBrowserCompat.subscribe()
para compilar una representación local de la IU. El método subscribe()
envía la devolución de llamada onLoadChildren()
al servicio, el cual muestra una lista de objetos MediaBrowser.MediaItem
.
Cada MediaItem tiene una string de ID único, que es un token opaco. Cuando un cliente quiere abrir un submenú o reproducir un elemento, pasa el ID. Tu servicio está a cargo de asociar el ID con el nodo de menú o elemento de contenido apropiado.
Una implementación simple de onLoadChildren()
podría verse de esta manera:
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); }
Nota: MediaItem
objetos entregados por MediaBrowserService
no debe contener mapas de bits de íconos. Usa un Uri
llamando a
setIconUri()
cuando compiles el MediaDescription
para cada elemento.
Para ver un ejemplo de cómo implementar onLoadChildren()
, consulta la app de Universal Android Music Player de muestra.
Ciclo de vida del servicio de exploración multimedia
El comportamiento de un servicio de Android depende de si está iniciado o está vinculado a uno o más clientes. Después de crear un servicio, se puede iniciar o vincular, o ambas opciones. En todos estos estados, es completamente funcional y puede realizar las tareas para las que se diseñó. La diferencia es la duración del servicio. No se destruye un servicio vinculado hasta que todos sus clientes vinculados se hayan desvinculado. Se puede detener y destruir de manera explícita un servicio iniciado (suponiendo que ya no está vinculado a ningún cliente).
Cuando un MediaBrowser
que se ejecuta en otra actividad se conecta a un MediaBrowserService
, vincula la actividad al servicio, con lo cual el servicio se vincula, pero no se inicia. Este comportamiento predeterminado está integrado en la clase MediaBrowserServiceCompat
.
Un servicio que solo está vinculado (y no iniciado) se destruye cuando se desvinculan todos sus clientes. Si se desconecta la actividad de tu IU en este punto, se destruye el servicio. Esto no es un problema si aún no reprodujiste música. Sin embargo, cuando se inicie la reproducción, es posible que el usuario espere seguir escuchando la música, incluso después de cambiar a otra app. No es la idea destruir el reproductor cuando desvinculas la IU para usar otra app.
Por este motivo, debe asegurarse de que el servicio se inicie cuando se inicie
para jugar llamando a startService()
. R
un servicio iniciado debe detenerse de forma explícita, ya sea que esté vinculado o no. Esta
garantiza que tu reproductor continúe funcionando incluso si la IU de control
desvinculación de la actividad.
Para detener un servicio iniciado, llama a Context.stopService()
o a stopSelf()
. El sistema detiene y destruye el servicio lo antes posible. Sin embargo, si uno o más clientes siguen vinculados al servicio, la llamada para detenerlo se demora hasta que todos sus clientes se hayan desvinculado.
El ciclo de vida de MediaBrowserService
está controlado por la forma en que se crea, el número de clientes vinculados a él y las llamadas que recibe de las devoluciones de llamada de la sesión multimedia. En resumen:
- Se crea el servicio cuando se inicia en respuesta a un botón multimedia o cuando se vincula una actividad a él (después de conectarse a través de su
MediaBrowser
). - La devolución de llamada
onPlay()
de la sesión multimedia debe incluir código que llame astartService()
. Esto garantiza que se inicie y se siga ejecutando el servicio, incluso si se desvinculan todas las actividadesMediaBrowser
de la IU que estaban vinculadas. - La devolución de llamada
onStop()
debe llamar astopSelf()
. Si el servicio está iniciado, esta acción lo detiene. Además, se destruye el servicio si no hay actividades vinculadas a él. De lo contrario, permanece vinculado hasta que se desvinculan todas sus actividades. (Si se recibe una llamadastartService()
posterior antes de que se destruya el servicio, se cancela la detención pendiente).
El siguiente diagrama de flujo demuestra cómo se administra el ciclo de vida de un servicio. El contador de variables realiza un seguimiento del número de clientes vinculados:
Cómo usar notificaciones MediaStyle con un servicio en primer plano
Cuando se está reproduciendo un servicio, debe ejecutarse en primer plano. De este modo, el sistema sabe que el servicio está realizando una función útil y que no se debe cerrar si el sistema tiene poca memoria. Un servicio en primer plano debe mostrar una notificación para que el usuario esté al tanto y tenga la opción de controlarlo. La devolución de llamada onPlay()
debe colocar el servicio en primer plano. (Ten en cuenta que este es un significado especial de "primer plano". Si bien Android considera que el servicio está en primer plano para los fines de administración de procesos, desde el punto de vista del usuario, el reproductor se está ejecutando en segundo plano y hay otra app visible en el "primer plano" de la pantalla).
Cuando un servicio se ejecuta en primer plano, debe mostrar una notificación, idealmente con uno o más controles de transporte. La notificación también debe incluir información útil proveniente de los metadatos de la sesión.
Compila y muestra la notificación cuando el reproductor comience a reproducir contenido. El mejor lugar para hacerlo es dentro del método MediaSessionCompat.Callback.onPlay()
.
En el siguiente ejemplo, se usa la
NotificationCompat.MediaStyle
,
diseñado para apps de música. Indica cómo compilar una notificación que muestre metadatos y controles de transporte. El método de conveniencia
getController()
te permite crear un controlador multimedia directamente desde tu sesión multimedia.
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());
Cuando uses notificaciones MediaStyle, ten en cuenta el comportamiento de estas Configuración de NotificationCompat:
- Si usas
setContentIntent()
, el servicio se inicia automáticamente cuando se envía la notificación cuando el botón se hace clic, una función muy útil. - En una canalización “no confiable”, situación
como la pantalla de bloqueo, la visibilidad predeterminada del contenido de las notificaciones es
VISIBILITY_PRIVATE
. Es probable que quieras ver controles de transporte en la pantalla de bloqueo, por lo queVISIBILITY_PUBLIC
es la mejor opción. - Ten cuidado cuando establezcas el color de fondo. En una notificación común en En Android 5.0 o versiones posteriores, el color se aplica solo al fondo de ícono pequeño de la app. Pero para las notificaciones MediaStyle anteriores a Android 7.0, el color se usa para todo el fondo de la notificación. Prueba el color de fondo. Ir suave para los ojos y evita los colores extremadamente brillantes o fluorescentes.
Esta configuración solo está disponible cuando usas NotificationCompat.MediaStyle:
- Usar
setMediaSession()
para asociar la notificación con tu sesión. Esto permite que las apps de terceros y dispositivos complementarios para acceder a la sesión y controlarla. - Utiliza
setShowActionsInCompactView()
para agregar hasta 3 acciones que se mostrarán en el contentView de tamaño estándar de la notificación. (Aquí, el botón de pausa está especificadas). - En Android 5.0 (nivel de API 21) y versiones posteriores, puedes deslizar una notificación para descartarla
el reproductor cuando el servicio deja de ejecutarse en primer plano. No puedes hacer lo siguiente:
en versiones anteriores. Cómo permitir que los usuarios quiten la notificación y detengan la reproducción
en versiones anteriores a Android 5.0 (nivel de API 21), puedes agregar un botón para cancelar en la esquina superior derecha de la
llamando a
setShowCancelButton(true)
ysetCancelButtonIntent()
.
Cuando agregues los botones de pausa y cancelación, necesitarás adjuntar un PendingIntent
a la acción de reproducción. El método MediaButtonReceiver.buildMediaButtonPendingIntent()
convierte
una acción PlaybackState en un PendingIntent.