@nethesis/phone-island 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3661 @@
1
+ "use strict";
2
+
3
+ /*
4
+ The MIT License (MIT)
5
+
6
+ Copyright (c) 2016 Meetecho
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining
9
+ a copy of this software and associated documentation files (the "Software"),
10
+ to deal in the Software without restriction, including without limitation
11
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
12
+ and/or sell copies of the Software, and to permit persons to whom the
13
+ Software is furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included
16
+ in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
22
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
23
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ OTHER DEALINGS IN THE SOFTWARE.
25
+ */
26
+
27
+ // List of sessions
28
+ Janus.sessions = {};
29
+
30
+ Janus.isExtensionEnabled = function() {
31
+ if(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
32
+ // No need for the extension, getDisplayMedia is supported
33
+ return true;
34
+ }
35
+ if(window.navigator.userAgent.match('Chrome')) {
36
+ var chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10);
37
+ var maxver = 33;
38
+ if(window.navigator.userAgent.match('Linux'))
39
+ maxver = 35; // "known" crash in chrome 34 and 35 on linux
40
+ if(chromever >= 26 && chromever <= maxver) {
41
+ // Older versions of Chrome don't support this extension-based approach, so lie
42
+ return true;
43
+ }
44
+ return Janus.extension.isInstalled();
45
+ } else {
46
+ // Firefox and others, no need for the extension (but this doesn't mean it will work)
47
+ return true;
48
+ }
49
+ };
50
+
51
+ var defaultExtension = {
52
+ // Screensharing Chrome Extension ID
53
+ extensionId: 'hapfgfdkleiggjjpfpenajgdnfckjpaj',
54
+ isInstalled: function() { return document.querySelector('#janus-extension-installed') !== null; },
55
+ getScreen: function (callback) {
56
+ var pending = window.setTimeout(function () {
57
+ var error = new Error('NavigatorUserMediaError');
58
+ error.name = 'The required Chrome extension is not installed: click <a href="#">here</a> to install it. (NOTE: this will need you to refresh the page)';
59
+ return callback(error);
60
+ }, 1000);
61
+ this.cache[pending] = callback;
62
+ window.postMessage({ type: 'janusGetScreen', id: pending }, '*');
63
+ },
64
+ init: function () {
65
+ var cache = {};
66
+ this.cache = cache;
67
+ // Wait for events from the Chrome Extension
68
+ window.addEventListener('message', function (event) {
69
+ if(event.origin != window.location.origin)
70
+ return;
71
+ if(event.data.type == 'janusGotScreen' && cache[event.data.id]) {
72
+ var callback = cache[event.data.id];
73
+ delete cache[event.data.id];
74
+
75
+ if (event.data.sourceId === '') {
76
+ // user canceled
77
+ var error = new Error('NavigatorUserMediaError');
78
+ error.name = 'You cancelled the request for permission, giving up...';
79
+ callback(error);
80
+ } else {
81
+ callback(null, event.data.sourceId);
82
+ }
83
+ } else if (event.data.type == 'janusGetScreenPending') {
84
+ console.log('clearing ', event.data.id);
85
+ window.clearTimeout(event.data.id);
86
+ }
87
+ });
88
+ }
89
+ };
90
+
91
+ Janus.useDefaultDependencies = function (deps) {
92
+ var f = (deps && deps.fetch) || fetch;
93
+ var p = (deps && deps.Promise) || Promise;
94
+ var socketCls = (deps && deps.WebSocket) || WebSocket;
95
+
96
+ return {
97
+ newWebSocket: function(server, proto) { return new socketCls(server, proto); },
98
+ extension: (deps && deps.extension) || defaultExtension,
99
+ isArray: function(arr) { return Array.isArray(arr); },
100
+ webRTCAdapter: (deps && deps.adapter) || adapter,
101
+ httpAPICall: function(url, options) {
102
+ var fetchOptions = {
103
+ method: options.verb,
104
+ headers: {
105
+ 'Accept': 'application/json, text/plain, */*'
106
+ },
107
+ cache: 'no-cache'
108
+ };
109
+ if(options.verb === "POST") {
110
+ fetchOptions.headers['Content-Type'] = 'application/json';
111
+ }
112
+ if(options.withCredentials !== undefined) {
113
+ fetchOptions.credentials = options.withCredentials === true ? 'include' : (options.withCredentials ? options.withCredentials : 'omit');
114
+ }
115
+ if(options.body) {
116
+ fetchOptions.body = JSON.stringify(options.body);
117
+ }
118
+
119
+ var fetching = f(url, fetchOptions).catch(function(error) {
120
+ return p.reject({message: 'Probably a network error, is the server down?', error: error});
121
+ });
122
+
123
+ /*
124
+ * fetch() does not natively support timeouts.
125
+ * Work around this by starting a timeout manually, and racing it agains the fetch() to see which thing resolves first.
126
+ */
127
+
128
+ if(options.timeout) {
129
+ var timeout = new p(function(resolve, reject) {
130
+ var timerId = setTimeout(function() {
131
+ clearTimeout(timerId);
132
+ return reject({message: 'Request timed out', timeout: options.timeout});
133
+ }, options.timeout);
134
+ });
135
+ fetching = p.race([fetching, timeout]);
136
+ }
137
+
138
+ fetching.then(function(response) {
139
+ if(response.ok) {
140
+ if(typeof(options.success) === typeof(Janus.noop)) {
141
+ return response.json().then(function(parsed) {
142
+ try {
143
+ options.success(parsed);
144
+ } catch(error) {
145
+ Janus.error('Unhandled httpAPICall success callback error', error);
146
+ }
147
+ }, function(error) {
148
+ return p.reject({message: 'Failed to parse response body', error: error, response: response});
149
+ });
150
+ }
151
+ }
152
+ else {
153
+ return p.reject({message: 'API call failed', response: response});
154
+ }
155
+ }).catch(function(error) {
156
+ if(typeof(options.error) === typeof(Janus.noop)) {
157
+ options.error(error.message || '<< internal error >>', error);
158
+ }
159
+ });
160
+
161
+ return fetching;
162
+ }
163
+ }
164
+ };
165
+
166
+ Janus.useOldDependencies = function (deps) {
167
+ var jq = (deps && deps.jQuery) || jQuery;
168
+ var socketCls = (deps && deps.WebSocket) || WebSocket;
169
+ return {
170
+ newWebSocket: function(server, proto) { return new socketCls(server, proto); },
171
+ isArray: function(arr) { return jq.isArray(arr); },
172
+ extension: (deps && deps.extension) || defaultExtension,
173
+ webRTCAdapter: (deps && deps.adapter) || adapter,
174
+ httpAPICall: function(url, options) {
175
+ var payload = options.body !== undefined ? {
176
+ contentType: 'application/json',
177
+ data: JSON.stringify(options.body)
178
+ } : {};
179
+ var credentials = options.withCredentials !== undefined ? {xhrFields: {withCredentials: options.withCredentials}} : {};
180
+
181
+ return jq.ajax(jq.extend(payload, credentials, {
182
+ url: url,
183
+ type: options.verb,
184
+ cache: false,
185
+ dataType: 'json',
186
+ async: options.async,
187
+ timeout: options.timeout,
188
+ success: function(result) {
189
+ if(typeof(options.success) === typeof(Janus.noop)) {
190
+ options.success(result);
191
+ }
192
+ },
193
+ error: function(xhr, status, err) {
194
+ if(typeof(options.error) === typeof(Janus.noop)) {
195
+ options.error(status, err);
196
+ }
197
+ }
198
+ }));
199
+ }
200
+ };
201
+ };
202
+
203
+ Janus.noop = function() {};
204
+
205
+ Janus.dataChanDefaultLabel = "JanusDataChannel";
206
+
207
+ // Note: in the future we may want to change this, e.g., as was
208
+ // attempted in https://github.com/meetecho/janus-gateway/issues/1670
209
+ Janus.endOfCandidates = null;
210
+
211
+ // Stop all tracks from a given stream
212
+ Janus.stopAllTracks = function(stream) {
213
+ try {
214
+ // Try a MediaStreamTrack.stop() for each track
215
+ var tracks = stream.getTracks();
216
+ for(var mst of tracks) {
217
+ Janus.log(mst);
218
+ if(mst) {
219
+ mst.stop();
220
+ }
221
+ }
222
+ } catch(e) {
223
+ // Do nothing if this fails
224
+ }
225
+ }
226
+
227
+ // Initialization
228
+ Janus.init = function(options) {
229
+ options = options || {};
230
+ options.callback = (typeof options.callback == "function") ? options.callback : Janus.noop;
231
+ if(Janus.initDone) {
232
+ // Already initialized
233
+ options.callback();
234
+ } else {
235
+ if(typeof console.log == "undefined") {
236
+ console.log = function() {};
237
+ }
238
+ // Console logging (all debugging disabled by default)
239
+ Janus.trace = Janus.noop;
240
+ Janus.debug = Janus.noop;
241
+ Janus.vdebug = Janus.noop;
242
+ Janus.log = Janus.noop;
243
+ Janus.warn = Janus.noop;
244
+ Janus.error = Janus.noop;
245
+ if(options.debug === true || options.debug === "all") {
246
+ // Enable all debugging levels
247
+ Janus.trace = console.trace.bind(console);
248
+ Janus.debug = console.debug.bind(console);
249
+ Janus.vdebug = console.debug.bind(console);
250
+ Janus.log = console.log.bind(console);
251
+ Janus.warn = console.warn.bind(console);
252
+ Janus.error = console.error.bind(console);
253
+ } else if(Array.isArray(options.debug)) {
254
+ for(var d of options.debug) {
255
+ switch(d) {
256
+ case "trace":
257
+ Janus.trace = console.trace.bind(console);
258
+ break;
259
+ case "debug":
260
+ Janus.debug = console.debug.bind(console);
261
+ break;
262
+ case "vdebug":
263
+ Janus.vdebug = console.debug.bind(console);
264
+ break;
265
+ case "log":
266
+ Janus.log = console.log.bind(console);
267
+ break;
268
+ case "warn":
269
+ Janus.warn = console.warn.bind(console);
270
+ break;
271
+ case "error":
272
+ Janus.error = console.error.bind(console);
273
+ break;
274
+ default:
275
+ console.error("Unknown debugging option '" + d + "' (supported: 'trace', 'debug', 'vdebug', 'log', warn', 'error')");
276
+ break;
277
+ }
278
+ }
279
+ }
280
+ Janus.log("Initializing library");
281
+
282
+ var usedDependencies = options.dependencies || Janus.useDefaultDependencies();
283
+ Janus.isArray = usedDependencies.isArray;
284
+ Janus.webRTCAdapter = usedDependencies.webRTCAdapter;
285
+ Janus.httpAPICall = usedDependencies.httpAPICall;
286
+ Janus.newWebSocket = usedDependencies.newWebSocket;
287
+ Janus.extension = usedDependencies.extension;
288
+ Janus.extension.init();
289
+
290
+ // Helper method to enumerate devices
291
+ Janus.listDevices = function(callback, config) {
292
+ callback = (typeof callback == "function") ? callback : Janus.noop;
293
+ if (config == null) config = { audio: true, video: true };
294
+ if(Janus.isGetUserMediaAvailable()) {
295
+ navigator.mediaDevices.getUserMedia(config)
296
+ .then(function(stream) {
297
+ navigator.mediaDevices.enumerateDevices().then(function(devices) {
298
+ Janus.debug(devices);
299
+ callback(devices);
300
+ // Get rid of the now useless stream
301
+ Janus.stopAllTracks(stream)
302
+ });
303
+ })
304
+ .catch(function(err) {
305
+ Janus.error(err);
306
+ callback([]);
307
+ });
308
+ } else {
309
+ Janus.warn("navigator.mediaDevices unavailable");
310
+ callback([]);
311
+ }
312
+ };
313
+ // Helper methods to attach/reattach a stream to a video element (previously part of adapter.js)
314
+ Janus.attachMediaStream = function(element, stream) {
315
+ try {
316
+ element.srcObject = stream;
317
+ } catch (e) {
318
+ try {
319
+ element.src = URL.createObjectURL(stream);
320
+ } catch (e) {
321
+ Janus.error("Error attaching stream to element");
322
+ }
323
+ }
324
+ };
325
+ Janus.reattachMediaStream = function(to, from) {
326
+ try {
327
+ to.srcObject = from.srcObject;
328
+ } catch (e) {
329
+ try {
330
+ to.src = from.src;
331
+ } catch (e) {
332
+ Janus.error("Error reattaching stream to element");
333
+ }
334
+ }
335
+ };
336
+ // Detect tab close: make sure we don't loose existing onbeforeunload handlers
337
+ // (note: for iOS we need to subscribe to a different event, 'pagehide', see
338
+ // https://gist.github.com/thehunmonkgroup/6bee8941a49b86be31a787fe8f4b8cfe)
339
+ var iOS = ['iPad', 'iPhone', 'iPod'].indexOf(navigator.platform) >= 0;
340
+ var eventName = iOS ? 'pagehide' : 'beforeunload';
341
+ var oldOBF = window["on" + eventName];
342
+ window.addEventListener(eventName, function() {
343
+ Janus.log("Closing window");
344
+ for(var s in Janus.sessions) {
345
+ if(Janus.sessions[s] && Janus.sessions[s].destroyOnUnload) {
346
+ Janus.log("Destroying session " + s);
347
+ Janus.sessions[s].destroy({unload: true, notifyDestroyed: false});
348
+ }
349
+ }
350
+ if(oldOBF && typeof oldOBF == "function") {
351
+ oldOBF();
352
+ }
353
+ });
354
+ // If this is a Safari Technology Preview, check if VP8 is supported
355
+ Janus.safariVp8 = false;
356
+ if(Janus.webRTCAdapter.browserDetails.browser === 'safari' &&
357
+ Janus.webRTCAdapter.browserDetails.version >= 605) {
358
+ // Let's see if RTCRtpSender.getCapabilities() is there
359
+ if(RTCRtpSender && RTCRtpSender.getCapabilities && RTCRtpSender.getCapabilities("video") &&
360
+ RTCRtpSender.getCapabilities("video").codecs && RTCRtpSender.getCapabilities("video").codecs.length) {
361
+ for(var codec of RTCRtpSender.getCapabilities("video").codecs) {
362
+ if(codec && codec.mimeType && codec.mimeType.toLowerCase() === "video/vp8") {
363
+ Janus.safariVp8 = true;
364
+ break;
365
+ }
366
+ }
367
+ if(Janus.safariVp8) {
368
+ Janus.log("This version of Safari supports VP8");
369
+ } else {
370
+ Janus.warn("This version of Safari does NOT support VP8: if you're using a Technology Preview, " +
371
+ "try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu");
372
+ }
373
+ } else {
374
+ // We do it in a very ugly way, as there's no alternative...
375
+ // We create a PeerConnection to see if VP8 is in an offer
376
+ var testpc = new RTCPeerConnection({});
377
+ testpc.createOffer({offerToReceiveVideo: true}).then(function(offer) {
378
+ Janus.safariVp8 = offer.sdp.indexOf("VP8") !== -1;
379
+ if(Janus.safariVp8) {
380
+ Janus.log("This version of Safari supports VP8");
381
+ } else {
382
+ Janus.warn("This version of Safari does NOT support VP8: if you're using a Technology Preview, " +
383
+ "try enabling the 'WebRTC VP8 codec' setting in the 'Experimental Features' Develop menu");
384
+ }
385
+ testpc.close();
386
+ testpc = null;
387
+ });
388
+ }
389
+ }
390
+ // Check if this browser supports Unified Plan and transceivers
391
+ // Based on https://codepen.io/anon/pen/ZqLwWV?editors=0010
392
+ Janus.unifiedPlan = false;
393
+ if(Janus.webRTCAdapter.browserDetails.browser === 'firefox' &&
394
+ Janus.webRTCAdapter.browserDetails.version >= 59) {
395
+ // Firefox definitely does, starting from version 59
396
+ Janus.unifiedPlan = true;
397
+ } else if(Janus.webRTCAdapter.browserDetails.browser === 'chrome' &&
398
+ Janus.webRTCAdapter.browserDetails.version >= 72) {
399
+ // Chrome does, but it's only usable from version 72 on
400
+ Janus.unifiedPlan = true;
401
+ } else if(!window.RTCRtpTransceiver || !('currentDirection' in RTCRtpTransceiver.prototype)) {
402
+ // Safari supports addTransceiver() but not Unified Plan when
403
+ // currentDirection is not defined (see codepen above).
404
+ Janus.unifiedPlan = false;
405
+ } else {
406
+ // Check if addTransceiver() throws an exception
407
+ var tempPc = new RTCPeerConnection();
408
+ try {
409
+ tempPc.addTransceiver('audio');
410
+ Janus.unifiedPlan = true;
411
+ } catch (e) {}
412
+ tempPc.close();
413
+ }
414
+ Janus.initDone = true;
415
+ options.callback();
416
+ }
417
+ };
418
+
419
+ // Helper method to check whether WebRTC is supported by this browser
420
+ Janus.isWebrtcSupported = function() {
421
+ return !!window.RTCPeerConnection;
422
+ };
423
+ // Helper method to check whether devices can be accessed by this browser (e.g., not possible via plain HTTP)
424
+ Janus.isGetUserMediaAvailable = function() {
425
+ return navigator.mediaDevices && navigator.mediaDevices.getUserMedia;
426
+ };
427
+
428
+ // Helper method to create random identifiers (e.g., transaction)
429
+ Janus.randomString = function(len) {
430
+ var charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
431
+ var randomString = '';
432
+ for (var i = 0; i < len; i++) {
433
+ var randomPoz = Math.floor(Math.random() * charSet.length);
434
+ randomString += charSet.substring(randomPoz,randomPoz+1);
435
+ }
436
+ return randomString;
437
+ };
438
+
439
+ function Janus(gatewayCallbacks) {
440
+ gatewayCallbacks = gatewayCallbacks || {};
441
+ gatewayCallbacks.success = (typeof gatewayCallbacks.success == "function") ? gatewayCallbacks.success : Janus.noop;
442
+ gatewayCallbacks.error = (typeof gatewayCallbacks.error == "function") ? gatewayCallbacks.error : Janus.noop;
443
+ gatewayCallbacks.destroyed = (typeof gatewayCallbacks.destroyed == "function") ? gatewayCallbacks.destroyed : Janus.noop;
444
+ if(!Janus.initDone) {
445
+ gatewayCallbacks.error("Library not initialized");
446
+ return {};
447
+ }
448
+ if(!Janus.isWebrtcSupported()) {
449
+ gatewayCallbacks.error("WebRTC not supported by this browser");
450
+ return {};
451
+ }
452
+ Janus.log("Library initialized: " + Janus.initDone);
453
+ if(!gatewayCallbacks.server) {
454
+ gatewayCallbacks.error("Invalid server url");
455
+ return {};
456
+ }
457
+ var websockets = false;
458
+ var ws = null;
459
+ var wsHandlers = {};
460
+ var wsKeepaliveTimeoutId = null;
461
+ var servers = null;
462
+ var serversIndex = 0;
463
+ var server = gatewayCallbacks.server;
464
+ if(Janus.isArray(server)) {
465
+ Janus.log("Multiple servers provided (" + server.length + "), will use the first that works");
466
+ server = null;
467
+ servers = gatewayCallbacks.server;
468
+ Janus.debug(servers);
469
+ } else {
470
+ if(server.indexOf("ws") === 0) {
471
+ websockets = true;
472
+ Janus.log("Using WebSockets to contact Janus: " + server);
473
+ } else {
474
+ websockets = false;
475
+ Janus.log("Using REST API to contact Janus: " + server);
476
+ }
477
+ }
478
+ var iceServers = gatewayCallbacks.iceServers || [{urls: "stun:stun.l.google.com:19302"}];
479
+ var iceTransportPolicy = gatewayCallbacks.iceTransportPolicy;
480
+ var bundlePolicy = gatewayCallbacks.bundlePolicy;
481
+ // Whether IPv6 candidates should be gathered
482
+ var ipv6Support = (gatewayCallbacks.ipv6 === true);
483
+ // Whether we should enable the withCredentials flag for XHR requests
484
+ var withCredentials = false;
485
+ if(gatewayCallbacks.withCredentials !== undefined && gatewayCallbacks.withCredentials !== null)
486
+ withCredentials = gatewayCallbacks.withCredentials === true;
487
+ // Optional max events
488
+ var maxev = 10;
489
+ if(gatewayCallbacks.max_poll_events !== undefined && gatewayCallbacks.max_poll_events !== null)
490
+ maxev = gatewayCallbacks.max_poll_events;
491
+ if(maxev < 1)
492
+ maxev = 1;
493
+ // Token to use (only if the token based authentication mechanism is enabled)
494
+ var token = null;
495
+ if(gatewayCallbacks.token !== undefined && gatewayCallbacks.token !== null)
496
+ token = gatewayCallbacks.token;
497
+ // API secret to use (only if the shared API secret is enabled)
498
+ var apisecret = null;
499
+ if(gatewayCallbacks.apisecret !== undefined && gatewayCallbacks.apisecret !== null)
500
+ apisecret = gatewayCallbacks.apisecret;
501
+ // Whether we should destroy this session when onbeforeunload is called
502
+ this.destroyOnUnload = true;
503
+ if(gatewayCallbacks.destroyOnUnload !== undefined && gatewayCallbacks.destroyOnUnload !== null)
504
+ this.destroyOnUnload = (gatewayCallbacks.destroyOnUnload === true);
505
+ // Some timeout-related values
506
+ var keepAlivePeriod = 25000;
507
+ if(gatewayCallbacks.keepAlivePeriod !== undefined && gatewayCallbacks.keepAlivePeriod !== null)
508
+ keepAlivePeriod = gatewayCallbacks.keepAlivePeriod;
509
+ if(isNaN(keepAlivePeriod))
510
+ keepAlivePeriod = 25000;
511
+ var longPollTimeout = 60000;
512
+ if(gatewayCallbacks.longPollTimeout !== undefined && gatewayCallbacks.longPollTimeout !== null)
513
+ longPollTimeout = gatewayCallbacks.longPollTimeout;
514
+ if(isNaN(longPollTimeout))
515
+ longPollTimeout = 60000;
516
+
517
+ // overrides for default maxBitrate values for simulcasting
518
+ function getMaxBitrates(simulcastMaxBitrates) {
519
+ var maxBitrates = {
520
+ high: 900000,
521
+ medium: 300000,
522
+ low: 100000,
523
+ };
524
+
525
+ if (simulcastMaxBitrates !== undefined && simulcastMaxBitrates !== null) {
526
+ if (simulcastMaxBitrates.high)
527
+ maxBitrates.high = simulcastMaxBitrates.high;
528
+ if (simulcastMaxBitrates.medium)
529
+ maxBitrates.medium = simulcastMaxBitrates.medium;
530
+ if (simulcastMaxBitrates.low)
531
+ maxBitrates.low = simulcastMaxBitrates.low;
532
+ }
533
+
534
+ return maxBitrates;
535
+ }
536
+
537
+ var connected = false;
538
+ var sessionId = null;
539
+ var pluginHandles = {};
540
+ var that = this;
541
+ var retries = 0;
542
+ var transactions = {};
543
+ createSession(gatewayCallbacks);
544
+
545
+ // Public methods
546
+ this.getServer = function() { return server; };
547
+ this.isConnected = function() { return connected; };
548
+ this.reconnect = function(callbacks) {
549
+ callbacks = callbacks || {};
550
+ callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
551
+ callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
552
+ callbacks["reconnect"] = true;
553
+ createSession(callbacks);
554
+ };
555
+ this.getSessionId = function() { return sessionId; };
556
+ this.getInfo = function(callbacks) { getInfo(callbacks); };
557
+ this.destroy = function(callbacks) { destroySession(callbacks); };
558
+ this.attach = function(callbacks) { createHandle(callbacks); };
559
+
560
+ function eventHandler() {
561
+ if(sessionId == null)
562
+ return;
563
+ Janus.debug('Long poll...');
564
+ if(!connected) {
565
+ Janus.warn("Is the server down? (connected=false)");
566
+ return;
567
+ }
568
+ var longpoll = server + "/" + sessionId + "?rid=" + new Date().getTime();
569
+ if(maxev)
570
+ longpoll = longpoll + "&maxev=" + maxev;
571
+ if(token)
572
+ longpoll = longpoll + "&token=" + encodeURIComponent(token);
573
+ if(apisecret)
574
+ longpoll = longpoll + "&apisecret=" + encodeURIComponent(apisecret);
575
+ Janus.httpAPICall(longpoll, {
576
+ verb: 'GET',
577
+ withCredentials: withCredentials,
578
+ success: handleEvent,
579
+ timeout: longPollTimeout,
580
+ error: function(textStatus, errorThrown) {
581
+ Janus.error(textStatus + ":", errorThrown);
582
+ retries++;
583
+ if(retries > 3) {
584
+ // Did we just lose the server? :-(
585
+ connected = false;
586
+ gatewayCallbacks.error("Lost connection to the server (is it down?)");
587
+ return;
588
+ }
589
+ eventHandler();
590
+ }
591
+ });
592
+ }
593
+
594
+ // Private event handler: this will trigger plugin callbacks, if set
595
+ function handleEvent(json, skipTimeout) {
596
+ retries = 0;
597
+ if(!websockets && sessionId !== undefined && sessionId !== null && skipTimeout !== true)
598
+ eventHandler();
599
+ if(!websockets && Janus.isArray(json)) {
600
+ // We got an array: it means we passed a maxev > 1, iterate on all objects
601
+ for(var i=0; i<json.length; i++) {
602
+ handleEvent(json[i], true);
603
+ }
604
+ return;
605
+ }
606
+ if(json["janus"] === "keepalive") {
607
+ // Nothing happened
608
+ Janus.vdebug("Got a keepalive on session " + sessionId);
609
+ return;
610
+ } else if(json["janus"] === "server_info") {
611
+ // Just info on the Janus instance
612
+ Janus.debug("Got info on the Janus instance");
613
+ Janus.debug(json);
614
+ const transaction = json["transaction"];
615
+ if(transaction) {
616
+ const reportSuccess = transactions[transaction];
617
+ if(reportSuccess)
618
+ reportSuccess(json);
619
+ delete transactions[transaction];
620
+ }
621
+ return;
622
+ } else if(json["janus"] === "ack") {
623
+ // Just an ack, we can probably ignore
624
+ Janus.debug("Got an ack on session " + sessionId);
625
+ Janus.debug(json);
626
+ const transaction = json["transaction"];
627
+ if(transaction) {
628
+ const reportSuccess = transactions[transaction];
629
+ if(reportSuccess)
630
+ reportSuccess(json);
631
+ delete transactions[transaction];
632
+ }
633
+ return;
634
+ } else if(json["janus"] === "success") {
635
+ // Success!
636
+ Janus.debug("Got a success on session " + sessionId);
637
+ Janus.debug(json);
638
+ const transaction = json["transaction"];
639
+ if(transaction) {
640
+ const reportSuccess = transactions[transaction];
641
+ if(reportSuccess)
642
+ reportSuccess(json);
643
+ delete transactions[transaction];
644
+ }
645
+ return;
646
+ } else if(json["janus"] === "trickle") {
647
+ // We got a trickle candidate from Janus
648
+ const sender = json["sender"];
649
+ if(!sender) {
650
+ Janus.warn("Missing sender...");
651
+ return;
652
+ }
653
+ const pluginHandle = pluginHandles[sender];
654
+ if(!pluginHandle) {
655
+ Janus.debug("This handle is not attached to this session");
656
+ return;
657
+ }
658
+ var candidate = json["candidate"];
659
+ Janus.debug("Got a trickled candidate on session " + sessionId);
660
+ Janus.debug(candidate);
661
+ var config = pluginHandle.webrtcStuff;
662
+ if(config.pc && config.remoteSdp) {
663
+ // Add candidate right now
664
+ Janus.debug("Adding remote candidate:", candidate);
665
+ if(!candidate || candidate.completed === true) {
666
+ // end-of-candidates
667
+ config.pc.addIceCandidate(Janus.endOfCandidates);
668
+ } else {
669
+ // New candidate
670
+ config.pc.addIceCandidate(candidate);
671
+ }
672
+ } else {
673
+ // We didn't do setRemoteDescription (trickle got here before the offer?)
674
+ Janus.debug("We didn't do setRemoteDescription (trickle got here before the offer?), caching candidate");
675
+ if(!config.candidates)
676
+ config.candidates = [];
677
+ config.candidates.push(candidate);
678
+ Janus.debug(config.candidates);
679
+ }
680
+ } else if(json["janus"] === "webrtcup") {
681
+ // The PeerConnection with the server is up! Notify this
682
+ Janus.debug("Got a webrtcup event on session " + sessionId);
683
+ Janus.debug(json);
684
+ const sender = json["sender"];
685
+ if(!sender) {
686
+ Janus.warn("Missing sender...");
687
+ return;
688
+ }
689
+ const pluginHandle = pluginHandles[sender];
690
+ if(!pluginHandle) {
691
+ Janus.debug("This handle is not attached to this session");
692
+ return;
693
+ }
694
+ pluginHandle.webrtcState(true);
695
+ return;
696
+ } else if(json["janus"] === "hangup") {
697
+ // A plugin asked the core to hangup a PeerConnection on one of our handles
698
+ Janus.debug("Got a hangup event on session " + sessionId);
699
+ Janus.debug(json);
700
+ const sender = json["sender"];
701
+ if(!sender) {
702
+ Janus.warn("Missing sender...");
703
+ return;
704
+ }
705
+ const pluginHandle = pluginHandles[sender];
706
+ if(!pluginHandle) {
707
+ Janus.debug("This handle is not attached to this session");
708
+ return;
709
+ }
710
+ pluginHandle.webrtcState(false, json["reason"]);
711
+ pluginHandle.hangup();
712
+ } else if(json["janus"] === "detached") {
713
+ // A plugin asked the core to detach one of our handles
714
+ Janus.debug("Got a detached event on session " + sessionId);
715
+ Janus.debug(json);
716
+ const sender = json["sender"];
717
+ if(!sender) {
718
+ Janus.warn("Missing sender...");
719
+ return;
720
+ }
721
+ const pluginHandle = pluginHandles[sender];
722
+ if(!pluginHandle) {
723
+ // Don't warn here because destroyHandle causes this situation.
724
+ return;
725
+ }
726
+ pluginHandle.ondetached();
727
+ pluginHandle.detach();
728
+ } else if(json["janus"] === "media") {
729
+ // Media started/stopped flowing
730
+ Janus.debug("Got a media event on session " + sessionId);
731
+ Janus.debug(json);
732
+ const sender = json["sender"];
733
+ if(!sender) {
734
+ Janus.warn("Missing sender...");
735
+ return;
736
+ }
737
+ const pluginHandle = pluginHandles[sender];
738
+ if(!pluginHandle) {
739
+ Janus.debug("This handle is not attached to this session");
740
+ return;
741
+ }
742
+ pluginHandle.mediaState(json["type"], json["receiving"]);
743
+ } else if(json["janus"] === "slowlink") {
744
+ Janus.debug("Got a slowlink event on session " + sessionId);
745
+ Janus.debug(json);
746
+ // Trouble uplink or downlink
747
+ const sender = json["sender"];
748
+ if(!sender) {
749
+ Janus.warn("Missing sender...");
750
+ return;
751
+ }
752
+ const pluginHandle = pluginHandles[sender];
753
+ if(!pluginHandle) {
754
+ Janus.debug("This handle is not attached to this session");
755
+ return;
756
+ }
757
+ pluginHandle.slowLink(json["uplink"], json["lost"]);
758
+ } else if(json["janus"] === "error") {
759
+ // Oops, something wrong happened
760
+ Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
761
+ Janus.debug(json);
762
+ var transaction = json["transaction"];
763
+ if(transaction) {
764
+ var reportSuccess = transactions[transaction];
765
+ if(reportSuccess) {
766
+ reportSuccess(json);
767
+ }
768
+ delete transactions[transaction];
769
+ }
770
+ return;
771
+ } else if(json["janus"] === "event") {
772
+ Janus.debug("Got a plugin event on session " + sessionId);
773
+ Janus.debug(json);
774
+ const sender = json["sender"];
775
+ if(!sender) {
776
+ Janus.warn("Missing sender...");
777
+ return;
778
+ }
779
+ var plugindata = json["plugindata"];
780
+ if(!plugindata) {
781
+ Janus.warn("Missing plugindata...");
782
+ return;
783
+ }
784
+ Janus.debug(" -- Event is coming from " + sender + " (" + plugindata["plugin"] + ")");
785
+ var data = plugindata["data"];
786
+ Janus.debug(data);
787
+ const pluginHandle = pluginHandles[sender];
788
+ if(!pluginHandle) {
789
+ Janus.warn("This handle is not attached to this session");
790
+ return;
791
+ }
792
+ var jsep = json["jsep"];
793
+ if(jsep) {
794
+ Janus.debug("Handling SDP as well...");
795
+ Janus.debug(jsep);
796
+ }
797
+ var callback = pluginHandle.onmessage;
798
+ if(callback) {
799
+ Janus.debug("Notifying application...");
800
+ // Send to callback specified when attaching plugin handle
801
+ callback(data, jsep);
802
+ } else {
803
+ // Send to generic callback (?)
804
+ Janus.debug("No provided notification callback");
805
+ }
806
+ } else if(json["janus"] === "timeout") {
807
+ Janus.error("Timeout on session " + sessionId);
808
+ Janus.debug(json);
809
+ if (websockets) {
810
+ ws.close(3504, "Gateway timeout");
811
+ }
812
+ return;
813
+ } else {
814
+ Janus.warn("Unknown message/event '" + json["janus"] + "' on session " + sessionId);
815
+ Janus.debug(json);
816
+ }
817
+ }
818
+
819
+ // Private helper to send keep-alive messages on WebSockets
820
+ function keepAlive() {
821
+ if(!server || !websockets || !connected)
822
+ return;
823
+ wsKeepaliveTimeoutId = setTimeout(keepAlive, keepAlivePeriod);
824
+ var request = { "janus": "keepalive", "session_id": sessionId, "transaction": Janus.randomString(12) };
825
+ if(token)
826
+ request["token"] = token;
827
+ if(apisecret)
828
+ request["apisecret"] = apisecret;
829
+ ws.send(JSON.stringify(request));
830
+ }
831
+
832
+ // Private method to create a session
833
+ function createSession(callbacks) {
834
+ var transaction = Janus.randomString(12);
835
+ var request = { "janus": "create", "transaction": transaction };
836
+ if(callbacks["reconnect"]) {
837
+ // We're reconnecting, claim the session
838
+ connected = false;
839
+ request["janus"] = "claim";
840
+ request["session_id"] = sessionId;
841
+ // If we were using websockets, ignore the old connection
842
+ if(ws) {
843
+ ws.onopen = null;
844
+ ws.onerror = null;
845
+ ws.onclose = null;
846
+ if(wsKeepaliveTimeoutId) {
847
+ clearTimeout(wsKeepaliveTimeoutId);
848
+ wsKeepaliveTimeoutId = null;
849
+ }
850
+ }
851
+ }
852
+ if(token)
853
+ request["token"] = token;
854
+ if(apisecret)
855
+ request["apisecret"] = apisecret;
856
+ if(!server && Janus.isArray(servers)) {
857
+ // We still need to find a working server from the list we were given
858
+ server = servers[serversIndex];
859
+ if(server.indexOf("ws") === 0) {
860
+ websockets = true;
861
+ Janus.log("Server #" + (serversIndex+1) + ": trying WebSockets to contact Janus (" + server + ")");
862
+ } else {
863
+ websockets = false;
864
+ Janus.log("Server #" + (serversIndex+1) + ": trying REST API to contact Janus (" + server + ")");
865
+ }
866
+ }
867
+ if(websockets) {
868
+ ws = Janus.newWebSocket(server, 'janus-protocol');
869
+ wsHandlers = {
870
+ 'error': function() {
871
+ Janus.error("Error connecting to the Janus WebSockets server... " + server);
872
+ if (Janus.isArray(servers) && !callbacks["reconnect"]) {
873
+ serversIndex++;
874
+ if (serversIndex === servers.length) {
875
+ // We tried all the servers the user gave us and they all failed
876
+ callbacks.error("Error connecting to any of the provided Janus servers: Is the server down?");
877
+ return;
878
+ }
879
+ // Let's try the next server
880
+ server = null;
881
+ setTimeout(function() {
882
+ createSession(callbacks);
883
+ }, 200);
884
+ return;
885
+ }
886
+ callbacks.error("Error connecting to the Janus WebSockets server: Is the server down?");
887
+ },
888
+
889
+ 'open': function() {
890
+ // We need to be notified about the success
891
+ transactions[transaction] = function(json) {
892
+ Janus.debug(json);
893
+ if (json["janus"] !== "success") {
894
+ Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
895
+ callbacks.error(json["error"].reason);
896
+ return;
897
+ }
898
+ wsKeepaliveTimeoutId = setTimeout(keepAlive, keepAlivePeriod);
899
+ connected = true;
900
+ sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
901
+ if(callbacks["reconnect"]) {
902
+ Janus.log("Claimed session: " + sessionId);
903
+ } else {
904
+ Janus.log("Created session: " + sessionId);
905
+ }
906
+ Janus.sessions[sessionId] = that;
907
+ callbacks.success();
908
+ };
909
+ ws.send(JSON.stringify(request));
910
+ },
911
+
912
+ 'message': function(event) {
913
+ handleEvent(JSON.parse(event.data));
914
+ },
915
+
916
+ 'close': function() {
917
+ if (!server || !connected) {
918
+ return;
919
+ }
920
+ connected = false;
921
+ // FIXME What if this is called when the page is closed?
922
+ gatewayCallbacks.error("Lost connection to the server (is it down?)");
923
+ }
924
+ };
925
+
926
+ for(var eventName in wsHandlers) {
927
+ ws.addEventListener(eventName, wsHandlers[eventName]);
928
+ }
929
+
930
+ return;
931
+ }
932
+ Janus.httpAPICall(server, {
933
+ verb: 'POST',
934
+ withCredentials: withCredentials,
935
+ body: request,
936
+ success: function(json) {
937
+ Janus.debug(json);
938
+ if(json["janus"] !== "success") {
939
+ Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
940
+ callbacks.error(json["error"].reason);
941
+ return;
942
+ }
943
+ connected = true;
944
+ sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
945
+ if(callbacks["reconnect"]) {
946
+ Janus.log("Claimed session: " + sessionId);
947
+ } else {
948
+ Janus.log("Created session: " + sessionId);
949
+ }
950
+ Janus.sessions[sessionId] = that;
951
+ eventHandler();
952
+ callbacks.success();
953
+ },
954
+ error: function(textStatus, errorThrown) {
955
+ Janus.error(textStatus + ":", errorThrown); // FIXME
956
+ if(Janus.isArray(servers) && !callbacks["reconnect"]) {
957
+ serversIndex++;
958
+ if(serversIndex === servers.length) {
959
+ // We tried all the servers the user gave us and they all failed
960
+ callbacks.error("Error connecting to any of the provided Janus servers: Is the server down?");
961
+ return;
962
+ }
963
+ // Let's try the next server
964
+ server = null;
965
+ setTimeout(function() { createSession(callbacks); }, 200);
966
+ return;
967
+ }
968
+ if(errorThrown === "")
969
+ callbacks.error(textStatus + ": Is the server down?");
970
+ else if (errorThrown && errorThrown.error)
971
+ callbacks.error(textStatus + ": " + errorThrown.error.message);
972
+ else
973
+ callbacks.error(textStatus + ": " + errorThrown);
974
+ }
975
+ });
976
+ }
977
+
978
+ // Private method to get info on the server
979
+ function getInfo(callbacks) {
980
+ callbacks = callbacks || {};
981
+ // FIXME This method triggers a success even when we fail
982
+ callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
983
+ callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
984
+ Janus.log("Getting info on Janus instance");
985
+ if(!connected) {
986
+ Janus.warn("Is the server down? (connected=false)");
987
+ callbacks.error("Is the server down? (connected=false)");
988
+ return;
989
+ }
990
+ // We just need to send an "info" request
991
+ var transaction = Janus.randomString(12);
992
+ var request = { "janus": "info", "transaction": transaction };
993
+ if(token)
994
+ request["token"] = token;
995
+ if(apisecret)
996
+ request["apisecret"] = apisecret;
997
+ if(websockets) {
998
+ transactions[transaction] = function(json) {
999
+ Janus.log("Server info:");
1000
+ Janus.debug(json);
1001
+ if(json["janus"] !== "server_info") {
1002
+ Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
1003
+ }
1004
+ callbacks.success(json);
1005
+ }
1006
+ ws.send(JSON.stringify(request));
1007
+ return;
1008
+ }
1009
+ Janus.httpAPICall(server, {
1010
+ verb: 'POST',
1011
+ withCredentials: withCredentials,
1012
+ body: request,
1013
+ success: function(json) {
1014
+ Janus.log("Server info:");
1015
+ Janus.debug(json);
1016
+ if(json["janus"] !== "server_info") {
1017
+ Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
1018
+ }
1019
+ callbacks.success(json);
1020
+ },
1021
+ error: function(textStatus, errorThrown) {
1022
+ Janus.error(textStatus + ":", errorThrown); // FIXME
1023
+ if(errorThrown === "")
1024
+ callbacks.error(textStatus + ": Is the server down?");
1025
+ else
1026
+ callbacks.error(textStatus + ": " + errorThrown);
1027
+ }
1028
+ });
1029
+ }
1030
+
1031
+ // Private method to destroy a session
1032
+ function destroySession(callbacks) {
1033
+ callbacks = callbacks || {};
1034
+ // FIXME This method triggers a success even when we fail
1035
+ callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
1036
+ callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
1037
+ var unload = (callbacks.unload === true);
1038
+ var notifyDestroyed = true;
1039
+ if(callbacks.notifyDestroyed !== undefined && callbacks.notifyDestroyed !== null)
1040
+ notifyDestroyed = (callbacks.notifyDestroyed === true);
1041
+ var cleanupHandles = (callbacks.cleanupHandles === true);
1042
+ Janus.log("Destroying session " + sessionId + " (unload=" + unload + ")");
1043
+ if(!sessionId) {
1044
+ Janus.warn("No session to destroy");
1045
+ callbacks.success();
1046
+ if(notifyDestroyed)
1047
+ gatewayCallbacks.destroyed();
1048
+ return;
1049
+ }
1050
+ if(cleanupHandles) {
1051
+ for(var handleId in pluginHandles)
1052
+ destroyHandle(handleId, { noRequest: true });
1053
+ }
1054
+ if(!connected) {
1055
+ Janus.warn("Is the server down? (connected=false)");
1056
+ sessionId = null;
1057
+ callbacks.success();
1058
+ return;
1059
+ }
1060
+ // No need to destroy all handles first, Janus will do that itself
1061
+ var request = { "janus": "destroy", "transaction": Janus.randomString(12) };
1062
+ if(token)
1063
+ request["token"] = token;
1064
+ if(apisecret)
1065
+ request["apisecret"] = apisecret;
1066
+ if(unload) {
1067
+ // We're unloading the page: use sendBeacon for HTTP instead,
1068
+ // or just close the WebSocket connection if we're using that
1069
+ if(websockets) {
1070
+ ws.onclose = null;
1071
+ ws.close();
1072
+ ws = null;
1073
+ } else {
1074
+ navigator.sendBeacon(server + "/" + sessionId, JSON.stringify(request));
1075
+ }
1076
+ Janus.log("Destroyed session:");
1077
+ sessionId = null;
1078
+ connected = false;
1079
+ callbacks.success();
1080
+ if(notifyDestroyed)
1081
+ gatewayCallbacks.destroyed();
1082
+ return;
1083
+ }
1084
+ if(websockets) {
1085
+ request["session_id"] = sessionId;
1086
+
1087
+ var unbindWebSocket = function() {
1088
+ for(var eventName in wsHandlers) {
1089
+ ws.removeEventListener(eventName, wsHandlers[eventName]);
1090
+ }
1091
+ ws.removeEventListener('message', onUnbindMessage);
1092
+ ws.removeEventListener('error', onUnbindError);
1093
+ if(wsKeepaliveTimeoutId) {
1094
+ clearTimeout(wsKeepaliveTimeoutId);
1095
+ }
1096
+ ws.close();
1097
+ };
1098
+
1099
+ var onUnbindMessage = function(event){
1100
+ var data = JSON.parse(event.data);
1101
+ if(data.session_id == request.session_id && data.transaction == request.transaction) {
1102
+ unbindWebSocket();
1103
+ callbacks.success();
1104
+ if(notifyDestroyed)
1105
+ gatewayCallbacks.destroyed();
1106
+ }
1107
+ };
1108
+ var onUnbindError = function() {
1109
+ unbindWebSocket();
1110
+ callbacks.error("Failed to destroy the server: Is the server down?");
1111
+ if(notifyDestroyed)
1112
+ gatewayCallbacks.destroyed();
1113
+ };
1114
+
1115
+ ws.addEventListener('message', onUnbindMessage);
1116
+ ws.addEventListener('error', onUnbindError);
1117
+
1118
+ if (ws.readyState === 1) {
1119
+ ws.send(JSON.stringify(request));
1120
+ } else {
1121
+ onUnbindError();
1122
+ }
1123
+
1124
+ return;
1125
+ }
1126
+ Janus.httpAPICall(server + "/" + sessionId, {
1127
+ verb: 'POST',
1128
+ withCredentials: withCredentials,
1129
+ body: request,
1130
+ success: function(json) {
1131
+ Janus.log("Destroyed session:");
1132
+ Janus.debug(json);
1133
+ sessionId = null;
1134
+ connected = false;
1135
+ if(json["janus"] !== "success") {
1136
+ Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
1137
+ }
1138
+ callbacks.success();
1139
+ if(notifyDestroyed)
1140
+ gatewayCallbacks.destroyed();
1141
+ },
1142
+ error: function(textStatus, errorThrown) {
1143
+ Janus.error(textStatus + ":", errorThrown); // FIXME
1144
+ // Reset everything anyway
1145
+ sessionId = null;
1146
+ connected = false;
1147
+ callbacks.success();
1148
+ if(notifyDestroyed)
1149
+ gatewayCallbacks.destroyed();
1150
+ }
1151
+ });
1152
+ }
1153
+
1154
+ // Private method to create a plugin handle
1155
+ function createHandle(callbacks) {
1156
+ callbacks = callbacks || {};
1157
+ callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
1158
+ callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
1159
+ callbacks.dataChannelOptions = callbacks.dataChannelOptions || { ordered: true };
1160
+ callbacks.consentDialog = (typeof callbacks.consentDialog == "function") ? callbacks.consentDialog : Janus.noop;
1161
+ callbacks.iceState = (typeof callbacks.iceState == "function") ? callbacks.iceState : Janus.noop;
1162
+ callbacks.mediaState = (typeof callbacks.mediaState == "function") ? callbacks.mediaState : Janus.noop;
1163
+ callbacks.webrtcState = (typeof callbacks.webrtcState == "function") ? callbacks.webrtcState : Janus.noop;
1164
+ callbacks.slowLink = (typeof callbacks.slowLink == "function") ? callbacks.slowLink : Janus.noop;
1165
+ callbacks.onmessage = (typeof callbacks.onmessage == "function") ? callbacks.onmessage : Janus.noop;
1166
+ callbacks.onlocalstream = (typeof callbacks.onlocalstream == "function") ? callbacks.onlocalstream : Janus.noop;
1167
+ callbacks.onremotestream = (typeof callbacks.onremotestream == "function") ? callbacks.onremotestream : Janus.noop;
1168
+ callbacks.ondata = (typeof callbacks.ondata == "function") ? callbacks.ondata : Janus.noop;
1169
+ callbacks.ondataopen = (typeof callbacks.ondataopen == "function") ? callbacks.ondataopen : Janus.noop;
1170
+ callbacks.oncleanup = (typeof callbacks.oncleanup == "function") ? callbacks.oncleanup : Janus.noop;
1171
+ callbacks.ondetached = (typeof callbacks.ondetached == "function") ? callbacks.ondetached : Janus.noop;
1172
+ if(!connected) {
1173
+ Janus.warn("Is the server down? (connected=false)");
1174
+ callbacks.error("Is the server down? (connected=false)");
1175
+ return;
1176
+ }
1177
+ var plugin = callbacks.plugin;
1178
+ if(!plugin) {
1179
+ Janus.error("Invalid plugin");
1180
+ callbacks.error("Invalid plugin");
1181
+ return;
1182
+ }
1183
+ var opaqueId = callbacks.opaqueId;
1184
+ var loopIndex = callbacks.loopIndex;
1185
+ var handleToken = callbacks.token ? callbacks.token : token;
1186
+ var transaction = Janus.randomString(12);
1187
+ var request = { "janus": "attach", "plugin": plugin, "opaque_id": opaqueId, "loop_index": loopIndex, "transaction": transaction };
1188
+ if(handleToken)
1189
+ request["token"] = handleToken;
1190
+ if(apisecret)
1191
+ request["apisecret"] = apisecret;
1192
+ if(websockets) {
1193
+ transactions[transaction] = function(json) {
1194
+ Janus.debug(json);
1195
+ if(json["janus"] !== "success") {
1196
+ Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
1197
+ callbacks.error("Ooops: " + json["error"].code + " " + json["error"].reason);
1198
+ return;
1199
+ }
1200
+ var handleId = json.data["id"];
1201
+ Janus.log("Created handle: " + handleId);
1202
+ var pluginHandle =
1203
+ {
1204
+ session : that,
1205
+ plugin : plugin,
1206
+ id : handleId,
1207
+ token : handleToken,
1208
+ detached : false,
1209
+ webrtcStuff : {
1210
+ started : false,
1211
+ myStream : null,
1212
+ streamExternal : false,
1213
+ remoteStream : null,
1214
+ mySdp : null,
1215
+ mediaConstraints : null,
1216
+ pc : null,
1217
+ dataChannelOptions: callbacks.dataChannelOptions,
1218
+ dataChannel : {},
1219
+ dtmfSender : null,
1220
+ trickle : true,
1221
+ iceDone : false,
1222
+ volume : {
1223
+ value : null,
1224
+ timer : null
1225
+ },
1226
+ bitrate : {
1227
+ value : null,
1228
+ bsnow : null,
1229
+ bsbefore : null,
1230
+ tsnow : null,
1231
+ tsbefore : null,
1232
+ timer : null
1233
+ }
1234
+ },
1235
+ getId : function() { return handleId; },
1236
+ getPlugin : function() { return plugin; },
1237
+ getVolume : function() { return getVolume(handleId, true); },
1238
+ getRemoteVolume : function() { return getVolume(handleId, true); },
1239
+ getLocalVolume : function() { return getVolume(handleId, false); },
1240
+ isAudioMuted : function() { return isMuted(handleId, false); },
1241
+ muteAudio : function() { return mute(handleId, false, true); },
1242
+ unmuteAudio : function() { return mute(handleId, false, false); },
1243
+ isVideoMuted : function() { return isMuted(handleId, true); },
1244
+ muteVideo : function() { return mute(handleId, true, true); },
1245
+ unmuteVideo : function() { return mute(handleId, true, false); },
1246
+ getBitrate : function() { return getBitrate(handleId); },
1247
+ send : function(callbacks) { sendMessage(handleId, callbacks); },
1248
+ data : function(callbacks) { sendData(handleId, callbacks); },
1249
+ dtmf : function(callbacks) { sendDtmf(handleId, callbacks); },
1250
+ consentDialog : callbacks.consentDialog,
1251
+ iceState : callbacks.iceState,
1252
+ mediaState : callbacks.mediaState,
1253
+ webrtcState : callbacks.webrtcState,
1254
+ slowLink : callbacks.slowLink,
1255
+ onmessage : callbacks.onmessage,
1256
+ createOffer : function(callbacks) { prepareWebrtc(handleId, true, callbacks); },
1257
+ createAnswer : function(callbacks) { prepareWebrtc(handleId, false, callbacks); },
1258
+ handleRemoteJsep : function(callbacks) { prepareWebrtcPeer(handleId, callbacks); },
1259
+ onlocalstream : callbacks.onlocalstream,
1260
+ onremotestream : callbacks.onremotestream,
1261
+ ondata : callbacks.ondata,
1262
+ ondataopen : callbacks.ondataopen,
1263
+ oncleanup : callbacks.oncleanup,
1264
+ ondetached : callbacks.ondetached,
1265
+ hangup : function(sendRequest) { cleanupWebrtc(handleId, sendRequest === true); },
1266
+ detach : function(callbacks) { destroyHandle(handleId, callbacks); }
1267
+ };
1268
+ pluginHandles[handleId] = pluginHandle;
1269
+ callbacks.success(pluginHandle);
1270
+ };
1271
+ request["session_id"] = sessionId;
1272
+ ws.send(JSON.stringify(request));
1273
+ return;
1274
+ }
1275
+ Janus.httpAPICall(server + "/" + sessionId, {
1276
+ verb: 'POST',
1277
+ withCredentials: withCredentials,
1278
+ body: request,
1279
+ success: function(json) {
1280
+ Janus.debug(json);
1281
+ if(json["janus"] !== "success") {
1282
+ Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
1283
+ callbacks.error("Ooops: " + json["error"].code + " " + json["error"].reason);
1284
+ return;
1285
+ }
1286
+ var handleId = json.data["id"];
1287
+ Janus.log("Created handle: " + handleId);
1288
+ var pluginHandle =
1289
+ {
1290
+ session : that,
1291
+ plugin : plugin,
1292
+ id : handleId,
1293
+ token : handleToken,
1294
+ detached : false,
1295
+ webrtcStuff : {
1296
+ started : false,
1297
+ myStream : null,
1298
+ streamExternal : false,
1299
+ remoteStream : null,
1300
+ mySdp : null,
1301
+ mediaConstraints : null,
1302
+ pc : null,
1303
+ dataChannelOptions: callbacks.dataChannelOptions,
1304
+ dataChannel : {},
1305
+ dtmfSender : null,
1306
+ trickle : true,
1307
+ iceDone : false,
1308
+ volume : {
1309
+ value : null,
1310
+ timer : null
1311
+ },
1312
+ bitrate : {
1313
+ value : null,
1314
+ bsnow : null,
1315
+ bsbefore : null,
1316
+ tsnow : null,
1317
+ tsbefore : null,
1318
+ timer : null
1319
+ }
1320
+ },
1321
+ getId : function() { return handleId; },
1322
+ getPlugin : function() { return plugin; },
1323
+ getVolume : function() { return getVolume(handleId, true); },
1324
+ getRemoteVolume : function() { return getVolume(handleId, true); },
1325
+ getLocalVolume : function() { return getVolume(handleId, false); },
1326
+ isAudioMuted : function() { return isMuted(handleId, false); },
1327
+ muteAudio : function() { return mute(handleId, false, true); },
1328
+ unmuteAudio : function() { return mute(handleId, false, false); },
1329
+ isVideoMuted : function() { return isMuted(handleId, true); },
1330
+ muteVideo : function() { return mute(handleId, true, true); },
1331
+ unmuteVideo : function() { return mute(handleId, true, false); },
1332
+ getBitrate : function() { return getBitrate(handleId); },
1333
+ send : function(callbacks) { sendMessage(handleId, callbacks); },
1334
+ data : function(callbacks) { sendData(handleId, callbacks); },
1335
+ dtmf : function(callbacks) { sendDtmf(handleId, callbacks); },
1336
+ consentDialog : callbacks.consentDialog,
1337
+ iceState : callbacks.iceState,
1338
+ mediaState : callbacks.mediaState,
1339
+ webrtcState : callbacks.webrtcState,
1340
+ slowLink : callbacks.slowLink,
1341
+ onmessage : callbacks.onmessage,
1342
+ createOffer : function(callbacks) { prepareWebrtc(handleId, true, callbacks); },
1343
+ createAnswer : function(callbacks) { prepareWebrtc(handleId, false, callbacks); },
1344
+ handleRemoteJsep : function(callbacks) { prepareWebrtcPeer(handleId, callbacks); },
1345
+ onlocalstream : callbacks.onlocalstream,
1346
+ onremotestream : callbacks.onremotestream,
1347
+ ondata : callbacks.ondata,
1348
+ ondataopen : callbacks.ondataopen,
1349
+ oncleanup : callbacks.oncleanup,
1350
+ ondetached : callbacks.ondetached,
1351
+ hangup : function(sendRequest) { cleanupWebrtc(handleId, sendRequest === true); },
1352
+ detach : function(callbacks) { destroyHandle(handleId, callbacks); }
1353
+ }
1354
+ pluginHandles[handleId] = pluginHandle;
1355
+ callbacks.success(pluginHandle);
1356
+ },
1357
+ error: function(textStatus, errorThrown) {
1358
+ Janus.error(textStatus + ":", errorThrown); // FIXME
1359
+ if(errorThrown === "")
1360
+ callbacks.error(textStatus + ": Is the server down?");
1361
+ else
1362
+ callbacks.error(textStatus + ": " + errorThrown);
1363
+ }
1364
+ });
1365
+ }
1366
+
1367
+ // Private method to send a message
1368
+ function sendMessage(handleId, callbacks) {
1369
+ callbacks = callbacks || {};
1370
+ callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
1371
+ callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
1372
+ if(!connected) {
1373
+ Janus.warn("Is the server down? (connected=false)");
1374
+ callbacks.error("Is the server down? (connected=false)");
1375
+ return;
1376
+ }
1377
+ var pluginHandle = pluginHandles[handleId];
1378
+ if(!pluginHandle || !pluginHandle.webrtcStuff) {
1379
+ Janus.warn("Invalid handle");
1380
+ callbacks.error("Invalid handle");
1381
+ return;
1382
+ }
1383
+ var message = callbacks.message;
1384
+ var jsep = callbacks.jsep;
1385
+ var transaction = Janus.randomString(12);
1386
+ var request = { "janus": "message", "body": message, "transaction": transaction };
1387
+ if(pluginHandle.token)
1388
+ request["token"] = pluginHandle.token;
1389
+ if(apisecret)
1390
+ request["apisecret"] = apisecret;
1391
+ if(jsep) {
1392
+ request.jsep = {
1393
+ type: jsep.type,
1394
+ sdp: jsep.sdp
1395
+ };
1396
+ if(jsep.e2ee)
1397
+ request.jsep.e2ee = true;
1398
+ if(jsep.rid_order === "hml" || jsep.rid_order === "lmh")
1399
+ request.jsep.rid_order = jsep.rid_order;
1400
+ if(jsep.force_relay)
1401
+ request.jsep.force_relay = true;
1402
+ }
1403
+ Janus.debug("Sending message to plugin (handle=" + handleId + "):");
1404
+ Janus.debug(request);
1405
+ if(websockets) {
1406
+ request["session_id"] = sessionId;
1407
+ request["handle_id"] = handleId;
1408
+ transactions[transaction] = function(json) {
1409
+ Janus.debug("Message sent!");
1410
+ Janus.debug(json);
1411
+ if(json["janus"] === "success") {
1412
+ // We got a success, must have been a synchronous transaction
1413
+ var plugindata = json["plugindata"];
1414
+ if(!plugindata) {
1415
+ Janus.warn("Request succeeded, but missing plugindata...");
1416
+ callbacks.success();
1417
+ return;
1418
+ }
1419
+ Janus.log("Synchronous transaction successful (" + plugindata["plugin"] + ")");
1420
+ var data = plugindata["data"];
1421
+ Janus.debug(data);
1422
+ callbacks.success(data);
1423
+ return;
1424
+ } else if(json["janus"] !== "ack") {
1425
+ // Not a success and not an ack, must be an error
1426
+ if(json["error"]) {
1427
+ Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
1428
+ callbacks.error(json["error"].code + " " + json["error"].reason);
1429
+ } else {
1430
+ Janus.error("Unknown error"); // FIXME
1431
+ callbacks.error("Unknown error");
1432
+ }
1433
+ return;
1434
+ }
1435
+ // If we got here, the plugin decided to handle the request asynchronously
1436
+ callbacks.success();
1437
+ };
1438
+ ws.send(JSON.stringify(request));
1439
+ return;
1440
+ }
1441
+ Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, {
1442
+ verb: 'POST',
1443
+ withCredentials: withCredentials,
1444
+ body: request,
1445
+ success: function(json) {
1446
+ Janus.debug("Message sent!");
1447
+ Janus.debug(json);
1448
+ if(json["janus"] === "success") {
1449
+ // We got a success, must have been a synchronous transaction
1450
+ var plugindata = json["plugindata"];
1451
+ if(!plugindata) {
1452
+ Janus.warn("Request succeeded, but missing plugindata...");
1453
+ callbacks.success();
1454
+ return;
1455
+ }
1456
+ Janus.log("Synchronous transaction successful (" + plugindata["plugin"] + ")");
1457
+ var data = plugindata["data"];
1458
+ Janus.debug(data);
1459
+ callbacks.success(data);
1460
+ return;
1461
+ } else if(json["janus"] !== "ack") {
1462
+ // Not a success and not an ack, must be an error
1463
+ if(json["error"]) {
1464
+ Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
1465
+ callbacks.error(json["error"].code + " " + json["error"].reason);
1466
+ } else {
1467
+ Janus.error("Unknown error"); // FIXME
1468
+ callbacks.error("Unknown error");
1469
+ }
1470
+ return;
1471
+ }
1472
+ // If we got here, the plugin decided to handle the request asynchronously
1473
+ callbacks.success();
1474
+ },
1475
+ error: function(textStatus, errorThrown) {
1476
+ Janus.error(textStatus + ":", errorThrown); // FIXME
1477
+ callbacks.error(textStatus + ": " + errorThrown);
1478
+ }
1479
+ });
1480
+ }
1481
+
1482
+ // Private method to send a trickle candidate
1483
+ function sendTrickleCandidate(handleId, candidate) {
1484
+ if(!connected) {
1485
+ Janus.warn("Is the server down? (connected=false)");
1486
+ return;
1487
+ }
1488
+ var pluginHandle = pluginHandles[handleId];
1489
+ if(!pluginHandle || !pluginHandle.webrtcStuff) {
1490
+ Janus.warn("Invalid handle");
1491
+ return;
1492
+ }
1493
+ var request = { "janus": "trickle", "candidate": candidate, "transaction": Janus.randomString(12) };
1494
+ if(pluginHandle.token)
1495
+ request["token"] = pluginHandle.token;
1496
+ if(apisecret)
1497
+ request["apisecret"] = apisecret;
1498
+ Janus.vdebug("Sending trickle candidate (handle=" + handleId + "):");
1499
+ Janus.vdebug(request);
1500
+ if(websockets) {
1501
+ request["session_id"] = sessionId;
1502
+ request["handle_id"] = handleId;
1503
+ ws.send(JSON.stringify(request));
1504
+ return;
1505
+ }
1506
+ Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, {
1507
+ verb: 'POST',
1508
+ withCredentials: withCredentials,
1509
+ body: request,
1510
+ success: function(json) {
1511
+ Janus.vdebug("Candidate sent!");
1512
+ Janus.vdebug(json);
1513
+ if(json["janus"] !== "ack") {
1514
+ Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
1515
+ return;
1516
+ }
1517
+ },
1518
+ error: function(textStatus, errorThrown) {
1519
+ Janus.error(textStatus + ":", errorThrown); // FIXME
1520
+ }
1521
+ });
1522
+ }
1523
+
1524
+ // Private method to create a data channel
1525
+ function createDataChannel(handleId, dclabel, dcprotocol, incoming, pendingData) {
1526
+ var pluginHandle = pluginHandles[handleId];
1527
+ if(!pluginHandle || !pluginHandle.webrtcStuff) {
1528
+ Janus.warn("Invalid handle");
1529
+ return;
1530
+ }
1531
+ var config = pluginHandle.webrtcStuff;
1532
+ if(!config.pc) {
1533
+ Janus.warn("Invalid PeerConnection");
1534
+ return;
1535
+ }
1536
+ var onDataChannelMessage = function(event) {
1537
+ Janus.log('Received message on data channel:', event);
1538
+ var label = event.target.label;
1539
+ pluginHandle.ondata(event.data, label);
1540
+ };
1541
+ var onDataChannelStateChange = function(event) {
1542
+ Janus.log('Received state change on data channel:', event);
1543
+ var label = event.target.label;
1544
+ var protocol = event.target.protocol;
1545
+ var dcState = config.dataChannel[label] ? config.dataChannel[label].readyState : "null";
1546
+ Janus.log('State change on <' + label + '> data channel: ' + dcState);
1547
+ if(dcState === 'open') {
1548
+ // Any pending messages to send?
1549
+ if(config.dataChannel[label].pending && config.dataChannel[label].pending.length > 0) {
1550
+ Janus.log("Sending pending messages on <" + label + ">:", config.dataChannel[label].pending.length);
1551
+ for(var data of config.dataChannel[label].pending) {
1552
+ Janus.log("Sending data on data channel <" + label + ">");
1553
+ Janus.debug(data);
1554
+ config.dataChannel[label].send(data);
1555
+ }
1556
+ config.dataChannel[label].pending = [];
1557
+ }
1558
+ // Notify the open data channel
1559
+ pluginHandle.ondataopen(label, protocol);
1560
+ }
1561
+ };
1562
+ var onDataChannelError = function(error) {
1563
+ Janus.error('Got error on data channel:', error);
1564
+ // TODO
1565
+ };
1566
+ if(!incoming) {
1567
+ // FIXME Add options (ordered, maxRetransmits, etc.)
1568
+ var dcoptions = config.dataChannelOptions;
1569
+ if(dcprotocol)
1570
+ dcoptions.protocol = dcprotocol;
1571
+ config.dataChannel[dclabel] = config.pc.createDataChannel(dclabel, dcoptions);
1572
+ } else {
1573
+ // The channel was created by Janus
1574
+ config.dataChannel[dclabel] = incoming;
1575
+ }
1576
+ config.dataChannel[dclabel].onmessage = onDataChannelMessage;
1577
+ config.dataChannel[dclabel].onopen = onDataChannelStateChange;
1578
+ config.dataChannel[dclabel].onclose = onDataChannelStateChange;
1579
+ config.dataChannel[dclabel].onerror = onDataChannelError;
1580
+ config.dataChannel[dclabel].pending = [];
1581
+ if(pendingData)
1582
+ config.dataChannel[dclabel].pending.push(pendingData);
1583
+ }
1584
+
1585
+ // Private method to send a data channel message
1586
+ function sendData(handleId, callbacks) {
1587
+ callbacks = callbacks || {};
1588
+ callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
1589
+ callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
1590
+ var pluginHandle = pluginHandles[handleId];
1591
+ if(!pluginHandle || !pluginHandle.webrtcStuff) {
1592
+ Janus.warn("Invalid handle");
1593
+ callbacks.error("Invalid handle");
1594
+ return;
1595
+ }
1596
+ var config = pluginHandle.webrtcStuff;
1597
+ var data = callbacks.text || callbacks.data;
1598
+ if(!data) {
1599
+ Janus.warn("Invalid data");
1600
+ callbacks.error("Invalid data");
1601
+ return;
1602
+ }
1603
+ var label = callbacks.label ? callbacks.label : Janus.dataChanDefaultLabel;
1604
+ if(!config.dataChannel[label]) {
1605
+ // Create new data channel and wait for it to open
1606
+ createDataChannel(handleId, label, callbacks.protocol, false, data, callbacks.protocol);
1607
+ callbacks.success();
1608
+ return;
1609
+ }
1610
+ if(config.dataChannel[label].readyState !== "open") {
1611
+ config.dataChannel[label].pending.push(data);
1612
+ callbacks.success();
1613
+ return;
1614
+ }
1615
+ Janus.log("Sending data on data channel <" + label + ">");
1616
+ Janus.debug(data);
1617
+ config.dataChannel[label].send(data);
1618
+ callbacks.success();
1619
+ }
1620
+
1621
+ // Private method to send a DTMF tone
1622
+ function sendDtmf(handleId, callbacks) {
1623
+ callbacks = callbacks || {};
1624
+ callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
1625
+ callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
1626
+ var pluginHandle = pluginHandles[handleId];
1627
+ if(!pluginHandle || !pluginHandle.webrtcStuff) {
1628
+ Janus.warn("Invalid handle");
1629
+ callbacks.error("Invalid handle");
1630
+ return;
1631
+ }
1632
+ var config = pluginHandle.webrtcStuff;
1633
+ if(!config.dtmfSender) {
1634
+ // Create the DTMF sender the proper way, if possible
1635
+ if(config.pc) {
1636
+ var senders = config.pc.getSenders();
1637
+ var audioSender = senders.find(function(sender) {
1638
+ return sender.track && sender.track.kind === 'audio';
1639
+ });
1640
+ if(!audioSender) {
1641
+ Janus.warn("Invalid DTMF configuration (no audio track)");
1642
+ callbacks.error("Invalid DTMF configuration (no audio track)");
1643
+ return;
1644
+ }
1645
+ config.dtmfSender = audioSender.dtmf;
1646
+ if(config.dtmfSender) {
1647
+ Janus.log("Created DTMF Sender");
1648
+ config.dtmfSender.ontonechange = function(tone) { Janus.debug("Sent DTMF tone: " + tone.tone); };
1649
+ }
1650
+ }
1651
+ if(!config.dtmfSender) {
1652
+ Janus.warn("Invalid DTMF configuration");
1653
+ callbacks.error("Invalid DTMF configuration");
1654
+ return;
1655
+ }
1656
+ }
1657
+ var dtmf = callbacks.dtmf;
1658
+ if(!dtmf) {
1659
+ Janus.warn("Invalid DTMF parameters");
1660
+ callbacks.error("Invalid DTMF parameters");
1661
+ return;
1662
+ }
1663
+ var tones = dtmf.tones;
1664
+ if(!tones) {
1665
+ Janus.warn("Invalid DTMF string");
1666
+ callbacks.error("Invalid DTMF string");
1667
+ return;
1668
+ }
1669
+ var duration = (typeof dtmf.duration === 'number') ? dtmf.duration : 500; // We choose 500ms as the default duration for a tone
1670
+ var gap = (typeof dtmf.gap === 'number') ? dtmf.gap : 50; // We choose 50ms as the default gap between tones
1671
+ Janus.debug("Sending DTMF string " + tones + " (duration " + duration + "ms, gap " + gap + "ms)");
1672
+ config.dtmfSender.insertDTMF(tones, duration, gap);
1673
+ callbacks.success();
1674
+ }
1675
+
1676
+ // Private method to destroy a plugin handle
1677
+ function destroyHandle(handleId, callbacks) {
1678
+ callbacks = callbacks || {};
1679
+ callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
1680
+ callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
1681
+ var noRequest = (callbacks.noRequest === true);
1682
+ Janus.log("Destroying handle " + handleId + " (only-locally=" + noRequest + ")");
1683
+ cleanupWebrtc(handleId);
1684
+ var pluginHandle = pluginHandles[handleId];
1685
+ if(!pluginHandle || pluginHandle.detached) {
1686
+ // Plugin was already detached by Janus, calling detach again will return a handle not found error, so just exit here
1687
+ delete pluginHandles[handleId];
1688
+ callbacks.success();
1689
+ return;
1690
+ }
1691
+ pluginHandle.detached = true;
1692
+ if(noRequest) {
1693
+ // We're only removing the handle locally
1694
+ delete pluginHandles[handleId];
1695
+ callbacks.success();
1696
+ return;
1697
+ }
1698
+ if(!connected) {
1699
+ Janus.warn("Is the server down? (connected=false)");
1700
+ callbacks.error("Is the server down? (connected=false)");
1701
+ return;
1702
+ }
1703
+ var request = { "janus": "detach", "transaction": Janus.randomString(12) };
1704
+ if(pluginHandle.token)
1705
+ request["token"] = pluginHandle.token;
1706
+ if(apisecret)
1707
+ request["apisecret"] = apisecret;
1708
+ if(websockets) {
1709
+ request["session_id"] = sessionId;
1710
+ request["handle_id"] = handleId;
1711
+ ws.send(JSON.stringify(request));
1712
+ delete pluginHandles[handleId];
1713
+ callbacks.success();
1714
+ return;
1715
+ }
1716
+ Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, {
1717
+ verb: 'POST',
1718
+ withCredentials: withCredentials,
1719
+ body: request,
1720
+ success: function(json) {
1721
+ Janus.log("Destroyed handle:");
1722
+ Janus.debug(json);
1723
+ if(json["janus"] !== "success") {
1724
+ Janus.error("Ooops: " + json["error"].code + " " + json["error"].reason); // FIXME
1725
+ }
1726
+ delete pluginHandles[handleId];
1727
+ callbacks.success();
1728
+ },
1729
+ error: function(textStatus, errorThrown) {
1730
+ Janus.error(textStatus + ":", errorThrown); // FIXME
1731
+ // We cleanup anyway
1732
+ delete pluginHandles[handleId];
1733
+ callbacks.success();
1734
+ }
1735
+ });
1736
+ }
1737
+
1738
+ // WebRTC stuff
1739
+ function streamsDone(handleId, jsep, media, callbacks, stream) {
1740
+ var pluginHandle = pluginHandles[handleId];
1741
+ if(!pluginHandle || !pluginHandle.webrtcStuff) {
1742
+ Janus.warn("Invalid handle");
1743
+ // Close all tracks if the given stream has been created internally
1744
+ if(!callbacks.stream) {
1745
+ Janus.stopAllTracks(stream);
1746
+ }
1747
+ callbacks.error("Invalid handle");
1748
+ return;
1749
+ }
1750
+ var config = pluginHandle.webrtcStuff;
1751
+ Janus.debug("streamsDone:", stream);
1752
+ if(stream) {
1753
+ Janus.debug(" -- Audio tracks:", stream.getAudioTracks());
1754
+ Janus.debug(" -- Video tracks:", stream.getVideoTracks());
1755
+ }
1756
+ // We're now capturing the new stream: check if we're updating or if it's a new thing
1757
+ var addTracks = false;
1758
+ if(!config.myStream || !media.update || (config.streamExternal && !media.replaceAudio && !media.replaceVideo)) {
1759
+ config.myStream = stream;
1760
+ addTracks = true;
1761
+ } else {
1762
+ // We only need to update the existing stream
1763
+ if(((!media.update && isAudioSendEnabled(media)) || (media.update && (media.addAudio || media.replaceAudio))) &&
1764
+ stream.getAudioTracks() && stream.getAudioTracks().length) {
1765
+ config.myStream.addTrack(stream.getAudioTracks()[0]);
1766
+ if(Janus.unifiedPlan) {
1767
+ // Use Transceivers
1768
+ Janus.log((media.replaceAudio ? "Replacing" : "Adding") + " audio track:", stream.getAudioTracks()[0]);
1769
+ var audioTransceiver = null;
1770
+ const transceivers = config.pc.getTransceivers();
1771
+ if(transceivers && transceivers.length > 0) {
1772
+ for(const t of transceivers) {
1773
+ if((t.sender && t.sender.track && t.sender.track.kind === "audio") ||
1774
+ (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) {
1775
+ audioTransceiver = t;
1776
+ break;
1777
+ }
1778
+ }
1779
+ }
1780
+ if(audioTransceiver && audioTransceiver.sender) {
1781
+ audioTransceiver.sender.replaceTrack(stream.getAudioTracks()[0]);
1782
+ } else {
1783
+ config.pc.addTrack(stream.getAudioTracks()[0], stream);
1784
+ }
1785
+ } else {
1786
+ Janus.log((media.replaceAudio ? "Replacing" : "Adding") + " audio track:", stream.getAudioTracks()[0]);
1787
+ config.pc.addTrack(stream.getAudioTracks()[0], stream);
1788
+ }
1789
+ }
1790
+ if(((!media.update && isVideoSendEnabled(media)) || (media.update && (media.addVideo || media.replaceVideo))) &&
1791
+ stream.getVideoTracks() && stream.getVideoTracks().length) {
1792
+ config.myStream.addTrack(stream.getVideoTracks()[0]);
1793
+ if(Janus.unifiedPlan) {
1794
+ // Use Transceivers
1795
+ Janus.log((media.replaceVideo ? "Replacing" : "Adding") + " video track:", stream.getVideoTracks()[0]);
1796
+ var videoTransceiver = null;
1797
+ const transceivers = config.pc.getTransceivers();
1798
+ if(transceivers && transceivers.length > 0) {
1799
+ for(const t of transceivers) {
1800
+ if((t.sender && t.sender.track && t.sender.track.kind === "video") ||
1801
+ (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) {
1802
+ videoTransceiver = t;
1803
+ break;
1804
+ }
1805
+ }
1806
+ }
1807
+ if(videoTransceiver && videoTransceiver.sender) {
1808
+ videoTransceiver.sender.replaceTrack(stream.getVideoTracks()[0]);
1809
+ } else {
1810
+ config.pc.addTrack(stream.getVideoTracks()[0], stream);
1811
+ }
1812
+ } else {
1813
+ Janus.log((media.replaceVideo ? "Replacing" : "Adding") + " video track:", stream.getVideoTracks()[0]);
1814
+ config.pc.addTrack(stream.getVideoTracks()[0], stream);
1815
+ }
1816
+ }
1817
+ }
1818
+ // If we still need to create a PeerConnection, let's do that
1819
+ if(!config.pc) {
1820
+ var pc_config = {"iceServers": iceServers, "iceTransportPolicy": iceTransportPolicy, "bundlePolicy": bundlePolicy};
1821
+ if(Janus.webRTCAdapter.browserDetails.browser === "chrome") {
1822
+ // For Chrome versions before 72, we force a plan-b semantic, and unified-plan otherwise
1823
+ pc_config["sdpSemantics"] = (Janus.webRTCAdapter.browserDetails.version < 72) ? "plan-b" : "unified-plan";
1824
+ }
1825
+ var pc_constraints = {
1826
+ "optional": [{"DtlsSrtpKeyAgreement": true}]
1827
+ };
1828
+ if(ipv6Support) {
1829
+ pc_constraints.optional.push({"googIPv6":true});
1830
+ }
1831
+ // Any custom constraint to add?
1832
+ if(callbacks.rtcConstraints && typeof callbacks.rtcConstraints === 'object') {
1833
+ Janus.debug("Adding custom PeerConnection constraints:", callbacks.rtcConstraints);
1834
+ for(var i in callbacks.rtcConstraints) {
1835
+ pc_constraints.optional.push(callbacks.rtcConstraints[i]);
1836
+ }
1837
+ }
1838
+ if(Janus.webRTCAdapter.browserDetails.browser === "edge") {
1839
+ // This is Edge, enable BUNDLE explicitly
1840
+ pc_config.bundlePolicy = "max-bundle";
1841
+ }
1842
+ // Check if a sender or receiver transform has been provided
1843
+ if(RTCRtpSender && (RTCRtpSender.prototype.createEncodedStreams ||
1844
+ (RTCRtpSender.prototype.createEncodedAudioStreams &&
1845
+ RTCRtpSender.prototype.createEncodedVideoStreams)) &&
1846
+ (callbacks.senderTransforms || callbacks.receiverTransforms)) {
1847
+ config.senderTransforms = callbacks.senderTransforms;
1848
+ config.receiverTransforms = callbacks.receiverTransforms;
1849
+ pc_config["forceEncodedAudioInsertableStreams"] = true;
1850
+ pc_config["forceEncodedVideoInsertableStreams"] = true;
1851
+ pc_config["encodedInsertableStreams"] = true;
1852
+ }
1853
+ Janus.log("Creating PeerConnection");
1854
+ Janus.debug(pc_constraints);
1855
+ config.pc = new RTCPeerConnection(pc_config, pc_constraints);
1856
+ Janus.debug(config.pc);
1857
+ if(config.pc.getStats) { // FIXME
1858
+ config.volume = {};
1859
+ config.bitrate.value = "0 kbits/sec";
1860
+ }
1861
+ Janus.log("Preparing local SDP and gathering candidates (trickle=" + config.trickle + ")");
1862
+ config.pc.oniceconnectionstatechange = function() {
1863
+ if(config.pc)
1864
+ pluginHandle.iceState(config.pc.iceConnectionState);
1865
+ };
1866
+ config.pc.onicecandidate = function(event) {
1867
+ if (!event.candidate ||
1868
+ (Janus.webRTCAdapter.browserDetails.browser === 'edge' && event.candidate.candidate.indexOf('endOfCandidates') > 0)) {
1869
+ Janus.log("End of candidates.");
1870
+ config.iceDone = true;
1871
+ if(config.trickle === true) {
1872
+ // Notify end of candidates
1873
+ sendTrickleCandidate(handleId, {"completed": true});
1874
+ } else {
1875
+ // No trickle, time to send the complete SDP (including all candidates)
1876
+ sendSDP(handleId, callbacks);
1877
+ }
1878
+ } else {
1879
+ // JSON.stringify doesn't work on some WebRTC objects anymore
1880
+ // See https://code.google.com/p/chromium/issues/detail?id=467366
1881
+ var candidate = {
1882
+ "candidate": event.candidate.candidate,
1883
+ "sdpMid": event.candidate.sdpMid,
1884
+ "sdpMLineIndex": event.candidate.sdpMLineIndex
1885
+ };
1886
+ if(config.trickle === true) {
1887
+ // Send candidate
1888
+ sendTrickleCandidate(handleId, candidate);
1889
+ }
1890
+ }
1891
+ };
1892
+ config.pc.ontrack = function(event) {
1893
+ Janus.log("Handling Remote Track");
1894
+ Janus.debug(event);
1895
+ if(!event.streams)
1896
+ return;
1897
+ config.remoteStream = event.streams[0];
1898
+ pluginHandle.onremotestream(config.remoteStream);
1899
+ if(event.track.onended)
1900
+ return;
1901
+ if(config.receiverTransforms) {
1902
+ var receiverStreams = null;
1903
+ if(RTCRtpSender.prototype.createEncodedStreams) {
1904
+ receiverStreams = event.receiver.createEncodedStreams();
1905
+ } else if(RTCRtpSender.prototype.createAudioEncodedStreams || RTCRtpSender.prototype.createEncodedVideoStreams) {
1906
+ if(event.track.kind === "audio" && config.receiverTransforms["audio"]) {
1907
+ receiverStreams = event.receiver.createEncodedAudioStreams();
1908
+ } else if(event.track.kind === "video" && config.receiverTransforms["video"]) {
1909
+ receiverStreams = event.receiver.createEncodedVideoStreams();
1910
+ }
1911
+ }
1912
+ if(receiverStreams) {
1913
+ console.log(receiverStreams);
1914
+ if(receiverStreams.readableStream && receiverStreams.writableStream) {
1915
+ receiverStreams.readableStream
1916
+ .pipeThrough(config.receiverTransforms[event.track.kind])
1917
+ .pipeTo(receiverStreams.writableStream);
1918
+ } else if(receiverStreams.readable && receiverStreams.writable) {
1919
+ receiverStreams.readable
1920
+ .pipeThrough(config.receiverTransforms[event.track.kind])
1921
+ .pipeTo(receiverStreams.writable);
1922
+ }
1923
+ }
1924
+ }
1925
+ var trackMutedTimeoutId = null;
1926
+ Janus.log("Adding onended callback to track:", event.track);
1927
+ event.track.onended = function(ev) {
1928
+ Janus.log("Remote track removed:", ev);
1929
+ if(config.remoteStream) {
1930
+ clearTimeout(trackMutedTimeoutId);
1931
+ config.remoteStream.removeTrack(ev.target);
1932
+ pluginHandle.onremotestream(config.remoteStream);
1933
+ }
1934
+ };
1935
+ event.track.onmute = function(ev) {
1936
+ Janus.log("Remote track muted:", ev);
1937
+ if(config.remoteStream && trackMutedTimeoutId == null) {
1938
+ trackMutedTimeoutId = setTimeout(function() {
1939
+ Janus.log("Removing remote track");
1940
+ if (config.remoteStream) {
1941
+ config.remoteStream.removeTrack(ev.target);
1942
+ pluginHandle.onremotestream(config.remoteStream);
1943
+ }
1944
+ trackMutedTimeoutId = null;
1945
+ // Chrome seems to raise mute events only at multiples of 834ms;
1946
+ // we set the timeout to three times this value (rounded to 840ms)
1947
+ }, 3 * 840);
1948
+ }
1949
+ };
1950
+ event.track.onunmute = function(ev) {
1951
+ Janus.log("Remote track flowing again:", ev);
1952
+ if(trackMutedTimeoutId != null) {
1953
+ clearTimeout(trackMutedTimeoutId);
1954
+ trackMutedTimeoutId = null;
1955
+ } else {
1956
+ try {
1957
+ config.remoteStream.addTrack(ev.target);
1958
+ pluginHandle.onremotestream(config.remoteStream);
1959
+ } catch(e) {
1960
+ Janus.error(e);
1961
+ }
1962
+ }
1963
+ };
1964
+ };
1965
+ }
1966
+ if(addTracks && stream) {
1967
+ Janus.log('Adding local stream');
1968
+ var simulcast = (callbacks.simulcast === true || callbacks.simulcast2 === true) && Janus.unifiedPlan;
1969
+ var svc = callbacks.svc;
1970
+ stream.getTracks().forEach(function(track) {
1971
+ Janus.log('Adding local track:', track);
1972
+ var sender = null;
1973
+ if((!simulcast && !svc) || track.kind === 'audio') {
1974
+ sender = config.pc.addTrack(track, stream);
1975
+ } else if(simulcast) {
1976
+ Janus.log('Enabling rid-based simulcasting:', track);
1977
+ let maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates);
1978
+ let tr = config.pc.addTransceiver(track, {
1979
+ direction: "sendrecv",
1980
+ streams: [stream],
1981
+ sendEncodings: callbacks.sendEncodings || [
1982
+ { rid: "h", active: true, maxBitrate: maxBitrates.high },
1983
+ { rid: "m", active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2 },
1984
+ { rid: "l", active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4 }
1985
+ ]
1986
+ });
1987
+ if(tr)
1988
+ sender = tr.sender;
1989
+ } else {
1990
+ Janus.log('Enabling SVC (' + svc + '):', track);
1991
+ let tr = config.pc.addTransceiver(track, {
1992
+ direction: "sendrecv",
1993
+ streams: [stream],
1994
+ sendEncodings: [
1995
+ { scalabilityMode: svc }
1996
+ ]
1997
+ });
1998
+ if(tr)
1999
+ sender = tr.sender;
2000
+ }
2001
+ // Check if insertable streams are involved
2002
+ if(sender && config.senderTransforms) {
2003
+ var senderStreams = null;
2004
+ if(RTCRtpSender.prototype.createEncodedStreams) {
2005
+ senderStreams = sender.createEncodedStreams();
2006
+ } else if(RTCRtpSender.prototype.createAudioEncodedStreams || RTCRtpSender.prototype.createEncodedVideoStreams) {
2007
+ if(sender.track.kind === "audio" && config.senderTransforms["audio"]) {
2008
+ senderStreams = sender.createEncodedAudioStreams();
2009
+ } else if(sender.track.kind === "video" && config.senderTransforms["video"]) {
2010
+ senderStreams = sender.createEncodedVideoStreams();
2011
+ }
2012
+ }
2013
+ if(senderStreams) {
2014
+ console.log(senderStreams);
2015
+ if(senderStreams.readableStream && senderStreams.writableStream) {
2016
+ senderStreams.readableStream
2017
+ .pipeThrough(config.senderTransforms[sender.track.kind])
2018
+ .pipeTo(senderStreams.writableStream);
2019
+ } else if(senderStreams.readable && senderStreams.writable) {
2020
+ senderStreams.readable
2021
+ .pipeThrough(config.senderTransforms[sender.track.kind])
2022
+ .pipeTo(senderStreams.writable);
2023
+ }
2024
+ }
2025
+ }
2026
+ });
2027
+ }
2028
+ // Any data channel to create?
2029
+ if(isDataEnabled(media) && !config.dataChannel[Janus.dataChanDefaultLabel]) {
2030
+ Janus.log("Creating default data channel");
2031
+ createDataChannel(handleId, Janus.dataChanDefaultLabel, null, false);
2032
+ config.pc.ondatachannel = function(event) {
2033
+ Janus.log("Data channel created by Janus:", event);
2034
+ createDataChannel(handleId, event.channel.label, event.channel.protocol, event.channel);
2035
+ };
2036
+ }
2037
+ // If there's a new local stream, let's notify the application
2038
+ if(config.myStream) {
2039
+ pluginHandle.onlocalstream(config.myStream);
2040
+ }
2041
+ // Create offer/answer now
2042
+ if(!jsep) {
2043
+ createOffer(handleId, media, callbacks);
2044
+ } else {
2045
+ config.pc.setRemoteDescription(jsep)
2046
+ .then(function() {
2047
+ Janus.log("Remote description accepted!");
2048
+ config.remoteSdp = jsep.sdp;
2049
+ // Any trickle candidate we cached?
2050
+ if(config.candidates && config.candidates.length > 0) {
2051
+ for(var i = 0; i< config.candidates.length; i++) {
2052
+ var candidate = config.candidates[i];
2053
+ Janus.debug("Adding remote candidate:", candidate);
2054
+ if(!candidate || candidate.completed === true) {
2055
+ // end-of-candidates
2056
+ config.pc.addIceCandidate(Janus.endOfCandidates);
2057
+ } else {
2058
+ // New candidate
2059
+ config.pc.addIceCandidate(candidate);
2060
+ }
2061
+ }
2062
+ config.candidates = [];
2063
+ }
2064
+ // Create the answer now
2065
+ createAnswer(handleId, media, callbacks);
2066
+ }, callbacks.error);
2067
+ }
2068
+ }
2069
+
2070
+ function prepareWebrtc(handleId, offer, callbacks) {
2071
+ callbacks = callbacks || {};
2072
+ callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
2073
+ callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError;
2074
+ var jsep = callbacks.jsep;
2075
+ if(offer && jsep) {
2076
+ Janus.error("Provided a JSEP to a createOffer");
2077
+ callbacks.error("Provided a JSEP to a createOffer");
2078
+ return;
2079
+ } else if(!offer && (!jsep || !jsep.type || !jsep.sdp)) {
2080
+ Janus.error("A valid JSEP is required for createAnswer");
2081
+ callbacks.error("A valid JSEP is required for createAnswer");
2082
+ return;
2083
+ }
2084
+ /* Check that callbacks.media is a (not null) Object */
2085
+ callbacks.media = (typeof callbacks.media === 'object' && callbacks.media) ? callbacks.media : { audio: true, video: true };
2086
+ var media = callbacks.media;
2087
+ var pluginHandle = pluginHandles[handleId];
2088
+ if(!pluginHandle || !pluginHandle.webrtcStuff) {
2089
+ Janus.warn("Invalid handle");
2090
+ callbacks.error("Invalid handle");
2091
+ return;
2092
+ }
2093
+ var config = pluginHandle.webrtcStuff;
2094
+ config.trickle = isTrickleEnabled(callbacks.trickle);
2095
+ // Are we updating a session?
2096
+ if(!config.pc) {
2097
+ // Nope, new PeerConnection
2098
+ media.update = false;
2099
+ media.keepAudio = false;
2100
+ media.keepVideo = false;
2101
+ } else {
2102
+ Janus.log("Updating existing media session");
2103
+ media.update = true;
2104
+ // Check if there's anything to add/remove/replace, or if we
2105
+ // can go directly to preparing the new SDP offer or answer
2106
+ if(callbacks.stream) {
2107
+ // External stream: is this the same as the one we were using before?
2108
+ if(callbacks.stream !== config.myStream) {
2109
+ Janus.log("Renegotiation involves a new external stream");
2110
+ }
2111
+ } else {
2112
+ // Check if there are changes on audio
2113
+ if(media.addAudio) {
2114
+ media.keepAudio = false;
2115
+ media.replaceAudio = false;
2116
+ media.removeAudio = false;
2117
+ media.audioSend = true;
2118
+ if(config.myStream && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) {
2119
+ Janus.error("Can't add audio stream, there already is one");
2120
+ callbacks.error("Can't add audio stream, there already is one");
2121
+ return;
2122
+ }
2123
+ } else if(media.removeAudio) {
2124
+ media.keepAudio = false;
2125
+ media.replaceAudio = false;
2126
+ media.addAudio = false;
2127
+ media.audioSend = false;
2128
+ } else if(media.replaceAudio) {
2129
+ media.keepAudio = false;
2130
+ media.addAudio = false;
2131
+ media.removeAudio = false;
2132
+ media.audioSend = true;
2133
+ }
2134
+ if(!config.myStream) {
2135
+ // No media stream: if we were asked to replace, it's actually an "add"
2136
+ if(media.replaceAudio) {
2137
+ media.keepAudio = false;
2138
+ media.replaceAudio = false;
2139
+ media.addAudio = true;
2140
+ media.audioSend = true;
2141
+ }
2142
+ if(isAudioSendEnabled(media)) {
2143
+ media.keepAudio = false;
2144
+ media.addAudio = true;
2145
+ }
2146
+ } else {
2147
+ if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) {
2148
+ // No audio track: if we were asked to replace, it's actually an "add"
2149
+ if(media.replaceAudio) {
2150
+ media.keepAudio = false;
2151
+ media.replaceAudio = false;
2152
+ media.addAudio = true;
2153
+ media.audioSend = true;
2154
+ }
2155
+ if(isAudioSendEnabled(media)) {
2156
+ media.keepAudio = false;
2157
+ media.addAudio = true;
2158
+ }
2159
+ } else {
2160
+ // We have an audio track: should we keep it as it is?
2161
+ if(isAudioSendEnabled(media) &&
2162
+ !media.removeAudio && !media.replaceAudio) {
2163
+ media.keepAudio = true;
2164
+ }
2165
+ }
2166
+ }
2167
+ // Check if there are changes on video
2168
+ if(media.addVideo) {
2169
+ media.keepVideo = false;
2170
+ media.replaceVideo = false;
2171
+ media.removeVideo = false;
2172
+ media.videoSend = true;
2173
+ if(config.myStream && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) {
2174
+ Janus.error("Can't add video stream, there already is one");
2175
+ callbacks.error("Can't add video stream, there already is one");
2176
+ return;
2177
+ }
2178
+ } else if(media.removeVideo) {
2179
+ media.keepVideo = false;
2180
+ media.replaceVideo = false;
2181
+ media.addVideo = false;
2182
+ media.videoSend = false;
2183
+ } else if(media.replaceVideo) {
2184
+ media.keepVideo = false;
2185
+ media.addVideo = false;
2186
+ media.removeVideo = false;
2187
+ media.videoSend = true;
2188
+ }
2189
+ if(!config.myStream) {
2190
+ // No media stream: if we were asked to replace, it's actually an "add"
2191
+ if(media.replaceVideo) {
2192
+ media.keepVideo = false;
2193
+ media.replaceVideo = false;
2194
+ media.addVideo = true;
2195
+ media.videoSend = true;
2196
+ }
2197
+ if(isVideoSendEnabled(media)) {
2198
+ media.keepVideo = false;
2199
+ media.addVideo = true;
2200
+ }
2201
+ } else {
2202
+ if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) {
2203
+ // No video track: if we were asked to replace, it's actually an "add"
2204
+ if(media.replaceVideo) {
2205
+ media.keepVideo = false;
2206
+ media.replaceVideo = false;
2207
+ media.addVideo = true;
2208
+ media.videoSend = true;
2209
+ }
2210
+ if(isVideoSendEnabled(media)) {
2211
+ media.keepVideo = false;
2212
+ media.addVideo = true;
2213
+ }
2214
+ } else {
2215
+ // We have a video track: should we keep it as it is?
2216
+ if(isVideoSendEnabled(media) && !media.removeVideo && !media.replaceVideo) {
2217
+ media.keepVideo = true;
2218
+ }
2219
+ }
2220
+ }
2221
+ // Data channels can only be added
2222
+ if(media.addData) {
2223
+ media.data = true;
2224
+ }
2225
+ }
2226
+ // If we're updating and keeping all tracks, let's skip the getUserMedia part
2227
+ if((isAudioSendEnabled(media) && media.keepAudio) &&
2228
+ (isVideoSendEnabled(media) && media.keepVideo)) {
2229
+ pluginHandle.consentDialog(false);
2230
+ streamsDone(handleId, jsep, media, callbacks, config.myStream);
2231
+ return;
2232
+ }
2233
+ }
2234
+ // If we're updating, check if we need to remove/replace one of the tracks
2235
+ if(media.update && (!config.streamExternal || (config.streamExternal && (media.replaceAudio || media.replaceVideo)))) {
2236
+ if(media.removeAudio || media.replaceAudio) {
2237
+ if(config.myStream && config.myStream.getAudioTracks() && config.myStream.getAudioTracks().length) {
2238
+ var at = config.myStream.getAudioTracks()[0];
2239
+ Janus.log("Removing audio track:", at);
2240
+ config.myStream.removeTrack(at);
2241
+ try {
2242
+ at.stop();
2243
+ } catch(e) {}
2244
+ }
2245
+ if(config.pc.getSenders() && config.pc.getSenders().length) {
2246
+ var ra = true;
2247
+ if(media.replaceAudio && Janus.unifiedPlan) {
2248
+ // We can use replaceTrack
2249
+ ra = false;
2250
+ }
2251
+ if(ra) {
2252
+ for(var asnd of config.pc.getSenders()) {
2253
+ if(asnd && asnd.track && asnd.track.kind === "audio") {
2254
+ Janus.log("Removing audio sender:", asnd);
2255
+ config.pc.removeTrack(asnd);
2256
+ }
2257
+ }
2258
+ }
2259
+ }
2260
+ }
2261
+ if(media.removeVideo || media.replaceVideo) {
2262
+ if(config.myStream && config.myStream.getVideoTracks() && config.myStream.getVideoTracks().length) {
2263
+ var vt = config.myStream.getVideoTracks()[0];
2264
+ Janus.log("Removing video track:", vt);
2265
+ config.myStream.removeTrack(vt);
2266
+ try {
2267
+ vt.stop();
2268
+ } catch(e) {}
2269
+ }
2270
+ if(config.pc.getSenders() && config.pc.getSenders().length) {
2271
+ var rv = true;
2272
+ if(media.replaceVideo && Janus.unifiedPlan) {
2273
+ // We can use replaceTrack
2274
+ rv = false;
2275
+ }
2276
+ if(rv) {
2277
+ for(var vsnd of config.pc.getSenders()) {
2278
+ if(vsnd && vsnd.track && vsnd.track.kind === "video") {
2279
+ Janus.log("Removing video sender:", vsnd);
2280
+ config.pc.removeTrack(vsnd);
2281
+ }
2282
+ }
2283
+ }
2284
+ }
2285
+ }
2286
+ }
2287
+ // Was a MediaStream object passed, or do we need to take care of that?
2288
+ if(callbacks.stream) {
2289
+ var stream = callbacks.stream;
2290
+ Janus.log("MediaStream provided by the application");
2291
+ Janus.debug(stream);
2292
+ // If this is an update, let's check if we need to release the previous stream
2293
+ if(media.update && config.myStream && config.myStream !== callbacks.stream && !config.streamExternal && !media.replaceAudio && !media.replaceVideo) {
2294
+ // We're replacing a stream we captured ourselves with an external one
2295
+ Janus.stopAllTracks(config.myStream);
2296
+ config.myStream = null;
2297
+ }
2298
+ // Skip the getUserMedia part
2299
+ config.streamExternal = true;
2300
+ pluginHandle.consentDialog(false);
2301
+ streamsDone(handleId, jsep, media, callbacks, stream);
2302
+ return;
2303
+ }
2304
+ if(isAudioSendEnabled(media) || isVideoSendEnabled(media)) {
2305
+ if(!Janus.isGetUserMediaAvailable()) {
2306
+ callbacks.error("getUserMedia not available");
2307
+ return;
2308
+ }
2309
+ var constraints = { mandatory: {}, optional: []};
2310
+ pluginHandle.consentDialog(true);
2311
+ var audioSupport = isAudioSendEnabled(media);
2312
+ if(audioSupport && media && typeof media.audio === 'object')
2313
+ audioSupport = media.audio;
2314
+ var videoSupport = isVideoSendEnabled(media);
2315
+ if(videoSupport && media) {
2316
+ var simulcast = (callbacks.simulcast === true || callbacks.simulcast2 === true);
2317
+ var svc = callbacks.svc;
2318
+ if((simulcast || svc) && !jsep && !media.video)
2319
+ media.video = "hires";
2320
+ if(media.video && media.video != 'screen' && media.video != 'window') {
2321
+ if(typeof media.video === 'object') {
2322
+ videoSupport = media.video;
2323
+ } else {
2324
+ var width = 0;
2325
+ var height = 0;
2326
+ if(media.video === 'lowres') {
2327
+ // Small resolution, 4:3
2328
+ height = 240;
2329
+ width = 320;
2330
+ } else if(media.video === 'lowres-16:9') {
2331
+ // Small resolution, 16:9
2332
+ height = 180;
2333
+ width = 320;
2334
+ } else if(media.video === 'hires' || media.video === 'hires-16:9' || media.video === 'hdres') {
2335
+ // High(HD) resolution is only 16:9
2336
+ height = 720;
2337
+ width = 1280;
2338
+ } else if(media.video === 'fhdres') {
2339
+ // Full HD resolution is only 16:9
2340
+ height = 1080;
2341
+ width = 1920;
2342
+ } else if(media.video === '4kres') {
2343
+ // 4K resolution is only 16:9
2344
+ height = 2160;
2345
+ width = 3840;
2346
+ } else if(media.video === 'stdres') {
2347
+ // Normal resolution, 4:3
2348
+ height = 480;
2349
+ width = 640;
2350
+ } else if(media.video === 'stdres-16:9') {
2351
+ // Normal resolution, 16:9
2352
+ height = 360;
2353
+ width = 640;
2354
+ } else {
2355
+ Janus.log("Default video setting is stdres 4:3");
2356
+ height = 480;
2357
+ width = 640;
2358
+ }
2359
+ Janus.log("Adding media constraint:", media.video);
2360
+ videoSupport = {
2361
+ 'height': {'ideal': height},
2362
+ 'width': {'ideal': width}
2363
+ };
2364
+ Janus.log("Adding video constraint:", videoSupport);
2365
+ }
2366
+ } else if(media.video === 'screen' || media.video === 'window') {
2367
+ if(navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
2368
+ // The new experimental getDisplayMedia API is available, let's use that
2369
+ // https://groups.google.com/forum/#!topic/discuss-webrtc/Uf0SrR4uxzk
2370
+ // https://webrtchacks.com/chrome-screensharing-getdisplaymedia/
2371
+ constraints.video = {};
2372
+ if(media.screenshareFrameRate) {
2373
+ constraints.video.frameRate = media.screenshareFrameRate;
2374
+ }
2375
+ if(media.screenshareHeight) {
2376
+ constraints.video.height = media.screenshareHeight;
2377
+ }
2378
+ if(media.screenshareWidth) {
2379
+ constraints.video.width = media.screenshareWidth;
2380
+ }
2381
+ constraints.audio = media.captureDesktopAudio;
2382
+ navigator.mediaDevices.getDisplayMedia(constraints)
2383
+ .then(function(stream) {
2384
+ pluginHandle.consentDialog(false);
2385
+ if(isAudioSendEnabled(media) && !media.keepAudio) {
2386
+ navigator.mediaDevices.getUserMedia({ audio: true, video: false })
2387
+ .then(function (audioStream) {
2388
+ stream.addTrack(audioStream.getAudioTracks()[0]);
2389
+ streamsDone(handleId, jsep, media, callbacks, stream);
2390
+ })
2391
+ } else {
2392
+ streamsDone(handleId, jsep, media, callbacks, stream);
2393
+ }
2394
+ }, function (error) {
2395
+ pluginHandle.consentDialog(false);
2396
+ callbacks.error(error);
2397
+ });
2398
+ return;
2399
+ }
2400
+ // We're going to try and use the extension for Chrome 34+, the old approach
2401
+ // for older versions of Chrome, or the experimental support in Firefox 33+
2402
+ const callbackUserMedia = function(error, stream) {
2403
+ pluginHandle.consentDialog(false);
2404
+ if(error) {
2405
+ callbacks.error(error);
2406
+ } else {
2407
+ streamsDone(handleId, jsep, media, callbacks, stream);
2408
+ }
2409
+ }
2410
+ const getScreenMedia = function(constraints, gsmCallback, useAudio) {
2411
+ Janus.log("Adding media constraint (screen capture)");
2412
+ Janus.debug(constraints);
2413
+ navigator.mediaDevices.getUserMedia(constraints)
2414
+ .then(function(stream) {
2415
+ if(useAudio) {
2416
+ navigator.mediaDevices.getUserMedia({ audio: true, video: false })
2417
+ .then(function (audioStream) {
2418
+ stream.addTrack(audioStream.getAudioTracks()[0]);
2419
+ gsmCallback(null, stream);
2420
+ })
2421
+ } else {
2422
+ gsmCallback(null, stream);
2423
+ }
2424
+ })
2425
+ .catch(function(error) { pluginHandle.consentDialog(false); gsmCallback(error); });
2426
+ }
2427
+ if(Janus.webRTCAdapter.browserDetails.browser === 'chrome') {
2428
+ var chromever = Janus.webRTCAdapter.browserDetails.version;
2429
+ var maxver = 33;
2430
+ if(window.navigator.userAgent.match('Linux'))
2431
+ maxver = 35; // "known" crash in chrome 34 and 35 on linux
2432
+ if(chromever >= 26 && chromever <= maxver) {
2433
+ // Chrome 26->33 requires some awkward chrome://flags manipulation
2434
+ constraints = {
2435
+ video: {
2436
+ mandatory: {
2437
+ googLeakyBucket: true,
2438
+ maxWidth: window.screen.width,
2439
+ maxHeight: window.screen.height,
2440
+ minFrameRate: media.screenshareFrameRate,
2441
+ maxFrameRate: media.screenshareFrameRate,
2442
+ chromeMediaSource: 'screen'
2443
+ }
2444
+ },
2445
+ audio: isAudioSendEnabled(media) && !media.keepAudio
2446
+ };
2447
+ getScreenMedia(constraints, callbackUserMedia);
2448
+ } else {
2449
+ // Chrome 34+ requires an extension
2450
+ Janus.extension.getScreen(function (error, sourceId) {
2451
+ if (error) {
2452
+ pluginHandle.consentDialog(false);
2453
+ return callbacks.error(error);
2454
+ }
2455
+ constraints = {
2456
+ audio: false,
2457
+ video: {
2458
+ mandatory: {
2459
+ chromeMediaSource: 'desktop',
2460
+ maxWidth: window.screen.width,
2461
+ maxHeight: window.screen.height,
2462
+ minFrameRate: media.screenshareFrameRate,
2463
+ maxFrameRate: media.screenshareFrameRate,
2464
+ },
2465
+ optional: [
2466
+ {googLeakyBucket: true},
2467
+ {googTemporalLayeredScreencast: true}
2468
+ ]
2469
+ }
2470
+ };
2471
+ constraints.video.mandatory.chromeMediaSourceId = sourceId;
2472
+ getScreenMedia(constraints, callbackUserMedia,
2473
+ isAudioSendEnabled(media) && !media.keepAudio);
2474
+ });
2475
+ }
2476
+ } else if(Janus.webRTCAdapter.browserDetails.browser === 'firefox') {
2477
+ if(Janus.webRTCAdapter.browserDetails.version >= 33) {
2478
+ // Firefox 33+ has experimental support for screen sharing
2479
+ constraints = {
2480
+ video: {
2481
+ mozMediaSource: media.video,
2482
+ mediaSource: media.video
2483
+ },
2484
+ audio: isAudioSendEnabled(media) && !media.keepAudio
2485
+ };
2486
+ getScreenMedia(constraints, function (err, stream) {
2487
+ callbackUserMedia(err, stream);
2488
+ // Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1045810
2489
+ if (!err) {
2490
+ var lastTime = stream.currentTime;
2491
+ var polly = window.setInterval(function () {
2492
+ if(!stream)
2493
+ window.clearInterval(polly);
2494
+ if(stream.currentTime == lastTime) {
2495
+ window.clearInterval(polly);
2496
+ if(stream.onended) {
2497
+ stream.onended();
2498
+ }
2499
+ }
2500
+ lastTime = stream.currentTime;
2501
+ }, 500);
2502
+ }
2503
+ });
2504
+ } else {
2505
+ var error = new Error('NavigatorUserMediaError');
2506
+ error.name = 'Your version of Firefox does not support screen sharing, please install Firefox 33 (or more recent versions)';
2507
+ pluginHandle.consentDialog(false);
2508
+ callbacks.error(error);
2509
+ return;
2510
+ }
2511
+ }
2512
+ return;
2513
+ }
2514
+ }
2515
+ // If we got here, we're not screensharing
2516
+ if(!media || media.video !== 'screen') {
2517
+ // Check whether all media sources are actually available or not
2518
+ navigator.mediaDevices.enumerateDevices().then(function(devices) {
2519
+ var audioExist = devices.some(function(device) {
2520
+ return device.kind === 'audioinput';
2521
+ }),
2522
+ videoExist = isScreenSendEnabled(media) || devices.some(function(device) {
2523
+ return device.kind === 'videoinput';
2524
+ });
2525
+
2526
+ // Check whether a missing device is really a problem
2527
+ var audioSend = isAudioSendEnabled(media);
2528
+ var videoSend = isVideoSendEnabled(media);
2529
+ var needAudioDevice = isAudioSendRequired(media);
2530
+ var needVideoDevice = isVideoSendRequired(media);
2531
+ if(audioSend || videoSend || needAudioDevice || needVideoDevice) {
2532
+ // We need to send either audio or video
2533
+ var haveAudioDevice = audioSend ? audioExist : false;
2534
+ var haveVideoDevice = videoSend ? videoExist : false;
2535
+ if(!haveAudioDevice && !haveVideoDevice) {
2536
+ // FIXME Should we really give up, or just assume recvonly for both?
2537
+ pluginHandle.consentDialog(false);
2538
+ callbacks.error('No capture device found');
2539
+ return false;
2540
+ } else if(!haveAudioDevice && needAudioDevice) {
2541
+ pluginHandle.consentDialog(false);
2542
+ callbacks.error('Audio capture is required, but no capture device found');
2543
+ return false;
2544
+ } else if(!haveVideoDevice && needVideoDevice) {
2545
+ pluginHandle.consentDialog(false);
2546
+ callbacks.error('Video capture is required, but no capture device found');
2547
+ return false;
2548
+ }
2549
+ }
2550
+
2551
+ var gumConstraints = {
2552
+ audio: (audioExist && !media.keepAudio) ? audioSupport : false,
2553
+ video: (videoExist && !media.keepVideo) ? videoSupport : false
2554
+ };
2555
+ Janus.debug("getUserMedia constraints", gumConstraints);
2556
+ if (!gumConstraints.audio && !gumConstraints.video) {
2557
+ pluginHandle.consentDialog(false);
2558
+ streamsDone(handleId, jsep, media, callbacks, stream);
2559
+ } else {
2560
+ navigator.mediaDevices.getUserMedia(gumConstraints)
2561
+ .then(function(stream) {
2562
+ pluginHandle.consentDialog(false);
2563
+ streamsDone(handleId, jsep, media, callbacks, stream);
2564
+ }).catch(function(error) {
2565
+ pluginHandle.consentDialog(false);
2566
+ callbacks.error({code: error.code, name: error.name, message: error.message});
2567
+ });
2568
+ }
2569
+ })
2570
+ .catch(function(error) {
2571
+ pluginHandle.consentDialog(false);
2572
+ callbacks.error(error);
2573
+ });
2574
+ }
2575
+ } else {
2576
+ // No need to do a getUserMedia, create offer/answer right away
2577
+ streamsDone(handleId, jsep, media, callbacks);
2578
+ }
2579
+ }
2580
+
2581
+ function prepareWebrtcPeer(handleId, callbacks) {
2582
+ callbacks = callbacks || {};
2583
+ callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
2584
+ callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : webrtcError;
2585
+ callbacks.customizeSdp = (typeof callbacks.customizeSdp == "function") ? callbacks.customizeSdp : Janus.noop;
2586
+ var jsep = callbacks.jsep;
2587
+ var pluginHandle = pluginHandles[handleId];
2588
+ if(!pluginHandle || !pluginHandle.webrtcStuff) {
2589
+ Janus.warn("Invalid handle");
2590
+ callbacks.error("Invalid handle");
2591
+ return;
2592
+ }
2593
+ var config = pluginHandle.webrtcStuff;
2594
+ if(jsep) {
2595
+ if(!config.pc) {
2596
+ Janus.warn("Wait, no PeerConnection?? if this is an answer, use createAnswer and not handleRemoteJsep");
2597
+ callbacks.error("No PeerConnection: if this is an answer, use createAnswer and not handleRemoteJsep");
2598
+ return;
2599
+ }
2600
+ callbacks.customizeSdp(jsep);
2601
+ config.pc.setRemoteDescription(jsep)
2602
+ .then(function() {
2603
+ Janus.log("Remote description accepted!");
2604
+ config.remoteSdp = jsep.sdp;
2605
+ // Any trickle candidate we cached?
2606
+ if(config.candidates && config.candidates.length > 0) {
2607
+ for(var i = 0; i< config.candidates.length; i++) {
2608
+ var candidate = config.candidates[i];
2609
+ Janus.debug("Adding remote candidate:", candidate);
2610
+ if(!candidate || candidate.completed === true) {
2611
+ // end-of-candidates
2612
+ config.pc.addIceCandidate(Janus.endOfCandidates);
2613
+ } else {
2614
+ // New candidate
2615
+ config.pc.addIceCandidate(candidate);
2616
+ }
2617
+ }
2618
+ config.candidates = [];
2619
+ }
2620
+ // Done
2621
+ callbacks.success();
2622
+ }, callbacks.error);
2623
+ } else {
2624
+ callbacks.error("Invalid JSEP");
2625
+ }
2626
+ }
2627
+
2628
+ function createOffer(handleId, media, callbacks) {
2629
+ callbacks = callbacks || {};
2630
+ callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
2631
+ callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
2632
+ callbacks.customizeSdp = (typeof callbacks.customizeSdp == "function") ? callbacks.customizeSdp : Janus.noop;
2633
+ var pluginHandle = pluginHandles[handleId];
2634
+ if(!pluginHandle || !pluginHandle.webrtcStuff) {
2635
+ Janus.warn("Invalid handle");
2636
+ callbacks.error("Invalid handle");
2637
+ return;
2638
+ }
2639
+ var config = pluginHandle.webrtcStuff;
2640
+ var simulcast = (callbacks.simulcast === true);
2641
+ if(!simulcast) {
2642
+ Janus.log("Creating offer (iceDone=" + config.iceDone + ")");
2643
+ } else {
2644
+ Janus.log("Creating offer (iceDone=" + config.iceDone + ", simulcast=" + simulcast + ")");
2645
+ }
2646
+ // https://code.google.com/p/webrtc/issues/detail?id=3508
2647
+ var mediaConstraints = {};
2648
+ if(Janus.unifiedPlan) {
2649
+ // We can use Transceivers
2650
+ var audioTransceiver = null, videoTransceiver = null;
2651
+ var transceivers = config.pc.getTransceivers();
2652
+ if(transceivers && transceivers.length > 0) {
2653
+ for(var t of transceivers) {
2654
+ if((t.sender && t.sender.track && t.sender.track.kind === "audio") ||
2655
+ (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) {
2656
+ if(!audioTransceiver) {
2657
+ audioTransceiver = t;
2658
+ }
2659
+ continue;
2660
+ }
2661
+ if((t.sender && t.sender.track && t.sender.track.kind === "video") ||
2662
+ (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) {
2663
+ if(!videoTransceiver) {
2664
+ videoTransceiver = t;
2665
+ }
2666
+ continue;
2667
+ }
2668
+ }
2669
+ }
2670
+ // Handle audio (and related changes, if any)
2671
+ var audioSend = isAudioSendEnabled(media);
2672
+ var audioRecv = isAudioRecvEnabled(media);
2673
+ if(!audioSend && !audioRecv) {
2674
+ // Audio disabled: have we removed it?
2675
+ if(media.removeAudio && audioTransceiver) {
2676
+ if (audioTransceiver.setDirection) {
2677
+ audioTransceiver.setDirection("inactive");
2678
+ } else {
2679
+ audioTransceiver.direction = "inactive";
2680
+ }
2681
+ Janus.log("Setting audio transceiver to inactive:", audioTransceiver);
2682
+ }
2683
+ } else {
2684
+ // Take care of audio m-line
2685
+ if(audioSend && audioRecv) {
2686
+ if(audioTransceiver) {
2687
+ if (audioTransceiver.setDirection) {
2688
+ audioTransceiver.setDirection("sendrecv");
2689
+ } else {
2690
+ audioTransceiver.direction = "sendrecv";
2691
+ }
2692
+ Janus.log("Setting audio transceiver to sendrecv:", audioTransceiver);
2693
+ }
2694
+ } else if(audioSend && !audioRecv) {
2695
+ if(audioTransceiver) {
2696
+ if (audioTransceiver.setDirection) {
2697
+ audioTransceiver.setDirection("sendonly");
2698
+ } else {
2699
+ audioTransceiver.direction = "sendonly";
2700
+ }
2701
+ Janus.log("Setting audio transceiver to sendonly:", audioTransceiver);
2702
+ }
2703
+ } else if(!audioSend && audioRecv) {
2704
+ if(audioTransceiver) {
2705
+ if (audioTransceiver.setDirection) {
2706
+ audioTransceiver.setDirection("recvonly");
2707
+ } else {
2708
+ audioTransceiver.direction = "recvonly";
2709
+ }
2710
+ Janus.log("Setting audio transceiver to recvonly:", audioTransceiver);
2711
+ } else {
2712
+ // In theory, this is the only case where we might not have a transceiver yet
2713
+ audioTransceiver = config.pc.addTransceiver("audio", { direction: "recvonly" });
2714
+ Janus.log("Adding recvonly audio transceiver:", audioTransceiver);
2715
+ }
2716
+ }
2717
+ }
2718
+ // Handle video (and related changes, if any)
2719
+ var videoSend = isVideoSendEnabled(media);
2720
+ var videoRecv = isVideoRecvEnabled(media);
2721
+ if(!videoSend && !videoRecv) {
2722
+ // Video disabled: have we removed it?
2723
+ if(media.removeVideo && videoTransceiver) {
2724
+ if (videoTransceiver.setDirection) {
2725
+ videoTransceiver.setDirection("inactive");
2726
+ } else {
2727
+ videoTransceiver.direction = "inactive";
2728
+ }
2729
+ Janus.log("Setting video transceiver to inactive:", videoTransceiver);
2730
+ }
2731
+ } else {
2732
+ // Take care of video m-line
2733
+ if(videoSend && videoRecv) {
2734
+ if(videoTransceiver) {
2735
+ if (videoTransceiver.setDirection) {
2736
+ videoTransceiver.setDirection("sendrecv");
2737
+ } else {
2738
+ videoTransceiver.direction = "sendrecv";
2739
+ }
2740
+ Janus.log("Setting video transceiver to sendrecv:", videoTransceiver);
2741
+ }
2742
+ } else if(videoSend && !videoRecv) {
2743
+ if(videoTransceiver) {
2744
+ if (videoTransceiver.setDirection) {
2745
+ videoTransceiver.setDirection("sendonly");
2746
+ } else {
2747
+ videoTransceiver.direction = "sendonly";
2748
+ }
2749
+ Janus.log("Setting video transceiver to sendonly:", videoTransceiver);
2750
+ }
2751
+ } else if(!videoSend && videoRecv) {
2752
+ if(videoTransceiver) {
2753
+ if (videoTransceiver.setDirection) {
2754
+ videoTransceiver.setDirection("recvonly");
2755
+ } else {
2756
+ videoTransceiver.direction = "recvonly";
2757
+ }
2758
+ Janus.log("Setting video transceiver to recvonly:", videoTransceiver);
2759
+ } else {
2760
+ // In theory, this is the only case where we might not have a transceiver yet
2761
+ videoTransceiver = config.pc.addTransceiver("video", { direction: "recvonly" });
2762
+ Janus.log("Adding recvonly video transceiver:", videoTransceiver);
2763
+ }
2764
+ }
2765
+ }
2766
+ } else {
2767
+ mediaConstraints["offerToReceiveAudio"] = isAudioRecvEnabled(media);
2768
+ mediaConstraints["offerToReceiveVideo"] = isVideoRecvEnabled(media);
2769
+ }
2770
+ var iceRestart = (callbacks.iceRestart === true);
2771
+ if(iceRestart) {
2772
+ mediaConstraints["iceRestart"] = true;
2773
+ }
2774
+ Janus.debug(mediaConstraints);
2775
+ // Check if this is Firefox and we've been asked to do simulcasting
2776
+ var sendVideo = isVideoSendEnabled(media);
2777
+ if(sendVideo && simulcast && Janus.webRTCAdapter.browserDetails.browser === "firefox") {
2778
+ // FIXME Based on https://gist.github.com/voluntas/088bc3cc62094730647b
2779
+ Janus.log("Enabling Simulcasting for Firefox (RID)");
2780
+ var sender = config.pc.getSenders().find(function(s) {return s.track && s.track.kind === "video"});
2781
+ if(sender) {
2782
+ var parameters = sender.getParameters();
2783
+ if(!parameters) {
2784
+ parameters = {};
2785
+ }
2786
+ var maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates);
2787
+ parameters.encodings = callbacks.sendEncodings || [
2788
+ { rid: "h", active: true, maxBitrate: maxBitrates.high },
2789
+ { rid: "m", active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2 },
2790
+ { rid: "l", active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4 }
2791
+ ];
2792
+ sender.setParameters(parameters);
2793
+ }
2794
+ }
2795
+ config.pc.createOffer(mediaConstraints)
2796
+ .then(function(offer) {
2797
+ Janus.debug(offer);
2798
+ // JSON.stringify doesn't work on some WebRTC objects anymore
2799
+ // See https://code.google.com/p/chromium/issues/detail?id=467366
2800
+ var jsep = {
2801
+ "type": offer.type,
2802
+ "sdp": offer.sdp
2803
+ };
2804
+ callbacks.customizeSdp(jsep);
2805
+ offer.sdp = jsep.sdp;
2806
+ Janus.log("Setting local description");
2807
+ if(sendVideo && simulcast && !Janus.unifiedPlan) {
2808
+ // We only do simulcast via SDP munging on older versions of Chrome and Safari
2809
+ if(Janus.webRTCAdapter.browserDetails.browser === "chrome" ||
2810
+ Janus.webRTCAdapter.browserDetails.browser === "safari") {
2811
+ Janus.log("Enabling Simulcasting for Chrome (SDP munging)");
2812
+ offer.sdp = mungeSdpForSimulcasting(offer.sdp);
2813
+ }
2814
+ }
2815
+ config.mySdp = {
2816
+ type: "offer",
2817
+ sdp: offer.sdp
2818
+ };
2819
+ config.pc.setLocalDescription(offer)
2820
+ .catch(callbacks.error);
2821
+ config.mediaConstraints = mediaConstraints;
2822
+ if(!config.iceDone && !config.trickle) {
2823
+ // Don't do anything until we have all candidates
2824
+ Janus.log("Waiting for all candidates...");
2825
+ return;
2826
+ }
2827
+ // If transforms are present, notify Janus that the media is end-to-end encrypted
2828
+ if(config.senderTransforms || config.receiverTransforms) {
2829
+ offer["e2ee"] = true;
2830
+ }
2831
+ callbacks.success(offer);
2832
+ }, callbacks.error);
2833
+ }
2834
+
2835
+ function createAnswer(handleId, media, callbacks) {
2836
+ callbacks = callbacks || {};
2837
+ callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
2838
+ callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
2839
+ callbacks.customizeSdp = (typeof callbacks.customizeSdp == "function") ? callbacks.customizeSdp : Janus.noop;
2840
+ var pluginHandle = pluginHandles[handleId];
2841
+ if(!pluginHandle || !pluginHandle.webrtcStuff) {
2842
+ Janus.warn("Invalid handle");
2843
+ callbacks.error("Invalid handle");
2844
+ return;
2845
+ }
2846
+ var config = pluginHandle.webrtcStuff;
2847
+ var simulcast = (callbacks.simulcast === true || callbacks.simulcast2 === true);
2848
+ if(!simulcast) {
2849
+ Janus.log("Creating answer (iceDone=" + config.iceDone + ")");
2850
+ } else {
2851
+ Janus.log("Creating answer (iceDone=" + config.iceDone + ", simulcast=" + simulcast + ")");
2852
+ }
2853
+ var mediaConstraints = null;
2854
+ if(Janus.unifiedPlan) {
2855
+ // We can use Transceivers
2856
+ mediaConstraints = {};
2857
+ var audioTransceiver = null, videoTransceiver = null;
2858
+ var transceivers = config.pc.getTransceivers();
2859
+ if(transceivers && transceivers.length > 0) {
2860
+ for(var t of transceivers) {
2861
+ if((t.sender && t.sender.track && t.sender.track.kind === "audio") ||
2862
+ (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")) {
2863
+ if(!audioTransceiver)
2864
+ audioTransceiver = t;
2865
+ continue;
2866
+ }
2867
+ if((t.sender && t.sender.track && t.sender.track.kind === "video") ||
2868
+ (t.receiver && t.receiver.track && t.receiver.track.kind === "video")) {
2869
+ if(!videoTransceiver)
2870
+ videoTransceiver = t;
2871
+ continue;
2872
+ }
2873
+ }
2874
+ }
2875
+ // Handle audio (and related changes, if any)
2876
+ var audioSend = isAudioSendEnabled(media);
2877
+ var audioRecv = isAudioRecvEnabled(media);
2878
+ if(!audioSend && !audioRecv) {
2879
+ // Audio disabled: have we removed it?
2880
+ if(media.removeAudio && audioTransceiver) {
2881
+ try {
2882
+ if (audioTransceiver.setDirection) {
2883
+ audioTransceiver.setDirection("inactive");
2884
+ } else {
2885
+ audioTransceiver.direction = "inactive";
2886
+ }
2887
+ Janus.log("Setting audio transceiver to inactive:", audioTransceiver);
2888
+ } catch(e) {
2889
+ Janus.error(e);
2890
+ }
2891
+ }
2892
+ } else {
2893
+ // Take care of audio m-line
2894
+ if(audioSend && audioRecv) {
2895
+ if(audioTransceiver) {
2896
+ try {
2897
+ if (audioTransceiver.setDirection) {
2898
+ audioTransceiver.setDirection("sendrecv");
2899
+ } else {
2900
+ audioTransceiver.direction = "sendrecv";
2901
+ }
2902
+ Janus.log("Setting audio transceiver to sendrecv:", audioTransceiver);
2903
+ } catch(e) {
2904
+ Janus.error(e);
2905
+ }
2906
+ }
2907
+ } else if(audioSend && !audioRecv) {
2908
+ try {
2909
+ if(audioTransceiver) {
2910
+ if (audioTransceiver.setDirection) {
2911
+ audioTransceiver.setDirection("sendonly");
2912
+ } else {
2913
+ audioTransceiver.direction = "sendonly";
2914
+ }
2915
+ Janus.log("Setting audio transceiver to sendonly:", audioTransceiver);
2916
+ }
2917
+ } catch(e) {
2918
+ Janus.error(e);
2919
+ }
2920
+ } else if(!audioSend && audioRecv) {
2921
+ if(audioTransceiver) {
2922
+ try {
2923
+ if (audioTransceiver.setDirection) {
2924
+ audioTransceiver.setDirection("recvonly");
2925
+ } else {
2926
+ audioTransceiver.direction = "recvonly";
2927
+ }
2928
+ Janus.log("Setting audio transceiver to recvonly:", audioTransceiver);
2929
+ } catch(e) {
2930
+ Janus.error(e);
2931
+ }
2932
+ } else {
2933
+ // In theory, this is the only case where we might not have a transceiver yet
2934
+ audioTransceiver = config.pc.addTransceiver("audio", { direction: "recvonly" });
2935
+ Janus.log("Adding recvonly audio transceiver:", audioTransceiver);
2936
+ }
2937
+ }
2938
+ }
2939
+ // Handle video (and related changes, if any)
2940
+ var videoSend = isVideoSendEnabled(media);
2941
+ var videoRecv = isVideoRecvEnabled(media);
2942
+ if(!videoSend && !videoRecv) {
2943
+ // Video disabled: have we removed it?
2944
+ if(media.removeVideo && videoTransceiver) {
2945
+ try {
2946
+ if (videoTransceiver.setDirection) {
2947
+ videoTransceiver.setDirection("inactive");
2948
+ } else {
2949
+ videoTransceiver.direction = "inactive";
2950
+ }
2951
+ Janus.log("Setting video transceiver to inactive:", videoTransceiver);
2952
+ } catch(e) {
2953
+ Janus.error(e);
2954
+ }
2955
+ }
2956
+ } else {
2957
+ // Take care of video m-line
2958
+ if(videoSend && videoRecv) {
2959
+ if(videoTransceiver) {
2960
+ try {
2961
+ if (videoTransceiver.setDirection) {
2962
+ videoTransceiver.setDirection("sendrecv");
2963
+ } else {
2964
+ videoTransceiver.direction = "sendrecv";
2965
+ }
2966
+ Janus.log("Setting video transceiver to sendrecv:", videoTransceiver);
2967
+ } catch(e) {
2968
+ Janus.error(e);
2969
+ }
2970
+ }
2971
+ } else if(videoSend && !videoRecv) {
2972
+ if(videoTransceiver) {
2973
+ try {
2974
+ if (videoTransceiver.setDirection) {
2975
+ videoTransceiver.setDirection("sendonly");
2976
+ } else {
2977
+ videoTransceiver.direction = "sendonly";
2978
+ }
2979
+ Janus.log("Setting video transceiver to sendonly:", videoTransceiver);
2980
+ } catch(e) {
2981
+ Janus.error(e);
2982
+ }
2983
+ }
2984
+ } else if(!videoSend && videoRecv) {
2985
+ if(videoTransceiver) {
2986
+ try {
2987
+ if (videoTransceiver.setDirection) {
2988
+ videoTransceiver.setDirection("recvonly");
2989
+ } else {
2990
+ videoTransceiver.direction = "recvonly";
2991
+ }
2992
+ Janus.log("Setting video transceiver to recvonly:", videoTransceiver);
2993
+ } catch(e) {
2994
+ Janus.error(e);
2995
+ }
2996
+ } else {
2997
+ // In theory, this is the only case where we might not have a transceiver yet
2998
+ videoTransceiver = config.pc.addTransceiver("video", { direction: "recvonly" });
2999
+ Janus.log("Adding recvonly video transceiver:", videoTransceiver);
3000
+ }
3001
+ }
3002
+ }
3003
+ } else {
3004
+ if(Janus.webRTCAdapter.browserDetails.browser === "firefox" || Janus.webRTCAdapter.browserDetails.browser === "edge") {
3005
+ mediaConstraints = {
3006
+ offerToReceiveAudio: isAudioRecvEnabled(media),
3007
+ offerToReceiveVideo: isVideoRecvEnabled(media)
3008
+ };
3009
+ } else {
3010
+ mediaConstraints = {
3011
+ mandatory: {
3012
+ OfferToReceiveAudio: isAudioRecvEnabled(media),
3013
+ OfferToReceiveVideo: isVideoRecvEnabled(media)
3014
+ }
3015
+ };
3016
+ }
3017
+ }
3018
+ Janus.debug(mediaConstraints);
3019
+ // Check if this is Firefox and we've been asked to do simulcasting
3020
+ var sendVideo = isVideoSendEnabled(media);
3021
+ if(sendVideo && simulcast && Janus.webRTCAdapter.browserDetails.browser === "firefox") {
3022
+ // FIXME Based on https://gist.github.com/voluntas/088bc3cc62094730647b
3023
+ Janus.log("Enabling Simulcasting for Firefox (RID)");
3024
+ var sender = config.pc.getSenders()[1];
3025
+ Janus.log(sender);
3026
+ var parameters = sender.getParameters();
3027
+ Janus.log(parameters);
3028
+
3029
+ var maxBitrates = getMaxBitrates(callbacks.simulcastMaxBitrates);
3030
+ sender.setParameters({encodings: callbacks.sendEncodings || [
3031
+ { rid: "h", active: true, maxBitrate: maxBitrates.high },
3032
+ { rid: "m", active: true, maxBitrate: maxBitrates.medium, scaleResolutionDownBy: 2},
3033
+ { rid: "l", active: true, maxBitrate: maxBitrates.low, scaleResolutionDownBy: 4}
3034
+ ]});
3035
+ }
3036
+ config.pc.createAnswer(mediaConstraints)
3037
+ .then(function(answer) {
3038
+ Janus.debug(answer);
3039
+ // JSON.stringify doesn't work on some WebRTC objects anymore
3040
+ // See https://code.google.com/p/chromium/issues/detail?id=467366
3041
+ var jsep = {
3042
+ "type": answer.type,
3043
+ "sdp": answer.sdp
3044
+ };
3045
+ callbacks.customizeSdp(jsep);
3046
+ answer.sdp = jsep.sdp;
3047
+ Janus.log("Setting local description");
3048
+ if(sendVideo && simulcast && !Janus.unifiedPlan) {
3049
+ // We only do simulcast via SDP munging on older versions of Chrome and Safari
3050
+ if(Janus.webRTCAdapter.browserDetails.browser === "chrome") {
3051
+ // FIXME Apparently trying to simulcast when answering breaks video in Chrome...
3052
+ //~ Janus.log("Enabling Simulcasting for Chrome (SDP munging)");
3053
+ //~ answer.sdp = mungeSdpForSimulcasting(answer.sdp);
3054
+ Janus.warn("simulcast=true, but this is an answer, and video breaks in Chrome if we enable it");
3055
+ }
3056
+ }
3057
+ config.mySdp = {
3058
+ type: "answer",
3059
+ sdp: answer.sdp
3060
+ };
3061
+ config.pc.setLocalDescription(answer)
3062
+ .catch(callbacks.error);
3063
+ config.mediaConstraints = mediaConstraints;
3064
+ if(!config.iceDone && !config.trickle) {
3065
+ // Don't do anything until we have all candidates
3066
+ Janus.log("Waiting for all candidates...");
3067
+ return;
3068
+ }
3069
+ // If transforms are present, notify Janus that the media is end-to-end encrypted
3070
+ if(config.senderTransforms || config.receiverTransforms) {
3071
+ answer["e2ee"] = true;
3072
+ }
3073
+ callbacks.success(answer);
3074
+ }, callbacks.error);
3075
+ }
3076
+
3077
+ function sendSDP(handleId, callbacks) {
3078
+ callbacks = callbacks || {};
3079
+ callbacks.success = (typeof callbacks.success == "function") ? callbacks.success : Janus.noop;
3080
+ callbacks.error = (typeof callbacks.error == "function") ? callbacks.error : Janus.noop;
3081
+ var pluginHandle = pluginHandles[handleId];
3082
+ if(!pluginHandle || !pluginHandle.webrtcStuff) {
3083
+ Janus.warn("Invalid handle, not sending anything");
3084
+ return;
3085
+ }
3086
+ var config = pluginHandle.webrtcStuff;
3087
+ Janus.log("Sending offer/answer SDP...");
3088
+ if(!config.mySdp) {
3089
+ Janus.warn("Local SDP instance is invalid, not sending anything...");
3090
+ return;
3091
+ }
3092
+ config.mySdp = {
3093
+ "type": config.pc.localDescription.type,
3094
+ "sdp": config.pc.localDescription.sdp
3095
+ };
3096
+ if(config.trickle === false)
3097
+ config.mySdp["trickle"] = false;
3098
+ Janus.debug(callbacks);
3099
+ config.sdpSent = true;
3100
+ callbacks.success(config.mySdp);
3101
+ }
3102
+
3103
+ function getVolume(handleId, remote) {
3104
+ var pluginHandle = pluginHandles[handleId];
3105
+ if(!pluginHandle || !pluginHandle.webrtcStuff) {
3106
+ Janus.warn("Invalid handle");
3107
+ return 0;
3108
+ }
3109
+ var stream = remote ? "remote" : "local";
3110
+ var config = pluginHandle.webrtcStuff;
3111
+ if(!config.volume[stream])
3112
+ config.volume[stream] = { value: 0 };
3113
+ // Start getting the volume, if audioLevel in getStats is supported (apparently
3114
+ // they're only available in Chrome/Safari right now: https://webrtc-stats.callstats.io/)
3115
+ if(config.pc.getStats && (Janus.webRTCAdapter.browserDetails.browser === "chrome" ||
3116
+ Janus.webRTCAdapter.browserDetails.browser === "safari")) {
3117
+ if(remote && !config.remoteStream) {
3118
+ Janus.warn("Remote stream unavailable");
3119
+ return 0;
3120
+ } else if(!remote && !config.myStream) {
3121
+ Janus.warn("Local stream unavailable");
3122
+ return 0;
3123
+ }
3124
+ if(!config.volume[stream].timer) {
3125
+ Janus.log("Starting " + stream + " volume monitor");
3126
+ config.volume[stream].timer = setInterval(function() {
3127
+ config.pc.getStats()
3128
+ .then(function(stats) {
3129
+ stats.forEach(function (res) {
3130
+ if(!res || res.kind !== "audio")
3131
+ return;
3132
+ if((remote && !res.remoteSource) || (!remote && res.type !== "media-source"))
3133
+ return;
3134
+ config.volume[stream].value = (res.audioLevel ? res.audioLevel : 0);
3135
+ });
3136
+ });
3137
+ }, 200);
3138
+ return 0; // We don't have a volume to return yet
3139
+ }
3140
+ return config.volume[stream].value;
3141
+ } else {
3142
+ // audioInputLevel and audioOutputLevel seem only available in Chrome? audioLevel
3143
+ // seems to be available on Chrome and Firefox, but they don't seem to work
3144
+ Janus.warn("Getting the " + stream + " volume unsupported by browser");
3145
+ return 0;
3146
+ }
3147
+ }
3148
+
3149
+ function isMuted(handleId, video) {
3150
+ var pluginHandle = pluginHandles[handleId];
3151
+ if(!pluginHandle || !pluginHandle.webrtcStuff) {
3152
+ Janus.warn("Invalid handle");
3153
+ return true;
3154
+ }
3155
+ var config = pluginHandle.webrtcStuff;
3156
+ if(!config.pc) {
3157
+ Janus.warn("Invalid PeerConnection");
3158
+ return true;
3159
+ }
3160
+ if(!config.myStream) {
3161
+ Janus.warn("Invalid local MediaStream");
3162
+ return true;
3163
+ }
3164
+ if(video) {
3165
+ // Check video track
3166
+ if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) {
3167
+ Janus.warn("No video track");
3168
+ return true;
3169
+ }
3170
+ return !config.myStream.getVideoTracks()[0].enabled;
3171
+ } else {
3172
+ // Check audio track
3173
+ if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) {
3174
+ Janus.warn("No audio track");
3175
+ return true;
3176
+ }
3177
+ return !config.myStream.getAudioTracks()[0].enabled;
3178
+ }
3179
+ }
3180
+
3181
+ function mute(handleId, video, mute) {
3182
+ var pluginHandle = pluginHandles[handleId];
3183
+ if(!pluginHandle || !pluginHandle.webrtcStuff) {
3184
+ Janus.warn("Invalid handle");
3185
+ return false;
3186
+ }
3187
+ var config = pluginHandle.webrtcStuff;
3188
+ if(!config.pc) {
3189
+ Janus.warn("Invalid PeerConnection");
3190
+ return false;
3191
+ }
3192
+ if(!config.myStream) {
3193
+ Janus.warn("Invalid local MediaStream");
3194
+ return false;
3195
+ }
3196
+ if(video) {
3197
+ // Mute/unmute video track
3198
+ if(!config.myStream.getVideoTracks() || config.myStream.getVideoTracks().length === 0) {
3199
+ Janus.warn("No video track");
3200
+ return false;
3201
+ }
3202
+ config.myStream.getVideoTracks()[0].enabled = !mute;
3203
+ return true;
3204
+ } else {
3205
+ // Mute/unmute audio track
3206
+ if(!config.myStream.getAudioTracks() || config.myStream.getAudioTracks().length === 0) {
3207
+ Janus.warn("No audio track");
3208
+ return false;
3209
+ }
3210
+ config.myStream.getAudioTracks()[0].enabled = !mute;
3211
+ return true;
3212
+ }
3213
+ }
3214
+
3215
+ function getBitrate(handleId) {
3216
+ var pluginHandle = pluginHandles[handleId];
3217
+ if(!pluginHandle || !pluginHandle.webrtcStuff) {
3218
+ Janus.warn("Invalid handle");
3219
+ return "Invalid handle";
3220
+ }
3221
+ var config = pluginHandle.webrtcStuff;
3222
+ if(!config.pc)
3223
+ return "Invalid PeerConnection";
3224
+ // Start getting the bitrate, if getStats is supported
3225
+ if(config.pc.getStats) {
3226
+ if(!config.bitrate.timer) {
3227
+ Janus.log("Starting bitrate timer (via getStats)");
3228
+ config.bitrate.timer = setInterval(function() {
3229
+ config.pc.getStats()
3230
+ .then(function(stats) {
3231
+ stats.forEach(function (res) {
3232
+ if(!res)
3233
+ return;
3234
+ var inStats = false;
3235
+ // Check if these are statistics on incoming media
3236
+ if((res.mediaType === "video" || res.id.toLowerCase().indexOf("video") > -1) &&
3237
+ res.type === "inbound-rtp" && res.id.indexOf("rtcp") < 0) {
3238
+ // New stats
3239
+ inStats = true;
3240
+ } else if(res.type == 'ssrc' && res.bytesReceived &&
3241
+ (res.googCodecName === "VP8" || res.googCodecName === "")) {
3242
+ // Older Chromer versions
3243
+ inStats = true;
3244
+ }
3245
+ // Parse stats now
3246
+ if(inStats) {
3247
+ config.bitrate.bsnow = res.bytesReceived;
3248
+ config.bitrate.tsnow = res.timestamp;
3249
+ if(config.bitrate.bsbefore === null || config.bitrate.tsbefore === null) {
3250
+ // Skip this round
3251
+ config.bitrate.bsbefore = config.bitrate.bsnow;
3252
+ config.bitrate.tsbefore = config.bitrate.tsnow;
3253
+ } else {
3254
+ // Calculate bitrate
3255
+ var timePassed = config.bitrate.tsnow - config.bitrate.tsbefore;
3256
+ if(Janus.webRTCAdapter.browserDetails.browser === "safari")
3257
+ timePassed = timePassed/1000; // Apparently the timestamp is in microseconds, in Safari
3258
+ var bitRate = Math.round((config.bitrate.bsnow - config.bitrate.bsbefore) * 8 / timePassed);
3259
+ if(Janus.webRTCAdapter.browserDetails.browser === "safari")
3260
+ bitRate = parseInt(bitRate/1000);
3261
+ config.bitrate.value = bitRate + ' kbits/sec';
3262
+ //~ Janus.log("Estimated bitrate is " + config.bitrate.value);
3263
+ config.bitrate.bsbefore = config.bitrate.bsnow;
3264
+ config.bitrate.tsbefore = config.bitrate.tsnow;
3265
+ }
3266
+ }
3267
+ });
3268
+ });
3269
+ }, 1000);
3270
+ return "0 kbits/sec"; // We don't have a bitrate value yet
3271
+ }
3272
+ return config.bitrate.value;
3273
+ } else {
3274
+ Janus.warn("Getting the video bitrate unsupported by browser");
3275
+ return "Feature unsupported by browser";
3276
+ }
3277
+ }
3278
+
3279
+ function webrtcError(error) {
3280
+ Janus.error("WebRTC error:", error);
3281
+ }
3282
+
3283
+ function cleanupWebrtc(handleId, hangupRequest) {
3284
+ Janus.log("Cleaning WebRTC stuff");
3285
+ var pluginHandle = pluginHandles[handleId];
3286
+ if(!pluginHandle) {
3287
+ // Nothing to clean
3288
+ return;
3289
+ }
3290
+ var config = pluginHandle.webrtcStuff;
3291
+ if(config) {
3292
+ if(hangupRequest === true) {
3293
+ // Send a hangup request (we don't really care about the response)
3294
+ var request = { "janus": "hangup", "transaction": Janus.randomString(12) };
3295
+ if(pluginHandle.token)
3296
+ request["token"] = pluginHandle.token;
3297
+ if(apisecret)
3298
+ request["apisecret"] = apisecret;
3299
+ Janus.debug("Sending hangup request (handle=" + handleId + "):");
3300
+ Janus.debug(request);
3301
+ if(websockets) {
3302
+ request["session_id"] = sessionId;
3303
+ request["handle_id"] = handleId;
3304
+ ws.send(JSON.stringify(request));
3305
+ } else {
3306
+ Janus.httpAPICall(server + "/" + sessionId + "/" + handleId, {
3307
+ verb: 'POST',
3308
+ withCredentials: withCredentials,
3309
+ body: request
3310
+ });
3311
+ }
3312
+ }
3313
+ // Cleanup stack
3314
+ config.remoteStream = null;
3315
+ if(config.volume) {
3316
+ if(config.volume["local"] && config.volume["local"].timer)
3317
+ clearInterval(config.volume["local"].timer);
3318
+ if(config.volume["remote"] && config.volume["remote"].timer)
3319
+ clearInterval(config.volume["remote"].timer);
3320
+ }
3321
+ config.volume = {};
3322
+ if(config.bitrate.timer)
3323
+ clearInterval(config.bitrate.timer);
3324
+ config.bitrate.timer = null;
3325
+ config.bitrate.bsnow = null;
3326
+ config.bitrate.bsbefore = null;
3327
+ config.bitrate.tsnow = null;
3328
+ config.bitrate.tsbefore = null;
3329
+ config.bitrate.value = null;
3330
+ if(!config.streamExternal && config.myStream) {
3331
+ Janus.log("Stopping local stream tracks");
3332
+ Janus.stopAllTracks(config.myStream);
3333
+ }
3334
+ config.streamExternal = false;
3335
+ config.myStream = null;
3336
+ // Close PeerConnection
3337
+ try {
3338
+ config.pc.close();
3339
+ } catch(e) {
3340
+ // Do nothing
3341
+ }
3342
+ config.pc = null;
3343
+ config.candidates = null;
3344
+ config.mySdp = null;
3345
+ config.remoteSdp = null;
3346
+ config.iceDone = false;
3347
+ config.dataChannel = {};
3348
+ config.dtmfSender = null;
3349
+ config.senderTransforms = null;
3350
+ config.receiverTransforms = null;
3351
+ }
3352
+ pluginHandle.oncleanup();
3353
+ }
3354
+
3355
+ // Helper method to munge an SDP to enable simulcasting (Chrome only)
3356
+ function mungeSdpForSimulcasting(sdp) {
3357
+ // Let's munge the SDP to add the attributes for enabling simulcasting
3358
+ // (based on https://gist.github.com/ggarber/a19b4c33510028b9c657)
3359
+ var lines = sdp.split("\r\n");
3360
+ var video = false;
3361
+ var ssrc = [ -1 ], ssrc_fid = [ -1 ];
3362
+ var cname = null, msid = null, mslabel = null, label = null;
3363
+ var insertAt = -1;
3364
+ for(let i=0; i<lines.length; i++) {
3365
+ const mline = lines[i].match(/m=(\w+) */);
3366
+ if(mline) {
3367
+ const medium = mline[1];
3368
+ if(medium === "video") {
3369
+ // New video m-line: make sure it's the first one
3370
+ if(ssrc[0] < 0) {
3371
+ video = true;
3372
+ } else {
3373
+ // We're done, let's add the new attributes here
3374
+ insertAt = i;
3375
+ break;
3376
+ }
3377
+ } else {
3378
+ // New non-video m-line: do we have what we were looking for?
3379
+ if(ssrc[0] > -1) {
3380
+ // We're done, let's add the new attributes here
3381
+ insertAt = i;
3382
+ break;
3383
+ }
3384
+ }
3385
+ continue;
3386
+ }
3387
+ if(!video)
3388
+ continue;
3389
+ var sim = lines[i].match(/a=ssrc-group:SIM (\d+) (\d+) (\d+)/);
3390
+ if(sim) {
3391
+ Janus.warn("The SDP already contains a SIM attribute, munging will be skipped");
3392
+ return sdp;
3393
+ }
3394
+ var fid = lines[i].match(/a=ssrc-group:FID (\d+) (\d+)/);
3395
+ if(fid) {
3396
+ ssrc[0] = fid[1];
3397
+ ssrc_fid[0] = fid[2];
3398
+ lines.splice(i, 1); i--;
3399
+ continue;
3400
+ }
3401
+ if(ssrc[0]) {
3402
+ var match = lines[i].match('a=ssrc:' + ssrc[0] + ' cname:(.+)')
3403
+ if(match) {
3404
+ cname = match[1];
3405
+ }
3406
+ match = lines[i].match('a=ssrc:' + ssrc[0] + ' msid:(.+)')
3407
+ if(match) {
3408
+ msid = match[1];
3409
+ }
3410
+ match = lines[i].match('a=ssrc:' + ssrc[0] + ' mslabel:(.+)')
3411
+ if(match) {
3412
+ mslabel = match[1];
3413
+ }
3414
+ match = lines[i].match('a=ssrc:' + ssrc[0] + ' label:(.+)')
3415
+ if(match) {
3416
+ label = match[1];
3417
+ }
3418
+ if(lines[i].indexOf('a=ssrc:' + ssrc_fid[0]) === 0) {
3419
+ lines.splice(i, 1); i--;
3420
+ continue;
3421
+ }
3422
+ if(lines[i].indexOf('a=ssrc:' + ssrc[0]) === 0) {
3423
+ lines.splice(i, 1); i--;
3424
+ continue;
3425
+ }
3426
+ }
3427
+ if(lines[i].length == 0) {
3428
+ lines.splice(i, 1); i--;
3429
+ continue;
3430
+ }
3431
+ }
3432
+ if(ssrc[0] < 0) {
3433
+ // Couldn't find a FID attribute, let's just take the first video SSRC we find
3434
+ insertAt = -1;
3435
+ video = false;
3436
+ for(let i=0; i<lines.length; i++) {
3437
+ const mline = lines[i].match(/m=(\w+) */);
3438
+ if(mline) {
3439
+ const medium = mline[1];
3440
+ if(medium === "video") {
3441
+ // New video m-line: make sure it's the first one
3442
+ if(ssrc[0] < 0) {
3443
+ video = true;
3444
+ } else {
3445
+ // We're done, let's add the new attributes here
3446
+ insertAt = i;
3447
+ break;
3448
+ }
3449
+ } else {
3450
+ // New non-video m-line: do we have what we were looking for?
3451
+ if(ssrc[0] > -1) {
3452
+ // We're done, let's add the new attributes here
3453
+ insertAt = i;
3454
+ break;
3455
+ }
3456
+ }
3457
+ continue;
3458
+ }
3459
+ if(!video)
3460
+ continue;
3461
+ if(ssrc[0] < 0) {
3462
+ var value = lines[i].match(/a=ssrc:(\d+)/);
3463
+ if(value) {
3464
+ ssrc[0] = value[1];
3465
+ lines.splice(i, 1); i--;
3466
+ continue;
3467
+ }
3468
+ } else {
3469
+ let match = lines[i].match('a=ssrc:' + ssrc[0] + ' cname:(.+)')
3470
+ if(match) {
3471
+ cname = match[1];
3472
+ }
3473
+ match = lines[i].match('a=ssrc:' + ssrc[0] + ' msid:(.+)')
3474
+ if(match) {
3475
+ msid = match[1];
3476
+ }
3477
+ match = lines[i].match('a=ssrc:' + ssrc[0] + ' mslabel:(.+)')
3478
+ if(match) {
3479
+ mslabel = match[1];
3480
+ }
3481
+ match = lines[i].match('a=ssrc:' + ssrc[0] + ' label:(.+)')
3482
+ if(match) {
3483
+ label = match[1];
3484
+ }
3485
+ if(lines[i].indexOf('a=ssrc:' + ssrc_fid[0]) === 0) {
3486
+ lines.splice(i, 1); i--;
3487
+ continue;
3488
+ }
3489
+ if(lines[i].indexOf('a=ssrc:' + ssrc[0]) === 0) {
3490
+ lines.splice(i, 1); i--;
3491
+ continue;
3492
+ }
3493
+ }
3494
+ if(lines[i].length === 0) {
3495
+ lines.splice(i, 1); i--;
3496
+ continue;
3497
+ }
3498
+ }
3499
+ }
3500
+ if(ssrc[0] < 0) {
3501
+ // Still nothing, let's just return the SDP we were asked to munge
3502
+ Janus.warn("Couldn't find the video SSRC, simulcasting NOT enabled");
3503
+ return sdp;
3504
+ }
3505
+ if(insertAt < 0) {
3506
+ // Append at the end
3507
+ insertAt = lines.length;
3508
+ }
3509
+ // Generate a couple of SSRCs (for retransmissions too)
3510
+ // Note: should we check if there are conflicts, here?
3511
+ ssrc[1] = Math.floor(Math.random()*0xFFFFFFFF);
3512
+ ssrc[2] = Math.floor(Math.random()*0xFFFFFFFF);
3513
+ ssrc_fid[1] = Math.floor(Math.random()*0xFFFFFFFF);
3514
+ ssrc_fid[2] = Math.floor(Math.random()*0xFFFFFFFF);
3515
+ // Add attributes to the SDP
3516
+ for(var i=0; i<ssrc.length; i++) {
3517
+ if(cname) {
3518
+ lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' cname:' + cname);
3519
+ insertAt++;
3520
+ }
3521
+ if(msid) {
3522
+ lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' msid:' + msid);
3523
+ insertAt++;
3524
+ }
3525
+ if(mslabel) {
3526
+ lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' mslabel:' + mslabel);
3527
+ insertAt++;
3528
+ }
3529
+ if(label) {
3530
+ lines.splice(insertAt, 0, 'a=ssrc:' + ssrc[i] + ' label:' + label);
3531
+ insertAt++;
3532
+ }
3533
+ // Add the same info for the retransmission SSRC
3534
+ if(cname) {
3535
+ lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' cname:' + cname);
3536
+ insertAt++;
3537
+ }
3538
+ if(msid) {
3539
+ lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' msid:' + msid);
3540
+ insertAt++;
3541
+ }
3542
+ if(mslabel) {
3543
+ lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' mslabel:' + mslabel);
3544
+ insertAt++;
3545
+ }
3546
+ if(label) {
3547
+ lines.splice(insertAt, 0, 'a=ssrc:' + ssrc_fid[i] + ' label:' + label);
3548
+ insertAt++;
3549
+ }
3550
+ }
3551
+ lines.splice(insertAt, 0, 'a=ssrc-group:FID ' + ssrc[2] + ' ' + ssrc_fid[2]);
3552
+ lines.splice(insertAt, 0, 'a=ssrc-group:FID ' + ssrc[1] + ' ' + ssrc_fid[1]);
3553
+ lines.splice(insertAt, 0, 'a=ssrc-group:FID ' + ssrc[0] + ' ' + ssrc_fid[0]);
3554
+ lines.splice(insertAt, 0, 'a=ssrc-group:SIM ' + ssrc[0] + ' ' + ssrc[1] + ' ' + ssrc[2]);
3555
+ sdp = lines.join("\r\n");
3556
+ if(!sdp.endsWith("\r\n"))
3557
+ sdp += "\r\n";
3558
+ return sdp;
3559
+ }
3560
+
3561
+ // Helper methods to parse a media object
3562
+ function isAudioSendEnabled(media) {
3563
+ Janus.debug("isAudioSendEnabled:", media);
3564
+ if(!media)
3565
+ return true; // Default
3566
+ if(media.audio === false)
3567
+ return false; // Generic audio has precedence
3568
+ if(media.audioSend === undefined || media.audioSend === null)
3569
+ return true; // Default
3570
+ return (media.audioSend === true);
3571
+ }
3572
+
3573
+ function isAudioSendRequired(media) {
3574
+ Janus.debug("isAudioSendRequired:", media);
3575
+ if(!media)
3576
+ return false; // Default
3577
+ if(media.audio === false || media.audioSend === false)
3578
+ return false; // If we're not asking to capture audio, it's not required
3579
+ if(media.failIfNoAudio === undefined || media.failIfNoAudio === null)
3580
+ return false; // Default
3581
+ return (media.failIfNoAudio === true);
3582
+ }
3583
+
3584
+ function isAudioRecvEnabled(media) {
3585
+ Janus.debug("isAudioRecvEnabled:", media);
3586
+ if(!media)
3587
+ return true; // Default
3588
+ if(media.audio === false)
3589
+ return false; // Generic audio has precedence
3590
+ if(media.audioRecv === undefined || media.audioRecv === null)
3591
+ return true; // Default
3592
+ return (media.audioRecv === true);
3593
+ }
3594
+
3595
+ function isVideoSendEnabled(media) {
3596
+ Janus.debug("isVideoSendEnabled:", media);
3597
+ if(!media)
3598
+ return true; // Default
3599
+ if(media.video === false)
3600
+ return false; // Generic video has precedence
3601
+ if(media.videoSend === undefined || media.videoSend === null)
3602
+ return true; // Default
3603
+ return (media.videoSend === true);
3604
+ }
3605
+
3606
+ function isVideoSendRequired(media) {
3607
+ Janus.debug("isVideoSendRequired:", media);
3608
+ if(!media)
3609
+ return false; // Default
3610
+ if(media.video === false || media.videoSend === false)
3611
+ return false; // If we're not asking to capture video, it's not required
3612
+ if(media.failIfNoVideo === undefined || media.failIfNoVideo === null)
3613
+ return false; // Default
3614
+ return (media.failIfNoVideo === true);
3615
+ }
3616
+
3617
+ function isVideoRecvEnabled(media) {
3618
+ Janus.debug("isVideoRecvEnabled:", media);
3619
+ if(!media)
3620
+ return true; // Default
3621
+ if(media.video === false)
3622
+ return false; // Generic video has precedence
3623
+ if(media.videoRecv === undefined || media.videoRecv === null)
3624
+ return true; // Default
3625
+ return (media.videoRecv === true);
3626
+ }
3627
+
3628
+ function isScreenSendEnabled(media) {
3629
+ Janus.debug("isScreenSendEnabled:", media);
3630
+ if (!media)
3631
+ return false;
3632
+ if (typeof media.video !== 'object' || typeof media.video.mandatory !== 'object')
3633
+ return false;
3634
+ var constraints = media.video.mandatory;
3635
+ if (constraints.chromeMediaSource)
3636
+ return constraints.chromeMediaSource === 'desktop' || constraints.chromeMediaSource === 'screen';
3637
+ else if (constraints.mozMediaSource)
3638
+ return constraints.mozMediaSource === 'window' || constraints.mozMediaSource === 'screen';
3639
+ else if (constraints.mediaSource)
3640
+ return constraints.mediaSource === 'window' || constraints.mediaSource === 'screen';
3641
+ return false;
3642
+ }
3643
+
3644
+ function isDataEnabled(media) {
3645
+ Janus.debug("isDataEnabled:", media);
3646
+ if(Janus.webRTCAdapter.browserDetails.browser === "edge") {
3647
+ Janus.warn("Edge doesn't support data channels yet");
3648
+ return false;
3649
+ }
3650
+ if(media === undefined || media === null)
3651
+ return false; // Default
3652
+ return (media.data === true);
3653
+ }
3654
+
3655
+ function isTrickleEnabled(trickle) {
3656
+ Janus.debug("isTrickleEnabled:", trickle);
3657
+ return (trickle === false) ? false : true;
3658
+ }
3659
+ }
3660
+
3661
+ export default Janus