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
| Field | Type | If missing |
|---|---|---|
version | Always 1 | Required |
bundleID | Your app’s bundle identifier | Required. Playstate uses it for your icon, to open your app and to address commands. |
state | playing, paused or stopped | Required |
title | Text | Required unless stopped |
appName | Text | Playstate shows the name macOS gives your running app, and uses this only if there’s none |
artist, album | Text | Left blank |
duration, position | Seconds | No progress bar |
artworkURL | An https URL to an image under 1 MB and 3000 pixels a side | No artwork |
artworkBase64 | Image data with the same limits | Prefer artworkURL |
isMusic | Boolean | true when there’s an artist; never without one. Send false for podcasts, audiobooks and video. |
commands | Any of playPause, play, pause, next, previous, setVolume, setMuted | Controls you don’t list are shown disabled. Without playPause, list both play and pause and Playstate sends whichever fits. |
volume, muted | 0 to 1, boolean | No 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
- People can turn off all apps that share their playback with one checkbox in Settings, under Sources.
- Music.app and the players Playstate supports directly take priority when several play at once. Your app takes its turn with speakers: the last one to start playing is shown.
- Any app on a Mac can post these notifications, so Playstate only accepts a bundle identifier that is running, and shows that app’s own name. Keep anything you don’t want shown out of the payload.
Questions or a field you need? Email support@playstateapp.com.