teleportxr 1.0.47 → 1.0.49

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
@@ -11,6 +11,11 @@ const resources= require("../scene/resources.js");
11
11
  const { BackgroundMode } = require("../core/core.js");
12
12
 
13
13
 
14
+ // Maximum number of times an unacknowledged origin/lighting command will be
15
+ // resent before we give up and log a disconnect. With a 3s ack timeout this
16
+ // gives roughly MAX_ACK_RESENDS*3 seconds before we stop trying.
17
+ const MAX_ACK_RESENDS = 5;
18
+
14
19
  class OriginState
15
20
  {
16
21
  constructor() {
@@ -20,6 +25,8 @@ class OriginState
20
25
  this.acknowledged=false;
21
26
  this.serverTimeSentUs=BigInt(0);
22
27
  this.valid_counter=BigInt(0);
28
+ this.resendCount=0;
29
+ this.givenUp=false;
23
30
  }
24
31
  };
25
32
 
@@ -30,6 +37,8 @@ class LightingState
30
37
  this.acknowledged=false;
31
38
  this.serverTimeSentUs=BigInt(0);
32
39
  this.clientDynamicLighting=new core.ClientDynamicLighting();
40
+ this.resendCount=0;
41
+ this.givenUp=false;
33
42
  }
34
43
  };
35
44
 
@@ -64,9 +73,24 @@ class Client {
64
73
  this.webRtcConnected=false;
65
74
  }
66
75
  }
67
- receivedMessageReliable(id,data)
76
+ receivedMessageReliable(id,pkt)
68
77
  {
69
- console.log('Client receivedMessage reliable ch.'+id+' received: '+data+'.');
78
+ var dataView=new DataView(pkt.data,0,1);
79
+ const messageType=dataView.getUint8(0);
80
+ switch(messageType){
81
+ case message.MessagePayloadType.ReceivedResources:
82
+ this.receiveReceivedResourcesMessage(pkt.data);
83
+ return;
84
+ case message.MessagePayloadType.Acknowledgement:
85
+ this.ReceiveAcknowledgement(pkt.data);
86
+ return;
87
+ case message.MessagePayloadType.ControllerPoses:
88
+ this.ReceiveNodePoses(pkt.data);
89
+ return;
90
+ default:
91
+ console.log('Client receivedMessageReliable ch.'+id+' unknown messageType '+messageType+'.');
92
+ break;
93
+ }
70
94
  }
71
95
  // Is the Buffer bf too small to contain a type tp?
72
96
  checkTooSmall(tp,bf) {
@@ -131,8 +155,14 @@ class Client {
131
155
  this.currentOriginState.acknowledged = false;
132
156
  this.currentOriginState.serverTimeSentUs = BigInt(0);
133
157
  this.currentOriginState.ackId = 0;
158
+ this.currentOriginState.resendCount = 0;
159
+ this.currentOriginState.givenUp = false;
134
160
  this.currentLightingState = new LightingState();
135
161
  this.setupCommand.float32_draw_distance=10.0;
162
+ // The C++ client uses startTimestamp_utc_unix_us as the session epoch
163
+ // against which subsequent message timestamps are measured. Without it
164
+ // some message handlers will not progress.
165
+ this.setupCommand.int64_startTimestamp_utc_unix_us = BigInt(core.getStartTimeUnixUs());
136
166
  if(this.scene)
137
167
  {
138
168
  if(this.scene.backgroundTexturePath&&this.scene.backgroundTexturePath!="")
@@ -182,7 +212,14 @@ class Client {
182
212
  SendCommand(command){
183
213
  let array=core.encodeToUint8Array(command);
184
214
  console.log("SendCommand: encoded to", array.length, "bytes, expected", command.size(), "bytes");
185
- this.signalingSend(array);
215
+ // Once the WebRTC reliable data channel is open, route commands through it
216
+ // (matches the C++ server's sendCommand path). Bootstrap commands sent
217
+ // before the channel is open (SetupCommand, AcknowledgeHandshakeCommand)
218
+ // fall back to the signaling WebSocket.
219
+ if(this.webRtcConnection && this.webRtcConnection.isReliableOpen())
220
+ this.webRtcConnection.sendReliable(array);
221
+ else
222
+ this.signalingSend(array);
186
223
  }
187
224
  // We call StartStreaming once the SetupCommand has been acknowledged.
188
225
  StartStreaming()
@@ -220,10 +257,12 @@ class Client {
220
257
  if(msg.uint64_ackId==this.currentOriginState.ackId)
221
258
  {
222
259
  this.currentOriginState.acknowledged=true;
260
+ this.currentOriginState.resendCount=0;
223
261
  }
224
262
  if(msg.uint64_ackId==this.currentLightingState.ackId)
225
263
  {
226
264
  this.currentLightingState.acknowledged=true;
265
+ this.currentLightingState.resendCount=0;
227
266
  }
228
267
  }
229
268
  ReceiveNodePoses(data)
@@ -298,16 +337,16 @@ class Client {
298
337
  {
299
338
  this.SendTexture(uid);
300
339
  }
301
- if(!this.currentOriginState.acknowledged)
340
+ if(!this.currentOriginState.acknowledged && !this.currentOriginState.givenUp)
302
341
  this.SendOrigin();
303
- if(!this.currentLightingState.acknowledged)
342
+ if(!this.currentLightingState.acknowledged && !this.currentLightingState.givenUp)
304
343
  this.SendLighting();
305
344
  }
306
345
  SendOrigin()
307
346
  {
308
347
  let time_now_us=core.getTimestampUs();
309
348
  let originAckWaitTimeUs=BigInt(3000000);// three seconds
310
- if(this.setupCommand.startTimestamp_utc_unix_us==0)
349
+ if(this.setupCommand.int64_startTimestamp_utc_unix_us==BigInt(0))
311
350
  {
312
351
  console.log("Start timestamp is not set, so not sending origin.");
313
352
  return ;
@@ -329,6 +368,18 @@ class Client {
329
368
  console.log("Origin client has is 0, so not sending origin.");
330
369
  return;
331
370
  }
371
+ // A previous send timed out without an ack. Bound the resend loop so
372
+ // we don't spam the client forever if it never acknowledges.
373
+ if(this.currentOriginState.serverTimeSentUs!=BigInt(0))
374
+ {
375
+ this.currentOriginState.resendCount++;
376
+ if(this.currentOriginState.resendCount>MAX_ACK_RESENDS)
377
+ {
378
+ console.log("Client "+this.clientID+": gave up resending SetOriginNodeCommand after "+this.currentOriginState.resendCount+" attempts; treating client as disconnected for origin.");
379
+ this.currentOriginState.givenUp=true;
380
+ return;
381
+ }
382
+ }
332
383
  this.currentOriginState.valid_counter++;
333
384
  this.geometryService.SetOriginNode(this.currentOriginState.originClientHas);
334
385
  var setp=new command.SetOriginNodeCommand();
@@ -350,7 +401,7 @@ class Client {
350
401
  {
351
402
  let time_now_us=core.getTimestampUs();
352
403
  let ackWaitTimeUs=BigInt(3000000);// three seconds
353
- if(this.setupCommand.startTimestamp_utc_unix_us==0)
404
+ if(this.setupCommand.int64_startTimestamp_utc_unix_us==BigInt(0))
354
405
  {
355
406
  console.log("Start timestamp is not set, so not sending lighting.");
356
407
  return;
@@ -367,6 +418,18 @@ class Client {
367
418
  console.log("WebRTC connection is not established, so not sending lighting.");
368
419
  return;
369
420
  }
421
+ // A previous send timed out without an ack. Bound the resend loop so
422
+ // we don't spam the client forever if it never acknowledges.
423
+ if(this.currentLightingState.serverTimeSentUs!=BigInt(0))
424
+ {
425
+ this.currentLightingState.resendCount++;
426
+ if(this.currentLightingState.resendCount>MAX_ACK_RESENDS)
427
+ {
428
+ console.log("Client "+this.clientID+": gave up resending SetLightingCommand after "+this.currentLightingState.resendCount+" attempts; treating client as disconnected for lighting.");
429
+ this.currentLightingState.givenUp=true;
430
+ return;
431
+ }
432
+ }
370
433
 
371
434
  var setl =new command.SetLightingCommand();
372
435
  setl.uint64_ackId =this.next_ack_id++;
@@ -119,7 +119,7 @@ class GeometryService {
119
119
  var index = clientIDToIndex.get(this.clientID);
120
120
  if (GeometryService.trackedResources.has(uid)) {
121
121
  var res = GeometryService.GetOrCreateTrackedResource(uid);
122
- res.clientNeeds.BitSet(index, false);
122
+ res.clientNeeds.set(index, false);
123
123
  }
124
124
  // Should certainly be in this set:
125
125
  this.nodesToStreamEventually.delete(uid);
@@ -208,7 +208,13 @@ class WebRtcConnection extends EventEmitter
208
208
  if (this.peerConnection !== pc) return;
209
209
  await pc.setLocalDescription(offer);
210
210
  if (this.peerConnection !== pc) return;
211
- var message = '{"teleport-signal-type":"offer","sdp":"'+offer.sdp+'"}'; //
211
+ // Use pc.localDescription.sdp rather than the createOffer() result: the
212
+ // ICE ufrag/pwd in the SDP returned by createOffer() are provisional and
213
+ // libwebrtc may activate a transport with different credentials in
214
+ // setLocalDescription(), which would cause STUN ufrag check failures on
215
+ // the remote peer.
216
+ const localSdp = (pc.localDescription && pc.localDescription.sdp) || offer.sdp;
217
+ var message = '{"teleport-signal-type":"offer","sdp":"'+localSdp+'"}';
212
218
  this.sendConfigMessage(this.id,message);
213
219
  await this.waitUntilIceGatheringStateComplete(pc, this.options);
214
220
  } catch (error)
@@ -343,19 +349,30 @@ class WebRtcConnection extends EventEmitter
343
349
  this.emit('closed');
344
350
  };
345
351
  sendGeometry(buffer) {
346
- try {
352
+ try {
347
353
  this.geometryDataChannel.send(buffer);
348
354
  }
349
355
  catch(exception) {
350
356
  console.error('datachannel.sendGeometry exception: '+exception.message);
351
357
  }
358
+ }
359
+ isReliableOpen() {
360
+ return !!(this.reliableDataChannel && this.reliableDataChannel.readyState === 'open');
361
+ }
362
+ sendReliable(buffer) {
363
+ try {
364
+ this.reliableDataChannel.send(buffer);
365
+ }
366
+ catch(exception) {
367
+ console.error('datachannel.sendReliable exception: '+exception.message);
368
+ }
352
369
  }
353
370
  beforeOffer() {
354
371
 
355
372
  this.videoDataChannel = this.createDataChannel("video",20);
356
373
  this.tagDataChannel = this.createDataChannel("video_tags",40);
357
374
  this.audioToClientDataChannel = this.createDataChannel("audio_server_to_client",60);
358
- this.geometryDataChannel = this.createDataChannel("geometry_unframed",80);
375
+ this.geometryDataChannel = this.createDataChannel("geometry",80);
359
376
  this.reliableDataChannel = this.createDataChannel("reliable",100);
360
377
  this.unreliableDataChannel = this.createDataChannel("unreliable",120,false);
361
378
 
package/package.json CHANGED
@@ -15,7 +15,7 @@
15
15
  },
16
16
  "name": "teleportxr",
17
17
  "description": "Teleport Spatial Server on node.js",
18
- "version": "1.0.47",
18
+ "version": "1.0.49",
19
19
  "repository": {
20
20
  "type": "git",
21
21
  "url": "git+https://github.com/simul/teleport-nodejs.git"
package/signaling.js CHANGED
@@ -53,6 +53,7 @@ var webRtcConnectionManager = null;
53
53
  var newClient=null;
54
54
  var disconnectClient=null;
55
55
  var clientHostHeader = ""; // Store the first client's host header for resource URLs
56
+ var clientProtoHeader = ""; // Store the first client's X-Forwarded-Proto header
56
57
  function startStreaming(signalingClient) {
57
58
  signalingClient.ChangeSignalingState(SignalingState.ACCEPTED);
58
59
  // And we send the WebSockets connect-response.
@@ -185,20 +186,20 @@ function OnWebSocket(ws, req) {
185
186
  signalingClient.ip_addr_port.toString()
186
187
  );
187
188
 
188
- // Capture the Host header from the first client connection for resource URLs
189
- // This allows us to auto-detect the server's public address
189
+ // Capture the Host and X-Forwarded-Proto headers from the first client connection
190
+ // for resource URL auto-detection.
190
191
  if (!clientHostHeader && req.headers) {
191
- // Try to get the host from headers, preferring X-Forwarded-Host (for reverse proxies)
192
- const xForwardedHost = req.headers['x-forwarded-host'];
193
- const hostHeader = req.headers['host'];
194
- clientHostHeader = xForwardedHost || hostHeader || '';
192
+ // Prefer X-Forwarded-Host / X-Forwarded-Proto set by a reverse proxy.
193
+ const xForwardedHost = req.headers['x-forwarded-host'];
194
+ const xForwardedProto = req.headers['x-forwarded-proto'];
195
+ const hostHeader = req.headers['host'];
196
+ clientHostHeader = xForwardedHost || hostHeader || '';
197
+ clientProtoHeader = xForwardedProto || '';
195
198
  if (clientHostHeader) {
196
199
  console.log("Auto-detected resource server host from client connection: " + clientHostHeader);
197
- if (xForwardedHost) {
198
- console.log(" (from X-Forwarded-Host: " + xForwardedHost + ")");
199
- } else if (hostHeader) {
200
- console.log(" (from Host: " + hostHeader + ")");
201
- }
200
+ if (xForwardedHost) console.log(" (from X-Forwarded-Host: " + xForwardedHost + ")");
201
+ else console.log(" (from Host: " + hostHeader + ")");
202
+ if (xForwardedProto) console.log(" (from X-Forwarded-Proto: " + xForwardedProto + ")");
202
203
  } else {
203
204
  console.log("WARNING: Could not auto-detect host from request headers");
204
205
  console.log(" Available headers: " + JSON.stringify(req.headers));
@@ -300,3 +301,8 @@ exports.signalingClients = signalingClients;
300
301
  exports.getClientHostHeader = function () {
301
302
  return clientHostHeader;
302
303
  };
304
+
305
+ // Export function to retrieve the auto-detected X-Forwarded-Proto header
306
+ exports.getClientProtoHeader = function () {
307
+ return clientProtoHeader;
308
+ };