Cómo crear servicios para el navegador multimedia

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:

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, es posible que quieras controlar 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 a startService(). Esto garantiza que se inicie y se siga ejecutando el servicio, incluso si se desvinculan todas las actividades MediaBrowser de la IU que estaban vinculadas.
  • La devolución de llamada onStop() debe llamar a stopSelf(). 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 llamada startService() 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:

Ciclo de vida del servicio

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 que VISIBILITY_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:

  • Usa 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á especificada).
  • 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) y setCancelButtonIntent().

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.