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/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
+ }