bridgething
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);
}

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

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 */ } });

pause(): Promise<void>

Pause playback.

await client.player.pause();

resume(): Promise<void>

Resume playback.

await client.player.resume();

skipNext(): Promise<void>

Skip to the next track.

await client.player.skipNext();

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

setShuffle(SetShuffle): Promise<void>

Toggle shuffle.

await client.player.setShuffle({ on: true });

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';