teleportxr 1.0.83 → 1.0.84

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.
@@ -67,6 +67,10 @@ class WebRtcConnection extends EventEmitter
67
67
  {
68
68
  console.log("ICE candidate error: "+event.errorCode+" "+event.errorText+" "+event.port+" "+event.url);
69
69
  };
70
+ this._onTrack = this._handleTrack.bind(this);
71
+ this._audioSource = null;
72
+ this._localAudioTrack = null;
73
+ this._audioSink = null;
70
74
 
71
75
 
72
76
  Object.defineProperties(this, {
@@ -115,8 +119,10 @@ class WebRtcConnection extends EventEmitter
115
119
  this.peerConnection.removeEventListener('icegatheringstatechange', this._onIceGatheringStateChange);
116
120
  this.peerConnection.removeEventListener("icecandidateerror", this._onIceCandidateError);
117
121
  this.peerConnection.removeEventListener("connectionstatechange", this._onConnectionStateChange);
122
+ this.peerConnection.removeEventListener('track', this._onTrack);
118
123
  if (this.onIceCandidate)
119
124
  this.peerConnection.removeEventListener('icecandidate', this.onIceCandidate);
125
+ this._teardownAudioMediaTrack();
120
126
  try { this.peerConnection.close(); } catch (e) {}
121
127
  this.peerConnection = null;
122
128
  }
@@ -157,6 +163,7 @@ class WebRtcConnection extends EventEmitter
157
163
  this.peerConnection.addEventListener('icegatheringstatechange', this._onIceGatheringStateChange);
158
164
  this.peerConnection.addEventListener("icecandidateerror", this._onIceCandidateError);
159
165
  this.peerConnection.addEventListener("connectionstatechange", this._onConnectionStateChange);
166
+ this.peerConnection.addEventListener('track', this._onTrack);
160
167
  // Attach the icecandidate listener here, before doOffer can call
161
168
  // setLocalDescription(). libwebrtc starts the ICE agent inside
162
169
  // setLocalDescription() and emits host candidates synchronously on
@@ -329,11 +336,13 @@ class WebRtcConnection extends EventEmitter
329
336
  this.peerConnection.removeEventListener('icegatheringstatechange', this._onIceGatheringStateChange);
330
337
  this.peerConnection.removeEventListener("icecandidateerror", this._onIceCandidateError);
331
338
  this.peerConnection.removeEventListener("connectionstatechange", this._onConnectionStateChange);
339
+ this.peerConnection.removeEventListener('track', this._onTrack);
332
340
  if (this.onIceCandidate)
333
341
  {
334
342
  this.peerConnection.removeEventListener('icecandidate', this.onIceCandidate);
335
343
  }
336
344
  }
345
+ this._teardownAudioMediaTrack();
337
346
  if (this.connectionTimer)
338
347
  {
339
348
  this.options.clearTimeout(this.connectionTimer);
@@ -415,20 +424,22 @@ class WebRtcConnection extends EventEmitter
415
424
  }
416
425
  }
417
426
  beforeOffer() {
418
-
427
+
419
428
  this.videoDataChannel = this.createDataChannel("video",20);
420
429
  this.tagDataChannel = this.createDataChannel("video_tags",40);
421
430
  this.audioToClientDataChannel = this.createDataChannel("audio_server_to_client",60);
422
431
  this.geometryDataChannel = this.createDataChannel("geometry_unframed",80);
423
432
  this.reliableDataChannel = this.createDataChannel("reliable",100);
424
433
  this.unreliableDataChannel = this.createDataChannel("unreliable",120,false);
425
-
434
+
435
+ this._setupAudioMediaTrack();
436
+
426
437
  function onMessage({ data }) {
427
438
  if (data === 'ping') {
428
439
  //dataChannel.send('pong');
429
440
  }
430
441
  }
431
-
442
+
432
443
  // NOTE(mroberts): This is a hack so that we can get a callback when the
433
444
  // RTCPeerConnection is closed. In the future, we can subscribe to
434
445
  // "connectionstatechange" events.
@@ -439,6 +450,52 @@ class WebRtcConnection extends EventEmitter
439
450
  return close.apply(this, arguments);
440
451
  };
441
452
  }
453
+ // Adds one sendrecv audio transceiver per the protocol (see docs/protocol/audio.rst).
454
+ // Our outbound track is an RTCAudioSource so a future SFU can push selected peer
455
+ // PCM into it; incoming RTP is unwrapped via RTCAudioSink and surfaced as the
456
+ // 'micFrame' event. If options.audioEchoTest is set, received frames are looped
457
+ // straight back to the source, which lets a single browser tab self-test the path.
458
+ _setupAudioMediaTrack() {
459
+ try {
460
+ this._audioSource = new wrtc.nonstandard.RTCAudioSource();
461
+ this._localAudioTrack = this._audioSource.createTrack();
462
+ this.peerConnection.addTransceiver(this._localAudioTrack, { direction: 'sendrecv' });
463
+ } catch (e) {
464
+ console.error('_setupAudioMediaTrack failed: '+e.message);
465
+ this._teardownAudioMediaTrack();
466
+ }
467
+ }
468
+ _handleTrack(event) {
469
+ if (!event || !event.track || event.track.kind !== 'audio')
470
+ return;
471
+ if (this._audioSink) {
472
+ try { this._audioSink.stop(); } catch (e) {}
473
+ this._audioSink = null;
474
+ }
475
+ try {
476
+ this._audioSink = new wrtc.nonstandard.RTCAudioSink(event.track);
477
+ } catch (e) {
478
+ console.error('RTCAudioSink construction failed: '+e.message);
479
+ return;
480
+ }
481
+ this._audioSink.ondata = (data) => {
482
+ this.emit('micFrame', this.id, data);
483
+ if (this.options && this.options.audioEchoTest && this._audioSource) {
484
+ try { this._audioSource.onData(data); } catch (e) {}
485
+ }
486
+ };
487
+ }
488
+ _teardownAudioMediaTrack() {
489
+ if (this._audioSink) {
490
+ try { this._audioSink.stop(); } catch (e) {}
491
+ this._audioSink = null;
492
+ }
493
+ if (this._localAudioTrack) {
494
+ try { this._localAudioTrack.stop(); } catch (e) {}
495
+ this._localAudioTrack = null;
496
+ }
497
+ this._audioSource = null;
498
+ }
442
499
 
443
500
  receiveMessage(id,event)
444
501
  {
@@ -91,6 +91,10 @@ class WebRtcConnectionManager
91
91
  if (policy === 'all' || policy === 'relay')
92
92
  this.options.iceTransportPolicy=policy;
93
93
  }
94
+ SetAudioEchoTest(enabled)
95
+ {
96
+ this.options.audioEchoTest = !!enabled;
97
+ }
94
98
  }
95
99
 
96
100
  WebRtcConnectionManager.create = function create (options)
package/index.js CHANGED
@@ -29,6 +29,8 @@ function initServer(signaling_port, options) {
29
29
  webRtcConnectionManager.SetIceServers(options.iceServers);
30
30
  if (options && options.iceTransportPolicy)
31
31
  webRtcConnectionManager.SetIceTransportPolicy(options.iceTransportPolicy);
32
+ if (options && typeof options.audioEchoTest !== 'undefined')
33
+ webRtcConnectionManager.SetAudioEchoTest(options.audioEchoTest);
32
34
  return signaling.init(serverID, webRtcConnectionManager,cm.newClient.bind(cm),cm.disconnectClient.bind(cm),signaling_port);
33
35
  }
34
36
 
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.83",
19
+ "version": "1.0.84",
20
20
  "repository": {
21
21
  "type": "git",
22
22
  "url": "git+https://github.com/simul/teleport-nodejs.git"
@@ -0,0 +1,129 @@
1
+ const sharp = require('sharp');
2
+ const { parseKtx, buildKtx } = require('ktx-parse'); // install ktx-parse
3
+ const os = require('os');
4
+
5
+ const FACE_NAMES = ['+X','-X','+Y','-Y','+Z','-Z'];
6
+ const CONVENTIONS = {
7
+ Engineering: { upAxis: 'Z', handed: 'R' },
8
+ OpenGL: { upAxis: 'Y', handed: 'R' },
9
+ Unreal: { upAxis: 'Z', handed: 'L' },
10
+ Unity: { upAxis: 'Y', handed: 'L' }
11
+ };
12
+
13
+ // ... (include basisForConvention, mat3MulVec, mat3Transpose, dirToFaceUV, etc.)
14
+ // Use same helper implementations from previous file (omitted here for brevity).
15
+ // Paste the functions: basisForConvention, mat3MulVec, mat3Transpose, dirToFaceUV,
16
+ // mat3Transpose, convertVectorBetweenConventions, sampleFaceNearest, readFaces, FACE_NAMES, CONVENTIONS, etc.
17
+
18
+ // ---------- KTX I/O helpers ----------
19
+
20
+ // parseKtx(buffer) returns an object describing levels/images; we expect cubemap with 6 faces.
21
+ // This helper extracts the first mip level and returns an object:
22
+ // { faces: Map faceName-> { w,h,data }, faceOrder: array of face names in input file order }
23
+ async function readKtx(buffer) {
24
+ const k = parseKtx(buffer); // throws on invalid
25
+ // ktx-parse exposes ktx.header, ktx.images: array of levels; for cubemap, images[0] is array of 6 faces
26
+ if (!k.header || !k.images || k.images.length === 0) throw new Error('Invalid KTX file');
27
+ const level0 = k.images[0];
28
+ // level0 should be an array of 6 face objects (each with pixelData Uint8Array)
29
+ if (!Array.isArray(level0) || level0.length < 6) throw new Error('Not a cubemap or missing faces');
30
+ // Determine width/height from header
31
+ const w = k.header.pixelWidth;
32
+ const h = k.header.pixelHeight;
33
+ if (w !== h) throw new Error('Non-square faces not supported');
34
+ // ktx-parse gives faces in the order stored in file. We'll assume canonical order +X,-X,+Y,-Y,+Z,-Z
35
+ // If the file stores in a different order, user must provide ordering; many KTX cubemaps follow canonical.
36
+ const faces = {};
37
+ for (let i = 0; i < 6; i++) {
38
+ const img = level0[i];
39
+ // img.pixelData is Uint8Array in the file format's pixel encoding; parseKtx should give raw RGBA for uncompressed
40
+ let data = img.pixelData;
41
+ // If data is not RGBA raw, use sharp to decode buffer (e.g., PNG inside KTX) — handle common cases:
42
+ if (!(data instanceof Uint8Array) || data.length !== w * h * 4) {
43
+ // try to decode with sharp (supports PNG/JPEG inside)
44
+ const decoded = await sharp(Buffer.from(data)).ensureAlpha().raw().toBuffer();
45
+ data = decoded;
46
+ } else {
47
+ data = Buffer.from(data);
48
+ }
49
+ faces[FACE_NAMES[i]] = { w, h, data, channels: 4 };
50
+ }
51
+ return { faces, faceOrder: FACE_NAMES.slice() };
52
+ }
53
+
54
+ // Build a KTX2 buffer from 6 RGBA face Buffers using ktx-parse builder.
55
+ // options: {isKTX2: true/false, format: 'RGBA8' }
56
+ function writeKtx(faceBuffersMap, faceOrder = FACE_NAMES, options = { isKTX2: true }) {
57
+ // build images array: single level with 6 faces in the specified order
58
+ const facesArray = faceOrder.map(fn => {
59
+ const f = faceBuffersMap[fn];
60
+ return { pixelData: f.data, width: f.w, height: f.h };
61
+ });
62
+ const ktx = buildKtx({
63
+ pixelWidth: facesArray[0].width,
64
+ pixelHeight: facesArray[0].height,
65
+ images: [facesArray],
66
+ isKTX2: options.isKTX2,
67
+ // choose uncompressed RGBA8 format
68
+ vkFormat: 'VK_FORMAT_R8G8B8A8_UNORM'
69
+ });
70
+ return Buffer.from(ktx);
71
+ }
72
+
73
+ // ---------- Core conversion using raw face maps ----------
74
+
75
+ async function convertCubemapFromFaceMap(faceMap, srcConvention, dstConvention) {
76
+ // faceMap: faceName-> {w,h,data}
77
+ const srcBasis = basisForConvention(CONVENTIONS[srcConvention]);
78
+ const dstBasis = basisForConvention(CONVENTIONS[dstConvention]);
79
+ const faceSize = faceMap[FACE_NAMES[0]].w;
80
+ const dstRawFaces = {};
81
+ for (const faceName of FACE_NAMES) {
82
+ const w = faceSize, h = faceSize;
83
+ const out = Buffer.alloc(w*h*4);
84
+ for (let y=0;y<h;y++) {
85
+ for (let x=0;x<w;x++) {
86
+ const u = (2*(x + 0.5)/w - 1);
87
+ const v = (2*(y + 0.5)/h - 1);
88
+ let dirLocal;
89
+ switch(faceName) {
90
+ case '+X': dirLocal = [1, -v, -u]; break;
91
+ case '-X': dirLocal = [-1, -v, u]; break;
92
+ case '+Y': dirLocal = [u, 1, v]; break;
93
+ case '-Y': dirLocal = [u, -1, -v]; break;
94
+ case '+Z': dirLocal = [u, -v, 1]; break;
95
+ case '-Z': dirLocal = [-u, -v, -1]; break;
96
+ }
97
+ const L = Math.hypot(dirLocal[0],dirLocal[1],dirLocal[2]);
98
+ dirLocal = dirLocal.map(c => c / L);
99
+ const srcDirLocal = convertVectorBetweenConventions(dirLocal, dstBasis, srcBasis);
100
+ const { face: srcFace, u: su, v: sv } = dirToFaceUV(srcDirLocal[0], srcDirLocal[1], srcDirLocal[2]);
101
+ const rgba = sampleFaceNearest(faceMap[srcFace], su, sv);
102
+ const idx = (y*w + x)*4;
103
+ out[idx]=rgba[0]; out[idx+1]=rgba[1]; out[idx+2]=rgba[2]; out[idx+3]=rgba[3];
104
+ }
105
+ }
106
+ dstRawFaces[faceName] = { w: faceSize, h: faceSize, data: out };
107
+ }
108
+ return dstRawFaces;
109
+ }
110
+
111
+ // ---------- Public wrapper: accepts KTX/KTX2 Buffer in, returns KTX/KTX2 Buffer out ----------
112
+ async function convertCubemapKtx(inputKtxBuffer, srcConvention, dstConvention, opts = { isKTX2: true }) {
113
+ // read KTX into face map
114
+ const { faces: srcFaces, faceOrder } = await readKtx(inputKtxBuffer);
115
+ // convert
116
+ const dstFaces = await convertCubemapFromFaceMap(srcFaces, srcConvention, dstConvention);
117
+ // write KTX (preserve KTX2 if requested)
118
+ const outBuf = writeKtx(dstFaces, FACE_NAMES, opts);
119
+ return outBuf;
120
+ }
121
+
122
+ module.exports = {
123
+ convertCubemapKtx,
124
+ convertCubemapFromFaceMap,
125
+ writeKtx,
126
+ readKtx,
127
+ FACE_NAMES,
128
+ CONVENTIONS
129
+ };