Spaces:
Paused
Paused
reconnecting websocket client with binary PCM audio handling
Browse files- web/src/ws.js +80 -0
web/src/ws.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getToken } from './api.js';
|
| 2 |
+
|
| 3 |
+
const AUDIO_TYPE = 0x01;
|
| 4 |
+
const HDR = 9; // [1B type][4B groupId][4B memberId]
|
| 5 |
+
|
| 6 |
+
export class DispatchSocket {
|
| 7 |
+
constructor(handlers) {
|
| 8 |
+
this.handlers = handlers;
|
| 9 |
+
this.closed = false;
|
| 10 |
+
this.retries = 0;
|
| 11 |
+
this.connect();
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
connect() {
|
| 15 |
+
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
| 16 |
+
this.ws = new WebSocket(`${proto}://${location.host}/ws`);
|
| 17 |
+
this.ws.binaryType = 'arraybuffer';
|
| 18 |
+
|
| 19 |
+
this.ws.onopen = () => {
|
| 20 |
+
this.retries = 0;
|
| 21 |
+
this.ws.send(JSON.stringify({ type: 'auth', token: getToken() }));
|
| 22 |
+
this.handlers.onStatus?.(true);
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
this.ws.onmessage = (ev) => {
|
| 26 |
+
if (typeof ev.data === 'string') {
|
| 27 |
+
let msg;
|
| 28 |
+
try { msg = JSON.parse(ev.data); } catch { return; }
|
| 29 |
+
switch (msg.type) {
|
| 30 |
+
case 'welcome': this.handlers.onWelcome?.(msg); break;
|
| 31 |
+
case 'members': this.handlers.onMembers?.(msg.members); break;
|
| 32 |
+
case 'groups': this.handlers.onGroups?.(msg.groups); break;
|
| 33 |
+
case 'sos_new': this.handlers.onSosNew?.(msg.alert); break;
|
| 34 |
+
case 'sos_update': this.handlers.onSosUpdate?.(msg.alert); break;
|
| 35 |
+
case 'floor': this.handlers.onFloor?.(msg); break;
|
| 36 |
+
case 'error': console.warn('[ws]', msg.message); break;
|
| 37 |
+
}
|
| 38 |
+
} else if (ev.data instanceof ArrayBuffer && ev.data.byteLength > HDR) {
|
| 39 |
+
const dv = new DataView(ev.data);
|
| 40 |
+
if (dv.getUint8(0) !== AUDIO_TYPE) return;
|
| 41 |
+
this.handlers.onAudio?.({
|
| 42 |
+
groupId: dv.getUint32(1),
|
| 43 |
+
memberId: dv.getUint32(5),
|
| 44 |
+
pcm: ev.data.slice(HDR),
|
| 45 |
+
});
|
| 46 |
+
}
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
this.ws.onclose = () => {
|
| 50 |
+
this.handlers.onStatus?.(false);
|
| 51 |
+
if (this.closed) return;
|
| 52 |
+
const delay = Math.min(10000, 500 * 2 ** this.retries++);
|
| 53 |
+
setTimeout(() => this.connect(), delay);
|
| 54 |
+
};
|
| 55 |
+
this.ws.onerror = () => this.ws.close();
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
send(obj) {
|
| 59 |
+
if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(obj));
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
sendAudio(groupId, memberId, int16Buffer) {
|
| 63 |
+
if (this.ws?.readyState !== WebSocket.OPEN) return;
|
| 64 |
+
const head = new Uint8Array(HDR);
|
| 65 |
+
const dv = new DataView(head.buffer);
|
| 66 |
+
dv.setUint8(0, AUDIO_TYPE);
|
| 67 |
+
dv.setUint32(1, groupId);
|
| 68 |
+
dv.setUint32(5, memberId);
|
| 69 |
+
const body = new Uint8Array(int16Buffer);
|
| 70 |
+
const out = new Uint8Array(HDR + body.byteLength);
|
| 71 |
+
out.set(head);
|
| 72 |
+
out.set(body, HDR);
|
| 73 |
+
this.ws.send(out);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
close() {
|
| 77 |
+
this.closed = true;
|
| 78 |
+
this.ws?.close();
|
| 79 |
+
}
|
| 80 |
+
}
|