Media controls
Publish "Now Playing" metadata and receive playback events from the OS HUD — macOS MPNowPlayingInfoCenter, Windows SMTC, Linux MPRIS — through one Kotlin API.
Every modern OS exposes a system-level media HUD — Control Center on macOS, the volume overlay on Windows 11, GNOME / KDE indicators on Linux. Show up there with your track metadata and respond to play / pause / next / prev events using a single Kotlin module.
TL;DR
- Linux: MPRIS D-Bus spec — integrates with GNOME Shell, KDE Plasma,
playerctl. - macOS:
MPNowPlayingInfoCenter+MPRemoteCommandCenter. Integrates with Control Center, the Now Playing widget, media keys. - Windows:
SystemMediaTransportControls. Integrates with the Windows 10/11 media overlay, SoundBar, lock screen, hardware media keys. - One callback for incoming events, one setter for outgoing metadata and state.
Install
dependencies {
implementation("dev.nucleusframework:nucleus.media-control:<version>")
}Quickstart
import dev.nucleusframework.media.control.*
if (MediaControlService.isAvailable()) {
MediaControlService.configure()
MediaControlService.attach { event ->
when (event) {
MediaControlEvent.Play -> player.play()
MediaControlEvent.Pause -> player.pause()
MediaControlEvent.Next -> player.skipToNext()
MediaControlEvent.Previous -> player.skipToPrevious()
is MediaControlEvent.Seek -> player.seekTo(event.toUs)
else -> {}
}
}
MediaControlService.setMetadata(
MediaMetadata(
title = "Strobe",
artist = "deadmau5",
album = "For Lack of a Better Name",
artUri = "https://example.com/cover.png",
durationUs = 10 * 60 * 1_000_000L,
),
)
MediaControlService.setPlaybackState(
MediaPlaybackState(status = MediaPlaybackStatus.Playing, positionUs = 0, rate = 1.0),
)
}How it works
MediaControlService is a singleton wrapping a per-OS native bridge:
- Linux uses GLib/GIO (
libgio-2.0) to publish anorg.mpris.MediaPlayer2.PlayerD-Bus object. - macOS drives
MediaPlayer.frameworkvia an Objective-C JNI bridge. - Windows drives
SystemMediaTransportControlsvia C++/WRL.
You configure once at startup, then push metadata and playback state as your player evolves. Incoming events arrive on the bridge thread — bounce to your audio thread or main loop yourself.
Capability per platform
The events the OS can send back differ:
- Linux (MPRIS) can emit every
MediaControlEventvariant. - macOS emits only
Play,Pause,Toggle,Next,Previous,Stop,SetPosition. - Windows (SMTC) emits
Play,Pause,Next,Previous,Stop,SetPosition, plus relativeSeekBy(±10 s fast-forward / rewind). SetVolume,OpenUri,Raise,Quitare Linux-only.setVolume(...)is a no-op on macOS and Windows — system volume is owned separately from the media controls.
Reference
Metadata
MediaControlService.setMetadata(
MediaMetadata(
title = "Track",
artist = "Artist",
album = "Album",
artUri = "file:///tmp/cover.png",
durationUs = 3 * 60 * 1_000_000L,
),
)Playback state
MediaControlService.setPlaybackState(
MediaPlaybackState(
status = MediaPlaybackStatus.Playing,
positionUs = currentPositionUs,
rate = 1.0,
),
)Push state updates whenever you start, pause, or seek — the HUD's seek bar mirrors positionUs and rate.
Event listener
MediaControlService.attach { event -> /* handle */ }
MediaControlService.detach()Per-platform integration tabs
Set the Info.plist LSApplicationCategoryType to public.app-category.music (or media) so the Now Playing widget treats the app correctly. Hardware media keys deliver Play/Pause/Next/Prev when the app is the active "now playing" client.
Toast / Action Center integration is automatic. The system picks the most recent SMTC publisher as the "now playing" source.
Set appId correctly via NucleusApp.appId — MPRIS exposes the bus name as org.mpris.MediaPlayer2.<appId>. playerctl and indicators discover the player by enumerating this prefix.
Notes
- Always call
MediaControlService.isAvailable()beforeconfigure()— on Linux without a session D-Bus, the service degrades silently. - Update
setPlaybackStateat least once per second while playing; otherwise the HUD's progress bar drifts.