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.

  1. In Xcode: File > Add Packages...
  2. Enter: https://github.com/ejbills/mediaremote-adapter.git
  3. Add MediaRemoteAdapter to your target.

Embedding the Framework

After adding the package, ensure the framework is correctly embedded:

  1. Select your project, then your main application target.
  2. Go to the General tab.
  3. In "Frameworks, Libraries, and Embedded Content", set MediaRemoteAdapter.framework to "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

CallbackDescription
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

MethodDescription
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

PropertyTypeDescription
title, artist, album, applicationName, bundleIdentifierString?
isPlayingBool?
durationMicros, elapsedTimeMicros, timestampEpochMicrosDouble?
playbackRateDouble?1.0 = playing, 0.0 = paused
currentElapsedTimeTimeInterval?Computed - real-time position in seconds
artworkNSImage?Decoded once from base64 data at init
PIDpid_t?
shuffleModeShuffleMode?
repeatModeRepeatMode?

Playback position: elapsedTimeMicros is a snapshot from the last state change. Use currentElapsedTime to compute an accurate position on demand -- it interpolates from the snapshot using timestampEpochMicros and playbackRate.

Acknowledgements

This project was originally inspired by ungive/mediaremote-adapter. The core technique of using Perl to access the private framework was pioneered there.