A modern Swift Package for controlling macOS media playback (play/pause, track info). Bypasses sandbox restrictions by bridging through the system's entitled Perl interpreter. A fork focused on easy integration for modern Swift apps.
54
stars
135
commits
Objective-C
primary language
Jul 31, 2026
updated
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.
https://github.com/ejbills/mediaremote-adapter.gitMediaRemoteAdapter to your target.After adding the package, ensure the framework is correctly embedded:
MediaRemoteAdapter.framework to "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")")
// 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() }
}
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")")
}
| 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 |
| 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? |
Playback position:
elapsedTimeMicrosis a snapshot from the last state change. UsecurrentElapsedTimeto compute an accurate position on demand -- it interpolates from the snapshot usingtimestampEpochMicrosandplaybackRate.
This project was originally inspired by ungive/mediaremote-adapter. The core technique of using Perl to access the private framework was pioneered there.
Objective-C
49.0%
Swift
34.7%
C
8.2%
Perl
8.2%
A modern Swift Package for controlling macOS media playback (play/pause, track info). Bypasses sandbox restrictions by bridging through the system's entitled Perl interpreter. A fork focused on easy integration for modern Swift apps.
54
stars
135
commits
Objective-C
primary language
Jul 31, 2026
updated
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.
https://github.com/ejbills/mediaremote-adapter.gitMediaRemoteAdapter to your target.After adding the package, ensure the framework is correctly embedded:
MediaRemoteAdapter.framework to "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")")
// 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() }
}
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")")
}
| 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 |
| 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? |
Playback position:
elapsedTimeMicrosis a snapshot from the last state change. UsecurrentElapsedTimeto compute an accurate position on demand -- it interpolates from the snapshot usingtimestampEpochMicrosandplaybackRate.
This project was originally inspired by ungive/mediaremote-adapter. The core technique of using Perl to access the private framework was pioneered there.
Objective-C
49.0%
Swift
34.7%
C
8.2%
Perl
8.2%