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.
@@ -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;