teleportxr 1.0.0
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/.vscode/launch.json +18 -0
- package/.vscode/settings.json +12 -0
- package/LICENSE +21 -0
- package/LICENSES/Apache-2.0.txt +73 -0
- package/LICENSES/BSD-3-Clause.txt +11 -0
- package/LICENSES/BSL-1.0.txt +7 -0
- package/LICENSES/CC-BY-4.0.txt +156 -0
- package/LICENSES/CC0-1.0.txt +121 -0
- package/LICENSES/MIT.txt +9 -0
- package/README.md +6 -0
- package/assets/scene.json +8 -0
- package/client/client.js +305 -0
- package/client/client_manager.js +114 -0
- package/client/geometry_service.js +422 -0
- package/connections/connection.js +0 -0
- package/connections/connectionmanager.js +0 -0
- package/connections/webrtcconnection.js +384 -0
- package/connections/webrtcconnectionmanager.js +86 -0
- package/core/core.js +447 -0
- package/index.js +13 -0
- package/package.json +36 -0
- package/protocol/command.js +127 -0
- package/protocol/encoders/node_encoder.js +28 -0
- package/protocol/encoders/resource_encoder.js +28 -0
- package/protocol/message.js +145 -0
- package/scene/material.js +105 -0
- package/scene/node.js +255 -0
- package/scene/resources.js +62 -0
- package/scene/scene.js +83 -0
- package/server.js +35 -0
- package/signaling.js +255 -0
- package/temp.js +1 -0
- package/test.json +2 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const EventEmitter = require('events');
|
|
4
|
+
const wrtc =require('wrtc');
|
|
5
|
+
const DefaultRTCPeerConnection = require('wrtc').RTCPeerConnection;
|
|
6
|
+
|
|
7
|
+
const TIME_TO_CONNECTED = 10000;
|
|
8
|
+
const TIME_TO_HOST_CANDIDATES = 3000; // NOTE: Too long.
|
|
9
|
+
const TIME_TO_RECONNECTED = 10000;
|
|
10
|
+
|
|
11
|
+
function EVEN_ID(id) {
|
|
12
|
+
return (id-(id%2))
|
|
13
|
+
}
|
|
14
|
+
function ODD_ID(id) {
|
|
15
|
+
return (EVEN_ID(id)+1)
|
|
16
|
+
}
|
|
17
|
+
class WebRtcConnection extends EventEmitter
|
|
18
|
+
{
|
|
19
|
+
constructor(id, options = {})
|
|
20
|
+
{
|
|
21
|
+
super();
|
|
22
|
+
this.id = id;
|
|
23
|
+
this.state = 'open';
|
|
24
|
+
|
|
25
|
+
options = {
|
|
26
|
+
RTCPeerConnection: DefaultRTCPeerConnection,
|
|
27
|
+
clearTimeout,
|
|
28
|
+
setTimeout,
|
|
29
|
+
timeToConnected: TIME_TO_CONNECTED,
|
|
30
|
+
timeToHostCandidates: TIME_TO_HOST_CANDIDATES,
|
|
31
|
+
timeToReconnected: TIME_TO_RECONNECTED,
|
|
32
|
+
...options
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
RTCPeerConnection,
|
|
37
|
+
timeToConnected,
|
|
38
|
+
timeToReconnected
|
|
39
|
+
} = options;
|
|
40
|
+
|
|
41
|
+
this.connectionStateChanged =options.connectionStateChanged;
|
|
42
|
+
this.messageReceivedReliableCb =options.messageReceivedReliable;
|
|
43
|
+
this.messageReceivedUnreliableCb =options.messageReceivedUnreliable;
|
|
44
|
+
|
|
45
|
+
this.sendConfigMessage =options.sendConfigMessage;
|
|
46
|
+
const peerConnection =new RTCPeerConnection({ sdpSemantics: 'unified-plan'});
|
|
47
|
+
this.pc =peerConnection;
|
|
48
|
+
|
|
49
|
+
this.beforeOffer(peerConnection);
|
|
50
|
+
|
|
51
|
+
let connectionTimer = options.setTimeout(() =>
|
|
52
|
+
{
|
|
53
|
+
if (peerConnection.iceConnectionState !== 'connected'
|
|
54
|
+
&& peerConnection.iceConnectionState !== 'completed')
|
|
55
|
+
{
|
|
56
|
+
this.close();
|
|
57
|
+
}
|
|
58
|
+
}, timeToConnected);
|
|
59
|
+
|
|
60
|
+
let reconnectionTimer = null;
|
|
61
|
+
|
|
62
|
+
const onIceConnectionStateChange = () =>
|
|
63
|
+
{
|
|
64
|
+
console.log("ICE state changed to: "+peerConnection.iceConnectionState);
|
|
65
|
+
if (peerConnection.iceConnectionState === 'connected'
|
|
66
|
+
|| peerConnection.iceConnectionState === 'completed')
|
|
67
|
+
{
|
|
68
|
+
if (connectionTimer)
|
|
69
|
+
{
|
|
70
|
+
options.clearTimeout(connectionTimer);
|
|
71
|
+
connectionTimer = null;
|
|
72
|
+
}
|
|
73
|
+
options.clearTimeout(reconnectionTimer);
|
|
74
|
+
reconnectionTimer = null;
|
|
75
|
+
} else if (peerConnection.iceConnectionState === 'disconnected'
|
|
76
|
+
|| peerConnection.iceConnectionState === 'failed')
|
|
77
|
+
{
|
|
78
|
+
if (!connectionTimer && !reconnectionTimer)
|
|
79
|
+
{
|
|
80
|
+
const self = this;
|
|
81
|
+
reconnectionTimer = options.setTimeout(() =>
|
|
82
|
+
{
|
|
83
|
+
self.close();
|
|
84
|
+
}, timeToReconnected);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const onIceGatheringStateChange = () =>
|
|
89
|
+
{
|
|
90
|
+
console.log("ICE gathering state changed to: "+peerConnection.iceGatheringState);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
peerConnection.addEventListener('iceconnectionstatechange', onIceConnectionStateChange);
|
|
94
|
+
peerConnection.addEventListener('icegatheringstatechange', onIceGatheringStateChange);
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
const onConnectionStateChange = () =>
|
|
98
|
+
{
|
|
99
|
+
console.log("Connection State changed to: "+peerConnection.connectionState.toString());
|
|
100
|
+
this.connectionStateChanged(this,peerConnection.connectionState);
|
|
101
|
+
}
|
|
102
|
+
peerConnection.addEventListener("connectionstatechange", onConnectionStateChange);
|
|
103
|
+
|
|
104
|
+
this.onIceCandidate= ({ candidate })=>
|
|
105
|
+
{
|
|
106
|
+
if (!candidate)
|
|
107
|
+
{
|
|
108
|
+
options.clearTimeout(this.timeout);
|
|
109
|
+
//peerConnection.removeEventListener('icecandidate', this.onIceCandidate);
|
|
110
|
+
this.deferred.resolve();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// send the candidate
|
|
114
|
+
var mid=candidate.sdpMid;
|
|
115
|
+
var mlineindex=candidate.sdpMLineIndex;
|
|
116
|
+
var message = '{"teleport-signal-type":"candidate","candidate":"'+candidate.candidate+'","mid":"'+mid.toString()+'","mlineindex":'+mlineindex.toString()+'}';
|
|
117
|
+
this.sendConfigMessage(this.id,message);
|
|
118
|
+
}
|
|
119
|
+
this.waitUntilIceGatheringStateComplete= async (peerConnection, options) =>
|
|
120
|
+
{
|
|
121
|
+
if (peerConnection.iceGatheringState === 'complete')
|
|
122
|
+
{
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const { timeToHostCandidates } = options;
|
|
127
|
+
|
|
128
|
+
this.deferred = {};
|
|
129
|
+
this.deferred.promise = new Promise((resolve, reject) =>
|
|
130
|
+
{
|
|
131
|
+
this.deferred.resolve = resolve;
|
|
132
|
+
this.deferred.reject = reject;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.timeout = options.setTimeout(() =>
|
|
136
|
+
{
|
|
137
|
+
peerConnection.removeEventListener('icecandidate', this.onIceCandidate);
|
|
138
|
+
this.deferred.reject(new Error('Timed out waiting for host candidates'));
|
|
139
|
+
}, timeToHostCandidates);
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
peerConnection.addEventListener('icecandidate', this.onIceCandidate);
|
|
143
|
+
|
|
144
|
+
await this.deferred.promise;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.doOffer = async () =>
|
|
148
|
+
{
|
|
149
|
+
try
|
|
150
|
+
{
|
|
151
|
+
const offer = await peerConnection.createOffer();
|
|
152
|
+
await peerConnection.setLocalDescription(offer);
|
|
153
|
+
var message = '{"teleport-signal-type":"offer","sdp":"'+offer.sdp+'"}'; //
|
|
154
|
+
this.sendConfigMessage(this.id,message);
|
|
155
|
+
await this.waitUntilIceGatheringStateComplete(peerConnection, options);
|
|
156
|
+
} catch (error)
|
|
157
|
+
{
|
|
158
|
+
console.error(error.toString());
|
|
159
|
+
this.close();
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
this.applyAnswer = async answer =>
|
|
165
|
+
{
|
|
166
|
+
console.log("received remote answer.");
|
|
167
|
+
var escapedStr=answer.toString();
|
|
168
|
+
try{
|
|
169
|
+
escapedStr=escapedStr.replaceAll('\r','\\r');
|
|
170
|
+
escapedStr=escapedStr.replaceAll('\n','\\n');
|
|
171
|
+
} catch(error)
|
|
172
|
+
{
|
|
173
|
+
}
|
|
174
|
+
var sessionDescription=new wrtc.RTCSessionDescription();
|
|
175
|
+
sessionDescription.sdp=answer;
|
|
176
|
+
sessionDescription.type="answer";
|
|
177
|
+
await peerConnection.setRemoteDescription( sessionDescription);
|
|
178
|
+
};
|
|
179
|
+
this.applyRemoteCandidate = async(candidate_txt,mid,mlineindex)=>
|
|
180
|
+
{
|
|
181
|
+
console.log("received remote candidate.");
|
|
182
|
+
peerConnection.addIceCandidate(new wrtc.RTCIceCandidate({
|
|
183
|
+
candidate: candidate_txt,
|
|
184
|
+
sdpMLineIndex: mlineindex,
|
|
185
|
+
sdpMid: mid
|
|
186
|
+
}));
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
this.close = () =>
|
|
190
|
+
{
|
|
191
|
+
peerConnection.removeEventListener('iceconnectionstatechange', onIceConnectionStateChange);
|
|
192
|
+
if (connectionTimer)
|
|
193
|
+
{
|
|
194
|
+
options.clearTimeout(connectionTimer);
|
|
195
|
+
connectionTimer = null;
|
|
196
|
+
}
|
|
197
|
+
if (reconnectionTimer)
|
|
198
|
+
{
|
|
199
|
+
options.clearTimeout(reconnectionTimer);
|
|
200
|
+
reconnectionTimer = null;
|
|
201
|
+
}
|
|
202
|
+
peerConnection.close();
|
|
203
|
+
this.state = 'closed';
|
|
204
|
+
this.emit('closed');
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
this.toJSON = () =>
|
|
208
|
+
{
|
|
209
|
+
return {
|
|
210
|
+
id: this.id,
|
|
211
|
+
state: this.state,
|
|
212
|
+
iceConnectionState: this.iceConnectionState,
|
|
213
|
+
localDescription: this.localDescription,
|
|
214
|
+
remoteDescription: this.remoteDescription,
|
|
215
|
+
signalingState: this.signalingState
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
Object.defineProperties(this, {
|
|
220
|
+
iceConnectionState: {
|
|
221
|
+
get ()
|
|
222
|
+
{
|
|
223
|
+
return peerConnection.iceConnectionState;
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
localDescription: {
|
|
227
|
+
get ()
|
|
228
|
+
{
|
|
229
|
+
return descriptionToJSON(peerConnection.localDescription, true);
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
remoteDescription: {
|
|
233
|
+
get ()
|
|
234
|
+
{
|
|
235
|
+
return descriptionToJSON(peerConnection.remoteDescription);
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
signalingState: {
|
|
239
|
+
get ()
|
|
240
|
+
{
|
|
241
|
+
return peerConnection.signalingState;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
sendGeometry(buffer) {
|
|
247
|
+
try {
|
|
248
|
+
this.geometryDataChannel.send(buffer);
|
|
249
|
+
}
|
|
250
|
+
catch(exception) {
|
|
251
|
+
console.error('datachannel.sendGeometry exception: '+exception.message);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
beforeOffer(peerConnection) {
|
|
255
|
+
|
|
256
|
+
this.videoDataChannel = this.createDataChannel("video",20);
|
|
257
|
+
this.tagDataChannel = this.createDataChannel("video_tags",40);
|
|
258
|
+
this.audioToClientDataChannel = this.createDataChannel("audio_server_to_client",60);
|
|
259
|
+
this.geometryDataChannel = this.createDataChannel("geometry_unframed",80);
|
|
260
|
+
this.reliableDataChannel = this.createDataChannel("reliable",100);
|
|
261
|
+
this.unreliableDataChannel = this.createDataChannel("unreliable",120,false);
|
|
262
|
+
// this.dataChannel = peerConnection.createDataChannel('ping-pong',{id:2050});
|
|
263
|
+
|
|
264
|
+
function onMessage({ data }) {
|
|
265
|
+
if (data === 'ping') {
|
|
266
|
+
//dataChannel.send('pong');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// NOTE(mroberts): This is a hack so that we can get a callback when the
|
|
271
|
+
// RTCPeerConnection is closed. In the future, we can subscribe to
|
|
272
|
+
// "connectionstatechange" events.
|
|
273
|
+
const { close } = peerConnection;
|
|
274
|
+
var self=this;
|
|
275
|
+
peerConnection.close = function() {
|
|
276
|
+
//self.dataChannel.removeEventListener('message', onMessage);
|
|
277
|
+
return close.apply(this, arguments);
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
receiveMessage(id,event)
|
|
282
|
+
{
|
|
283
|
+
//.videoDataChannel = .("video",20);
|
|
284
|
+
//.tagDataChannel = .("video_tags",40);
|
|
285
|
+
//.audioToClientDataChannel = .("audio_server_to_client",60);
|
|
286
|
+
//.geometryDataChannel =l("geometry_unframed",80);
|
|
287
|
+
//.reliableDataChannel =l("reliable",100);
|
|
288
|
+
//.unreliableDataChannelnel("unreliable",120,false);
|
|
289
|
+
switch(id)
|
|
290
|
+
{
|
|
291
|
+
case 120:
|
|
292
|
+
this.messageReceivedUnreliableCb(id,event);
|
|
293
|
+
break;
|
|
294
|
+
case 100:
|
|
295
|
+
this.messageReceivedReliableCb(id,event);
|
|
296
|
+
break;
|
|
297
|
+
default:
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
// event is an ArrayBuffer.
|
|
301
|
+
};
|
|
302
|
+
createDataChannel(label,id,reliable=true)
|
|
303
|
+
{
|
|
304
|
+
//See https://web.dev/articles/webrtc-datachannels. Can only use id if negotiated=true.
|
|
305
|
+
const dataChannelOptions={ordered:reliable,maxRetransmits:reliable?10000:0,id:id};
|
|
306
|
+
|
|
307
|
+
var dc=this.pc.createDataChannel(label,dataChannelOptions);
|
|
308
|
+
|
|
309
|
+
dc.onmessage = this.receiveMessage.bind(this,id);
|
|
310
|
+
|
|
311
|
+
dc.onopen = (event) => {
|
|
312
|
+
console.log('datachannel '+label+' open');
|
|
313
|
+
//dc.send('XXXX');
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
dc.onclose = (event) => {
|
|
317
|
+
console.log('datachannel '+label+' close');
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
dc.onclosing = (event) => {
|
|
321
|
+
console.log('datachannel '+label+' onclosing');
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
dc.onbufferedamountlow = (event) => {
|
|
326
|
+
console.log('datachannel '+label+' onbufferedamountlow;');
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
// dc.addEventListener('message', onMessage);
|
|
331
|
+
return dc;
|
|
332
|
+
}
|
|
333
|
+
receiveStreamingControlMessage(txt)
|
|
334
|
+
{
|
|
335
|
+
var message = JSON.parse(txt);
|
|
336
|
+
if (!message.hasOwnProperty("teleport-signal-type"))
|
|
337
|
+
{
|
|
338
|
+
console.error("Streaming message ill-formed.");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
var teleport_signal_type=message["teleport-signal-type"];
|
|
342
|
+
if (teleport_signal_type == "answer")
|
|
343
|
+
{
|
|
344
|
+
var sdp = message["sdp"];
|
|
345
|
+
this.receiveAnswer(sdp);
|
|
346
|
+
}
|
|
347
|
+
else if (teleport_signal_type == "candidate")
|
|
348
|
+
{
|
|
349
|
+
var candidate = message["candidate"];
|
|
350
|
+
var mid = message["mid"];
|
|
351
|
+
var mlineindex = message["mlineindex"];
|
|
352
|
+
this.applyRemoteCandidate(candidate, mid, mlineindex);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
receiveAnswer(sdp)
|
|
356
|
+
{
|
|
357
|
+
this.applyAnswer(sdp);
|
|
358
|
+
}
|
|
359
|
+
receiveCandidate(candidate,mid,mlineindex)
|
|
360
|
+
{
|
|
361
|
+
peerConn.addIceCandidate(new RTCIceCandidate({
|
|
362
|
+
candidate: message.candidate,
|
|
363
|
+
sdpMLineIndex: message.label,
|
|
364
|
+
sdpMid: message.id
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
function descriptionToJSON (description, shouldDisableTrickleIce)
|
|
372
|
+
{
|
|
373
|
+
return !description ? {} : {
|
|
374
|
+
type: description.type,
|
|
375
|
+
sdp: shouldDisableTrickleIce ? disableTrickleIce(description.sdp) : description.sdp
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function disableTrickleIce (sdp)
|
|
380
|
+
{
|
|
381
|
+
return sdp.replace(/\r\na=ice-options:trickle/g, '');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
module.exports = WebRtcConnection;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const WebRtcConnection = require('./webrtcconnection');
|
|
4
|
+
|
|
5
|
+
class WebRtcConnectionManager
|
|
6
|
+
{
|
|
7
|
+
constructor(options = {})
|
|
8
|
+
{
|
|
9
|
+
options = {
|
|
10
|
+
Connection: WebRtcConnection,
|
|
11
|
+
...options
|
|
12
|
+
};
|
|
13
|
+
const { Connection } = options;
|
|
14
|
+
const connections = new Map();
|
|
15
|
+
const closedListeners = new Map();
|
|
16
|
+
|
|
17
|
+
function deleteConnection(connection) {
|
|
18
|
+
// 1. Remove "closed" listener?
|
|
19
|
+
//const closedListener = closedListeners.get(connection);
|
|
20
|
+
//connection.removeListener("closed", closedListener);
|
|
21
|
+
//closedListeners.delete(connection);
|
|
22
|
+
|
|
23
|
+
// 2. Remove the Connection from the Map.
|
|
24
|
+
connections.delete(connection.id);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.createConnection = (clientID,connectionStateChangedcb,messageReceivedReliableCb,messageReceivedUnreliableCb) =>
|
|
28
|
+
{
|
|
29
|
+
options.sendConfigMessage =this.sendConfigMessage;
|
|
30
|
+
|
|
31
|
+
options.messageReceivedReliable =messageReceivedReliableCb;
|
|
32
|
+
options.messageReceivedUnreliable =messageReceivedUnreliableCb;
|
|
33
|
+
const connection = new Connection(clientID,options);
|
|
34
|
+
connection.connectionStateChanged=connectionStateChangedcb;
|
|
35
|
+
// 1. Add the "closed" listener.
|
|
36
|
+
function closedListener() { deleteConnection(connection); }
|
|
37
|
+
this.createConnection = (clientID) =>
|
|
38
|
+
closedListeners.set(connection, closedListener);
|
|
39
|
+
connection.once('closed', closedListener);
|
|
40
|
+
|
|
41
|
+
// 2. Add the Connection to the Map.
|
|
42
|
+
connections.set(connection.id, connection);
|
|
43
|
+
|
|
44
|
+
connection.doOffer();
|
|
45
|
+
return connection;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
this.getConnection = id =>
|
|
49
|
+
{
|
|
50
|
+
return connections.get(id) || null;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
this.getConnections = () =>
|
|
54
|
+
{
|
|
55
|
+
return [...connections.values()];
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
toJSON ()
|
|
60
|
+
{
|
|
61
|
+
return this.getConnections().map(connection => connection.toJSON());
|
|
62
|
+
}
|
|
63
|
+
SetSendConfigMessage(cfm)
|
|
64
|
+
{
|
|
65
|
+
this.sendConfigMessage=cfm;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
WebRtcConnectionManager.create = function create (options)
|
|
70
|
+
{
|
|
71
|
+
return new WebRtcConnectionManager({
|
|
72
|
+
Connection: function (id)
|
|
73
|
+
{
|
|
74
|
+
return new WebRtcConnection(id,options);
|
|
75
|
+
},
|
|
76
|
+
...options
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
WebRtcConnectionManager.getInstance = function(){
|
|
81
|
+
if(global.WebRtcConnectionManager_instance === undefined)
|
|
82
|
+
global.WebRtcConnectionManager_instance = new WebRtcConnectionManager();
|
|
83
|
+
return global.WebRtcConnectionManager_instance;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = WebRtcConnectionManager;
|