teleportxr 1.0.83 → 1.0.85
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.
|
@@ -126,6 +126,7 @@ class GeometryService {
|
|
|
126
126
|
// MAY not be in this set:
|
|
127
127
|
this.streamedNodes.delete(uid);
|
|
128
128
|
// TODO: now reduce the counts for all the dependent resources.
|
|
129
|
+
console.log("Unstreaming node ", node_uid," for client ", this.clientID);
|
|
129
130
|
}
|
|
130
131
|
StreamOrUnstream(resourceMap, uid, diff) {
|
|
131
132
|
// exclude "undefined"
|
|
@@ -256,11 +257,11 @@ class GeometryService {
|
|
|
256
257
|
|
|
257
258
|
var node = this.scene.GetNode(node_uid);
|
|
258
259
|
if(diff>0 && !already_present)
|
|
259
|
-
console.log("Adding node ", node.name, " for client ", this.clientID);
|
|
260
|
+
console.log("Adding node ", node.name," (", node_uid,") for client ", this.clientID);
|
|
260
261
|
else if(diff<0 && already_present)
|
|
261
|
-
console.log("Removing node ", node.name, " for client ", this.clientID);
|
|
262
|
+
console.log("Removing node ", node.name," (", node_uid,") for client ", this.clientID);
|
|
262
263
|
else
|
|
263
|
-
console.log("Changing node ", node.name, " count from ", old_count, " to ", old_count+diff, " for client ", this.clientID);
|
|
264
|
+
console.log("Changing node ", node.name," (", node_uid,") count from ", old_count, " to ", old_count+diff, " for client ", this.clientID);
|
|
264
265
|
this.streamedNodes.set(node_uid, old_count + diff);
|
|
265
266
|
var meshResources = [];
|
|
266
267
|
node.components.forEach((component) => {
|
|
@@ -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
|
@@ -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
|
+
};
|