Skip to content
Documentation
Download
English

State and events

Sound Show sends the current show state after authentication and whenever it changes. Build your controller from these snapshots and use the command reference to request changes.

A state message replaces your entire local state. You receive one automatically after hello_ok, after broad or project changes, and in response to request_state. This reduced example shows the main fields; real projects contain populated arrays and additional metadata.

{
"type": "state",
"protocol_version": 2,
"server_time_ms": 1712345678901,
"state": {
"project": { "name": "My show", "categories": [] },
"players": [],
"playable_runtime": [],
"playlists": { "current_index": 0, "playlists": [] },
"instructions": { "current_index": 0, "players": [] },
"timers": [],
"logical_devices": [],
"control": {
"next_sound_volume": 70,
"selected_category_id": null,
"low_volume_active": false,
"volume_level_presets": [],
"active_volume_level_ids": [],
"effective_volume_level_id": null
},
"capabilities": { "audio_controls": [], "manual_grid_layout": false },
"audio_controls": { "revision": 1, "logical_devices": [], "players": [] },
"lights": { "revision": 1, "lights": [] }
}
}

Handle a missing or null project, empty arrays, optional fields, and unknown additive fields. These docs describe the current v2 implementation. An older Sound Show 2 build may expose fewer fields, commands, or features; check hello_ok.features and the state capabilities before using optional features.

After the full state, most runtime updates replace just one domain (a named part of the state). An empty array is meaningful: for example, players: [] clears your current players. Do not merge array entries into the old list.

Message type Replace this field in local state Delivery
control_snapshot control Direct replies to volume-level commands; all operators’ control updates with runtime_snapshots.
players_snapshot players Player changes.
instructions_snapshot instructions With runtime_snapshots.
playlists_snapshot playlists With runtime_snapshots.
playable_runtime_snapshot playable_runtime With runtime_snapshots.
logical_devices_snapshot logical_devices Output changes.
timers_snapshot timers Timer changes.
audio_controls_snapshot audio_controls Effect changes; compare revision.
lights_snapshot lights Light changes; compare revision.

Each domain message has the correspondingly named payload field and server_time_ms. For example:

{
"type": "players_snapshot",
"players": [],
"server_time_ms": 1712345678901
}

Audio-control and light messages also carry a domain revision:

{
"type": "audio_controls_snapshot",
"revision": 5,
"audio_controls": { "revision": 5, "logical_devices": [], "players": [] },
"server_time_ms": 1712345678901
}

For these two domains, ignore a message whose revision is lower than the revision already applied to that domain. Do not compare revisions between domains. A full state resets the baseline; clear all local state and revision tracking on disconnect. Apply other domains in received wire order.

Request runtime_snapshots in hello.features. Without it, control, instruction, and playlist changes use coalesced full states instead, apart from direct control replies. Playable-activity-only changes do not trigger full states for clients without this feature.

Runtime changes are coalesced on a 50 ms cycle. This is not a promise of a message every 50 ms, or an event log containing every intermediate change. A full state can replace pending domain updates. See the JavaScript example for a reducer that handles all current snapshot types.

You want to… Use…
Launch a project button project.categories[].playables[].id as playable_id.
Select a category project.categories[].id as category_id.
Stop, pause, seek, or adjust one running instance players[].id as player_id.
Control an output logical_devices[].id as logical_device_id.
Activate a volume preset control.volume_level_presets[].id as level_id.
Adjust an effect A descriptor’s id as control_id, plus a current player or logical-device target.
Set a light level lights.lights[].id as project_light_id.
Play a playlist item Current zero-based playlist and queue positions.
Select an instruction Current zero-based instruction player position and the item’s path.
Control a timer Its current zero-based position as timer_index.

Player IDs describe running instances and must not be saved as permanent project identity. A playable can have multiple active players. To control its current instances, find matching source_playable_id or state_playable_id in the current players snapshot and choose the intended instance explicitly. Names are useful for display but need not be unique.

project.categories[] contains category id, name, color, is_hidden, optional icon_url, and playables. A playable includes:

{
"id": "playable-id",
"name": "Wind",
"kind": "sound",
"color": "#3b82f6",
"tags": [],
"is_searchable": true,
"runtime_state_key": "playable-id",
"target_category_id": null
}

Common kinds include sound, video, image, random, timer, playlist, action, webbrowser, category_link, and instruction. Tolerate new or unsupported kinds. Optional metadata includes icon_url, keyboard shortcuts, and configured click-action IDs.

playable_runtime contains entries with state_playable_id, play_count, is_locked, and is_playing. Match a playable’s runtime_state_key (or its id if that field is absent) to state_playable_id. Linked playables can share the same activity and lock state. This project-button lock is different from a running player’s control lock.

Categories can also include optional grid_layout with enabled, columns, rows, pages, positions, and revision. Check capabilities.manual_grid_layout before following it. Clients can ignore this metadata and render the ordinary playable list; there is no remote layout-edit command.

{
"id": "runtime-player-id",
"kind": "sound",
"title": "Wind",
"color": "#3b82f6",
"panel_position": 0,
"position_seconds": 12.3,
"duration_seconds": 95,
"start_time_seconds": 5,
"stop_time_seconds": 90,
"timeline_duration_seconds": 95,
"volume_percent": 70,
"muted": false,
"is_playing": true,
"is_locked": false,
"loop_enabled": true,
"output_logical_device_id": "main",
"source_playable_id": "playable-id",
"state_playable_id": "playable-id"
}

Use volume_percent directly for display. The separate raw volume field, when present, is not a percentage to multiply by 100; the app can use a nonlinear volume scale.

Timeline fields are in seconds. start_time_seconds, stop_time_seconds, and timeline_duration_seconds can be absent or null. Use them to map a trimmed playback window to the source timeline when seeking. Other optional fields include remaining_playback_seconds, markers, and icon_url.

logical_devices[] provides id, label, display_label, color, output_role, available, selected, and volume_percent. Use display_label for presentation and id for commands.

control exposes the next launch volume and selected category, plus volume_level_presets, active_volume_level_ids, and effective_volume_level_id. Presets include id, name, direction (both, up_only, or down_only), target_volume_percent, fade_down_ms, and fade_up_ms. low_volume_active supports older controllers. Read active IDs from the snapshot rather than assuming your controller is the only operator changing them.

Domain Useful fields
playlists current_index and playlists[]. Each playlist has id, title, autoplay, loop_enabled, and queue. Queue items include playable_id, title, and optional icon_url. Runtime metadata can include active_item_id, active_item_title, and active_player_id.
instructions current_index and players[]. Players have id, title, has_instruction, waiting_for_go, next_instruction_text, and items. Items expose item_id, title, kind, depth, path, can_set_next, is_next_to_play, and is_visibly_playing.
timers Array with numeric id, mode, is_running, is_locked, elapsed_ms, duration_ms, remaining_ms, and is_over_time. Timer times use milliseconds.

For a GO preview, prefer the deepest next item with a nonempty path and a kind other than group, falling back to any deepest next item. For selecting a next item, use its actual path, not a path inferred from its title or depth.

capabilities.audio_controls[] describes the available effect controls. Each descriptor supplies:

Field Meaning
id, label Wire identifier and display label.
scopes Supported target kinds: logical_device and/or player.
min, max, default_value Native numeric bounds and reset value.
suggested_step Suggested adjustment step.
unit, scale Display unit and control scale. log2 expresses steps in octaves.
display_multiplier Multiply native values for display; divide user input by this before sending.

For example, a ratio of 0.5 with display_multiplier: 100 displays as 50%. Use the advertised catalog rather than hard-coding ranges.

audio_controls contains revision, logical_devices[], and players[]. Each target has kind, id, label, available, locked, and a values object mapping control IDs to native numeric values. Linear controls fade linearly; log2 controls fade geometrically.

lights contains revision and lights[]. Each light has id, name, capability (currently level), and available. Its value_percent is omitted when the backing adapter is unavailable. Show that as unavailable, rather than treating it as zero.

Snapshot entries may include an opaque, relative icon_url, for example /asset/file?id=opaque-asset-id. Resolve it against the remote server’s HTTP address and fetch it over HTTP. Icons are not WebSocket messages.

Use the provided URL without constructing IDs or sending file paths. Cache icons for the current server session, and expect new IDs after a restart. Assets can become unavailable after project changes; handle a failed image request with a fallback. Local filesystem paths are not exposed in snapshots.

An authenticated connection can receive:

{
"type": "error",
"code": "invalid_value",
"command_id": "eq-change-1",
"message": "Human-readable error description"
}

code and command_id are optional. Audio/light command errors can include unknown_control, target_locked, target_not_found, invalid_value, target_unavailable, or command_failed. Older command paths may return only message. Do not parse the human-readable text as a stable error code. An error may leave the connection open; keep consuming snapshots.

Limit Client behavior
First message within 10 seconds Send hello immediately on open.
16 simultaneous WebSocket connections Reuse connections and close unused clients. The limit includes unauthenticated sockets.
64 KiB per incoming message/frame Send one small JSON command per message.
Burst of 60 commands, replenished at 20 per second per connection Throttle sliders and coalesce pending values. Ping and refresh requests also use this budget.
Five bad passwords from one source address within 60 seconds Correct the password and wait; new upgrades are blocked for 60 seconds and return HTTP 429 with Retry-After.

A command rejected by the rate limiter is not applied and returns code: "rate_limited". Sustained excess commands can close the connection. Follow Retry-After when your client exposes the HTTP response. If a browser only reports a failed connection after repeated authentication failures, wait at least 60 seconds before trying again. Do not blindly retry playback, toggle, or GO commands: a retry can trigger a second action.

When hello_ok.features includes ping, an authenticated client can send {"type":"ping"} and expect {"type":"pong"}. The built-in remote sends a ping every ten seconds and, on that check, reconnects if no valid message has arrived for more than 25 seconds. Unchanged domains can otherwise be silent.

For a persistent controller:

  1. Disable command sending as soon as the socket closes or fails a heartbeat.
  2. Clear cached state, player IDs, and revision tracking.
  3. Reconnect with a capped delay that increases on repeated failures. Respect authentication errors and rate limits rather than retrying continuously.
  4. Send a new hello, wait for the new full state, and resolve targets again.
  5. Re-enable controls using that state. Do not replay queued launch, toggle, or GO commands after reconnection.

Connection-scoped volume holds are released on disconnect. Treat reconnecting as a new session, even if the server address has not changed.

Changing Allow external origins in Sound Show takes effect immediately and disconnects current clients. After the option is disabled, browsers on other origins cannot reconnect. See custom browser remotes for configuration and browser restrictions.