Media3 MediaSession and Background Playback

AndroidKotlinMedia3Background Work
Share on LinkedIn

Background audio on Android used to be a swamp of MediaSessionCompat, a foreground service you hand-rolled, and a notification you assembled by hand. Media3 replaces all of it with two collaborating pieces: a MediaSession that wraps your Player, and a MediaSessionService that hosts the session so playback survives your UI being gone. Get these two right and the system media notification, lock-screen controls, Android Auto, and Wear surfaces all read from the same source of truth.

I'll walk through the setup that actually holds up in production — not just "audio plays," but audio that keeps playing correctly through backgrounding, audio focus changes, and process death.

The two pieces and why both exist

The split maps cleanly onto responsibilities:

Your UI never touches the service's Player directly. It connects with a MediaController, which speaks the same Player interface but marshals calls over IPC to the session. That decoupling is the whole point: the service owns playback, the UI is just one of many possible controllers.

The service

class PlaybackService : MediaSessionService() {
    private var session: MediaSession? = null

    override fun onCreate() {
        super.onCreate()
        val player = ExoPlayer.Builder(this)
            .setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus = */ true)
            .setHandleAudioBecomingNoisy(true)
            .build()
        session = MediaSession.Builder(this, player).build()
    }

    override fun onGetSession(info: MediaSession.ControllerInfo) = session

    override fun onTaskRemoved(rootIntent: Intent?) {
        val player = session?.player ?: return
        if (!player.playWhenReady || player.mediaItemCount == 0) {
            stopSelf()
        }
    }

    override fun onDestroy() {
        session?.run { player.release(); release() }
        session = null
        super.onDestroy()
    }
}

Two flags earn their keep in the builder. handleAudioFocus = true makes the player request focus, duck for notifications, and pause on a phone call automatically — behavior users expect and that's miserable to reimplement. setHandleAudioBecomingNoisy(true) pauses when headphones are unplugged, so your track doesn't suddenly blast from the phone speaker on a train.

Register it with the foreground service type in the manifest:

<service
    android:name=".PlaybackService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="true">
    <intent-filter>
        <action android:name="androidx.media3.session.MediaSessionService"/>
    </intent-filter>
</service>

The notification comes for free

This is the biggest quality-of-life win over the old world. MediaSessionService generates the media-style notification from the session's current MediaItem metadata and playback state — title, artist, artwork, play/pause/skip. You don't build a Notification at all in the common case. If you need custom command buttons, you provide them via a MediaNotification.Provider or custom session commands, but the default covers most audio apps.

Because it derives from the session, the notification, lock screen, and any Android Auto surface stay in sync automatically. One state, many views.

Connecting the UI

Your Activity or Compose screen builds a controller against the service's token:

val token = SessionToken(context, ComponentName(context, PlaybackService::class.java))
val controllerFuture = MediaController.Builder(context, token).buildAsync()

controllerFuture.addListener({
    val controller = controllerFuture.get()
    controller.setMediaItems(playlist)
    controller.prepare()
    controller.play()
}, MoreExecutors.directExecutor())

The controller is a Player, so your UI observes Player.Listener callbacks for position, buffering, and item transitions exactly as if it held the player locally. Release the controller (MediaController.releaseFuture) in your UI's teardown so you don't leak the connection.

The things that bite in production

A checklist from shipping this:

  1. Release order. Always release the Player before the MediaSession, and null your references. Reversed order or a leaked player is the classic Media3 memory leak.
  2. onTaskRemoved. Decide your behavior when the user swipes the app away. Music apps often keep playing; a video app usually stops. Handle it explicitly or you'll get inconsistent behavior across OEMs.
  3. Process death. If the system kills your service under memory pressure, playback stops. For resume-after-kill, persist the current item and position and restore on reconnect — the OS won't do it for you.
  4. Notification permission. On Android 13+ the media notification needs POST_NOTIFICATIONS. Request it or your foreground service notification may not appear.
  5. Playlist vs single item. Feed the player a full MediaItem list so skip-next/prev work from the notification without you wiring anything.

What I'd take away

Model background audio as a MediaSession wrapping the Player plus a MediaSessionService hosting it, and let the service generate the media notification for you. Turn on handleAudioFocus and handleAudioBecomingNoisy so the player behaves like users expect, connect your UI through a MediaController rather than reaching into the service, and be deliberate about release order, onTaskRemoved, and restoring state after process death. That's the setup that keeps audio playing correctly with the screen off, through a phone call, and after a swipe-away — which is the entire job of a media app.

Common production mistakes

Teams get media3 media session wrong in predictable ways:

Shipping media3 media session on Android fails quietly when you test only on flagship devices, skip process-death scenarios, or assume minSdk behavior matches latest API docs. Emulator-only validation misses OEM-specific battery optimizations and background execution limits.

Resources

Frequently asked questions

What is the difference between MediaSession and MediaSessionService in Media3?

MediaSession is the object that wraps your Player and exposes playback state, metadata, and transport controls to external clients. MediaSessionService is the foreground service that hosts one or more MediaSessions so playback continues when your UI is gone. You use the session for control and the service for lifecycle — together they give you background audio with a system media notification.

Do I need a foreground service for background audio in Media3?

Yes. Continuous audio playback while the app is backgrounded requires a foreground service with the mediaPlayback service type, which is exactly what MediaSessionService provides. The service publishes a media-style notification that satisfies the foreground-service requirement and gives users transport controls.

How does MediaController connect to a MediaSession?

MediaController is built asynchronously against a SessionToken that points at your MediaSessionService. Once connected it implements the same Player interface as the underlying player, so your UI calls play, pause, and seek on the controller and those commands travel over IPC to the session. This decouples your UI from the service's lifecycle.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →