A Swift package for macOS that provides a modern interface for controlling media playback and receiving track information from the private MediaRemote.framework.
Add MediaRemoteAdapter to your project using Swift Package Manager.
- In Xcode: File > Add Packages...
- Enter:
https://github.com/ejbills/mediaremote-adapter.git - Add
MediaRemoteAdapterto your target.
After adding the package, ensure the framework is correctly embedded:
- Select your project, then your main application target.
- Go to the General tab.
- In "Frameworks, Libraries, and Embedded Content", set
MediaRemoteAdapter.frameworkto "Embed & Sign".
import MediaRemoteAdapter
class YourAppController {
let mediaController = MediaController()
init() {
// Handle incoming track data (nil when no media player is active)
mediaController.onTrackInfoReceived = { trackInfo in
guard let trackInfo = trackInfo else {
print("No media playing")
return
}
print("Now Playing: \(trackInfo.payload.title ?? "N/A")")
}
// Handle listener termination
mediaController.onListenerTerminated = {
print("Listener terminated")
}
}
func start() {
mediaController.startListening()
}
// Playback controls
func play() { mediaController.play() }
func pause() { mediaController.pause() }
func togglePlayPause() { mediaController.togglePlayPause() }
func nextTrack() { mediaController.nextTrack() }
func previousTrack() { mediaController.previousTrack() }
func stop() { mediaController.stop() }
func seek(to seconds: Double) { mediaController.setTime(seconds: seconds) }
// Shuffle and repeat
func setShuffle(_ mode: TrackInfo.ShuffleMode) { mediaController.setShuffleMode(mode) }
func setRepeat(_ mode: TrackInfo.RepeatMode) { mediaController.setRepeatMode(mode) }
func toggleShuffle() { mediaController.toggleShuffle() }
func toggleRepeat() { mediaController.toggleRepeat() }
// Seeking
func skipFifteenSeconds() { mediaController.skipFifteenSeconds() }
func goBackFifteenSeconds() { mediaController.goBackFifteenSeconds() }
func startForwardSeek() { mediaController.startForwardSeek() }
func endForwardSeek() { mediaController.endForwardSeek() }
func startBackwardSeek() { mediaController.startBackwardSeek() }
func endBackwardSeek() { mediaController.endBackwardSeek() }
// Rating
func likeTrack() { mediaController.likeTrack() }
func banTrack() { mediaController.banTrack() }
func addToWishList() { mediaController.addToWishList() }
func removeFromWishList() { mediaController.removeFromWishList() }
}Get current track info without starting a listener:
mediaController.getTrackInfo { trackInfo in
guard let trackInfo = trackInfo else { return }
print("Currently playing: \(trackInfo.payload.title ?? "Unknown")")
}let musicController = MediaController(bundleIdentifier: "com.apple.Music")
let spotifyController = MediaController(bundleIdentifier: "com.spotify.client")| Callback | Description |
|---|---|
onTrackInfoReceived: ((TrackInfo?) -> Void)? |
Called with track info, or nil when no media is playing |
onPlaybackTimeUpdate: ((TimeInterval) -> Void)? |
Elapsed time updates (polling-based, use sparingly) |
onDecodingError: ((Error, Data) -> Void)? |
JSON decode errors |
onListenerTerminated: (() -> Void)? |
Listener process terminated |
| Method | Description |
|---|---|
startListening() |
Start background listener |
stopListening() |
Stop listener |
getTrackInfo(_:) |
One-shot track info fetch |
play(), pause(), togglePlayPause() |
Playback control |
nextTrack(), previousTrack(), stop() |
Track navigation |
setTime(seconds:) |
Seek to position |
setShuffleMode(_:) |
Set shuffle (.off, .songs, .albums) |
setRepeatMode(_:) |
Set repeat (.off, .one, .all) |
toggleShuffle() |
Cycle through shuffle modes |
toggleRepeat() |
Cycle through repeat modes |
skipFifteenSeconds() |
Skip forward 15 seconds |
goBackFifteenSeconds() |
Skip back 15 seconds |
startForwardSeek(), endForwardSeek() |
Continuous forward seek (hold/release) |
startBackwardSeek(), endBackwardSeek() |
Continuous backward seek (hold/release) |
likeTrack() |
Like the current track |
banTrack() |
Ban/dislike the current track |
addToWishList() |
Add current track to wish list |
removeFromWishList() |
Remove current track from wish list |
| Property | Type | Description |
|---|---|---|
title, artist, album, applicationName, bundleIdentifier |
String? |
|
isPlaying |
Bool? |
|
durationMicros, elapsedTimeMicros, timestampEpochMicros |
Double? |
|
playbackRate |
Double? |
1.0 = playing, 0.0 = paused |
currentElapsedTime |
TimeInterval? |
Computed - real-time position in seconds |
artwork |
NSImage? |
Decoded once from base64 data at init |
PID |
pid_t? |
|
shuffleMode |
ShuffleMode? |
|
repeatMode |
RepeatMode? |
Note:
elapsedTimeMicrosis stale (captured at last state change). UsecurrentElapsedTimefor accurate position.
This project was originally inspired by ungive/mediaremote-adapter. The core technique of using Perl to access the private framework was pioneered there.