teleportxr 1.0.93 → 1.0.94
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 +18 -0
- package/connections/webrtcconnection.js +98 -1
- package/core/core.js +39 -1
- package/package.json +1 -1
- package/protocol/command.js +45 -13
- package/scene/node.js +2 -1
package/client/client.js
CHANGED
|
@@ -67,6 +67,12 @@ class Client {
|
|
|
67
67
|
this.webRtcConnection=null;
|
|
68
68
|
this.currentOriginState=new OriginState();
|
|
69
69
|
this.currentLightingState=new LightingState();
|
|
70
|
+
// Latest head pose reported by the client (node.Pose), or null before the first
|
|
71
|
+
// NodePosesMessage. Used server-side for proximity-based audio selection.
|
|
72
|
+
this.currentHeadPose=null;
|
|
73
|
+
// When true, the SetupCommand tells the client to send its microphone track
|
|
74
|
+
// (needed for the audio SFU). Set by the host before Start(); default off.
|
|
75
|
+
this.acceptMicrophone=false;
|
|
70
76
|
this.next_ack_id=BigInt(1);
|
|
71
77
|
this.clientStartMs=Date.now();
|
|
72
78
|
this.webRtcConnectedAtMs=0;
|
|
@@ -211,6 +217,9 @@ class Client {
|
|
|
211
217
|
this.clientStartMs=Date.now();
|
|
212
218
|
console.log("[T+0ms] Client.Start() — sending SetupCommand for client "+this.clientID);
|
|
213
219
|
this.setupCommand=new command.SetupCommand();
|
|
220
|
+
// Accept the client's microphone media track when the host has enabled it
|
|
221
|
+
// (required for the audio SFU to receive and forward this client's voice).
|
|
222
|
+
this.setupCommand.uint8_audio_input_enabled = this.acceptMicrophone ? 1 : 0;
|
|
214
223
|
this.clientDynamicLighting=new core.ClientDynamicLighting();
|
|
215
224
|
// Session is (re)starting; the client has zero state, so retract any
|
|
216
225
|
// outstanding ack tracking from a previous session and force a resend.
|
|
@@ -385,6 +394,15 @@ class Client {
|
|
|
385
394
|
ProcessNodePoses(headPose,numPoses,nodePoses)
|
|
386
395
|
{
|
|
387
396
|
//console.log("Client: ProcessNodePoses ", numPoses, " poses.");
|
|
397
|
+
// Retain the head pose as this client's current world position, for
|
|
398
|
+
// proximity-based audio source selection (see src/mic-router.js).
|
|
399
|
+
if (headPose)
|
|
400
|
+
this.currentHeadPose = headPose;
|
|
401
|
+
}
|
|
402
|
+
// This client's current world position {x,y,z}, or null if not yet known.
|
|
403
|
+
GetHeadPosition()
|
|
404
|
+
{
|
|
405
|
+
return (this.currentHeadPose && this.currentHeadPose.position) ? this.currentHeadPose.position : null;
|
|
388
406
|
}
|
|
389
407
|
UpdateStreaming()
|
|
390
408
|
{
|
|
@@ -228,7 +228,12 @@ class WebRtcConnection extends EventEmitter
|
|
|
228
228
|
{
|
|
229
229
|
const offer = await pc.createOffer();
|
|
230
230
|
if (this.peerConnection !== pc) return;
|
|
231
|
-
|
|
231
|
+
// Rename each node-audio m-line's mid to the emitting node's uid
|
|
232
|
+
// (see docs/protocol/audio.rst) before it is committed, so the binding
|
|
233
|
+
// is intrinsic to the track. Munging pre-setLocalDescription is accepted
|
|
234
|
+
// by wrtc and reflected in pc.localDescription.
|
|
235
|
+
const mungedSdp = this._mungeAudioMids(offer.sdp);
|
|
236
|
+
await pc.setLocalDescription({ type: 'offer', sdp: mungedSdp });
|
|
232
237
|
if (this.peerConnection !== pc) return;
|
|
233
238
|
// Use pc.localDescription.sdp rather than the createOffer() result: the
|
|
234
239
|
// ICE ufrag/pwd in the SDP returned by createOffer() are provisional and
|
|
@@ -499,6 +504,98 @@ class WebRtcConnection extends EventEmitter
|
|
|
499
504
|
this._localAudioTrack = null;
|
|
500
505
|
}
|
|
501
506
|
this._audioSource = null;
|
|
507
|
+
if (this._nodeAudioSources) {
|
|
508
|
+
for (const v of this._nodeAudioSources.values()) {
|
|
509
|
+
try { v.track.stop(); } catch (e) {}
|
|
510
|
+
}
|
|
511
|
+
this._nodeAudioSources.clear();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// ── Per-node forwarded audio tracks ────────────────────────────────────────
|
|
515
|
+
// A node may emit audio (a participant's microphone, but equally an object,
|
|
516
|
+
// ambient source or announcer). Each such source is its own sendonly
|
|
517
|
+
// transceiver whose SDP mid is set to the emitting node's uid (see
|
|
518
|
+
// docs/protocol/audio.rst), so the client binds and spatialises it by node.
|
|
519
|
+
// The caller pushes that node's PCM into the returned RTCAudioSource. Call
|
|
520
|
+
// renegotiate() after adding/removing to re-offer.
|
|
521
|
+
// sourceNodeUid: BigInt|number|string. Returns the RTCAudioSource (or null).
|
|
522
|
+
addNodeAudioSource(sourceNodeUid) {
|
|
523
|
+
const uid = BigInt(sourceNodeUid);
|
|
524
|
+
if (!uid) return null;
|
|
525
|
+
if (!this._nodeAudioSources) this._nodeAudioSources = new Map();
|
|
526
|
+
const existing = this._nodeAudioSources.get(uid);
|
|
527
|
+
if (existing) return existing.audioSource;
|
|
528
|
+
if (!this.peerConnection) return null;
|
|
529
|
+
try {
|
|
530
|
+
const audioSource = new wrtc.nonstandard.RTCAudioSource();
|
|
531
|
+
const track = audioSource.createTrack();
|
|
532
|
+
const transceiver = this.peerConnection.addTransceiver(track, { direction: 'sendonly' });
|
|
533
|
+
this._nodeAudioSources.set(uid, { audioSource, track, transceiver, trackId: track.id });
|
|
534
|
+
return audioSource;
|
|
535
|
+
} catch (e) {
|
|
536
|
+
console.error('addNodeAudioSource failed for node ' + uid + ': ' + e.message);
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
removeNodeAudioSource(sourceNodeUid) {
|
|
541
|
+
const uid = BigInt(sourceNodeUid);
|
|
542
|
+
if (!this._nodeAudioSources) return;
|
|
543
|
+
const v = this._nodeAudioSources.get(uid);
|
|
544
|
+
if (!v) return;
|
|
545
|
+
try { v.track.stop(); } catch (e) {}
|
|
546
|
+
// The m-line cannot be removed, but going inactive/silent retires the source.
|
|
547
|
+
try { if (v.transceiver) v.transceiver.direction = 'inactive'; } catch (e) {}
|
|
548
|
+
this._nodeAudioSources.delete(uid);
|
|
549
|
+
}
|
|
550
|
+
// The node uids of the node-audio sources currently attached to this listener.
|
|
551
|
+
getNodeAudioSourceUids() {
|
|
552
|
+
if (!this._nodeAudioSources) return [];
|
|
553
|
+
return [...this._nodeAudioSources.keys()];
|
|
554
|
+
}
|
|
555
|
+
// Re-offer to apply added/removed node-audio tracks. No-op unless the connection
|
|
556
|
+
// is in a stable signalling state (a pending offer will be picked up next tick).
|
|
557
|
+
async renegotiate() {
|
|
558
|
+
const pc = this.peerConnection;
|
|
559
|
+
if (!pc || typeof pc.signalingState === 'string' && pc.signalingState !== 'stable') return;
|
|
560
|
+
await this.doOffer();
|
|
561
|
+
}
|
|
562
|
+
// Rewrite each node-audio m-line's mid to its emitting node uid, correlating
|
|
563
|
+
// m-line -> node by the track id carried in the m-line's a=msid, and repair the
|
|
564
|
+
// BUNDLE group. Idempotent: an m-line already carrying its node-uid mid is skipped.
|
|
565
|
+
_mungeAudioMids(sdp) {
|
|
566
|
+
if (!this._nodeAudioSources || this._nodeAudioSources.size === 0) return sdp;
|
|
567
|
+
const byTrack = new Map();
|
|
568
|
+
for (const [uid, v] of this._nodeAudioSources) byTrack.set(v.trackId, uid.toString());
|
|
569
|
+
const lines = sdp.split(/\r\n|\n/);
|
|
570
|
+
const renames = new Map(); // oldMid -> newMid
|
|
571
|
+
let i = 0;
|
|
572
|
+
while (i < lines.length) {
|
|
573
|
+
if (!lines[i].startsWith('m=')) { i++; continue; }
|
|
574
|
+
let j = i + 1, midIdx = -1, oldMid = null, trackId = null;
|
|
575
|
+
while (j < lines.length && !lines[j].startsWith('m=')) {
|
|
576
|
+
const l = lines[j];
|
|
577
|
+
if (l.startsWith('a=mid:')) { midIdx = j; oldMid = l.slice(6).trim(); }
|
|
578
|
+
else if (l.startsWith('a=msid:')) {
|
|
579
|
+
const parts = l.slice(7).trim().split(/\s+/); // "- <trackId>"
|
|
580
|
+
if (parts.length >= 2) trackId = parts[1];
|
|
581
|
+
}
|
|
582
|
+
j++;
|
|
583
|
+
}
|
|
584
|
+
if (midIdx >= 0 && trackId && byTrack.has(trackId)) {
|
|
585
|
+
const newMid = byTrack.get(trackId);
|
|
586
|
+
if (newMid !== oldMid) { lines[midIdx] = 'a=mid:' + newMid; renames.set(oldMid, newMid); }
|
|
587
|
+
}
|
|
588
|
+
i = j;
|
|
589
|
+
}
|
|
590
|
+
if (renames.size === 0) return sdp;
|
|
591
|
+
for (let k = 0; k < lines.length; k++) {
|
|
592
|
+
if (!lines[k].startsWith('a=group:BUNDLE')) continue;
|
|
593
|
+
const toks = lines[k].split(/\s+/);
|
|
594
|
+
for (let t = 1; t < toks.length; t++)
|
|
595
|
+
if (renames.has(toks[t])) toks[t] = renames.get(toks[t]);
|
|
596
|
+
lines[k] = toks.join(' ');
|
|
597
|
+
}
|
|
598
|
+
return lines.join('\r\n');
|
|
502
599
|
}
|
|
503
600
|
// Start streaming all SoundComponents in the given scene as a single mixed
|
|
504
601
|
// 48 kHz / mono / int16 PCM track via the outbound audio media track. Idempotent
|
package/core/core.js
CHANGED
|
@@ -32,6 +32,10 @@ function SizeOfType(member) {
|
|
|
32
32
|
return [16, "struct"];
|
|
33
33
|
case "uid":
|
|
34
34
|
return [8, "uint64"];
|
|
35
|
+
case "int16":
|
|
36
|
+
return [2, "int16"];
|
|
37
|
+
case "uint16":
|
|
38
|
+
return [2, "uint16"];
|
|
35
39
|
case "int32":
|
|
36
40
|
return [4, "int32"];
|
|
37
41
|
case "uint32":
|
|
@@ -42,6 +46,8 @@ function SizeOfType(member) {
|
|
|
42
46
|
return [8, "int64"];
|
|
43
47
|
case "VideoConfig":
|
|
44
48
|
return [89, "struct"];
|
|
49
|
+
case "AudioConfig":
|
|
50
|
+
return [17, "struct"];
|
|
45
51
|
case "VideoCodec":
|
|
46
52
|
case "AxesStandard":
|
|
47
53
|
case "GeometryPayloadType":
|
|
@@ -67,6 +73,12 @@ function encodeIntoDataView(obj, dataView, byteOffset) {
|
|
|
67
73
|
else if (tp == "int8") {
|
|
68
74
|
dataView.setInt8(byteOffset, value, endian);
|
|
69
75
|
}
|
|
76
|
+
else if (tp == "uint16") {
|
|
77
|
+
dataView.setUint16(byteOffset, value, endian);
|
|
78
|
+
}
|
|
79
|
+
else if (tp == "int16") {
|
|
80
|
+
dataView.setInt16(byteOffset, value, endian);
|
|
81
|
+
}
|
|
70
82
|
else if (tp == "uint32") {
|
|
71
83
|
dataView.setUint32(byteOffset, value, endian);
|
|
72
84
|
}
|
|
@@ -110,6 +122,14 @@ function decodeFromDataView(obj, dataView, byteOffset) {
|
|
|
110
122
|
var value = dataView.getInt8(byteOffset);
|
|
111
123
|
obj[key] = value;
|
|
112
124
|
}
|
|
125
|
+
else if (tp == "uint16") {
|
|
126
|
+
var value = dataView.getUint16(byteOffset, endian);
|
|
127
|
+
obj[key] = value;
|
|
128
|
+
}
|
|
129
|
+
else if (tp == "int16") {
|
|
130
|
+
var value = dataView.getInt16(byteOffset, endian);
|
|
131
|
+
obj[key] = value;
|
|
132
|
+
}
|
|
113
133
|
else if (tp == "uint32") {
|
|
114
134
|
var value = dataView.getUint32(byteOffset, endian);
|
|
115
135
|
obj[key] = value;
|
|
@@ -395,6 +415,24 @@ class VideoConfig {
|
|
|
395
415
|
}
|
|
396
416
|
}; // 89 bytes
|
|
397
417
|
|
|
418
|
+
//! Audio configuration carried inside SetupCommand (17 bytes).
|
|
419
|
+
//! See docs/protocol/audio.rst §AudioConfig for the full specification.
|
|
420
|
+
class AudioConfig {
|
|
421
|
+
constructor() {
|
|
422
|
+
this.uint8_codec = 1; //!< 0=disabled; 1=Opus
|
|
423
|
+
this.uint8_rtpPayloadType = 111;
|
|
424
|
+
this.uint32_sampleRateHz = 48000;
|
|
425
|
+
this.uint8_channelCount = 1;
|
|
426
|
+
this.uint8_frameDurationMs = 20;
|
|
427
|
+
this.uint8_flags = 3; //!< bit0=FEC, bit1=DTX
|
|
428
|
+
this.uint8_maxInboundStreams = 0;
|
|
429
|
+
this.uint8_selectionPolicy = 0; //!< 0=All
|
|
430
|
+
this.float32_proximityRadiusMetres = 0.0;
|
|
431
|
+
this.uint16_evictionGraceMs = 0;
|
|
432
|
+
}
|
|
433
|
+
static sizeof() { return 17; }
|
|
434
|
+
};
|
|
435
|
+
|
|
398
436
|
//! Setup for dynamically-lit objects on the client.
|
|
399
437
|
class ClientDynamicLighting {
|
|
400
438
|
constructor() {
|
|
@@ -568,7 +606,7 @@ module.exports = {
|
|
|
568
606
|
, vec4, BackgroundMode, AxesStandard, AxesStandardToCubemapSuffix
|
|
569
607
|
, ConvertPosition, ConvertRotation, ConvertScale, ConvertPose
|
|
570
608
|
, GeometryPayloadType, DisplayInfo, RenderingFeatures, LightingMode, VideoCodec
|
|
571
|
-
, VideoConfig, ClientDynamicLighting, encodeToUint8Array, decodeFromUint8Array
|
|
609
|
+
, VideoConfig, AudioConfig, ClientDynamicLighting, encodeToUint8Array, decodeFromUint8Array
|
|
572
610
|
, generateUid, getStartTimeUnixUs, getTimestampUs,
|
|
573
611
|
unixTimeToUTCString, put_float32, put_uint16, put_int32, put_uint32
|
|
574
612
|
, put_uint64, put_uint8, put_vec2, put_vec3, put_vec4, put_string
|
package/package.json
CHANGED
package/protocol/command.js
CHANGED
|
@@ -21,7 +21,9 @@ const CommandPayloadType =
|
|
|
21
21
|
AssignNodePosePath:14,
|
|
22
22
|
SetupInputs:15,
|
|
23
23
|
PingForLatency:16,
|
|
24
|
-
|
|
24
|
+
AudioSourceMapping:17,
|
|
25
|
+
AudioParticipantStateChange:18,
|
|
26
|
+
SetOriginNode:128
|
|
25
27
|
};
|
|
26
28
|
class Command{
|
|
27
29
|
constructor(){
|
|
@@ -44,19 +46,20 @@ class SetupCommand extends Command
|
|
|
44
46
|
this.int32_requiredLatencyMs = 0; //!< 9+4=13
|
|
45
47
|
this.uint32_idle_connection_timeout = 5000.0; //!< 13+4=17
|
|
46
48
|
this.uint64_session_id = BigInt.asUintN(64, BigInt(0)); //!< 17+8=25 The server's session id changes when the server session changes. 37 bytes
|
|
47
|
-
this.VideoConfig_video_config=new core.VideoConfig(); //!< 25+89=114 Video setup structure.
|
|
48
|
-
this.
|
|
49
|
-
this.
|
|
50
|
-
this.
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
49
|
+
this.VideoConfig_video_config=new core.VideoConfig(); //!< 25+89=114 Video setup structure.
|
|
50
|
+
this.AudioConfig_audio_config=new core.AudioConfig(); //!< 114+17=131 Audio media-track config.
|
|
51
|
+
this.float32_draw_distance = 0.0; //!< 131+4=135 Maximum distance in metres to render locally.
|
|
52
|
+
this.AxesStandard_axesStandard = core.AxesStandard.NotInitialized; //!< 135+1=136 The axis standard that the server uses.
|
|
53
|
+
this.uint8_audio_input_enabled = 0; //!< 136+1=137 Server accepts a microphone media track from the client.
|
|
54
|
+
this.bool_using_ssl = true; //!< 137+1=138 Not in use, for later.
|
|
55
|
+
this.int64_startTimestamp_utc_unix_us = BigInt.asUintN(64, BigInt(0)); //!< 138+8=146 UTC Unix Timestamp in microseconds when the server session began.
|
|
56
|
+
// TODO: replace this with a background Material, which MAY contain video, texture and/or plain colours.
|
|
57
|
+
this.BackgroundMode_backgroundMode=core.BackgroundMode.COLOUR; //!< 146+1=147 Whether the server supplies a background, and of which type.
|
|
58
|
+
this.vec4_backgroundColour=new core.vec4(); //!< 147+16=163 If the background is of the COLOUR type, which colour to use.
|
|
59
|
+
this.uid_backgroundTexture=BigInt(0); //!< 163+8=171
|
|
57
60
|
}
|
|
58
61
|
static sizeof(){
|
|
59
|
-
return
|
|
62
|
+
return 171;
|
|
60
63
|
}
|
|
61
64
|
size(){
|
|
62
65
|
return SetupCommand.sizeof();
|
|
@@ -143,4 +146,33 @@ class SetLightingCommand extends AckedCommand
|
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
|
|
146
|
-
|
|
149
|
+
//! Sent from server to client when the set of audio tracks delivered to a client changes.
|
|
150
|
+
//! The fixed-size header is followed on the wire by addedCount AddedEntry records
|
|
151
|
+
//! (uint8 midLen + midLen UTF-8 bytes + uint64 sourceClientUid) then removedCount
|
|
152
|
+
//! RemovedEntry records (uint8 midLen + midLen UTF-8 bytes).
|
|
153
|
+
class AudioSourceMappingCommand extends Command
|
|
154
|
+
{
|
|
155
|
+
constructor(){
|
|
156
|
+
super();
|
|
157
|
+
this.CommandPayloadType_commandPayloadType = CommandPayloadType.AudioSourceMapping;
|
|
158
|
+
this.uint16_addedCount = 0;
|
|
159
|
+
this.uint16_removedCount = 0;
|
|
160
|
+
}
|
|
161
|
+
static sizeof() { return 5; } // 1 tag + 2 addedCount + 2 removedCount
|
|
162
|
+
size() { return AudioSourceMappingCommand.sizeof(); }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
//! Sent from server to client to report user-visible audio state changes for participants.
|
|
166
|
+
//! Followed by updateCount × 10-byte Update records (uint64 sourceClientUid + uint8 state + uint8 reason).
|
|
167
|
+
class AudioParticipantStateChangeCommand extends Command
|
|
168
|
+
{
|
|
169
|
+
constructor(){
|
|
170
|
+
super();
|
|
171
|
+
this.CommandPayloadType_commandPayloadType = CommandPayloadType.AudioParticipantStateChange;
|
|
172
|
+
this.uint16_updateCount = 0;
|
|
173
|
+
}
|
|
174
|
+
static sizeof() { return 3; } // 1 tag + 2 updateCount
|
|
175
|
+
size() { return AudioParticipantStateChangeCommand.sizeof(); }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports= {Command,CommandPayloadType,SetupCommand,AcknowledgeHandshakeCommand,SetOriginNodeCommand,SetLightingCommand,AudioSourceMappingCommand,AudioParticipantStateChangeCommand};
|
package/scene/node.js
CHANGED