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 la biblioteca de compatibilidad de media-compat. En esta página, el término "MediaBrowserService" hace referencia a una instancia 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

Un MediaBrowserService tiene dos métodos que controlan las conexiones de clientes: onGetRoot() controla el acceso al servicio y onLoadChildren() proporciona la capacidad de 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 método muestra un valor nulo, 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 no nulo de BrowserRoot, que es un ID raíz que representa la jerarquía de tu contenido.

Para permitir que los clientes se conecten a tu MediaSession sin navegar, onGetRoot() igual debe mostrar un valor de BrowserRoot no nulo, pero el ID raíz debe representar una 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 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, de manera alternativa, que enumere qué conexiones deben prohibirse. Para ver un ejemplo de cómo implementar una LCA que permita conexiones específicas, consulta la clase PackageValidator en la app de ejemplo de Universal Android Music Player.

Debes considerar proporcionar diferentes jerarquías de contenido según el tipo de cliente que realice la consulta. En particular, Android Auto limita la forma en que los usuarios interactúan con las apps de audio. Para obtener más información, consulta Cómo reproducir audio en modo automático. Puedes observar el clientPackageName en el momento de la conexión para determinar el tipo de cliente y mostrar 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: Los objetos MediaItem que entrega MediaBrowserService no deben contener mapas de bits de íconos. En su lugar, llama a setIconUri() para usar un Uri 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, debes llamar a startService() para asegurarte de que el servicio esté iniciado cuando comience a reproducirse. Un servicio iniciado debe detenerse de forma explícita, ya sea que esté vinculado o no. De esta manera, se garantiza que el reproductor siga funcionando incluso si se desvincula la actividad de la IU que lo controla.

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 NotificationCompat.MediaStyle, que está 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 esta configuración de NotificationCompat:

  • Si usas setContentIntent(), el servicio se inicia automáticamente cuando se hace clic en la notificación, una función muy útil.
  • En una situación "no confiable", como la pantalla de bloqueo, la visibilidad predeterminada del contenido de las notificaciones es VISIBILITY_PRIVATE. Es probable que quieras ver los controles de transporte en la pantalla de bloqueo. Por eso, VISIBILITY_PUBLIC es la mejor opción.
  • Ten cuidado cuando establezcas el color de fondo. En una notificación común en Android 5.0 o versiones posteriores, el color se aplica solo al fondo del ícono pequeño de la app. Sin embargo, para las notificaciones MediaStyle anteriores a Android 7.0, el color se usa para todo el fondo de las notificaciones. Prueba el color de fondo. Usa un tono suave para la vista 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 los dispositivos complementarios accedan a la sesión y la controlen.
  • Usa setShowActionsInCompactView() para agregar hasta 3 acciones que se mostrarán en el contentView de tamaño estándar de la notificación. (aquí se especifica el botón de pausa).
  • En Android 5.0 (nivel de API 21) y versiones posteriores, puedes deslizar una notificación para detener el reproductor una vez que el servicio ya no se ejecute en primer plano. No era posible hacer esto en versiones anteriores. Para 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 notificación llamando a setShowCancelButton(true) y setCancelButtonIntent().

Cuando agregues los botones para pausar y cancelar, necesitarás adjuntar un PendingIntent a la acción de reproducción. El método MediaButtonReceiver.buildMediaButtonPendingIntent() hace el trabajo de convertir una acción de PlaybackState en un PendingIntent.