teleportxr 1.0.92 → 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 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.
@@ -228,6 +237,9 @@ class Client {
228
237
  this.setupCommand.int64_startTimestamp_utc_unix_us = BigInt(core.getStartTimeUnixUs());
229
238
  if(this.scene)
230
239
  {
240
+ // Tell the client which axes standard the server authored the scene in. Node transforms
241
+ // are converted to the client's own standard at encode time (see SendNode).
242
+ this.setupCommand.AxesStandard_axesStandard=this.scene.serverAxesStandard;
231
243
  if(this.scene.backgroundTexturePath&&this.scene.backgroundTexturePath!="")
232
244
  {
233
245
  this.setupCommand.BackgroundMode_backgroundMode=BackgroundMode.TEXTURE;
@@ -382,6 +394,15 @@ class Client {
382
394
  ProcessNodePoses(headPose,numPoses,nodePoses)
383
395
  {
384
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;
385
406
  }
386
407
  UpdateStreaming()
387
408
  {
@@ -550,7 +571,9 @@ class Client {
550
571
  var node=this.scene.GetNode(uid);
551
572
  const MAX_NODE_SIZE=500;
552
573
  const buffer = new ArrayBuffer(MAX_NODE_SIZE);
553
- const nodeSize=node_encoder.encodeNode(node,buffer);
574
+ // Convert the node's transform from the server's axes standard to this client's, exactly as
575
+ // the C++ server does in GeometryEncoder::encodeNodes (ConvertTransform server->client).
576
+ const nodeSize=node_encoder.encodeNode(node,buffer,this.scene.serverAxesStandard,this.clientAxesStandard);
554
577
  const view2 = new DataView(buffer, 0, nodeSize);
555
578
  if(!this.webRtcConnection)
556
579
  {
@@ -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
- await pc.setLocalDescription(offer);
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;
@@ -200,6 +220,111 @@ function AxesStandardToCubemapSuffix(axesStandard)
200
220
  }
201
221
  }
202
222
 
223
+ // ---- Axes-standard conversions for object transforms ----
224
+ // Ported from the C++ server (libavstream common_maths.h ConvertPosition/Rotation/Scale), which
225
+ // converts each node's transform from the server's axes standard to the client's during encoding.
226
+ // The C++ table only covers Unreal/Unity <-> Gl/Engineering (its server is engine-driven), so the
227
+ // Engineering <-> GlStyle cases below are added here (both right-handed: position/quat vector part
228
+ // maps as (x, z, -y); its inverse as (x, -z, y); scale permutes y/z).
229
+ function ConvertPosition(from, to, p)
230
+ {
231
+ const A = AxesStandard;
232
+ if (from === to) return { x: p.x, y: p.y, z: p.z };
233
+ if (from === A.UnrealStyle)
234
+ {
235
+ if (to === A.GlStyle) return { x: +p.y, y: +p.z, z: -p.x };
236
+ if (to === A.EngineeringStyle) return { x: p.y, y: p.x, z: p.z };
237
+ }
238
+ else if (from === A.UnityStyle)
239
+ {
240
+ if (to === A.GlStyle) return { x: p.x, y: p.y, z: -p.z };
241
+ if (to === A.EngineeringStyle) return { x: p.x, y: p.z, z: p.y };
242
+ }
243
+ else if (from === A.EngineeringStyle)
244
+ {
245
+ if (to === A.UnrealStyle) return { x: p.y, y: p.x, z: p.z };
246
+ if (to === A.UnityStyle) return { x: p.x, y: p.z, z: p.y };
247
+ if (to === A.GlStyle) return { x: p.x, y: p.z, z: -p.y }; // added
248
+ }
249
+ else if (from === A.GlStyle)
250
+ {
251
+ if (to === A.UnrealStyle) return { x: -p.z, y: +p.x, z: +p.y };
252
+ if (to === A.UnityStyle) return { x: p.x, y: p.y, z: -p.z };
253
+ if (to === A.EngineeringStyle) return { x: p.x, y: -p.z, z: p.y }; // added
254
+ }
255
+ console.warn("ConvertPosition: unsupported axes "+from+"->"+to+"; leaving unchanged");
256
+ return { x: p.x, y: p.y, z: p.z };
257
+ }
258
+
259
+ function ConvertRotation(from, to, q)
260
+ {
261
+ const A = AxesStandard;
262
+ if (from === to) return { x: q.x, y: q.y, z: q.z, w: q.w };
263
+ if (from === A.UnrealStyle)
264
+ {
265
+ if (to === A.GlStyle) return { x: -q.y, y: -q.z, z: +q.x, w: q.w };
266
+ if (to === A.EngineeringStyle) return { x: -q.y, y: -q.x, z: -q.z, w: q.w };
267
+ }
268
+ else if (from === A.EngineeringStyle)
269
+ {
270
+ if (to === A.UnrealStyle) return { x: -q.y, y: -q.x, z: -q.z, w: q.w };
271
+ if (to === A.UnityStyle) return { x: -q.x, y: -q.z, z: -q.y, w: q.w };
272
+ if (to === A.GlStyle) return { x: q.x, y: q.z, z: -q.y, w: q.w }; // added
273
+ }
274
+ else if (from === A.GlStyle)
275
+ {
276
+ if (to === A.UnrealStyle) return { x: +q.z, y: -q.x, z: -q.y, w: q.w };
277
+ if (to === A.UnityStyle) return { x: -q.x, y: -q.y, z: q.z, w: q.w };
278
+ if (to === A.EngineeringStyle) return { x: q.x, y: -q.z, z: q.y, w: q.w }; // added
279
+ }
280
+ else if (from === A.UnityStyle)
281
+ {
282
+ if (to === A.GlStyle) return { x: -q.x, y: -q.y, z: q.z, w: q.w };
283
+ if (to === A.EngineeringStyle) return { x: -q.x, y: -q.z, z: -q.y, w: q.w };
284
+ }
285
+ console.warn("ConvertRotation: unsupported axes "+from+"->"+to+"; leaving unchanged");
286
+ return { x: q.x, y: q.y, z: q.z, w: q.w };
287
+ }
288
+
289
+ function ConvertScale(from, to, s)
290
+ {
291
+ const A = AxesStandard;
292
+ if (from === to) return { x: s.x, y: s.y, z: s.z };
293
+ if (from === A.UnrealStyle)
294
+ {
295
+ if (to === A.GlStyle) return { x: +s.y, y: +s.z, z: s.x };
296
+ if (to === A.EngineeringStyle) return { x: s.y, y: s.x, z: s.z };
297
+ }
298
+ else if (from === A.UnityStyle)
299
+ {
300
+ if (to === A.GlStyle) return { x: s.x, y: s.y, z: s.z };
301
+ if (to === A.EngineeringStyle) return { x: s.x, y: s.z, z: s.y };
302
+ }
303
+ else if (from === A.EngineeringStyle)
304
+ {
305
+ if (to === A.UnrealStyle) return { x: s.y, y: s.x, z: s.z };
306
+ if (to === A.UnityStyle) return { x: s.x, y: s.z, z: s.y };
307
+ if (to === A.GlStyle) return { x: s.x, y: s.z, z: s.y }; // added (abs of position map)
308
+ }
309
+ else if (from === A.GlStyle)
310
+ {
311
+ if (to === A.UnrealStyle) return { x: s.z, y: +s.x, z: +s.y };
312
+ if (to === A.UnityStyle) return { x: s.x, y: s.y, z: s.z };
313
+ if (to === A.EngineeringStyle) return { x: s.x, y: s.z, z: s.y }; // added
314
+ }
315
+ return { x: s.x, y: s.y, z: s.z };
316
+ }
317
+
318
+ //! Convert a {position, orientation, scale} pose from one axes standard to another.
319
+ function ConvertPose(from, to, pose)
320
+ {
321
+ return {
322
+ position: ConvertPosition(from, to, pose.position),
323
+ orientation: ConvertRotation(from, to, pose.orientation),
324
+ scale: ConvertScale(from, to, pose.scale || { x: 1, y: 1, z: 1 }),
325
+ };
326
+ }
327
+
203
328
  const GeometryPayloadType =
204
329
  {
205
330
  Invalid: 0,
@@ -290,6 +415,24 @@ class VideoConfig {
290
415
  }
291
416
  }; // 89 bytes
292
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
+
293
436
  //! Setup for dynamically-lit objects on the client.
294
437
  class ClientDynamicLighting {
295
438
  constructor() {
@@ -460,8 +603,10 @@ function put_string(dataView, byteOffset, name) {
460
603
 
461
604
  module.exports = {
462
605
  UID_SIZE, endian, SizeOfType, encodeIntoDataView, decodeFromDataView
463
- , vec4, BackgroundMode, AxesStandard, AxesStandardToCubemapSuffix, GeometryPayloadType, DisplayInfo, RenderingFeatures, LightingMode, VideoCodec
464
- , VideoConfig, ClientDynamicLighting, encodeToUint8Array, decodeFromUint8Array
606
+ , vec4, BackgroundMode, AxesStandard, AxesStandardToCubemapSuffix
607
+ , ConvertPosition, ConvertRotation, ConvertScale, ConvertPose
608
+ , GeometryPayloadType, DisplayInfo, RenderingFeatures, LightingMode, VideoCodec
609
+ , VideoConfig, AudioConfig, ClientDynamicLighting, encodeToUint8Array, decodeFromUint8Array
465
610
  , generateUid, getStartTimeUnixUs, getTimestampUs,
466
611
  unixTimeToUTCString, put_float32, put_uint16, put_int32, put_uint32
467
612
  , put_uint64, put_uint8, put_vec2, put_vec3, put_vec4, put_string
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.92",
19
+ "version": "1.0.94",
20
20
  "repository": {
21
21
  "type": "git",
22
22
  "url": "git+https://github.com/simul/teleport-nodejs.git"
@@ -21,7 +21,9 @@ const CommandPayloadType =
21
21
  AssignNodePosePath:14,
22
22
  SetupInputs:15,
23
23
  PingForLatency:16,
24
- SetOriginNode:128
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. 41+89=130 bytes
48
- this.float32_draw_distance = 0.0; //!< 114+4=118 Maximum distance in metres to render locally. 134
49
- this.AxesStandard_axesStandard = core.AxesStandard.NotInitialized; //!< 118+1=119 The axis standard that the server uses, may be different from the client's. 147
50
- this.uint8_audio_input_enabled = 0; //!< 119+1=120 Server accepts audio stream from client.
51
- this.bool_using_ssl = true; //!< 120+1=121 Not in use, for later.
52
- this.int64_startTimestamp_utc_unix_us = BigInt.asUintN(64, BigInt(0)); //!< 121+8=129 UTC Unix Timestamp in microseconds when the server session began.
53
- // TODO: replace this with a background Material, which MAY contain video, te xture and/or plain colours.
54
- this.BackgroundMode_backgroundMode=core.BackgroundMode.COLOUR; //!< 129+1=130 Whether the server supplies a background, and of which type.
55
- this.vec4_backgroundColour=new core.vec4(); //!< 130+16=146 If the background is of the COLOUR type, which colour to use.
56
- this.uid_backgroundTexture=BigInt(0);
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 154;
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
- module.exports= {Command,CommandPayloadType,SetupCommand,AcknowledgeHandshakeCommand,SetOriginNodeCommand,SetLightingCommand};
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};
@@ -13,13 +13,15 @@ function putPlaceholderSize(dataView)
13
13
  }
14
14
 
15
15
  // return the size of the encoded node.
16
- function encodeNode(node,buffer)
16
+ // fromAxes/toAxes, when supplied, convert the node's transform from the server's axes standard to
17
+ // the client's during encoding (matches the C++ server). Omit them to encode the pose unchanged.
18
+ function encodeNode(node,buffer,fromAxes,toAxes)
17
19
  {
18
20
  var byteOffset=0;
19
- const dataView = new DataView(buffer);
21
+ const dataView = new DataView(buffer);
20
22
  byteOffset=putPlaceholderSize(dataView);
21
23
 
22
- var t=node.encodeIntoDataView(dataView,byteOffset);
24
+ var t=node.encodeIntoDataView(dataView,byteOffset,fromAxes,toAxes);
23
25
  byteOffset=t;
24
26
  // Actual size is now known: write the count of bytes that follow the
25
27
  // size field, in little-endian (the protocol convention). Without the
package/scene/node.js CHANGED
@@ -12,7 +12,8 @@ const NodeDataType =
12
12
  SubScene:5,
13
13
  Skeleton:6,
14
14
  Link:7,
15
- Script:8
15
+ Script:8,
16
+ AudioEmitter:9 // Reserved. Audio is bound via the track SDP mid = emitting node uid; not carried in the node payload.
16
17
  };
17
18
 
18
19
  class Pose
@@ -308,7 +309,7 @@ class Node {
308
309
  );
309
310
  this.components.push(tc);
310
311
  }
311
- encodeIntoDataView(dataView, byteOffset) {
312
+ encodeIntoDataView(dataView, byteOffset, fromAxes, toAxes) {
312
313
  byteOffset = core.put_uint8(
313
314
  dataView,
314
315
  byteOffset,
@@ -317,8 +318,19 @@ class Node {
317
318
 
318
319
  byteOffset = core.put_uint64(dataView, byteOffset, this.uid);
319
320
  byteOffset = core.put_string(dataView, byteOffset, this.name);
321
+ // Convert the node's transform into the client's axes standard (server -> client). When no
322
+ // conversion is requested, or the standards match, the pose is encoded unchanged.
320
323
  var clientsidePose = this.pose;
321
-
324
+ if (fromAxes !== undefined && toAxes !== undefined && fromAxes !== toAxes &&
325
+ toAxes !== core.AxesStandard.NotInitialized)
326
+ {
327
+ const c = core.ConvertPose(fromAxes, toAxes, this.pose);
328
+ clientsidePose = new Pose();
329
+ clientsidePose.position = c.position;
330
+ clientsidePose.orientation = c.orientation;
331
+ clientsidePose.scale = c.scale;
332
+ }
333
+
322
334
  byteOffset = clientsidePose.encodeIntoDataView(dataView, byteOffset);
323
335
 
324
336
  byteOffset = core.put_uint8(dataView, byteOffset, this.stationary);
package/scene/scene.js CHANGED
@@ -15,6 +15,10 @@ class Scene {
15
15
  this.specularCubemapPath="";
16
16
  this.assetsPath="assets";
17
17
  this.publicPath="http_resources";
18
+ // The axes standard the scene (node poses, cubemaps) is authored in. Node transforms are
19
+ // converted from this to each client's axes standard when streamed. Z-up Engineering by
20
+ // default, matching the C++ client's native frame and the *_eng cubemap source.
21
+ this.serverAxesStandard=core.AxesStandard.EngineeringStyle;
18
22
  }
19
23
  GetOrCreateNode(uid) {
20
24
  if (!this.nodes.has(uid)) {