palava-client 2.2.1 → 3.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/dist/peer.js ADDED
@@ -0,0 +1,150 @@
1
+ // Generated by CoffeeScript 2.7.0
2
+ var boundMethodCheck = function(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new Error('Bound instance method accessed before binding'); } };
3
+
4
+ import EventEmitter from 'wolfy87-eventemitter';
5
+
6
+ import * as browser from './browser.js';
7
+
8
+ // Class representing a participant in a room
9
+
10
+ export var Peer = class Peer extends EventEmitter {
11
+ // @param id [String] ID of the participant
12
+ // @param status [Object] An object conataining state which is exchanged through the palava machine
13
+ // @option staus name [String] The chosen name of the participant
14
+
15
+ constructor(id, status) {
16
+ var base;
17
+ super();
18
+ // Checks whether the participant is sending audio
19
+
20
+ // @return [Boolean] `true` if participant is sending audio
21
+
22
+ this.transmitsAudio = this.transmitsAudio.bind(this);
23
+ // Checks whether the participant is could send audio (but maybe has it muted)
24
+
25
+ // @return [Boolean] `true` if participant has audio tracks
26
+
27
+ this.hasAudio = this.hasAudio.bind(this);
28
+ // Checks whether the participant is sending audio
29
+
30
+ // @return [Boolean] `true` if participant is sending audio
31
+
32
+ this.transmitsVideo = this.transmitsVideo.bind(this);
33
+ // Checks whether the participant is could send video (but maybe put in on hold)
34
+
35
+ // @return [Boolean] `true` if participant has audio tracks
36
+
37
+ this.hasVideo = this.hasVideo.bind(this);
38
+ // Checks whether the peer connection is somewhat erroneous
39
+
40
+ // @return [Boolean] `true` if participant connection has an error
41
+
42
+ this.hasError = this.hasError.bind(this);
43
+ // Returns the error message of the peer
44
+
45
+ // @return [String] error message
46
+
47
+ this.getError = this.getError.bind(this);
48
+ // Checks whether the participant is muted
49
+
50
+ // @return [Boolean] `true` if participant is muted
51
+
52
+ this.isMuted = this.isMuted.bind(this);
53
+ // Checks whether the peer is ready
54
+
55
+ // @return [Boolean] `true` if participant is ready, that they have a stream
56
+
57
+ this.isReady = this.isReady.bind(this);
58
+ // Checks whether the participant is local
59
+
60
+ // @return [Boolean] `true` if participant is the local peer
61
+
62
+ this.isLocal = this.isLocal.bind(this);
63
+ // Checks whether the participant is remote
64
+
65
+ // @return [Boolean] `true` if participant is the remote peer
66
+
67
+ this.isRemote = this.isRemote.bind(this);
68
+ this.id = id;
69
+ this.status = status || {};
70
+ (base = this.status).user_agent || (base.user_agent = browser.getUserAgent());
71
+ this.joinTime = (new Date()).getTime();
72
+ this.ready = false;
73
+ this.error = null;
74
+ }
75
+
76
+ transmitsAudio() {
77
+ var ref, ref1, ref2;
78
+ boundMethodCheck(this, Peer);
79
+ return !!((ref = this.getStream()) != null ? (ref1 = ref.getAudioTracks()) != null ? (ref2 = ref1[0]) != null ? ref2.enabled : void 0 : void 0 : void 0);
80
+ }
81
+
82
+ hasAudio() {
83
+ var ref, ref1;
84
+ boundMethodCheck(this, Peer);
85
+ return !!((ref = this.getStream()) != null ? (ref1 = ref.getAudioTracks()) != null ? ref1[0] : void 0 : void 0);
86
+ }
87
+
88
+ transmitsVideo() {
89
+ var ref, ref1, ref2;
90
+ boundMethodCheck(this, Peer);
91
+ return !!((ref = this.getStream()) != null ? (ref1 = ref.getVideoTracks()) != null ? (ref2 = ref1[0]) != null ? ref2.enabled : void 0 : void 0 : void 0);
92
+ }
93
+
94
+ hasVideo() {
95
+ var ref, ref1;
96
+ boundMethodCheck(this, Peer);
97
+ return !!((ref = this.getStream()) != null ? (ref1 = ref.getVideoTracks()) != null ? ref1[0] : void 0 : void 0);
98
+ }
99
+
100
+ hasError() {
101
+ boundMethodCheck(this, Peer);
102
+ if (this.error) {
103
+ return true;
104
+ } else {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ getError() {
110
+ boundMethodCheck(this, Peer);
111
+ return this.error;
112
+ }
113
+
114
+ isMuted() {
115
+ boundMethodCheck(this, Peer);
116
+ if (this.muted) {
117
+ return true;
118
+ } else {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ isReady() {
124
+ boundMethodCheck(this, Peer);
125
+ if (this.ready) {
126
+ return true;
127
+ } else {
128
+ return false;
129
+ }
130
+ }
131
+
132
+ isLocal() {
133
+ boundMethodCheck(this, Peer);
134
+ if (this.local) {
135
+ return true;
136
+ } else {
137
+ return false;
138
+ }
139
+ }
140
+
141
+ isRemote() {
142
+ boundMethodCheck(this, Peer);
143
+ if (this.local) {
144
+ return false;
145
+ } else {
146
+ return true;
147
+ }
148
+ }
149
+
150
+ };
@@ -0,0 +1,484 @@
1
+ // Generated by CoffeeScript 2.7.0
2
+ var boundMethodCheck = function(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new Error('Bound instance method accessed before binding'); } };
3
+
4
+ import * as browser from './browser.js';
5
+
6
+ import {
7
+ Peer
8
+ } from './peer.js';
9
+
10
+ import {
11
+ Distributor
12
+ } from './distributor.js';
13
+
14
+ import {
15
+ DataChannel
16
+ } from './data_channel.js';
17
+
18
+ // A remote participant in a room
19
+
20
+ export var RemotePeer = class RemotePeer extends Peer {
21
+ // @param id [String] ID of the participant
22
+ // @param status [Object] Status object of the participant
23
+ // @param room [Room] Room the participant is in
24
+ // @param hasOfferPriority [Boolean] If true, we send the initial offer and win offer collisions (impolite)
25
+ // @param turnCredentials [Object] username and password for the turn server (optional)
26
+
27
+ constructor(id, status, room, hasOfferPriority, turnCredentials) {
28
+ super(id, status);
29
+ // Get the stream
30
+
31
+ // @return [MediaStream] Remote stream as defined by WebRTC
32
+
33
+ this.getStream = this.getStream.bind(this);
34
+ // Toggle the mute state of the peer
35
+
36
+ this.toggleMute = this.toggleMute.bind(this);
37
+ // Generates the STUN and TURN options for a peer connection
38
+
39
+ // @return [Object] ICE options for the peer connections
40
+
41
+ this.generateIceOptions = this.generateIceOptions.bind(this);
42
+ // Sets up the peer connection and its events
43
+
44
+ // @nodoc
45
+
46
+ this.setupPeerConnection = this.setupPeerConnection.bind(this);
47
+ // Queues a negotiation task to ensure sequential execution
48
+ // Each task is a function that returns a Promise
49
+
50
+ // @param task [Function] Function returning a Promise
51
+
52
+ this.queueNegotiation = this.queueNegotiation.bind(this);
53
+ // Creates and sends an offer
54
+ // This is queued via queueNegotiation to prevent race conditions
55
+
56
+ // @nodoc
57
+
58
+ this.createAndSendOffer = this.createAndSendOffer.bind(this);
59
+ // Handles an incoming offer
60
+ // This is queued via queueNegotiation to prevent race conditions
61
+
62
+ // @param sdp [RTCSessionDescription] The remote offer
63
+ // @nodoc
64
+
65
+ this.handleOffer = this.handleOffer.bind(this);
66
+ // Handles an incoming answer
67
+ // This is queued via queueNegotiation to prevent race conditions
68
+
69
+ // @param sdp [RTCSessionDescription] The remote answer
70
+ // @nodoc
71
+
72
+ this.handleAnswer = this.handleAnswer.bind(this);
73
+ // Adds a new track to this peer connection
74
+ // Used when the local user enables video/audio after initially joining without it
75
+ // Triggers renegotiation via onnegotiationneeded
76
+
77
+ // @param track [MediaStreamTrack] The track to add
78
+ // @param stream [MediaStream] The stream the track belongs to
79
+
80
+ this.addTrack = this.addTrack.bind(this);
81
+ // Removes a track from this peer connection
82
+ // Used when the local user disables video/audio
83
+ // Triggers renegotiation via onnegotiationneeded
84
+
85
+ // @param track [MediaStreamTrack] The track to remove
86
+
87
+ this.removeTrack = this.removeTrack.bind(this);
88
+ // Sets up the distributor connecting to the participant
89
+
90
+ // @nodoc
91
+
92
+ this.setupDistributor = this.setupDistributor.bind(this);
93
+ // Forward events to the room and listen for local peer events
94
+
95
+ // @nodoc
96
+
97
+ this.setupRoom = this.setupRoom.bind(this);
98
+ // Listen for video_added and audio_added events from the local peer
99
+ // and add those tracks to this peer connection
100
+
101
+ // @nodoc
102
+
103
+ this.setupLocalPeerListeners = this.setupLocalPeerListeners.bind(this);
104
+ this.sendMessage = this.sendMessage.bind(this);
105
+ // End peer connection
106
+
107
+ this.closePeerConnection = this.closePeerConnection.bind(this);
108
+ this.muted = false;
109
+ this.local = false;
110
+ this.room = room;
111
+ this.remoteStream = null;
112
+ this.turnCredentials = turnCredentials;
113
+ this.hasOfferPriority = hasOfferPriority;
114
+ // Queue to ensure negotiation operations happen sequentially
115
+ this.negotiationQueue = Promise.resolve();
116
+ this.dataChannels = {};
117
+ this.setupRoom();
118
+ this.setupPeerConnection();
119
+ this.setupDistributor();
120
+ if (this.hasOfferPriority) {
121
+ this.queueNegotiation(this.createAndSendOffer);
122
+ }
123
+ }
124
+
125
+ getStream() {
126
+ boundMethodCheck(this, RemotePeer);
127
+ return this.remoteStream;
128
+ }
129
+
130
+ toggleMute() {
131
+ boundMethodCheck(this, RemotePeer);
132
+ return this.muted = !this.muted;
133
+ }
134
+
135
+ generateIceOptions() {
136
+ var options;
137
+ boundMethodCheck(this, RemotePeer);
138
+ options = [];
139
+ if (this.room.options.stun) {
140
+ options.push({
141
+ urls: [this.room.options.stun]
142
+ });
143
+ }
144
+ if (this.room.options.turnUrls && this.turnCredentials) {
145
+ options.push({
146
+ urls: this.room.options.turnUrls,
147
+ username: this.turnCredentials.user,
148
+ credential: this.turnCredentials.password
149
+ });
150
+ }
151
+ return {
152
+ iceServers: options
153
+ };
154
+ }
155
+
156
+ setupPeerConnection() {
157
+ var channel, i, label, len, localStream, options, ref, ref1, registerChannel, track;
158
+ boundMethodCheck(this, RemotePeer);
159
+ this.peerConnection = new RTCPeerConnection(this.generateIceOptions(), browser.getPeerConnectionOptions());
160
+ this.peerConnection.onicecandidate = (event) => {
161
+ if (event.candidate) {
162
+ return this.distributor.send({
163
+ event: 'ice_candidate',
164
+ sdpmlineindex: event.candidate.sdpMLineIndex,
165
+ sdpmid: event.candidate.sdpMid,
166
+ candidate: event.candidate.candidate
167
+ });
168
+ }
169
+ };
170
+ this.peerConnection.ontrack = (event) => {
171
+ var stream, track;
172
+ stream = event.streams[0];
173
+ track = event.track;
174
+ if (!this.remoteStream) {
175
+ this.remoteStream = stream;
176
+ this.ready = true;
177
+ this.emit('stream_ready');
178
+ // Listen for tracks being added/removed from the remote stream
179
+ this.remoteStream.onaddtrack = (e) => {
180
+ if (e.track.kind === 'video') {
181
+ return this.emit('video_added', e.track, this.remoteStream);
182
+ } else if (e.track.kind === 'audio') {
183
+ return this.emit('audio_added', e.track, this.remoteStream);
184
+ }
185
+ };
186
+ return this.remoteStream.onremovetrack = (e) => {
187
+ if (e.track.kind === 'video') {
188
+ return this.emit('video_removed', e.track, this.remoteStream);
189
+ } else if (e.track.kind === 'audio') {
190
+ return this.emit('audio_removed', e.track, this.remoteStream);
191
+ }
192
+ };
193
+ } else {
194
+ // Additional track added to existing stream
195
+ if (track.kind === 'video') {
196
+ return this.emit('video_added', track, this.remoteStream);
197
+ } else if (track.kind === 'audio') {
198
+ return this.emit('audio_added', track, this.remoteStream);
199
+ }
200
+ }
201
+ };
202
+ this.peerConnection.onremovestream = (event) => {
203
+ this.remoteStream = null;
204
+ this.ready = false;
205
+ return this.emit('stream_removed');
206
+ };
207
+ this.peerConnection.oniceconnectionstatechange = (event) => {
208
+ var connectionState;
209
+ connectionState = event.target.iceConnectionState;
210
+ switch (connectionState) {
211
+ case 'connecting':
212
+ this.error = null;
213
+ return this.emit('connection_pending');
214
+ case 'connected':
215
+ this.error = null;
216
+ return this.emit('connection_established');
217
+ case 'failed':
218
+ this.error = "connection_failed";
219
+ return this.emit('connection_failed');
220
+ case 'disconnected':
221
+ this.error = "connection_disconnected";
222
+ return this.emit('connection_disconnected');
223
+ case 'closed':
224
+ this.error = "connection_closed";
225
+ return this.emit('connection_closed');
226
+ }
227
+ };
228
+ // Handle negotiationneeded event - queue an offer
229
+ this.peerConnection.onnegotiationneeded = () => {
230
+ return this.queueNegotiation(this.createAndSendOffer);
231
+ };
232
+ // Add local tracks if we have a stream
233
+ localStream = this.room.localPeer.getStream();
234
+ if (localStream) {
235
+ ref = localStream.getTracks();
236
+ for (i = 0, len = ref.length; i < len; i++) {
237
+ track = ref[i];
238
+ this.peerConnection.addTrack(track, localStream);
239
+ }
240
+ }
241
+ // data channel setup
242
+ if (this.room.options.dataChannels != null) {
243
+ registerChannel = (channel) => {
244
+ var name, wrapper;
245
+ name = channel.label;
246
+ wrapper = new DataChannel(channel);
247
+ this.dataChannels[name] = wrapper;
248
+ return this.emit('channel_ready', name, wrapper);
249
+ };
250
+ if (this.hasOfferPriority) {
251
+ ref1 = this.room.options.dataChannels;
252
+ for (label in ref1) {
253
+ options = ref1[label];
254
+ channel = this.peerConnection.createDataChannel(label, options);
255
+ channel.onopen = function() {
256
+ return registerChannel(this);
257
+ };
258
+ }
259
+ } else {
260
+ this.peerConnection.ondatachannel = (event) => {
261
+ return registerChannel(event.channel);
262
+ };
263
+ }
264
+ }
265
+ return this.peerConnection;
266
+ }
267
+
268
+ queueNegotiation(task) {
269
+ boundMethodCheck(this, RemotePeer);
270
+ return this.negotiationQueue = this.negotiationQueue.then(task).catch((error) => {
271
+ return this.emit('oaerror', error);
272
+ });
273
+ }
274
+
275
+ createAndSendOffer() {
276
+ boundMethodCheck(this, RemotePeer);
277
+ return this.peerConnection.createOffer(browser.getConstraints()).then((offer) => {
278
+ return this.peerConnection.setLocalDescription(offer);
279
+ }).then(() => {
280
+ this.distributor.send({
281
+ event: 'offer',
282
+ sdp: this.peerConnection.localDescription
283
+ });
284
+ return this.emit('offer');
285
+ });
286
+ }
287
+
288
+ handleOffer(sdp) {
289
+ boundMethodCheck(this, RemotePeer);
290
+ // Check for offer collision: both peers sent offers at the same time
291
+ // If we're impolite (hasOfferPriority) and we have a pending local offer, ignore incoming offer
292
+ if (this.hasOfferPriority && this.peerConnection.signalingState === 'have-local-offer') {
293
+ return Promise.resolve();
294
+ }
295
+ // If we're polite and have a pending local offer, implicit rollback will happen
296
+ // when we call setRemoteDescription with the incoming offer
297
+ return this.peerConnection.setRemoteDescription(sdp).then(() => {
298
+ return this.peerConnection.createAnswer(browser.getConstraints());
299
+ }).then((answer) => {
300
+ return this.peerConnection.setLocalDescription(answer);
301
+ }).then(() => {
302
+ this.distributor.send({
303
+ event: 'answer',
304
+ sdp: this.peerConnection.localDescription
305
+ });
306
+ return this.emit('answer');
307
+ });
308
+ }
309
+
310
+ handleAnswer(sdp) {
311
+ boundMethodCheck(this, RemotePeer);
312
+ // Only process answer if we're expecting one
313
+ if (this.peerConnection.signalingState !== 'have-local-offer') {
314
+ return Promise.resolve();
315
+ }
316
+ return this.peerConnection.setRemoteDescription(sdp);
317
+ }
318
+
319
+ addTrack(track, stream) {
320
+ boundMethodCheck(this, RemotePeer);
321
+ if (!this.peerConnection) {
322
+ return;
323
+ }
324
+ return this.peerConnection.addTrack(track, stream);
325
+ }
326
+
327
+ removeTrack(track) {
328
+ var sender;
329
+ boundMethodCheck(this, RemotePeer);
330
+ if (!this.peerConnection) {
331
+ return;
332
+ }
333
+ // Find the sender for this track and remove it
334
+ sender = this.peerConnection.getSenders().find(function(s) {
335
+ return s.track === track;
336
+ });
337
+ if (sender) {
338
+ return this.peerConnection.removeTrack(sender);
339
+ }
340
+ }
341
+
342
+ setupDistributor() {
343
+ boundMethodCheck(this, RemotePeer);
344
+ this.distributor = new Distributor(this.room.channel, this.id);
345
+ this.distributor.on('peer_left', (msg) => {
346
+ if (this.ready) {
347
+ this.remoteStream = null;
348
+ this.emit('stream_removed');
349
+ this.ready = false;
350
+ }
351
+ this.peerConnection.close();
352
+ return this.emit('left');
353
+ });
354
+ this.distributor.on('ice_candidate', (msg) => {
355
+ var candidate;
356
+ // empty msg.candidate causes error messages in firefox, so let RTCPeerConnection deal with it and return here
357
+ if (msg.candidate === "") {
358
+ return;
359
+ }
360
+ candidate = new RTCIceCandidate({
361
+ candidate: msg.candidate,
362
+ sdpMLineIndex: msg.sdpmlineindex,
363
+ sdpMid: msg.sdpmid
364
+ });
365
+ if (!this.room.options.filterIceCandidateTypes.includes(candidate.type)) {
366
+ return this.peerConnection.addIceCandidate(candidate);
367
+ }
368
+ });
369
+ this.distributor.on('offer', (msg) => {
370
+ var sdp;
371
+ sdp = new RTCSessionDescription(msg.sdp);
372
+ return this.queueNegotiation(() => {
373
+ return this.handleOffer(sdp);
374
+ });
375
+ });
376
+ this.distributor.on('answer', (msg) => {
377
+ var sdp;
378
+ sdp = new RTCSessionDescription(msg.sdp);
379
+ return this.queueNegotiation(() => {
380
+ return this.handleAnswer(sdp);
381
+ });
382
+ });
383
+ this.distributor.on('peer_updated_status', (msg) => {
384
+ this.status = msg.status;
385
+ return this.emit('update');
386
+ });
387
+ this.distributor.on('message', (msg) => {
388
+ return this.emit('message', msg.data);
389
+ });
390
+ return this.distributor;
391
+ }
392
+
393
+ setupRoom() {
394
+ boundMethodCheck(this, RemotePeer);
395
+ this.room.peers[this.id] = this;
396
+ this.on('left', () => {
397
+ delete this.room.peers[this.id];
398
+ return this.room.emit('peer_left', this);
399
+ });
400
+ this.on('offer', () => {
401
+ return this.room.emit('peer_offer', this);
402
+ });
403
+ this.on('answer', () => {
404
+ return this.room.emit('peer_answer', this);
405
+ });
406
+ this.on('update', () => {
407
+ return this.room.emit('peer_update', this);
408
+ });
409
+ this.on('stream_ready', () => {
410
+ return this.room.emit('peer_stream_ready', this);
411
+ });
412
+ this.on('stream_removed', () => {
413
+ return this.room.emit('peer_stream_removed', this);
414
+ });
415
+ this.on('connection_pending', () => {
416
+ return this.room.emit('peer_connection_pending', this);
417
+ });
418
+ this.on('connection_established', () => {
419
+ return this.room.emit('peer_connection_established', this);
420
+ });
421
+ this.on('connection_failed', () => {
422
+ return this.room.emit('peer_connection_failed', this);
423
+ });
424
+ this.on('connection_disconnected', () => {
425
+ return this.room.emit('peer_connection_disconnected', this);
426
+ });
427
+ this.on('connection_closed', () => {
428
+ return this.room.emit('peer_connection_closed', this);
429
+ });
430
+ this.on('oaerror', (e) => {
431
+ return this.room.emit('peer_oaerror', this, e);
432
+ });
433
+ this.on('channel_ready', (n, c) => {
434
+ return this.room.emit('peer_channel_ready', this, n, c);
435
+ });
436
+ // Listen for local peer adding new tracks (when user enables video/audio after joining)
437
+ return this.setupLocalPeerListeners();
438
+ }
439
+
440
+ setupLocalPeerListeners() {
441
+ boundMethodCheck(this, RemotePeer);
442
+ this.localPeerVideoAddedHandler = (track, stream) => {
443
+ return this.addTrack(track, stream);
444
+ };
445
+ this.localPeerAudioAddedHandler = (track, stream) => {
446
+ return this.addTrack(track, stream);
447
+ };
448
+ this.localPeerVideoRemovedHandler = (track, stream) => {
449
+ return this.removeTrack(track);
450
+ };
451
+ this.localPeerAudioRemovedHandler = (track, stream) => {
452
+ return this.removeTrack(track);
453
+ };
454
+ this.room.localPeer.on('video_added', this.localPeerVideoAddedHandler);
455
+ this.room.localPeer.on('audio_added', this.localPeerAudioAddedHandler);
456
+ this.room.localPeer.on('video_removed', this.localPeerVideoRemovedHandler);
457
+ this.room.localPeer.on('audio_removed', this.localPeerAudioRemovedHandler);
458
+ // Clean up listeners when this peer leaves
459
+ return this.on('left', () => {
460
+ this.room.localPeer.off('video_added', this.localPeerVideoAddedHandler);
461
+ this.room.localPeer.off('audio_added', this.localPeerAudioAddedHandler);
462
+ this.room.localPeer.off('video_removed', this.localPeerVideoRemovedHandler);
463
+ return this.room.localPeer.off('audio_removed', this.localPeerAudioRemovedHandler);
464
+ });
465
+ }
466
+
467
+ sendMessage(data) {
468
+ boundMethodCheck(this, RemotePeer);
469
+ return this.distributor.send({
470
+ event: 'message',
471
+ data: data
472
+ });
473
+ }
474
+
475
+ closePeerConnection() {
476
+ var ref;
477
+ boundMethodCheck(this, RemotePeer);
478
+ if ((ref = this.peerConnection) != null) {
479
+ ref.close();
480
+ }
481
+ return this.peerConnection = null;
482
+ }
483
+
484
+ };