Las sesiones multimedia proporcionan una forma universal de interactuar con un reproductor de audio o video. En Media3, el reproductor predeterminado es la clase ExoPlayer
, que implementa la interfaz Player
. Conectar la sesión multimedia al reproductor permite que una app anuncie la reproducción de contenido multimedia de forma externa y reciba comandos de reproducción de fuentes externas.
Los comandos pueden provenir de botones físicos, como el botón de reproducción de un control remoto de auriculares o de TV. También pueden provenir de apps cliente que tienen un controlador de medios, como cuando se le indica "pausar" al Asistente de Google. La sesión multimedia delega estos comandos al reproductor de la app de música.
Cuándo elegir una sesión multimedia
Cuando implementas MediaSession
, permites que los usuarios controlen la reproducción:
- A través de sus auriculares A menudo, hay botones o interacciones táctiles que un usuario puede realizar en sus auriculares para reproducir o pausar contenido multimedia, o bien ir a la pista anterior o siguiente.
- Hablando con Asistente de Google Un patrón común es decir "OK Google, pausa" para pausar cualquier contenido multimedia que se esté reproduciendo en el dispositivo.
- A través de su reloj Wear OS Esto permite acceder con mayor facilidad a los controles de reproducción más comunes mientras se reproduce contenido en el teléfono.
- A través de los controles de contenido multimedia Este carrusel muestra controles para cada sesión multimedia en ejecución.
- En TV. Permite acciones con botones de reproducción físicos, control de reproducción de la plataforma y administración de energía (por ejemplo, si se apaga la TV, la barra de sonido o el receptor AV, o si se cambia la entrada, la reproducción debería detenerse en la app).
- A través de los controles multimedia de Android Auto Esto permite controlar la reproducción de forma segura mientras conduces.
- Y cualquier otro proceso externo que deba influir en la reproducción.
Esto es ideal para muchos casos de uso. En particular, te recomendamos que uses MediaSession
en los siguientes casos:
- Estás transmitiendo contenido de video de formato largo, como películas o TV en vivo.
- Estás transmitiendo contenido de audio de formato largo, como podcasts o playlists de música.
- Estás creando una app para TVs.
Sin embargo, no todos los casos de uso se ajustan bien a MediaSession
. Te recomendamos que uses solo Player
en los siguientes casos:
- Muestras contenido de formato corto, en el que no se necesita ningún control externo ni reproducción en segundo plano.
- No hay un solo video activo, por ejemplo, el usuario se desplaza por una lista y se muestran varios videos en la pantalla al mismo tiempo.
- Estás reproduciendo un video explicativo o de introducción único que esperas que el usuario mire de forma activa sin necesidad de controles de reproducción externos.
- Tu contenido es sensible a la privacidad y no quieres que los procesos externos accedan a los metadatos de los medios (por ejemplo, el modo Incógnito en un navegador).
Si tu caso de uso no se ajusta a ninguno de los anteriores, considera si te parece bien que tu app siga reproduciendo contenido cuando el usuario no interactúe de forma activa con él. Si la respuesta es sí, probablemente quieras elegir MediaSession
. Si la respuesta es no, probablemente quieras usar Player
en su lugar.
Crea una sesión multimedia
Una sesión multimedia existe junto al reproductor que administra. Puedes construir una sesión de medios con un objeto Context
y un objeto Player
. Debes crear e inicializar una sesión multimedia cuando sea necesario, como el método de ciclo de vida onStart()
o onResume()
de Activity
o Fragment
, o el método onCreate()
de Service
que posee la sesión multimedia y su reproductor asociado.
Para crear una sesión multimedia, inicializa un Player
y proporciónalo a MediaSession.Builder
de la siguiente manera:
Kotlin
val player = ExoPlayer.Builder(context).build() val mediaSession = MediaSession.Builder(context, player).build()
Java
ExoPlayer player = new ExoPlayer.Builder(context).build(); MediaSession mediaSession = new MediaSession.Builder(context, player).build();
Manejo automático del estado
La biblioteca de Media3 actualiza automáticamente la sesión multimedia con el estado del reproductor. Por lo tanto, no es necesario que controles manualmente la asignación del jugador a la sesión.
Esto es diferente de la sesión de medios de la plataforma, en la que debías crear y mantener un PlaybackState
independientemente del reproductor, por ejemplo, para indicar cualquier error.
ID de sesión único
De forma predeterminada, MediaSession.Builder
crea una sesión con una cadena vacía como ID de sesión. Esto es suficiente si una app solo pretende crear una instancia de sesión, que es el caso más común.
Si una app quiere administrar varias instancias de sesión al mismo tiempo, debe asegurarse de que el ID de cada sesión sea único. El ID de sesión se puede establecer cuando se compila la sesión con MediaSession.Builder.setId(String id)
.
Si ves que IllegalStateException
falla en tu app con el mensaje de error IllegalStateException: Session ID must be unique. ID=
, es probable que se haya creado una sesión de forma inesperada antes de que se liberara una instancia creada anteriormente con el mismo ID. Para evitar que se filtren sesiones debido a un error de programación, se detectan estos casos y se notifica sobre ellos a través de una excepción.
Otorga control a otros clientes
La sesión multimedia es la clave para controlar la reproducción. Te permite enrutar comandos de fuentes externas al reproductor que se encarga de reproducir tu contenido multimedia. Estas fuentes pueden ser botones físicos, como el botón de reproducción de un control remoto de auriculares o de TV, o comandos indirectos, como indicarle "pausar" a Asistente de Google. Del mismo modo, es posible que desees otorgar acceso al sistema Android para facilitar los controles de notificaciones y de la pantalla de bloqueo, o a un reloj Wear OS para que puedas controlar la reproducción desde la carátula. Los clientes externos pueden usar un controlador multimedia para enviar comandos de reproducción a tu app de música. Tu sesión multimedia los recibe y, en última instancia, delega los comandos al reproductor multimedia.

Cuando un control está a punto de conectarse a tu sesión multimedia, se llama al método onConnect()
. Puedes usar el ControllerInfo
proporcionado para decidir si aceptar o rechazar la solicitud. Consulta un ejemplo de cómo aceptar una solicitud de conexión en la sección Declara comandos personalizados.
Después de conectarse, un controlador puede enviar comandos de reproducción a la sesión. Luego, la sesión delega esos comandos al reproductor. La sesión controla automáticamente los comandos de reproducción y de playlist definidos en la interfaz Player
.
Otros métodos de devolución de llamada te permiten controlar, por ejemplo, solicitudes de comandos personalizados y modificar la playlist. Estas devoluciones de llamada también incluyen un objeto ControllerInfo
para que puedas modificar la forma en que respondes a cada solicitud por controlador.
Modificar la playlist
Una sesión multimedia puede modificar directamente la playlist de su reproductor, como se explica en la guía de ExoPlayer para playlists.
Los controladores también pueden modificar la playlist si COMMAND_SET_MEDIA_ITEM
o COMMAND_CHANGE_MEDIA_ITEMS
están disponibles para el controlador.
Cuando se agregan elementos nuevos a la playlist, el reproductor suele requerir instancias de MediaItem
con un URI definido para que se puedan reproducir. De forma predeterminada, los elementos agregados recientemente se reenvían automáticamente a los métodos del reproductor, como player.addMediaItem
, si tienen un URI definido.
Si deseas personalizar las instancias de MediaItem
que se agregan al reproductor, puedes anular onAddMediaItems()
.
Este paso es necesario cuando deseas admitir controladores que solicitan contenido multimedia sin un URI definido. En cambio, el objeto MediaItem
suele tener uno o más de los siguientes campos configurados para describir el contenido multimedia solicitado:
MediaItem.id
: Es un ID genérico que identifica el contenido multimedia.MediaItem.RequestMetadata.mediaUri
: Es un URI de solicitud que puede usar un esquema personalizado y que el reproductor no necesariamente puede reproducir directamente.MediaItem.RequestMetadata.searchQuery
: Es una búsqueda textual, por ejemplo, de Asistente de Google.MediaItem.MediaMetadata
: Son metadatos estructurados, como "título" o "artista".
Para obtener más opciones de personalización de playlists completamente nuevas, también puedes anular onSetMediaItems()
, lo que te permite definir el elemento de inicio y la posición en la playlist. Por ejemplo, puedes expandir un solo elemento solicitado a una playlist completa y darle instrucciones al reproductor para que comience en el índice del elemento solicitado originalmente. En la app de demostración de sesiones, puedes encontrar una implementación de ejemplo de onSetMediaItems()
con esta función.
Administra las preferencias de los botones de medios
Cada controlador, por ejemplo, la IU del sistema, Android Auto o Wear OS, puede tomar sus propias decisiones sobre qué botones mostrarle al usuario. Para indicar qué controles de reproducción quieres exponer al usuario, puedes especificar las preferencias de los botones de medios en el objeto MediaSession
. Estas preferencias consisten en una lista ordenada de instancias de CommandButton
, cada una de las cuales define una preferencia para un botón en la interfaz de usuario.
Cómo definir botones de comando
Las instancias de CommandButton
se usan para definir las preferencias de los botones multimedia. Cada botón define tres aspectos del elemento de IU deseado:
- El ícono, que define la apariencia visual. El ícono debe establecerse en una de las constantes predefinidas cuando se crea un
CommandButton.Builder
. Ten en cuenta que no se trata de un recurso de imagen o mapa de bits real. Una constante genérica ayuda a los controladores a elegir un recurso adecuado para lograr un aspecto y un estilo coherentes en su propia IU. Si ninguna de las constantes de ícono predefinidas se ajusta a tu caso de uso, puedes usarsetCustomIconResId
. - El comando, que define la acción que se activa cuando el usuario interactúa con el botón. Puedes usar
setPlayerCommand
para unPlayer.Command
osetSessionCommand
para unSessionCommand
predefinido o personalizado. - El Slot, que define dónde se debe colocar el botón en la IU del controlador. Este campo es opcional y se configura automáticamente en función del ícono y el comando. Por ejemplo, permite especificar que un botón se debe mostrar en el área de navegación "hacia adelante" de la IU en lugar del área de "desbordamiento" predeterminada.
Kotlin
val button = CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15) .setSessionCommand(SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY)) .setSlots(CommandButton.SLOT_FORWARD) .build()
Java
CommandButton button = new CommandButton.Builder(CommandButton.ICON_SKIP_FORWARD_15) .setSessionCommand(new SessionCommand(CUSTOM_ACTION_ID, Bundle.EMPTY)) .setSlots(CommandButton.SLOT_FORWARD) .build();
Cuando se resuelven las preferencias de los botones de medios, se aplica el siguiente algoritmo:
- Para cada
CommandButton
en las preferencias de botones de medios, coloca el botón en la primera ranura disponible y permitida. - Si alguna de las ranuras centrales, hacia adelante y hacia atrás no se completa con un botón, agrega botones predeterminados para esta ranura.
Puedes usar CommandButton.DisplayConstraints
para generar una vista previa de cómo se resolverán las preferencias de los botones de medios según las restricciones de visualización de la IU.
Cómo establecer preferencias de botones de medios
La forma más sencilla de establecer las preferencias de los botones de medios es definir la lista cuando se compila el objeto MediaSession
. Como alternativa, puedes anular MediaSession.Callback.onConnect
para personalizar las preferencias de los botones de medios de cada control conectado.
Kotlin
val mediaSession = MediaSession.Builder(context, player) .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton)) .build()
Java
MediaSession mediaSession = new MediaSession.Builder(context, player) .setMediaButtonPreferences(ImmutableList.of(likeButton, favoriteButton)) .build();
Actualiza las preferencias de los botones de medios después de una interacción del usuario
Después de controlar una interacción con el reproductor, es posible que desees actualizar los botones que se muestran en la IU del controlador. Un ejemplo típico es un botón de activación que cambia su ícono y acción después de activar la acción asociada con este botón. Para actualizar las preferencias de los botones multimedia, puedes usar MediaSession.setMediaButtonPreferences
para actualizar las preferencias de todos los controladores o de un controlador específico:
Kotlin
// Handle "favoritesButton" action, replace by opposite button mediaSession.setMediaButtonPreferences( ImmutableList.of(likeButton, removeFromFavoritesButton))
Java
// Handle "favoritesButton" action, replace by opposite button mediaSession.setMediaButtonPreferences( ImmutableList.of(likeButton, removeFromFavoritesButton));
Agrega comandos personalizados y personaliza el comportamiento predeterminado
Los comandos disponibles del reproductor se pueden extender con comandos personalizados, y también es posible interceptar los comandos entrantes del reproductor y los botones de medios para cambiar el comportamiento predeterminado.
Cómo declarar y controlar comandos personalizados
Las aplicaciones de medios pueden definir comandos personalizados que, por ejemplo, se pueden usar en las preferencias de los botones de medios. Por ejemplo, es posible que desees implementar botones que permitan al usuario guardar un elemento multimedia en una lista de elementos favoritos. El MediaController
envía comandos personalizados y el MediaSession.Callback
los recibe.
Para definir comandos personalizados, debes anular MediaSession.Callback.onConnect()
para establecer los comandos personalizados disponibles para cada controlador conectado.
Kotlin
private class CustomMediaSessionCallback: MediaSession.Callback { // Configure commands available to the controller in onConnect() override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo ): MediaSession.ConnectionResult { val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() .add(SessionCommand(SAVE_TO_FAVORITES, Bundle.EMPTY)) .build() return AcceptedResultBuilder(session) .setAvailableSessionCommands(sessionCommands) .build() } }
Java
class CustomMediaSessionCallback implements MediaSession.Callback { // Configure commands available to the controller in onConnect() @Override public ConnectionResult onConnect( MediaSession session, ControllerInfo controller) { SessionCommands sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle())) .build(); return new AcceptedResultBuilder(session) .setAvailableSessionCommands(sessionCommands) .build(); } }
Para recibir solicitudes de comandos personalizados de un MediaController
, anula el método onCustomCommand()
en el Callback
.
Kotlin
private class CustomMediaSessionCallback: MediaSession.Callback { ... override fun onCustomCommand( session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle ): ListenableFuture<SessionResult> { if (customCommand.customAction == SAVE_TO_FAVORITES) { // Do custom logic here saveToFavorites(session.player.currentMediaItem) return Futures.immediateFuture( SessionResult(SessionResult.RESULT_SUCCESS) ) } ... } }
Java
class CustomMediaSessionCallback implements MediaSession.Callback { ... @Override public ListenableFuture<SessionResult> onCustomCommand( MediaSession session, ControllerInfo controller, SessionCommand customCommand, Bundle args ) { if(customCommand.customAction.equals(SAVE_TO_FAVORITES)) { // Do custom logic here saveToFavorites(session.getPlayer().getCurrentMediaItem()); return Futures.immediateFuture( new SessionResult(SessionResult.RESULT_SUCCESS) ); } ... } }
Puedes hacer un seguimiento del controlador de medios que realiza una solicitud con la propiedad packageName
del objeto MediaSession.ControllerInfo
que se pasa a los métodos Callback
. Esto te permite adaptar el comportamiento de tu app en respuesta a un comando determinado, ya sea que provenga del sistema, de tu propia app o de otras apps cliente.
Personaliza los comandos predeterminados del reproductor
Todos los comandos predeterminados y el control de estado se delegan en el Player
que se encuentra en el MediaSession
. Para personalizar el comportamiento de un comando definido en la interfaz Player
, como play()
o seekToNext()
, incluye tu Player
en un ForwardingSimpleBasePlayer
antes de pasarlo a MediaSession
:
Kotlin
val player = (logic to build a Player instance) val forwardingPlayer = object : ForwardingSimpleBasePlayer(player) { // Customizations } val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()
Java
ExoPlayer player = (logic to build a Player instance) ForwardingSimpleBasePlayer forwardingPlayer = new ForwardingSimpleBasePlayer(player) { // Customizations }; MediaSession mediaSession = new MediaSession.Builder(context, forwardingPlayer).build();
Para obtener más información sobre ForwardingSimpleBasePlayer
, consulta la guía de ExoPlayer sobre Personalización.
Identifica el controlador solicitante de un comando del reproductor
Cuando un MediaController
origina una llamada a un método Player
, puedes identificar la fuente de origen con MediaSession.controllerForCurrentRequest
y adquirir el ControllerInfo
para la solicitud actual:
Kotlin
class CallerAwarePlayer(player: Player) : ForwardingSimpleBasePlayer(player) { override fun handleSeek( mediaItemIndex: Int, positionMs: Long, seekCommand: Int, ): ListenableFuture<*> { Log.d( "caller", "seek operation from package ${session.controllerForCurrentRequest?.packageName}", ) return super.handleSeek(mediaItemIndex, positionMs, seekCommand) } }
Java
public class CallerAwarePlayer extends ForwardingSimpleBasePlayer { public CallerAwarePlayer(Player player) { super(player); } @Override protected ListenableFuture<?> handleSeek( int mediaItemIndex, long positionMs, int seekCommand) { Log.d( "caller", "seek operation from package: " + session.getControllerForCurrentRequest().getPackageName()); return super.handleSeek(mediaItemIndex, positionMs, seekCommand); } }
Cómo personalizar el control de los botones de medios
Los botones de medios son botones de hardware que se encuentran en dispositivos Android y otros dispositivos periféricos, como el botón de reproducción/pausa en auriculares Bluetooth. Media3 controla los eventos de botones de medios por ti cuando llegan a la sesión y llama al método Player
apropiado en el reproductor de sesión.
Se recomienda controlar todos los eventos de botones de medios entrantes en el método Player
correspondiente. Para casos de uso más avanzados, los eventos de los botones de medios se pueden interceptar en MediaSession.Callback.onMediaButtonEvent(Intent)
.
Manejo y generación de informes de errores
Hay dos tipos de errores que una sesión emite y comunica a los controladores. Los errores fatales informan una falla técnica en la reproducción del reproductor de sesión que interrumpe la reproducción. Los errores graves se informan al controlador automáticamente cuando ocurren. Los errores recuperables son errores no técnicos o de política que no interrumpen la reproducción y que la aplicación envía manualmente a los controladores.
Errores de reproducción graves
El reproductor informa un error de reproducción irrecuperable a la sesión y, luego, se informa a los controladores para que llamen a través de Player.Listener.onPlayerError(PlaybackException)
y Player.Listener.onPlayerErrorChanged(@Nullable PlaybackException)
.
En ese caso, el estado de reproducción pasa a STATE_IDLE
y MediaController.getPlaybackError()
devuelve el PlaybackException
que provocó la transición. Un controlador puede inspeccionar el objeto PlayerException.errorCode
para obtener información sobre el motivo del error.
Para la interoperabilidad, se replica un error fatal en la sesión de la plataforma. Para ello, se hace una transición de su estado a STATE_ERROR
y se configuran el código y el mensaje de error según el PlaybackException
.
Personalización de errores irrecuperables
Para proporcionar información localizada y significativa al usuario, el código de error, el mensaje de error y los extras de error de un error de reproducción fatal se pueden personalizar con un ForwardingPlayer
cuando se compila la sesión:
Kotlin
val forwardingPlayer = ErrorForwardingPlayer(player) val session = MediaSession.Builder(context, forwardingPlayer).build()
Java
Player forwardingPlayer = new ErrorForwardingPlayer(player); MediaSession session = new MediaSession.Builder(context, forwardingPlayer).build();
El reproductor de reenvío puede usar ForwardingSimpleBasePlayer
para interceptar el error y personalizar el código, el mensaje o los elementos adicionales del error. De la misma manera, también puedes generar errores nuevos que no existen en el reproductor original:
Kotlin
class ErrorForwardingPlayer (private val context: Context, player: Player) : ForwardingSimpleBasePlayer(player) { override fun getState(): State { var state = super.getState() if (state.playerError != null) { state = state.buildUpon() .setPlayerError(customizePlaybackException(state.playerError!!)) .build() } return state } fun customizePlaybackException(error: PlaybackException): PlaybackException { val buttonLabel: String val errorMessage: String when (error.errorCode) { PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> { buttonLabel = context.getString(R.string.err_button_label_restart_stream) errorMessage = context.getString(R.string.err_msg_behind_live_window) } else -> { buttonLabel = context.getString(R.string.err_button_label_ok) errorMessage = context.getString(R.string.err_message_default) } } val extras = Bundle() extras.putString("button_label", buttonLabel) return PlaybackException(errorMessage, error.cause, error.errorCode, extras) } }
Java
class ErrorForwardingPlayer extends ForwardingSimpleBasePlayer { private final Context context; public ErrorForwardingPlayer(Context context, Player player) { super(player); this.context = context; } @Override protected State getState() { State state = super.getState(); if (state.playerError != null) { state = state.buildUpon() .setPlayerError(customizePlaybackException(state.playerError)) .build(); } return state; } private PlaybackException customizePlaybackException(PlaybackException error) { String buttonLabel; String errorMessage; switch (error.errorCode) { case PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW: buttonLabel = context.getString(R.string.err_button_label_restart_stream); errorMessage = context.getString(R.string.err_msg_behind_live_window); break; default: buttonLabel = context.getString(R.string.err_button_label_ok); errorMessage = context.getString(R.string.err_message_default); break; } Bundle extras = new Bundle(); extras.putString("button_label", buttonLabel); return new PlaybackException(errorMessage, error.getCause(), error.errorCode, extras); } }
Errores recuperables
Los errores no fatales que no se originan a partir de una excepción técnica pueden enviarse desde una app a todos los controladores o a uno específico:
Kotlin
val sessionError = SessionError( SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, context.getString(R.string.error_message_authentication_expired), ) // Option 1: Sending a nonfatal error to all controllers. mediaSession.sendError(sessionError) // Option 2: Sending a nonfatal error to the media notification controller only // to set the error code and error message in the playback state of the platform // media session. mediaSession.mediaNotificationControllerInfo?.let { mediaSession.sendError(it, sessionError) }
Java
SessionError sessionError = new SessionError( SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED, context.getString(R.string.error_message_authentication_expired)); // Option 1: Sending a nonfatal error to all controllers. mediaSession.sendError(sessionError); // Option 2: Sending a nonfatal error to the media notification controller only // to set the error code and error message in the playback state of the platform // media session. ControllerInfo mediaNotificationControllerInfo = mediaSession.getMediaNotificationControllerInfo(); if (mediaNotificationControllerInfo != null) { mediaSession.sendError(mediaNotificationControllerInfo, sessionError); }
Cuando se envía un error no fatal al controlador de notificaciones de medios, el código y el mensaje de error se replican en la sesión de medios de la plataforma, mientras que PlaybackState.state
no cambia a STATE_ERROR
.
Recibir errores recuperables
Un MediaController
recibe un error no fatal implementando MediaController.Listener.onError
:
Kotlin
val future = MediaController.Builder(context, sessionToken) .setListener(object : MediaController.Listener { override fun onError(controller: MediaController, sessionError: SessionError) { // Handle nonfatal error. } }) .buildAsync()
Java
MediaController.Builder future = new MediaController.Builder(context, sessionToken) .setListener( new MediaController.Listener() { @Override public void onError(MediaController controller, SessionError sessionError) { // Handle nonfatal error. } });