HomeDocsWebSocket RPC

WebSocket RPC

Real-time bidirectional communication with Soundie via WebSocket.

WebSocket Endpoint

ws://127.0.0.1:45289

JSON-RPC 2.0 protocol. Connect once, get real-time events and send commands.

Connection

connect.js
js
1const ws = new WebSocket("ws://127.0.0.1:45289");
2
3ws.addEventListener(class="token-prop">"open", () => {
4 console.log("Connected to Soundie");
5
6 // Subscribe to specific events (optional — default: all)
7 ws.send(JSON.stringify({
8 jsonrpc: class="token-prop">"class="token-number">2.0",
9 method: class="token-prop">"subscribe",
10 params: {
11 events: [class="token-prop">"player:*", "queue:*"]
12 },
13 id: class="token-number">1,
14 }));
15});
16
17ws.addEventListener(class="token-prop">"message", (event) => {
18 const msg = JSON.parse(event.data);
19
20 // It's an event notification
21 if (msg.method) {
22 handleEvent(msg.method, msg.params);
23 }
24
25 // It's a command response
26 if (msg.id) {
27 handleResponse(msg.id, msg.result, msg.error);
28 }
29});
30
31ws.addEventListener(class="token-prop">"close", () => {
32 console.log("Disconnected — reconnecting in 2s...");
33 setTimeout(connect, class="token-number">2000);
34});

Events

Events are pushed by Soundie whenever state changes. They use JSON-RPC notification format (no id field).

player:track_changed

Fires when the current track changes.

{ track: TrackObject, source: string }
player:playback_state

Play/pause state changed.

{ is_playing: boolean }
player:position_update

Emitted every 500ms while playing.

{ position_ms: number, duration_ms: number }
player:volume_changed

Volume was changed.

{ volume: number }
player:shuffle_changed

Shuffle toggled.

{ enabled: boolean }
player:repeat_changed

Repeat mode changed.

{ mode: "off" | "track" | "queue" }
queue:updated

Track added, removed, or reordered.

{ queue: TrackObject[], index: number }
queue:cleared

Queue was cleared.

{}
library:playlist_created

New playlist was created.

{ id: string, name: string }
library:playlist_deleted

Playlist was deleted.

{ id: string }
system:theme_changed

Active theme changed.

{ theme_id: string, name: string }
handle-events.js
js
1ws.addEventListener(class="token-prop">"message", ({ data }) => {
2 const msg = JSON.parse(data);
3 if (!msg.method) return; // skip responses
4
5 switch (msg.method) {
6 case class="token-prop">"player:track_changed":
7 const { title, artist, artwork_url } = msg.params.track;
8 updateNowPlayingOverlay({ title, artist, artwork_url });
9 break;
10
11 case class="token-prop">"player:position_update":
12 const { position_ms, duration_ms } = msg.params;
13 const pct = position_ms / duration_ms * class="token-number">100;
14 progressBar.style.width = pct + "%";
15 break;
16
17 case class="token-prop">"player:playback_state":
18 playBtn.textContent = msg.params.is_playing ? class="token-prop">"⏸" : : class="token-string">"▶";
19 break;
20 }
21});

Commands

Send JSON-RPC 2.0 requests with an id to receive a response.

player.play

Resume playback.

player.pause

Pause playback.

player.toggle

Toggle play/pause.

player.next

Next track.

player.previous

Previous track or restart.

player.seek

Seek to position.

{ position_ms: number }
player.set_volume

Set volume.

{ volume: number }
player.set_shuffle

Set shuffle.

{ enabled: boolean }
player.set_repeat

Set repeat mode.

{ mode: "off" | "track" | "queue" }
queue.add

Add URL to queue.

{ url: string, play_immediately?: boolean }
queue.clear

Clear queue.

queue.play_index

Play track at index.

{ index: number }
system.get_state

Get full player state.

send-commands.js
js
1// Helper
2let cmdId = class="token-number">0;
3function cmd(method, params = {}) {
4 return new Promise((resolve, reject) => {
5 const id = ++cmdId;
6 const pending = new Map();
7
8 ws.send(JSON.stringify({ jsonrpc: class="token-prop">"class="token-number">2.0", id, method, params }));
9
10 pending.set(id, { resolve, reject });
11
12 ws.addEventListener(class="token-prop">"message", ({ data }) => {
13 const msg = JSON.parse(data);
14 if (msg.id && pending.has(msg.id)) {
15 const { resolve, reject } = pending.get(msg.id);
16 pending.delete(msg.id);
17 msg.error ? reject(msg.error) : resolve(msg.result);
18 }
19 }, { once: class="token-keyword">false });
20 });
21}
22
23// Usage
24await cmd(class="token-prop">"player.set_volume", { volume: class="token-number">0.5 });
25await cmd(class="token-prop">"queue.add", { url: "https://youtu.be/..." });
26const state = await cmd("system.get_state");
27console.log(class="token-prop">"Now playing:", state.track?.title);

Stream Deck / OBS Example

stream-integration.js
js
1// Stream Deck plugin example
2class SoundiePlugin {
3 constructor() {
4 this.ws = class="token-keyword">null;
5 this.currentTrack = class="token-keyword">null;
6 this.connect();
7 }
8
9 connect() {
10 this.ws = new WebSocket("ws://127.0.0.1:45289");
11 this.ws.onmessage = (e) => this.onMessage(JSON.parse(e.data));
12 this.ws.onclose = () => setTimeout(() => this.connect(), class="token-number">3000);
13 }
14
15 onMessage({ method, params }) {
16 if (method === "player:track_changed") {
17 this.currentTrack = params.track;
18 this.updateStreamDeckDisplay();
19 }
20 }
21
22 updateStreamDeckDisplay() {
23 const { title, artist } = this.currentTrack || {};
24 streamDeck.setTitle(`${title}\n${artist}`);
25 }
26
27 async togglePlayPause() {
28 this.ws.send(JSON.stringify({
29 jsonrpc: class="token-prop">"class="token-number">2.0", id: class="token-number">1,
30 method: : class="token-string">"player.toggle"
31 }));
32 }
33}
34
35// OBS WebSocket source overlay
36window.__soundie__ = new SoundiePlugin();