MediaRemoteAdapter
April 5, 2026 ยท View on GitHub
A Swift package for macOS that provides a modern interface for controlling media playback and receiving track information from the private MediaRemote.framework.
Installation
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.
Embedding the Framework
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".
Usage
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")")
// Get real-time playback position on demand
if let position = trackInfo.payload.currentElapsedTime {
print("Position: \(position)s")
}
}
// 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() }
}
One-Shot Track Info
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")")
}
API
Callbacks
| Callback | Description |
|---|---|
onTrackInfoReceived: ((TrackInfo?) -> Void)? | Called with track info, or nil when no media is playing |
onDecodingError: ((Error, Data) -> Void)? | JSON decode errors |
onListenerTerminated: (() -> Void)? | Listener process terminated |
Methods
| 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 |
TrackInfo.Payload
| 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? |
Playback position:
elapsedTimeMicrosis a snapshot from the last state change. UsecurrentElapsedTimeto compute an accurate position on demand -- it interpolates from the snapshot usingtimestampEpochMicrosandplaybackRate.
Acknowledgements
This project was originally inspired by ungive/mediaremote-adapter. The core technique of using Perl to access the private framework was pioneered there.