Media sessions provide a universal way of interacting with an audio or video
player. In Media3, the default player is the ExoPlayer
class, which implements
the Player
interface. Connecting the media session to the player allows an app
to advertise media playback externally and to receive playback commands from
external sources.
Commands may originate from physical buttons such as the play button on a headset or TV remote control. They might also come from client apps that have a media controller, such as instructing "pause" to Google Assistant. The media session delegates these commands to the media app's player.
Create a media session
A media session lives alongside the player that it manages. You can construct a
media session with a Context
and a Player
object. You should create and
initialize a media session when it is needed, such as the onStart()
or
onResume()
lifecycle method of the Activity
or Fragment
, or onCreate()
method of the Service
that owns the media session and its associated player.
To create a media session, initialize a Player
and supply it to
MediaSession.Builder
like this:
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();
Automatic state handling
The Media3 library automatically updates the media session using the player's state. As such, you don't need to manually handle the mapping from player to session.
This is a break from the legacy approach where you needed to create and maintain
a PlaybackStateCompat
independently from the player itself, for example to
indicate any errors.
Grant control to other clients
The media session is the key to controlling playback. It enables you to route commands from external sources to the player that does the work of playing your media. These sources can be physical buttons such as the play button on a headset or TV remote control, or indirect commands such as instructing "pause" to Google Assistant. Likewise, you may wish to grant access to the Android system to facilitate notification and lock screen controls, or to a Wear OS watch so that you can control playback from the watchface. External clients can use a media controller to issue playback commands to your media app. These are received by your media session, which ultimately delegates commands to the media player.

When a controller is about to connect to your media session, the
onConnect()
method is called. You can use the provided ControllerInfo
to decide whether to accept
or reject
the request. See an example of accepting a connection request in the Declare
supported commands section.
After connecting, a controller can send playback commands to the session. The
session then delegates those commands down to the player. Playback and playlist
commands defined in the Player
interface are automatically handled by the
session.
Other callback methods allow you to handle, for example, requests for
custom playback commands and
modifying the playlist.
These callbacks similarly include a ControllerInfo
object so you
can modify how you respond to each request on a per-controller basis.
Manage playback commands
The following sections describe how to declare supported commands, create custom commands, pick which controls to display in client apps, and modify command behavior.
Declare supported commands
With Media3, you can customize the set of commands available to client apps
using a Media3 MediaController
on a per-controller basis. Configure and return
the set of available commands when accepting a connection request from a
MediaController
in the onConnect
callback method:
Kotlin
class MyCallback : MediaSession.Callback { override fun onConnect( session: MediaSession, controller: ControllerInfo ) : MediaSession.ConnectionResult { // Configure and return the set of available commands when accepting the // connection return MediaSession.ConnectionResult.accept( availableSessionCommands, availablePlayerCommands ) } }
Java
class MyCallback implements MediaSession.Callback { @Override MediaSession.ConnectionResult onConnect( MediaSession session, ControllerInfo controller ) { // Configure and return the set of available commands when accepting the // connection return MediaSession.ConnectionResult.accept( availableSessionCommands, availablePlayerCommands ); } }
Media3 handles
converting
from a Command
to platform PlaybackState
actions.
Customize playback command behavior
To customize the behavior of a command defined in the Player
interface, such
as play()
or seekToNext()
, wrap your Player
in a ForwardingPlayer
.
Kotlin
val player = ExoPlayer.Builder(context).build() val forwardingPlayer = object : ForwardingPlayer(player) { override fun play() { // Add custom logic super.play() } override fun setPlayWhenReady(playWhenReady: Boolean) { // Add custom logic super.setPlayWhenReady(playWhenReady) } } val mediaSession = MediaSession.Builder(context, forwardingPlayer).build()
Java
ExoPlayer player = new ExoPlayer.Builder(context).build(); ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player) { @Override public void play() { // Add custom logic super.play(); } @Override public void setPlayWhenReady(boolean playWhenReady) { // Add custom logic super.setPlayWhenReady(playWhenReady); } }; MediaSession mediaSession = new MediaSession.Builder(context, forwardingPlayer).build();
For more information, see the ExoPlayer guide on Customization.
Add custom commands
Media applications can define custom commands. For example, you might wish to
implement buttons that allow the user to save a media item to a list of
favorite items. The MediaController
sends custom commands and the
MediaSession.Callback
receives them.
You can define which custom session commands are available to a
MediaController when it connects to your media session. You achieve by
overriding MediaSession.Callback.onConnect()
. See below an example of
how you might do so.
Kotlin
// Create a MediaSession in the onCreate() method of your Activity or Service override fun onCreate() { super.onCreate() // Create and assign a custom Callback to the MediaSession val customCallback = CustomMediaSessionCallback() mediaSession = MediaSession.Builder(this, player) .setCallback(customCallback) .build() } private inner class CustomMediaSessionCallback: MediaSession.Callback { // Configure commands available to the controller in onConnect() override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo ): MediaSession.ConnectionResult { val connectionResult = super.onConnect(session, controller) val sessionCommands = connectionResult.availableSessionCommands .buildUpon() // Add custom commands .add(SessionCommand(SAVE_TO_FAVORITES, Bundle())) .build() return MediaSession.ConnectionResult.accept( sessionCommands, connectionResult.availablePlayerCommands ) } }
Java
// Create a MediaSession in the onCreate() method of your Activity or Service @Override public void onCreate() { super.onCreate(); // Create and assign a custom Callback to the MediaSession MediaSession.Callback customCallback = new CustomMediaSessionCallback(); mediaSession = MediaSession.Builder(this, player) .setCallback(customCallback) .build(); } class CustomMediaSessionCallback implements MediaSession.Callback { // Configure commands available to the controller in onConnect() @Override public ConnectionResult onConnect( MediaSession session, ControllerInfo controller ) { ConnectionResult connectionResult = MediaSession.Callback.super.onConnect(session, controller); SessionCommands sessionCommands = connectionResult.availableSessionCommands .buildUpon() // Add custom commands .add(new SessionCommand(SAVE_TO_FAVORITES, new Bundle())) .build(); return MediaSession.ConnectionResult.accept( sessionCommands, connectionResult.availablePlayerCommands ); } }
To receive custom command requests from a MediaController
, override the
onCustomCommand()
method in the Callback
.
Kotlin
private inner 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 == SAVE_TO_FAVORITES) { // Do custom logic here saveToFavorites(session.getPlayer().getCurrentMediaItem()); return Futures.immediateFuture( new SessionResult(SessionResult.RESULT_SUCCESS) ); } ... } }
You can track which media controller is making a request by using the
packageName
property of the MediaSession.ControllerInfo
object that is
passed into Callback
methods. This allows you to tailor your app's
behavior in response to a given command if it originates from the system, your
own app, or other client apps.
Display playback controls in clients
To indicate to client apps which playback controls you want to surface to the
user, call setCustomLayout()
in the onPostConnect()
callback method.
Kotlin
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) { // Start by creating a CommandButton for each control you want in the layout. This is where // you can specify the name for each button and which icon should represent it. val playPauseButton = CommandButton.Builder() .setDisplayName(if(session.player.isPlaying) "Play" else "Pause") .setIconResId(if(session.player.isPlaying) R.drawable.play_icon else R.drawable.pause_icon) .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) .build() val likeButton = CommandButton.Builder() .setDisplayName("Like") .setIconResId(R.drawable.like_icon) .setSessionCommand(SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING)) .build() val favoriteButton = CommandButton.Builder() .setDisplayName("Save to favorites") .setIconResId(R.drawable.favorite_icon) .setSessionCommand(SessionCommand(SAVE_TO_FAVORITES, Bundle())) .build() // Pass in a list of the controls that the client app should display to users. The arrangement // of these controls in the UI is managed by the client app. session.setCustomLayout(controller, listOf(playPauseButton, likeButton, favoriteButton)) // Use session.setCustomLayout(listOf(playPauseButton, likeButton, favoriteButton)) // for clients using a legacy controller super.onPostConnect(session, controller) }
Java
@Override public void onPostConnect(MediaSession session, ControllerInfo controller) { // Start by creating a CommandButton for each control you want in the layout. This is where // you can specify the name for each button and which icon should represent it. CommandButton playPauseButton = new CommandButton.Builder() .setDisplayName(session.player.isPlaying ? "Play" : "Pause") .setIconResId(session.player.isPlaying ? R.drawable.play_icon : R.drawable.pause_icon) .setPlayerCommand(Player.COMMAND_PLAY_PAUSE) .build(); CommandButton likeButton = new CommandButton.Builder() .setDisplayName("Like") .setIconResId(R.drawable.like_icon) .setSessionCommand(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING)) .build(); CommandButton favoriteButton = new CommandButton.Builder() .setDisplayName("Save to favorites") .setIconResId(R.drawable.favorite_icon) .setSessionCommand(new SessionCommand(SAVE_TO_FAVORITES, Bundle())) .build(); // Pass in a list of the controls that the client app should display to users. The arrangement // of these controls in the UI is managed by the client app. session.setCustomLayout( controller, ImmutableList.of(playPauseButton, likeButton, favoriteButton) ); MediaSession.Callback.super.onPostConnect(session, controller); }
Respond to media buttons
Media buttons are hardware buttons found on Android devices and other peripheral
devices, such as the play/pause button on a Bluetooth headset. Media3
can handle media button inputs for you. To enable this behavior, declare
the Media3 MediaButtonReceiver
in your manifest.
<receiver android:name="androidx.media3.session.MediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
Playback resumption
Media3 includes an API to enable users to resume
playback after an app has terminated and even after the device has been
restarted. By default, playback resumption is turned off. This means the user
can't resume playback after your session is released. To opt-in, start by
declaring the MediaButtonReceiver
in your manifest.
When playback resumption is requested by either a Bluetooth device or the
Android System UI resumption feature,
the onPlaybackResumption()
callback method is called.
Kotlin
override fun onPlaybackResumption( mediaSession: MediaSession, controller: ControllerInfo ): ListenableFuture<MediaItemsWithStartPosition> { val settable = SettableFuture.create<MediaItemsWithStartPosition>() scope.launch { // Your app is responsible for storing the playlist and the start position // to use here val resumptionPlaylist = restorePlaylist() settable.set(resumptionPlaylist) } return settable }
Java
@Override public ListenableFuture<MediaItemsWithStartPosition> onPlaybackResumption( MediaSession mediaSession, ControllerInfo controller ) { SettableFuture<MediaItemsWithStartPosition> settableFuture = SettableFuture.create(); settableFuture.addListener(() -> { // Your app is responsible for storing the playlist and the start position // to use here MediaItemsWithStartPosition resumptionPlaylist = restorePlaylist(); settableFuture.set(resumptionPlaylist); }, MoreExecutors.directExecutor()); return settableFuture; }
If you've stored other parameters such as playback speed, repeat mode, or
shuffle mode, onPlaybackResumption()
is a good place to reconfigure these
parameters. Media3 will then handle preparing the player and starting playback.