Control and advertise playback using a MediaSession

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.

A diagram demonstrating the interaction between a MediaSession and MediaController.
Figure 1: The media controller facilitates passing commands from external sources to the media session.

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.