Skip to main content
Rocksky uses a JSON WebSocket protocol to connect players and controllers. A player publishes playback state and executes commands. A controller lists players, shows their queues, and sends commands. Playerd implements the player role; the Rocksky Web player implements the controller role. The WebSocket carries commands and state, not audio. Your player needs its own playback engine and must fetch audio separately.

Endpoint and authentication

Connect to wss://api.rocksky.app/ws. Include a Rocksky access token in every client JSON message. You can obtain one through rocksky login or Rocksky’s access-token settings. Devices and commands are scoped to the authenticated account’s DID. Both roles register a connection. Only connections that publish track state appear as players in device snapshots. Device IDs belong to a connection; capture a new ID after reconnecting. The primary device supplies the profile’s now-playing status. Select it with set_primary. When no primary is available, the server can adopt a lone player. Other players remain controllable.
Publishing playback state does not replace submitting scrobbles. Playerd submits its own scrobbles through the Rocksky API. If you build a player, implement scrobbling separately when you want to record listens.

Connection lifecycle

  1. Open the WebSocket to wss://api.rocksky.app/ws.
  2. Register. The server replies with your deviceId, then a devices snapshot.
  3. Heartbeat. Send the literal string "ping" every ~10s; the server replies "pong". This keeps the socket alive — the server times out an idle socket after ~60s.
  4. Reconnect. If the socket closes (network blip, background throttling, idle timeout), reconnect and re-register. The server re-sends the devices snapshot so you resync immediately.
Heartbeat note: some existing clients also send {"type":"heartbeat","token":"…"}; the server ignores it (only the raw "ping" yields "pong"). Prefer "ping".

Message envelope

All non-heartbeat frames are JSON objects with a type field. Client→server frames carry token. There are four client→server types (register, command, set_primary, message) and several server→client types (below).

Client → server messages

register

Announce this connection as a device.
  • clientName — the human label shown in the miniplayer’s device picker (e.g. “Rocksky CLI”, “Living Room”).
Reply: { "status": "registered", "deviceId": "<uuid>" }, immediately followed by a devices snapshot below.
Capture your deviceId only from this reply (the frame with status: "registered"). Do not read deviceId from other frames — the device_registered broadcast below carries another device’s id, and capturing it will make your pushes look like they came from that device.

message — push now-playing / status / queue (player devices)

Wrap a state payload. data.type is one of track, status, queue.
The server enriches data (album art, URIs, like status…), then broadcasts it to all of the user’s devices as a message below.
The server routes/tags broadcasts by the connection’s registered deviceId, not the device_id in your payload — so a wrong value can’t misroute commands. Still, send your own deviceId for clarity.
track — the current track. Push on track change, and re-push every few seconds so controllers can reconcile elapsed time.
codec and sample_rate are passed through verbatim on the broadcast; controller UIs (the web/mobile miniplayers) render them as audio-format badges. shuffle, repeat and volume report the player’s own transport state so a controller can render the toggles in the right position. Omit a field you have no control for — that is how a controller tells “this player has no shuffle” from “shuffle is off”, and it hides the control rather than showing it wrong. Fields the server fills in on the broadcast (you don’t send them): album_art (canonical, from the library), song_uri, album_uri, artist_uri, liked, sha256, duration_ms. status — transport state.
status: 0 = stopped, 1 = playing, 2 = paused (3 is also treated as paused). Send 1/2 on play/pause, 0 on stop. queue — the playback queue + current index. Push on queue change / track advance.
The server enriches each item’s album_art from the library where possible.

command — control a device (controllers)

  • target (optional) — send only to that device. Omit to broadcast to all the user’s devices.
  • args (optional) — action-specific (see Commands).
The server relays { "type": "command", "action": …, "args": … } to the target(s). args is omitted when absent.

set_primary — choose the profile now-playing source (controllers)

The server records the primary, broadcasts primary_changed, and re-points the profile now-playing at that device’s current track.

Server → client messages

devices — snapshot (sent to you right after you register)

Only players that have published track state appear (controllers are excluded). primary_device is a device ID or JSON null when none is selected.

device_registered — a new device joined (to the user’s other devices)

Informational. Never capture this deviceId as your own (see Register).

device_unregistered — a device left

message — a device’s enriched state (broadcast to all the user’s devices)

This is the enriched, broadcast form of a player state push — controllers render it; a pure player can ignore it (including the echo of its own pushes).

primary_changed

The primary device changed (via set_primary, or auto-adopt). Controllers should converge their “active device” on it.

command — a relayed control command (delivered to player devices)

Player devices execute these (see Commands).

Commands (what a player device must handle)

An enqueue descriptor is a track the controller resolved for you:
Stream it via uploadId (Rocksky uploads) or trackId (Navidrome/Subsonic id), whichever is present. After executing any command, push fresh track / status / queue state so all clients update promptly.

audio_settings — DSP

One command carries the whole DSP surface as a partial document: every section, and every field inside it, is optional.
A player applies the sections its engine implements and ignores the rest. It must not error on a section it doesn’t know — that is what lets a newer controller talk to an older player, and a settings UI send one document to every device without asking what each supports. An absent section means “leave alone”, so a controller changing only the EQ sends only equalizer. Enumerated values: channels is stereo | mono | custom | monoLeft | monoRight | karaoke | swap; crossfade.mode is off | enabled | shuffle | albumChange | trackChange | auto; fadeOutMixMode is crossfade | mix; replayGain.mode is off | track | album | trackIfShuffling; crossfeed.mode is off | meier | custom. Treat an unknown value as the off/default member rather than rejecting the document.

crossfade.mode: "auto" — Auto DJ

auto asks the player to derive each transition from the audio itself rather than from fixed times: analyse the outgoing and incoming tracks, and place the fade so it ends where the music ends instead of running through the silence after it. fadeOutDuration (or fadeInDuration) carries the desired music-over-music overlap in ms; the delay fields are ignored, since the player computes them per transition. This is deliberately a mode and not a new action: a player that has never heard of Auto DJ reads an unknown enum value, falls back to off per the rule above, and keeps playing. playerd implements it; other players may not. Units are the same as the app.rocksky.rockbox.audio.settings record, so a settings UI can put its saved document straight on the wire: EQ bands is positional — index 0 is the lowest band. Band centre frequencies are the fixed rockbox band table keyed by index, so a player should trust the index over a frequency that may have been persisted from an older table.

Implementation notes

After a disconnect, reconnect with backoff, register again, and replace your device list with the new snapshot. Send the raw text ping every ten seconds and handle pong before attempting to parse a frame as JSON. Clear your heartbeat timer when the connection closes. For players, publish fresh state after commands and periodically while playing. Ignore commands and audio-setting sections your engine does not support. Only advertise optional controls that work in your player. For controllers, start from devices and update your view with message, device_unregistered, and primary_changed. A device_registered notification alone does not establish that the new connection is a player. Wait for track state. Always use an explicit target when you intend to control one device: omitting it broadcasts to all of your devices. Queue indexes and startIndex are zero-based. An enqueue descriptor needs a resolvable uploadId or Navidrome/Subsonic trackId; a title or a catalog URI alone is not an audio source. Fetch the stream through the appropriate upload or library API. Commands are relayed to players; use their subsequent state updates to reflect what actually happened in your UI.

Use the SDK

The Rocksky SDKs provide RemotePlayer and RemoteController wrappers. Use them when you do not need to implement the wire protocol yourself. The Rust SDK’s remote support uses the remote-player feature. See the TypeScript SDK or the remote service source for reference implementations. The official headless player is playerd.