Skip to content
 
 

Latest commit

 

History

135 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MediaRemoteAdapter

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

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: 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.

About

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.

Resources

Stars

54 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages