Access the operating systems "Media Remote"/Now Playing interface from Java/Kotlin. Works on all modern operating systems.
- Event-driven with very low idle CPU on Windows and Linux, using OS notifications instead of polling (see Event-driven pipeline)
- Get current system media sessions
- Support for multiple apps playing at the same time
- Query name, album, artist, artwork data, duration and additional metadata
- Get the play head position
- Control playback (play/pause/toggle/next/prev/stop)
- Seek support
- Cross-platform universal interface (one API for all platforms)
Requires Java 11 or newer.
Use mediainterface-all to get the core API + all platform providers in one dependency.
Replace VERSION_HERE with the latest version shown in the badge above.
Javadocs:
- Core API: https://javadoc.io/doc/org.endlesssource.mediainterface/core
- Linux: https://javadoc.io/doc/org.endlesssource.mediainterface/linux
- Windows: https://javadoc.io/doc/org.endlesssource.mediainterface/windows
- macOS: https://javadoc.io/doc/org.endlesssource.mediainterface/macos
repositories {
maven("https://maven.endlesssource.org/repository/maven-releases/")
}
dependencies {
implementation("org.endlesssource.mediainterface:all:VERSION_HERE")
}<repositories>
<repository>
<id>endlesssource</id>
<url>https://maven.endlesssource.org/repository/maven-releases/</url>
</repository>
</repositories>
<dependency>
<groupId>org.endlesssource.mediainterface</groupId>
<artifactId>all</artifactId>
<version>VERSION_HERE</version>
</dependency>Also available on Maven Central (note that releases might be delayed):
Gradle (Kotlin):
dependencies {
implementation("org.endlesssource.mediainterface:all:VERSION_HERE")
}Maven:
<dependency>
<groupId>org.endlesssource.mediainterface</groupId>
<artifactId>all</artifactId>
<version>VERSION_HERE</version>
</dependency>Snapshot builds are available from the EndlessSource Nexus snapshot repository:
Gradle (Kotlin):
repositories {
maven("https://maven.endlesssource.org/repository/maven-snapshots/")
}Maven:
<repositories>
<repository>
<id>endlesssource-snapshots</id>
<url>https://maven.endlesssource.org/repository/maven-snapshots/</url>
</repository>
</repositories>If you use shadowJar, add this to your Gradle configuration to merge the included service file:
tasks.shadowJar {
duplicatesStrategy = DuplicatesStrategy.INCLUDE // include all service files
mergeServiceFiles { // always merge service files
include("META-INF/services/org.endlesssource.mediainterface.spi.PlatformMediaProvider")
}
}import org.endlesssource.mediainterface.SystemMediaFactory;
import org.endlesssource.mediainterface.api.SystemMediaInterface;
public class Main {
public static void main(String[] args) {
try (SystemMediaInterface media = SystemMediaFactory.createSystemInterface()) {
media.getActiveSession().ifPresent(session -> {
System.out.println("App: " + session.getApplicationName());
session.getNowPlaying().ifPresent(now -> {
String title = now.getTitle().orElse("Unknown title");
String artist = now.getArtist().orElse("Unknown artist");
System.out.println("Now playing: " + title + " - " + artist);
});
});
}
}
}See examples module
On Windows and Linux, event-driven mode uses the operating system's own change notifications rather than polling on a timer. Because of this, the library only does work when something actually changes. While media is paused or nothing is happening, it stays close to 0% CPU.
On Windows, the native bridge subscribes to the WinRT System Media Transport Controls events (session list, media properties, playback info, and timeline changes). When one fires, it reads the affected session's state once and hands it up to Java, which keeps its own copy. Your reads are served from that cached copy, so the library doesn't call back into the OS on every access. On Linux, the same idea is built on D-Bus/MPRIS2 signals, which arrive as changes happen.
In practice this means your listener callbacks (onNowPlayingChanged, onPlaybackStateChanged, onSessionActiveChanged, onSessionAdded/onSessionRemoved) fire right when a real change occurs, and an idle system stays idle. Callbacks run on the library's own executor, so a slow listener won't hold up event delivery from the OS.
SystemMediaOptions options = SystemMediaOptions.defaults()
.withEventDrivenEnabled(true); // default
try (SystemMediaInterface media = SystemMediaFactory.createSystemInterface(options)) {
media.addSessionListener(new MediaSessionListener() {
@Override
public void onNowPlayingChanged(MediaSession session, Optional<NowPlaying> nowPlaying) {
// fired only when the track/metadata actually changes
}
});
// ... your app keeps running; no polling loop needed
}One exception: if you want the play-head to advance smoothly between OS updates (for a progress bar, say), the library emits interpolated position updates on a light timer. That's the only periodic work in event-driven mode, and it does no OS or native calls, since the position is computed from the last known value. You can turn it off with SystemMediaOptions.withPositionUpdatesEnabled(false) if you'd rather have no periodic activity at all.
Polling mode (withEventDrivenEnabled(false)) is still available everywhere as a fallback, and macOS uses a polling-based backend.
| Platform | Architecture | Backend |
|---|---|---|
| Linux | any | Java D-Bus / MPRIS2 |
| Windows | x64, ARM64 | JNI (mediainterface_winrt) |
| macOS | Intel, Apple Silicon | Perl + MediaRemoteAdapter |
Feature matrix
| Feature | Linux | Windows | macOS |
|---|---|---|---|
| Session discovery (all sessions) | Yes | Yes | No |
| Control/Query multiple sessions at once | Yes | Yes | No |
| Active session selection | Yes | Yes | Yes |
| Session lookup by app name | Yes | Yes | Partial |
| Playback state | Yes | Yes | Yes |
| Now playing: title/album/artist/duration | Yes | Yes | Yes |
| Now playing: artwork | Yes | Yes | Yes |
| Now playing: additional metadata | Yes | Yes | No |
| Now playing: position | Yes | Yes | Yes |
| Now playing: computed position progression | Yes | Yes | Yes |
| Now playing: livestream detection | Untested | Untested | Untested |
| Playback controls: play/pause/toggle/next/prev/stop | Yes | Yes | Yes |
| Playback controls: seek | Yes | Yes | Yes |
| Polling | Yes | Yes | Yes |
| Event-driven | Yes | Yes | Yes |
Event-driven: onPlaybackStateChanged |
Yes | Yes | Yes |
Event-driven: onNowPlayingChanged |
Yes | Yes | Yes |
Event-driven: onSessionActiveChanged |
Yes | Yes | Yes |
Event-driven: onSessionAdded/Removed |
Yes | Yes | No |
| Configurable poll/update intervals | Yes | Yes | Yes |
Java Media Interface is licensed under the Apache 2.0 License. (see LICENSE)
- @TimLohrer for the idea
- mediaremote-adapter for the MediaRemote Perl workaround