topazcube 0.0.3 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +82 -0
- package/dist/compress-browser.d.ts +2 -0
- package/dist/compress-buffer.d.ts +2 -0
- package/dist/compress-node.d.ts +2 -0
- package/dist/index.d.ts +7 -0
- package/dist/server.d.ts +99 -0
- package/dist/terminal.d.ts +62 -0
- package/dist/topazcube-client.js +481 -0
- package/dist/topazcube-server.js +740 -0
- package/dist/topazcube.d.ts +2 -0
- package/dist/utils.d.ts +31 -0
- package/package.json +26 -12
- package/src/client.ts +859 -0
- package/src/compress-browser.ts +34 -0
- package/src/compress-node.ts +39 -0
- package/src/server.ts +1111 -0
- package/src/terminal.js +144 -0
- package/src/utils.ts +402 -0
- package/dist/topazcube.js +0 -1527
- package/src/client.js +0 -251
- package/src/server.js +0 -335
- package/src/utils.js +0 -216
- /package/src/{topazcube.js → topazcube.ts} +0 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
import { applyOperation } from 'fast-json-patch'
|
|
2
|
+
import {
|
|
3
|
+
reactive,
|
|
4
|
+
opmsg,
|
|
5
|
+
msgop,
|
|
6
|
+
decode_uint32,
|
|
7
|
+
decode_fp412,
|
|
8
|
+
decode_fp168,
|
|
9
|
+
decode_fp1616,
|
|
10
|
+
deepGet,
|
|
11
|
+
encode,
|
|
12
|
+
decode,
|
|
13
|
+
} from './utils'
|
|
14
|
+
import { compress, decompress } from './compress-browser'
|
|
15
|
+
import { glMatrix, vec3, quat } from 'gl-matrix'
|
|
16
|
+
import { argv0 } from 'process'
|
|
17
|
+
|
|
18
|
+
const MAX_PACKAGE_SIZE = 65400; // Slightly below the 65535 limit to allow for overhead
|
|
19
|
+
|
|
20
|
+
export default class TopazCubeClient {
|
|
21
|
+
CYCLE = 200 // update/patch rate in ms
|
|
22
|
+
url = ''
|
|
23
|
+
documents = {}
|
|
24
|
+
autoReconnect = true
|
|
25
|
+
allowSync = true
|
|
26
|
+
allowWebRTC = false
|
|
27
|
+
isConnected = false
|
|
28
|
+
isConnecting = false
|
|
29
|
+
isPatched = false
|
|
30
|
+
stats = {
|
|
31
|
+
send: 0,
|
|
32
|
+
rec: 0,
|
|
33
|
+
recRTC: 0,
|
|
34
|
+
|
|
35
|
+
sendBps: 0,
|
|
36
|
+
recBps: 0,
|
|
37
|
+
recRTCBps: 0,
|
|
38
|
+
|
|
39
|
+
ping: 0,
|
|
40
|
+
stdiff: 0, // server time difference
|
|
41
|
+
}
|
|
42
|
+
lastFullState = 0
|
|
43
|
+
lastPatch = 0
|
|
44
|
+
_chunks = {}
|
|
45
|
+
le = true // Server is little endian
|
|
46
|
+
_documentChanges:any = {}
|
|
47
|
+
|
|
48
|
+
ID = 0
|
|
49
|
+
socket:any = null
|
|
50
|
+
_peerConnection:any = null
|
|
51
|
+
_dataChannel:any = null // our data channel
|
|
52
|
+
_serverDataChannel:any = null // server data channel
|
|
53
|
+
_webRTCConnected:any = null
|
|
54
|
+
|
|
55
|
+
isInterpolated = false
|
|
56
|
+
_lastInterpolate = Date.now()
|
|
57
|
+
_lastUpdateId = {}
|
|
58
|
+
_dpos:vec3 = [0, 0, 0]
|
|
59
|
+
_drot:quat = [0, 0, 0, 1]
|
|
60
|
+
_sca:vec3 = [1, 1, 1]
|
|
61
|
+
_notifyChanges = true
|
|
62
|
+
_siv:any = null
|
|
63
|
+
_loopiv:any = null
|
|
64
|
+
_updateiv:any = null
|
|
65
|
+
_pingiv:any = null
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
constructor({
|
|
69
|
+
url, // server url
|
|
70
|
+
autoReconnect = true, // auto reconnect on disconnect
|
|
71
|
+
allowSync = true, // allow sync on connect
|
|
72
|
+
allowWebRTC = false,
|
|
73
|
+
}) {
|
|
74
|
+
this.url = url
|
|
75
|
+
this.autoReconnect = autoReconnect
|
|
76
|
+
this.allowSync = allowSync
|
|
77
|
+
this.allowWebRTC = allowWebRTC
|
|
78
|
+
this.socket = null
|
|
79
|
+
this._startLoop()
|
|
80
|
+
console.log('Client initialized')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/*= UPDATE ===================================================================*/
|
|
84
|
+
|
|
85
|
+
_startLoop() {
|
|
86
|
+
if (this._loopiv) {
|
|
87
|
+
clearInterval(this._loopiv)
|
|
88
|
+
}
|
|
89
|
+
this._loopiv = setInterval(() => {
|
|
90
|
+
this._loop()
|
|
91
|
+
}, this.CYCLE)
|
|
92
|
+
this._siv = setInterval(() => {
|
|
93
|
+
this._updateStats()
|
|
94
|
+
}, 1000)
|
|
95
|
+
this._pingiv = setInterval(() => {
|
|
96
|
+
this._ping()
|
|
97
|
+
}, 10000)
|
|
98
|
+
}
|
|
99
|
+
_loop() {
|
|
100
|
+
if (!this.isConnected) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
this._sendPatches()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_updateStats() {
|
|
107
|
+
this.stats.recBps = this.stats.rec
|
|
108
|
+
this.stats.rec = 0
|
|
109
|
+
this.stats.recRTCBps = this.stats.recRTC
|
|
110
|
+
this.stats.recRTC = 0
|
|
111
|
+
this.stats.sendBps = this.stats.send
|
|
112
|
+
this.stats.send = 0
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_clear() {
|
|
116
|
+
this.stats.sendBps = 0
|
|
117
|
+
this.stats.recBps = 0
|
|
118
|
+
this.stats.recRTC = 0
|
|
119
|
+
this.stats.recRTCBps = 0
|
|
120
|
+
this.stats.send = 0
|
|
121
|
+
this.stats.rec = 0
|
|
122
|
+
|
|
123
|
+
this.ID = 0
|
|
124
|
+
this.documents = {}
|
|
125
|
+
this._documentChanges = {}
|
|
126
|
+
this._lastUpdateId = {}
|
|
127
|
+
this.lastFullState = 0
|
|
128
|
+
this.lastPatch = 0
|
|
129
|
+
this._lastInterpolate = 0
|
|
130
|
+
this.isPatched = false
|
|
131
|
+
this.le = true
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/*= INTERPOLATION ============================================================*/
|
|
135
|
+
|
|
136
|
+
// to be called in display rate (like 60fps) to interpolate .position, .rotation and .scale
|
|
137
|
+
interpolate() {
|
|
138
|
+
let now = Date.now()
|
|
139
|
+
let dt = now - this._lastInterpolate
|
|
140
|
+
this._lastInterpolate = now
|
|
141
|
+
if (dt <= 0 || dt > 200) { return }
|
|
142
|
+
this.isInterpolated = true
|
|
143
|
+
for (let name in this.documents) {
|
|
144
|
+
let doc = this.documents[name]
|
|
145
|
+
let entities = doc.entities
|
|
146
|
+
if (!entities) { continue }
|
|
147
|
+
for (let id in entities) {
|
|
148
|
+
let e = entities[id]
|
|
149
|
+
if (e._lpostime1 && e._lpostime2) {
|
|
150
|
+
let t1 = e._lpostime1
|
|
151
|
+
let t2 = e._lpostime2
|
|
152
|
+
const interval = t2 - t1;
|
|
153
|
+
const elapsed = now - t1;
|
|
154
|
+
const alpha = Math.max(0, elapsed / interval)
|
|
155
|
+
vec3.lerp(this._dpos, e._lpos1, e._lpos2, alpha)
|
|
156
|
+
vec3.lerp(e.position, e.position, this._dpos, 0.07)
|
|
157
|
+
e._changed_position = now
|
|
158
|
+
}
|
|
159
|
+
if (e._lrottime1 && e._lrottime2) {
|
|
160
|
+
|
|
161
|
+
let t1 = e._lrottime1
|
|
162
|
+
let t2 = e._lrottime2
|
|
163
|
+
const interval = t2 - t1;
|
|
164
|
+
const elapsed = now - t1;
|
|
165
|
+
const alpha = Math.max(0, elapsed / interval)
|
|
166
|
+
quat.slerp(this._drot, e._lrot1, e._lrot2, alpha)
|
|
167
|
+
quat.slerp(e.rotation, e.rotation, this._drot, 0.07)
|
|
168
|
+
e._changed_rotation = now
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
this.isInterpolated = false
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/*= CONNECTION ===============================================================*/
|
|
177
|
+
|
|
178
|
+
subscribe(name) {
|
|
179
|
+
this.documents[name] = {}
|
|
180
|
+
this.send({ c: 'sub', n: name })
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
unsubscribe(name) {
|
|
184
|
+
this.send({ c: 'unsub', n: name })
|
|
185
|
+
delete this.documents[name]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
connect() {
|
|
189
|
+
if (this.isConnecting) {
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
this.isConnecting = true
|
|
193
|
+
this._clear()
|
|
194
|
+
console.log('connecting...')
|
|
195
|
+
|
|
196
|
+
this.socket = new WebSocket(this.url)
|
|
197
|
+
|
|
198
|
+
// message received
|
|
199
|
+
this.socket.onmessage = async (event) => {
|
|
200
|
+
let buffer = await event.data.arrayBuffer()
|
|
201
|
+
this.stats.rec += buffer.byteLength
|
|
202
|
+
let dec = await decompress(buffer)
|
|
203
|
+
let message = decode(dec)
|
|
204
|
+
this._onMessage(message)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// connection closed
|
|
208
|
+
this.socket.onclose = (event) => {
|
|
209
|
+
this._clear()
|
|
210
|
+
this.isConnected = false
|
|
211
|
+
this.isConnecting = false
|
|
212
|
+
this.lastFullState = 0
|
|
213
|
+
this.socket = null
|
|
214
|
+
this.onDisconnect()
|
|
215
|
+
if (this.allowWebRTC) {
|
|
216
|
+
this._destroyWebRTC()
|
|
217
|
+
}
|
|
218
|
+
if (this.autoReconnect) {
|
|
219
|
+
setTimeout(
|
|
220
|
+
() => {
|
|
221
|
+
this._reconnect()
|
|
222
|
+
},
|
|
223
|
+
500 + Math.random() * 500
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.socket.onerror = (event) => {
|
|
229
|
+
this._clear()
|
|
230
|
+
this.isConnected = false
|
|
231
|
+
this.isConnecting = false
|
|
232
|
+
this.lastFullState = 0
|
|
233
|
+
this.socket = null
|
|
234
|
+
this.onDisconnect()
|
|
235
|
+
if (this.allowWebRTC) {
|
|
236
|
+
this._destroyWebRTC()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (this.autoReconnect) {
|
|
240
|
+
setTimeout(
|
|
241
|
+
() => {
|
|
242
|
+
this._reconnect()
|
|
243
|
+
},
|
|
244
|
+
500 + Math.random() * 500
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
this.socket.onopen = async (event) => {
|
|
250
|
+
this._clear()
|
|
251
|
+
this.isConnecting = false
|
|
252
|
+
this.isConnected = true
|
|
253
|
+
this.lastFullState = 0
|
|
254
|
+
this._ping()
|
|
255
|
+
this.onConnect()
|
|
256
|
+
if (this.allowWebRTC) {
|
|
257
|
+
await this._initializeWebRTC()
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
disconnect() {
|
|
263
|
+
this._clear()
|
|
264
|
+
this.isConnected = false
|
|
265
|
+
this.isConnecting = false
|
|
266
|
+
this.lastFullState = 0
|
|
267
|
+
this.socket.close()
|
|
268
|
+
this.socket = null
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
destroy() {
|
|
272
|
+
this._clear()
|
|
273
|
+
this.autoReconnect = false
|
|
274
|
+
this.disconnect()
|
|
275
|
+
this.socket = null
|
|
276
|
+
clearInterval(this._siv)
|
|
277
|
+
clearInterval(this._loopiv)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
onConnect() {}
|
|
281
|
+
|
|
282
|
+
onDisconnect() {}
|
|
283
|
+
|
|
284
|
+
_reconnect() {
|
|
285
|
+
if (!this.isConnected) {
|
|
286
|
+
if (!this.isConnecting) {
|
|
287
|
+
this.connect()
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_ping() {
|
|
293
|
+
if (this.isConnected) {
|
|
294
|
+
this.send({ c: 'ping', ct: Date.now() })
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/*= MESSAGES =================================================================*/
|
|
299
|
+
|
|
300
|
+
onChange(name, doc) {}
|
|
301
|
+
|
|
302
|
+
onMessage(message) {}
|
|
303
|
+
|
|
304
|
+
send(operation) {
|
|
305
|
+
try {
|
|
306
|
+
let enc = encode(operation)
|
|
307
|
+
this.stats.send += enc.byteLength
|
|
308
|
+
this.socket.send(enc)
|
|
309
|
+
} catch (e) {
|
|
310
|
+
console.error('send failed', e)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
get document() {
|
|
315
|
+
let names = Object.keys(this.documents)
|
|
316
|
+
return this.documents[names[0]]
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async _onMessage(message) {
|
|
320
|
+
let time = Date.now()
|
|
321
|
+
if (message.c == 'full') {
|
|
322
|
+
console.log('full:', message)
|
|
323
|
+
let name = message.n
|
|
324
|
+
let doc = message.doc
|
|
325
|
+
this.documents[name] = doc
|
|
326
|
+
this._decodeFastChanges(message)
|
|
327
|
+
this.isPatched = false
|
|
328
|
+
if (this.allowSync) {
|
|
329
|
+
this.documents[name] = reactive(
|
|
330
|
+
name,
|
|
331
|
+
this.documents[name],
|
|
332
|
+
this._onDocumentChange.bind(this)
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
this.isPatched = false
|
|
336
|
+
this.lastFullState = message.t
|
|
337
|
+
this.le = message.le
|
|
338
|
+
if (this._notifyChanges) {
|
|
339
|
+
this.onChange(name, this.documents[name])
|
|
340
|
+
}
|
|
341
|
+
} else if (message.c == 'patch') {
|
|
342
|
+
// patch
|
|
343
|
+
this.lastPatch = message.t
|
|
344
|
+
let name = message.n
|
|
345
|
+
if (message.doc) {
|
|
346
|
+
this.isPatched = true
|
|
347
|
+
for (let op of message.doc) {
|
|
348
|
+
let dop = msgop(op)
|
|
349
|
+
applyOperation(this.documents[name], dop)
|
|
350
|
+
}
|
|
351
|
+
this.isPatched = false
|
|
352
|
+
}
|
|
353
|
+
if (this._notifyChanges) {
|
|
354
|
+
this.onChange(name, this.documents[name])
|
|
355
|
+
}
|
|
356
|
+
} else if (message.c == 'chunk') {
|
|
357
|
+
//console.log('chunk', message)
|
|
358
|
+
this._chunks[message.mid+'-'+message.seq] = message
|
|
359
|
+
if (message.last) {
|
|
360
|
+
let cfound = 0
|
|
361
|
+
let ts = message.ts
|
|
362
|
+
let cdata = new Uint8Array(ts)
|
|
363
|
+
for (const cid in this._chunks) {
|
|
364
|
+
let chunk = this._chunks[cid]
|
|
365
|
+
if (chunk.mid == message.mid) {
|
|
366
|
+
let offset = chunk.ofs
|
|
367
|
+
let csize = chunk.chs
|
|
368
|
+
cdata.set(new Uint8Array(chunk.data), offset);
|
|
369
|
+
cfound++
|
|
370
|
+
delete this._chunks[cid]
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
//console.log('found chunks ', cfound, 'of', message.seq + 1)
|
|
374
|
+
if (cfound == message.seq + 1) {
|
|
375
|
+
try {
|
|
376
|
+
let cdec = await decompress(cdata)
|
|
377
|
+
let nmessage = decode(cdec)
|
|
378
|
+
//console.log('decoded message', nmessage)
|
|
379
|
+
this._onMessage(nmessage)
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.error('Error decoding chunks:', error)
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
console.warn('missing chunks', cfound, 'of', message.seq + 1)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
} else if (message.c == 'fpatch') {
|
|
388
|
+
time = Date.now()
|
|
389
|
+
let name = message.n
|
|
390
|
+
//console.log('fpatch', message)
|
|
391
|
+
let doPatch = true
|
|
392
|
+
if (!this._lastUpdateId[name]) {
|
|
393
|
+
this._lastUpdateId[name] = message.u
|
|
394
|
+
} else {
|
|
395
|
+
if (this._lastUpdateId[name] < message.u) {
|
|
396
|
+
let lp = message.u - this._lastUpdateId[name] - 1
|
|
397
|
+
if (lp > 0) {
|
|
398
|
+
console.warn('Lost ' + lp + ' updates')
|
|
399
|
+
}
|
|
400
|
+
this._lastUpdateId[name] = message.u
|
|
401
|
+
} else if (this._lastUpdateId[name] > message.u) {
|
|
402
|
+
// Handle the case where the server's update ID is older than the client's
|
|
403
|
+
// This could be due to a network issue or a clock skew
|
|
404
|
+
console.warn(`Received outdated update ID for document ${name}: ${message.u} < ${this._lastUpdateId[name]}`)
|
|
405
|
+
doPatch = false
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (doPatch) {
|
|
409
|
+
this._decodeFastChanges(message)
|
|
410
|
+
}
|
|
411
|
+
} else if (message.c == 'pong') {
|
|
412
|
+
this.ID = message.ID
|
|
413
|
+
time = Date.now()
|
|
414
|
+
let lastct = message.ct
|
|
415
|
+
let ping = time - lastct
|
|
416
|
+
let stime = message.t
|
|
417
|
+
this.send({ c: 'peng', ct: Date.now(), st: stime })
|
|
418
|
+
this.stats.stdiff = stime + ping / 2 - time
|
|
419
|
+
this.stats.ping = ping
|
|
420
|
+
console.log('ping', ping, 'ms', 'stdiff', this.stats.stdiff, 'ms')
|
|
421
|
+
} else if (message.c == 'rtc-offer') {
|
|
422
|
+
//console.log("RTC: offer received:", message);
|
|
423
|
+
// You might need to handle this if the server sends offers
|
|
424
|
+
} else if (message.c == 'rtc-answer') {
|
|
425
|
+
//console.log("RTC: answer received:", message);
|
|
426
|
+
try {
|
|
427
|
+
const sessionDesc = new RTCSessionDescription({
|
|
428
|
+
type: message.type,
|
|
429
|
+
sdp: message.sdp,
|
|
430
|
+
})
|
|
431
|
+
await this._peerConnection.setRemoteDescription(sessionDesc)
|
|
432
|
+
//console.log("RTC: Remote description set successfully");
|
|
433
|
+
|
|
434
|
+
// Log the current state after setting remote description
|
|
435
|
+
//console.log("RTC: Current connection state:", this._peerConnection.connectionState);
|
|
436
|
+
//console.log("RTC: Current ICE connection state:", this._peerConnection.iceConnectionState);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
console.error('RTC: Error setting remote description:', error)
|
|
439
|
+
}
|
|
440
|
+
} else if (message.c == 'rtc-candidate') {
|
|
441
|
+
//console.log("RTC: candidate received", message);
|
|
442
|
+
try {
|
|
443
|
+
if (this._peerConnection && message.candidate) {
|
|
444
|
+
await this._peerConnection.addIceCandidate(
|
|
445
|
+
new RTCIceCandidate(message.candidate)
|
|
446
|
+
)
|
|
447
|
+
//console.log("RTC: ICE candidate added successfully");
|
|
448
|
+
} else {
|
|
449
|
+
console.warn(
|
|
450
|
+
'RTC: Received candidate but peerConnection not ready or candidate missing'
|
|
451
|
+
)
|
|
452
|
+
}
|
|
453
|
+
} catch (error) {
|
|
454
|
+
//console.error("RTC: Error adding ICE candidate:", error);
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
this.onMessage(message)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
_onDocumentChange(name, op, target, path, value) {
|
|
462
|
+
if (this.isPatched || !this.allowSync) {
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
if (path.indexOf('/_') >= 0) {
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
if (!this._documentChanges[name]) {
|
|
469
|
+
this._documentChanges[name] = []
|
|
470
|
+
}
|
|
471
|
+
this._documentChanges[name].push(opmsg(op, target, path, value))
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
_sendPatches() {
|
|
475
|
+
for (let name in this._documentChanges) {
|
|
476
|
+
let dc = this._documentChanges[name]
|
|
477
|
+
if (dc.length == 0) {
|
|
478
|
+
continue
|
|
479
|
+
}
|
|
480
|
+
let record = {
|
|
481
|
+
n: name,
|
|
482
|
+
c: 'sync',
|
|
483
|
+
ct: Date.now(),
|
|
484
|
+
p: null
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (dc.length > 0) {
|
|
488
|
+
record.p = dc
|
|
489
|
+
}
|
|
490
|
+
this.send(record)
|
|
491
|
+
this._documentChanges[name].length = 0
|
|
492
|
+
if (this._notifyChanges) {
|
|
493
|
+
this.onChange(name, this.documents[name])
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
_decodeFastChanges(message) {
|
|
499
|
+
let time = Date.now()
|
|
500
|
+
let name = message.n
|
|
501
|
+
let fdata = message.fdata
|
|
502
|
+
if (!fdata) {
|
|
503
|
+
return
|
|
504
|
+
}
|
|
505
|
+
let doc = this.documents[name]
|
|
506
|
+
if (!doc) {
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
let entities = doc.entities
|
|
510
|
+
if (!entities) {
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
let origin = this.documents[name].origin
|
|
514
|
+
if (!origin) {
|
|
515
|
+
origin = [0, 0, 0]
|
|
516
|
+
}
|
|
517
|
+
for (let key in fdata) {
|
|
518
|
+
let changes = fdata[key]
|
|
519
|
+
if (changes.dict) {
|
|
520
|
+
let pdata = changes.pdata
|
|
521
|
+
let dict = changes.dict
|
|
522
|
+
// Reverse the dictionary for lookup (value to key)
|
|
523
|
+
let rdict = {};
|
|
524
|
+
for (let key in dict) {
|
|
525
|
+
rdict[dict[key]] = key;
|
|
526
|
+
}
|
|
527
|
+
let offset = 0
|
|
528
|
+
|
|
529
|
+
while (offset < pdata.byteLength) {
|
|
530
|
+
let id = ''+decode_uint32(pdata, offset)
|
|
531
|
+
offset += 4
|
|
532
|
+
let did = ''+decode_uint32(pdata, offset)
|
|
533
|
+
offset += 4
|
|
534
|
+
let e = entities[id]
|
|
535
|
+
if (!e) {
|
|
536
|
+
//console.log('Entity not found:', id)
|
|
537
|
+
continue
|
|
538
|
+
}
|
|
539
|
+
let value = rdict[did]
|
|
540
|
+
e[key] = value
|
|
541
|
+
//console.log('FCHANGE', key, id, did, value, rdict)
|
|
542
|
+
e['_changed_'+key] = time
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
let pdata = changes.pdata
|
|
546
|
+
let offset = 0
|
|
547
|
+
while (offset < pdata.byteLength) {
|
|
548
|
+
let id = ''+decode_uint32(pdata, offset)
|
|
549
|
+
let e = entities[id]
|
|
550
|
+
if (!e) {
|
|
551
|
+
if (key == 'position') {
|
|
552
|
+
offset += 13
|
|
553
|
+
} else if (key == 'rotation') {
|
|
554
|
+
offset += 8
|
|
555
|
+
} else if (key == 'scale') {
|
|
556
|
+
offset += 16
|
|
557
|
+
}
|
|
558
|
+
continue
|
|
559
|
+
}
|
|
560
|
+
offset += 4
|
|
561
|
+
|
|
562
|
+
if (key == 'position') {
|
|
563
|
+
if (!e._lpos2) {
|
|
564
|
+
e._lpos1 = [0, 0, 0]
|
|
565
|
+
e._lpos2 = [0, 0, 0]
|
|
566
|
+
} else {
|
|
567
|
+
e._lpos1[0] = e._lpos2[0]
|
|
568
|
+
e._lpos1[1] = e._lpos2[1]
|
|
569
|
+
e._lpos1[2] = e._lpos2[2]
|
|
570
|
+
e._lpostime1 = e._lpostime2
|
|
571
|
+
}
|
|
572
|
+
e._lpostime2 = time
|
|
573
|
+
e._lpos2[0] = origin[0] + decode_fp168(pdata, offset)
|
|
574
|
+
offset += 3
|
|
575
|
+
e._lpos2[1] = origin[1] + decode_fp168(pdata, offset)
|
|
576
|
+
offset += 3
|
|
577
|
+
e._lpos2[2] = origin[2] + decode_fp168(pdata, offset)
|
|
578
|
+
offset += 3
|
|
579
|
+
if (!e.position) {
|
|
580
|
+
e.position = [
|
|
581
|
+
e._lpos2[0],
|
|
582
|
+
e._lpos2[1],
|
|
583
|
+
e._lpos2[2],
|
|
584
|
+
]
|
|
585
|
+
}
|
|
586
|
+
} else if (key == 'rotation') {
|
|
587
|
+
if (!e._lrot2) {
|
|
588
|
+
e._lrot1 = [0, 0, 0, 1]
|
|
589
|
+
e._lrot2 = [0, 0, 0, 1]
|
|
590
|
+
} else {
|
|
591
|
+
e._lrot1[0] = e._lrot2[0]
|
|
592
|
+
e._lrot1[1] = e._lrot2[1]
|
|
593
|
+
e._lrot1[2] = e._lrot2[2]
|
|
594
|
+
e._lrot1[3] = e._lrot2[3]
|
|
595
|
+
e._lrottime1 = e._lrottime2
|
|
596
|
+
}
|
|
597
|
+
e._lrottime2 = time
|
|
598
|
+
e._lrot2[0] = decode_fp412(pdata, offset)
|
|
599
|
+
offset += 2
|
|
600
|
+
e._lrot2[1] = decode_fp412(pdata, offset)
|
|
601
|
+
offset += 2
|
|
602
|
+
e._lrot2[2] = decode_fp412(pdata, offset)
|
|
603
|
+
offset += 2
|
|
604
|
+
e._lrot2[3] = decode_fp412(pdata, offset)
|
|
605
|
+
offset += 2
|
|
606
|
+
quat.normalize(e._lrot2, e._lrot2)
|
|
607
|
+
if (!e.rotation) {
|
|
608
|
+
e.rotation = [
|
|
609
|
+
e._lrot2[0],
|
|
610
|
+
e._lrot2[1],
|
|
611
|
+
e._lrot2[2],
|
|
612
|
+
e._lrot2[3],
|
|
613
|
+
]
|
|
614
|
+
}
|
|
615
|
+
} else if (key == 'scale') {
|
|
616
|
+
if (!e._lsca2) {
|
|
617
|
+
e._lsca1 = [0, 0, 0, 1]
|
|
618
|
+
e._lsca2 = [0, 0, 0, 1]
|
|
619
|
+
} else {
|
|
620
|
+
e._lsca1[0] = e._lsca2[0]
|
|
621
|
+
e._lsca1[1] = e._lsca2[1]
|
|
622
|
+
e._lsca1[2] = e._lsca2[2]
|
|
623
|
+
e._lsca1[3] = e._lsca2[3]
|
|
624
|
+
e._lscatime1 = e._lscatime2
|
|
625
|
+
}
|
|
626
|
+
e._lscatime2 = time
|
|
627
|
+
e._lsca2[0] = decode_fp1616(pdata, offset)
|
|
628
|
+
offset += 4
|
|
629
|
+
e._lsca2[1] = decode_fp1616(pdata, offset)
|
|
630
|
+
offset += 4
|
|
631
|
+
e._lsca2[2] = decode_fp1616(pdata, offset)
|
|
632
|
+
offset += 4
|
|
633
|
+
if (!e.sca) {
|
|
634
|
+
e.sca = [
|
|
635
|
+
e._lsca2[0],
|
|
636
|
+
e._lsca2[1],
|
|
637
|
+
e._lsca2[2],
|
|
638
|
+
]
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/*= WEBRTC ===================================================================*/
|
|
647
|
+
|
|
648
|
+
sendRTC(message) {
|
|
649
|
+
if (this._dataChannel && this._dataChannel.readyState === 'open') {
|
|
650
|
+
this._dataChannel.send(encode(message))
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
_onRTCConnect() {
|
|
655
|
+
console.log('RTC: Connected')
|
|
656
|
+
this.send({ c: 'test', message: 'Hello RTC from client' })
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
_onRTCDisconnect() {
|
|
660
|
+
this._webRTCConnected = true
|
|
661
|
+
console.log('RTC: Disconnected')
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async _onRTCMessage(data) {
|
|
665
|
+
this.stats.recRTC += data.byteLength
|
|
666
|
+
let dec = await decompress(data)
|
|
667
|
+
let message = decode(dec)
|
|
668
|
+
this._onMessage(message)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async _initializeWebRTC() {
|
|
672
|
+
//console.log("RTC: _initializeWebRTC")
|
|
673
|
+
this._peerConnection = null
|
|
674
|
+
try {
|
|
675
|
+
// Create RTCPeerConnection with more comprehensive STUN server list
|
|
676
|
+
this._peerConnection = new RTCPeerConnection({
|
|
677
|
+
iceServers: [
|
|
678
|
+
{ urls: 'stun:stun.l.google.com:19302' },
|
|
679
|
+
{ urls: 'stun:stun.cloudflare.com:3478' },
|
|
680
|
+
{ urls: 'stun:freestun.net:3478' },
|
|
681
|
+
],
|
|
682
|
+
iceCandidatePoolSize: 10,
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
//console.log("RTC: peerConnection created", this._peerConnection)
|
|
686
|
+
|
|
687
|
+
// Handle ICE candidates
|
|
688
|
+
this._peerConnection.onicecandidate = (event) => {
|
|
689
|
+
//console.log("RTC: onicecandidate", event.candidate)
|
|
690
|
+
if (event.candidate) {
|
|
691
|
+
this.send({
|
|
692
|
+
c: 'rtc-candidate',
|
|
693
|
+
type: 'ice-candidate',
|
|
694
|
+
candidate: event.candidate,
|
|
695
|
+
})
|
|
696
|
+
} else {
|
|
697
|
+
//console.log("RTC: ICE candidate gathering complete")
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Log connection state changes
|
|
702
|
+
this._peerConnection.onconnectionstatechange = () => {
|
|
703
|
+
//console.log(`RTC: Connection state changed: ${this._peerConnection.connectionState}`)
|
|
704
|
+
if (this._peerConnection.connectionState === 'connected') {
|
|
705
|
+
this._webRTCConnected = true
|
|
706
|
+
} else if (
|
|
707
|
+
this._peerConnection.connectionState === 'failed' ||
|
|
708
|
+
this._peerConnection.connectionState === 'disconnected' ||
|
|
709
|
+
this._peerConnection.connectionState === 'closed'
|
|
710
|
+
) {
|
|
711
|
+
this._webRTCConnected = false
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
this._peerConnection.onicegatheringstatechange = () => {
|
|
716
|
+
//console.log(`RTC: ICE gathering state: ${this._peerConnection.iceGatheringState}`)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
this._peerConnection.oniceconnectionstatechange = () => {
|
|
720
|
+
//console.log(`RTC: ICE connection state: ${this._peerConnection.iceConnectionState}`)
|
|
721
|
+
|
|
722
|
+
// This is critical - when ICE succeeds, the connection should be established
|
|
723
|
+
if (
|
|
724
|
+
this._peerConnection.iceConnectionState === 'connected' ||
|
|
725
|
+
this._peerConnection.iceConnectionState === 'completed'
|
|
726
|
+
) {
|
|
727
|
+
//console.log("RTC: ICE connection established!")
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Create a data channel on our side as well (belt and suspenders approach)
|
|
732
|
+
this._dataChannel = this._peerConnection.createDataChannel(
|
|
733
|
+
'clientchannel',
|
|
734
|
+
{
|
|
735
|
+
ordered: true,
|
|
736
|
+
maxRetransmits: 1,
|
|
737
|
+
}
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
this._dataChannel.onopen = () => {
|
|
741
|
+
this._onRTCConnect()
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
this._dataChannel.onclose = () => {
|
|
745
|
+
this._onRTCDisconnect()
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
this._dataChannel.onerror = (error) => {
|
|
749
|
+
console.error('RTC: Client data channel error', error)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Handle data channels created by the server
|
|
753
|
+
this._peerConnection.ondatachannel = (event) => {
|
|
754
|
+
//console.log("RTC: Server data channel received", event.channel.label);
|
|
755
|
+
const dataChannel = event.channel
|
|
756
|
+
this._serverDataChannel = dataChannel
|
|
757
|
+
|
|
758
|
+
dataChannel.onopen = () => {
|
|
759
|
+
//console.log("RTC: Server data channel open");
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
dataChannel.onclose = () => {
|
|
763
|
+
//console.log("RTC: Server data channel closed");
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
dataChannel.onerror = (error) => {
|
|
767
|
+
//console.error("RTC: Server data channel error", error);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
dataChannel.onmessage = (event) => {
|
|
771
|
+
this._onRTCMessage(event.data)
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Create and send offer with specific constraints
|
|
776
|
+
const offerOptions = {
|
|
777
|
+
offerToReceiveAudio: false,
|
|
778
|
+
offerToReceiveVideo: false,
|
|
779
|
+
iceRestart: true,
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const offer = await this._peerConnection.createOffer(offerOptions)
|
|
783
|
+
//console.log("RTC: our offer:", offer);
|
|
784
|
+
await this._peerConnection.setLocalDescription(offer)
|
|
785
|
+
|
|
786
|
+
// Wait a moment to ensure the local description is set
|
|
787
|
+
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
788
|
+
|
|
789
|
+
let ld = this._peerConnection.localDescription
|
|
790
|
+
//console.log("RTC: our localDescription", ld);
|
|
791
|
+
|
|
792
|
+
const offerPayload = {
|
|
793
|
+
c: 'rtc-offer',
|
|
794
|
+
type: ld.type,
|
|
795
|
+
sdp: ld.sdp,
|
|
796
|
+
}
|
|
797
|
+
//console.log("RTC: our offer payload", offerPayload);
|
|
798
|
+
this.send(offerPayload)
|
|
799
|
+
|
|
800
|
+
// Set a timeout to check connection status
|
|
801
|
+
setTimeout(() => {
|
|
802
|
+
if (!this._webRTCConnected && this._peerConnection) {
|
|
803
|
+
/*
|
|
804
|
+
console.log("RTC: Connection not established after timeout, current states:");
|
|
805
|
+
console.log("Connection state:", this._peerConnection.connectionState);
|
|
806
|
+
console.log("ICE connection state:", this._peerConnection.iceConnectionState);
|
|
807
|
+
console.log("ICE gathering state:", this._peerConnection.iceGatheringState);
|
|
808
|
+
*/
|
|
809
|
+
// Attempt to restart ICE if needed
|
|
810
|
+
if (this._peerConnection.iceConnectionState === 'failed') {
|
|
811
|
+
console.log('RTC: Attempting ICE restart')
|
|
812
|
+
this._restartIce()
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}, 5000)
|
|
816
|
+
} catch (error) {
|
|
817
|
+
console.error('RTC: error:', error)
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Add this method to restart ICE if needed
|
|
822
|
+
async _restartIce() {
|
|
823
|
+
try {
|
|
824
|
+
const offerOptions = {
|
|
825
|
+
offerToReceiveAudio: false,
|
|
826
|
+
offerToReceiveVideo: false,
|
|
827
|
+
iceRestart: true,
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const offer = await this._peerConnection.createOffer(offerOptions)
|
|
831
|
+
await this._peerConnection.setLocalDescription(offer)
|
|
832
|
+
|
|
833
|
+
const offerPayload = {
|
|
834
|
+
c: 'rtc-offer',
|
|
835
|
+
type: offer.type,
|
|
836
|
+
sdp: offer.sdp,
|
|
837
|
+
}
|
|
838
|
+
//console.log("RTC: ICE restart offer payload", offerPayload);
|
|
839
|
+
this.send(offerPayload)
|
|
840
|
+
} catch (error) {
|
|
841
|
+
//console.error("RTC: Error during ICE restart:", error);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async _destroyWebRTC() {
|
|
846
|
+
if (this._peerConnection) {
|
|
847
|
+
if (this._peerConnection.dataChannel) {
|
|
848
|
+
this._peerConnection.dataChannel.close()
|
|
849
|
+
}
|
|
850
|
+
this._peerConnection.close()
|
|
851
|
+
this._peerConnection = null
|
|
852
|
+
}
|
|
853
|
+
if (this._dataChannel) {
|
|
854
|
+
this._dataChannel.close()
|
|
855
|
+
this._dataChannel = null
|
|
856
|
+
}
|
|
857
|
+
this._webRTCConnected = false
|
|
858
|
+
}
|
|
859
|
+
}
|