Surfaces ↓
Player
client.player Daemon -> webapp player surface. Snapshot lands on connect with the
current player state; Delta is the NowPlayingUpdate stream the
SDK auto-merges; QueueChanged fires when the queue mutates without
a track change. StateReply/QueueReply are the typed responses
to the matching webapp queries.
Requests
you ask, the daemon answers; await the tagged result and check .ok
stateGet(): Promise<TypedRequestResult<PlayerStateReply, never>>
push form: onStateReply (subscribe instead of awaiting)
Webapp asks for the current PlayerState snapshot. Most webapps
don't need this - the SDK auto-merges deltas into a cached state.
const res = await client.player.stateGet();
if (res.ok) {
console.log(res.response.state);
} type Result =
| { ok: true; response: PlayerStateReply }
| { ok: false; kind: 'domain'; error: never }
| { ok: false; kind: 'protocol'; error: WireError }; queueGet(): Promise<TypedRequestResult<PlayerQueueReply, never>>
push form: onQueueReply (subscribe instead of awaiting)
Webapp asks for the current queue snapshot. Rarely needed - the SDK
tracks the queue from QueueChanged events.
const res = await client.player.queueGet();
if (res.ok) {
console.log(res.response.current);
} type Result =
| { ok: true; response: PlayerQueueReply }
| { ok: false; kind: 'domain'; error: never }
| { ok: false; kind: 'protocol'; error: WireError }; Commands
fire-and-forget; the promise resolves once the daemon has taken the message
play(PlayUri): Promise<void>
Start playback of a uri, optionally within a context.
await client.player.play({ uri: 'spotify:track:...' }); queue(QueueUri): Promise<void>
Add a uri to the queue.
await client.player.queue({ uri: 'spotify:track:...', position: { /* QueuePosition */ } }); skipPrev(SkipPrev): Promise<void>
Skip to the previous track, or restart the current one.
await client.player.skipPrev({ allowSeeking: true }); skipToIndex(SkipToIndex): Promise<void>
Jump to a specific queue index.
await client.player.skipToIndex({ index: 0 }); seekTo(SeekTo): Promise<void>
Seek to an absolute position.
await client.player.seekTo({ positionMs: 0 }); setRepeat(SetRepeat): Promise<void>
Set the repeat mode.
await client.player.setRepeat({ mode: 'off' }); setSpeed(SetSpeed): Promise<void>
Change playback speed.
await client.player.setSpeed({ speed: 0 }); setCrossfade(SetCrossfade): Promise<void>
Set the crossfade duration.
await client.player.setCrossfade({ durationMs: 0 }); Events
the daemon pushes these unprompted; subscribing returns an unsubscribe function
onSnapshot(handler: (PlayerStateReply) => void): () => void
const off = client.player.onSnapshot((reply) => {
console.log(reply.state);
});
// call off() to unsubscribe onDelta(handler: (NowPlayingUpdate) => void): () => void
const off = client.player.onDelta((update) => {
console.log(update.mediaItem);
});
// call off() to unsubscribe onQueueChanged(handler: (PlayerQueueReply) => void): () => void
const off = client.player.onQueueChanged((reply) => {
console.log(reply.current);
});
// call off() to unsubscribe Types
shapes referenced above, as the SDK types them
Response to stateGet, also carried by the Snapshot event.
type PlayerStateReply = {
state: PlayerState;
activeApp?: CurrentlyActiveApplication; // The app currently driving playback, when known (iOS surfaces this over iAP2).
}; Delta event the companion or iAP2 stream emits whenever a player attribute changes. Every field is optional: producers send partial updates populating only what changed, and fields left unset keep their prior value.
type NowPlayingUpdate = {
mediaItem?: MediaItemUpdate;
playback?: PlaybackUpdate;
}; Response to queueGet, also carried by the QueueChanged event.
type PlayerQueueReply = {
current?: QueueItem; // The now-playing track, when one is loaded.
items: QueueItem[]; // Upcoming tracks in queue order.
previous: QueueItem[]; // Recently-played history.
}; Play a URI on the gateway. context lets the gateway honor playlist
/ album semantics for skip-next when both sides understand the
scheme. The daemon parses the scheme and only forwards if a
connected gateway claims it; otherwise returns PlayerError::SchemeUnclaimed.
type PlayUri = {
uri: string;
context?: PlayContext;
}; type QueueUri = {
uri: string;
position: QueuePosition;
}; Payload for skipPrev: go to the previous track, or restart the current one.
type SkipPrev = {
allowSeeking: boolean; // When true, restart the current track if it is progressed past the restart threshold; otherwise always move to the previous track.
}; type SkipToIndex = {
index: number;
}; type SeekTo = {
positionMs: number;
}; type SetShuffle = {
on: boolean;
}; type SetRepeat = {
mode: RepeatMode;
}; Set absolute playback rate. 1.0 is normal speed; gateways with
limited speed support clamp to their nearest supported value.
type SetSpeed = {
speed: number;
}; duration_ms = None turns crossfade off; Some(0) is also off but
distinguishes intent.
type SetCrossfade = {
durationMs?: number;
}; Full player snapshot the daemon broadcasts to webapps. Initial value
arrives on BridgeToClientPlayerMsg::StateChange at connect time;
subsequent changes flow as NowPlayingUpdate deltas the client SDK
merges into the cached snapshot.
type PlayerState = {
track?: MediaItem;
playback: Playback;
queue: QueueItem[];
options: PlayerOptions;
context?: PlaybackContext;
}; The foreground app driving playback, surfaced on iOS over iAP2.
type CurrentlyActiveApplication = {
id: string; // Bundle identifier, e.g. `com.spotify.client`.
name: string;
}; Per-track attributes that vary per song. persistent_id is a stable
per-platform identifier (iAP2 sends u64; we hex-encode it on the
wire). artwork_id is an opaque asset id - webapps pass this value
to ClientToBridgeAssetMsg::Get to retrieve the bytes. The id namespace
is producer-defined: iAP2 emits iap2/art/<persistent_hex>/<n>, the
companion picks whatever shape it wants (e.g. spotify/track/<id>/image).
Webapps treat the value as opaque.
type MediaItemUpdate = {
persistentId?: string;
title?: string;
album?: string;
albumUri?: string;
albumArtist?: string;
artist?: string;
artistUri?: string;
liked?: boolean;
artworkId?: string;
durationMs?: number;
mediaTypes?: MediaType[];
trackNumber?: number;
trackCount?: number;
isLikeSupported?: boolean;
isBanSupported?: boolean;
isBanned?: boolean;
isResidentOnDevice?: boolean;
chapterCount?: number;
}; Per-playback-session attributes that vary regardless of track:
playing/paused, position, shuffle/repeat, and the iOS bundle
identifier of the app currently driving playback (e.g.
"com.spotify.client"). app_bundle is null on the Android path
since it isn't a meaningful surface there.
set_elapsed_time_available is the gate webapps must honor for
scrub UI: when false, scrubbing is unsupported by the foreground
app and the seek button must be disabled.
type PlaybackUpdate = {
playing?: boolean;
positionMs?: number;
shuffle?: boolean;
shuffleMode?: ShuffleMode;
repeat?: RepeatMode;
appBundle?: string;
appDisplayName?: string;
queueIndex?: number;
queueCount?: number;
queueChapterIndex?: number;
playbackSpeed?: number;
setElapsedTimeAvailable?: boolean;
queueListAvail?: boolean;
appleMusicRadioAd?: boolean;
appleMusicRadioStationName?: string;
}; One row in the player queue. Lean cross-platform shape - gateways
that have richer per-track data still surface what fields they have.
uri is required because every queued item must be addressable for
skipToIndex. persistent_id is the platform-stable id when
available; webapps treat it as opaque.
type QueueItem = {
uri: string;
title?: string;
artist?: string;
artistUri?: string;
album?: string;
albumUri?: string;
artworkId?: string;
durationMs?: number;
persistentId?: string;
queued: boolean;
}; Optional context for play({ uri }). context_uri is the album /
playlist / show URI the track is being played from; gateways with
playlist support honor it for skip-next semantics. position is the
0-based index inside the context.
type PlayContext = {
contextUri: string;
position?: number;
}; Where in the queue a queue({ uri }) should land. Append (default)
goes at the end; Next is play-next; Index is an explicit position.
type QueuePosition =
| { type: 'append' }
| { type: 'next' }
| { type: 'index'; data: u32 }; repeat is a typed enum (Off/All/One) shared across the player
surface and the iAP2 NowPlaying CSM / MediaSession backends, which
all expose three repeat states.
type RepeatMode = 'off' | 'all' | 'one'; Currently-playing track, populated to the extent the gateway/iAP2 stream has surfaced. All fields are optional because each one arrives as a separate attribute fetch on iAP2; the daemon accumulates and the snapshot reflects whatever's known so far.
type MediaItem = {
uri?: string;
persistentId?: string;
title?: string;
album?: string;
albumUri?: string;
albumArtist?: string;
artist?: string;
artistUri?: string;
liked?: boolean;
artworkId?: string;
durationMs?: number;
mediaTypes?: MediaType[];
trackNumber?: number;
trackCount?: number;
isLikeSupported?: boolean;
isBanSupported?: boolean;
isBanned?: boolean;
chapterCount?: number;
}; Per-session playback snapshot: where in the song we are, what mode is
engaged. position_ms is the live playhead at snapshot time; webapps
extrapolate forward locally while state == Playing.
set_elapsed_time_available gates scrub UI: when false, the foreground
app refuses absolute-position seeks and webapps must disable the scrub
thumb. None means unknown (no signal received yet); webapps treat
unknown as "available" for backward compatibility with older gateways.
type Playback = {
state: PlaybackState;
positionMs: number;
positionAgeMs?: number;
shuffle: boolean;
shuffleMode?: ShuffleMode;
repeat: RepeatMode;
queueIndex?: number;
queueCount?: number;
queueChapterIndex?: number;
setElapsedTimeAvailable?: boolean;
queueListAvail?: boolean;
appleMusicRadioAd?: boolean;
}; User-tunable knobs that are not "currently playing" state.
crossfade_ms = None is "crossfade off"; Some(0) is also off but
distinguishes "user explicitly set zero" from "feature unsupported by
gateway".
type PlayerOptions = {
speed: number;
crossfadeMs?: number;
}; What the current track is playing from: the playlist / album / show /
artist context. Webapps render "playing from uri lets them
drill into it. name is None until the companion resolves it.
type PlaybackContext = {
uri: string;
name?: string;
}; The kind of media currently playing. Multi-typed: an item can be
e.g. both Podcast and AudioBook (rare). Drives webapp UI choices
like skip-15s-vs-skip-track and chapter UI.
type MediaType = 'music' | 'podcast' | 'audioBook'; Three-state shuffle. iAP2 and Apple Music distinguish track-level
from album-level shuffle; companion gateways without that distinction
project to Songs when on. Webapps that just need an on/off signal
read shuffle_on (None when the underlying mode is unknown).
type ShuffleMode = 'off' | 'songs' | 'albums'; Three-state playback. Stopped is the no-track-loaded resting state;
Paused is "track is loaded, position is held"; Playing is the
progressing state.
type PlaybackState = 'stopped' | 'paused' | 'playing';