Skip to main content

EZPlayer API Documentation

Koa Server REST APIs

note

The REST API is subject to change. Currently used internally, could be used externally if you don't mind churn. The goal is to finalize the shape and provide backward compatibility after the 1.0 release.

GET /api/hello

Health check endpoint. Simple endpoint to verify the server is running.

Request:

  • Method: GET
  • Headers: None required

Response:

  • Status: 200 OK
  • Body:
{
"message": "Hello from Koa + Electron!"
}

GET /api/current-show

Get current show data. Returns the complete current show state, including sequences, playlists, schedules, user info, and status.

Request:

  • Method: GET
  • Headers: None required

Response:

  • Status: 200 OK
  • Body: FullPlayerState object
{
"showFolder": "/path/to/show",
"sequences": [...],
"playlists": [...],
"schedule": [...],
"user": {...},
"show": {...},
"pStatus": {...},
"cStatus": {...},
"nStatus": {...}
}

GET /api/getimage/:sequenceId

Get sequence thumbnail image. Serves thumbnail images for sequences by sequence ID. Supports multiple image formats (PNG, JPG, JPEG, GIF, WEBP, SVG, ICO, BMP).

Request:

  • Method: GET
  • Path Parameters:
    • sequenceId (string, required) - Sequence identifier (alphanumeric, hyphens, underscores only)

Response:

  • Status: 200 OK - Image file with appropriate MIME type
  • Status: 400 Bad Request - Invalid or missing sequence ID
  • Status: 404 Not Found - Image not found for sequence ID

Example:

GET /api/getimage/seq-123-abc

Error Response (400):

{
"error": "Invalid sequence ID"
}

Error Response (404):

{
"error": "Image not found for sequence ID"
}

POST /api/player-command

Send player command. Sends a command to control player playback, volume, or request playback of songs/playlists.

Request:

  • Method: POST
  • Headers:
    • Content-Type: application/json
  • Body: EZPlayerCommand object

Available Commands:

CommandDescriptionAdditional Fields
stopnowStop all playing immediately
stopgracefulStop all playing at a convenient spot
pausePause all playback
resumeResume playback
reloadcontrollersReset playback from current show folder, reloading network and reopening controllers
resetplaybackReread and reset playback from current schedule items
resetstatsReset cumulative stats counters
suppressoutputContinue playback but suppress audio/video output
activateoutputRe-enable audio/video output
playsongPlay or enqueue a songsongId, immediate, priority, requestId
playplaylistPlay or enqueue a playlistplaylistId, immediate, priority, requestId
deleterequestCancel a pending song or playlist requestrequestId
clearrequestsClear all pending requests
setvolumeSet volume level and/or mutevolume?, mute?

Request Body Examples:

Stop playback immediately:

{
"command": "stopnow"
}

Play a song immediately:

{
"command": "playsong",
"songId": "seq-123",
"immediate": true,
"priority": 1,
"requestId": "req-456"
}

Play a playlist:

{
"command": "playplaylist",
"playlistId": "playlist-1",
"immediate": true,
"priority": 1,
"requestId": "req-789"
}

Set volume:

{
"command": "setvolume",
"volume": 75,
"mute": false
}

Response:

  • Status: 200 OK - Command sent successfully
  • Status: 400 Bad Request - Invalid command format
  • Status: 503 Service Unavailable - Playback worker not available
  • Status: 500 Internal Server Error - Server error

Success Response:

{
"success": true,
"message": "Command sent"
}

Error Response (400):

{
"error": "Invalid command format"
}

Error Response (503):

{
"error": "Playback worker not available"
}

POST /api/playlists

Update playlists. Accepts an array of playlist records. Updates updatedAt timestamp automatically.

Request:

  • Method: POST
  • Headers:
    • Content-Type: application/json
  • Body: Array of PlaylistRecord objects

Request Body Example:

[
{
"id": "playlist-1",
"title": "Christmas Songs",
"tags": ["holiday", "christmas"],
"items": [
{
"id": "seq-1",
"sequence": 0
},
{
"id": "seq-2",
"sequence": 1
}
],
"createdAt": 1609459200000
}
]

Response:

  • Status: 200 OK - Playlists updated successfully
  • Status: 400 Bad Request - Invalid format (expected array)
  • Status: 500 Internal Server Error - Server error

Success Response:

{
"success": true,
"playlists": [
{
"id": "playlist-1",
"title": "Christmas Songs",
"tags": ["holiday", "christmas"],
"items": [...],
"createdAt": 1609459200000,
"updatedAt": 1704067200000
}
]
}

Note: The response includes only non-deleted playlists (deleted !== true).


POST /api/schedules

Update schedules. Accepts an array of scheduled playlist records. Updates updatedAt timestamp automatically.

Request:

  • Method: POST
  • Headers:
    • Content-Type: application/json
  • Body: Array of ScheduledPlaylist objects

Request Body Example:

[
{
"id": "schedule-1",
"playlistId": "playlist-1",
"title": "Evening Show",
"date": 1704067200000,
"fromTime": "18:00",
"toTime": "22:00",
"playlistTitle": "Christmas Songs",
"duration": 14400,
"priority": "normal"
}
]

Response:

  • Status: 200 OK - Schedules updated successfully
  • Status: 400 Bad Request - Invalid format (expected array)
  • Status: 500 Internal Server Error - Server error

Success Response:

{
"success": true,
"schedules": [
{
"id": "schedule-1",
"playlistId": "playlist-1",
"title": "Evening Show",
"date": 1704067200000,
"fromTime": "18:00",
"toTime": "22:00",
"playlistTitle": "Christmas Songs",
"duration": 14400,
"priority": "normal",
"updatedAt": 1704067200000
}
]
}

Note: The response includes only non-deleted schedules (deleted !== true).


POST /api/playback-settings

Update playback settings. Updates playback configuration settings including audio sync, background sequence mode, viewer control, and volume control.

Request:

  • Method: POST
  • Headers:
    • Content-Type: application/json
  • Body: PlaybackSettings object

Request Body Example:

{
"audioSyncAdjust": 50,
"backgroundSequence": "overlay",
"viewerControl": {
"enabled": true,
"type": "remote-falcon",
"remoteFalconToken": "token-123",
"schedule": [
{
"id": "vc-1",
"days": "all",
"startTime": "18:00",
"endTime": "22:00",
"playlist": "viewer-playlist"
}
]
},
"volumeControl": {
"defaultVolume": 75,
"schedule": [
{
"id": "vol-1",
"days": "weekday-mon-fri",
"startTime": "08:00",
"endTime": "18:00",
"volumeLevel": 50
}
]
}
}

Response:

  • Status: 200 OK - Settings updated successfully
  • Status: 400 Bad Request - Invalid format (expected object)
  • Status: 500 Internal Server Error - Server error

Success Response:

{
"success": true
}

GET /api/model-coordinates

Get model coordinates for 3D preview. Returns coordinate data used to render the 3D light layout preview.

Request:

  • Method: GET
  • Headers: None required

Response:

  • Status: 200 OK - Model coordinates object
  • Status: 500 Internal Server Error - Failed to get model coordinates

Error Response (500):

{
"error": "Failed to get model coordinates"
}

GET /api/model-coordinates-2d

Get model coordinates for 2D preview. Returns coordinate data used to render the 2D light layout preview.

Request:

  • Method: GET
  • Headers: None required

Response:

  • Status: 200 OK - 2D model coordinates object
  • Status: 500 Internal Server Error - Failed to get 2D model coordinates

Error Response (500):

{
"error": "Failed to get 2D model coordinates"
}

GET /api/frames

Get binary frame data for the live 3D viewer. Returns the latest frame of light channel data as a binary application/octet-stream response.

Request:

  • Method: GET
  • Headers: None required

Response:

  • Status: 200 OK - Binary frame data (application/octet-stream)
  • Status: 204 No Content - No frame data available yet

Response Binary Format:

OffsetSizeTypeDescription
04 bytesuint32 LEFrame size in bytes
44 bytesuint32 LESequence number
8N bytesrawFrame data

Response Headers:

  • Cache-Control: no-store
  • Content-Type: application/octet-stream
  • Access-Control-Allow-Origin: *

GET /api/frames-zstd

Get ZSTD-compressed binary frame data for the live 3D viewer. Same semantics as /api/frames but the frame payload is compressed with ZSTD at level 1 (fastest). Useful for remote/embedded clients on bandwidth-constrained links.

Request:

  • Method: GET
  • Headers: None required

Response:

  • Status: 200 OK - Compressed binary frame data (application/octet-stream)
  • Status: 204 No Content - No frame data available yet
  • Status: 503 Service Unavailable - ZSTD codec not yet initialized

Response Binary Format:

OffsetSizeTypeDescription
04 bytesuint32 LEUncompressed frame size in bytes
44 bytesuint32 LESequence number
8N bytesrawZSTD-compressed frame data (level 1)

The 8-byte header is not compressed. Decompress the payload starting at offset 8 to recover the original frame bytes.

Response Headers:

  • Cache-Control: no-store
  • Content-Type: application/octet-stream
  • Access-Control-Allow-Origin: *

GET /api/time

Server clock for client clock-offset estimation. Returns the server's current Date.now() value. Clients can measure the round-trip time of this request and compute the offset between their local clock and the server's clock, enabling accurate audio scheduling on remote devices.

Request:

  • Method: GET
  • Headers: None required

Response:

  • Status: 200 OK
  • Body:
{
"now": 1704067200000
}

Response Headers:

  • Cache-Control: no-store
  • Access-Control-Allow-Origin: *

Clock Offset Estimation:

The recommended approach is to take several samples and trust the one with the lowest round-trip time (it had the least scheduling noise, making the "halfway" assumption most accurate). Discard any sample with RTT > 100ms.

const t0 = Date.now();
const res = await fetch('/api/time');
const t1 = Date.now();
const { now: serverNow } = await res.json();
const rtt = t1 - t0;
const clockOffset = serverNow - (t0 + rtt / 2);
// To convert server timestamps to local time: localTime = serverTime - clockOffset

GET /api/audio

Get binary audio chunk data for web client audio streaming. Returns all audio chunks published since a given sequence number. Used by the web UI to stream audio from the player in sync with the live pixel data.

Request:

  • Method: GET
  • Query Parameters:
    • afterSeq (number, optional) - Return chunks after this sequence number. Defaults to 0. Use the latestSeq from the previous response.

Response:

  • Status: 200 OK - Binary audio data (application/octet-stream)
  • Status: 204 No Content - No audio buffer available or no new chunks since afterSeq

Response Binary Format:

The response is a binary application/octet-stream with the following layout:

Header (8 bytes):

OffsetSizeTypeDescription
04 bytesuint32 LEChunk count
44 bytesuint32 LELatest sequence number (use as next afterSeq)

Per-chunk (repeated chunkCount times):

OffsetSizeTypeDescription
08 bytesfloat64 LEplayAtRealTime - server wall-clock time (ms) when chunk should play
84 bytesuint32 LEincarnation - increments on song/segment boundaries
124 bytesuint32 LEsampleRate - e.g. 48000
164 bytesuint32 LEchannels - number of audio channels
204 bytesuint32 LEsampleCount - total number of Float32 samples (all channels interleaved)
24sampleCount × 4Float32 LEInterleaved audio sample data

Response Headers:

  • Cache-Control: no-store
  • Content-Type: application/octet-stream
  • Access-Control-Allow-Origin: *

Notes:

  • playAtRealTime is a server-side Date.now() timestamp. Remote clients should use the /api/time endpoint to estimate clock offset and adjust accordingly.
  • incarnation changes when a new song or audio segment begins. Clients should reset their audio scheduling state when incarnation changes.
  • Audio samples are interleaved: for stereo, the pattern is [L0, R0, L1, R1, ...]. Clients must deinterleave into per-channel buffers for Web Audio API playback.
  • The ring buffer holds approximately 5 seconds of audio. If a client falls behind, the oldest chunks are silently lost. The response will include chunks starting from the oldest still available.
  • Polling at ~50ms intervals is recommended for smooth playback.

Example (curl):

# First request - get all available chunks
curl http://localhost:3000/api/audio?afterSeq=0 --output audio.bin

# Subsequent requests - only get new chunks
curl http://localhost:3000/api/audio?afterSeq=42 --output audio.bin

GET /api/view-objects

Get view objects for the 3D preview. Returns the list of view objects (meshes and image planes) parsed from the xLights XML layout. Each entry describes an OBJ mesh or background image with its position, rotation, scale, brightness, and channel mapping.

Request:

  • Method: GET
  • Headers: None required

Response:

  • Status: 200 OK - Array of ViewObject records (may be empty [] when no show is loaded)

Example Response:

[
{
"name": "House",
"displayAs": "Mesh",
"objFile": "HouseModel/house.obj",
"worldPosX": 0,
"worldPosY": 0,
"worldPosZ": 0,
"scaleX": 1,
"scaleY": 1,
"scaleZ": 1,
"rotateX": 0,
"rotateY": 0,
"rotateZ": 0,
"brightness": 100,
"active": true
},
{
"name": "Background",
"displayAs": "Image",
"imageFile": "images/yard.png",
"worldPosX": 0,
"worldPosY": 50,
"worldPosZ": -100,
"scaleX": 1,
"scaleY": 1,
"scaleZ": 1,
"rotateX": 0,
"rotateY": 0,
"rotateZ": 0,
"brightness": 100,
"transparency": 0,
"active": true
}
]

GET /api/show-file

Serve a file from the current show folder. Used by the 3D viewer to load OBJ models, MTL materials, and texture images. Only accepts show-folder-relative paths and a restricted set of file extensions for security.

Request:

  • Method: GET
  • Query Parameters:
    • path (string, required) - Show-folder-relative file path (e.g., HouseModel/house.obj)

Security Constraints:

  • Absolute paths are rejected (no drive letters or leading /)
  • Path traversal (..) segments are rejected
  • Only these file extensions are allowed: .obj, .mtl, .png, .jpg, .jpeg, .gif, .webp, .bmp, .tga, .dds
  • Resolved path must remain within the show folder

Response:

  • Status: 200 OK - File contents with inferred MIME type
  • Status: 400 Bad Request - Missing path, absolute path, or show folder not set
  • Status: 403 Forbidden - Path traversal or disallowed file extension
  • Status: 404 Not Found - File does not exist
  • Status: 500 Internal Server Error - Server error

Example:

GET /api/show-file?path=HouseModel/house.obj
GET /api/show-file?path=HouseModel/texture_1001.png

Error Responses:

{ "error": "File path is required" }
{ "error": "Absolute paths are not allowed — use show-folder-relative paths" }
{ "error": "Path traversal not allowed" }
{ "error": "File type not allowed: .exe" }
{ "error": "Resolved path outside show folder" }
{ "error": "File not found" }

GET /api/layout-settings

Returns layout-level settings parsed from the xLights <settings> element in xlights_rgbeffects.xml. Includes background image path, brightness, and preview canvas dimensions.

Request:

  • Method: GET
  • Headers: None required

Response:

  • Status: 200 OK
{
"backgroundImage": "PIFar.jpg",
"backgroundBrightness": 20,
"previewWidth": 1280,
"previewHeight": 720
}

All fields are optional — the object may be empty if no settings are present in the XML. The backgroundImage path is show-folder-relative and can be loaded via /api/show-file?path=PIFar.jpg.


GET /api/moving-heads

Get DMX moving head fixture definitions. Returns the list of DmxMovingHead and DmxMovingHeadAdv fixtures parsed from the xLights XML layout. Each entry contains everything needed to compute beam position, direction, and color from live frame data: motor definitions, color channels, beam geometry, and world transform. Returns an empty array when no show is loaded or no moving head fixtures are present.

Request:

  • Method: GET
  • Headers: None required

Response:

  • Status: 200 OK - Array of MhFixtureInfo records (may be empty [])

Example Response:

[
{
"name": "House MH Left",
"channelOffset": 115084,
"numChannels": 14,
"definition": {
"panMotor": {
"channelCoarse": 1,
"channelFine": 2,
"rangeOfMotion": 540,
"orientZero": 270,
"reverse": false
},
"tiltMotor": {
"channelCoarse": 3,
"channelFine": 4,
"rangeOfMotion": 270,
"orientZero": 90,
"reverse": false
},
"color": {
"colorType": "RGBW",
"redChannel": 5,
"greenChannel": 6,
"blueChannel": 7,
"whiteChannel": 8
},
"dimmer": { "channel": 9 },
"shutter": { "channel": 10, "openThreshold": 128 }
},
"beamParams": {
"dmxBeamWidth": 1.0,
"dmxBeamLength": 20.0,
"dmxBeamYOffset": 17.0,
"dmxBeamLimit": 0,
"meshWidth": 50.0,
"meshHeight": 100.0,
"meshDepth": 50.0
},
"worldTransform": {
"worldPosX": 300,
"worldPosY": 600,
"worldPosZ": 0,
"rotateX": 0,
"rotateY": 0,
"rotateZ": 0,
"scaleX": 1,
"scaleY": 1,
"scaleZ": 1
}
}
]

Field Reference:

FieldDescription
nameModel name as defined in xLights
channelOffset0-based start channel of this fixture in the frame buffer
numChannelsNumber of DMX channels for this fixture (from parm1)
definition.panMotorPan motor: coarse/fine channels, range of motion (degrees), orient-zero offset, reverse flag
definition.tiltMotorTilt motor: same fields as panMotor
definition.color.colorTypeOne of "RGBW", "CMY", "ColorWheel", "None"
definition.dimmer.channel1-based dimmer channel (0 = no dimmer, fixture always full)
definition.shutter.openThresholdDMX value at or above which the shutter is considered open
beamParams.dmxBeamWidthBeam cone half-angle in degrees
beamParams.dmxBeamLengthBeam length in model-space units (multiply by sbl for world length — see below)
beamParams.dmxBeamYOffsetY offset of beam emission point from fixture origin
beamParams.dmxBeamLimitMaximum world beam length cap (0 = no limit)
beamParams.meshWidth/Height/DepthControlling mesh bounding box dimensions — used to compute world beam length: sbl = max(meshWidth × |scaleX|, meshHeight × |scaleY|, meshDepth × |scaleZ|), then worldBeamLength = dmxBeamLength × sbl
worldTransformPosition, rotation (degrees), and scale of the fixture in world coordinates

Usage:

To render a live beam, slice channelOffset … channelOffset + numChannels bytes from the current frame buffer, then pass that slice along with definition, beamParams, and worldTransform to the xllayoutcalcs functions mhChannelsToState and computeBeamDescriptor. The resulting MhBeamDescriptor gives world-space origin, direction, length, coneHalfAngle, and colour.


GET /api/debug-show-folder

Diagnostic endpoint. Returns the current show folder path and a dump of all cached server state. Intended for development and troubleshooting only.

Request:

  • Method: GET
  • Headers: None required

Response:

  • Status: 200 OK
{
"showFolder": "/path/to/show",
"hasShowFolder": true,
"allStateKeys": ["showFolder", "sequences", "playlists", "..."],
"state": { "...full cached state..." }
}

/proxy/<target-url>

HTTP and WebSocket proxy for multi-NIC bridging. Forwards requests to a target URL extracted from the path. Allows the browser-based UI to reach devices on networks that are only reachable from the server host (e.g., a light controller on a dedicated NIC).

URL Pattern:

/proxy/<full-target-URL>

The protocol prefix is optional; http:// is assumed when omitted.

Examples:

GET /proxy/http://192.168.1.50:8080/api/status
POST /proxy/192.168.1.50/api/config
WS /proxy/ws://192.168.1.50:9090/ws

Behavior:

  • All HTTP methods are forwarded (GET, POST, PUT, DELETE, PATCH, etc.)
  • Request headers are forwarded with hop-by-hop headers stripped
  • Request body is streamed through for POST/PUT/PATCH
  • WebSocket upgrade requests are proxied transparently
  • 30-second request timeout

Response:

  • Status and headers from the target are returned as-is
  • Status: 400 Bad Request - Invalid or missing target URL in path

WebSocket API

Connection

  • Endpoint: ws://localhost:{port}/ws
  • Protocol: WebSocket (ws:// or wss://)

Connection Behavior:

  • Server sends initial state snapshot on connection
  • Heartbeat ping every 5 seconds
  • Client must respond with pong within 15 seconds
  • Maximum buffer: 8MB per connection
  • Automatic reconnection supported (client-side)

Server-to-Client Messages

snapshot

State update broadcast. Broadcasts player state updates. Contains version numbers for each state key and partial state data.

Message Format:

{
"type": "snapshot",
"v": { "<key>": <version_number>, ... },
"data": { "<key>": <value>, ... }
}

Example:

{
"type": "snapshot",
"v": {
"showFolder": 1,
"sequences": 5,
"playlists": 3,
"schedule": 2,
"user": 1,
"show": 1,
"cStatus": 10,
"pStatus": 25,
"nStatus": 8,
"playbackSettings": 2,
"playbackStatistics": 15,
"versions": 1
},
"data": {
"pStatus": {
"ptype": "EZP",
"status": "Playing",
"reported_time": 1704067200000,
"now_playing": {
"type": "Scheduled",
"item": "Playlist",
"title": "Christmas Songs",
"at": 1704067200000
},
"volume": {
"level": 75,
"muted": false
}
}
}
}

State Keys (FullPlayerState):

KeyTypeDescription
showFolderstringCurrent show folder path
sequencesSequenceRecord[]Array of sequences
playlistsPlaylistRecord[]Array of playlists
scheduleScheduledPlaylist[]Array of scheduled playlists
userEndUserUser object
showEndUserShowSettingsShow settings object
cStatusPlayerCStatusContentController status
pStatusPlayerPStatusContentPlayback status
nStatusPlayerNStatusContentNetwork status
playbackSettingsPlaybackSettingsPlayback settings
playbackStatisticsPlaybackStatisticsPlayback statistics
versionsEZPlayerVersionsVersion info

Behavior:

  • Version numbers increment on each update
  • Only changed keys are included in data
  • Multiple updates to the same key are coalesced (latest wins)
  • Client receives full snapshot on initial connection

ping

Heartbeat ping. Server sends ping every 5 seconds to check connection health. Client must respond with pong.

Message Format:

{
"type": "ping",
"now": 1704067200000
}

Client must send a pong message with the same now value within 15 seconds.

Timeout: If no pong received within 15 seconds, server disconnects with a kick message.

kick

Server-initiated disconnection. Server sends this before disconnecting a client. Reasons include heartbeat timeout or excessive buffering.

Message Format:

{
"type": "kick",
"reason": "heartbeat timeout"
}

Common Reasons:

  • "heartbeat timeout" - Client didn't respond to pings within 15 seconds
  • "backpressure: buffered={bytes}" - Client buffer exceeded 8MB limit
  • "socket closed" - Connection closed
  • "socket error" - Connection error
  • "send failed" - Failed to send message

After sending kick, the server closes the connection.

Client-to-Server Messages

pong

Response to server ping. Client must respond to server ping messages to maintain the connection.

Message Format:

{
"type": "pong",
"now": 1704067200000
}

Requirements:

  • Must be sent within 15 seconds of receiving ping
  • Must use the same now value from the ping message
  • Failure to respond results in disconnection

subscribe (defined but not yet implemented)

Note: The subscribe message type is defined in the TypeScript types (PlayerClientWebSocketMessage) but the server does not currently process it. All clients receive updates for all state keys. This may be implemented in a future release.

Message Format:

{
"type": "subscribe",
"keys": ["pStatus", "cStatus", "playbackSettings"]
}

Valid Keys: All keys from FullPlayerState (see State Keys table above).


WebSocket Features

Heartbeat Mechanism

  • Server sends ping every 5 seconds
  • Client must respond with pong within 15 seconds
  • Timeout results in kick and disconnection

Backpressure Management

  • Maximum buffer: 8MB per connection
  • Server monitors bufferedAmount
  • If buffer exceeds limit, server sends kick and disconnects
  • Prevents memory issues with slow clients

State Versioning

  • Each state key has a version number
  • Versions increment on each update
  • Client can track which updates it has received
  • Enables efficient state synchronization

Update Coalescing

  • Multiple rapid updates to the same key are coalesced
  • Only the latest value is sent
  • Reduces network traffic
  • Prevents client from being overwhelmed

Error Handling

HTTP Error Responses

All REST API endpoints may return the following error responses:

400 Bad Request:

{
"error": "Error description"
}

404 Not Found:

{
"error": "Resource not found"
}

500 Internal Server Error:

{
"error": "Internal server error"
}

503 Service Unavailable:

{
"error": "Service unavailable description"
}

WebSocket Error Handling

  • Connection errors: Client should attempt reconnection
  • Parse errors: Invalid JSON messages are ignored
  • Timeout errors: Server disconnects after timeout
  • Buffer overflow: Server disconnects when buffer exceeds limit

Complete WebSocket Workflow Example

// Connect to WebSocket
const ws = new WebSocket('ws://localhost:3000/ws');

// Handle connection
ws.onopen = () => {
console.log('Connected');
};

// Handle messages
ws.onmessage = (event) => {
const message = JSON.parse(event.data);

switch (message.type) {
case 'snapshot':
console.log('State update:', message.data);
break;
case 'ping':
// Respond to ping
ws.send(
JSON.stringify({
type: 'pong',
now: message.now,
}),
);
break;
case 'kick':
console.log('Kicked:', message.reason);
ws.close();
break;
}
};

// Handle errors
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};

// Handle close
ws.onclose = () => {
console.log('Disconnected');
};