teleportxr 1.0.65 → 1.0.67

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/client/client.js CHANGED
@@ -16,6 +16,11 @@ const { BackgroundMode } = require("../core/core.js");
16
16
  // gives roughly MAX_ACK_RESENDS*3 seconds before we stop trying.
17
17
  const MAX_ACK_RESENDS = 5;
18
18
 
19
+ // Timeout in milliseconds for WebRTC connection establishment.
20
+ // Clients that don't establish WebRTC within this time will be disconnected.
21
+ // Configurable via WEBRTC_CONNECT_TIMEOUT_MS environment variable.
22
+ const WEBRTC_CONNECT_TIMEOUT_MS = parseInt(process.env.WEBRTC_CONNECT_TIMEOUT_MS || '10000', 10);
23
+
19
24
  class OriginState
20
25
  {
21
26
  constructor() {
@@ -56,6 +61,7 @@ class Client {
56
61
  this.next_ack_id=BigInt(1);
57
62
  this.clientStartMs=Date.now();
58
63
  this.webRtcConnectedAtMs=0;
64
+ this.webRtcConnectionInitiatedAtMs=0; // Track when WebRTC connection attempt started
59
65
  }
60
66
  elapsedMsSinceStart(){
61
67
  return Date.now()-this.clientStartMs;
@@ -63,6 +69,18 @@ class Client {
63
69
  elapsedMsSinceConnected(){
64
70
  return this.webRtcConnectedAtMs?Date.now()-this.webRtcConnectedAtMs:-1;
65
71
  }
72
+ hasWebRtcConnectionTimedOut(){
73
+ // If WebRTC isn't connected yet and we initiated the connection attempt,
74
+ // check if we've exceeded the timeout
75
+ if(!this.webRtcConnected && this.webRtcConnectionInitiatedAtMs > 0) {
76
+ const elapsedMs = Date.now() - this.webRtcConnectionInitiatedAtMs;
77
+ if(elapsedMs > WEBRTC_CONNECT_TIMEOUT_MS) {
78
+ console.log("[T+"+this.elapsedMsSinceStart()+"ms] WebRTC connection timeout for client "+this.clientID+" ("+elapsedMs+"ms > "+WEBRTC_CONNECT_TIMEOUT_MS+"ms)");
79
+ return true;
80
+ }
81
+ }
82
+ return false;
83
+ }
66
84
  tick(timestamp){
67
85
  this.geometryService.GetNodesToSend();
68
86
  }
@@ -250,6 +268,7 @@ class Client {
250
268
  StartStreaming()
251
269
  {
252
270
  console.log("[T+"+this.elapsedMsSinceStart()+"ms] Client.StartStreaming() — creating WebRTC connection for client "+this.clientID);
271
+ this.webRtcConnectionInitiatedAtMs=Date.now(); // Record when we started trying to establish WebRTC
253
272
  this.webRtcConnectionManager=WebRtcConnectionManager.getInstance();
254
273
  // We make sure WebRTC has a connection for this client.
255
274
  this.webRtcConnection = this.webRtcConnectionManager.createConnection(
@@ -274,6 +293,7 @@ class Client {
274
293
  // Generic message acknowledgement. Certain kinds of message are expected to be ack'ed.
275
294
  ReceiveAcknowledgement(data)
276
295
  {
296
+ console.log("ReceiveAcknowledgement called with " + data.byteLength + " bytes");
277
297
  if (data.byteLength!=message.AcknowledgementMessage.sizeof())
278
298
  {
279
299
  console.log("Client: Received malformed AcknowledgementMessage packet of length: ",data.length);
@@ -291,13 +311,16 @@ class Client {
291
311
  ?new DataView(data.buffer,data.byteOffset,data.byteLength)
292
312
  :new DataView(data,0,data.byteLength);
293
313
  core.decodeFromDataView(msg,dataView,0);
314
+ console.log("ReceiveAcknowledgement decoded ack_id="+msg.uint64_ackId+", waiting for origin="+this.currentOriginState.ackId+", lighting="+this.currentLightingState.ackId);
294
315
  if(msg.uint64_ackId==this.currentOriginState.ackId)
295
316
  {
317
+ console.log("ReceiveAcknowledgement: acknowledged SetOriginNodeCommand");
296
318
  this.currentOriginState.acknowledged=true;
297
319
  this.currentOriginState.resendCount=0;
298
320
  }
299
321
  if(msg.uint64_ackId==this.currentLightingState.ackId)
300
322
  {
323
+ console.log("ReceiveAcknowledgement: acknowledged SetLightingCommand");
301
324
  this.currentLightingState.acknowledged=true;
302
325
  this.currentLightingState.resendCount=0;
303
326
  }
@@ -29,8 +29,26 @@ class ClientManager
29
29
  clearInterval(this.geometryIntervalId);
30
30
  }
31
31
  UpdateStreaming() {
32
+ // Track clients to remove due to timeout (can't modify Map during iteration)
33
+ const clientsToRemove = [];
34
+
32
35
  for (let [cl_id,cl] of this.clients) {
33
- cl.UpdateStreaming();
36
+ // Check if this client's WebRTC connection has timed out
37
+ if(cl.hasWebRtcConnectionTimedOut()) {
38
+ clientsToRemove.push(cl_id);
39
+ } else {
40
+ cl.UpdateStreaming();
41
+ }
42
+ }
43
+
44
+ // Remove clients that timed out
45
+ for (const cl_id of clientsToRemove) {
46
+ console.log("Removing client "+cl_id+" due to WebRTC connection timeout");
47
+ const cl = this.clients.get(cl_id);
48
+ if(cl) {
49
+ cl.StopStreaming();
50
+ }
51
+ this.RemoveClient(cl_id);
34
52
  }
35
53
  }
36
54
  GetOrCreateClient(clientID)
package/package.json CHANGED
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "name": "teleportxr",
18
18
  "description": "Teleport Spatial Server on node.js",
19
- "version": "1.0.65",
19
+ "version": "1.0.67",
20
20
  "repository": {
21
21
  "type": "git",
22
22
  "url": "git+https://github.com/simul/teleport-nodejs.git"
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+ // Tests for WebRTC connection timeout functionality.
3
+ // Verifies that clients that don't establish WebRTC within the configured
4
+ // timeout are automatically disconnected.
5
+
6
+ const test = require('node:test');
7
+ const assert = require('node:assert');
8
+ const { Client } = require('../client/client');
9
+ const { ClientManager } = require('../client/client_manager');
10
+
11
+ // Helper to create a stub client
12
+ function makeStubClient(opts = {}) {
13
+ const c = Object.create(Client.prototype);
14
+ c.clientID = opts.clientID || 1;
15
+ c.scene = { GetAllNodeUids: () => [] };
16
+ c.geometryService = {
17
+ StreamNode: () => {},
18
+ GetNodesToSend: () => [],
19
+ GetMeshesToSend: () => [],
20
+ GetCanvasesToSend: () => [],
21
+ GetFontAtlasesToSend: () => [],
22
+ GetTexturesToSend: () => [],
23
+ };
24
+ c.clientStartMs = opts.clientStartMs || Date.now();
25
+ c.webRtcConnectedAtMs = 0;
26
+ c.webRtcConnected = false;
27
+ c.webRtcConnection = null;
28
+ c.webRtcConnectionInitiatedAtMs = opts.webRtcConnectionInitiatedAtMs || 0;
29
+ c.StopStreaming = opts.StopStreaming || (() => {});
30
+ return c;
31
+ }
32
+
33
+ test('hasWebRtcConnectionTimedOut returns false when no connection attempt', () => {
34
+ const c = makeStubClient();
35
+ assert.strictEqual(c.hasWebRtcConnectionTimedOut(), false);
36
+ });
37
+
38
+ test('hasWebRtcConnectionTimedOut returns false when already connected', () => {
39
+ const c = makeStubClient();
40
+ c.webRtcConnected = true;
41
+ c.webRtcConnectionInitiatedAtMs = Date.now() - 100000; // old time
42
+ assert.strictEqual(c.hasWebRtcConnectionTimedOut(), false);
43
+ });
44
+
45
+ test('hasWebRtcConnectionTimedOut returns false when under timeout', () => {
46
+ const c = makeStubClient({
47
+ webRtcConnectionInitiatedAtMs: Date.now() - 5000, // 5 seconds ago
48
+ });
49
+ c.webRtcConnected = false;
50
+ assert.strictEqual(c.hasWebRtcConnectionTimedOut(), false);
51
+ });
52
+
53
+ test('hasWebRtcConnectionTimedOut returns true when over timeout', async () => {
54
+ const c = makeStubClient({
55
+ webRtcConnectionInitiatedAtMs: Date.now() - 15000, // 15 seconds ago
56
+ });
57
+ c.webRtcConnected = false;
58
+ assert.strictEqual(c.hasWebRtcConnectionTimedOut(), true);
59
+ });
60
+
61
+ test('ClientManager.UpdateStreaming removes timed-out clients', async () => {
62
+ const cm = new ClientManager();
63
+ const clientsRemoved = [];
64
+
65
+ // Create two stub clients
66
+ const client1 = makeStubClient({ clientID: 1 });
67
+ const client2 = makeStubClient({
68
+ clientID: 2,
69
+ webRtcConnectionInitiatedAtMs: Date.now() - 15000, // Will timeout
70
+ });
71
+
72
+ client2.webRtcConnected = false;
73
+
74
+ cm.clients.set(1, client1);
75
+ cm.clients.set(2, client2);
76
+
77
+ // Mock the RemoveClient to track removals
78
+ const originalRemoveClient = cm.RemoveClient.bind(cm);
79
+ cm.RemoveClient = (clientID) => {
80
+ clientsRemoved.push(clientID);
81
+ originalRemoveClient(clientID);
82
+ };
83
+
84
+ cm.UpdateStreaming();
85
+
86
+ // Client 2 should have been removed due to timeout
87
+ assert.deepStrictEqual(clientsRemoved, [2]);
88
+ assert.strictEqual(cm.clients.has(1), true, 'Client 1 should still exist');
89
+ assert.strictEqual(cm.clients.has(2), false, 'Client 2 should be removed');
90
+ });