teleportxr 1.0.95 → 1.0.97
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 +40 -1
- package/client/client_manager.js +10 -0
- package/client/geometry_service.js +16 -0
- package/connections/webrtcconnection.js +12 -0
- package/core/core.js +1 -0
- package/package.json +1 -1
- package/protocol/encoders/node_encoder.js +20 -1
- package/scene/scene.js +7 -0
- package/test/test_remove_nodes.js +85 -0
package/client/client.js
CHANGED
|
@@ -431,6 +431,11 @@ class Client {
|
|
|
431
431
|
{
|
|
432
432
|
this.SendNode(uid);
|
|
433
433
|
}
|
|
434
|
+
var remove_nodes_uids=this.geometryService.GetRemoveNodesToSend();
|
|
435
|
+
if (remove_nodes_uids.length)
|
|
436
|
+
{
|
|
437
|
+
this.SendRemoveNodes(remove_nodes_uids);
|
|
438
|
+
}
|
|
434
439
|
var mesh_uids=this.geometryService.GetMeshesToSend();
|
|
435
440
|
for (const uid of mesh_uids)
|
|
436
441
|
{
|
|
@@ -599,7 +604,41 @@ class Client {
|
|
|
599
604
|
if(sendSuccess)
|
|
600
605
|
this.geometryService.EncodedResource(uid);
|
|
601
606
|
}
|
|
602
|
-
//!
|
|
607
|
+
//! Tell the client to destroy the given nodes (RemoveNodes payload). If the
|
|
608
|
+
//! geometry channel is not open the uids are re-queued so the next tick retries.
|
|
609
|
+
SendRemoveNodes(uids)
|
|
610
|
+
{
|
|
611
|
+
if(!this.webRtcConnection)
|
|
612
|
+
{
|
|
613
|
+
console.error("Client "+this.clientID+", SendRemoveNodes: this.webRtcConnection is null");
|
|
614
|
+
for (const uid of uids)
|
|
615
|
+
this.geometryService.removedNodesToSend.add(uid);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if(!this.webRtcConnection.isGeometryOpen())
|
|
619
|
+
{
|
|
620
|
+
// Channel not yet open; re-queue so it retries on the next tick.
|
|
621
|
+
for (const uid of uids)
|
|
622
|
+
this.geometryService.removedNodesToSend.add(uid);
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const buffer = new ArrayBuffer(8+1+2+uids.length*8);
|
|
626
|
+
const size=node_encoder.encodeRemoveNodes(uids,buffer);
|
|
627
|
+
const view2 = new DataView(buffer, 0, size);
|
|
628
|
+
const sendSuccess = this.webRtcConnection.sendGeometry(view2);
|
|
629
|
+
console.log("[T+"+this.elapsedMsSinceStart()+"ms, conn+"+this.elapsedMsSinceConnected()+"ms] Sending RemoveNodes ["+uids.join(", ")+"] to Client "+this.clientID+", size: "+size+" bytes — "+
|
|
630
|
+
(sendSuccess ? "OK" : "FAILED"));
|
|
631
|
+
if(sendSuccess)
|
|
632
|
+
{
|
|
633
|
+
for (const uid of uids)
|
|
634
|
+
this.geometryService.EncodedResource(uid);
|
|
635
|
+
}
|
|
636
|
+
else
|
|
637
|
+
{
|
|
638
|
+
for (const uid of uids)
|
|
639
|
+
this.geometryService.removedNodesToSend.add(uid);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
603
642
|
//! standard (e.g. /envCloudyCubemap.ktx2 -> /envCloudyCubemap_ogl.ktx2). Returns undefined
|
|
604
643
|
//! to mean "use the resource's base url" — for non-cubemaps, unknown axes standards, or
|
|
605
644
|
//! when the variant file is missing under the public path (so streaming never breaks).
|
package/client/client_manager.js
CHANGED
|
@@ -13,6 +13,7 @@ class ClientManager
|
|
|
13
13
|
this.clients= new Map();
|
|
14
14
|
this.addClientNodeAndReturnOriginUid=null;
|
|
15
15
|
this.onClientPostCreate=null;
|
|
16
|
+
this.onClientDisconnect=null;
|
|
16
17
|
this.geometryIntervalId=0;
|
|
17
18
|
let unixt_us=core.getStartTimeUnixUs();
|
|
18
19
|
console.log("Start Time: "+unixt_us+" us = "+core.unixTimeToUTCString(unixt_us)+"\n");
|
|
@@ -81,6 +82,11 @@ class ClientManager
|
|
|
81
82
|
RemoveClient(clientID){
|
|
82
83
|
if(this.clients.has(clientID)) {
|
|
83
84
|
this.clients.delete(clientID);
|
|
85
|
+
// Notify the host application after removal, so per-client state
|
|
86
|
+
// (e.g. avatar nodes) can be torn down while this client is gone
|
|
87
|
+
// from the map but the remaining clients are still iterable.
|
|
88
|
+
if(this.onClientDisconnect!=null)
|
|
89
|
+
this.onClientDisconnect(clientID);
|
|
84
90
|
if(this.clients.size==0)
|
|
85
91
|
this.StopStreaming();
|
|
86
92
|
}
|
|
@@ -106,6 +112,10 @@ class ClientManager
|
|
|
106
112
|
{
|
|
107
113
|
this.onClientPostCreate=cb;
|
|
108
114
|
}
|
|
115
|
+
SetClientDisconnectionCallback(cb)
|
|
116
|
+
{
|
|
117
|
+
this.onClientDisconnect=cb;
|
|
118
|
+
}
|
|
109
119
|
// This is a callback, signaling service calls this when the client has signalled.
|
|
110
120
|
newClient(clientID, signalingClient) {
|
|
111
121
|
// then we tell the client manager to start this client.
|
|
@@ -88,6 +88,9 @@ class GeometryService {
|
|
|
88
88
|
this.streamedAnimations = new Map();
|
|
89
89
|
this.streamedTextCanvases = new Map();
|
|
90
90
|
this.streamedFontAtlases = new Map();
|
|
91
|
+
// Nodes that were streamed to this client and have since been unstreamed.
|
|
92
|
+
// Drained by GetRemoveNodesToSend() and sent as a RemoveNodes payload.
|
|
93
|
+
this.removedNodesToSend = new Set();
|
|
91
94
|
|
|
92
95
|
this.backgroundTextureUid = 0;
|
|
93
96
|
// ten seconds for timeout. Tweak this.
|
|
@@ -128,6 +131,11 @@ class GeometryService {
|
|
|
128
131
|
return;
|
|
129
132
|
}
|
|
130
133
|
res.clientNeeds.set(index, false);
|
|
134
|
+
// If the node was actually sent to this client, the client must be
|
|
135
|
+
// told to destroy it via a RemoveNodes payload.
|
|
136
|
+
if (res.WasSentToClient(this.clientID)) {
|
|
137
|
+
this.removedNodesToSend.add(uid);
|
|
138
|
+
}
|
|
131
139
|
}
|
|
132
140
|
// Should certainly be in this set:
|
|
133
141
|
this.nodesToStreamEventually.delete(uid);
|
|
@@ -136,6 +144,14 @@ class GeometryService {
|
|
|
136
144
|
// TODO: now reduce the counts for all the dependent resources.
|
|
137
145
|
console.log("Unstreaming node ", uid," for client ", this.clientID);
|
|
138
146
|
}
|
|
147
|
+
//! Drain the set of nodes the client must destroy. Returns an array of uids;
|
|
148
|
+
//! the caller re-queues via UnstreamNode semantics if the send fails.
|
|
149
|
+
GetRemoveNodesToSend() {
|
|
150
|
+
if (this.removedNodesToSend.size == 0) return [];
|
|
151
|
+
var uids = Array.from(this.removedNodesToSend);
|
|
152
|
+
this.removedNodesToSend.clear();
|
|
153
|
+
return uids;
|
|
154
|
+
}
|
|
139
155
|
StreamOrUnstream(resourceMap, uid, diff) {
|
|
140
156
|
// exclude "undefined"
|
|
141
157
|
if (!uid) return;
|
|
@@ -113,6 +113,16 @@ class WebRtcConnection extends EventEmitter
|
|
|
113
113
|
}
|
|
114
114
|
reconnect()
|
|
115
115
|
{
|
|
116
|
+
// A browser peer that has already accepted an SDP with N m-sections will
|
|
117
|
+
// reject any later offer with fewer (RTCPeerConnection throws "New remote
|
|
118
|
+
// description has fewer m-sections than the previous remote description"),
|
|
119
|
+
// tearing down the whole connection. Recreating the PeerConnection below
|
|
120
|
+
// starts with zero transceivers, so remember which per-node audio sources
|
|
121
|
+
// were live beforehand and re-add matching ones on the new PeerConnection
|
|
122
|
+
// (below) before the next offer goes out, keeping the m-section count from
|
|
123
|
+
// shrinking relative to what the browser already negotiated.
|
|
124
|
+
const previousAudioSourceUids = this.getNodeAudioSourceUids();
|
|
125
|
+
|
|
116
126
|
// Close and clean up the previous PeerConnection before creating a new one.
|
|
117
127
|
// Failing to do so leaves the old ICE agent (and its UDP socket) alive, which
|
|
118
128
|
// causes it to keep sending/receiving STUN messages with stale credentials and
|
|
@@ -136,6 +146,8 @@ class WebRtcConnection extends EventEmitter
|
|
|
136
146
|
// the previous PeerConnection's lifetime suppresses UpdateStreaming kick.
|
|
137
147
|
this._dataChannelsOpenFired = false;
|
|
138
148
|
this.beforeOffer();
|
|
149
|
+
for (const uid of previousAudioSourceUids)
|
|
150
|
+
this.addNodeAudioSource(uid);
|
|
139
151
|
this.connectionTimer = this.options.setTimeout(() =>
|
|
140
152
|
{
|
|
141
153
|
if (this.peerConnection.iceConnectionState !== 'connected'
|
package/core/core.js
CHANGED
package/package.json
CHANGED
|
@@ -29,4 +29,23 @@ function encodeNode(node,buffer,fromAxes,toAxes)
|
|
|
29
29
|
dataView.setBigUint64(0,BigInt(byteOffset-8),core.endian);
|
|
30
30
|
return byteOffset;
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
|
|
33
|
+
// Encode a RemoveNodes payload: uint8 payload type, uint16 count, then count
|
|
34
|
+
// uint64 node uids (matches GeometryEncoder::encodeRemoveNodes on the C++ server
|
|
35
|
+
// and GeometryDecoder::decodeRemoveNodes on the client). Returns the size written.
|
|
36
|
+
function encodeRemoveNodes(node_uids,buffer)
|
|
37
|
+
{
|
|
38
|
+
var byteOffset=0;
|
|
39
|
+
const dataView = new DataView(buffer);
|
|
40
|
+
byteOffset=putPlaceholderSize(dataView);
|
|
41
|
+
|
|
42
|
+
byteOffset=core.put_uint8(dataView,byteOffset,core.GeometryPayloadType.RemoveNodes);
|
|
43
|
+
byteOffset=core.put_uint16(dataView,byteOffset,node_uids.length);
|
|
44
|
+
for (var i=0;i<node_uids.length;i++)
|
|
45
|
+
{
|
|
46
|
+
byteOffset=core.put_uint64(dataView,byteOffset,node_uids[i]);
|
|
47
|
+
}
|
|
48
|
+
dataView.setBigUint64(0,BigInt(byteOffset-8),core.endian);
|
|
49
|
+
return byteOffset;
|
|
50
|
+
}
|
|
51
|
+
module.exports= {encodeNode,encodeRemoveNodes};
|
package/scene/scene.js
CHANGED
|
@@ -52,6 +52,13 @@ class Scene {
|
|
|
52
52
|
this.nodes.set(uid, newNode);
|
|
53
53
|
return uid;
|
|
54
54
|
}
|
|
55
|
+
//! Remove a node from the scene (e.g. a session-scoped avatar node whose
|
|
56
|
+
//! owning client has disconnected). Returns true if the node was present.
|
|
57
|
+
//! Callers must UnstreamNode(uid) for any clients that received it, so they
|
|
58
|
+
//! are sent a RemoveNodes payload.
|
|
59
|
+
RemoveNode(uid) {
|
|
60
|
+
return this.nodes.delete(uid);
|
|
61
|
+
}
|
|
55
62
|
GetAllNodeUids() {
|
|
56
63
|
let node_uids = Array.from(this.nodes.keys());
|
|
57
64
|
return node_uids;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Tests for the RemoveNodes geometry payload: the encoder's byte layout must
|
|
3
|
+
// match the C++ wire format (GeometryEncoder::encodeRemoveNodes /
|
|
4
|
+
// GeometryDecoder::decodeRemoveNodes), and GeometryService.UnstreamNode must
|
|
5
|
+
// queue a removal only for nodes that were actually sent to the client.
|
|
6
|
+
|
|
7
|
+
const test = require('node:test');
|
|
8
|
+
const assert = require('node:assert');
|
|
9
|
+
const core = require('../core/core.js');
|
|
10
|
+
const { encodeRemoveNodes } = require('../protocol/encoders/node_encoder.js');
|
|
11
|
+
const { GeometryService } = require('../client/geometry_service');
|
|
12
|
+
|
|
13
|
+
test('encodeRemoveNodes produces size-prefixed payload matching the C++ wire format', () => {
|
|
14
|
+
const uids = [5n, 7n];
|
|
15
|
+
const buffer = new ArrayBuffer(8 + 1 + 2 + uids.length * 8);
|
|
16
|
+
const size = encodeRemoveNodes(uids, buffer);
|
|
17
|
+
assert.strictEqual(size, 8 + 1 + 2 + 16);
|
|
18
|
+
|
|
19
|
+
const dataView = new DataView(buffer);
|
|
20
|
+
// Size prefix: count of bytes following the size field.
|
|
21
|
+
assert.strictEqual(dataView.getBigUint64(0, core.endian), BigInt(1 + 2 + 16));
|
|
22
|
+
// Payload type.
|
|
23
|
+
assert.strictEqual(dataView.getUint8(8), core.GeometryPayloadType.RemoveNodes);
|
|
24
|
+
assert.strictEqual(core.GeometryPayloadType.RemoveNodes, 13,
|
|
25
|
+
'RemoveNodes must keep the C++ enum value (after MaterialPointer=12)');
|
|
26
|
+
// Count and uids.
|
|
27
|
+
assert.strictEqual(dataView.getUint16(9, core.endian), 2);
|
|
28
|
+
assert.strictEqual(dataView.getBigUint64(11, core.endian), 5n);
|
|
29
|
+
assert.strictEqual(dataView.getBigUint64(19, core.endian), 7n);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('encodeRemoveNodes handles a single uid', () => {
|
|
33
|
+
const buffer = new ArrayBuffer(8 + 1 + 2 + 8);
|
|
34
|
+
const size = encodeRemoveNodes([42n], buffer);
|
|
35
|
+
const dataView = new DataView(buffer);
|
|
36
|
+
assert.strictEqual(size, 19);
|
|
37
|
+
assert.strictEqual(dataView.getUint16(9, core.endian), 1);
|
|
38
|
+
assert.strictEqual(dataView.getBigUint64(11, core.endian), 42n);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function makeService(clientID) {
|
|
42
|
+
// Reset the static trackedResources map between tests so uids don't leak
|
|
43
|
+
// across cases (same pattern as test_geometry_service_send_gate.js).
|
|
44
|
+
GeometryService.trackedResources = new Map();
|
|
45
|
+
return new GeometryService(clientID);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
test('UnstreamNode queues a removal for a node that was sent to the client', () => {
|
|
49
|
+
const clientID = 201;
|
|
50
|
+
const svc = makeService(clientID);
|
|
51
|
+
svc.StreamNode(9n);
|
|
52
|
+
// Simulate a successful send.
|
|
53
|
+
GeometryService.GetOrCreateTrackedResource(9n).Sent(clientID, 123n);
|
|
54
|
+
|
|
55
|
+
svc.UnstreamNode(9n);
|
|
56
|
+
assert.deepStrictEqual(svc.GetRemoveNodesToSend(), [9n]);
|
|
57
|
+
// Drained: a second call returns nothing.
|
|
58
|
+
assert.deepStrictEqual(svc.GetRemoveNodesToSend(), []);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('UnstreamNode does not queue a removal for a node that was never sent', () => {
|
|
62
|
+
const svc = makeService(202);
|
|
63
|
+
svc.StreamNode(11n);
|
|
64
|
+
// Not sent (e.g. geometry channel never opened).
|
|
65
|
+
svc.UnstreamNode(11n);
|
|
66
|
+
assert.deepStrictEqual(svc.GetRemoveNodesToSend(), []);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('UnstreamNode on a node the client never needed is a no-op', () => {
|
|
70
|
+
const svc = makeService(203);
|
|
71
|
+
GeometryService.GetOrCreateTrackedResource(13n).Sent(203, 1n);
|
|
72
|
+
// StreamNode was never called, so clientNeeds is false: nothing to do.
|
|
73
|
+
svc.UnstreamNode(13n);
|
|
74
|
+
assert.deepStrictEqual(svc.GetRemoveNodesToSend(), []);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('UnstreamNode twice only queues one removal', () => {
|
|
78
|
+
const clientID = 204;
|
|
79
|
+
const svc = makeService(clientID);
|
|
80
|
+
svc.StreamNode(15n);
|
|
81
|
+
GeometryService.GetOrCreateTrackedResource(15n).Sent(clientID, 1n);
|
|
82
|
+
svc.UnstreamNode(15n);
|
|
83
|
+
svc.UnstreamNode(15n);
|
|
84
|
+
assert.deepStrictEqual(svc.GetRemoveNodesToSend(), [15n]);
|
|
85
|
+
});
|