Skip to content

ejbills/mediaremote-adapter

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

116 Commits
 
 
 
 
 
 
 
 
 
 

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")")
        }

        // 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")")
}

Filtering by Application (this doesn't work)

let musicController = MediaController(bundleIdentifier: "com.apple.Music")
let spotifyController = MediaController(bundleIdentifier: "com.spotify.client")

API

Callbacks

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

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?

Note: elapsedTimeMicros is stale (captured at last state change). Use currentElapsedTime for accurate position.

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

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Objective-C 41.6%
  • Swift 41.6%
  • Perl 8.7%
  • C 8.1%