For developers

Show your app in Playstate.

Any Mac music app can tell Playstate what it’s playing and take playback commands back, with one notification and no permissions.

Playstate shows what’s playing in the menu bar and on the desktop, with artwork and controls. Your app joins in by posting a distributed notification whenever its playback changes. It works from sandboxed and Mac App Store apps, and needs no entitlement, framework or account.

1. Announce what’s playing

Post app.playstate.nowPlaying with a JSON string as the notification’s object. Use the object, not userInfo: macOS strips userInfo from distributed notifications sent by sandboxed apps.

import Foundation

func announce(title: String, artist: String, album: String?, duration: Double,
              position: Double, isPlaying: Bool) {
    var payload: [String: Any] = [
        "version": 1,
        "bundleID": Bundle.main.bundleIdentifier!,
        "appName": "My Player",
        "state": isPlaying ? "playing" : "paused",
        "title": title,
        "artist": artist,
        "duration": duration,
        "position": position,
        "commands": ["playPause", "next", "previous"],
    ]
    if let album { payload["album"] = album }
    let data = try! JSONSerialization.data(withJSONObject: payload)
    DistributedNotificationCenter.default().postNotificationName(
        .init("app.playstate.nowPlaying"),
        object: String(decoding: data, as: UTF8.self),
        userInfo: nil, deliverImmediately: true)
}

Post it when the track, play state or volume changes, after a seek, and as a heartbeat every 10 seconds while your app has something loaded. Playstate drops an app it hasn’t heard from in 30 seconds, or one that quits. When nothing is loaded any more, send "state": "stopped" to clear it right away.

Fields

FieldTypeIf missing
versionAlways 1Required
bundleIDYour app’s bundle identifierRequired. Playstate uses it for your icon, to open your app and to address commands.
stateplaying, paused or stoppedRequired
titleTextRequired unless stopped
appNameTextPlaystate shows the name macOS gives your running app, and uses this only if there’s none
artist, albumTextLeft blank
duration, positionSecondsNo progress bar
artworkURLAn https URL to an image under 1 MB and 3000 pixels a sideNo artwork
artworkBase64Image data with the same limitsPrefer artworkURL
isMusicBooleantrue when there’s an artist; never without one. Send false for podcasts, audiobooks and video.
commandsAny of playPause, play, pause, next, previous, setVolume, setMutedControls you don’t list are shown disabled. Without playPause, list both play and pause and Playstate sends whichever fits.
volume, muted0 to 1, booleanNo volume slider (it also needs setVolume)

Each field must have the JSON type shown: a message with a wrong type, an unknown version or a missing required field is ignored. Text is limited to 500 characters on one line.

2. Take commands

When someone uses a control in Playstate, it posts app.playstate.command. Act on the ones addressed to your bundle identifier, then announce your new state. Any process can post this notification too, so treat it like a media key: fine for playback, not for anything that needs authorization.

let center = DistributedNotificationCenter.default()
center.addObserver(forName: .init("app.playstate.command"), object: nil, queue: .main) { note in
    guard let json = note.object as? String,
          let command = try? JSONSerialization.jsonObject(with: Data(json.utf8)) as? [String: Any],
          command["bundleID"] as? String == Bundle.main.bundleIdentifier else { return }
    switch command["command"] as? String {
    case "playPause": player.togglePlayPause()
    case "play": player.play()
    case "pause": player.pause()
    case "next": player.next()
    case "previous": player.previous()
    case "setVolume": player.volume = command["value"] as? Double ?? player.volume  // 0 to 1
    case "setMuted": player.isMuted = command["value"] as? Bool ?? player.isMuted
    default: break  // ignore commands added in later versions
    }
}

3. Answer when Playstate starts

Playstate posts app.playstate.requestState (no object) when it launches. Announce your state in response, so people don’t wait for your next heartbeat.

How Playstate treats your app

Questions or a field you need? Email support@playstateapp.com.