|
| 1 | +# WSC (Web Show Control) Client |
| 2 | + |
| 3 | +A WebRTC-based client that connects to a [WSC-compatible gateway](https://github.com/ASLS-org/WSC/tree/main/implementations/js), and handles the creation, transmission, and parsing of WSC packets. |
| 4 | + |
| 5 | +>**About WSC:**<br> |
| 6 | +[WSC](https://github.com/ASLS-org/WSC) defines a unified wire format that transports DMX data, linear timecode, cue triggers, structured parameter updates, and arbitrary binary payloads. A gateway can then relay this data to downstream protocols such as Art-Net, sACN, OSC, MIDI, Modbus, and others. |
| 7 | + |
| 8 | +## Quick Start |
| 9 | + |
| 10 | +Install the following dependencies in your project: |
| 11 | +```bash |
| 12 | +npm install -S @asls/wsc-sdk |
| 13 | +npm install -S @asls/wsc-client |
| 14 | +``` |
| 15 | + |
| 16 | +Import the client and the SDK binding: |
| 17 | + |
| 18 | +```js |
| 19 | +import { WscClient, WSC_REMOTE_STATE, KEEPALIVE_INTERVAL } from '@asls/wsc-client'; |
| 20 | +import { WscPacket, WscTransport, WscFlags, WscAddress } from '@asls/wsc-sdk'; |
| 21 | +``` |
| 22 | + |
| 23 | +### 1. Connect to a gateway |
| 24 | + |
| 25 | +```js |
| 26 | +const client = new WscClient( |
| 27 | + '192.168.1.50', // gateway host or IP |
| 28 | + 4515, // signaling WebSocket port |
| 29 | + onOpen, |
| 30 | + onMessage, |
| 31 | + onClose, |
| 32 | + onError, |
| 33 | +); |
| 34 | + |
| 35 | +client.connect(); |
| 36 | + |
| 37 | +function onOpen() { |
| 38 | + console.log('Connected — WSC ready'); |
| 39 | + client.startKeepAliveSession(); |
| 40 | +} |
| 41 | + |
| 42 | +function onMessage(packet) { |
| 43 | + const decoded = WscPacket.decode(packet); |
| 44 | + console.log(`← ${packet.typeName()}`, decoded); |
| 45 | +} |
| 46 | + |
| 47 | +function onClose() { |
| 48 | + console.log('Disconnected'); |
| 49 | +} |
| 50 | + |
| 51 | +function onError(err) { |
| 52 | + console.error('Error:', err); |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +### 2. Stream DMX channels via Art-Net |
| 57 | + |
| 58 | +```js |
| 59 | +const values = new Uint8Array(512).fill(0); |
| 60 | +values[0] = 255; // channel 1 full |
| 61 | +values[1] = 128; // channel 2 half |
| 62 | + |
| 63 | +const packet = WscPacket.create( |
| 64 | + WscPacket.Type.STREAM_CHANNELS, |
| 65 | + { universe: 0, startChannel: 1, values }, |
| 66 | + { |
| 67 | + flags: new WscFlags(true, true), // TR + GW |
| 68 | + transport: WscTransport.udp( |
| 69 | + WscTransport.Protocol.ARTNET, |
| 70 | + '127.0.0.1', |
| 71 | + 6454, |
| 72 | + ), |
| 73 | + }, |
| 74 | +); |
| 75 | + |
| 76 | +client.send(packet); |
| 77 | +``` |
| 78 | + |
| 79 | +### 3. Fire a cue via OSC |
| 80 | + |
| 81 | +```js |
| 82 | +const packet = WscPacket.create( |
| 83 | + WscPacket.Type.CONTROL_CUE, |
| 84 | + { |
| 85 | + address: WscAddress.parse('lighting.cue.42'), |
| 86 | + action: WscPacket.CueAction.START, |
| 87 | + }, |
| 88 | + { |
| 89 | + flags: new WscFlags(true, true), // TR + GW |
| 90 | + transport: WscTransport.udp( |
| 91 | + WscTransport.Protocol.OSC, |
| 92 | + '127.0.0.1', |
| 93 | + 8000, |
| 94 | + ), |
| 95 | + }, |
| 96 | +); |
| 97 | + |
| 98 | +client.send(packet); |
| 99 | +``` |
| 100 | + |
| 101 | +### 4. Set a named parameter via OSC |
| 102 | + |
| 103 | +```js |
| 104 | +const packet = WscPacket.create( |
| 105 | + WscPacket.Type.CONTROL_PARAM, |
| 106 | + { |
| 107 | + address: WscAddress.parse('lighting.layer.2.intensity'), |
| 108 | + valueType: WscPacket.ValueType.F32, |
| 109 | + value: 0.75, |
| 110 | + }, |
| 111 | + { |
| 112 | + flags: new WscFlags(true, true), |
| 113 | + transport: WscTransport.udp( |
| 114 | + WscTransport.Protocol.OSC, |
| 115 | + '127.0.0.1', |
| 116 | + 8000, |
| 117 | + ), |
| 118 | + }, |
| 119 | +); |
| 120 | + |
| 121 | +client.send(packet); |
| 122 | +``` |
| 123 | + |
| 124 | +### 5. Stream linear timecode |
| 125 | + |
| 126 | +```js |
| 127 | +const packet = WscPacket.create( |
| 128 | + WscPacket.Type.STREAM_TIMECODE, |
| 129 | + { hours: 1, minutes: 0, seconds: 30, frames: 0, rate: 30 }, |
| 130 | +); |
| 131 | + |
| 132 | +client.send(packet); |
| 133 | +``` |
| 134 | + |
| 135 | +### 6. Tunnel raw bytes |
| 136 | + |
| 137 | +```js |
| 138 | +const raw = new Uint8Array([0xF0, 0x42, 0x40, 0x7F, 0xF7]); |
| 139 | + |
| 140 | +const packet = WscPacket.create( |
| 141 | + WscPacket.Type.TUNNEL_RAW, |
| 142 | + { raw }, |
| 143 | + { |
| 144 | + flags: new WscFlags(true, true), |
| 145 | + transport: WscTransport.raw(), |
| 146 | + }, |
| 147 | +); |
| 148 | + |
| 149 | +client.send(packet); |
| 150 | +``` |
| 151 | + |
| 152 | +--- |
| 153 | + |
| 154 | +## Client — API Reference |
| 155 | + |
| 156 | +### `new WscClient(host, port, onOpen, onMessage, onClose, onError)` |
| 157 | + |
| 158 | +| Parameter | Type | Description | |
| 159 | +|---|---|---| |
| 160 | +| `host` | `string` | Gateway hostname or IP address | |
| 161 | +| `port` | `number` | Signaling WebSocket port | |
| 162 | +| `onOpen` | `() => void` | Called when the first DataChannel is open | |
| 163 | +| `onMessage` | `(packet: WscPacket) => void` | Called for each received packet | |
| 164 | +| `onClose` | `() => void` | Called on disconnection | |
| 165 | +| `onError` | `(err: any) => void` | Called on transport or signaling error | |
| 166 | + |
| 167 | +### Methods |
| 168 | + |
| 169 | +| Method | Description | |
| 170 | +|---|---| |
| 171 | +| `connect()` | Initiate WebSocket signaling and establish the DataChannel | |
| 172 | +| `send(packet)` | Serialize and write a `WscPacket` to the appropriate DataChannel; silently drops if the channel is not open | |
| 173 | +| `close()` | Disconnect cleanly and transition to `IDLE` | |
| 174 | +| `startKeepAliveSession()` | Begin sending `STATE_QUERY(KEEPALIVE)` every 1 000 ms | |
| 175 | +| `stopKeepaliveSession()` | Stop the keepalive timer | |
| 176 | + |
| 177 | +### Properties |
| 178 | + |
| 179 | +| Property | Type | Description | |
| 180 | +|---|---|---| |
| 181 | +| `state` | `WSC_REMOTE_STATE` | Current connection state | |
| 182 | +| `debug` | `DebugEntry[]` | Rolling debug log, last 100 entries | |
| 183 | + |
| 184 | +### `WSC_REMOTE_STATE` |
| 185 | + |
| 186 | +| Value | Meaning | |
| 187 | +|---|---| |
| 188 | +| `IDLE` (`0`) | Not connected | |
| 189 | +| `CONNECTING` (`1`) | Signaling in progress | |
| 190 | +| `CONNECTED` (`2`) | DataChannel open | |
| 191 | +| `ERROR` (`-1`) | Unrecoverable transport error | |
| 192 | + |
| 193 | +--- |
| 194 | + |
| 195 | +## DataChannel naming |
| 196 | + |
| 197 | +The client creates one DataChannel per message type, named: |
| 198 | + |
| 199 | +``` |
| 200 | +WSC!DC:<TYPE_NAME> |
| 201 | +``` |
| 202 | + |
| 203 | +For example: `WSC!DC:STREAM_CHANNELS`, `WSC!DC:CONTROL_CUE`. |
| 204 | + |
| 205 | +The gateway identifies WSC channels by the `WSC!DC` prefix. Response channels used by the server: |
| 206 | + |
| 207 | +| Channel | Purpose | |
| 208 | +|---|---| |
| 209 | +| `WSC!DC:STATE_QUERY` | Carries `STATE_ANSWER` responses | |
| 210 | +| `WSC!DC:STATE_ERROR` | Carries `STATE_ERROR` responses | |
| 211 | + |
| 212 | +--- |
| 213 | + |
| 214 | +## Validation |
| 215 | + |
| 216 | +Always validate packets in development to catch flag and compatibility errors early: |
| 217 | + |
| 218 | +```js |
| 219 | +const { valid, errors, warnings } = WscPacket.validate(packet); |
| 220 | + |
| 221 | +if (!valid) { |
| 222 | + console.error('Invalid packet:', errors); |
| 223 | + return; |
| 224 | +} |
| 225 | +if (warnings.length) { |
| 226 | + console.warn('Packet warnings:', warnings); |
| 227 | +} |
| 228 | + |
| 229 | +client.send(packet); |
| 230 | +``` |
| 231 | + |
| 232 | +--- |
| 233 | + |
| 234 | +## Connection lifecycle example |
| 235 | + |
| 236 | +```js |
| 237 | +let client = null; |
| 238 | + |
| 239 | +function connect(host, port) { |
| 240 | + client = new WscClient(host, port, |
| 241 | + () => { |
| 242 | + client.startKeepAliveSession(); |
| 243 | + }, |
| 244 | + (packet) => { |
| 245 | + // handle incoming STATE_ANSWER, STATE_ERROR, etc. |
| 246 | + }, |
| 247 | + () => { |
| 248 | + client = null; |
| 249 | + setTimeout(() => connect(host, port), 2000); |
| 250 | + }, |
| 251 | + (err) => console.error(err), |
| 252 | + ); |
| 253 | + client.connect(); |
| 254 | +} |
| 255 | +``` |
0 commit comments