Media Sessions
Media Sessions verbinden deine App mit Androids Systemsteuerungen und externen Geräten. Du lernst, wie Playback-Controls korrekt integriert werden.
Wer eine Musik- oder Podcast-App entwickelt, stößt früh auf eine zentrale Anforderung: Die App soll auf Steuerungsbefehle reagieren, die nicht aus der eigenen UI kommen – sei es der Sperr-Bildschirm, ein Bluetooth-Headset, eine Smartwatch oder ein Auto-Display. Media Sessions sind die standardisierte Brücke, die genau das ermöglicht, und sie sind kein optionaler Komfort, sondern die offizielle Schnittstelle des Betriebssystems für alle Medienintegrationspunkte.
Was ist das?
Eine Media Session ist ein Protokoll, mit dem eine Android-App ihren Wiedergabe-Zustand dem Betriebssystem mitteilt und im Gegenzug Steuerungsbefehle von außen entgegennimmt. Das System weiß dadurch jederzeit, welche App aktiv spielt, welcher Titel läuft und welche Aktionen – Pause, Weiter, Skip – gerade erlaubt sind.
Technisch bildet MediaSession aus der Jetpack-Bibliothek Media3 das Herzstück. Wichtig: Die Session ist kein Audio-Player. Der eigentliche Ton läuft in einem ExoPlayer; die Session ist nur der Metadaten- und Steuerungs-Kanal, den Android nach außen sieht.
Eine aktive Media Session ist der Schlüssel zu mehreren Systemintegrationen gleichzeitig:
- Lock-Screen-Controls – Wiedergabe-Buttons direkt auf dem gesperrten Display.
- Medien-Notification – Der persistente Player in der Benachrichtigungsleiste und im Schnelleinstellungsbereich.
- Externe Geräte – Bluetooth-Headsets, Android Auto, Wear OS reagieren auf Play/Pause/Skip.
- Sprachassistenten – Google Assistant kann deine App steuern, sofern eine Session registriert ist.
Ohne Media Session sind all diese Eintrittspunkte blind für deine App.
Wie funktioniert es?
Eine Media Session besteht aus drei zusammenhängenden Teilen: Erstellen, Metadaten pflegen, Callbacks verarbeiten.
Session erstellen und aktivieren
val player = ExoPlayer.Builder(context).build()
val session = MediaSession.Builder(context, player).build()
Mit Media3 übergibst du dem Builder direkt eine Player-Instanz. Die Session übernimmt von da an die Synchronisierung von Zustand und Steuerungsbefehlen automatisch.
Metadaten setzen
Das System zeigt nur das an, was du ihm mitteilst. Mediendaten hinterlegst du pro MediaItem:
val mediaItem = MediaItem.Builder()
.setMediaId("track-42")
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle("Mein Titel")
.setArtist("Künstlername")
.setArtworkUri(artworkUri)
.build()
)
.build()
player.setMediaItem(mediaItem)
player.prepare()
player.play()
PlaybackState (spielend, pausiert, gepuffert) leitet Media3 direkt vom ExoPlayer-Zustand ab – du musst ihn nicht manuell setzen.
Steuerungsbefehle und Custom Commands
Standardaktionen wie Play, Pause und Skip leitet die Session automatisch an den Player weiter. Eigene Aktionen – etwa „Zu Favoriten hinzufügen” – registrierst du als SessionCommand:
val session = MediaSession.Builder(context, player)
.setCallback(object : MediaSession.Callback {
override fun onCustomCommand(
session: MediaSession,
controller: MediaSession.ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
if (customCommand.customAction == "ADD_FAVORITE") {
addToFavorites(session.player.currentMediaItem)
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
})
.build()
In der Praxis
Die empfohlene Architektur lässt den Player und die Session in einem MediaSessionService laufen – einem Foreground Service, der aktiv bleibt, auch wenn die UI geschlossen ist.
class PlaybackService : MediaSessionService() {
private lateinit var player: ExoPlayer
private lateinit var session: MediaSession
override fun onCreate() {
super.onCreate()
player = ExoPlayer.Builder(this).build()
session = MediaSession.Builder(this, player).build()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = session
override fun onDestroy() {
session.release()
player.release()
super.onDestroy()
}
}
Die UI verbindet sich dann über einen MediaController und spiegelt den Zustand in Compose-State. Der Controller läuft auf dem Main Thread und liefert StateFlow-kompatible Updates.
Stolperfalle 1 – Session nicht freigeben
Wer session.release() in onDestroy() vergisst, hinterlässt eine Zombie-Session. Android zeigt dann veraltete Steuerungselemente auf dem Lock-Screen, die keine Reaktion mehr zeigen. Nutzer erleben eingefrorene Controls; der eigentliche Fehler liegt im Service-Lifecycle.
Stolperfalle 2 – Fehlender Intent-Filter im Manifest
Ohne den korrekten <intent-filter> findet Android Auto, Wear OS oder der Bluetooth-Stack den Service nicht. Die Deklaration muss exakt so lauten:
<service
android:name=".PlaybackService"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
</service>
Fehlt diese Zeile, kompiliert die App problemlos, aber externe Geräte können die Session schlicht nicht finden – ein Fehler, der im eigenen Emulator oft unbemerkt bleibt und erst beim Test mit echtem Zubehör auffällt.
Fazit
Media Sessions sind die Pflichtschnittstelle für jede Medien-App, die über die eigene UI hinausgehen soll. Sie verbinden Player-Zustand und Metadaten mit Lock-Screen, Benachrichtigungen und externen Geräten – und das alles über einen einzigen, klar definierten Kanal. Überprüfe deine eigene App: Öffne den Schnelleinstellungsbereich, während Musik läuft – erscheint dort der Media-Player-Eintrag? Verbinde anschließend ein Bluetooth-Headset und prüfe, ob Play und Pause korrekt weitergeleitet werden. Schreibe außerdem einen einfachen Instrumentierungstest, der einen MediaController mit deinem PlaybackService verbindet, einen play()-Befehl sendet und dann den PlaybackState verifiziert. Nur wer diese Integrationspunkte aktiv testet, bemerkt rechtzeitig, wenn ein Manifest-Eintrag oder ein release()-Aufruf fehlt.