Source: easyrtc_int.js

1
/* global define, module, require, console, MediaStreamTrack, createIceServer, RTCIceCandidate, RTCPeerConnection, RTCSessionDescription */
2
/*!
3
Script: easyrtc.js
4
5
Provides client side support for the EasyRTC framework.
6
See the easyrtc_client_api.md and easyrtc_client_tutorial.md
7
for more details.
8
9
About: License
10
11
Copyright (c) 2016, Priologic Software Inc.
12
All rights reserved.
13
14
Redistribution and use in source and binary forms, with or without
15
modification, are permitted provided that the following conditions are met:
16
17
* Redistributions of source code must retain the above copyright notice,
18
this list of conditions and the following disclaimer.
19
* Redistributions in binary form must reproduce the above copyright
20
notice, this list of conditions and the following disclaimer in the
21
documentation and/or other materials provided with the distribution.
22
23
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
24
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
25
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
26
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
27
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
28
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
29
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
30
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
31
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
32
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33
POSSIBILITY OF SUCH DAMAGE.
34
*/
35
36
(function (root, factory) {
37
if (typeof define === 'function' && define.amd) {
38
//RequireJS (AMD) build system
39
define(['easyrtc_lang', 'webrtc-adapter', 'socket.io'], factory);
40
} else if (typeof module === 'object' && module.exports) {
41
//CommonJS build system
42
module.exports = factory(require('easyrtc_lang'), require('webrtc-adapter'), require('socket.io'));
43
} else {
44
//Vanilla JS, ensure dependencies are loaded correctly
45
if (typeof window.io === 'undefined' || !window.io) {
46
throw new Error("easyrtc requires socket.io");
47
}
48
root.easyrtc = factory(window.easyrtc_lang, window.adapter, window.io);
49
}
50
}(this, function (easyrtc_lang, adapter, io, undefined) {
51
52
"use strict";
53
/**
54
* @class Easyrtc.
55
*
56
* @returns {Easyrtc} the new easyrtc instance.
57
*
58
* @constructs Easyrtc
59
*/
60
var Easyrtc = function() {
61
62
var self = this;
63
64
function logDebug (message, obj) {
65
if (self.debugPrinter) {
66
self.debugPrinter(message, obj);
67
}
68
}
69
70
function isEmptyObj(obj) {
71
if (obj === null || obj === undefined) {
72
return true;
73
}
74
var key;
75
for (key in obj) {
76
if (obj.hasOwnProperty(key)) {
77
return false;
78
}
79
}
80
return true;
81
}
82
83
/** @private */
84
var autoInitUserMedia = true;
85
/** @private */
86
var sdpLocalFilter = null;
87
/** @private */
88
var sdpRemoteFilter = null;
89
/** @private */
90
var iceCandidateFilter = null;
91
/** @private */
92
var iceConnectionStateChangeListener = null;
93
var signalingStateChangeListener = null;
94
/** @private */
95
var connectionOptions =  {
96
'connect timeout': 10000,
97
'force new connection': true
98
};
99
100
/** @private */
101
//
102
// this function replaces the deprecated MediaStream.stop method
103
//
104
function stopStream(stream) {
105
var i;
106
var tracks;
107
108
tracks = stream.getAudioTracks();
109
for( i = 0; i < tracks.length; i++ ) {
110
try {
111
tracks[i].stop();
112
} catch(err){}
113
}
114
tracks = stream.getVideoTracks();
115
for( i = 0; i < tracks.length; i++ ) {
116
try {
117
tracks[i].stop();
118
} catch(err){}
119
}
120
121
if (typeof stream.stop === 'function') {
122
try {
123
stream.stop();
124
} catch(err){}
125
}
126
}
127
128
/**
129
* Sets functions which filter sdp records before calling setLocalDescription or setRemoteDescription.
130
* This is advanced functionality which can break things, easily. See the easyrtc_rates.js file for a
131
* filter builder.
132
* @param {Function} localFilter a function that takes an sdp string and returns an sdp string.
133
* @param {Function} remoteFilter a function that takes an sdp string and returns an sdp string.
134
*/
135
this.setSdpFilters = function(localFilter, remoteFilter) {
136
sdpLocalFilter = localFilter;
137
sdpRemoteFilter = remoteFilter;
138
};
139
140
/**
141
* Sets a function to warn about the peer connection closing.
142
*  @param {Function} handler: a function that gets an easyrtcid as an argument.
143
*/
144
this.setPeerClosedListener = function( handler ) {
145
this.onPeerClosed = handler;
146
};
147
148
/**
149
* Sets a function to warn about the peer connection open.
150
*  @param {Function} handler: a function that gets an easyrtcid as an argument.
151
*/
152
this.setPeerOpenListener = function( handler ) {
153
this.onPeerOpen = handler;
154
};
155
156
/**
157
* Sets a function to receive warnings about the peer connection
158
* failing. The peer connection may recover by itself.
159
*  @param {Function} failingHandler: a function that gets an easyrtcid as an argument.
160
*  @param {Function} recoveredHandler: a function that gets an easyrtcid as an argument.
161
*/
162
this.setPeerFailingListener = function( failingHandler, recoveredHandler ) {
163
this.onPeerFailing = failingHandler;
164
this.onPeerRecovered = recoveredHandler;
165
};
166
167
/**
168
* Sets a function which filters IceCandidate records being sent or received.
169
*
170
* Candidate records can be received while they are being generated locally (before being
171
* sent to a peer), and after they are received by the peer. The filter receives two arguments, the candidate record and a boolean
172
* flag that is true for a candidate being received from another peer,
173
* and false for a candidate that was generated locally. The candidate record has the form:
174
*  {type: 'candidate', label: sdpMLineIndex, id: sdpMid, candidate: candidateString}
175
* The function should return one of the following: the input candidate record, a modified candidate record, or null (indicating that the
176
* candidate should be discarded).
177
* @param {Function} filter
178
*/
179
this.setIceCandidateFilter = function(filter) {
180
iceCandidateFilter = filter;
181
};
182
183
/**
184
* Sets a function that listens on IceConnectionStateChange events.
185
*
186
* During ICE negotiation the peer connection fires the iceconnectionstatechange event.
187
* It is sometimes useful for the application to learn about these changes, especially if the ICE connection fails.
188
* The function should accept three parameters: the easyrtc id of the peer, the iceconnectionstatechange event target and the iceconnectionstate.
189
* @param {Function} listener
190
*/
191
this.setIceConnectionStateChangeListener = function(listener) {
192
iceConnectionStateChangeListener = listener;
193
};
194
195
/**
196
* Sets a function that listens on SignalingStateChange events.
197
*
198
* During ICE negotiation the peer connection fires the signalingstatechange event.
199
* The function should accept three parameters: the easyrtc id of the peer, the signalingstatechange event target and the signalingstate.
200
* @param {Function} listener
201
*/
202
this.setSignalingStateChangeListener = function(listener) {
203
signalingStateChangeListener = listener;
204
};
205
206
/**
207
* Controls whether a default local media stream should be acquired automatically during calls and accepts
208
* if a list of streamNames is not supplied. The default is true, which mimics the behaviour of earlier releases
209
* that didn't support multiple streams. This function should be called before easyrtc.call or before entering an
210
* accept  callback.
211
* @param {Boolean} flag true to allocate a default local media stream.
212
*/
213
this.setAutoInitUserMedia = function(flag) {
214
autoInitUserMedia = !!flag;
215
};
216
217
/**
218
* This function performs a printf like formatting. It actually takes an unlimited
219
* number of arguments, the declared arguments arg1, arg2, arg3 are present just for
220
* documentation purposes.
221
* @param {String} format A string like "abcd{1}efg{2}hij{1}."
222
* @param {String} arg1 The value that replaces {1}
223
* @param {String} arg2 The value that replaces {2}
224
* @param {String} arg3 The value that replaces {3}
225
* @returns {String} the formatted string.
226
*/
227
this.format = function(format, arg1, arg2, arg3) {
228
var formatted = arguments[0];
229
for (var i = 1; i < arguments.length; i++) {
230
var regexp = new RegExp('\\{' + (i - 1) + '\\}', 'gi');
231
formatted = formatted.replace(regexp, arguments[i]);
232
}
233
return formatted;
234
};
235
236
/**
237
* This function checks if a socket is actually connected.
238
* @private
239
* @param {Object} socket a socket.io socket.
240
* @return true if the socket exists and is connected, false otherwise.
241
*/
242
function isSocketConnected(socket) {
243
return socket && (
244
(socket.socket && socket.socket.connected) || socket.connected
245
);
246
}
247
248
/** @private */
249
//
250
// Maps a key to a language specific string using the easyrtc_lang map.
251
// Defaults to the key if the key can not be found, but outputs a warning in that case.
252
// This function is only used internally by easyrtc.js
253
//
254
var haveAudioVideo = {
255
audio: false,
256
video: false
257
};
258
259
/**
260
* @private
261
* @param {String} key
262
*/
263
this.getConstantString = function(key) {
264
if (easyrtc_lang[key]) {
265
return easyrtc_lang[key];
266
}
267
else {
268
self.showError(self.errCodes.DEVELOPER_ERR, "Could not find key='" + key + "' in easyrtc_lang");
269
return key;
270
}
271
};
272
273
/** @private */
274
//
275
// this is a list of the events supported by the generalized event listener.
276
//
277
var allowedEvents = {
278
roomOccupant: true,  // this receives the list of everybody in any room you belong to
279
roomOccupants: true  // this receives a {roomName:..., occupants:...} value for a specific room
280
};
281
282
/** @private */
283
//
284
// A map of eventListeners. The key is the event type.
285
//
286
var eventListeners = {};
287
288
/**
289
* This function checks if an attempt was made to add an event listener or
290
* or emit an unlisted event, since such is typically a typo.
291
* @private
292
* @param {String} eventName
293
* @param {String} callingFunction the name of the calling function.
294
*/
295
function event(eventName, callingFunction) {
296
if (typeof eventName !== 'string') {
297
self.showError(self.errCodes.DEVELOPER_ERR, callingFunction + " called without a string as the first argument");
298
throw "developer error";
299
}
300
if (!allowedEvents[eventName]) {
301
self.showError(self.errCodes.DEVELOPER_ERR, callingFunction + " called with a bad event name = " + eventName);
302
throw "developer error";
303
}
304
}
305
306
/**
307
* Adds an event listener for a particular type of event.
308
* Currently the only eventName supported is "roomOccupant".
309
* @param {String} eventName the type of the event
310
* @param {Function} eventListener the function that expects the event.
311
* The eventListener gets called with the eventName as it's first argument, and the event
312
* data as it's second argument.
313
* @returns {void}
314
*/
315
this.addEventListener = function(eventName, eventListener) {
316
event(eventName, "addEventListener");
317
if (typeof eventListener !== 'function') {
318
self.showError(self.errCodes.DEVELOPER_ERR, "addEventListener called with a non-function for second argument");
319
throw "developer error";
320
}
321
//
322
// remove the event listener if it's already present so we don't end up with two copies
323
//
324
self.removeEventListener(eventName, eventListener);
325
if (!eventListeners[eventName]) {
326
eventListeners[eventName] = [];
327
}
328
eventListeners[eventName][eventListeners[eventName].length] = eventListener;
329
};
330
331
/**
332
* Removes an event listener.
333
* @param {String} eventName
334
* @param {Function} eventListener
335
*/
336
this.removeEventListener = function(eventName, eventListener) {
337
event(eventName, "removeEventListener");
338
var listeners = eventListeners[eventName];
339
var i = 0;
340
if (listeners) {
341
for (i = 0; i < listeners.length; i++) {
342
if (listeners[i] === eventListener) {
343
if (i < listeners.length - 1) {
344
listeners[i] = listeners[listeners.length - 1];
345
}
346
listeners.length = listeners.length - 1;
347
}
348
}
349
}
350
};
351
352
/**
353
* Emits an event, or in other words, calls all the eventListeners for a
354
* particular event.
355
* @param {String} eventName
356
* @param {Object} eventData
357
*/
358
this.emitEvent = function(eventName, eventData) {
359
event(eventName, "emitEvent");
360
var listeners = eventListeners[eventName];
361
var i = 0;
362
if (listeners) {
363
for (i = 0; i < listeners.length; i++) {
364
listeners[i](eventName, eventData);
365
}
366
}
367
};
368
369
/**
370
* Error codes that the EasyRTC will use in the errorCode field of error object passed
371
* to error handler set by easyrtc.setOnError. The error codes are short printable strings.
372
* @type Object
373
*/
374
this.errCodes = {
375
BAD_NAME: "BAD_NAME", // a user name wasn't of the desired form
376
CALL_ERR: "CALL_ERR", // something went wrong creating the peer connection
377
DEVELOPER_ERR: "DEVELOPER_ERR", // the developer using the EasyRTC library made a mistake
378
SYSTEM_ERR: "SYSTEM_ERR", // probably an error related to the network
379
CONNECT_ERR: "CONNECT_ERR", // error occurred when trying to create a connection
380
MEDIA_ERR: "MEDIA_ERR", // unable to get the local media
381
MEDIA_WARNING: "MEDIA_WARNING", // didn't get the desired resolution
382
INTERNAL_ERR: "INTERNAL_ERR",
383
PEER_GONE: "PEER_GONE", // peer doesn't exist
384
ALREADY_CONNECTED: "ALREADY_CONNECTED",
385
BAD_CREDENTIAL: "BAD_CREDENTIAL",
386
ICECANDIDATE_ERR: "ICECANDIDATE_ERR",
387
NOVIABLEICE: "NOVIABLEICE",
388
SIGNAL_ERR: "SIGNAL_ERR"
389
};
390
391
this.apiVersion = "1.1.0";
392
393
/** Most basic message acknowledgment object */
394
this.ackMessage = {msgType: "ack"};
395
396
/** Regular expression pattern for user ids. This will need modification to support non US character sets */
397
this.usernameRegExp = /^(.){1,64}$/;
398
399
/** Default cookieId name */
400
this.cookieId = "easyrtcsid";
401
402
/** @private */
403
var username = null;
404
405
/** Flag to indicate that user is currently logging out */
406
this.loggingOut = false;
407
408
/** @private */
409
this.disconnecting = false;
410
411
/** @private */
412
//
413
// A map of ids to local media streams.
414
//
415
var namedLocalMediaStreams = {};
416
417
/** @private */
418
var sessionFields = [];
419
420
/** @private */
421
var receivedMediaConstraints = {};
422
423
/**
424
* Control whether the client requests audio from a peer during a call.
425
* Must be called before the call to have an effect.
426
* @param value - true to receive audio, false otherwise. The default is true.
427
*/
428
this.enableAudioReceive = function(value) {
429
if (
430
adapter && adapter.browserDetails &&
431
(adapter.browserDetails.browser === "firefox" || adapter.browserDetails.browser === "edge")
432
) {
433
receivedMediaConstraints.offerToReceiveAudio = value;
434
}
435
else {
436
receivedMediaConstraints.mandatory = receivedMediaConstraints.mandatory || {};
437
receivedMediaConstraints.mandatory.OfferToReceiveAudio = value;
438
}
439
};
440
441
/**
442
* Control whether the client requests video from a peer during a call.
443
* Must be called before the call to have an effect.
444
* @param value - true to receive video, false otherwise. The default is true.
445
*/
446
this.enableVideoReceive = function(value) {
447
if (
448
adapter && adapter.browserDetails &&
449
(adapter.browserDetails.browser === "firefox" || adapter.browserDetails.browser === "edge")
450
) {
451
receivedMediaConstraints.offerToReceiveVideo = value;
452
}
453
else {
454
receivedMediaConstraints.mandatory = receivedMediaConstraints.mandatory || {};
455
receivedMediaConstraints.mandatory.OfferToReceiveVideo = value;
456
}
457
};
458
459
// True by default
460
// TODO should not be true by default only for legacy
461
this.enableAudioReceive(true);
462
this.enableVideoReceive(true);
463
464
function getSourceList(callback, sourceType) {
465
navigator.mediaDevices.enumerateDevices().then(
466
function(values) {
467
var results = [];
468
for (var i = 0; i < values.length; i++) {
469
var source = values[i];
470
if (source.kind === sourceType) {
471
source.id = source.deviceId; //backwards compatibility
472
results.push(source);
473
}
474
}
475
callback(results);
476
}
477
).catch(
478
function(reason) {
479
logDebug("Unable to enumerate devices (" + reason + ")");
480
}
481
);
482
}
483
484
/**
485
* Sets the audio output device of a Video object. 
486
* That is to say, this controls what speakers get the sound.
487
* In theory, this works on Chrome but probably doesn't work anywhere else yet.
488
* This code was cribbed from https://webrtc.github.io/samples/src/content/devices/multi/.
489
*  @param {Object} element an HTML5 video element
490
*  @param {String} sinkId a deviceid from getAudioSinkList
491
*/
492
this.setAudioOutput = function(element, sinkId) {
493
if (typeof element.sinkId !== 'undefined') {
494
element.setSinkId(sinkId)
495
.then(function() {
496
logDebug('Success, audio output device attached: ' + sinkId + ' to ' +
497
'element with ' + element.title + ' as source.');
498
})
499
.catch(function(error) {
500
var errorMessage = error;
501
if (error.name === 'SecurityError') {
502
errorMessage = 'You need to use HTTPS for selecting audio output ' +
503
'device: ' + error;
504
}
505
logDebug(errorMessage);
506
});
507
} else {
508
logDebug('Browser does not support output device selection.');
509
}
510
};
511
512
/**
513
* Gets a list of the available audio sinks (ie, speakers)
514
* @param {Function} callback receives list of {deviceId:String, groupId:String, label:String, kind:"audio"}
515
* @example  easyrtc.getAudioSinkList( function(list) {
516
*               var i;
517
*               for( i = 0; i < list.length; i++ ) {
518
*                   console.log("label=" + list[i].label + ", id= " + list[i].deviceId);
519
*               }
520
*          });
521
*/
522
this.getAudioSinkList = function(callback){
523
getSourceList(callback, "audiooutput");
524
};
525
/**
526
* Gets a list of the available audio sources (ie, microphones)
527
* @param {Function} callback receives list of {deviceId:String, groupId:String, label:String, kind:"audio"}
528
* @example  easyrtc.getAudioSourceList( function(list) {
529
*               var i;
530
*               for( i = 0; i < list.length; i++ ) {
531
*                   console.log("label=" + list[i].label + ", id= " + list[i].deviceId);
532
*               }
533
*          });
534
*/
535
this.getAudioSourceList = function(callback){
536
getSourceList(callback, "audioinput");
537
};
538
539
/**
540
* Gets a list of the available video sources (ie, cameras)
541
* @param {Function} callback receives list of {deviceId:String, groupId:String, label:String, kind:"video"}
542
* @example  easyrtc.getVideoSourceList( function(list) {
543
*               var i;
544
*               for( i = 0; i < list.length; i++ ) {
545
*                   console.log("label=" + list[i].label + ", id= " + list[i].deviceId);
546
*               }
547
*          });
548
*/
549
this.getVideoSourceList = function(callback) {
550
getSourceList(callback, "videoinput");
551
};
552
553
554
/** @private */
555
var dataChannelName = "dc";
556
/** @private */
557
var oldConfig = {};
558
/** @private */
559
var offersPending = {};
560
/** @private */
561
var credential = null;
562
563
/** @private */
564
self.audioEnabled = true;
565
/** @private */
566
self.videoEnabled = true;
567
/** @private */
568
this.debugPrinter = null;
569
/** Your easyrtcid */
570
this.myEasyrtcid = "";
571
572
/** The height of the local media stream video in pixels. This field is set an indeterminate period
573
* of time after easyrtc.initMediaSource succeeds. Note: in actuality, the dimensions of a video stream
574
* change dynamically in response to external factors, you should check the videoWidth and videoHeight attributes
575
* of your video objects before you use them for pixel specific operations.
576
*/
577
this.nativeVideoHeight = 0;
578
579
/** This constant determines how long (in bytes) a message can be before being split in chunks of that size.
580
* This is because there is a limitation of the length of the message you can send on the
581
* data channel between browsers.
582
*/
583
this.maxP2PMessageLength = 1000;
584
585
/** The width of the local media stream video in pixels. This field is set an indeterminate period
586
* of time after easyrtc.initMediaSource succeeds.  Note: in actuality, the dimensions of a video stream
587
* change dynamically in response to external factors, you should check the videoWidth and videoHeight attributes
588
* of your video objects before you use them for pixel specific operations.
589
*/
590
this.nativeVideoWidth = 0;
591
592
/** The rooms the user is in. This only applies to room oriented applications and is set at the same
593
* time a token is received.
594
*/
595
this.roomJoin = {};
596
597
/** Checks if the supplied string is a valid user name (standard identifier rules)
598
* @param {String} name
599
* @return {Boolean} true for a valid user name
600
* @example
601
*    var name = document.getElementById('nameField').value;
602
*    if( !easyrtc.isNameValid(name)){
603
*        console.error("Bad user name");
604
*    }
605
*/
606
this.isNameValid = function(name) {
607
return self.usernameRegExp.test(name);
608
};
609
610
/**
611
* This function sets the name of the cookie that client side library will look for
612
* and transmit back to the server as it's easyrtcsid in the first message.
613
* @param {String} cookieId
614
*/
615
this.setCookieId = function(cookieId) {
616
self.cookieId = cookieId;
617
};
618
619
/** @private */
620
this._desiredVideoProperties = {}; // default camera
621
622
/**
623
* Specify particular video source. Call this before you call easyrtc.initMediaSource().
624
* @param {String} videoSrcId is a id value from one of the entries fetched by getVideoSourceList. null for default.
625
* @example easyrtc.setVideoSource( videoSrcId);
626
*/
627
this.setVideoSource = function(videoSrcId) {
628
self._desiredVideoProperties.videoSrcId = videoSrcId;
629
delete self._desiredVideoProperties.screenCapture;
630
};
631
632
/** @private */
633
this._desiredAudioProperties = {}; // default camera
634
635
/**
636
* Specify particular video source. Call this before you call easyrtc.initMediaSource().
637
* @param {String} audioSrcId is a id value from one of the entries fetched by getAudioSourceList. null for default.
638
* @example easyrtc.setAudioSource( audioSrcId);
639
*/
640
this.setAudioSource = function(audioSrcId) {
641
self._desiredAudioProperties.audioSrcId = audioSrcId;
642
};
643
644
/** This function is used to set the dimensions of the local camera, usually to get HD.
645
*  If called, it must be called before calling easyrtc.initMediaSource (explicitly or implicitly).
646
*  assuming it is supported. If you don't pass any parameters, it will use default camera dimensions.
647
* @param {Number} width in pixels
648
* @param {Number} height in pixels
649
* @param {number} frameRate is optional
650
* @example
651
*    easyrtc.setVideoDims(1280,720);
652
* @example
653
*    easyrtc.setVideoDims();
654
*/
655
this.setVideoDims = function(width, height, frameRate) {
656
self._desiredVideoProperties.width = width;
657
self._desiredVideoProperties.height = height;
658
if (frameRate !== undefined) {
659
self._desiredVideoProperties.frameRate = frameRate;
660
}
661
};
662
663
/** This function requests that screen capturing be used to provide the local media source
664
* rather than a webcam. If you have multiple screens, they are composited side by side.
665
* Note: this functionality is not supported by Firefox, has to be called before calling initMediaSource (or easyApp), we don't currently supply a way to
666
* turn it off (once it's on), only works if the website is hosted SSL (https), and the image quality is rather
667
* poor going across a network because it tries to transmit so much data. In short, screen sharing
668
* through WebRTC isn't worth using at this point, but it is provided here so people can try it out.
669
* @example
670
*    easyrtc.setScreenCapture();
671
* @deprecated: use easyrtc.initScreenCapture (same parameters as easyrtc.initMediaSource.
672
*/
673
this.setScreenCapture = function(enableScreenCapture) {
674
self._desiredVideoProperties.screenCapture = (enableScreenCapture !== false);
675
};
676
677
/**
678
* Builds the constraint object passed to getUserMedia.
679
* @returns {Object} mediaConstraints
680
*/
681
self.getUserMediaConstraints = function() {
682
var constraints = {};
683
//
684
// _presetMediaConstraints allow you to provide your own constraints to be used
685
// with initMediaSource.
686
//
687
if (self._presetMediaConstraints) {
688
constraints = self._presetMediaConstraints;
689
delete self._presetMediaConstraints;
690
return constraints;
691
}
692
else if (self._desiredVideoProperties.screenCapture) {
693
return {
694
video: {
695
mandatory: {
696
chromeMediaSource: 'screen',
697
maxWidth: screen.width,
698
maxHeight: screen.height,
699
minWidth: screen.width,
700
minHeight: screen.height,
701
minFrameRate: 1,
702
maxFrameRate: 5},
703
optional: []
704
},
705
audio: false
706
};
707
}
708
else if (!self.videoEnabled) {
709
constraints.video = false;
710
}
711
else {
712
713
// Tested Firefox 49 and MS Edge require minFrameRate and maxFrameRate 
714
// instead max,min,ideal that cause GetUserMedia failure.
715
// Until confirmed both browser support idea,max and min we need this.
716
if (
717
adapter && adapter.browserDetails &&
718
(adapter.browserDetails.browser === "firefox" || adapter.browserDetails.browser === "edge")
719
) {
720
constraints.video = {};
721
if (self._desiredVideoProperties.width) {
722
constraints.video.width = self._desiredVideoProperties.width;
723
}
724
if (self._desiredVideoProperties.height) {
725
constraints.video.height = self._desiredVideoProperties.height;
726
}
727
if (self._desiredVideoProperties.frameRate) {
728
constraints.video.frameRate = { 
729
minFrameRate: self._desiredVideoProperties.frameRate,
730
maxFrameRate: self._desiredVideoProperties.frameRate
731
};
732
}
733
if (self._desiredVideoProperties.videoSrcId) {
734
constraints.video.deviceId = self._desiredVideoProperties.videoSrcId;
735
}
736
737
// chrome and opera
738
} else { 
739
constraints.video = {};
740
if (self._desiredVideoProperties.width) {
741
constraints.video.width = { 
742
max: self._desiredVideoProperties.width,
743
min : self._desiredVideoProperties.width,
744
ideal : self._desiredVideoProperties.width 
745
};
746
}
747
if (self._desiredVideoProperties.height) {
748
constraints.video.height = {
749
max: self._desiredVideoProperties.height,
750
min: self._desiredVideoProperties.height,
751
ideal: self._desiredVideoProperties.height
752
};
753
}
754
if (self._desiredVideoProperties.frameRate) {
755
constraints.video.frameRate = {
756
max: self._desiredVideoProperties.frameRate,
757
ideal: self._desiredVideoProperties.frameRate
758
};
759
}
760
if (self._desiredVideoProperties.videoSrcId) {
761
constraints.video.deviceId = self._desiredVideoProperties.videoSrcId;
762
}
763
// hack for opera
764
if (Object.keys(constraints.video).length === 0 ) {
765
constraints.video = true;
766
}
767
}
768
}
769
770
if (!self.audioEnabled) {
771
constraints.audio = false;
772
}
773
else {
774
if (adapter && adapter.browserDetails && adapter.browserDetails.browser === "firefox") {
775
constraints.audio = {};
776
if (self._desiredAudioProperties.audioSrcId) {
777
constraints.audio.deviceId = self._desiredAudioProperties.audioSrcId;
778
}
779
}
780
else { // chrome and opera
781
constraints.audio = {mandatory: {}, optional: []};
782
if (self._desiredAudioProperties.audioSrcId) {
783
constraints.audio.optional = constraints.audio.optional || [];
784
constraints.audio.optional.push({deviceId: self._desiredAudioProperties.audioSrcId});
785
}
786
}
787
}
788
return constraints;
789
};
790
791
/** Set the application name. Applications can only communicate with other applications
792
* that share the same API Key and application name. There is no predefined set of application
793
* names. Maximum length is
794
* @param {String} name
795
* @example
796
*    easyrtc.setApplicationName('simpleAudioVideo');
797
*/
798
this.setApplicationName = function(name) {
799
self.applicationName = name;
800
};
801
802
/** Enable or disable logging to the console.
803
* Note: if you want to control the printing of debug messages, override the
804
*    easyrtc.debugPrinter variable with a function that takes a message string as it's argument.
805
*    This is exactly what easyrtc.enableDebug does when it's enable argument is true.
806
* @param {Boolean} enable - true to turn on debugging, false to turn off debugging. Default is false.
807
* @example
808
*    easyrtc.enableDebug(true);
809
*/
810
this.enableDebug = function(enable) {
811
if (enable) {
812
self.debugPrinter = function(message, obj) {
813
var now = new Date().toISOString();
814
var stackString = new Error().stack;
815
var srcLine = "location unknown";
816
if (stackString) {
817
var stackFrameStrings = stackString.split('\n');
818
srcLine = "";
819
if (stackFrameStrings.length >= 5) {
820
srcLine = stackFrameStrings[4];
821
}
822
}
823
824
console.log("debug " + now + " : " + message + " [" + srcLine + "]");
825
826
if (typeof obj !== 'undefined') {
827
console.log("debug " + now + " : ", obj);
828
}
829
};
830
}
831
else {
832
self.debugPrinter = null;
833
}
834
};
835
836
/**
837
* Determines if the local browser supports WebRTC GetUserMedia (access to camera and microphone).
838
* @returns {Boolean} True getUserMedia is supported.
839
*/
840
this.supportsGetUserMedia = function() {
841
return typeof navigator.getUserMedia !== 'undefined';
842
};
843
844
/**
845
* Determines if the local browser supports WebRTC Peer connections to the extent of being able to do video chats.
846
* @returns {Boolean} True if Peer connections are supported.
847
*/
848
this.supportsPeerConnections = function() {
849
return typeof RTCPeerConnection !== 'undefined';
850
};
851
852
/** Determines whether the current browser supports the new data channels.
853
* EasyRTC will not open up connections with the old data channels.
854
* @returns {Boolean}
855
*/
856
this.supportsDataChannels = function() {
857
858
var hasCreateDataChannel = false;
859
860
if (self.supportsPeerConnections()) {
861
try {
862
var peer = new RTCPeerConnection({iceServers: []}, {});
863
hasCreateDataChannel = typeof peer.createDataChannel !== 'undefined';
864
peer.close();
865
}
866
catch (err) {
867
// Ignore possible RTCPeerConnection.close error
868
// hasCreateDataChannel should reflect the feature state still.
869
}
870
}
871
872
return hasCreateDataChannel;
873
};
874
875
/** @private */
876
//
877
// Experimental function to determine if statistics gathering is supported.
878
//
879
this.supportsStatistics = function() {
880
881
var hasGetStats = false;
882
883
if (self.supportsPeerConnections()) {
884
try {
885
var peer = new RTCPeerConnection({iceServers: []}, {});
886
hasGetStats = typeof peer.getStats !== 'undefined';
887
peer.close();
888
}
889
catch (err) {
890
// Ingore possible RTCPeerConnection.close error
891
// hasCreateDataChannel should reflect the feature state still.
892
}
893
}
894
895
return hasGetStats;
896
};
897
898
/** @private
899
* @param {Array} pc_config ice configuration array
900
* @param {Object} optionalStuff peer constraints.
901
*/
902
this.createRTCPeerConnection = function(pc_config, optionalStuff) {
903
if (self.supportsPeerConnections()) {
904
return new RTCPeerConnection(pc_config, optionalStuff);
905
}
906
else {
907
throw "Your browser doesn't support webRTC (RTCPeerConnection)";
908
}
909
};
910
911
//
912
// this should really be part of adapter.js
913
// Versions of chrome < 31 don't support reliable data channels transport.
914
// Firefox does.
915
//
916
this.getDatachannelConstraints = function() {
917
return {
918
reliable: adapter && adapter.browserDetails &&
919
adapter.browserDetails.browser !== "chrome" &&
920
adapter.browserDetails.version < 31
921
};
922
};
923
924
/** @private */
925
haveAudioVideo = {
926
audio: false,
927
video: false
928
};
929
/** @private */
930
var dataEnabled = false;
931
/** @private */
932
var serverPath = null; // this was null, but that was generating an error.
933
/** @private */
934
var roomOccupantListener = null;
935
/** @private */
936
var onDataChannelOpen = null;
937
/** @private */
938
var onDataChannelClose = null;
939
/** @private */
940
var lastLoggedInList = {};
941
/** @private */
942
var receivePeer = {msgTypes: {}};
943
/** @private */
944
var receiveServerCB = null;
945
/** @private */
946
// dummy placeholder for when we aren't connected
947
var updateConfigurationInfo = function() { };
948
/** @private */
949
//
950
//
951
//  peerConns is a map from caller names to the below object structure
952
//     {  startedAV: boolean,  -- true if we have traded audio/video streams
953
//        dataChannelS: RTPDataChannel for outgoing messages if present
954
//        dataChannelR: RTPDataChannel for incoming messages if present
955
//        dataChannelReady: true if the data channel can be used for sending yet
956
//        connectTime: timestamp when the connection was started
957
//        sharingAudio: true if audio is being shared
958
//        sharingVideo: true if video is being shared
959
//        cancelled: temporarily true if a connection was cancelled by the peer asking to initiate it
960
//        candidatesToSend: SDP candidates temporarily queued
961
//        streamsAddedAcks: ack callbacks waiting for stream received messages
962
//        pc: RTCPeerConnection
963
//        mediaStream: mediaStream
964
//     function callSuccessCB(string) - see the easyrtc.call documentation.
965
//        function callFailureCB(errorCode, string) - see the easyrtc.call documentation.
966
//        function wasAcceptedCB(boolean,string) - see the easyrtc.call documentation.
967
//     }
968
//
969
var peerConns = {};
970
/** @private */
971
//
972
// a map keeping track of whom we've requested a call with so we don't try to
973
// call them a second time before they've responded.
974
//
975
var acceptancePending = {};
976
977
/** @private
978
* @param {string} caller
979
* @param {Function} helper
980
*/
981
this.acceptCheck = function(caller, helper) {
982
helper(true);
983
};
984
985
/** @private
986
* @param {string} easyrtcid
987
* @param {HTMLMediaStream} stream
988
*/
989
this.streamAcceptor = function(easyrtcid, stream) {
990
};
991
992
/** @private
993
* @param {string} easyrtcid
994
*/
995
this.onStreamClosed = function(easyrtcid) {
996
};
997
998
/** @private
999
* @param {string} easyrtcid
1000
*/
1001
this.callCancelled = function(easyrtcid) {
1002
};
1003
1004
/**
1005
* This function gets the raw RTCPeerConnection for a given easyrtcid
1006
* @param {String} easyrtcid
1007
* @param {RTCPeerConnection} for that easyrtcid, or null if no connection exists
1008
* Submitted by Fabian Bernhard.
1009
*/
1010
this.getPeerConnectionByUserId = function(userId) {
1011
if (peerConns && peerConns[userId]) {
1012
return peerConns[userId].pc;
1013
}
1014
return null;
1015
};
1016
1017
1018
var chromeStatsFilter = [
1019
{
1020
"googTransmitBitrate": "transmitBitRate",
1021
"googActualEncBitrate": "encodeRate",
1022
"googAvailableSendBandwidth": "availableSendRate"
1023
},
1024
{
1025
"googCodecName": "audioCodec",
1026
"googTypingNoiseState": "typingNoise",
1027
"packetsSent": "audioPacketsSent",
1028
"bytesSent": "audioBytesSent"
1029
},
1030
{
1031
"googCodecName": "videoCodec",
1032
"googFrameRateSent": "outFrameRate",
1033
"packetsSent": "videoPacketsSent",
1034
"bytesSent": "videoBytesSent"
1035
},
1036
{
1037
"packetsLost": "videoPacketsLost",
1038
"packetsReceived": "videoPacketsReceived",
1039
"bytesReceived": "videoBytesReceived",
1040
"googFrameRateOutput": "frameRateOut"
1041
},
1042
{
1043
"packetsLost": "audioPacketsLost",
1044
"packetsReceived": "audioPacketsReceived",
1045
"bytesReceived": "audioBytesReceived",
1046
"audioOutputLevel": "audioOutputLevel"
1047
},
1048
{
1049
"googRemoteAddress": "remoteAddress",
1050
"googActiveConnection": "activeConnection"
1051
},
1052
{
1053
"audioInputLevel": "audioInputLevel"
1054
}
1055
];
1056
1057
var firefoxStatsFilter = {
1058
"outboundrtp_audio.bytesSent": "audioBytesSent",
1059
"outboundrtp_video.bytesSent": "videoBytesSent",
1060
"inboundrtp_video.bytesReceived": "videoBytesReceived",
1061
"inboundrtp_audio.bytesReceived": "audioBytesReceived",
1062
"outboundrtp_audio.packetsSent": "audioPacketsSent",
1063
"outboundrtp_video.packetsSent": "videoPacketsSent",
1064
"inboundrtp_video.packetsReceived": "videoPacketsReceived",
1065
"inboundrtp_audio.packetsReceived": "audioPacketsReceived",
1066
"inboundrtp_video.packetsLost": "videoPacketsLost",
1067
"inboundrtp_audio.packetsLost": "audioPacketsLost",
1068
"firefoxRemoteAddress": "remoteAddress"
1069
};
1070
1071
var standardStatsFilter = adapter && adapter.browserDetails &&
1072
adapter.browserDetails.browser === "firefox" ? firefoxStatsFilter : chromeStatsFilter;
1073
1074
function getFirefoxPeerStatistics(peerId, callback, filter) {
1075
1076
1077
if (!peerConns[peerId]) {
1078
callback(peerId, {"connected": false});
1079
}
1080
else if (peerConns[peerId].pc.getStats) {
1081
peerConns[peerId].pc.getStats(null, function(stats) {
1082
var items = {};
1083
var candidates = {};
1084
var activeId = null;
1085
var srcKey;
1086
//
1087
// the stats objects has a group of entries. Each entry is either an rtcp, rtp entry
1088
// or a candidate entry.
1089
//
1090
if (stats) {
1091
stats.forEach(function(entry) {
1092
var majorKey;
1093
var subKey;
1094
if (entry.type.match(/boundrtp/)) {
1095
if (entry.id.match(/audio/)) {
1096
majorKey = entry.type + "_audio";
1097
}
1098
else if (entry.id.match(/video/)) {
1099
majorKey = entry.type + "_video";
1100
}
1101
else {
1102
return;
1103
}
1104
for (subKey in entry) {
1105
if (entry.hasOwnProperty(subKey)) {
1106
items[majorKey + "." + subKey] = entry[subKey];
1107
}
1108
}
1109
}
1110
else {
1111
if( entry.hasOwnProperty("ipAddress") && entry.id) {
1112
candidates[entry.id] = entry.ipAddress + ":" +
1113
entry.portNumber;
1114
}
1115
else if( entry.hasOwnProperty("selected") &&
1116
entry.hasOwnProperty("remoteCandidateId") &&
1117
entry.selected ) {
1118
activeId =  entry.remoteCandidateId;
1119
}
1120
}
1121
});
1122
}
1123
1124
if( activeId ) {
1125
items["firefoxRemoteAddress"] = candidates[activeId];
1126
}
1127
if (!filter) {
1128
callback(peerId, items);
1129
}
1130
else {
1131
var filteredItems = {};
1132
for (srcKey in filter) {
1133
if (filter.hasOwnProperty(srcKey) && items.hasOwnProperty(srcKey)) {
1134
filteredItems[ filter[srcKey]] = items[srcKey];
1135
}
1136
}
1137
callback(peerId, filteredItems);
1138
}
1139
},
1140
function(error) {
1141
logDebug("unable to get statistics");
1142
});
1143
}
1144
else {
1145
callback(peerId, {"statistics": self.getConstantString("statsNotSupported")});
1146
}
1147
}
1148
1149
function getChromePeerStatistics(peerId, callback, filter) {
1150
1151
if (!peerConns[peerId]) {
1152
callback(peerId, {"connected": false});
1153
}
1154
else if (peerConns[peerId].pc.getStats) {
1155
1156
peerConns[peerId].pc.getStats(function(stats) {
1157
1158
var localStats = {};
1159
var part, parts = stats.result();
1160
var i, j;
1161
var itemKeys;
1162
var itemKey;
1163
var names;
1164
var userKey;
1165
var partNames = [];
1166
var partList;
1167
var bestBytes = 0;
1168
var bestI;
1169
var turnAddress = null;
1170
var hasActive, curReceived;
1171
var localAddress, remoteAddress;
1172
if (!filter) {
1173
for (i = 0; i < parts.length; i++) {
1174
names = parts[i].names();
1175
for (j = 0; j < names.length; j++) {
1176
itemKey = names[j];
1177
localStats[parts[i].id + "." + itemKey] = parts[i].stat(itemKey);
1178
}
1179
}
1180
}
1181
else {
1182
for (i = 0; i < parts.length; i++) {
1183
partNames[i] = {};
1184
//
1185
// convert the names into a dictionary
1186
//
1187
names = parts[i].names();
1188
for (j = 0; j < names.length; j++) {
1189
partNames[i][names[j]] = true;
1190
}
1191
1192
//
1193
// a chrome-firefox connection results in several activeConnections.
1194
// we only want one, so we look for the one with the most data being received on it.
1195
//
1196
if (partNames[i].googRemoteAddress && partNames[i].googActiveConnection) {
1197
hasActive = parts[i].stat("googActiveConnection");
1198
if (hasActive === true || hasActive === "true") {
1199
curReceived = parseInt(parts[i].stat("bytesReceived")) +
1200
parseInt(parts[i].stat("bytesSent"));
1201
if (curReceived > bestBytes) {
1202
bestI = i;
1203
bestBytes = curReceived;
1204
}
1205
}
1206
}
1207
}
1208
1209
for (i = 0; i < parts.length; i++) {
1210
//
1211
// discard info from any inactive connection.
1212
//
1213
if (partNames[i].googActiveConnection) {
1214
if (i !== bestI) {
1215
partNames[i] = {};
1216
}
1217
else {
1218
localAddress = parts[i].stat("googLocalAddress").split(":")[0];
1219
remoteAddress = parts[i].stat("googRemoteAddress").split(":")[0];
1220
if (self.isTurnServer(localAddress)) {
1221
turnAddress = localAddress;
1222
}
1223
else if (self.isTurnServer(remoteAddress)) {
1224
turnAddress = remoteAddress;
1225
}
1226
}
1227
}
1228
}
1229
1230
for (i = 0; i < filter.length; i++) {
1231
itemKeys = filter[i];
1232
partList = [];
1233
part = null;
1234
for (j = 0; j < parts.length; j++) {
1235
var fullMatch = true;
1236
for (itemKey in itemKeys) {
1237
if (itemKeys.hasOwnProperty(itemKey) && !partNames[j][itemKey]) {
1238
fullMatch = false;
1239
break;
1240
}
1241
}
1242
if (fullMatch && parts[j]) {
1243
partList.push(parts[j]);
1244
}
1245
}
1246
if (partList.length === 1) {
1247
for (j = 0; j < partList.length; j++) {
1248
part = partList[j];
1249
if (part) {
1250
for (itemKey in itemKeys) {
1251
if (itemKeys.hasOwnProperty(itemKey)) {
1252
userKey = itemKeys[itemKey];
1253
localStats[userKey] = part.stat(itemKey);
1254
}
1255
}
1256
}
1257
}
1258
}
1259
else if (partList.length > 1) {
1260
for (itemKey in itemKeys) {
1261
if (itemKeys.hasOwnProperty(itemKey)) {
1262
localStats[itemKeys[itemKey]] = [];
1263
}
1264
}
1265
for (j = 0; j < partList.length; j++) {
1266
part = partList[j];
1267
for (itemKey in itemKeys) {
1268
if (itemKeys.hasOwnProperty(itemKey)) {
1269
userKey = itemKeys[itemKey];
1270
localStats[userKey].push(part.stat(itemKey));
1271
}
1272
}
1273
}
1274
}
1275
}
1276
}
1277
1278
if (localStats.remoteAddress && turnAddress) {
1279
localStats.remoteAddress = turnAddress;
1280
}
1281
callback(peerId, localStats);
1282
});
1283
}
1284
else {
1285
callback(peerId, {"statistics": self.getConstantString("statsNotSupported")});
1286
}
1287
}
1288
1289
/**
1290
* This function gets the statistics for a particular peer connection.
1291
* @param {String} easyrtcid
1292
* @param {Function} callback gets the easyrtcid for the peer and a map of {userDefinedKey: value}. If there is no peer connection to easyrtcid, then the map will
1293
*  have a value of {connected:false}.
1294
* @param {Object} filter depends on whether Chrome or Firefox is used. See the default filters for guidance.
1295
* It is still experimental.
1296
*/
1297
this.getPeerStatistics = function(easyrtcid, callback, filter) {
1298
if (
1299
adapter && adapter.browserDetails &&
1300
adapter.browserDetails.browser === "firefox"
1301
) {
1302
getFirefoxPeerStatistics(easyrtcid, callback, filter);
1303
}
1304
else {
1305
getChromePeerStatistics(easyrtcid, callback, filter);
1306
}
1307
};
1308
1309
/**
1310
* @private
1311
* @param roomName
1312
* @param fields
1313
*/
1314
function sendRoomApiFields(roomName, fields) {
1315
var fieldAsString = JSON.stringify(fields);
1316
JSON.parse(fieldAsString);
1317
var dataToShip = {
1318
msgType: "setRoomApiField",
1319
msgData: {
1320
setRoomApiField: {
1321
roomName: roomName,
1322
field: fields
1323
}
1324
}
1325
};
1326
self.webSocket.json.emit("easyrtcCmd", dataToShip,
1327
function(ackMsg) {
1328
if (ackMsg.msgType === "error") {
1329
self.showError(ackMsg.msgData.errorCode, ackMsg.msgData.errorText);
1330
}
1331
}
1332
);
1333
}
1334
1335
/** @private */
1336
var roomApiFieldTimer = null;
1337
1338
/**
1339
* @private
1340
* @param {String} roomName
1341
*/
1342
function enqueueSendRoomApi(roomName) {
1343
//
1344
// Rather than issue the send request immediately, we set a timer so we can accumulate other
1345
// calls
1346
//
1347
if (roomApiFieldTimer) {
1348
clearTimeout(roomApiFieldTimer);
1349
}
1350
roomApiFieldTimer = setTimeout(function() {
1351
sendRoomApiFields(roomName, self._roomApiFields[roomName]);
1352
roomApiFieldTimer = null;
1353
}, 10);
1354
}
1355
1356
/** Provide a set of application defined fields that will be part of this instances
1357
* configuration information. This data will get sent to other peers via the websocket
1358
* path.
1359
* @param {String} roomName - the room the field is attached to.
1360
* @param {String} fieldName - the name of the field.
1361
* @param {Object} fieldValue - the value of the field.
1362
* @example
1363
*   easyrtc.setRoomApiField("trekkieRoom",  "favorite_alien", "Mr Spock");
1364
*   easyrtc.setRoomOccupantListener( function(roomName, list){
1365
*      for( var i in list ){
1366
*         console.log("easyrtcid=" + i + " favorite alien is " + list[i].apiFields.favorite_alien);
1367
*      }
1368
*   });
1369
*/
1370
this.setRoomApiField = function(roomName, fieldName, fieldValue) {
1371
//
1372
// if we're not connected yet, we'll just cache the fields until we are.
1373
//
1374
if (!self._roomApiFields) {
1375
self._roomApiFields = {};
1376
}
1377
if (!fieldName && !fieldValue) {
1378
delete self._roomApiFields[roomName];
1379
return;
1380
}
1381
1382
if (!self._roomApiFields[roomName]) {
1383
self._roomApiFields[roomName] = {};
1384
}
1385
if (fieldValue !== undefined && fieldValue !== null) {
1386
if (typeof fieldValue === "object") {
1387
try {
1388
JSON.stringify(fieldValue);
1389
}
1390
catch (jsonError) {
1391
self.showError(self.errCodes.DEVELOPER_ERR, "easyrtc.setRoomApiField passed bad object ");
1392
return;
1393
}
1394
}
1395
self._roomApiFields[roomName][fieldName] = {fieldName: fieldName, fieldValue: fieldValue};
1396
}
1397
else {
1398
delete self._roomApiFields[roomName][fieldName];
1399
}
1400
if (self.webSocketConnected) {
1401
enqueueSendRoomApi(roomName);
1402
}
1403
};
1404
1405
/**
1406
* Default error reporting function. The default implementation displays error messages
1407
* in a programmatically created div with the id easyrtcErrorDialog. The div has title
1408
* component with a class name of easyrtcErrorDialog_title. The error messages get added to a
1409
* container with the id easyrtcErrorDialog_body. Each error message is a text node inside a div
1410
* with a class of easyrtcErrorDialog_element. There is an "okay" button with the className of easyrtcErrorDialog_okayButton.
1411
* @param {String} messageCode An error message code
1412
* @param {String} message the error message text without any markup.
1413
* @example
1414
*     easyrtc.showError("BAD_NAME", "Invalid username");
1415
*/
1416
this.showError = function(messageCode, message) {
1417
self.onError({errorCode: messageCode, errorText: message});
1418
};
1419
1420
/**
1421
* @private
1422
* @param errorObject
1423
*/
1424
this.onError = function(errorObject) {
1425
logDebug("saw error " + errorObject.errorText);
1426
1427
var errorDiv = document.getElementById('easyrtcErrorDialog');
1428
var errorBody;
1429
if (!errorDiv) {
1430
errorDiv = document.createElement("div");
1431
errorDiv.id = 'easyrtcErrorDialog';
1432
var title = document.createElement("div");
1433
title.innerHTML = "Error messages";
1434
title.className = "easyrtcErrorDialog_title";
1435
errorDiv.appendChild(title);
1436
errorBody = document.createElement("div");
1437
errorBody.id = "easyrtcErrorDialog_body";
1438
errorDiv.appendChild(errorBody);
1439
var clearButton = document.createElement("button");
1440
clearButton.appendChild(document.createTextNode("Okay"));
1441
clearButton.className = "easyrtcErrorDialog_okayButton";
1442
clearButton.onclick = function() {
1443
errorBody.innerHTML = ""; // remove all inner nodes
1444
errorDiv.style.display = "none";
1445
};
1446
errorDiv.appendChild(clearButton);
1447
document.body.appendChild(errorDiv);
1448
}
1449
1450
errorBody = document.getElementById("easyrtcErrorDialog_body");
1451
var messageNode = document.createElement("div");
1452
messageNode.className = 'easyrtcErrorDialog_element';
1453
messageNode.appendChild(document.createTextNode(errorObject.errorText));
1454
errorBody.appendChild(messageNode);
1455
errorDiv.style.display = "block";
1456
};
1457
1458
/** @private
1459
* @param mediaStream */
1460
//
1461
// easyrtc.createObjectURL builds a URL from a media stream.
1462
// Arguments:
1463
//     mediaStream - a media stream object.
1464
// The video object in Chrome expects a URL.
1465
//
1466
this.createObjectURL = function(mediaStream) {
1467
var errMessage;
1468
if (window.URL && window.URL.createObjectURL) {
1469
return window.URL.createObjectURL(mediaStream);
1470
}
1471
else if (window.webkitURL && window.webkitURL.createObjectURL) {
1472
return window.webkit.createObjectURL(mediaStream);
1473
}
1474
else {
1475
errMessage = "Your browsers does not support URL.createObjectURL.";
1476
logDebug("saw exception " + errMessage);
1477
throw errMessage;
1478
}
1479
};
1480
1481
/**
1482
* A convenience function to ensure that a string doesn't have symbols that will be interpreted by HTML.
1483
* @param {String} idString
1484
* @return {String} The cleaned string.
1485
* @example
1486
*   console.log( easyrtc.cleanId('&hello'));
1487
*/
1488
this.cleanId = function(idString) {
1489
var MAP = {
1490
'&': '&amp;',
1491
'<': '&lt;',
1492
'>': '&gt;'
1493
};
1494
return idString.replace(/[&<>]/g, function(c) {
1495
return MAP[c];
1496
});
1497
};
1498
1499
/**
1500
* Set a callback that will be invoked when the application enters or leaves a room.
1501
* @param {Function} handler - the first parameter is true for entering a room, false for leaving a room. The second parameter is the room name.
1502
* @example
1503
*   easyrtc.setRoomEntryListener(function(entry, roomName){
1504
*       if( entry ){
1505
*           console.log("entering room " + roomName);
1506
*       }
1507
*       else{
1508
*           console.log("leaving room " + roomName);
1509
*       }
1510
*   });
1511
*/
1512
self.setRoomEntryListener = function(handler) {
1513
self.roomEntryListener = handler;
1514
};
1515
1516
/**
1517
* Set the callback that will be invoked when the list of people logged in changes.
1518
* The callback expects to receive a room name argument, and
1519
* a map whose ideas are easyrtcids and whose values are in turn maps
1520
* supplying user specific information. The inner maps have the following keys:
1521
* username, applicationName, browserFamily, browserMajor, osFamily, osMajor, deviceFamily.
1522
* The third argument is the listener is the innerMap for the connections own data (not needed by most applications).
1523
* @param {Function} listener
1524
* @example
1525
*   easyrtc.setRoomOccupantListener( function(roomName, list, selfInfo){
1526
*      for( var i in list ){
1527
*         ("easyrtcid=" + i + " belongs to user " + list[i].username);
1528
*      }
1529
*   });
1530
*/
1531
self.setRoomOccupantListener = function(listener) {
1532
roomOccupantListener = listener;
1533
};
1534
1535
/**
1536
* Sets a callback that is called when a data channel is open and ready to send data.
1537
* The callback will be called with an easyrtcid as it's sole argument.
1538
* @param {Function} listener
1539
* @example
1540
*    easyrtc.setDataChannelOpenListener( function(easyrtcid){
1541
*         easyrtc.sendDataP2P(easyrtcid, "greeting", "hello");
1542
*    });
1543
*/
1544
this.setDataChannelOpenListener = function(listener) {
1545
onDataChannelOpen = listener;
1546
};
1547
1548
/** Sets a callback that is called when a previously open data channel closes.
1549
* The callback will be called with an easyrtcid as it's sole argument.
1550
* @param {Function} listener
1551
* @example
1552
*    easyrtc.setDataChannelCloseListener( function(easyrtcid){
1553
*            ("No longer connected to " + easyrtc.idToName(easyrtcid));
1554
*    });
1555
*/
1556
this.setDataChannelCloseListener = function(listener) {
1557
onDataChannelClose = listener;
1558
};
1559
1560
/** Returns the number of live peer connections the client has.
1561
* @return {Number}
1562
* @example
1563
*    ("You have " + easyrtc.getConnectionCount() + " peer connections");
1564
*/
1565
this.getConnectionCount = function() {
1566
var count = 0;
1567
var i;
1568
for (i in peerConns) {
1569
if (peerConns.hasOwnProperty(i)) {
1570
if (self.getConnectStatus(i) === self.IS_CONNECTED) {
1571
count++;
1572
}
1573
}
1574
}
1575
return count;
1576
};
1577
1578
/** Sets the maximum length in bytes of P2P messages that can be sent.
1579
* @param {Number} maxLength maximum length to set
1580
* @example
1581
*     easyrtc.setMaxP2PMessageLength(10000);
1582
*/
1583
this.setMaxP2PMessageLength = function(maxLength) {
1584
this.maxP2PMessageLength = maxLength;
1585
};
1586
1587
/** Sets whether audio is transmitted by the local user in any subsequent calls.
1588
* @param {Boolean} enabled true to include audio, false to exclude audio. The default is true.
1589
* @example
1590
*      easyrtc.enableAudio(false);
1591
*/
1592
this.enableAudio = function(enabled) {
1593
self.audioEnabled = enabled;
1594
};
1595
1596
/**
1597
*Sets whether video is transmitted by the local user in any subsequent calls.
1598
* @param {Boolean} enabled - true to include video, false to exclude video. The default is true.
1599
* @example
1600
*      easyrtc.enableVideo(false);
1601
*/
1602
this.enableVideo = function(enabled) {
1603
self.videoEnabled = enabled;
1604
};
1605
1606
/**
1607
* Sets whether WebRTC data channels are used to send inter-client messages.
1608
* This is only the messages that applications explicitly send to other applications, not the WebRTC signaling messages.
1609
* @param {Boolean} enabled  true to use data channels, false otherwise. The default is false.
1610
* @example
1611
*     easyrtc.enableDataChannels(true);
1612
*/
1613
this.enableDataChannels = function(enabled) {
1614
dataEnabled = enabled;
1615
};
1616
1617
/**
1618
* @private
1619
* @param {Boolean} enable
1620
* @param {Array} tracks - an array of MediaStreamTrack
1621
*/
1622
function enableMediaTracks(enable, tracks) {
1623
var i;
1624
if (tracks) {
1625
for (i = 0; i < tracks.length; i++) {
1626
var track = tracks[i];
1627
track.enabled = enable;
1628
}
1629
}
1630
}
1631
1632
/** @private */
1633
//
1634
// fetches a stream by name. Treat a null/undefined streamName as "default".
1635
//
1636
function getLocalMediaStreamByName(streamName) {
1637
if (!streamName) {
1638
streamName = "default";
1639
}
1640
if (namedLocalMediaStreams.hasOwnProperty(streamName)) {
1641
return namedLocalMediaStreams[streamName];
1642
}
1643
else {
1644
return null;
1645
}
1646
}
1647
1648
/**
1649
* Returns the user assigned id's of currently active local media streams.
1650
* @return {Array}
1651
*/
1652
this.getLocalMediaIds = function() {
1653
return Object.keys(namedLocalMediaStreams);
1654
};
1655
1656
/** @private */
1657
function buildMediaIds() {
1658
var mediaMap = {};
1659
var streamName;
1660
for (streamName in namedLocalMediaStreams) {
1661
if (namedLocalMediaStreams.hasOwnProperty(streamName)) {
1662
mediaMap[streamName] = namedLocalMediaStreams[streamName].id || "default";
1663
}
1664
}
1665
return mediaMap;
1666
}
1667
1668
/** @private */
1669
function registerLocalMediaStreamByName(stream, streamName) {
1670
var roomName;
1671
if (!streamName) {
1672
streamName = "default";
1673
}
1674
stream.streamName = streamName;
1675
namedLocalMediaStreams[streamName] = stream;
1676
if (streamName !== "default") {
1677
var mediaIds = buildMediaIds(),
1678
roomData = self.roomData;
1679
for (roomName in roomData) {
1680
if (roomData.hasOwnProperty(roomName)) {
1681
self.setRoomApiField(roomName, "mediaIds", mediaIds);
1682
}
1683
}
1684
}
1685
}
1686
1687
/**
1688
* Allow an externally created mediastream (ie, created by another
1689
* library) to be used within easyrtc. Tracking when it closes
1690
* must be done by the supplying party.
1691
*/
1692
this.register3rdPartyLocalMediaStream = function(stream, streamName) {
1693
return registerLocalMediaStreamByName(stream, streamName);
1694
};
1695
1696
/** @private */
1697
//
1698
// look up a stream's name from the stream.id
1699
//
1700
function getNameOfRemoteStream(easyrtcId, webrtcStreamId) {
1701
var roomName;
1702
var mediaIds;
1703
var streamName;
1704
if (!webrtcStreamId) {
1705
webrtcStreamId = "default";
1706
}
1707
if (peerConns[easyrtcId]) {
1708
streamName = peerConns[easyrtcId].remoteStreamIdToName[webrtcStreamId];
1709
if (streamName) {
1710
return streamName;
1711
}
1712
}
1713
1714
for (roomName in self.roomData) {
1715
if (self.roomData.hasOwnProperty(roomName)) {
1716
mediaIds = self.getRoomApiField(roomName, easyrtcId, "mediaIds");
1717
if (!mediaIds) {
1718
continue;
1719
}
1720
for (streamName in mediaIds) {
1721
if (mediaIds.hasOwnProperty(streamName) &&
1722
mediaIds[streamName] === webrtcStreamId) {
1723
return streamName;
1724
}
1725
}
1726
//
1727
// a stream from chrome to firefox will be missing it's id/label.
1728
// there is no correct solution.
1729
//
1730
if (
1731
adapter && adapter.browserDetails &&
1732
adapter.browserDetails.browser === "firefox"
1733
) {
1734
1735
// if there is a stream called default, return it in preference
1736
if (mediaIds["default"]) {
1737
return "default";
1738
}
1739
1740
//
1741
// otherwise return the first name we find. If there is more than
1742
// one, complain to Mozilla.
1743
//
1744
for(var anyName in mediaIds) {
1745
if (mediaIds.hasOwnProperty(anyName)) {
1746
return anyName;
1747
}
1748
}
1749
}
1750
}
1751
}
1752
1753
return undefined;
1754
}
1755
1756
this.getNameOfRemoteStream = function(easyrtcId, webrtcStream){
1757
if(typeof webrtcStream === "string") {
1758
return getNameOfRemoteStream(easyrtcId, webrtcStream);
1759
}
1760
else if( webrtcStream.id) {
1761
return getNameOfRemoteStream(easyrtcId, webrtcStream.id);
1762
}
1763
};
1764
1765
/** @private */
1766
function closeLocalMediaStreamByName(streamName) {
1767
if (!streamName) {
1768
streamName = "default";
1769
}
1770
var stream = self.getLocalStream(streamName);
1771
if (!stream) {
1772
return;
1773
}
1774
var streamId = stream.id || "default";
1775
var id;
1776
var roomName;
1777
if (namedLocalMediaStreams[streamName]) {
1778
1779
for (id in peerConns) {
1780
if (peerConns.hasOwnProperty(id)) {
1781
try {
1782
peerConns[id].pc.removeStream(stream);
1783
} catch (err) {
1784
}
1785
self.sendPeerMessage(id, "__closingMediaStream", {streamId: streamId, streamName: streamName});
1786
}
1787
}
1788
1789
stopStream(namedLocalMediaStreams[streamName]);
1790
delete namedLocalMediaStreams[streamName];
1791
1792
if (streamName !== "default") {
1793
var mediaIds = buildMediaIds();
1794
for (roomName in self.roomData) {
1795
if (self.roomData.hasOwnProperty(roomName)) {
1796
self.setRoomApiField(roomName, "mediaIds", mediaIds);
1797
}
1798
}
1799
}
1800
}
1801
}
1802
1803
/**
1804
* Close the local media stream. You usually need to close the existing media stream
1805
* of a camera before reacquiring it at a different resolution.
1806
* @param {String} streamName - an option stream name.
1807
*/
1808
this.closeLocalMediaStream = function(streamName) {
1809
return closeLocalMediaStreamByName(streamName);
1810
};
1811
1812
/**
1813
* Alias for closeLocalMediaStream
1814
*/
1815
this.closeLocalStream = this.closeLocalMediaStream;
1816
1817
/**
1818
* This function is used to enable and disable the local camera. If you disable the
1819
* camera, video objects display it will "freeze" until the camera is re-enabled. *
1820
* By default, a camera is enabled.
1821
* @param {Boolean} enable - true to enable the camera, false to disable it.
1822
* @param {String} streamName - the name of the stream, optional.
1823
*/
1824
this.enableCamera = function(enable, streamName) {
1825
var stream = getLocalMediaStreamByName(streamName);
1826
if (stream && stream.getVideoTracks) {
1827
enableMediaTracks(enable, stream.getVideoTracks());
1828
}
1829
};
1830
1831
/**
1832
* This function is used to enable and disable the local microphone. If you disable
1833
* the microphone, sounds stops being transmitted to your peers. By default, the microphone
1834
* is enabled.
1835
* @param {Boolean} enable - true to enable the microphone, false to disable it.
1836
* @param {String} streamName - an optional streamName
1837
*/
1838
this.enableMicrophone = function(enable, streamName) {
1839
var stream = getLocalMediaStreamByName(streamName);
1840
if (stream && stream.getAudioTracks) {
1841
enableMediaTracks(enable, stream.getAudioTracks());
1842
}
1843
};
1844
1845
/**
1846
* Mute a video object.
1847
* @param {String} videoObjectName - A DOMObject or the id of the DOMObject.
1848
* @param {Boolean} mute - true to mute the video object, false to unmute it.
1849
*/
1850
this.muteVideoObject = function(videoObjectName, mute) {
1851
var videoObject;
1852
if (typeof (videoObjectName) === 'string') {
1853
videoObject = document.getElementById(videoObjectName);
1854
if (!videoObject) {
1855
throw "Unknown video object " + videoObjectName;
1856
}
1857
}
1858
else if (!videoObjectName) {
1859
throw "muteVideoObject passed a null";
1860
}
1861
else {
1862
videoObject = videoObjectName;
1863
}
1864
videoObject.muted = !!mute;
1865
};
1866
1867
/**
1868
* Returns a URL for your local camera and microphone.
1869
*  It can be called only after easyrtc.initMediaSource has succeeded.
1870
*  It returns a url that can be used as a source by the Chrome video element or the &lt;canvas&gt; element.
1871
*  @param {String} streamName - an option stream name.
1872
*  @return {URL}
1873
*  @example
1874
*      document.getElementById("myVideo").src = easyrtc.getLocalStreamAsUrl();
1875
*/
1876
self.getLocalStreamAsUrl = function(streamName) {
1877
var stream = getLocalMediaStreamByName(streamName);
1878
if (stream === null) {
1879
throw "Developer error: attempt to get a MediaStream without invoking easyrtc.initMediaSource successfully";
1880
}
1881
return self.createObjectURL(stream);
1882
};
1883
1884
/**
1885
* Returns a media stream for your local camera and microphone.
1886
*  It can be called only after easyrtc.initMediaSource has succeeded.
1887
*  It returns a stream that can be used as an argument to easyrtc.setVideoObjectSrc.
1888
*  Returns null if there is no local media stream acquired yet.
1889
* @return {?MediaStream}
1890
* @example
1891
*    easyrtc.setVideoObjectSrc( document.getElementById("myVideo"), easyrtc.getLocalStream());
1892
*/
1893
this.getLocalStream = function(streamName) {
1894
return getLocalMediaStreamByName(streamName) || null;
1895
};
1896
1897
/** Clears the media stream on a video object.
1898
*
1899
* @param {Object} element the video object.
1900
* @example
1901
*    easyrtc.clearMediaStream( document.getElementById('selfVideo'));
1902
*
1903
*/
1904
this.clearMediaStream = function(element) {
1905
if (typeof element.src !== 'undefined') {
1906
//noinspection JSUndefinedPropertyAssignment
1907
element.src = "";
1908
} else if (typeof element.srcObject !== 'undefined') {
1909
element.srcObject = "";
1910
} else if (typeof element.mozSrcObject !== 'undefined') {
1911
element.mozSrcObject = null;
1912
}
1913
};
1914
1915
/**
1916
*  Sets a video or audio object from a media stream.
1917
*  Chrome uses the src attribute and expects a URL, while firefox
1918
*  uses the mozSrcObject and expects a stream. This procedure hides
1919
*  that from you.
1920
*  If the media stream is from a local webcam, you may want to add the
1921
*  easyrtcMirror class to the video object so it looks like a proper mirror.
1922
*  The easyrtcMirror class is defined in this.css.
1923
*  Which is could be added using the same path of easyrtc.js file to an HTML file
1924
*  @param {Object} element an HTML5 video element
1925
*  @param {MediaStream|String} stream a media stream as returned by easyrtc.getLocalStream or your stream acceptor.
1926
* @example
1927
*    easyrtc.setVideoObjectSrc( document.getElementById("myVideo"), easyrtc.getLocalStream());
1928
*
1929
*/
1930
this.setVideoObjectSrc = function(element, stream) {
1931
if (stream && stream !== "") {
1932
element.autoplay = true;
1933
1934
if (typeof element.src !== 'undefined') {
1935
element.src = self.createObjectURL(stream);
1936
} else if (typeof element.srcObject !== 'undefined') {
1937
element.srcObject = stream;
1938
} else if (typeof element.mozSrcObject !== 'undefined') {
1939
element.mozSrcObject = self.createObjectURL(stream);
1940
}
1941
element.play();
1942
}
1943
else {
1944
self.clearMediaStream(element);
1945
}
1946
};
1947
1948
/**
1949
* This function builds a new named local media stream from a set of existing audio and video tracks from other media streams.
1950
* @param {String} streamName is the name of the new media stream.
1951
* @param {Array} audioTracks is an array of MediaStreamTracks
1952
* @param {Array} videoTracks is an array of MediaStreamTracks
1953
* @returns {?MediaStream} the track created.
1954
* @example
1955
*    easyrtc.buildLocalMediaStream("myComposedStream",
1956
*             easyrtc.getLocalStream("camera1").getVideoTracks(),
1957
*             easyrtc.getLocalStream("camera2").getAudioTracks());
1958
*/
1959
this.buildLocalMediaStream = function(streamName, audioTracks, videoTracks) {
1960
var i;
1961
if (typeof streamName !== 'string') {
1962
self.showError(self.errCodes.DEVELOPER_ERR,
1963
"easyrtc.buildLocalMediaStream not supplied a stream name");
1964
return null;
1965
}
1966
1967
var streamToClone = null;
1968
for(var key in namedLocalMediaStreams ) {
1969
if( namedLocalMediaStreams.hasOwnProperty(key)) {
1970
streamToClone = namedLocalMediaStreams[key];
1971
if(streamToClone) {
1972
break;
1973
}
1974
}
1975
}
1976
if( !streamToClone ) {
1977
for(key in peerConns) {
1978
if (peerConns.hasOwnProperty(key)) {
1979
var remoteStreams = peerConns[key].pc.getRemoteStreams();
1980
if( remoteStreams && remoteStreams.length > 0 ) {
1981
streamToClone = remoteStreams[0];
1982
}
1983
}
1984
}
1985
}
1986
if( !streamToClone ){
1987
self.showError(self.errCodes.DEVELOPER_ERR,
1988
"Attempt to create a mediastream without one to clone from");
1989
return null;
1990
}
1991
1992
//
1993
// clone whatever mediastream we found, and remove any of it's
1994
// tracks.
1995
//
1996
var mediaClone = streamToClone.clone();
1997
var oldTracks = mediaClone.getTracks();
1998
1999
if (audioTracks) {
2000
for (i = 0; i < audioTracks.length; i++) {
2001
mediaClone.addTrack(audioTracks[i].clone());
2002
}
2003
}
2004
2005
if (videoTracks) {
2006
for (i = 0; i < videoTracks.length; i++) {
2007
mediaClone.addTrack(videoTracks[i].clone());
2008
}
2009
}
2010
2011
for( i = 0; i < oldTracks.length; i++ ) {
2012
mediaClone.removeTrack(oldTracks[i]);
2013
}
2014
2015
registerLocalMediaStreamByName(mediaClone, streamName);
2016
return mediaClone;
2017
};
2018
2019
/* @private*/
2020
/** Load Easyrtc Stylesheet.
2021
*   Easyrtc Stylesheet define easyrtcMirror class and some basic css class for using easyrtc.js.
2022
*   That way, developers can override it or use it's own css file minified css or package.
2023
* @example
2024
*       easyrtc.loadStylesheet();
2025
*
2026
*/
2027
this.loadStylesheet = function() {
2028
2029
//
2030
// check to see if we already have an easyrtc.css file loaded
2031
// if we do, we can exit immediately.
2032
//
2033
var links = document.getElementsByTagName("link");
2034
var cssIndex, css;
2035
for (cssIndex in links) {
2036
if (links.hasOwnProperty(cssIndex)) {
2037
css = links[cssIndex];
2038
if (css.href && (css.href.match(/\/easyrtc.css/))) {
2039
return;
2040
}
2041
}
2042
}
2043
//
2044
// add the easyrtc.css file since it isn't present
2045
//
2046
var easySheet = document.createElement("link");
2047
easySheet.setAttribute("rel", "stylesheet");
2048
easySheet.setAttribute("type", "text/css");
2049
easySheet.setAttribute("href", "/easyrtc/easyrtc.css");
2050
var headSection = document.getElementsByTagName("head")[0];
2051
var firstHead = headSection.childNodes[0];
2052
headSection.insertBefore(easySheet, firstHead);
2053
};
2054
2055
/**
2056
* @private
2057
* @param {String} x
2058
*/
2059
this.formatError = function(x) {
2060
var name, result;
2061
if (x === null || typeof x === 'undefined') {
2062
return "null";
2063
}
2064
if (typeof x === 'string') {
2065
return x;
2066
}
2067
else if (x.type && x.description) {
2068
return x.type + " : " + x.description;
2069
}
2070
else if (typeof x === 'object') {
2071
try {
2072
return JSON.stringify(x);
2073
}
2074
catch (oops) {
2075
result = "{";
2076
for (name in x) {
2077
if (x.hasOwnProperty(name)) {
2078
if (typeof x[name] === 'string') {
2079
result = result + name + "='" + x[name] + "' ";
2080
}
2081
}
2082
}
2083
result = result + "}";
2084
return result;
2085
}
2086
}
2087
else {
2088
return "Strange case";
2089
}
2090
};
2091
2092
/**
2093
* Initializes your access to a local camera and microphone.
2094
* Failure could be caused a browser that didn't support WebRTC, or by the user not granting permission.
2095
* If you are going to call easyrtc.enableAudio or easyrtc.enableVideo, you need to do it before
2096
* calling easyrtc.initMediaSource.
2097
* @param {function(Object)} successCallback - will be called with localmedia stream on success.
2098
* @param {function(String,String)} errorCallback - is called with an error code and error description.
2099
* @param {String} streamName - an optional name for the media source so you can use multiple cameras and
2100
* screen share simultaneously.
2101
* @example
2102
*       easyrtc.initMediaSource(
2103
*          function(mediastream){
2104
*              easyrtc.setVideoObjectSrc( document.getElementById("mirrorVideo"), mediastream);
2105
*          },
2106
*          function(errorCode, errorText){
2107
*               easyrtc.showError(errorCode, errorText);
2108
*          });
2109
*/
2110
this.initMediaSource = function(successCallback, errorCallback, streamName) {
2111
2112
logDebug("about to request local media");
2113
2114
if (!streamName) {
2115
streamName = "default";
2116
}
2117
2118
haveAudioVideo = {
2119
audio: self.audioEnabled,
2120
video: self.videoEnabled
2121
};
2122
2123
if (!errorCallback) {
2124
errorCallback = function(errorCode, errorText) {
2125
var message = "easyrtc.initMediaSource: " + self.formatError(errorText);
2126
logDebug(message);
2127
self.showError(self.errCodes.MEDIA_ERR, message);
2128
};
2129
}
2130
2131
if (!self.supportsGetUserMedia()) {
2132
errorCallback(self.errCodes.MEDIA_ERR, self.getConstantString("noWebrtcSupport"));
2133
return;
2134
}
2135
2136
if (!successCallback) {
2137
self.showError(self.errCodes.DEVELOPER_ERR,
2138
"easyrtc.initMediaSource not supplied a successCallback");
2139
return;
2140
}
2141
2142
var mode = self.getUserMediaConstraints();
2143
/** @private
2144
* @param {Object} stream - A mediaStream object.
2145
*  */
2146
var onUserMediaSuccess = function(stream) {
2147
logDebug("getUserMedia success callback entered");
2148
logDebug("successfully got local media");
2149
2150
stream.streamName = streamName;
2151
registerLocalMediaStreamByName(stream, streamName);
2152
var videoObj, triesLeft, tryToGetSize, ele;
2153
if (haveAudioVideo.video) {
2154
videoObj = document.createElement('video');
2155
videoObj.muted = true;
2156
triesLeft = 30;
2157
tryToGetSize = function() {
2158
if (videoObj.videoWidth > 0 || triesLeft < 0) {
2159
self.nativeVideoWidth = videoObj.videoWidth;
2160
self.nativeVideoHeight = videoObj.videoHeight;
2161
if (self._desiredVideoProperties.height &&
2162
(self.nativeVideoHeight !== self._desiredVideoProperties.height ||
2163
self.nativeVideoWidth !== self._desiredVideoProperties.width)) {
2164
self.showError(self.errCodes.MEDIA_WARNING,
2165
self.format(self.getConstantString("resolutionWarning"),
2166
self._desiredVideoProperties.width, self._desiredVideoProperties.height,
2167
self.nativeVideoWidth, self.nativeVideoHeight));
2168
}
2169
self.setVideoObjectSrc(videoObj, null);
2170
if (videoObj.removeNode) {
2171
videoObj.removeNode(true);
2172
}
2173
else {
2174
ele = document.createElement('div');
2175
ele.appendChild(videoObj);
2176
ele.removeChild(videoObj);
2177
}
2178
2179
updateConfigurationInfo();
2180
if (successCallback) {
2181
successCallback(stream);
2182
}
2183
}
2184
else {
2185
triesLeft -= 1;
2186
setTimeout(tryToGetSize, 300);
2187
}
2188
};
2189
self.setVideoObjectSrc(videoObj, stream);
2190
tryToGetSize();
2191
}
2192
else {
2193
updateConfigurationInfo();
2194
if (successCallback) {
2195
successCallback(stream);
2196
}
2197
}
2198
};
2199
2200
/**
2201
* @private
2202
* @param {String} error
2203
*/
2204
var onUserMediaError = function(error) {
2205
logDebug("getusermedia failed");
2206
logDebug("failed to get local media");
2207
var errText;
2208
if (typeof error === 'string') {
2209
errText = error;
2210
}
2211
else if (error.name) {
2212
errText = error.name;
2213
}
2214
else {
2215
errText = "Unknown";
2216
}
2217
if (errorCallback) {
2218
logDebug("invoking error callback", errText);
2219
errorCallback(self.errCodes.MEDIA_ERR, self.format(self.getConstantString("gumFailed"), errText));
2220
}
2221
closeLocalMediaStreamByName(streamName);
2222
haveAudioVideo = {
2223
audio: false,
2224
video: false
2225
};
2226
updateConfigurationInfo();
2227
};
2228
2229
if (!self.audioEnabled && !self.videoEnabled) {
2230
onUserMediaError(self.getConstantString("requireAudioOrVideo"));
2231
return;
2232
}
2233
2234
function getCurrentTime() {
2235
return (new Date()).getTime();
2236
}
2237
2238
var firstCallTime;
2239
function tryAgain(err) {
2240
var currentTime = getCurrentTime();
2241
if (currentTime < firstCallTime + 1000) {
2242
logDebug("Trying getUserMedia a second time");
2243
try {
2244
navigator.getUserMedia(mode, onUserMediaSuccess, onUserMediaError);
2245
} catch (e) {
2246
onUserMediaError(err);
2247
}
2248
}
2249
else {
2250
onUserMediaError(err);
2251
}
2252
}
2253
2254
//
2255
// getUserMedia sometimes fails the first time I call it. I suspect it's a page loading
2256
// issue. So I'm going to try adding a 1 second delay to allow things to settle down first.
2257
// In addition, I'm going to try again after 3 seconds.
2258
//
2259
try {
2260
firstCallTime = getCurrentTime();
2261
navigator.getUserMedia(mode, onUserMediaSuccess, tryAgain);
2262
} catch (err) {
2263
tryAgain(err);
2264
}
2265
};
2266
2267
/**
2268
* Sets the callback used to decide whether to accept or reject an incoming call.
2269
* @param {Function} acceptCheck takes the arguments (callerEasyrtcid, acceptor).
2270
* The acceptCheck callback is passed an easyrtcid and an acceptor function. The acceptor function should be called with either
2271
* a true value (accept the call) or false value( reject the call) as it's first argument, and optionally,
2272
* an array of local media streamNames as a second argument.
2273
* @example
2274
*      easyrtc.setAcceptChecker( function(easyrtcid, acceptor){
2275
*           if( easyrtc.idToName(easyrtcid) === 'Fred' ){
2276
*              acceptor(true);
2277
*           }
2278
*           else if( easyrtc.idToName(easyrtcid) === 'Barney' ){
2279
*              setTimeout( function(){
2280
acceptor(true, ['myOtherCam']); // myOtherCam presumed to a streamName
2281
}, 10000);
2282
*           }
2283
*           else{
2284
*              acceptor(false);
2285
*           }
2286
*      });
2287
*/
2288
this.setAcceptChecker = function(acceptCheck) {
2289
self.acceptCheck = acceptCheck;
2290
};
2291
2292
/**
2293
* easyrtc.setStreamAcceptor sets a callback to receive media streams from other peers, independent
2294
* of where the call was initiated (caller or callee).
2295
* @param {Function} acceptor takes arguments (caller, mediaStream, mediaStreamName)
2296
* @example
2297
*  easyrtc.setStreamAcceptor(function(easyrtcid, stream, streamName){
2298
*     document.getElementById('callerName').innerHTML = easyrtc.idToName(easyrtcid);
2299
*     easyrtc.setVideoObjectSrc( document.getElementById("callerVideo"), stream);
2300
*  });
2301
*/
2302
this.setStreamAcceptor = function(acceptor) {
2303
self.streamAcceptor = acceptor;
2304
};
2305
2306
/** Sets the easyrtc.onError field to a user specified function.
2307
* @param {Function} errListener takes an object of the form {errorCode: String, errorText: String}
2308
* @example
2309
*    easyrtc.setOnError( function(errorObject){
2310
*        document.getElementById("errMessageDiv").innerHTML += errorObject.errorText;
2311
*    });
2312
*/
2313
self.setOnError = function(errListener) {
2314
self.onError = errListener;
2315
};
2316
2317
/**
2318
* Sets the callCancelled callback. This will be called when a remote user
2319
* initiates a call to you, but does a "hangup" before you have a chance to get his video stream.
2320
* @param {Function} callCancelled takes an easyrtcid as an argument and a boolean that indicates whether
2321
*  the call was explicitly cancelled remotely (true), or actually accepted by the user attempting a call to
2322
*  the same party.
2323
* @example
2324
*     easyrtc.setCallCancelled( function(easyrtcid, explicitlyCancelled){
2325
*        if( explicitlyCancelled ){
2326
*            console.log(easyrtc.idToName(easyrtcid) + " stopped trying to reach you");
2327
*         }
2328
*         else{
2329
*            console.log("Implicitly called "  + easyrtc.idToName(easyrtcid));
2330
*         }
2331
*     });
2332
*/
2333
this.setCallCancelled = function(callCancelled) {
2334
self.callCancelled = callCancelled;
2335
};
2336
2337
/**  Sets a callback to receive notification of a media stream closing. The usual
2338
*  use of this is to clear the source of your video object so you aren't left with
2339
*  the last frame of the video displayed on it.
2340
*  @param {Function} onStreamClosed takes an easyrtcid as it's first parameter, the stream as it's second argument, and name of the video stream as it's third.
2341
*  @example
2342
*     easyrtc.setOnStreamClosed( function(easyrtcid, stream, streamName){
2343
*         easyrtc.setVideoObjectSrc( document.getElementById("callerVideo"), "");
2344
*         ( easyrtc.idToName(easyrtcid) + " closed stream " + stream.id + " " + streamName);
2345
*     });
2346
*/
2347
this.setOnStreamClosed = function(onStreamClosed) {
2348
self.onStreamClosed = onStreamClosed;
2349
};
2350
2351
/**
2352
* Sets a listener for data sent from another client (either peer to peer or via websockets).
2353
* If no msgType or source is provided, the listener applies to all events that aren't otherwise handled.
2354
* If a msgType but no source is provided, the listener applies to all messages of that msgType that aren't otherwise handled.
2355
* If a msgType and a source is provided, the listener applies to only message of the specified type coming from the specified peer.
2356
* The most specific case takes priority over the more general.
2357
* @param {Function} listener has the signature (easyrtcid, msgType, msgData, targeting).
2358
*   msgType is a string. targeting is null if the message was received using WebRTC data channels, otherwise it
2359
*   is an object that contains one or more of the following string valued elements {targetEasyrtcid, targetGroup, targetRoom}.
2360
* @param {String} msgType - a string, optional.
2361
* @param {String} source - the sender's easyrtcid, optional.
2362
* @example
2363
*     easyrtc.setPeerListener( function(easyrtcid, msgType, msgData, targeting){
2364
*         console.log("From " + easyrtc.idToName(easyrtcid) +
2365
*             " sent the following data " + JSON.stringify(msgData));
2366
*     });
2367
*     easyrtc.setPeerListener( function(easyrtcid, msgType, msgData, targeting){
2368
*         console.log("From " + easyrtc.idToName(easyrtcid) +
2369
*             " sent the following data " + JSON.stringify(msgData));
2370
*     }, 'food', 'dkdjdekj44--');
2371
*     easyrtc.setPeerListener( function(easyrtcid, msgType, msgData, targeting){
2372
*         console.log("From " + easyrtcid +
2373
*             " sent the following data " + JSON.stringify(msgData));
2374
*     }, 'drink');
2375
*
2376
*
2377
*/
2378
this.setPeerListener = function(listener, msgType, source) {
2379
if (!msgType) {
2380
receivePeer.cb = listener;
2381
}
2382
else {
2383
if (!receivePeer.msgTypes[msgType]) {
2384
receivePeer.msgTypes[msgType] = {sources: {}};
2385
}
2386
if (!source) {
2387
receivePeer.msgTypes[msgType].cb = listener;
2388
}
2389
else {
2390
receivePeer.msgTypes[msgType].sources[source] = {cb: listener};
2391
}
2392
}
2393
};
2394
/* This function serves to distribute peer messages to the various peer listeners */
2395
/** @private
2396
* @param {String} easyrtcid
2397
* @param {Object} msg - needs to contain a msgType and a msgData field.
2398
* @param {Object} targeting
2399
*/
2400
this.receivePeerDistribute = function(easyrtcid, msg, targeting) {
2401
var msgType = msg.msgType;
2402
var msgData = msg.msgData;
2403
if (!msgType) {
2404
logDebug("received peer message without msgType", msg);
2405
return;
2406
}
2407
2408
if (receivePeer.msgTypes[msgType]) {
2409
if (receivePeer.msgTypes[msgType].sources[easyrtcid] &&
2410
receivePeer.msgTypes[msgType].sources[easyrtcid].cb) {
2411
receivePeer.msgTypes[msgType].sources[easyrtcid].cb(easyrtcid, msgType, msgData, targeting);
2412
return;
2413
}
2414
if (receivePeer.msgTypes[msgType].cb) {
2415
receivePeer.msgTypes[msgType].cb(easyrtcid, msgType, msgData, targeting);
2416
return;
2417
}
2418
}
2419
if (receivePeer.cb) {
2420
receivePeer.cb(easyrtcid, msgType, msgData, targeting);
2421
}
2422
};
2423
2424
/**
2425
* Sets a listener for messages from the server.
2426
* @param {Function} listener has the signature (msgType, msgData, targeting)
2427
* @example
2428
*     easyrtc.setServerListener( function(msgType, msgData, targeting){
2429
*         ("The Server sent the following message " + JSON.stringify(msgData));
2430
*     });
2431
*/
2432
this.setServerListener = function(listener) {
2433
receiveServerCB = listener;
2434
};
2435
2436
/**
2437
* Sets the url of the Socket server.
2438
* The node.js server is great as a socket server, but it doesn't have
2439
* all the hooks you'd like in a general web server, like PHP or Python
2440
* plug-ins. By setting the serverPath your application can get it's regular
2441
* pages from a regular web server, but the EasyRTC library can still reach the
2442
* socket server.
2443
* @param {String} socketUrl
2444
* @param {Object} options an optional dictionary of options for socket.io's connect method.
2445
* The default is {'connect timeout': 10000,'force new connection': true }
2446
* @example
2447
*     easyrtc.setSocketUrl(":8080", options);
2448
*/
2449
this.setSocketUrl = function(socketUrl, options) {
2450
logDebug("WebRTC signaling server URL set to " + socketUrl);
2451
serverPath = socketUrl;
2452
if( options ) {
2453
connectionOptions = options;
2454
}
2455
};
2456
2457
/**
2458
* Sets the user name associated with the connection.
2459
* @param {String} username must obey standard identifier conventions.
2460
* @returns {Boolean} true if the call succeeded, false if the username was invalid.
2461
* @example
2462
*    if( !easyrtc.setUsername("JohnSmith") ){
2463
*        console.error("bad user name);
2464
*    }
2465
*
2466
*/
2467
this.setUsername = function(username) {
2468
if( self.myEasyrtcid ) {
2469
self.showError(self.errCodes.DEVELOPER_ERR, "easyrtc.setUsername called after authentication");
2470
return false;
2471
}
2472
else if (self.isNameValid(username)) {
2473
self.username = username;
2474
return true;
2475
}
2476
else {
2477
self.showError(self.errCodes.BAD_NAME, self.format(self.getConstantString("badUserName"), username));
2478
return false;
2479
}
2480
};
2481
2482
/**
2483
* Get an array of easyrtcids that are using a particular username
2484
* @param {String} username - the username of interest.
2485
* @param {String} room - an optional room name argument limiting results to a particular room.
2486
* @returns {Array} an array of {easyrtcid:id, roomName: roomName}.
2487
*/
2488
this.usernameToIds = function(username, room) {
2489
var results = [];
2490
var id, roomName;
2491
for (roomName in lastLoggedInList) {
2492
if (!lastLoggedInList.hasOwnProperty(roomName)) {
2493
continue;
2494
}
2495
if (room && roomName !== room) {
2496
continue;
2497
}
2498
for (id in lastLoggedInList[roomName]) {
2499
if (!lastLoggedInList[roomName].hasOwnProperty(id)) {
2500
continue;
2501
}
2502
if (lastLoggedInList[roomName][id].username === username) {
2503
results.push({
2504
easyrtcid: id,
2505
roomName: roomName
2506
});
2507
}
2508
}
2509
}
2510
return results;
2511
};
2512
2513
/**
2514
* Returns another peers API field, if it exists.
2515
* @param {type} roomName
2516
* @param {type} easyrtcid
2517
* @param {type} fieldName
2518
* @returns {Object}  Undefined if the attribute does not exist, its value otherwise.
2519
*/
2520
this.getRoomApiField = function(roomName, easyrtcid, fieldName) {
2521
if (lastLoggedInList[roomName] &&
2522
lastLoggedInList[roomName][easyrtcid] &&
2523
lastLoggedInList[roomName][easyrtcid].apiField &&
2524
lastLoggedInList[roomName][easyrtcid].apiField[fieldName]) {
2525
return lastLoggedInList[roomName][easyrtcid].apiField[fieldName].fieldValue;
2526
}
2527
else {
2528
return undefined;
2529
}
2530
};
2531
2532
/**
2533
* Set the authentication credential if needed.
2534
* @param {Object} credentialParm - a JSONable object.
2535
*/
2536
this.setCredential = function(credentialParm) {
2537
try {
2538
JSON.stringify(credentialParm);
2539
credential = credentialParm;
2540
return true;
2541
}
2542
catch (oops) {
2543
self.showError(self.errCodes.BAD_CREDENTIAL, "easyrtc.setCredential passed a non-JSON-able object");
2544
throw "easyrtc.setCredential passed a non-JSON-able object";
2545
}
2546
};
2547
2548
/**
2549
* Sets the listener for socket disconnection by external (to the API) reasons.
2550
* @param {Function} disconnectListener takes no arguments and is not called as a result of calling easyrtc.disconnect.
2551
* @example
2552
*    easyrtc.setDisconnectListener(function(){
2553
*        easyrtc.showError("SYSTEM-ERROR", "Lost our connection to the socket server");
2554
*    });
2555
*/
2556
this.setDisconnectListener = function(disconnectListener) {
2557
self.disconnectListener = disconnectListener;
2558
};
2559
2560
/**
2561
* Convert an easyrtcid to a user name. This is useful for labeling buttons and messages
2562
* regarding peers.
2563
* @param {String} easyrtcid
2564
* @return {String} the username associated with the easyrtcid, or the easyrtcid if there is
2565
* no associated username.
2566
* @example
2567
*    console.log(easyrtcid + " is actually " + easyrtc.idToName(easyrtcid));
2568
*/
2569
this.idToName = function(easyrtcid) {
2570
var roomName;
2571
for (roomName in lastLoggedInList) {
2572
if (!lastLoggedInList.hasOwnProperty(roomName)) {
2573
continue;
2574
}
2575
if (lastLoggedInList[roomName][easyrtcid]) {
2576
if (lastLoggedInList[roomName][easyrtcid].username) {
2577
return lastLoggedInList[roomName][easyrtcid].username;
2578
}
2579
}
2580
}
2581
return easyrtcid;
2582
};
2583
2584
/* used in easyrtc.connect */
2585
/** @private */
2586
this.webSocket = null;
2587
/** @private */
2588
var pc_config = {};
2589
/** @private */
2590
var pc_config_to_use = null;
2591
/** @private */
2592
var use_fresh_ice_each_peer = false;
2593
2594
/**
2595
* Determines whether fresh ice server configuration should be requested from the server for each peer connection.
2596
* @param {Boolean} value the default is false.
2597
*/
2598
this.setUseFreshIceEachPeerConnection = function(value) {
2599
use_fresh_ice_each_peer = value;
2600
};
2601
2602
/**
2603
* Returns the last ice config supplied by the EasyRTC server. This function is not normally used, it is provided
2604
* for people who want to try filtering ice server configuration on the client.
2605
* @return {Object} which has the form {iceServers:[ice_server_entry, ice_server_entry, ...]}
2606
*/
2607
this.getServerIce = function() {
2608
return pc_config;
2609
};
2610
2611
/**
2612
* Sets the ice server configuration that will be used in subsequent calls. You only need this function if you are filtering
2613
* the ice server configuration on the client or if you are using TURN certificates that have a very short lifespan.
2614
* @param {Object} ice An object with iceServers element containing an array of ice server entries.
2615
* @example
2616
*     easyrtc.setIceUsedInCalls( {"iceServers": [
2617
*      {
2618
*         "url": "stun:stun.sipgate.net"
2619
*      },
2620
*      {
2621
*         "url": "stun:217.10.68.152"
2622
*      },
2623
*      {
2624
*         "url": "stun:stun.sipgate.net:10000"
2625
*      }
2626
*      ]});
2627
*      easyrtc.call(...);
2628
*/
2629
this.setIceUsedInCalls = function(ice) {
2630
if (!ice.iceServers) {
2631
self.showError(self.errCodes.DEVELOPER_ERR, "Bad ice configuration passed to easyrtc.setIceUsedInCalls");
2632
}
2633
else {
2634
pc_config_to_use = ice;
2635
}
2636
};
2637
2638
/** @private */
2639
function getRemoteStreamByName(peerConn, otherUser, streamName) {
2640
2641
var keyToMatch = null;
2642
var remoteStreams = peerConn.pc.getRemoteStreams();
2643
2644
// No streamName lead to default 
2645
if (!streamName) {
2646
streamName = "default";
2647
}
2648
2649
// default lead to first if available
2650
if (streamName === "default") {
2651
if (remoteStreams.length > 0) {
2652
return remoteStreams[0];
2653
}
2654
else {
2655
return null;
2656
}
2657
}
2658
2659
// Get mediaIds from user roomData
2660
for (var roomName in self.roomData) {
2661
if (self.roomData.hasOwnProperty(roomName)) {
2662
var mediaIds = self.getRoomApiField(roomName, otherUser, "mediaIds");
2663
keyToMatch = mediaIds ? mediaIds[streamName] : null;
2664
if (keyToMatch) {
2665
break;
2666
}
2667
}
2668
}
2669
2670
// 
2671
if (!keyToMatch) {
2672
self.showError(self.errCodes.DEVELOPER_ERR, "remote peer does not have media stream called " + streamName);
2673
}
2674
2675
// 
2676
for (var i = 0; i < remoteStreams.length; i++) {
2677
var remoteId;
2678
if (remoteStreams[i].id) {
2679
remoteId = remoteStreams[i].id;
2680
}  else {
2681
remoteId = "default";
2682
}
2683
2684
if (
2685
!keyToMatch || // No match
2686
remoteId === keyToMatch || // Full match
2687
remoteId.indexOf(keyToMatch) === 0 // Partial match
2688
) {
2689
return remoteStreams[i];
2690
}
2691
2692
}
2693
2694
return null;
2695
}
2696
2697
/**
2698
* @private
2699
* @param {string} easyrtcid
2700
* @param {boolean} checkAudio
2701
* @param {string} streamName
2702
*/
2703
function _haveTracks(easyrtcid, checkAudio, streamName) {
2704
var stream, peerConnObj;
2705
if (!easyrtcid) {
2706
stream = getLocalMediaStreamByName(streamName);
2707
}
2708
else {
2709
peerConnObj = peerConns[easyrtcid];
2710
if (!peerConnObj) {
2711
self.showError(self.errCodes.DEVELOPER_ERR, "haveTracks called about a peer you don't have a connection to");
2712
return false;
2713
}
2714
stream = getRemoteStreamByName(peerConns[easyrtcid], easyrtcid, streamName);
2715
}
2716
if (!stream) {
2717
return false;
2718
}
2719
2720
var tracks;
2721
try {
2722
2723
if (checkAudio) {
2724
tracks = stream.getAudioTracks();
2725
}
2726
else {
2727
tracks = stream.getVideoTracks();
2728
}
2729
2730
} catch (oops) {
2731
// TODO why do we return true here ?
2732
return true;
2733
}
2734
2735
if (!tracks) {
2736
return false;
2737
}
2738
2739
return tracks.length > 0;
2740
}
2741
2742
/** Determines if a particular peer2peer connection has an audio track.
2743
* @param {String} easyrtcid - the id of the other caller in the connection. If easyrtcid is not supplied, checks the local media.
2744
* @param {String} streamName - an optional stream id.
2745
* @return {Boolean} true if there is an audio track or the browser can't tell us.
2746
*/
2747
this.haveAudioTrack = function(easyrtcid, streamName) {
2748
return _haveTracks(easyrtcid, true, streamName);
2749
};
2750
2751
/** Determines if a particular peer2peer connection has a video track.
2752
* @param {String} easyrtcid - the id of the other caller in the connection. If easyrtcid is not supplied, checks the local media.
2753
* @param {String} streamName - an optional stream id.     *
2754
* @return {Boolean} true if there is an video track or the browser can't tell us.
2755
*/
2756
this.haveVideoTrack = function(easyrtcid, streamName) {
2757
return _haveTracks(easyrtcid, false, streamName);
2758
};
2759
2760
/**
2761
* Gets a data field associated with a room.
2762
* @param {String} roomName - the name of the room.
2763
* @param {String} fieldName - the name of the field.
2764
* @return {Object} dataValue - the value of the field if present, undefined if not present.
2765
*/
2766
this.getRoomField = function(roomName, fieldName) {
2767
var fields = self.getRoomFields(roomName);
2768
return (!fields || !fields[fieldName]) ? undefined : fields[fieldName].fieldValue;
2769
};
2770
2771
/** @private */
2772
var fields = null;
2773
2774
/** @private */
2775
var preallocatedSocketIo = null;
2776
2777
/** @private */
2778
var closedChannel = null;
2779
2780
//
2781
// easyrtc.disconnect performs a clean disconnection of the client from the server.
2782
//
2783
function disconnectBody() {
2784
var key;
2785
self.loggingOut = true;
2786
offersPending = {};
2787
acceptancePending = {};
2788
self.disconnecting = true;
2789
closedChannel = self.webSocket;
2790
if (self.webSocketConnected) {
2791
if (!preallocatedSocketIo) {
2792
self.webSocket.close();
2793
}
2794
self.webSocketConnected = false;
2795
}
2796
self.hangupAll();
2797
if (roomOccupantListener) {
2798
for (key in lastLoggedInList) {
2799
if (lastLoggedInList.hasOwnProperty(key)) {
2800
roomOccupantListener(key, {}, false);
2801
}
2802
}
2803
}
2804
lastLoggedInList = {};
2805
self.emitEvent("roomOccupant", {});
2806
self.roomData = {};
2807
self.roomJoin = {};
2808
self.loggingOut = false;
2809
self.myEasyrtcid = null;
2810
self.disconnecting = false;
2811
oldConfig = {};
2812
}
2813
2814
/**
2815
* Disconnect from the EasyRTC server.
2816
* @example
2817
*    easyrtc.disconnect();
2818
*/
2819
this.disconnect = function() {
2820
2821
logDebug("attempt to disconnect from WebRTC signalling server");
2822
2823
self.disconnecting = true;
2824
self.hangupAll();
2825
self.loggingOut = true;
2826
//
2827
// The hangupAll may try to send configuration information back to the server.
2828
// Collecting that information is asynchronous, we don't actually close the
2829
// connection until it's had a chance to be sent. We allocate 100ms for collecting
2830
// the info, so 250ms should be sufficient for the disconnecting.
2831
//
2832
setTimeout(function() {
2833
if (self.webSocket) {
2834
try {
2835
self.webSocket.disconnect();
2836
} catch (e) {
2837
// we don't really care if this fails.
2838
}
2839
2840
closedChannel = self.webSocket;
2841
self.webSocket = 0;
2842
}
2843
self.loggingOut = false;
2844
self.disconnecting = false;
2845
if (roomOccupantListener) {
2846
roomOccupantListener(null, {}, false);
2847
}
2848
self.emitEvent("roomOccupant", {});
2849
oldConfig = {};
2850
}, 250);
2851
};
2852
2853
/** @private */
2854
//
2855
// This function is used to send WebRTC signaling messages to another client. These messages all the form:
2856
//   destUser: some id or null
2857
//   msgType: one of ["offer"/"answer"/"candidate","reject","hangup", "getRoomList"]
2858
//   msgData: either null or an SDP record
2859
//   successCallback: a function with the signature  function(msgType, wholeMsg);
2860
//   errorCallback: a function with signature function(errorCode, errorText)
2861
//
2862
function sendSignalling(destUser, msgType, msgData, successCallback, errorCallback) {
2863
if (!self.webSocket) {
2864
throw "Attempt to send message without a valid connection to the server.";
2865
}
2866
else {
2867
var dataToShip = {
2868
msgType: msgType
2869
};
2870
if (destUser) {
2871
dataToShip.targetEasyrtcid = destUser;
2872
}
2873
if (msgData) {
2874
dataToShip.msgData = msgData;
2875
}
2876
2877
logDebug("sending socket message " + JSON.stringify(dataToShip));
2878
2879
self.webSocket.json.emit("easyrtcCmd", dataToShip,
2880
function(ackMsg) {
2881
if (ackMsg.msgType !== "error") {
2882
if (!ackMsg.hasOwnProperty("msgData")) {
2883
ackMsg.msgData = null;
2884
}
2885
if (successCallback) {
2886
successCallback(ackMsg.msgType, ackMsg.msgData);
2887
}
2888
}
2889
else {
2890
if (errorCallback) {
2891
errorCallback(ackMsg.msgData.errorCode, ackMsg.msgData.errorText);
2892
}
2893
else {
2894
self.showError(ackMsg.msgData.errorCode, ackMsg.msgData.errorText);
2895
}
2896
}
2897
}
2898
);
2899
}
2900
}
2901
2902
/** @private */
2903
//
2904
// This function is used to send large messages. it sends messages that have a transfer field
2905
// so that the receiver knows it's a transfer message. To differentiate the transfers, a
2906
// transferId is generated and passed for each message.
2907
//
2908
var sendByChunkUidCounter = 0;
2909
/** @private */
2910
function sendByChunkHelper(destUser, msgData) {
2911
var transferId = destUser + '-' + sendByChunkUidCounter++;
2912
2913
var pos, len, startMessage, message, endMessage;
2914
var numberOfChunks = Math.ceil(msgData.length / self.maxP2PMessageLength);
2915
startMessage = {
2916
transfer: 'start',
2917
transferId: transferId,
2918
parts: numberOfChunks
2919
};
2920
2921
endMessage = {
2922
transfer: 'end',
2923
transferId: transferId
2924
};
2925
2926
peerConns[destUser].dataChannelS.send(JSON.stringify(startMessage));
2927
2928
for (pos = 0, len = msgData.length; pos < len; pos += self.maxP2PMessageLength) {
2929
message = {
2930
transferId: transferId,
2931
data: msgData.substr(pos, self.maxP2PMessageLength),
2932
transfer: 'chunk'
2933
};
2934
peerConns[destUser].dataChannelS.send(JSON.stringify(message));
2935
}
2936
2937
peerConns[destUser].dataChannelS.send(JSON.stringify(endMessage));
2938
}
2939
2940
/**
2941
*Sends data to another user using previously established data channel. This method will
2942
* fail if no data channel has been established yet. Unlike the easyrtc.sendWS method,
2943
* you can't send a dictionary, convert dictionaries to strings using JSON.stringify first.
2944
* What data types you can send, and how large a data type depends on your browser.
2945
* @param {String} destUser (an easyrtcid)
2946
* @param {String} msgType - the type of message being sent (application specific).
2947
* @param {Object} msgData - a JSONable object.
2948
* @example
2949
*     easyrtc.sendDataP2P(someEasyrtcid, "roomData", {room:499, bldgNum:'asd'});
2950
*/
2951
this.sendDataP2P = function(destUser, msgType, msgData) {
2952
2953
var flattenedData = JSON.stringify({msgType: msgType, msgData: msgData});
2954
logDebug("sending p2p message to " + destUser + " with data=" + JSON.stringify(flattenedData));
2955
2956
if (!peerConns[destUser]) {
2957
self.showError(self.errCodes.DEVELOPER_ERR, "Attempt to send data peer to peer without a connection to " + destUser + ' first.');
2958
}
2959
else if (!peerConns[destUser].dataChannelS) {
2960
self.showError(self.errCodes.DEVELOPER_ERR, "Attempt to send data peer to peer without establishing a data channel to " + destUser + ' first.');
2961
}
2962
else if (!peerConns[destUser].dataChannelReady) {
2963
self.showError(self.errCodes.DEVELOPER_ERR, "Attempt to use data channel to " + destUser + " before it's ready to send.");
2964
}
2965
else {
2966
try {
2967
if (flattenedData.length > self.maxP2PMessageLength) {
2968
sendByChunkHelper(destUser, flattenedData);
2969
} else {
2970
peerConns[destUser].dataChannelS.send(flattenedData);
2971
}
2972
} catch (sendDataErr) {
2973
logDebug("sendDataP2P error: ", sendDataErr);
2974
throw sendDataErr;
2975
}
2976
}
2977
};
2978
2979
/** Sends data to another user using websockets. The easyrtc.sendServerMessage or easyrtc.sendPeerMessage methods
2980
* are wrappers for this method; application code should use them instead.
2981
* @param {String} destination - either a string containing the easyrtcId of the other user, or an object containing some subset of the following fields: targetEasyrtcid, targetGroup, targetRoom.
2982
* Specifying multiple fields restricts the scope of the destination (operates as a logical AND, not a logical OR).
2983
* @param {String} msgType -the type of message being sent (application specific).
2984
* @param {Object} msgData - a JSONable object.
2985
* @param {Function} ackhandler - by default, the ackhandler handles acknowledgments from the server that your message was delivered to it's destination.
2986
* However, application logic in the server can over-ride this. If you leave this null, a stub ackHandler will be used. The ackHandler
2987
* gets passed a message with the same msgType as your outgoing message, or a message type of "error" in which case
2988
* msgData will contain a errorCode and errorText fields.
2989
* @example
2990
*    easyrtc.sendDataWS(someEasyrtcid, "setPostalAddress", {room:499, bldgNum:'asd'},
2991
*      function(ackMsg){
2992
*          console.log("saw the following acknowledgment " + JSON.stringify(ackMsg));
2993
*      }
2994
*    );
2995
*/
2996
this.sendDataWS = function(destination, msgType, msgData, ackhandler) {
2997
logDebug("sending client message via websockets to " + destination + " with data=" + JSON.stringify(msgData));
2998
2999
if (!ackhandler) {
3000
ackhandler = function(msg) {
3001
if (msg.msgType === "error") {
3002
self.showError(msg.msgData.errorCode, msg.msgData.errorText);
3003
}
3004
};
3005
}
3006
3007
var outgoingMessage = {
3008
msgType: msgType,
3009
msgData: msgData
3010
};
3011
3012
if (destination) {
3013
if (typeof destination === 'string') {
3014
outgoingMessage.targetEasyrtcid = destination;
3015
}
3016
else if (typeof destination === 'object') {
3017
if (destination.targetEasyrtcid) {
3018
outgoingMessage.targetEasyrtcid = destination.targetEasyrtcid;
3019
}
3020
if (destination.targetRoom) {
3021
outgoingMessage.targetRoom = destination.targetRoom;
3022
}
3023
if (destination.targetGroup) {
3024
outgoingMessage.targetGroup = destination.targetGroup;
3025
}
3026
}
3027
}
3028
3029
if (self.webSocket) {
3030
self.webSocket.json.emit("easyrtcMsg", outgoingMessage, ackhandler);
3031
}
3032
else {
3033
logDebug("websocket failed because no connection to server");
3034
3035
throw "Attempt to send message without a valid connection to the server.";
3036
}
3037
};
3038
3039
/** Sends data to another user. This method uses data channels if one has been set up, or websockets otherwise.
3040
* @param {String} destUser - a string containing the easyrtcId of the other user.
3041
* Specifying multiple fields restricts the scope of the destination (operates as a logical AND, not a logical OR).
3042
* @param {String} msgType -the type of message being sent (application specific).
3043
* @param {Object} msgData - a JSONable object.
3044
* @param {Function} ackHandler - a function which receives acknowledgments. May only be invoked in
3045
*  the websocket case.
3046
* @example
3047
*    easyrtc.sendData(someEasyrtcid, "roomData",  {room:499, bldgNum:'asd'},
3048
*       function ackHandler(msgType, msgData);
3049
*    );
3050
*/
3051
this.sendData = function(destUser, msgType, msgData, ackHandler) {
3052
if (peerConns[destUser] && peerConns[destUser].dataChannelReady) {
3053
self.sendDataP2P(destUser, msgType, msgData);
3054
}
3055
else {
3056
self.sendDataWS(destUser, msgType, msgData, ackHandler);
3057
}
3058
};
3059
3060
/**
3061
* Sends a message to another peer on the easyrtcMsg channel.
3062
* @param {String} destination - either a string containing the easyrtcId of the other user, or an object containing some subset of the following fields: targetEasyrtcid, targetGroup, targetRoom.
3063
* Specifying multiple fields restricts the scope of the destination (operates as a logical AND, not a logical OR).
3064
* @param {String} msgType - the type of message being sent (application specific).
3065
* @param {Object} msgData - a JSONable object with the message contents.
3066
* @param {function(String, Object)} successCB - a callback function with results from the server.
3067
* @param {function(String, String)} failureCB - a callback function to handle errors.
3068
* @example
3069
*     easyrtc.sendPeerMessage(otherUser, 'offer_candy', {candy_name:'mars'},
3070
*             function(msgType, msgBody ){
3071
*                console.log("message was sent");
3072
*             },
3073
*             function(errorCode, errorText){
3074
*                console.log("error was " + errorText);
3075
*             });
3076
*/
3077
this.sendPeerMessage = function(destination, msgType, msgData, successCB, failureCB) {
3078
if (!destination) {
3079
self.showError(self.errCodes.DEVELOPER_ERR, "destination was null in sendPeerMessage");
3080
}
3081
3082
logDebug("sending peer message " + JSON.stringify(msgData));
3083
3084
function ackHandler(response) {
3085
if (response.msgType === "error") {
3086
if (failureCB) {
3087
failureCB(response.msgData.errorCode, response.msgData.errorText);
3088
}
3089
}
3090
else {
3091
if (successCB) {
3092
// firefox complains if you pass an undefined as an parameter.
3093
successCB(response.msgType, response.msgData ? response.msgData : null);
3094
}
3095
}
3096
}
3097
3098
self.sendDataWS(destination, msgType, msgData, ackHandler);
3099
};
3100
3101
/**
3102
* Sends a message to the application code in the server (ie, on the easyrtcMsg channel).
3103
* @param {String} msgType - the type of message being sent (application specific).
3104
* @param {Object} msgData - a JSONable object with the message contents.
3105
* @param {function(String, Object)} successCB - a callback function with results from the server.
3106
* @param {function(String, String)} failureCB - a callback function to handle errors.
3107
* @example
3108
*     easyrtc.sendServerMessage('get_candy', {candy_name:'mars'},
3109
*             function(msgType, msgData ){
3110
*                console.log("got candy count of " + msgData.barCount);
3111
*             },
3112
*             function(errorCode, errorText){
3113
*                console.log("error was " + errorText);
3114
*             });
3115
*/
3116
this.sendServerMessage = function(msgType, msgData, successCB, failureCB) {
3117
3118
var dataToShip = {msgType: msgType, msgData: msgData};
3119
logDebug("sending server message " + JSON.stringify(dataToShip));
3120
3121
function ackhandler(response) {
3122
if (response.msgType === "error") {
3123
if (failureCB) {
3124
failureCB(response.msgData.errorCode, response.msgData.errorText);
3125
}
3126
}
3127
else {
3128
if (successCB) {
3129
successCB(response.msgType, response.msgData ? response.msgData : null);
3130
}
3131
}
3132
}
3133
3134
self.sendDataWS(null, msgType, msgData, ackhandler);
3135
};
3136
3137
/** Sends the server a request for the list of rooms the user can see.
3138
* You must have already be connected to use this function.
3139
* @param {function(Object)} callback - on success, this function is called with a map of the form  { roomName:{"roomName":String, "numberClients": Number}}.
3140
* The roomName appears as both the key to the map, and as the value of the "roomName" field.
3141
* @param {function(String, String)} errorCallback   is called on failure. It gets an errorCode and errorText as it's too arguments.
3142
* @example
3143
*    easyrtc.getRoomList(
3144
*        function(roomList){
3145
*           for(roomName in roomList){
3146
*              console.log("saw room " + roomName);
3147
*           }
3148
*         },
3149
*         function(errorCode, errorText){
3150
*            easyrtc.showError(errorCode, errorText);
3151
*         }
3152
*    );
3153
*/
3154
this.getRoomList = function(callback, errorCallback) {
3155
sendSignalling(null, "getRoomList", null,
3156
function(msgType, msgData) {
3157
callback(msgData.roomList);
3158
},
3159
function(errorCode, errorText) {
3160
if (errorCallback) {
3161
errorCallback(errorCode, errorText);
3162
}
3163
else {
3164
self.showError(errorCode, errorText);
3165
}
3166
}
3167
);
3168
};
3169
3170
/** Value returned by easyrtc.getConnectStatus if the other user isn't connected to us. */
3171
this.NOT_CONNECTED = "not connected";
3172
3173
/** Value returned by easyrtc.getConnectStatus if the other user is in the process of getting connected */
3174
this.BECOMING_CONNECTED = "connection in progress to us.";
3175
3176
/** Value returned by easyrtc.getConnectStatus if the other user is connected to us. */
3177
this.IS_CONNECTED = "is connected";
3178
3179
/**
3180
* Check if the client has a peer-2-peer connection to another user.
3181
* The return values are text strings so you can use them in debugging output.
3182
*  @param {String} otherUser - the easyrtcid of the other user.
3183
*  @return {String} one of the following values: easyrtc.NOT_CONNECTED, easyrtc.BECOMING_CONNECTED, easyrtc.IS_CONNECTED
3184
*  @example
3185
*     if( easyrtc.getConnectStatus(otherEasyrtcid) == easyrtc.NOT_CONNECTED ){
3186
*         easyrtc.call(otherEasyrtcid,
3187
*                  function(){ console.log("success"); },
3188
*                  function(){ console.log("failure"); });
3189
*     }
3190
*/
3191
this.getConnectStatus = function(otherUser) {
3192
if (!peerConns.hasOwnProperty(otherUser)) {
3193
return self.NOT_CONNECTED;
3194
}
3195
var peer = peerConns[otherUser];
3196
if ((peer.sharingAudio || peer.sharingVideo) && !peer.startedAV) {
3197
return self.BECOMING_CONNECTED;
3198
}
3199
else if (peer.sharingData && !peer.dataChannelReady) {
3200
return self.BECOMING_CONNECTED;
3201
}
3202
else {
3203
return self.IS_CONNECTED;
3204
}
3205
};
3206
3207
/**
3208
* @private
3209
*/
3210
function buildPeerConstraints() {
3211
var options = [];
3212
options.push({'DtlsSrtpKeyAgreement': 'true'}); // for interoperability
3213
return {optional: options};
3214
}
3215
3216
/** @private */
3217
function sendQueuedCandidates(peer, onSignalSuccess, onSignalFailure) {
3218
var i;
3219
for (i = 0; i < peerConns[peer].candidatesToSend.length; i++) {
3220
sendSignalling(
3221
peer,
3222
"candidate",
3223
peerConns[peer].candidatesToSend[i],
3224
onSignalSuccess,
3225
onSignalFailure
3226
);
3227
}
3228
}
3229
3230
/** @private */
3231
//
3232
// This function calls the users onStreamClosed handler, passing it the easyrtcid of the peer, the stream itself,
3233
// and the name of the stream.
3234
//
3235
function emitOnStreamClosed(easyrtcid, stream) {
3236
if (!peerConns[easyrtcid]) {
3237
return;
3238
}
3239
var streamName;
3240
var id;
3241
if (stream.id) {
3242
id = stream.id;
3243
}
3244
else {
3245
id = "default";
3246
}
3247
streamName = peerConns[easyrtcid].remoteStreamIdToName[id] || "default";
3248
if (peerConns[easyrtcid].liveRemoteStreams[streamName] &&
3249
self.onStreamClosed) {
3250
delete peerConns[easyrtcid].liveRemoteStreams[streamName];
3251
self.onStreamClosed(easyrtcid, stream, streamName);
3252
}
3253
delete peerConns[easyrtcid].remoteStreamIdToName[id];
3254
}
3255
3256
/** @private */
3257
function onRemoveStreamHelper(easyrtcid, stream) {
3258
if (peerConns[easyrtcid]) {
3259
emitOnStreamClosed(easyrtcid, stream);
3260
updateConfigurationInfo();
3261
if (peerConns[easyrtcid].pc) {
3262
try {
3263
peerConns[easyrtcid].pc.removeStream(stream);
3264
} catch( err) {}
3265
}
3266
}
3267
}
3268
3269
/** @private */
3270
function buildDeltaRecord(added, deleted) {
3271
function objectNotEmpty(obj) {
3272
var i;
3273
for (i in obj) {
3274
if (obj.hasOwnProperty(i)) {
3275
return true;
3276
}
3277
}
3278
return false;
3279
}
3280
3281
var result = {};
3282
if (objectNotEmpty(added)) {
3283
result.added = added;
3284
}
3285
3286
if (objectNotEmpty(deleted)) {
3287
result.deleted = deleted;
3288
}
3289
3290
if (objectNotEmpty(result)) {
3291
return result;
3292
}
3293
else {
3294
return null;
3295
}
3296
}
3297
3298
/** @private */
3299
function findDeltas(oldVersion, newVersion) {
3300
var i;
3301
var added = {}, deleted = {};
3302
var subPart;
3303
for (i in newVersion) {
3304
if (newVersion.hasOwnProperty(i)) {
3305
if (oldVersion === null || typeof oldVersion[i] === 'undefined') {
3306
added[i] = newVersion[i];
3307
}
3308
else if (typeof newVersion[i] === 'object') {
3309
subPart = findDeltas(oldVersion[i], newVersion[i]);
3310
if (subPart !== null) {
3311
added[i] = newVersion[i];
3312
}
3313
}
3314
else if (newVersion[i] !== oldVersion[i]) {
3315
added[i] = newVersion[i];
3316
}
3317
}
3318
}
3319
for (i in oldVersion) {
3320
if (newVersion.hasOwnProperty(i)) {
3321
if (typeof newVersion[i] === 'undefined') {
3322
deleted[i] = oldVersion[i];
3323
}
3324
}
3325
}
3326
3327
return buildDeltaRecord(added, deleted);
3328
}
3329
3330
/** @private */
3331
//
3332
// this function collects configuration info that will be sent to the server.
3333
// It returns that information, leaving it the responsibility of the caller to
3334
// do the actual sending.
3335
//
3336
function collectConfigurationInfo(/* forAuthentication */) {
3337
var p2pList = {};
3338
var i;
3339
for (i in peerConns) {
3340
if (!peerConns.hasOwnProperty(i)) {
3341
continue;
3342
}
3343
p2pList[i] = {
3344
connectTime: peerConns[i].connectTime,
3345
isInitiator: !!peerConns[i].isInitiator
3346
};
3347
}
3348
3349
var newConfig = {
3350
userSettings: {
3351
sharingAudio: !!haveAudioVideo.audio,
3352
sharingVideo: !!haveAudioVideo.video,
3353
sharingData: !!dataEnabled,
3354
nativeVideoWidth: self.nativeVideoWidth,
3355
nativeVideoHeight: self.nativeVideoHeight,
3356
windowWidth: window.innerWidth,
3357
windowHeight: window.innerHeight,
3358
screenWidth: window.screen.width,
3359
screenHeight: window.screen.height,
3360
cookieEnabled: navigator.cookieEnabled,
3361
os: navigator.oscpu,
3362
language: navigator.language
3363
}
3364
};
3365
3366
if (!isEmptyObj(p2pList)) {
3367
newConfig.p2pList = p2pList;
3368
}
3369
3370
return newConfig;
3371
}
3372
3373
/** @private */
3374
function updateConfiguration() {
3375
3376
var newConfig = collectConfigurationInfo(false);
3377
//
3378
// we need to give the getStats calls a chance to fish out the data.
3379
// The longest I've seen it take is 5 milliseconds so 100 should be overkill.
3380
//
3381
var sendDeltas = function() {
3382
var alteredData = findDeltas(oldConfig, newConfig);
3383
//
3384
// send all the configuration information that changes during the session
3385
//
3386
if (alteredData) {
3387
logDebug("cfg=" + JSON.stringify(alteredData.added));
3388
3389
if (self.webSocket) {
3390
sendSignalling(null, "setUserCfg", {setUserCfg: alteredData.added}, null, null);
3391
}
3392
}
3393
oldConfig = newConfig;
3394
};
3395
if (oldConfig === {}) {
3396
sendDeltas();
3397
}
3398
else {
3399
setTimeout(sendDeltas, 100);
3400
}
3401
}
3402
3403
// Parse the uint32 PRIORITY field into its constituent parts from RFC 5245,
3404
// type preference, local preference, and (256 - component ID).
3405
// ex: 126 | 32252 | 255 (126 is host preference, 255 is component ID 1)
3406
function formatPriority(priority) {
3407
var s = '';
3408
s += (priority >> 24);
3409
s += ' | ';
3410
s += (priority >> 8) & 0xFFFF;
3411
s += ' | ';
3412
s += priority & 0xFF;
3413
return s;
3414
}
3415
3416
// Parse a candidate:foo string into an object, for easier use by other methods.
3417
/** @private */
3418
function parseCandidate(text) {
3419
var candidateStr = 'candidate:';
3420
var pos = text.indexOf(candidateStr) + candidateStr.length;
3421
var fields = text.substr(pos).split(' ');
3422
return {
3423
'component': fields[1],
3424
'type': fields[7],
3425
'foundation': fields[0],
3426
'protocol': fields[2],
3427
'address': fields[4],
3428
'port': fields[5],
3429
'priority': formatPriority(fields[3])
3430
};
3431
}
3432
3433
/** @private */
3434
function processCandicate(candicate) {
3435
self._candicates = self._candicates || [];
3436
self._candicates.push(parseCandidate(candicate));
3437
}
3438
3439
function processAddedStream(otherUser, theStream) {
3440
if (!peerConns[otherUser] ||  peerConns[otherUser].cancelled) {
3441
return;
3442
}
3443
3444
var peerConn = peerConns[otherUser];
3445
3446
if (!peerConn.startedAV) {
3447
peerConn.startedAV = true;
3448
peerConn.sharingAudio = haveAudioVideo.audio;
3449
peerConn.sharingVideo = haveAudioVideo.video;
3450
peerConn.connectTime = new Date().getTime();
3451
if (peerConn.callSuccessCB) {
3452
if (peerConn.sharingAudio || peerConn.sharingVideo) {
3453
peerConn.callSuccessCB(otherUser, "audiovideo");
3454
}
3455
}
3456
if (self.audioEnabled || self.videoEnabled) {
3457
updateConfiguration();
3458
}
3459
}
3460
3461
var remoteName = getNameOfRemoteStream(otherUser, theStream.id || "default");
3462
if (!remoteName) {
3463
remoteName = "default";
3464
}
3465
peerConn.remoteStreamIdToName[theStream.id || "default"] = remoteName;
3466
peerConn.liveRemoteStreams[remoteName] = true;
3467
theStream.streamName = remoteName;
3468
if (self.streamAcceptor) {
3469
self.streamAcceptor(otherUser, theStream, remoteName);
3470
//
3471
// Inform the other user that the stream they provided has been received.
3472
// This should be moved into signalling at some point
3473
//
3474
self.sendDataWS(otherUser, "easyrtc_streamReceived", {streamName:remoteName},function(){});
3475
}
3476
}
3477
3478
function processAddedTrack(otherUser, peerStreams) {
3479
3480
if (!peerConns[otherUser] ||  peerConns[otherUser].cancelled) {
3481
return;
3482
}
3483
3484
var peerConn = peerConns[otherUser];
3485
peerConn.trackTimers = peerConn.trackTimers || {};
3486
3487
// easyrtc thinks in terms of streams, not tracks.
3488
// so we'll add a timeout when the first track event
3489
// fires. Firefox produces two events (one of type "video",
3490
// and one of type "audio".
3491
3492
for (var i = 0, l = peerStreams.length; i < l; i++) {
3493
var peerStream = peerStreams[i],
3494
streamId = peerStream.id || "default";
3495
clearTimeout(peerConn.trackTimers[streamId]);
3496
peerConn.trackTimers[streamId] = setTimeout(function(peerStream) {
3497
processAddedStream(peerConn, otherUser, peerStream);
3498
}.bind(peerStream), 100); // Bind peerStream
3499
}
3500
}
3501
3502
/** @private */
3503
// TODO split buildPeerConnection it more thant 500 lines
3504
function buildPeerConnection(otherUser, isInitiator, failureCB, streamNames) {
3505
var pc;
3506
var message;
3507
var newPeerConn;
3508
var iceConfig = pc_config_to_use ? pc_config_to_use : pc_config;
3509
3510
logDebug("building peer connection to " + otherUser);
3511
3512
//
3513
// we don't support data channels on chrome versions < 31
3514
//
3515
3516
try {
3517
3518
pc = self.createRTCPeerConnection(iceConfig, buildPeerConstraints());
3519
3520
if (!pc) {
3521
message = "Unable to create PeerConnection object, check your ice configuration(" + JSON.stringify(iceConfig) + ")";
3522
logDebug(message);
3523
throw Error(message);
3524
}
3525
3526
//
3527
// turn off data channel support if the browser doesn't support it.
3528
//
3529
3530
if (dataEnabled && typeof pc.createDataChannel === 'undefined') {
3531
dataEnabled = false;
3532
}
3533
3534
pc.onnegotiationneeded = function(event) {
3535
if (
3536
peerConns[otherUser] &&
3537
(peerConns[otherUser].enableNegotiateListener)
3538
) {
3539
pc.createOffer(function(sdp) {
3540
if (sdpLocalFilter) {
3541
sdp.sdp = sdpLocalFilter(sdp.sdp);
3542
}
3543
pc.setLocalDescription(sdp, function() {
3544
self.sendPeerMessage(otherUser, "__addedMediaStream", {
3545
sdp: sdp
3546
});
3547
3548
}, function() {
3549
});
3550
}, function(error) {
3551
logDebug("unexpected error in creating offer");
3552
});
3553
}
3554
};
3555
3556
pc.onsignalingstatechange = function () {
3557
3558
var eventTarget = event.currentTarget || event.target || pc,
3559
signalingState = eventTarget.signalingState || 'unknown';
3560
3561
if (signalingStateChangeListener) {
3562
signalingStateChangeListener(otherUser, eventTarget, signalingState);
3563
}
3564
};
3565
3566
pc.oniceconnectionstatechange = function(event) {
3567
3568
var eventTarget = event.currentTarget || event.target || pc,
3569
connState = eventTarget.iceConnectionState || 'unknown';
3570
3571
if (iceConnectionStateChangeListener) {
3572
iceConnectionStateChangeListener(otherUser, eventTarget, connState);
3573
}
3574
3575
switch (connState) {
3576
case "connected":
3577
if (self.onPeerOpen ) {
3578
self.onPeerOpen(otherUser);
3579
}
3580
if (peerConns[otherUser] && peerConns[otherUser].callSuccessCB) {
3581
peerConns[otherUser].callSuccessCB(otherUser, "connection");
3582
}
3583
break;
3584
case "failed":
3585
if (failureCB) {
3586
failureCB(self.errCodes.NOVIABLEICE, "No usable STUN/TURN path");
3587
}
3588
delete peerConns[otherUser];
3589
break;
3590
case "disconnected":
3591
if (self.onPeerFailing) {
3592
self.onPeerFailing(otherUser);
3593
}
3594
if (peerConns[otherUser]) {
3595
peerConns[otherUser].failing = Date.now();
3596
}
3597
break;
3598
3599
case "closed":
3600
if (self.onPeerClosed) {
3601
self.onPeerClosed(otherUser);
3602
}
3603
break;
3604
case "completed":
3605
if (peerConns[otherUser]) {
3606
if (peerConns[otherUser].failing && self.onPeerRecovered) {
3607
self.onPeerRecovered(otherUser, peerConns[otherUser].failing, Date.now());
3608
}
3609
delete peerConns[otherUser].failing;
3610
}
3611
break;
3612
}
3613
};
3614
3615
pc.onconnection = function() {
3616
logDebug("onconnection called prematurely");
3617
};
3618
3619
newPeerConn = {
3620
pc: pc,
3621
candidatesToSend: [],
3622
startedAV: false,
3623
connectionAccepted: false,
3624
isInitiator: isInitiator,
3625
remoteStreamIdToName: {},
3626
streamsAddedAcks: {},
3627
liveRemoteStreams: {}
3628
};
3629
3630
pc.onicecandidate = function(event) {
3631
if (peerConns[otherUser] && peerConns[otherUser].cancelled) {
3632
return;
3633
}
3634
var candidateData;
3635
if (event.candidate && peerConns[otherUser]) {
3636
candidateData = {
3637
type: 'candidate',
3638
label: event.candidate.sdpMLineIndex,
3639
id: event.candidate.sdpMid,
3640
candidate: event.candidate.candidate
3641
};
3642
3643
if (iceCandidateFilter ) {
3644
candidateData = iceCandidateFilter(candidateData, false);
3645
if( !candidateData ) {
3646
return;
3647
}
3648
}
3649
//
3650
// some candidates include ip addresses of turn servers. we'll want those
3651
// later so we can see if our actual connection uses a turn server.
3652
// The keyword "relay" in the candidate identifies it as referencing a
3653
// turn server. The \d symbol in the regular expression matches a number.
3654
//
3655
processCandicate(event.candidate.candidate);
3656
3657
if (peerConns[otherUser].connectionAccepted) {
3658
sendSignalling(otherUser, "candidate", candidateData, null, function() {
3659
failureCB(self.errCodes.PEER_GONE, "Candidate disappeared");
3660
});
3661
}
3662
else {
3663
peerConns[otherUser].candidatesToSend.push(candidateData);
3664
}
3665
}
3666
};
3667
3668
pc.ontrack = function(event) {
3669
logDebug("empty ontrack method invoked, which is expected");
3670
processAddedTrack(otherUser, event.streams);
3671
};
3672
3673
pc.onaddstream = function(event) {
3674
logDebug("empty onaddstream method invoked, which is expected");
3675
processAddedStream(otherUser, event.stream);
3676
};
3677
3678
pc.onremovestream = function(event) {
3679
logDebug("saw remove on remote media stream");
3680
onRemoveStreamHelper(otherUser, event.stream);
3681
};
3682
3683
// Register PeerConn
3684
peerConns[otherUser] = newPeerConn;
3685
3686
} catch (error) {
3687
logDebug('buildPeerConnection error', error);
3688
failureCB(self.errCodes.SYSTEM_ERR, error.message);
3689
return null;
3690
}
3691
3692
var i, stream;
3693
if (streamNames) {
3694
for (i = 0; i < streamNames.length; i++) {
3695
stream = getLocalMediaStreamByName(streamNames[i]);
3696
if (stream) {
3697
pc.addStream(stream);
3698
}
3699
else {
3700
logDebug("Developer error, attempt to access unknown local media stream " + streamNames[i]);
3701
}
3702
}
3703
}
3704
else if (autoInitUserMedia && (self.videoEnabled || self.audioEnabled)) {
3705
stream = self.getLocalStream();
3706
pc.addStream(stream);
3707
}
3708
3709
//
3710
// This function handles data channel message events.
3711
//
3712
var pendingTransfer = {};
3713
function dataChannelMessageHandler(event) {
3714
logDebug("saw dataChannel.onmessage event: ", event.data);
3715
3716
if (event.data === "dataChannelPrimed") {
3717
self.sendDataWS(otherUser, "dataChannelPrimed", "");
3718
}
3719
else {
3720
//
3721
// Chrome and Firefox Interop is passing a event with a strange data="", perhaps
3722
// as it's own form of priming message. Comparing the data against "" doesn't
3723
// work, so I'm going with parsing and trapping the parse error.
3724
//
3725
var msg;
3726
3727
try {
3728
msg = JSON.parse(event.data);
3729
} catch (err) {
3730
logDebug('Developer error, unable to parse event data');
3731
}
3732
3733
if (msg) {
3734
if (msg.transfer && msg.transferId) {
3735
if (msg.transfer === 'start') {
3736
logDebug('start transfer #' + msg.transferId);
3737
3738
var parts = parseInt(msg.parts);
3739
pendingTransfer = {
3740
chunks: [],
3741
parts: parts,
3742
transferId: msg.transferId
3743
};
3744
3745
} else if (msg.transfer === 'chunk') {
3746
logDebug('got chunk for transfer #' + msg.transferId);
3747
3748
// check data is valid
3749
if (!(typeof msg.data === 'string' && msg.data.length <= self.maxP2PMessageLength)) {
3750
logDebug('Developer error, invalid data');
3751
3752
// check there's a pending transfer
3753
} else if (!pendingTransfer) {
3754
logDebug('Developer error, unexpected chunk');
3755
3756
// check that transferId is valid
3757
} else if (msg.transferId !== pendingTransfer.transferId) {
3758
logDebug('Developer error, invalid transfer id');
3759
3760
// check that the max length of transfer is not reached
3761
} else if (pendingTransfer.chunks.length + 1 > pendingTransfer.parts) {
3762
logDebug('Developer error, received too many chunks');
3763
3764
} else {
3765
pendingTransfer.chunks.push(msg.data);
3766
}
3767
3768
} else if (msg.transfer === 'end') {
3769
logDebug('end of transfer #' + msg.transferId);
3770
3771
// check there's a pending transfer
3772
if (!pendingTransfer) {
3773
logDebug('Developer error, unexpected end of transfer');
3774
3775
// check that transferId is valid
3776
} else if (msg.transferId !== pendingTransfer.transferId) {
3777
logDebug('Developer error, invalid transfer id');
3778
3779
// check that all the chunks were received
3780
} else if (pendingTransfer.chunks.length !== pendingTransfer.parts) {
3781
logDebug('Developer error, received wrong number of chunks');
3782
3783
} else {
3784
var chunkedMsg;
3785
try {
3786
chunkedMsg = JSON.parse(pendingTransfer.chunks.join(''));
3787
} catch (err) {
3788
logDebug('Developer error, unable to parse message');
3789
}
3790
3791
if (chunkedMsg) {
3792
self.receivePeerDistribute(otherUser, chunkedMsg, null);
3793
}
3794
}
3795
pendingTransfer = {  };
3796
3797
} else {
3798
logDebug('Developer error, got an unknown transfer message' + msg.transfer);
3799
}
3800
} else {
3801
self.receivePeerDistribute(otherUser, msg, null);
3802
}
3803
}
3804
}
3805
}
3806
3807
function initOutGoingChannel(otherUser) {
3808
logDebug("saw initOutgoingChannel call");
3809
3810
var dataChannel = pc.createDataChannel(dataChannelName, self.getDatachannelConstraints());
3811
peerConns[otherUser].dataChannelS = dataChannel;
3812
peerConns[otherUser].dataChannelR = dataChannel;
3813
dataChannel.onmessage = dataChannelMessageHandler;
3814
dataChannel.onopen = function(event) {
3815
logDebug("saw dataChannel.onopen event");
3816
3817
if (peerConns[otherUser]) {
3818
dataChannel.send("dataChannelPrimed");
3819
}
3820
};
3821
dataChannel.onclose = function(event) {
3822
logDebug("saw dataChannelS.onclose event");
3823
3824
if (peerConns[otherUser]) {
3825
peerConns[otherUser].dataChannelReady = false;
3826
delete peerConns[otherUser].dataChannelS;
3827
}
3828
if (onDataChannelClose) {
3829
onDataChannelClose(otherUser);
3830
}
3831
3832
updateConfigurationInfo();
3833
};
3834
}
3835
3836
function initIncomingChannel(otherUser) {
3837
logDebug("initializing incoming channel handler for " + otherUser);
3838
3839
peerConns[otherUser].pc.ondatachannel = function(event) {
3840
3841
logDebug("saw incoming data channel");
3842
3843
var dataChannel = event.channel;
3844
peerConns[otherUser].dataChannelR = dataChannel;
3845
peerConns[otherUser].dataChannelS = dataChannel;
3846
peerConns[otherUser].dataChannelReady = true;
3847
dataChannel.onmessage = dataChannelMessageHandler;
3848
dataChannel.onclose = function(event) {
3849
logDebug("saw dataChannelR.onclose event");
3850
3851
if (peerConns[otherUser]) {
3852
peerConns[otherUser].dataChannelReady = false;
3853
delete peerConns[otherUser].dataChannelR;
3854
}
3855
if (onDataChannelClose) {
3856
onDataChannelClose(otherUser);
3857
}
3858
3859
updateConfigurationInfo();
3860
};
3861
dataChannel.onopen = function(event) {
3862
logDebug("saw dataChannel.onopen event");
3863
3864
if (peerConns[otherUser]) {
3865
dataChannel.send("dataChannelPrimed");
3866
}
3867
};
3868
};
3869
}
3870
3871
//
3872
//  added for interoperability
3873
//
3874
// TODO check if both sides have the same browser and versions
3875
if (dataEnabled) {
3876
self.setPeerListener(function() {
3877
if (peerConns[otherUser]) {
3878
peerConns[otherUser].dataChannelReady = true;
3879
if (peerConns[otherUser].callSuccessCB) {
3880
peerConns[otherUser].callSuccessCB(otherUser, "datachannel");
3881
}
3882
if (onDataChannelOpen) {
3883
onDataChannelOpen(otherUser, true);
3884
}
3885
updateConfigurationInfo();
3886
} else {
3887
logDebug("failed to setup outgoing channel listener");
3888
}
3889
}, "dataChannelPrimed", otherUser);
3890
3891
if (isInitiator) {
3892
try {
3893
3894
initOutGoingChannel(otherUser);
3895
} catch (channelErrorEvent) {
3896
logDebug("failed to init outgoing channel");
3897
failureCB(self.errCodes.SYSTEM_ERR,
3898
self.formatError(channelErrorEvent));
3899
}
3900
}
3901
if (!isInitiator) {
3902
initIncomingChannel(otherUser);
3903
}
3904
}
3905
3906
pc.onconnection = function() {
3907
logDebug("setup pc.onconnection ");
3908
};
3909
3910
//
3911
// Temporary support for responding to acknowledgements of about streams being added.
3912
//
3913
self.setPeerListener(function(easyrtcid, msgType, msgData, targeting){
3914
if( newPeerConn.streamsAddedAcks[msgData.streamName]) {
3915
(newPeerConn.streamsAddedAcks[msgData.streamName])(easyrtcid, msgData.streamName);
3916
delete newPeerConn.streamsAddedAcks[msgData.streamName];
3917
}
3918
}, "easyrtc_streamReceived", otherUser);
3919
return pc;
3920
}
3921
3922
/** @private */
3923
function doAnswerBody(caller, msgData, streamNames) {
3924
var pc = buildPeerConnection(caller, false, function(message) {
3925
self.showError(self.errCodes.SYSTEM_ERR, message);
3926
}, streamNames);
3927
var newPeerConn = peerConns[caller];
3928
if (!pc) {
3929
logDebug("buildPeerConnection failed. Call not answered");
3930
return;
3931
}
3932
var setLocalAndSendMessage1 = function(sessionDescription) {
3933
3934
if (newPeerConn.cancelled) {
3935
return;
3936
}
3937
3938
var sendAnswer = function() {
3939
logDebug("sending answer");
3940
3941
function onSignalSuccess() {
3942
logDebug("sending success");
3943
}
3944
3945
function onSignalFailure(errorCode, errorText) {
3946
logDebug("sending error");
3947
delete peerConns[caller];
3948
self.showError(errorCode, errorText);
3949
}
3950
3951
sendSignalling(caller, "answer", sessionDescription, onSignalSuccess, onSignalFailure);
3952
peerConns[caller].connectionAccepted = true;
3953
sendQueuedCandidates(caller, onSignalSuccess, onSignalFailure);
3954
3955
if (pc.connectDataConnection) {
3956
logDebug("calling connectDataConnection(5002,5001)");
3957
pc.connectDataConnection(5002, 5001);
3958
}
3959
};
3960
if (sdpLocalFilter) {
3961
sessionDescription.sdp = sdpLocalFilter(sessionDescription.sdp);
3962
}
3963
pc.setLocalDescription(sessionDescription, sendAnswer, function(message) {
3964
self.showError(self.errCodes.INTERNAL_ERR, "setLocalDescription: " + message);
3965
});
3966
};
3967
var sd = new RTCSessionDescription(msgData);
3968
3969
if (!sd) {
3970
throw "Could not create the RTCSessionDescription";
3971
}
3972
3973
logDebug("sdp ||  " + JSON.stringify(sd));
3974
3975
var invokeCreateAnswer = function() {
3976
if (newPeerConn.cancelled) {
3977
return;
3978
}
3979
pc.createAnswer(setLocalAndSendMessage1,
3980
function(message) {
3981
self.showError(self.errCodes.INTERNAL_ERR, "create-answer: " + message);
3982
},
3983
receivedMediaConstraints);
3984
};
3985
3986
logDebug("about to call setRemoteDescription in doAnswer");
3987
3988
try {
3989
3990
if (sdpRemoteFilter) {
3991
sd.sdp = sdpRemoteFilter(sd.sdp);
3992
}
3993
pc.setRemoteDescription(sd, invokeCreateAnswer, function(message) {
3994
self.showError(self.errCodes.INTERNAL_ERR, "set-remote-description: " + message);
3995
});
3996
} catch (srdError) {
3997
logDebug("set remote description failed");
3998
self.showError(self.errCodes.INTERNAL_ERR, "setRemoteDescription failed: " + srdError.message);
3999
}
4000
}
4001
4002
/** @private */
4003
function doAnswer(caller, msgData, streamNames) {
4004
if (!streamNames && autoInitUserMedia) {
4005
var localStream = self.getLocalStream();
4006
if (!localStream && (self.videoEnabled || self.audioEnabled)) {
4007
self.initMediaSource(
4008
function() {
4009
doAnswer(caller, msgData);
4010
},
4011
function(errorCode, error) {
4012
self.showError(self.errCodes.MEDIA_ERR, self.format(self.getConstantString("localMediaError")));
4013
});
4014
return;
4015
}
4016
}
4017
if (use_fresh_ice_each_peer) {
4018
self.getFreshIceConfig(function(succeeded) {
4019
if (succeeded) {
4020
doAnswerBody(caller, msgData, streamNames);
4021
}
4022
else {
4023
self.showError(self.errCodes.CALL_ERR, "Failed to get fresh ice config");
4024
}
4025
});
4026
}
4027
else {
4028
doAnswerBody(caller, msgData, streamNames);
4029
}
4030
}
4031
4032
4033
/** @private */
4034
function callBody(otherUser, callSuccessCB, callFailureCB, wasAcceptedCB, streamNames) {
4035
acceptancePending[otherUser] = true;
4036
var pc = buildPeerConnection(otherUser, true, callFailureCB, streamNames);
4037
var message;
4038
if (!pc) {
4039
message = "buildPeerConnection failed, call not completed";
4040
logDebug(message);
4041
throw message;
4042
}
4043
4044
peerConns[otherUser].callSuccessCB = callSuccessCB;
4045
peerConns[otherUser].callFailureCB = callFailureCB;
4046
peerConns[otherUser].wasAcceptedCB = wasAcceptedCB;
4047
var peerConnObj = peerConns[otherUser];
4048
var setLocalAndSendMessage0 = function(sessionDescription) {
4049
if (peerConnObj.cancelled) {
4050
return;
4051
}
4052
var sendOffer = function() {
4053
4054
sendSignalling(otherUser, "offer", sessionDescription, null, callFailureCB);
4055
};
4056
if (sdpLocalFilter) {
4057
sessionDescription.sdp = sdpLocalFilter(sessionDescription.sdp);
4058
}
4059
pc.setLocalDescription(sessionDescription, sendOffer,
4060
function(errorText) {
4061
callFailureCB(self.errCodes.CALL_ERR, errorText);
4062
});
4063
};
4064
setTimeout(function() {
4065
//
4066
// if the call was cancelled, we don't want to continue getting the offer.
4067
// we can tell the call was cancelled because there won't be a peerConn object
4068
// for it.
4069
//
4070
if( !peerConns[otherUser]) {
4071
return;
4072
}
4073
pc.createOffer(setLocalAndSendMessage0, function(errorObj) {
4074
callFailureCB(self.errCodes.CALL_ERR, JSON.stringify(errorObj));
4075
},
4076
receivedMediaConstraints);
4077
}, 100);
4078
}
4079
4080
/**
4081
* Initiates a call to another user. If it succeeds, the streamAcceptor callback will be called.
4082
* @param {String} otherUser - the easyrtcid of the peer being called.
4083
* @param {Function} callSuccessCB (otherCaller, mediaType) - is called when the datachannel is established or the MediaStream is established. mediaType will have a value of "audiovideo" or "datachannel"
4084
* @param {Function} callFailureCB (errorCode, errMessage) - is called if there was a system error interfering with the call.
4085
* @param {Function} wasAcceptedCB (wasAccepted:boolean,otherUser:string) - is called when a call is accepted or rejected by another party. It can be left null.
4086
* @param {Array} streamNames - optional array of streamNames.
4087
* @example
4088
*    easyrtc.call( otherEasyrtcid,
4089
*        function(easyrtcid, mediaType){
4090
*           console.log("Got mediaType " + mediaType + " from " + easyrtc.idToName(easyrtcid));
4091
*        },
4092
*        function(errorCode, errMessage){
4093
*           console.log("call to  " + easyrtc.idToName(otherEasyrtcid) + " failed:" + errMessage);
4094
*        },
4095
*        function(wasAccepted, easyrtcid){
4096
*            if( wasAccepted ){
4097
*               console.log("call accepted by " + easyrtc.idToName(easyrtcid));
4098
*            }
4099
*            else{
4100
*                console.log("call rejected" + easyrtc.idToName(easyrtcid));
4101
*            }
4102
*        });
4103
*/
4104
this.call = function(otherUser, callSuccessCB, callFailureCB, wasAcceptedCB, streamNames) {
4105
4106
if (streamNames) {
4107
if (typeof streamNames === "string") { // accept a string argument if passed.
4108
streamNames = [streamNames];
4109
}
4110
else if (typeof streamNames.length === "undefined") {
4111
self.showError(self.errCodes.DEVELOPER_ERR, "easyrtc.call passed bad streamNames");
4112
return;
4113
}
4114
}
4115
4116
logDebug("initiating peer to peer call to " + otherUser +
4117
" audio=" + self.audioEnabled +
4118
" video=" + self.videoEnabled +
4119
" data=" + dataEnabled);
4120
4121
4122
if (!self.supportsPeerConnections()) {
4123
callFailureCB(self.errCodes.CALL_ERR, self.getConstantString("noWebrtcSupport"));
4124
return;
4125
}
4126
4127
var message;
4128
//
4129
// If we are sharing audio/video and we haven't allocated the local media stream yet,
4130
// we'll do so, recalling our self on success.
4131
//
4132
if (!streamNames && autoInitUserMedia) {
4133
var stream = self.getLocalStream();
4134
if (!stream && (self.audioEnabled || self.videoEnabled)) {
4135
self.initMediaSource(function() {
4136
self.call(otherUser, callSuccessCB, callFailureCB, wasAcceptedCB);
4137
}, callFailureCB);
4138
return;
4139
}
4140
}
4141
4142
if (!self.webSocket) {
4143
message = "Attempt to make a call prior to connecting to service";
4144
logDebug(message);
4145
throw message;
4146
}
4147
4148
//
4149
// If B calls A, and then A calls B before accepting, then A should treat the attempt to
4150
// call B as a positive offer to B's offer.
4151
//
4152
if (offersPending[otherUser]) {
4153
wasAcceptedCB(true, otherUser);
4154
doAnswer(otherUser, offersPending[otherUser], streamNames);
4155
delete offersPending[otherUser];
4156
self.callCancelled(otherUser, false);
4157
return;
4158
}
4159
4160
// do we already have a pending call?
4161
if (typeof acceptancePending[otherUser] !== 'undefined') {
4162
message = "Call already pending acceptance";
4163
logDebug(message);
4164
callFailureCB(self.errCodes.ALREADY_CONNECTED, message);
4165
return;
4166
}
4167
4168
if (use_fresh_ice_each_peer) {
4169
self.getFreshIceConfig(function(succeeded) {
4170
if (succeeded) {
4171
callBody(otherUser, callSuccessCB, callFailureCB, wasAcceptedCB, streamNames);
4172
}
4173
else {
4174
callFailureCB(self.errCodes.CALL_ERR, "Attempt to get fresh ice configuration failed");
4175
}
4176
});
4177
}
4178
else {
4179
callBody(otherUser, callSuccessCB, callFailureCB, wasAcceptedCB, streamNames);
4180
}
4181
};
4182
4183
/** @private */
4184
//
4185
// this function check the deprecated MediaStream.ended attribute
4186
// and new .active. Also fallback .enable on track for Firefox.
4187
//
4188
function isStreamActive(stream) {
4189
4190
var isActive;
4191
4192
if (stream.active === true || stream.ended === false)  {
4193
isActive = true;
4194
} else {
4195
isActive = stream.getTracks().reduce(function (track) {
4196
return track.enabled;
4197
});
4198
}
4199
4200
return isActive;
4201
}
4202
4203
/** @private */
4204
var queuedMessages = {};
4205
4206
/** @private */
4207
function clearQueuedMessages(caller) {
4208
queuedMessages[caller] = {
4209
candidates: []
4210
};
4211
}
4212
4213
/** @private */
4214
function closePeer(otherUser) {
4215
4216
if (acceptancePending[otherUser]) {
4217
delete acceptancePending[otherUser];
4218
}
4219
if (offersPending[otherUser]) {
4220
delete offersPending[otherUser];
4221
}
4222
4223
if (
4224
!peerConns[otherUser].cancelled &&
4225
peerConns[otherUser].pc
4226
) {
4227
try {
4228
var remoteStreams = peerConns[otherUser].pc.getRemoteStreams();
4229
for (var i = 0; i < remoteStreams.length; i++) {
4230
if (isStreamActive(remoteStreams[i])) {
4231
emitOnStreamClosed(otherUser, remoteStreams[i]);
4232
stopStream(remoteStreams[i]);
4233
}
4234
}
4235
4236
peerConns[otherUser].pc.close();
4237
peerConns[otherUser].cancelled = true;
4238
logDebug("peer closed");
4239
} catch (err) {
4240
logDebug("peer " + otherUser + " close failed:" + err);
4241
} finally {
4242
if (self.onPeerClosed) {
4243
self.onPeerClosed(otherUser);
4244
}
4245
}
4246
}
4247
}
4248
4249
/** @private */
4250
function hangupBody(otherUser) {
4251
4252
logDebug("Hanging up on " + otherUser);
4253
clearQueuedMessages(otherUser);
4254
4255
if (peerConns[otherUser]) {
4256
4257
if (peerConns[otherUser].pc) {
4258
closePeer(otherUser);
4259
4260
4261
if (peerConns[otherUser]) {
4262
delete peerConns[otherUser];
4263
}
4264
4265
updateConfigurationInfo();
4266
4267
if (self.webSocket) {
4268
sendSignalling(otherUser, "hangup", null, function() {
4269
logDebug("hangup succeeds");
4270
}, function(errorCode, errorText) {
4271
logDebug("hangup failed:" + errorText);
4272
self.showError(errorCode, errorText);
4273
});
4274
}
4275
}
4276
}
4277
4278
4279
4280
/**
4281
* Hang up on a particular user or all users.
4282
*  @param {String} otherUser - the easyrtcid of the person to hang up on.
4283
*  @example
4284
*     easyrtc.hangup(someEasyrtcid);
4285
*/
4286
this.hangup = function(otherUser) {
4287
hangupBody(otherUser);
4288
updateConfigurationInfo();
4289
};
4290
4291
/**
4292
* Hangs up on all current connections.
4293
* @example
4294
*    easyrtc.hangupAll();
4295
*/
4296
this.hangupAll = function() {
4297
4298
var sawAConnection = false;
4299
for (var otherUser in peerConns) {
4300
if (!peerConns.hasOwnProperty(otherUser)) {
4301
continue;
4302
}
4303
sawAConnection = true;
4304
hangupBody(otherUser);
4305
}
4306
4307
if (sawAConnection) {
4308
updateConfigurationInfo();
4309
}
4310
};
4311
4312
/**
4313
* Checks to see if data channels work between two peers.
4314
* @param {String} otherUser - the other peer.
4315
* @returns {Boolean} true if data channels work and are ready to be used
4316
*   between the two peers.
4317
*/
4318
this.doesDataChannelWork = function(otherUser) {
4319
if (!peerConns[otherUser]) {
4320
return false;
4321
}
4322
return !!peerConns[otherUser].dataChannelReady;
4323
};
4324
4325
/**
4326
* Return the media stream shared by a particular peer. This is needed when you
4327
* add a stream in the middle of a call.
4328
* @param {String} easyrtcid the peer.
4329
* @param {String} remoteStreamName an optional argument supplying the streamName.
4330
* @returns {Object} A mediaStream.
4331
*/
4332
this.getRemoteStream = function(easyrtcid, remoteStreamName) {
4333
if (!peerConns[easyrtcid]) {
4334
self.showError(self.errCodes.DEVELOPER_ERR, "attempt to get stream of uncalled party");
4335
throw "Developer err: no such stream";
4336
}
4337
else {
4338
return getRemoteStreamByName(peerConns[easyrtcid], easyrtcid, remoteStreamName);
4339
}
4340
};
4341
4342
/**
4343
* Assign a local streamName to a remote stream so that it can be forwarded to other callers.
4344
* @param {String} easyrtcid the peer supplying the remote stream
4345
* @param {String} remoteStreamName the streamName supplied by the peer.
4346
* @param {String} localStreamName streamName used when passing the stream to other peers.
4347
* @example
4348
*    easyrtc.makeLocalStreamFromRemoteStream(sourcePeer, "default", "forwardedStream");
4349
*    easyrtc.call(nextPeer, callSuccessCB, callFailureCB, wasAcceptedCB, ["forwardedStream"]);
4350
*/
4351
this.makeLocalStreamFromRemoteStream = function(easyrtcid, remoteStreamName, localStreamName) {
4352
var remoteStream;
4353
if (peerConns[easyrtcid].pc) {
4354
remoteStream = getRemoteStreamByName(peerConns[easyrtcid], easyrtcid, remoteStreamName);
4355
if (remoteStream) {
4356
registerLocalMediaStreamByName(remoteStream, localStreamName);
4357
}
4358
else {
4359
throw "Developer err: no such stream";
4360
}
4361
}
4362
else {
4363
throw "Developer err: no such peer ";
4364
}
4365
};
4366
4367
/**
4368
* Add a named local stream to a call.
4369
* @param {String} easyrtcId The id of client receiving the stream.
4370
* @param {String} streamName The name of the stream.
4371
* @param {Function} receiptHandler is a function that gets called when the other side sends a message
4372
*   that the stream has been received. The receiptHandler gets called with an easyrtcid and a stream name. This
4373
*   argument is optional.
4374
*/
4375
this.addStreamToCall = function(easyrtcId, streamName, receiptHandler) {
4376
if( !streamName) {
4377
streamName = "default";
4378
}
4379
var stream = getLocalMediaStreamByName(streamName);
4380
if (!stream) {
4381
logDebug("attempt to add nonexistent stream " + streamName);
4382
}
4383
else if (!peerConns[easyrtcId] || !peerConns[easyrtcId].pc) {
4384
logDebug("Can't add stream before a call has started.");
4385
}
4386
else {
4387
var pc = peerConns[easyrtcId].pc;
4388
peerConns[easyrtcId].enableNegotiateListener = true;
4389
pc.addStream(stream);
4390
if (receiptHandler) {
4391
peerConns[easyrtcId].streamsAddedAcks[streamName] = receiptHandler;
4392
}
4393
}
4394
};
4395
4396
//
4397
// these three listeners support the ability to add/remove additional media streams on the fly.
4398
//
4399
this.setPeerListener(function(easyrtcid, msgType, msgData) {
4400
if (!peerConns[easyrtcid] || !peerConns[easyrtcid].pc) {
4401
self.showError(self.errCodes.DEVELOPER_ERR,
4402
"Attempt to add additional stream before establishing the base call.");
4403
}
4404
else {
4405
var sdp = msgData.sdp;
4406
var pc = peerConns[easyrtcid].pc;
4407
4408
var setLocalAndSendMessage1 = function(sessionDescription) {
4409
var sendAnswer = function() {
4410
logDebug("sending answer");
4411
4412
function onSignalSuccess() {
4413
logDebug("sending answer succeeded");
4414
4415
}
4416
4417
function onSignalFailure(errorCode, errorText) {
4418
logDebug("sending answer failed");
4419
4420
delete peerConns[easyrtcid];
4421
self.showError(errorCode, errorText);
4422
}
4423
4424
sendSignalling(easyrtcid, "answer", sessionDescription,
4425
onSignalSuccess, onSignalFailure);
4426
peerConns[easyrtcid].connectionAccepted = true;
4427
sendQueuedCandidates(easyrtcid, onSignalSuccess, onSignalFailure);
4428
};
4429
4430
if (sdpLocalFilter) {
4431
sessionDescription.sdp = sdpLocalFilter(sessionDescription.sdp);
4432
}
4433
pc.setLocalDescription(sessionDescription, sendAnswer, function(message) {
4434
self.showError(self.errCodes.INTERNAL_ERR, "setLocalDescription: " + msgData);
4435
});
4436
};
4437
4438
var invokeCreateAnswer = function() {
4439
pc.createAnswer(setLocalAndSendMessage1,
4440
function(message) {
4441
self.showError(self.errCodes.INTERNAL_ERR, "create-answer: " + message);
4442
},
4443
receivedMediaConstraints);
4444
self.sendPeerMessage(easyrtcid, "__gotAddedMediaStream", {sdp: sdp});
4445
};
4446
4447
logDebug("about to call setRemoteDescription in doAnswer");
4448
4449
try {
4450
4451
if (sdpRemoteFilter) {
4452
sdp.sdp = sdpRemoteFilter(sdp.sdp);
4453
}
4454
pc.setRemoteDescription(new RTCSessionDescription(sdp),
4455
invokeCreateAnswer, function(message) {
4456
self.showError(self.errCodes.INTERNAL_ERR, "set-remote-description: " + message);
4457
});
4458
} catch (srdError) {
4459
logDebug("saw exception in setRemoteDescription", srdError);
4460
self.showError(self.errCodes.INTERNAL_ERR, "setRemoteDescription failed: " + srdError.message);
4461
}
4462
}
4463
}, "__addedMediaStream");
4464
4465
this.setPeerListener(function(easyrtcid, msgType, msgData) {
4466
if (!peerConns[easyrtcid] || !peerConns[easyrtcid].pc) {
4467
logDebug("setPeerListener failed: __gotAddedMediaStream Unknow easyrtcid " + easyrtcid);
4468
}
4469
else {
4470
var sdp = msgData.sdp;
4471
if (sdpRemoteFilter) {
4472
sdp.sdp = sdpRemoteFilter(sdp.sdp);
4473
}
4474
var pc = peerConns[easyrtcid].pc;
4475
pc.setRemoteDescription(new RTCSessionDescription(sdp), function(){},
4476
function(message) {
4477
self.showError(self.errCodes.INTERNAL_ERR, "set-remote-description: " + message);
4478
});
4479
}
4480
4481
}, "__gotAddedMediaStream");
4482
4483
this.setPeerListener(function(easyrtcid, msgType, msgData) {
4484
if (!peerConns[easyrtcid] || !peerConns[easyrtcid].pc) {
4485
logDebug("setPeerListener failed: __closingMediaStream Unknow easyrtcid " + easyrtcid);
4486
}
4487
else {
4488
var stream = getRemoteStreamByName(peerConns[easyrtcid], easyrtcid, msgData.streamName);
4489
if (stream) {
4490
onRemoveStreamHelper(easyrtcid, stream);
4491
stopStream(stream);
4492
}
4493
}
4494
4495
}, "__closingMediaStream");
4496
4497
/** @private */
4498
this.dumpPeerConnectionInfo = function() {
4499
var i;
4500
for (var peer in peerConns) {
4501
if (peerConns.hasOwnProperty(peer)) {
4502
var pc = peerConns[peer].pc;
4503
var remotes = pc.getRemoteStreams();
4504
var remoteIds = [];
4505
for (i = 0; i < remotes.length; i++) {
4506
remoteIds.push(remotes[i].id);
4507
}
4508
var locals = pc.getLocalStreams();
4509
var localIds = [];
4510
for (i = 0; i < locals.length; i++) {
4511
localIds.push(locals[i].id);
4512
}
4513
4514
logDebug("For peer " + peer);
4515
logDebug("    " + JSON.stringify({local: localIds, remote: remoteIds}));
4516
}
4517
}
4518
};
4519
4520
/** @private */
4521
function onRemoteHangup(otherUser) {
4522
4523
logDebug("Saw onRemote hangup event");
4524
clearQueuedMessages(otherUser);
4525
4526
if (peerConns[otherUser]) {
4527
4528
if (peerConns[otherUser].pc) {
4529
closePeer(otherUser);
4530
}
4531
else {
4532
if (self.callCancelled) {
4533
self.callCancelled(otherUser, true);
4534
}
4535
}
4536
4537
if (peerConns[otherUser]) {
4538
delete peerConns[otherUser];
4539
}
4540
}
4541
else {
4542
if (self.callCancelled) {
4543
self.callCancelled(otherUser, true);
4544
}
4545
}
4546
}
4547
4548
/** @private */
4549
//
4550
// checks to see if a particular peer is in any room at all.
4551
//
4552
function isPeerInAnyRoom(id) {
4553
var roomName;
4554
for (roomName in lastLoggedInList) {
4555
if (!lastLoggedInList.hasOwnProperty(roomName)) {
4556
continue;
4557
}
4558
if (lastLoggedInList[roomName][id]) {
4559
return true;
4560
}
4561
}
4562
return false;
4563
}
4564
4565
/**
4566
* Checks to see if a particular peer is present in any room.
4567
* If it isn't, we assume it's logged out.
4568
* @param {string} easyrtcid the easyrtcId of the peer.
4569
*/
4570
this.isPeerInAnyRoom = function(easyrtcid) {
4571
return isPeerInAnyRoom(easyrtcid);
4572
};
4573
4574
/** @private */
4575
function processLostPeers(peersInRoom) {
4576
var id;
4577
//
4578
// check to see the person is still in at least one room. If not, we'll hangup
4579
// on them. This isn't the correct behavior, but it's the best we can do without
4580
// changes to the server.
4581
//
4582
for (id in peerConns) {
4583
if (peerConns.hasOwnProperty(id) &&
4584
typeof peersInRoom[id] === 'undefined') {
4585
if (!isPeerInAnyRoom(id)) {
4586
if (peerConns[id].pc || peerConns[id].isInitiator) {
4587
onRemoteHangup(id);
4588
}
4589
delete offersPending[id];
4590
delete acceptancePending[id];
4591
clearQueuedMessages(id);
4592
}
4593
}
4594
}
4595
4596
for (id in offersPending) {
4597
if (offersPending.hasOwnProperty(id) && !isPeerInAnyRoom(id)) {
4598
onRemoteHangup(id);
4599
clearQueuedMessages(id);
4600
delete offersPending[id];
4601
delete acceptancePending[id];
4602
}
4603
}
4604
4605
for (id in acceptancePending) {
4606
if (acceptancePending.hasOwnProperty(id) && !isPeerInAnyRoom(id)) {
4607
onRemoteHangup(id);
4608
clearQueuedMessages(id);
4609
delete acceptancePending[id];
4610
}
4611
}
4612
}
4613
4614
/**
4615
* The idea of aggregating timers is that there are events that convey state and these can fire more frequently
4616
* than desired. Aggregating timers allow a bunch of events to be collapsed into one by only firing the last
4617
* event.
4618
* @private
4619
*/
4620
var aggregatingTimers = {};
4621
4622
/**
4623
* This function sets a timeout for a function to be called with the feature that if another
4624
* invocation comes along within a particular interval (with the same key), the second invocation
4625
* replaces the first. To prevent a continuous stream of events from preventing a callback from ever
4626
* firing, we'll collapse no more than 20 events.
4627
* @param {String} key A key used to identify callbacks that should be aggregated.
4628
* @param {Function} callback The callback to invoke.
4629
* @param {Number} period The aggregating period in milliseconds.
4630
* @private
4631
*/
4632
function addAggregatingTimer(key, callback, period) {
4633
if( !period) {
4634
period = 100; // 0.1 second
4635
}
4636
var counter = 0;
4637
if( aggregatingTimers[key]) {
4638
clearTimeout(aggregatingTimers[key].timer);
4639
counter = aggregatingTimers[key].counter;
4640
}
4641
if( counter > 20) {
4642
delete aggregatingTimers[key];
4643
callback();
4644
}
4645
else {
4646
aggregatingTimers[key] = {counter: counter +1};
4647
aggregatingTimers[key].timer = setTimeout(function () {
4648
delete aggregatingTimers[key];
4649
callback();
4650
}, period);
4651
}
4652
}
4653
4654
/** @private */
4655
//
4656
// this function gets called for each room when there is a room update.
4657
//
4658
function processOccupantList(roomName, occupantList) {
4659
var myInfo = null;
4660
var reducedList = {};
4661
4662
var id;
4663
for (id in occupantList) {
4664
if (occupantList.hasOwnProperty(id)) {
4665
if (id === self.myEasyrtcid) {
4666
myInfo = occupantList[id];
4667
}
4668
else {
4669
reducedList[id] = occupantList[id];
4670
}
4671
}
4672
}
4673
//
4674
// processLostPeers detects peers that have gone away and performs
4675
// house keeping accordingly.
4676
//
4677
processLostPeers(reducedList);
4678
//
4679
//
4680
//
4681
addAggregatingTimer("roomOccupants&" + roomName, function(){
4682
if (roomOccupantListener) {
4683
roomOccupantListener(roomName, reducedList, myInfo);
4684
}
4685
self.emitEvent("roomOccupants", {roomName:roomName, occupants:lastLoggedInList});
4686
}, 100);
4687
}
4688
4689
/** @private */
4690
function onChannelMsg(msg, ackAcceptorFunc) {
4691
4692
var targeting = {};
4693
if (ackAcceptorFunc) {
4694
ackAcceptorFunc(self.ackMessage);
4695
}
4696
if (msg.targetEasyrtcid) {
4697
targeting.targetEasyrtcid = msg.targetEasyrtcid;
4698
}
4699
if (msg.targetRoom) {
4700
targeting.targetRoom = msg.targetRoom;
4701
}
4702
if (msg.targetGroup) {
4703
targeting.targetGroup = msg.targetGroup;
4704
}
4705
if (msg.senderEasyrtcid) {
4706
self.receivePeerDistribute(msg.senderEasyrtcid, msg, targeting);
4707
}
4708
else {
4709
if (receiveServerCB) {
4710
receiveServerCB(msg.msgType, msg.msgData, targeting);
4711
}
4712
else {
4713
logDebug("Unhandled server message " + JSON.stringify(msg));
4714
}
4715
}
4716
}
4717
4718
/** @private */
4719
function processUrl(url) {
4720
var ipAddress;
4721
if (url.indexOf('turn:') === 0 || url.indexOf('turns:') === 0) {
4722
ipAddress = url.split(/[@:&]/g)[1];
4723
self._turnServers[ipAddress] = true;
4724
} else if (url.indexOf('stun:') === 0 || url.indexOf('stuns:') === 0) {
4725
ipAddress = url.split(/[@:&]/g)[1];
4726
self._stunServers[ipAddress] = true;
4727
}
4728
}
4729
4730
/** @private */
4731
function processIceConfig(iceConfig) {
4732
4733
var i, j, item;
4734
4735
pc_config = {
4736
iceServers: []
4737
};
4738
4739
self._turnServers = {};
4740
self._stunServers = {};
4741
4742
if (
4743
!iceConfig ||
4744
!iceConfig.iceServers ||
4745
typeof iceConfig.iceServers.length === "undefined"
4746
) {
4747
self.showError(
4748
self.errCodes.DEVELOPER_ERR,
4749
"iceConfig received from server didn't have an array called iceServers, ignoring it"
4750
);
4751
} else {
4752
pc_config = {
4753
iceServers: iceConfig.iceServers
4754
};
4755
}
4756
4757
for (i = 0; i < iceConfig.iceServers.length; i++) {
4758
item = iceConfig.iceServers[i];
4759
if( item.urls && item.urls.length ) {
4760
for( j = 0; j < item.urls.length; j++ ) {
4761
processUrl(item.urls[j]);
4762
}
4763
}
4764
else if( item.url ) {
4765
processUrl(item.url);
4766
}
4767
}
4768
}
4769
4770
/** @private */
4771
function processSessionData(sessionData) {
4772
if (sessionData) {
4773
if (sessionData.easyrtcsid) {
4774
self.easyrtcsid = sessionData.easyrtcsid;
4775
}
4776
if (sessionData.field) {
4777
sessionFields = sessionData.field;
4778
}
4779
}
4780
}
4781
4782
/** @private */
4783
function processRoomData(roomData) {
4784
self.roomData = roomData;
4785
4786
var k, roomName,
4787
stuffToRemove, stuffToAdd,
4788
id, removeId;
4789
4790
for (roomName in self.roomData) {
4791
if (!self.roomData.hasOwnProperty(roomName)) {
4792
continue;
4793
}
4794
if (roomData[roomName].roomStatus === "join") {
4795
if (!(self.roomJoin[roomName])) {
4796
self.roomJoin[roomName] = roomData[roomName];
4797
}
4798
var mediaIds = buildMediaIds();
4799
if (mediaIds !== {}) {
4800
self.setRoomApiField(roomName, "mediaIds", mediaIds);
4801
}
4802
}
4803
else if (roomData[roomName].roomStatus === "leave") {
4804
if (self.roomEntryListener) {
4805
self.roomEntryListener(false, roomName);
4806
}
4807
delete self.roomJoin[roomName];
4808
delete lastLoggedInList[roomName];
4809
continue;
4810
}
4811
4812
if (roomData[roomName].clientList) {
4813
lastLoggedInList[roomName] = roomData[roomName].clientList;
4814
}
4815
else if (roomData[roomName].clientListDelta) {
4816
stuffToAdd = roomData[roomName].clientListDelta.updateClient;
4817
if (stuffToAdd) {
4818
for (id in stuffToAdd) {
4819
if (!stuffToAdd.hasOwnProperty(id)) {
4820
continue;
4821
}
4822
if (!lastLoggedInList[roomName]) {
4823
lastLoggedInList[roomName] = [];
4824
}
4825
if( !lastLoggedInList[roomName][id] ) {
4826
lastLoggedInList[roomName][id] = stuffToAdd[id];
4827
}
4828
for( k in stuffToAdd[id] ) {
4829
if( k === "apiField" || k === "presence") {
4830
lastLoggedInList[roomName][id][k] = stuffToAdd[id][k];
4831
}
4832
}
4833
}
4834
}
4835
stuffToRemove = roomData[roomName].clientListDelta.removeClient;
4836
if (stuffToRemove && lastLoggedInList[roomName]) {
4837
for (removeId in stuffToRemove) {
4838
if (stuffToRemove.hasOwnProperty(removeId)) {
4839
delete lastLoggedInList[roomName][removeId];
4840
}
4841
}
4842
}
4843
}
4844
if (self.roomJoin[roomName] && roomData[roomName].field) {
4845
fields.rooms[roomName] = roomData[roomName].field;
4846
}
4847
if (roomData[roomName].roomStatus === "join") {
4848
if (self.roomEntryListener) {
4849
self.roomEntryListener(true, roomName);
4850
}
4851
}
4852
processOccupantList(roomName, lastLoggedInList[roomName]);
4853
}
4854
self.emitEvent("roomOccupant", lastLoggedInList);
4855
}
4856
4857
/** @private */
4858
function onChannelCmd(msg, ackAcceptorFn) {
4859
4860
var caller = msg.senderEasyrtcid;
4861
var msgType = msg.msgType;
4862
var msgData = msg.msgData;
4863
var pc;
4864
4865
logDebug('received message of type ' + msgType);
4866
4867
4868
if (typeof queuedMessages[caller] === "undefined") {
4869
clearQueuedMessages(caller);
4870
}
4871
4872
var processCandidateBody = function(caller, msgData) {
4873
var candidate = null;
4874
4875
if( iceCandidateFilter ) {
4876
msgData = iceCandidateFilter(msgData, true);
4877
if( !msgData ) {
4878
return;
4879
}
4880
}
4881
4882
candidate = new RTCIceCandidate({
4883
sdpMLineIndex: msgData.label,
4884
candidate: msgData.candidate
4885
});
4886
pc = peerConns[caller].pc;
4887
4888
function iceAddSuccess() {
4889
logDebug("iceAddSuccess: " +
4890
JSON.stringify(candidate));
4891
processCandicate(msgData.candidate);
4892
}
4893
4894
function iceAddFailure(domError) {
4895
self.showError(self.errCodes.ICECANDIDATE_ERR, "bad ice candidate (" + domError.name + "): " +
4896
JSON.stringify(candidate));
4897
}
4898
4899
pc.addIceCandidate(candidate, iceAddSuccess, iceAddFailure);
4900
};
4901
4902
var flushCachedCandidates = function(caller) {
4903
var i;
4904
if (queuedMessages[caller]) {
4905
for (i = 0; i < queuedMessages[caller].candidates.length; i++) {
4906
processCandidateBody(caller, queuedMessages[caller].candidates[i]);
4907
}
4908
delete queuedMessages[caller];
4909
}
4910
};
4911
4912
var processOffer = function(caller, msgData) {
4913
4914
var helper = function(wasAccepted, streamNames) {
4915
4916
if (streamNames) {
4917
if (typeof streamNames === "string") {
4918
streamNames = [streamNames];
4919
}
4920
else if (streamNames.length === undefined) {
4921
self.showError(self.errCodes.DEVELOPER_ERR, "accept callback passed invalid streamNames");
4922
return;
4923
}
4924
}
4925
4926
logDebug("offer accept=" + wasAccepted);
4927
4928
delete offersPending[caller];
4929
4930
if (wasAccepted) {
4931
if (!self.supportsPeerConnections()) {
4932
self.showError(self.errCodes.CALL_ERR, self.getConstantString("noWebrtcSupport"));
4933
return;
4934
}
4935
doAnswer(caller, msgData, streamNames);
4936
flushCachedCandidates(caller);
4937
}
4938
else {
4939
sendSignalling(caller, "reject", null, null, null);
4940
clearQueuedMessages(caller);
4941
}
4942
};
4943
//
4944
// There is a very rare case of two callers sending each other offers
4945
// before receiving the others offer. In such a case, the caller with the
4946
// greater valued easyrtcid will delete its pending call information and do a
4947
// simple answer to the other caller's offer.
4948
//
4949
if (acceptancePending[caller] && caller < self.myEasyrtcid) {
4950
delete acceptancePending[caller];
4951
if (queuedMessages[caller]) {
4952
delete queuedMessages[caller];
4953
}
4954
if (peerConns[caller]) {
4955
if (peerConns[caller].wasAcceptedCB) {
4956
peerConns[caller].wasAcceptedCB(true, caller);
4957
}
4958
delete peerConns[caller];
4959
}
4960
helper(true);
4961
return;
4962
}
4963
4964
offersPending[caller] = msgData;
4965
if (!self.acceptCheck) {
4966
helper(true);
4967
}
4968
else {
4969
self.acceptCheck(caller, helper);
4970
}
4971
};
4972
4973
function processReject(caller) {
4974
delete acceptancePending[caller];
4975
if (queuedMessages[caller]) {
4976
delete queuedMessages[caller];
4977
}
4978
if (peerConns[caller]) {
4979
if (peerConns[caller].wasAcceptedCB) {
4980
peerConns[caller].wasAcceptedCB(false, caller);
4981
}
4982
delete peerConns[caller];
4983
}
4984
}
4985
4986
function processAnswer(caller, msgData) {
4987
4988
delete acceptancePending[caller];
4989
4990
//
4991
// if we've discarded the peer connection, ignore the answer.
4992
//
4993
if (!peerConns[caller]) {
4994
return;
4995
}
4996
peerConns[caller].connectionAccepted = true;
4997
4998
4999
5000
if (peerConns[caller].wasAcceptedCB) {
5001
peerConns[caller].wasAcceptedCB(true, caller);
5002
}
5003
5004
var onSignalSuccess = function() {
5005
5006
};
5007
var onSignalFailure = function(errorCode, errorText) {
5008
if (peerConns[caller]) {
5009
delete peerConns[caller];
5010
}
5011
self.showError(errorCode, errorText);
5012
};
5013
// peerConns[caller].startedAV = true;
5014
sendQueuedCandidates(caller, onSignalSuccess, onSignalFailure);
5015
pc = peerConns[caller].pc;
5016
var sd = new RTCSessionDescription(msgData);
5017
if (!sd) {
5018
throw "Could not create the RTCSessionDescription";
5019
}
5020
5021
logDebug("about to call initiating setRemoteDescription");
5022
5023
try {
5024
if (sdpRemoteFilter) {
5025
sd.sdp = sdpRemoteFilter(sd.sdp);
5026
}
5027
pc.setRemoteDescription(sd, function() {
5028
if (pc.connectDataConnection) {
5029
logDebug("calling connectDataConnection(5001,5002)");
5030
5031
pc.connectDataConnection(5001, 5002); // these are like ids for data channels
5032
}
5033
}, function(message){
5034
logDebug("setRemoteDescription failed ", message);
5035
});
5036
} catch (smdException) {
5037
logDebug("setRemoteDescription failed ", smdException);
5038
}
5039
flushCachedCandidates(caller);
5040
}
5041
5042
function processCandidateQueue(caller, msgData) {
5043
5044
if (peerConns[caller] && peerConns[caller].pc) {
5045
processCandidateBody(caller, msgData);
5046
}
5047
else {
5048
if (!peerConns[caller]) {
5049
queuedMessages[caller] = {
5050
candidates: []
5051
};
5052
}
5053
queuedMessages[caller].candidates.push(msgData);
5054
}
5055
}
5056
5057
switch (msgType) {
5058
case "sessionData":
5059
processSessionData(msgData.sessionData);
5060
break;
5061
case "roomData":
5062
processRoomData(msgData.roomData);
5063
break;
5064
case "iceConfig":
5065
processIceConfig(msgData.iceConfig);
5066
break;
5067
case "forwardToUrl":
5068
if (msgData.newWindow) {
5069
window.open(msgData.forwardToUrl.url);
5070
}
5071
else {
5072
window.location.href = msgData.forwardToUrl.url;
5073
}
5074
break;
5075
case "offer":
5076
processOffer(caller, msgData);
5077
break;
5078
case "reject":
5079
processReject(caller);
5080
break;
5081
case "answer":
5082
processAnswer(caller, msgData);
5083
break;
5084
case "candidate":
5085
processCandidateQueue(caller, msgData);
5086
break;
5087
case "hangup":
5088
onRemoteHangup(caller);
5089
clearQueuedMessages(caller);
5090
break;
5091
case "error":
5092
self.showError(msgData.errorCode, msgData.errorText);
5093
break;
5094
default:
5095
self.showError(self.errCodes.DEVELOPER_ERR, "received unknown message type from server, msgType is " + msgType);
5096
return;
5097
}
5098
5099
if (ackAcceptorFn) {
5100
ackAcceptorFn(self.ackMessage);
5101
}
5102
}
5103
5104
/**
5105
* Sets the presence state on the server.
5106
* @param {String} state - one of 'away','chat','dnd','xa'
5107
* @param {String} statusText - User configurable status string. May be length limited.
5108
* @example   easyrtc.updatePresence('dnd', 'sleeping');
5109
*/
5110
this.updatePresence = function(state, statusText) {
5111
5112
self.presenceShow = state;
5113
self.presenceStatus = statusText;
5114
5115
if (self.webSocketConnected) {
5116
sendSignalling(null, 'setPresence', {
5117
setPresence: {
5118
'show': self.presenceShow,
5119
'status': self.presenceStatus
5120
}
5121
}, null);
5122
}
5123
};
5124
5125
/**
5126
* Fetch the collection of session fields as a map. The map has the structure:
5127
*  {key1: {"fieldName": key1, "fieldValue": value1}, ...,
5128
*   key2: {"fieldName": key2, "fieldValue": value2}
5129
*  }
5130
* @returns {Object}
5131
*/
5132
this.getSessionFields = function() {
5133
return sessionFields;
5134
};
5135
5136
/**
5137
* Fetch the value of a session field by name.
5138
* @param {String} name - name of the session field to be fetched.
5139
* @returns the field value (which can be anything). Returns undefined if the field does not exist.
5140
*/
5141
this.getSessionField = function(name) {
5142
if (sessionFields[name]) {
5143
return sessionFields[name].fieldValue;
5144
}
5145
else {
5146
return undefined;
5147
}
5148
};
5149
5150
/**
5151
* Returns an array of easyrtcid's of peers in a particular room.
5152
* @param roomName
5153
* @returns {Array} of easyrtcids or null if the client is not in the room.
5154
* @example
5155
*     var occupants = easyrtc.getRoomOccupants("default");
5156
*     var i;
5157
*     for( i = 0; i < occupants.length; i++ ) {
5158
*         console.log( occupants[i] + " is in the room");
5159
*     }
5160
*/
5161
this.getRoomOccupantsAsArray = function(roomName) {
5162
if (!lastLoggedInList[roomName]) {
5163
return null;
5164
}
5165
else {
5166
return Object.keys(lastLoggedInList[roomName]);
5167
}
5168
};
5169
5170
/**
5171
* Returns a map of easyrtcid's of peers in a particular room. You should only test elements in the map to see if they are
5172
* null; their actual values are not guaranteed to be the same in different releases.
5173
* @param roomName
5174
* @returns {Object} of easyrtcids or null if the client is not in the room.
5175
* @example
5176
*      if( easyrtc.getRoomOccupantsAsMap("default")[some_easyrtcid]) {
5177
*          console.log("yep, " + some_easyrtcid + " is in the room");
5178
*      }
5179
*/
5180
this.getRoomOccupantsAsMap = function(roomName) {
5181
return lastLoggedInList[roomName];
5182
};
5183
5184
/**
5185
* Returns true if the ipAddress parameter was the address of a turn server. This is done by checking against information
5186
* collected during peer to peer calls. Don't expect it to work before the first call, or to identify turn servers that aren't
5187
* in the ice config.
5188
* @param ipAddress
5189
* @returns {boolean} true if ip address is known to be that of a turn server, false otherwise.
5190
*/
5191
this.isTurnServer = function(ipAddress) {
5192
return !!self._turnServers[ipAddress];
5193
};
5194
5195
/**
5196
* Returns true if the ipAddress parameter was the address of a stun server. This is done by checking against information
5197
* collected during peer to peer calls. Don't expect it to work before the first call, or to identify turn servers that aren't
5198
* in the ice config.
5199
* @param {string} ipAddress
5200
* @returns {boolean} true if ip address is known to be that of a stun server, false otherwise.
5201
*/
5202
this.isStunServer = function(ipAddress) {
5203
return !!self._stunServers[ipAddress];
5204
};
5205
5206
/**
5207
* Request fresh ice config information from the server.
5208
* This should be done periodically by long running applications.
5209
* @param {Function} callback is called with a value of true on success, false on failure.
5210
*/
5211
this.getFreshIceConfig = function(callback) {
5212
var dataToShip = {
5213
msgType: "getIceConfig",
5214
msgData: {}
5215
};
5216
if (!callback) {
5217
callback = function() {
5218
};
5219
}
5220
self.webSocket.json.emit("easyrtcCmd", dataToShip,
5221
function(ackMsg) {
5222
if (ackMsg.msgType === "iceConfig") {
5223
processIceConfig(ackMsg.msgData.iceConfig);
5224
callback(true);
5225
}
5226
else {
5227
self.showError(ackMsg.msgData.errorCode, ackMsg.msgData.errorText);
5228
callback(false);
5229
}
5230
}
5231
);
5232
};
5233
5234
/**
5235
* This method allows you to join a single room. It may be called multiple times to be in
5236
* multiple rooms simultaneously. It may be called before or after connecting to the server.
5237
* Note: the successCB and failureDB will only be called if you are already connected to the server.
5238
* @param {String} roomName the room to be joined.
5239
* @param {Object} roomParameters application specific parameters, can be null.
5240
* @param {Function} successCB called once, with a roomName as it's argument, once the room is joined.
5241
* @param {Function} failureCB called if the room can not be joined. The arguments of failureCB are errorCode, errorText, roomName.
5242
*/
5243
this.joinRoom = function(roomName, roomParameters, successCB, failureCB) {
5244
if (self.roomJoin[roomName]) {
5245
self.showError(self.errCodes.DEVELOPER_ERR, "Attempt to join room " + roomName + " which you are already in.");
5246
return;
5247
}
5248
5249
var newRoomData = {roomName: roomName};
5250
if (roomParameters) {
5251
try {
5252
JSON.stringify(roomParameters);
5253
} catch (error) {
5254
self.showError(self.errCodes.DEVELOPER_ERR, "non-jsonable parameter to easyrtc.joinRoom");
5255
throw "Developer error, see application error messages";
5256
}
5257
var parameters = {};
5258
for (var key in roomParameters) {
5259
if (roomParameters.hasOwnProperty(key)) {
5260
parameters[key] = roomParameters[key];
5261
}
5262
}
5263
newRoomData.roomParameter = parameters;
5264
}
5265
var msgData = {
5266
roomJoin: {}
5267
};
5268
var roomData;
5269
var signallingSuccess, signallingFailure;
5270
if (self.webSocket) {
5271
5272
msgData.roomJoin[roomName] = newRoomData;
5273
signallingSuccess = function(msgType, msgData) {
5274
5275
roomData = msgData.roomData;
5276
self.roomJoin[roomName] = newRoomData;
5277
if (successCB) {
5278
successCB(roomName);
5279
}
5280
5281
processRoomData(roomData);
5282
};
5283
signallingFailure = function(errorCode, errorText) {
5284
if (failureCB) {
5285
failureCB(errorCode, errorText, roomName);
5286
}
5287
else {
5288
self.showError(errorCode, self.format(self.getConstantString("unableToEnterRoom"), roomName, errorText));
5289
}
5290
};
5291
sendSignalling(null, "roomJoin", msgData, signallingSuccess, signallingFailure);
5292
}
5293
else {
5294
self.roomJoin[roomName] = newRoomData;
5295
}
5296
5297
};
5298
5299
/**
5300
* This function allows you to leave a single room. Note: the successCB and failureDB
5301
*  arguments are optional and will only be called if you are already connected to the server.
5302
* @param {String} roomName
5303
* @param {Function} successCallback - A function which expects a roomName.
5304
* @param {Function} failureCallback - A function which expects the following arguments: errorCode, errorText, roomName.
5305
* @example
5306
*    easyrtc.leaveRoom("freds_room");
5307
*    easyrtc.leaveRoom("freds_room", function(roomName){ console.log("left the room")},
5308
*                       function(errorCode, errorText, roomName){ console.log("left the room")});
5309
*/
5310
this.leaveRoom = function(roomName, successCallback, failureCallback) {
5311
var roomItem;
5312
if (self.roomJoin[roomName]) {
5313
if (!self.webSocket) {
5314
delete self.roomJoin[roomName];
5315
}
5316
else {
5317
roomItem = {};
5318
roomItem[roomName] = {roomName: roomName};
5319
sendSignalling(null, "roomLeave", {roomLeave: roomItem},
5320
function(msgType, msgData) {
5321
var roomData = msgData.roomData;
5322
processRoomData(roomData);
5323
if (successCallback) {
5324
successCallback(roomName);
5325
}
5326
},
5327
function(errorCode, errorText) {
5328
if (failureCallback) {
5329
failureCallback(errorCode, errorText, roomName);
5330
}
5331
});
5332
}
5333
}
5334
};
5335
5336
/** Get a list of the rooms you are in. You must be connected to call this function.
5337
* @returns {Object} A map whose keys are the room names
5338
*/
5339
this.getRoomsJoined = function() {
5340
var roomsIn = {};
5341
var key;
5342
for (key in self.roomJoin) {
5343
if (self.roomJoin.hasOwnProperty(key)) {
5344
roomsIn[key] = true;
5345
}
5346
}
5347
return roomsIn;
5348
};
5349
5350
/** Get server defined fields associated with a particular room. Only valid
5351
* after a connection has been made.
5352
* @param {String} roomName - the name of the room you want the fields for.
5353
* @returns {Object} A dictionary containing entries of the form {key:{'fieldName':key, 'fieldValue':value1}} or undefined
5354
* if you are not connected to the room.
5355
*/
5356
this.getRoomFields = function(roomName) {
5357
return (!fields || !fields.rooms || !fields.rooms[roomName]) ?
5358
undefined : fields.rooms[roomName];
5359
};
5360
5361
/** Get server defined fields associated with the current application. Only valid
5362
* after a connection has been made.
5363
* @returns {Object} A dictionary containing entries of the form {key:{'fieldName':key, 'fieldValue':value1}}
5364
*/
5365
this.getApplicationFields = function() {
5366
return fields.application;
5367
};
5368
5369
/** Get server defined fields associated with the connection. Only valid
5370
* after a connection has been made.
5371
* @returns {Object} A dictionary containing entries of the form {key:{'fieldName':key, 'fieldValue':value1}}
5372
*/
5373
this.getConnectionFields = function() {
5374
return fields.connection;
5375
};
5376
5377
/**
5378
* Supply a socket.io connection that will be used instead of allocating a new socket.
5379
* The expected usage is that you allocate a websocket, assign options to it, call
5380
* easyrtc.useThisSocketConnection, followed by easyrtc.connect or easyrtc.easyApp. Easyrtc will not attempt to
5381
* close sockets that were supplied with easyrtc.useThisSocketConnection.
5382
* @param {Object} alreadyAllocatedSocketIo A value allocated with the connect method of socket.io.
5383
*/
5384
this.useThisSocketConnection = function(alreadyAllocatedSocketIo) {
5385
preallocatedSocketIo = alreadyAllocatedSocketIo;
5386
};
5387
5388
/** @private */
5389
function processToken(msg) {
5390
var msgData = msg.msgData;
5391
logDebug("entered process token");
5392
5393
if (msgData.easyrtcid) {
5394
self.myEasyrtcid = msgData.easyrtcid;
5395
}
5396
if (msgData.field) {
5397
fields.connection = msgData.field;
5398
}
5399
if (msgData.iceConfig) {
5400
processIceConfig(msgData.iceConfig);
5401
}
5402
5403
if (msgData.sessionData) {
5404
processSessionData(msgData.sessionData);
5405
}
5406
if (msgData.roomData) {
5407
processRoomData(msgData.roomData);
5408
}
5409
if (msgData.application.field) {
5410
fields.application = msgData.application.field;
5411
}
5412
}
5413
5414
/** @private */
5415
function sendAuthenticate(successCallback, errorCallback) {
5416
//
5417
// find our easyrtcsid
5418
//
5419
var cookies, target, i;
5420
var easyrtcsid = null;
5421
5422
if (self.cookieId && document.cookie) {
5423
cookies = document.cookie.split(/[; ]/g);
5424
target = self.cookieId + "=";
5425
for (i = 0; i < cookies.length; i++) {
5426
if (cookies[i].indexOf(target) === 0) {
5427
easyrtcsid = cookies[i].substring(target.length);
5428
}
5429
}
5430
}
5431
5432
var msgData = {
5433
apiVersion: self.apiVersion,
5434
applicationName: self.applicationName,
5435
setUserCfg: collectConfigurationInfo(true)
5436
};
5437
5438
if (!self.roomJoin) {
5439
self.roomJoin = {};
5440
}
5441
if (self.presenceShow) {
5442
msgData.setPresence = {
5443
show: self.presenceShow,
5444
status: self.presenceStatus
5445
};
5446
}
5447
if (self.username) {
5448
msgData.username = self.username;
5449
}
5450
if (self.roomJoin && !isEmptyObj(self.roomJoin)) {
5451
msgData.roomJoin = self.roomJoin;
5452
}
5453
if (easyrtcsid) {
5454
msgData.easyrtcsid = easyrtcsid;
5455
}
5456
if (credential) {
5457
msgData.credential = credential;
5458
}
5459
5460
self.webSocket.json.emit(
5461
"easyrtcAuth",
5462
{
5463
msgType: "authenticate",
5464
msgData: msgData
5465
},
5466
function(msg) {
5467
var room;
5468
if (msg.msgType === "error") {
5469
errorCallback(msg.msgData.errorCode, msg.msgData.errorText);
5470
self.roomJoin = {};
5471
}
5472
else {
5473
processToken(msg);
5474
if (self._roomApiFields) {
5475
for (room in self._roomApiFields) {
5476
if (self._roomApiFields.hasOwnProperty(room)) {
5477
enqueueSendRoomApi(room);
5478
}
5479
}
5480
}
5481
5482
if (successCallback) {
5483
successCallback(self.myEasyrtcid);
5484
}
5485
}
5486
}
5487
);
5488
}
5489
5490
/** @private */
5491
function connectToWSServer(successCallback, errorCallback) {
5492
var i;
5493
if (preallocatedSocketIo) {
5494
self.webSocket = preallocatedSocketIo;
5495
}
5496
else if (!self.webSocket) {
5497
try {
5498
self.webSocket = io.connect(serverPath, connectionOptions);
5499
5500
if (!self.webSocket) {
5501
throw "io.connect failed";
5502
}
5503
5504
} catch(socketErr) {
5505
self.webSocket = 0;
5506
errorCallback( self.errCodes.SYSTEM_ERROR, socketErr.toString());
5507
5508
return;
5509
}
5510
}
5511
else {
5512
for (i in self.websocketListeners) {
5513
if (!self.websocketListeners.hasOwnProperty(i)) {
5514
continue;
5515
}
5516
self.webSocket.removeEventListener(self.websocketListeners[i].event,
5517
self.websocketListeners[i].handler);
5518
}
5519
}
5520
5521
self.websocketListeners = [];
5522
5523
function addSocketListener(event, handler) {
5524
self.webSocket.on(event, handler);
5525
self.websocketListeners.push({event: event, handler: handler});
5526
}
5527
5528
addSocketListener("close", function(event) {
5529
logDebug("the web socket closed");
5530
});
5531
5532
addSocketListener('error', function(event) {
5533
function handleErrorEvent() {
5534
if (self.myEasyrtcid) {
5535
//
5536
// socket.io version 1 got rid of the socket member, moving everything up one level.
5537
//
5538
if (isSocketConnected(self.webSocket)) {
5539
self.showError(self.errCodes.SIGNAL_ERR, self.getConstantString("miscSignalError"));
5540
}
5541
else {
5542
/* socket server went down. this will generate a 'disconnect' event as well, so skip this event */
5543
errorCallback(self.errCodes.CONNECT_ERR, self.getConstantString("noServer"));
5544
}
5545
}
5546
else {
5547
errorCallback(self.errCodes.CONNECT_ERR, self.getConstantString("noServer"));
5548
}
5549
}
5550
handleErrorEvent();
5551
});
5552
5553
function connectHandler(event) {
5554
self.webSocketConnected = true;
5555
if (!self.webSocket) {
5556
self.showError(self.errCodes.CONNECT_ERR, self.getConstantString("badsocket"));
5557
}
5558
5559
logDebug("saw socket-server onconnect event");
5560
5561
if (self.webSocketConnected) {
5562
sendAuthenticate(successCallback, errorCallback);
5563
}
5564
else {
5565
errorCallback(self.errCodes.SIGNAL_ERR, self.getConstantString("icf"));
5566
}
5567
}
5568
5569
if (isSocketConnected(preallocatedSocketIo)) {
5570
connectHandler(null);
5571
}
5572
else {
5573
addSocketListener("connect", connectHandler);
5574
}
5575
5576
addSocketListener("easyrtcMsg", onChannelMsg);
5577
addSocketListener("easyrtcCmd", onChannelCmd);
5578
addSocketListener("disconnect", function(/* code, reason, wasClean */) {
5579
5580
self.webSocketConnected = false;
5581
updateConfigurationInfo = function() {}; // dummy update function
5582
oldConfig = {};
5583
disconnectBody();
5584
5585
if (self.disconnectListener) {
5586
self.disconnectListener();
5587
}
5588
});
5589
}
5590
5591
/**
5592
* Connects to the EasyRTC signaling server. You must connect before trying to
5593
* call other users.
5594
* @param {String} applicationName is a string that identifies the application so that different applications can have different
5595
*        lists of users. Note that the server configuration specifies a regular expression that is used to check application names
5596
*        for validity. The default pattern is that of an identifier, spaces are not allowed.
5597
* @param {Function} successCallback (easyrtcId, roomOwner) - is called on successful connect. easyrtcId is the
5598
*   unique name that the client is known to the server by. A client usually only needs it's own easyrtcId for debugging purposes.
5599
*       roomOwner is true if the user is the owner of a room. It's value is random if the user is in multiple rooms.
5600
* @param {Function} errorCallback (errorCode, errorText) - is called on unsuccessful connect. if null, an alert is called instead.
5601
*  The errorCode takes it's value from easyrtc.errCodes.
5602
* @example
5603
*   easyrtc.connect("my_chat_app",
5604
*                   function(easyrtcid, roomOwner){
5605
*                       if( roomOwner){ console.log("I'm the room owner"); }
5606
*                       console.log("my id is " + easyrtcid);
5607
*                   },
5608
*                   function(errorText){
5609
*                       console.log("failed to connect ", erFrText);
5610
*                   });
5611
*/
5612
this.connect = function(applicationName, successCallback, errorCallback) {
5613
5614
// Detect invalid or missing socket.io
5615
if (!io) {
5616
self.showError(self.errCodes.DEVELOPER_ERR, "Your HTML has not included the socket.io.js library");
5617
}
5618
5619
if (!preallocatedSocketIo && self.webSocket) {
5620
self.showError(self.errCodes.DEVELOPER_ERR, "Attempt to connect when already connected to socket server");
5621
return;
5622
}
5623
pc_config = {};
5624
closedChannel = null;
5625
oldConfig = {}; // used internally by updateConfiguration
5626
queuedMessages = {};
5627
self.applicationName = applicationName;
5628
fields = {
5629
rooms: {},
5630
application: {},
5631
connection: {}
5632
};
5633
5634
logDebug("attempt to connect to WebRTC signalling server with application name=" + applicationName);
5635
5636
if (errorCallback === null) {
5637
errorCallback = function(errorCode, errorText) {
5638
self.showError(errorCode, errorText);
5639
};
5640
}
5641
5642
connectToWSServer(successCallback, errorCallback);
5643
};
5644
};
5645
5646
return new Easyrtc();
5647
5648
}));