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.
Full state
Section titled “Full state”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.
Live domain snapshots
Section titled “Live domain snapshots”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.
Choosing the right target
Section titled “Choosing the right target”| 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 and playables
Section titled “Project and playables”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.
Players
Section titled “Players”{ "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.
Outputs and shared controls
Section titled “Outputs and shared controls”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.
Playlists, instructions, and timers
Section titled “Playlists, instructions, and timers”| 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.
Audio controls and capabilities
Section titled “Audio controls and capabilities”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
Section titled “Lights”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.
Errors and limits
Section titled “Errors and limits”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.
Heartbeat and reconnection
Section titled “Heartbeat and reconnection”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:
- Disable command sending as soon as the socket closes or fails a heartbeat.
- Clear cached state, player IDs, and revision tracking.
- Reconnect with a capped delay that increases on repeated failures. Respect authentication errors and rate limits rather than retrying continuously.
- Send a new
hello, wait for the new fullstate, and resolve targets again. - 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.