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.

When to choose a media session

When you implement MediaSession, you allow users to control playback:

  • Through their headphones. There are often buttons or touch interactions a user can perform on their headphones to play or pause media or go to the next or previous track.
  • By talking to the Google Assistant. A common pattern is to say "OK Google, pause" to pause any media that is currently playing on the device.
  • Through their Wear OS watch. This allows for easier access to the most common playback controls while playing on their phone.
  • Through the Media controls. This carousel shows controls for each running media session.
  • On TV. Allows actions with physical playback buttons, platform playback control, and power management (for example if the TV, soundbar or A/V receiver switches off or the input is switched, playback should stop in the app).
  • And any other external processes that need to influence playback.

This is great for many use cases. In particular, you should strongly consider using MediaSession when:

  • You're streaming long-form video content, such as movies or live TV.
  • You're streaming long-form audio content, such as podcasts or music playlists.
  • You're building a TV app.

However, not all use cases fit well with the MediaSession. You might want to use just the Player in the following cases:

  • You're showing short-form content, where user engagement and interaction is crucial.
  • There isn't a single active video, such as user is scrolling through a list and multiple videos are displayed on screen at the same time.
  • You're playing a one-off introduction or explanation video, which you expect your user to actively watch.
  • Your content is privacy-sensitive and you don't want external processes to access the media metadata (for example incognito mode in a browser)

If your use case does not fit any of those listed above, consider whether you're okay with your app continuing playback when the user is not actively engaging with the content. If the answer is yes, you probably want to choose MediaSession. If the answer is no, you probably want to use the Player instead.

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.

Unique session ID

By default, MediaSession.Builder creates a session with an empty string as the session ID. This is sufficient if an app intends to only create a single session instance, which is the most common case.

If an app wants to manage multiple session instances at the same time, the app has to ensure that the session ID of each session is unique. The session ID can be set when building the session with MediaSession.Builder.setId(String id).

If you see an IllegalStateException crashing your app with the error message IllegalStateException: Session ID must be unique. ID= then it is likely that a session has been unexpectedly created before a previously created instance with the same ID has been released. To avoid sessions to be leaked by a programming error, such cases are detected and notified by throwing an exception.

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 available 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.

Modify the playlist

A media session can directly modify the playlist of its player as explained in the ExoPlayer guide for playlists. Controllers are also able to modify the playlist if either COMMAND_SET_MEDIA_ITEM or COMMAND_CHANGE_MEDIA_ITEMS is available to the controller.

When adding new items to the playlist, the player typically requires MediaItem instances with a defined URI to make them playable. By default, newly added items are automatically forwarded to player methods like player.addMediaItem if they have a URI defined.

If you want to customize the MediaItem instances added to the player, you can override onAddMediaItems(). This step is needed when you want to support controllers that request media without a defined URI. Instead, the MediaItem typically has one or more of the following fields set to describe the requested media:

  • MediaItem.id: A generic ID identifying the media.
  • MediaItem.RequestMetadata.mediaUri: A request URI that may use a custom schema and is not necessarily directly playable by the player.
  • MediaItem.RequestMetadata.searchQuery: A textual search query, for example from Google Assistant.
  • MediaItem.MediaMetadata: Structured metadata like 'title' or 'artist'.

For more customization options for completely new playlists, you can additionally override onSetMediaItems() that lets you define the start item and position in the playlist. For example, you can expand a single requested item to an entire playlist and instruct the player to start at the index of the originally requested item. A sample implementation of onSetMediaItems() with this feature can be found in the session demo app.

Manage custom layout and custom commands

The following sections describe how to advertise a custom layout of custom command buttons to client apps and authorize controllers to send the custom commands.

Define custom layout of the session

To indicate to client apps which playback controls you want to surface to the user, set the custom layout of the session when building the MediaSession in the onCreate() method of your service.

Kotlin

override fun onCreate() {
  super.onCreate()

  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()

  session =
    MediaSession.Builder(this, player)
      .setCallback(CustomMediaSessionCallback())
      .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
      .build()
}

Java

@Override
public void onCreate() {
  super.onCreate();

  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, new Bundle()))
    .build();

  Player player = new ExoPlayer.Builder(this).build();
  mediaSession =
      new MediaSession.Builder(this, player)
          .setCallback(new CustomMediaSessionCallback())
          .setCustomLayout(ImmutableList.of(likeButton, favoriteButton))
          .build();
}

Declare available player and custom commands

Media applications can define custom commands that for instance can be used in a custom layout. 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 this by overriding MediaSession.Callback.onConnect(). Configure and return the set of available commands when accepting a connection request from a MediaController in the onConnect callback method:

Kotlin

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 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();
  }
}

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.equals(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.

Update custom layout after a user interaction

After handling a custom command or any other interaction with your player, you may want to update the layout displayed in the controller UI. A typical example is a toggle button that changes its icon after triggering the action associated with this button. To update the layout, you can use MediaSession.setCustomLayout:

Kotlin

val removeFromFavoritesButton = CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(SessionCommand(REMOVE_FROM_FAVORITES, Bundle()))
  .build()
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton))

Java

CommandButton removeFromFavoritesButton = new CommandButton.Builder()
  .setDisplayName("Remove from favorites")
  .setIconResId(R.drawable.favorite_remove_icon)
  .setSessionCommand(new SessionCommand(REMOVE_FROM_FAVORITES, new Bundle()))
  .build();
mediaSession.setCustomLayout(ImmutableList.of(likeButton, removeFromFavoritesButton));

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 about ForwardingPlayer, see the ExoPlayer guide on Customization.

Identify the requesting controller of a player command

When a call to a Player method is originated by a MediaController, you can identify the source of origin with MediaSession.controllerForCurrentRequest and acquire the ControllerInfo for the current request:

Kotlin

class CallerAwareForwardingPlayer(player: Player) :
  ForwardingPlayer(player) {

  override fun seekToNext() {
    Log.d(
      "caller",
      "seekToNext called from package ${session.controllerForCurrentRequest?.packageName}"
    )
    super.seekToNext()
  }
}

Java

public class CallerAwareForwardingPlayer extends ForwardingPlayer {
  public CallerAwareForwardingPlayer(Player player) {
    super(player);
  }

  @Override
  public void seekToNext() {
    Log.d(
        "caller",
        "seekToNext called from package: "
            + session.getControllerForCurrentRequest().getPackageName());
    super.seekToNext();
  }
}

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 handles media button events for you when they arrive at the session and calls the appropriate Player method on the session player.

An app can override the default behaviour by overriding MediaSession.Callback.onMediaButtonEvent(Intent). In such a case the app can/needs to handle all API specifics on its own.