It is often desirable to play media while an app is not in the foreground. For example, a music player generally keeps playing music when the user has locked their device or is using another app. The Media3 library provides a series of interfaces that allow you to support background playback.
Use a MediaSessionService
To enable background playback, you should contain the Player
and
MediaSession
inside a separate Service.
This allows the device to continue serving media even while your app is not in
the foreground.

MediaSessionService
allows the media
session to run separately from the app's activityWhen hosting a player inside a Service, you should use a MediaSessionService
.
To do this, create a class that extends MediaSessionService
and create your
media session inside of it.
Using MediaSessionService
makes it possible for external clients like Google
Assistant, system media controls, media buttons on peripheral devices, or
companion devices like Wear OS to discover your service, connect to it, and
control playback, all without accessing your app's UI activity at all. In fact,
there can be multiple client apps connected to the same MediaSessionService
at
the same time, each app with its own MediaController
.
Implement the service lifecycle
You need to implement two lifecycle methods of your service:
onCreate()
is called when the first controller is about to connect and the service is instantiated and started. It's the best place to buildPlayer
andMediaSession
.onDestroy()
is called when the service is being stopped. All resources including player and session need to be released.
You can optionally override onTaskRemoved(Intent)
to customize what happens
when the user dismisses the app from the recent tasks. By default, the service
is left running if playback is ongoing and is stopped otherwise.
Kotlin
class PlaybackService : MediaSessionService() { private var mediaSession: MediaSession? = null // Create your player and media session in the onCreate lifecycle event override fun onCreate() { super.onCreate() val player = ExoPlayer.Builder(this).build() mediaSession = MediaSession.Builder(this, player).build() } // Remember to release the player and media session in onDestroy override fun onDestroy() { mediaSession?.run { player.release() release() mediaSession = null } super.onDestroy() } }
Java
public class PlaybackService extends MediaSessionService { private MediaSession mediaSession = null; // Create your Player and MediaSession in the onCreate lifecycle event @Override public void onCreate() { super.onCreate(); ExoPlayer player = new ExoPlayer.Builder(this).build(); mediaSession = new MediaSession.Builder(this, player).build(); } // Remember to release the player and media session in onDestroy @Override public void onDestroy() { mediaSession.getPlayer().release(); mediaSession.release(); mediaSession = null; super.onDestroy(); } }
As an alternative of keeping playback ongoing in the background, you can stop the service in any case when the user dismisses the app:
Kotlin
override fun onTaskRemoved(rootIntent: Intent?) { pauseAllPlayersAndStopSelf() }
Java
@Override public void onTaskRemoved(@Nullable Intent rootIntent) { pauseAllPlayersAndStopSelf(); }
For any other manual implementation of onTaskRemoved
, you can use
isPlaybackOngoing()
to check if the playback is considered ongoing and the
foreground service is started.
Provide access to the media session
Override the onGetSession()
method to give other clients access to your media
session that was built when the service was created.
Kotlin
class PlaybackService : MediaSessionService() { private var mediaSession: MediaSession? = null // [...] lifecycle methods omitted override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = mediaSession }
Java
public class PlaybackService extends MediaSessionService { private MediaSession mediaSession = null; // [...] lifecycle methods omitted @Override public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) { return mediaSession; } }
Declare the service in the manifest
An app requires the FOREGROUND_SERVICE
and FOREGROUND_SERVICE_MEDIA_PLAYBACK
permissions to run a playback foreground service:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
You must also declare your Service
class in the manifest with an intent filter
of MediaSessionService
and a foregroundServiceType
that includes
mediaPlayback
.
<service
android:name=".PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>
</service>
Control playback using a MediaController
In the Activity or Fragment containing your player UI, you can establish a link
between the UI and your media session using a MediaController
. Your UI uses
the media controller to send commands from your UI to the player within the
session. See the
Create a MediaController
guide for details on creating and using a MediaController
.
Handle MediaController
commands
The MediaSession
receives commands from the controller through its
MediaSession.Callback
. Initializing a MediaSession
creates a default
implementation of MediaSession.Callback
that automatically handles all
commands a MediaController
sends to your player.
Notification
A MediaSessionService
automatically creates a MediaNotification
for you that
should work in most cases. By default, the published notification is a
MediaStyle
notification that stays updated with the latest
information from your media session and displays playback controls. The
MediaNotification
is aware of your session and can be used to control playback
for any other apps that are connected to the same session.
For example, a music streaming app using a MediaSessionService
would create a
MediaNotification
that displays the title, artist, and album art for the
current media item being played alongside playback controls based on your
MediaSession
configuration.
The required metadata can be provided in the media or declared as part of the media item as in the following snippet:
Kotlin
val mediaItem = MediaItem.Builder() .setMediaId("media-1") .setUri(mediaUri) .setMediaMetadata( MediaMetadata.Builder() .setArtist("David Bowie") .setTitle("Heroes") .setArtworkUri(artworkUri) .build() ) .build() mediaController.setMediaItem(mediaItem) mediaController.prepare() mediaController.play()
Java
MediaItem mediaItem = new MediaItem.Builder() .setMediaId("media-1") .setUri(mediaUri) .setMediaMetadata( new MediaMetadata.Builder() .setArtist("David Bowie") .setTitle("Heroes") .setArtworkUri(artworkUri) .build()) .build(); mediaController.setMediaItem(mediaItem); mediaController.prepare(); mediaController.play();
Notification lifecycle
The notification is created as soon as the Player
has MediaItem
instances
in its playlist.
All notification updates happen automatically based on the Player
and
MediaSession
state.
The notification cannot be removed while the foreground service is running. To
immediately remove the notification, you must call Player.release()
or clear
the playlist using Player.clearMediaItems()
.
If the player is paused, stopped or failed for more than 10 minutes without further user interactions, the service is automatically transitioned out of the foreground service state so it can be destroyed by the system. You can implement playback resumption to allow a user to restart the service lifecycle and resume playback at a later point in time.
Notification customization
The metadata about the currently playing item can be customized by modifying
the MediaItem.MediaMetadata
. If you want to update the metadata of an existing
item, you can use Player.replaceMediaItem
to update the metadata without
interrupting playback.
You can also customize some of the buttons shown in the notification by setting custom media button preferences for the Android Media controls. Read more about customizing Android Media controls.
To further customize the notification itself, create a
MediaNotification.Provider
with DefaultMediaNotificationProvider.Builder
or by creating a custom implementation of the provider interface. Add your
provider to your MediaSessionService
with
setMediaNotificationProvider
.
Playback resumption
After the MediaSessionService
has been terminated, and even after the device
has been rebooted, it is possible to offer playback resumption to let users
restart the service and resume playback where they left off. By default,
playback resumption is turned off. This means the user can't resume playback
when your service isn't running. To opt-in to this feature, you need to declare
a media button receiver and implement the onPlaybackResumption
method.
Declare the Media3 media button receiver
Start by declaring the 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>
Implement playback resumption callback
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 configure the player
with these parameters before Media3 prepares the player and starts playback when
the callback completes.
Advanced controller configuration and backward compatibility
A common scenario is using a MediaController
in the app UI for controlling
playback and displaying the playlist. At the same time, the session is exposed
to external clients like Android media controls and Assistant on mobile or TV,
Wear OS for watches and Android Auto in cars. The Media3 session demo app
is an example of an app that implements such a scenario.
These external clients may use APIs like MediaControllerCompat
of the legacy
AndroidX library or android.media.session.MediaController
of the Android
platform. Media3 is fully backward compatible with the legacy library and
provides interoperability with the Android platform API.
Use the media notification controller
It's important to understand that these legacy and platform controllers share
the same state and visibility can't be customized by controller (for example the
available PlaybackState.getActions()
and PlaybackState.getCustomActions()
).
You can can use the media notification controller to
configure the state set in the platform media session for compatibility to these
legacy and platform controllers.
For example, an app can provide an implementation of
MediaSession.Callback.onConnect()
to set available commands and
media button preferences specifically for the platform session as follows:
Kotlin
override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo ): ConnectionResult { if (session.isMediaNotificationController(controller)) { val sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build() val playerCommands = ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon() .remove(COMMAND_SEEK_TO_PREVIOUS) .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) .remove(COMMAND_SEEK_TO_NEXT) .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .build() // Custom button preferences and commands to configure the platform session. return AcceptedResultBuilder(session) .setMediaButtonPreferences( ImmutableList.of( createSeekBackwardButton(customCommandSeekBackward), createSeekForwardButton(customCommandSeekForward)) ) .setAvailablePlayerCommands(playerCommands) .setAvailableSessionCommands(sessionCommands) .build() } // Default commands with default button preferences for all other controllers. return AcceptedResultBuilder(session).build() }
Java
@Override public ConnectionResult onConnect( MediaSession session, MediaSession.ControllerInfo controller) { if (session.isMediaNotificationController(controller)) { SessionCommands sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS .buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build(); Player.Commands playerCommands = ConnectionResult.DEFAULT_PLAYER_COMMANDS .buildUpon() .remove(COMMAND_SEEK_TO_PREVIOUS) .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM) .remove(COMMAND_SEEK_TO_NEXT) .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM) .build(); // Custom button preferences and commands to configure the platform session. return new AcceptedResultBuilder(session) .setMediaButtonPreferences( ImmutableList.of( createSeekBackwardButton(customCommandSeekBackward), createSeekForwardButton(customCommandSeekForward))) .setAvailablePlayerCommands(playerCommands) .setAvailableSessionCommands(sessionCommands) .build(); } // Default commands with default button preferences for all other controllers. return new AcceptedResultBuilder(session).build(); }
Authorize Android Auto to send custom commands
When using a MediaLibraryService
and to support Android Auto with the mobile app, the Android Auto controller
requires appropriate available commands, otherwise Media3 would deny
incoming custom commands from that controller:
Kotlin
override fun onConnect( session: MediaSession, controller: MediaSession.ControllerInfo ): ConnectionResult { val sessionCommands = ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build() if (session.isMediaNotificationController(controller)) { // [...] See above. } else if (session.isAutoCompanionController(controller)) { // Available session commands to accept incoming custom commands from Auto. return AcceptedResultBuilder(session) .setAvailableSessionCommands(sessionCommands) .build() } // Default commands for all other controllers. return AcceptedResultBuilder(session).build() }
Java
@Override public ConnectionResult onConnect( MediaSession session, MediaSession.ControllerInfo controller) { SessionCommands sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS .buildUpon() .add(customCommandSeekBackward) .add(customCommandSeekForward) .build(); if (session.isMediaNotificationController(controller)) { // [...] See above. } else if (session.isAutoCompanionController(controller)) { // Available commands to accept incoming custom commands from Auto. return new AcceptedResultBuilder(session) .setAvailableSessionCommands(sessionCommands) .build(); } // Default commands for all other controllers. return new AcceptedResultBuilder(session).build(); }
The session demo app has an automotive module, that demonstrates support for Automotive OS that requires a separate APK.