@reactoo/watchtogether-sdk-js 2.8.48 → 2.8.52

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.
@@ -1,3716 +0,0 @@
1
- // Watch together janus webrtc library
2
-
3
- import adapter from 'webrtc-adapter';
4
- import emitter from './wt-emitter';
5
- import {decodeJanusDisplay, generateUUID, maxJitter, median, wait} from "../models/utils";
6
-
7
- class Room {
8
-
9
- static noop() {
10
-
11
- }
12
-
13
- constructor(debug) {
14
- this.debug = debug;
15
- this.sessions = [];
16
- this.safariVp8 = false;
17
- this.browser = adapter.browserDetails.browser;
18
- this.browserDetails = adapter.browserDetails;
19
- this.webrtcSupported = Room.isWebrtcSupported();
20
- this.safariVp8TestPromise = Room.testSafariVp8();
21
- this.safariVp8 = null;
22
- this.safariVp8TestPromise.then(safariVp8 => {
23
- this.safariVp8 = safariVp8;
24
- })
25
-
26
- this._log = Room.noop;
27
- if (this.debug) {
28
- this._enableDebug();
29
- }
30
-
31
- // Let's get it started
32
- this.whenInitialized = this.initialize();
33
- }
34
-
35
- _enableDebug() {
36
- this._log = console.log.bind(console);
37
- }
38
-
39
- initialize() {
40
- return this.safariVp8TestPromise
41
- .then(() => this);
42
- }
43
-
44
- createSession(constructId = null, type = 'reactooroom', options = {}) {
45
- return new RoomSession(constructId, type, {debug: this.debug, ...options});
46
- }
47
-
48
- static testSafariVp8() {
49
- return new Promise(resolve => {
50
- if (adapter.browserDetails.browser === 'safari' &&
51
- adapter.browserDetails.version >= 605) {
52
- if (RTCRtpSender && RTCRtpSender.getCapabilities && RTCRtpSender.getCapabilities("video") &&
53
- RTCRtpSender.getCapabilities("video").codecs && RTCRtpSender.getCapabilities("video").codecs.length) {
54
- var isVp8 = false;
55
- for (var i in RTCRtpSender.getCapabilities("video").codecs) {
56
- var codec = RTCRtpSender.getCapabilities("video").codecs[i];
57
- if (codec && codec.mimeType && codec.mimeType.toLowerCase() === "video/vp8") {
58
- isVp8 = true;
59
- break;
60
- }
61
- }
62
- resolve(isVp8);
63
- } else {
64
- // We do it in a very ugly way, as there's no alternative...
65
- // We create a PeerConnection to see if VP8 is in an offer
66
- var testpc = new RTCPeerConnection({}, {});
67
- testpc.createOffer({offerToReceiveVideo: true}).then(function (offer) {
68
- let result = offer.sdp.indexOf("VP8") !== -1;
69
- testpc.close();
70
- testpc = null;
71
- resolve(result);
72
- });
73
- }
74
- } else resolve(false);
75
- });
76
- }
77
-
78
- static isWebrtcSupported() {
79
- return window.RTCPeerConnection !== undefined && window.RTCPeerConnection !== null &&
80
- navigator.mediaDevices !== undefined && navigator.mediaDevices !== null &&
81
- navigator.mediaDevices.getUserMedia !== undefined && navigator.mediaDevices.getUserMedia !== null;
82
- }
83
- }
84
-
85
- class RoomSession {
86
-
87
- static noop() {
88
-
89
- }
90
-
91
- static randomString(len) {
92
- var charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
93
- var randomString = '';
94
- for (var i = 0; i < len; i++) {
95
- var randomPoz = Math.floor(Math.random() * charSet.length);
96
- randomString += charSet.substring(randomPoz, randomPoz + 1);
97
- }
98
- return randomString;
99
- }
100
-
101
- static sessionTypes = {
102
- 'reactooroom': 'janus.plugin.reactooroom',
103
- 'streaming': 'janus.plugin.streaming'
104
- }
105
-
106
- static userRoleSubscriptionRules = {
107
- participant: {
108
- "watchparty": ['participant', 'talkback'],
109
- "studio": ['participant', 'talkback', 'host', 'observer'],
110
- "commentary": ['participant', 'talkback', 'host'],
111
- "intercom": ['host', 'talkback', 'observer', 'observerSolo1', 'observerSolo2', 'observerSolo3', 'observerSolo4', 'observerSolo5'],
112
- "videowall": ['host', 'talkback', 'observer', 'observerSolo1', 'observerSolo2', 'observerSolo3', 'observerSolo4', 'observerSolo5'],
113
- "videowall-queue": ['host', 'talkback', 'observer', 'observerSolo1', 'observerSolo2', 'observerSolo3', 'observerSolo4', 'observerSolo5'],
114
- "videowall-queue-video": ['host', 'talkback', 'observer', 'observerSolo1', 'observerSolo2', 'observerSolo3', 'observerSolo4', 'observerSolo5']
115
- },
116
- monitor: {
117
- "watchparty": ['participant', 'host'],
118
- "studio": ['participant', 'host', 'observer'],
119
- "commentary": ['participant', 'host'],
120
- "intercom": ['host', 'participant'],
121
- "videowall": ['host', 'participant'],
122
- "videowall-queue": ['host', 'participant'],
123
- "videowall-queue-video": ['host', 'participant'],
124
- },
125
- talkback: {
126
- "watchparty": ['participant', 'host', 'talkback'],
127
- "studio": ['participant', 'host', 'observer', 'talkback'],
128
- "commentary": ['host', 'participant', 'talkback'],
129
- "intercom": ['host', 'participant', 'talkback'],
130
- "videowall": ['host', 'participant', 'talkback'],
131
- "videowall-queue": ['host', 'participant', 'talkback'],
132
- "videowall-queue-video": ['host', 'participant', 'talkback'],
133
- },
134
- observer: {
135
- "watchparty": ['participant'],
136
- "studio": ['participant'],
137
- "commentary": ['participant'],
138
- "intercom": ['participant'],
139
- "videowall": ['participant'],
140
- "videowall-queue": ['participant'],
141
- "videowall-queue-video": ['participant'],
142
- },
143
- observerSolo1: {
144
- "watchparty": ['participant'],
145
- "studio": ['participant'],
146
- "commentary": ['participant'],
147
- "intercom": ['participant'],
148
- "videowall": ['participant'],
149
- "videowall-queue": ['participant'],
150
- "videowall-queue-video": ['participant'],
151
- },
152
- observerSolo2: {
153
- "watchparty": ['participant'],
154
- "studio": ['participant'],
155
- "commentary": ['participant'],
156
- "intercom": ['participant'],
157
- "videowall": ['participant'],
158
- "videowall-queue": ['participant'],
159
- "videowall-queue-video": ['participant'],
160
- },
161
- observerSolo3: {
162
- "watchparty": ['participant'],
163
- "studio": ['participant'],
164
- "commentary": ['participant'],
165
- "intercom": ['participant'],
166
- "videowall": ['participant'],
167
- "videowall-queue": ['participant'],
168
- "videowall-queue-video": ['participant'],
169
- },
170
- observerSolo4: {
171
- "watchparty": ['participant'],
172
- "studio": ['participant'],
173
- "commentary": ['participant'],
174
- "intercom": ['participant'],
175
- "videowall": ['participant'],
176
- "videowall-queue": ['participant'],
177
- "videowall-queue-video": ['participant'],
178
- },
179
- observerSolo5: {
180
- "watchparty": ['participant'],
181
- "studio": ['participant'],
182
- "commentary": ['participant'],
183
- "intercom": ['participant'],
184
- "videowall": ['participant'],
185
- "videowall-queue": ['participant'],
186
- "videowall-queue-video": ['participant'],
187
- },
188
- host: {
189
- "watchparty": [],
190
- "studio": [],
191
- "commentary": [],
192
- "intercom": [],
193
- "videowall": [],
194
- "videowall-queue": [],
195
- "videowall-queue-video": [],
196
- },
197
- companionTV: {},
198
- companionPhone: {},
199
- };
200
-
201
- constructor(constructId = null, type = 'reactooroom', options = {}) {
202
-
203
- Object.assign(this, emitter());
204
- this.options = {...options};
205
- this.defaultDataChannelLabel = 'JanusDataChannel'
206
- this.server = null;
207
- this.iceServers = null;
208
- this.token = null;
209
- this.roomId = null;
210
- this.streamId = null;
211
- this.pin = null;
212
- this.userId = null;
213
- this.sessiontype = type;
214
- this.initialBitrate = 0;
215
- this.enableDtx = false;
216
- this.simulcast = false;
217
- this.defaultSimulcastSettings = {
218
- "default" : {
219
- mode: "controlled", // controlled, manual, browserControlled
220
- defaultSubstream: 0, // 2 lowest quality, 0 highest quality
221
- bitrates: [
222
- {
223
- "rid": "l",
224
- "active": true,
225
- "maxBitrate": 180000,
226
- "maxFramerate": 20,
227
- "scaleResolutionDownBy": 3.3333333333333335,
228
- "priority": "low"
229
- },
230
- {
231
- "rid": "m",
232
- "active": true,
233
- "maxBitrate": 500000,
234
- "maxFramerate": 25,
235
- "scaleResolutionDownBy": 1.3333333333333335,
236
- "priority": "low"
237
- },
238
- {
239
- "rid": "h",
240
- "active": true,
241
- "maxBitrate": 2000000,
242
- "maxFramerate": 30,
243
- "priority": "low"
244
- }
245
- ]
246
- },
247
- };
248
- this.recordingFilename = null;
249
- this.pluginName = RoomSession.sessionTypes[type];
250
- this.id = null;
251
- this.privateId = null;
252
- this.constructId = constructId || RoomSession.randomString(16);
253
- this.sessionId = null;
254
- this.apisecret = null;
255
- this.handleId = null;
256
- this.ws = null;
257
-
258
- // helper flags
259
-
260
- this.isSupposeToBeConnected = false;
261
- this.isRestarting = false;
262
- this.isConnecting = false;
263
- this.isDisconnecting = false;
264
- this.isConnected = false;
265
-
266
- // we added this because joined event that enables "isConnected" happened after we fired publish (long polling)
267
- // as a result publish was rejected due to isConnected flag being false
268
- // im' not sure what relies on current event based isConnected flag so i've added a new one that is set after join promise resolves
269
- this.isPossibleToPublish = false;
270
-
271
- this.isPublished = false;
272
- this.isStreaming = false;
273
- this.isMuted = [];
274
- this.isVideoEnabled = false;
275
- this.isAudioEnabed = false;
276
- this._statsMaxLength = 31;
277
- this._upStatsLength = 10;
278
- this._downStatsLength = 2;
279
- this._statsTimeoutStopped = true;
280
- this._statsTimeoutId = null;
281
- this._statsInterval = 3000;
282
- this._aqInterval = 2500;
283
- this._aqIntervalCounter = 0;
284
- this._aqIntervalDivisor = 4;
285
- this._aqTimeoutId = null;
286
- this._sendMessageTimeout = 10000;
287
- this._retries = 0;
288
- this._maxRetries = 5;
289
- this._keepAlivePeriod = 25000;
290
- this._longPollTimeout = 60000;
291
- this._maxev = 10;
292
- this._keepAliveId = null;
293
- this._participants = [];
294
- this._restrictSubscribeToUserIds = []; // all if empty
295
- this._talkIntercomChannels = ['participants'];
296
- this._listenIntercomChannels = ['participants'];
297
- this._roomType = 'watchparty';
298
- this._isDataChannelOpen = false;
299
- this._abortController = null;
300
- this._remoteUsersCache = [];
301
-
302
- this.userRoleSubscriptionRules = {
303
- ...RoomSession.userRoleSubscriptionRules,
304
- ...(this.options.userRoleSubscriptionRules || {})
305
- }
306
-
307
- this._log = RoomSession.noop;
308
- if (this.options.debug) {
309
- this._enableDebug();
310
- }
311
- }
312
-
313
- _httpAPICall = function(url, options) {
314
- return new Promise((resolve, reject) => {
315
- const xhr = new XMLHttpRequest();
316
-
317
- xhr.open(options.verb || 'POST', url);
318
-
319
- // Set default headers
320
- xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
321
- if (options.verb === "POST") {
322
- xhr.setRequestHeader('Content-Type', 'application/json');
323
- }
324
-
325
- // Handle credentials
326
- if (options.withCredentials) {
327
- xhr.withCredentials = true;
328
- }
329
-
330
- // Set timeout if specified
331
- if (options.timeout) {
332
- xhr.timeout = options.timeout;
333
- }
334
-
335
- // Setup handlers
336
- xhr.onload = function() {
337
- if (xhr.status >= 200 && xhr.status < 300) {
338
- let response;
339
- try {
340
- response = JSON.parse(xhr.responseText);
341
- } catch(e) {
342
- if (options.error) {
343
- options.error('Invalid JSON', xhr.responseText);
344
- }
345
- reject('Invalid JSON');
346
- return;
347
- }
348
- if (options.success) {
349
- options.success(response);
350
- }
351
- resolve(response);
352
- } else {
353
- let errorText = xhr.statusText || 'HTTP Error';
354
- if (options.error) {
355
- options.error(errorText, xhr.responseText);
356
- }
357
- reject(errorText);
358
- }
359
- };
360
-
361
- xhr.onerror = function() {
362
- let errorText = 'Network error';
363
- if (options.error) {
364
- options.error(errorText, 'Is the server down?');
365
- }
366
- reject(errorText);
367
- };
368
-
369
- xhr.ontimeout = function() {
370
- let errorText = 'Request timed out';
371
- if (options.error) {
372
- options.error(errorText, 'Is the server down?');
373
- }
374
- reject(errorText);
375
- };
376
-
377
- // Send request
378
- try {
379
- if (options.body) {
380
- xhr.send(JSON.stringify(options.body));
381
- } else {
382
- xhr.send();
383
- }
384
- } catch(e) {
385
- if (options.error) {
386
- options.error('Error sending request', e);
387
- }
388
- reject(e);
389
- }
390
- });
391
- }
392
-
393
- _pushToRemoteUsersCache(userId, streams, id) {
394
- const existingIndex = this._remoteUsersCache.findIndex(u => u.userId === userId);
395
- if (existingIndex > -1) {
396
- this._remoteUsersCache.splice(existingIndex, 1, {userId, streams, id})
397
- } else {
398
- this._remoteUsersCache.push({userId, streams, id});
399
- }
400
- }
401
-
402
- _removeFromRemoteUsersCache(rfid) {
403
- const existingIndex = this._remoteUsersCache.findIndex(u => u.id === rfid);
404
- if (existingIndex > -1) {
405
- this._remoteUsersCache.splice(existingIndex, 1);
406
- }
407
- }
408
-
409
- _participantShouldSubscribe(userId) {
410
- const myUser = decodeJanusDisplay(this.display);
411
- const remoteUser = decodeJanusDisplay(userId);
412
- let localUserRole = myUser?.role || 'participant';
413
- let remoteUserRole = remoteUser?.role || 'participant';
414
- return this.userRoleSubscriptionRules[localUserRole][(this._roomType || 'watchparty')].indexOf(remoteUserRole) > -1
415
- && (this._restrictSubscribeToUserIds.length === 0 || this._restrictSubscribeToUserIds.indexOf(remoteUser?.userId) > -1);
416
- }
417
-
418
- _intercomSubscribe = (stream) => {
419
- if(stream.type === 'data') {
420
- return true;
421
- }
422
- const intercomGroups = JSON.parse(stream.description || "[]")?.intercomGroups || [];
423
- return intercomGroups.some(g => this._listenIntercomChannels.indexOf(g) > -1);
424
- };
425
-
426
- _getAddParticipantEventName(handleId) {
427
-
428
- let handle = this._getHandle(handleId);
429
- if (!handle) {
430
- this.emit('error', {
431
- type: 'warning',
432
- id: 15,
433
- message: 'id non-existent',
434
- data: [handleId, 'getParticipantEventName']
435
- });
436
- return;
437
- }
438
-
439
- const participantRole = decodeJanusDisplay(handle.userId)?.role;
440
- switch (participantRole) {
441
- case 'participant':
442
- return 'addRemoteParticipant';
443
- case 'talkback':
444
- return 'addRemoteTalkback';
445
- case 'monitor':
446
- return 'addRemoteTalkback';
447
- case 'observer':
448
- case 'observerSolo1':
449
- case 'observerSolo2':
450
- case 'observerSolo3':
451
- case 'observerSolo4':
452
- case 'observerSolo5':
453
- return 'addRemoteObserver';
454
- case 'host':
455
- return 'addRemoteInstructor';
456
- case 'companionTV':
457
- return 'addRemoteCompanionTV';
458
- case 'companionPhone':
459
- return 'addRemoteCompanionPhone';
460
- default:
461
- return 'addRemoteParticipant';
462
- }
463
- }
464
-
465
- _getRemoveParticipantEventName(handleId) {
466
-
467
- let handle = this._getHandle(handleId);
468
- if (!handle) {
469
- this.emit('error', {
470
- type: 'warning',
471
- id: 15,
472
- message: 'id non-existent',
473
- data: [handleId, 'getParticipantEventName']
474
- });
475
- return;
476
- }
477
-
478
- const participantRole = decodeJanusDisplay(handle.userId)?.role;
479
- switch (participantRole) {
480
- case 'participant':
481
- return 'removeRemoteParticipant';
482
- case 'talkback':
483
- return 'removeRemoteTalkback';
484
- case 'monitor':
485
- return 'removeRemoteTalkback';
486
- case 'observer':
487
- case 'observerSolo1':
488
- case 'observerSolo2':
489
- case 'observerSolo3':
490
- case 'observerSolo4':
491
- case 'observerSolo5':
492
- return 'removeRemoteObserver';
493
- case 'host':
494
- return 'removeRemoteInstructor';
495
- case 'companionTV':
496
- return 'removeRemoteCompanionTV';
497
- case 'companionPhone':
498
- return 'removeRemoteCompanionPhone';
499
- default:
500
- return 'removeRemoteParticipant';
501
- }
502
- }
503
-
504
- sendMessage(handleId, message = {body: 'Example Body'}, dontWait = false, dontResolveOnAck = false, retry = 0) {
505
- return this._send({
506
- "janus": "message",
507
- "handle_id": handleId,
508
- ...message
509
- }, dontWait, dontResolveOnAck, retry)
510
- .then(json => {
511
- if (json && json["janus"] === "success") {
512
- let plugindata = json["plugindata"] || {};
513
- let data = plugindata["data"];
514
- return Promise.resolve(data);
515
- }
516
- return Promise.resolve();
517
- })
518
- .catch(json => {
519
- if (json && json["error"]) {
520
- return Promise.reject({type: 'warning', id: 1, message: 'sendMessage failed', data: json["error"]})
521
- } else {
522
- return Promise.reject({type: 'warning', id: 1, message: 'sendMessage failed', data: json});
523
- }
524
- })
525
- }
526
-
527
- _sendHTTP(request = {}, ignoreResponse = false, dontResolveOnAck = false, retry = 0) {
528
- let transaction = RoomSession.randomString(12);
529
- let requestData = {
530
- ...request,
531
- transaction,
532
- token: this.token,
533
- ...((this.sessionId && {'session_id': this.sessionId}) || {}),
534
- ...((this.apisecret && {'apisecret': this.apisecret}) || {})
535
- };
536
- this._log(requestData);
537
- return this._httpAPICall(this.server, {body:requestData})
538
- .then(json => {
539
- if(json['janus'] !== 'success' && json['janus'] !== 'ack') {
540
- // TODO: check if this can stay like this
541
- // not a success not an ack ... should be error
542
- return Promise.reject(json["error"])
543
- }
544
- return json;
545
- })
546
- }
547
- _sendWebsockets(request = {}, ignoreResponse = false, dontResolveOnAck = false, retry = 0) {
548
- let transaction = RoomSession.randomString(12);
549
- let requestData = {
550
- ...request,
551
- transaction,
552
- token: this.token,
553
- ...((this.sessionId && {'session_id': this.sessionId}) || {}),
554
- ...((this.apisecret && {'apisecret': this.apisecret}) || {})
555
- };
556
- this._log(requestData);
557
- const op = () => new Promise((resolve, reject) => {
558
- let messageTimeoutId = null;
559
- let abortResponse = () => {
560
- this._abortController.signal.removeEventListener('abort', abortResponse);
561
- clearTimeout(messageTimeoutId);
562
- this.ws.removeEventListener('message', parseResponse);
563
- reject({type: 'warning', id: 17, message: 'connection cancelled'})
564
- };
565
-
566
- let parseResponse = (event) => {
567
- let json = JSON.parse(event.data);
568
- let r_transaction = json['transaction'];
569
- if (r_transaction === transaction && (!dontResolveOnAck || json['janus'] !== 'ack')) {
570
- clearTimeout(messageTimeoutId);
571
- this._abortController.signal.removeEventListener('abort', abortResponse);
572
- this.ws.removeEventListener('message', parseResponse);
573
- if (json['janus'] === 'error') {
574
- if (json?.error?.code == 403) {
575
- this.disconnect(true);
576
- }
577
- reject({type: 'error', id: 2, message: 'send failed', data: json, requestData});
578
- } else {
579
- resolve(json);
580
- }
581
- }
582
- };
583
-
584
- if (ignoreResponse) {
585
- if (this.ws && this.ws.readyState === 1) {
586
- this.ws.send(JSON.stringify(requestData));
587
- }
588
- resolve();
589
- } else {
590
- if (this.ws && this.ws.readyState === 1) {
591
- this.ws.addEventListener('message', parseResponse);
592
- messageTimeoutId = setTimeout(() => {
593
- this.ws.removeEventListener('message', parseResponse);
594
- this._abortController.signal.removeEventListener('abort', abortResponse);
595
- reject({type: 'warning', id: 3, message: 'send timeout', data: requestData});
596
- }, this._sendMessageTimeout);
597
- this._abortController.signal.addEventListener('abort', abortResponse);
598
- this.ws.send(JSON.stringify(requestData));
599
- } else {
600
- reject({type: 'warning', id: 29, message: 'No connection to WebSockets', data: requestData});
601
- }
602
- }
603
- })
604
-
605
- return op().catch(e => {
606
- if (e.id === 17) {
607
- return Promise.reject(e);
608
- }
609
- else if(e.id === 29 && retry > 0) {
610
- return wait(this._sendMessageTimeout).then(() => this._send(request, ignoreResponse, dontResolveOnAck, retry - 1));
611
- }
612
- else if(retry > 0) {
613
- return this._send(request, ignoreResponse, dontResolveOnAck, retry - 1);
614
- }
615
- else {
616
- return Promise.reject(e);
617
- }
618
- })
619
- }
620
- _send(request = {}, ignoreResponse = false, dontResolveOnAck = false, retry = 0) {
621
- if(this.useWebsockets) {
622
- return this._sendWebsockets(request, ignoreResponse, dontResolveOnAck, retry);
623
- }
624
- else {
625
- return this._sendHTTP(request, ignoreResponse, dontResolveOnAck, retry)
626
- }
627
- }
628
-
629
- _longPoll() {
630
-
631
- if(!this.isSupposeToBeConnected) {
632
- return;
633
- }
634
-
635
-
636
- let longpoll = this.server + "/" + this.sessionId + "?rid=" + new Date().getTime();
637
- if(this._maxev)
638
- longpoll = longpoll + "&maxev=" + this._maxev;
639
- if(this.token)
640
- longpoll = longpoll + "&token=" + encodeURIComponent(this.token);
641
- if(this.apisecret)
642
- longpoll = longpoll + "&apisecret=" + encodeURIComponent(this.apisecret);
643
-
644
- this._httpAPICall(longpoll, {
645
- verb: 'GET',
646
- timeout: this._longPollTimeout
647
- })
648
- .then(this._handleWsEvents.bind(this))
649
- .catch(() => {
650
-
651
- this._retries++;
652
- if(this._retries > this._maxRetries) {
653
- if (this.sessiontype === 'reactooroom') {
654
- this.disconnect(true);
655
- } else if (this.sessiontype === 'streaming') {
656
- this.stopStream();
657
- }
658
- this.emit('error', {type: 'error', id: 33, message: 'Lost connection to server', data: null});
659
- }
660
-
661
- this._longPoll();
662
- })
663
- }
664
-
665
- _connectionClosed() {
666
-
667
- if (!this.isConnected || this.isConnecting || this.isDisconnecting) {
668
- return;
669
- }
670
-
671
- if (this._retries < this._maxRetries) {
672
- //TODO: clear this timeout
673
- setTimeout(() => {
674
- this._retries++;
675
- this._reconnect().catch(e => {
676
- this.emit('error', e)
677
- });
678
- }, 3000 * this._retries);
679
- } else {
680
- if (this.sessiontype === 'reactooroom') {
681
- this.disconnect(true);
682
- } else if (this.sessiontype === 'streaming') {
683
- this.stopStream();
684
- }
685
-
686
- this.emit('error', {type: 'error', id: 4, message: 'Lost connection to WebSockets', data: null});
687
- }
688
- }
689
-
690
- _wipeListeners() {
691
- if (this.ws) {
692
- this.ws.removeEventListener('close', this.__connectionClosedBoundFn);
693
- this.ws.removeEventListener('message', this.__handleWsEventsBoundFn);
694
- }
695
- }
696
-
697
- _startKeepAlive() {
698
- this._send({"janus": "keepalive"})
699
- .then(json => {
700
- if (json["janus"] !== 'ack') {
701
- this.emit('error', {
702
- type: 'warning',
703
- id: 5,
704
- message: 'keepalive response suspicious',
705
- data: json["janus"]
706
- });
707
- }
708
- })
709
- .catch((e) => {
710
- this.emit('error', {type: 'warning', id: 6, message: 'keepalive dead', data: e});
711
- this._connectionClosed();
712
- });
713
-
714
- this._keepAliveId = setTimeout(() => {
715
- this._startKeepAlive();
716
- }, this._keepAlivePeriod);
717
- }
718
-
719
- _stopKeepAlive() {
720
- clearTimeout(this._keepAliveId);
721
- }
722
-
723
- _handleWsEvents(event, skipPoll = false) {
724
-
725
- this._retries = 0;
726
-
727
- // we have a response from long poll, that means we need to run it again
728
- if(!this.useWebsockets && !skipPoll) {
729
- this._longPoll();
730
- }
731
-
732
- if(!this.useWebsockets && skipPoll) {
733
- // we need to fire those events into the wild
734
- this.emit('longPollEvent', event);
735
- }
736
-
737
- if(Array.isArray(event)) {
738
- event.forEach(ev => this._handleWsEvents({data:ev}, true));
739
- return;
740
- }
741
-
742
- let json = typeof event.data === 'string'
743
- ? JSON.parse(event.data)
744
- : event.data;
745
-
746
- var sender = json["sender"];
747
- var type = json["janus"];
748
-
749
- let handle = this._getHandle(sender);
750
- if (!handle) {
751
- return;
752
- }
753
-
754
- if (type === "trickle") {
755
- let candidate = json["candidate"];
756
- let config = handle.webrtcStuff;
757
- if (config.pc && config.remoteSdp) {
758
- if (!candidate || candidate.completed === true) {
759
- config.pc.addIceCandidate(null);
760
- } else {
761
- config.pc.addIceCandidate(candidate);
762
- }
763
- } else {
764
- if (!config.candidates) {
765
- config.candidates = [];
766
- }
767
- config.candidates.push(candidate);
768
- }
769
- } else if (type === "webrtcup") {
770
- //none universal
771
- } else if (type === "hangup") {
772
- this._log('hangup on', handle.handleId, handle.handleId === this.handleId, json);
773
- this._removeParticipant(handle.handleId, null, false);
774
- } else if (type === "detached") {
775
- this._log('detached on', handle.handleId, handle.handleId === this.handleId, json);
776
- this._removeParticipant(handle.handleId, null, true);
777
- } else if (type === "media") {
778
- this._log('Media event:', handle.handleId, json["type"], json["receiving"], json["mid"]);
779
- } else if (type === "slowlink") {
780
- this._log('Slowlink', handle.handleId, json["uplink"], json["lost"], json["mid"]);
781
- } else if (type === "event") {
782
- //none universal
783
- } else if (type === 'timeout') {
784
- this._log('WebSockets Gateway timeout', json);
785
- this.ws.close(3504, "Gateway timeout");
786
- } else if (type === 'success' || type === 'error') {
787
- // we're capturing those elsewhere
788
- } else {
789
- this._log(`Unknown event: ${type} on session: ${this.sessionId}`);
790
- }
791
-
792
-
793
- // LOCAL
794
-
795
- if (sender === this.handleId) {
796
- if (type === "event") {
797
-
798
- var plugindata = json["plugindata"] || {};
799
- var msg = plugindata["data"] || {};
800
- var jsep = json["jsep"];
801
- let result = msg["result"] || null;
802
- let event = msg["videoroom"] || null;
803
- let list = msg["publishers"] || {};
804
- let leaving = msg["leaving"];
805
- let kicked = msg["kicked"];
806
- let substream = msg["substream"];
807
- let temporal = msg["temporal"];
808
-
809
- //let joining = msg["joining"];
810
- let unpublished = msg["unpublished"];
811
- let error = msg["error"];
812
-
813
- if (event === "joined") {
814
- this.id = msg["id"];
815
- this.privateId = msg["private_id"];
816
- this.isConnected = true;
817
- this.isPossibleToPublish = true;
818
- this._log('We have successfully joined Room');
819
- this.emit('joined', true, this.constructId);
820
- this.emit('addLocalParticipant', {
821
- tid: generateUUID(),
822
- id: handle.handleId,
823
- constructId: this.constructId,
824
- userId: decodeJanusDisplay(handle.userId)?.userId,
825
- role: decodeJanusDisplay(this.display)?.role,
826
- track: null,
827
- stream: null,
828
- streamMap: {},
829
- source: null,
830
- adding: false,
831
- removing: false,
832
- hasAudioTrack: false,
833
- hasVideoTrack: false,
834
- });
835
- for (let f in list) {
836
- let userId = list[f]["display"];
837
- let streams = list[f]["streams"] || [];
838
- let id = list[f]["id"];
839
- for (let i in streams) {
840
- streams[i]["id"] = id;
841
- streams[i]["display"] = userId;
842
- }
843
- this._log('Remote userId: ', userId);
844
- this._pushToRemoteUsersCache(userId, streams, id);
845
- if (this._participantShouldSubscribe(userId)) {
846
- this._log('Creating user: ', userId);
847
- this._createParticipant(userId, id)
848
- .then(handle => {
849
- this._updateParticipantsTrackData(handle.handleId, streams);
850
- const subscribe = streams.filter(s => !s.disabled && this._intercomSubscribe(s)).map(stream => ({feed: stream.id, mid: stream.mid}));
851
- handle.webrtcStuff.subscribeMap = structuredClone(subscribe);
852
- return this.sendMessage(handle.handleId, {
853
- body: {
854
- "request": "join",
855
- "room": this.roomId,
856
- "ptype": "subscriber",
857
- "private_id": this.privateId,
858
- streams: subscribe,
859
- //"feed": id,
860
- ...(this.webrtcVersion > 1000 ? {id: this.userId} : {}),
861
- pin: this.pin
862
- }
863
- })
864
- })
865
- .catch(err => {
866
- this.emit('error', err);
867
- })
868
- }
869
- }
870
- }
871
- else if (event === "event") {
872
-
873
- if(substream !== undefined && substream !== null) {
874
- this._log('Substream event:', substream, sender);
875
- }
876
-
877
- if(temporal !== undefined && temporal !== null) {
878
- this._log('Temporal event:', temporal);
879
- }
880
-
881
- if (msg["streams"] !== undefined && msg["streams"] !== null) {
882
- this._log('Got my own streams back', msg["streams"]);
883
- }
884
-
885
- for (let f in list) {
886
-
887
- let userId = list[f]["display"];
888
- let streams = list[f]["streams"] || [];
889
- let id = list[f]["id"];
890
- for (let i in streams) {
891
- streams[i]["id"] = id;
892
- streams[i]["display"] = userId;
893
- }
894
- this._log('Remote userId: ', userId);
895
-
896
- this._pushToRemoteUsersCache(userId, streams, id);
897
-
898
- if (this._participantShouldSubscribe(userId)) {
899
-
900
- let handle = this._getHandle(null, id);
901
- if(handle) {
902
-
903
- this._updateParticipantsTrackData(handle.handleId, streams)
904
-
905
- let subscribe = streams
906
- .filter(stream => !stream.disabled && this._intercomSubscribe(stream) && !this._isAlreadySubscribed(handle.handleId, stream.id, stream.mid))
907
- .map(s => ({feed: s.id, mid: s.mid}));
908
-
909
- let unsubscribe = streams.filter(stream => stream.disabled || !this._intercomSubscribe(stream))
910
- .map(s => ({feed: s.id, mid: s.mid}));
911
-
912
- this._updateSubscribeMap(handle.handleId, subscribe, unsubscribe);
913
-
914
- this._log('Already subscribed to user: ', userId, 'Update streams', subscribe, unsubscribe);
915
-
916
- if(subscribe.length || unsubscribe.length) {
917
- this.sendMessage(handle.handleId, {
918
- body: {
919
- "request": "update",
920
- ...(subscribe.length ? {subscribe}: {}),
921
- ...(unsubscribe.length ? {unsubscribe}: {})
922
- }
923
- })
924
- }
925
- }
926
- else {
927
- this._log('Creating user: ', userId, streams);
928
- this._createParticipant(userId, id)
929
- .then(handle => {
930
- this._updateParticipantsTrackData(handle.handleId, streams);
931
- const subscribe = streams.filter(s => !s.disabled && this._intercomSubscribe(s)).map(stream => ({feed: stream.id, mid: stream.mid}));
932
- handle.webrtcStuff.subscribeMap = structuredClone(subscribe);
933
- return this.sendMessage(handle.handleId, {
934
- body: {
935
- "request": "join",
936
- "room": this.roomId,
937
- "ptype": "subscriber",
938
- "private_id": this.privateId,
939
- streams: subscribe,
940
- ...(this.webrtcVersion > 1000 ? {id: this.userId} : {}),
941
- pin: this.pin
942
- }
943
- })
944
- })
945
- .catch(err => {
946
- this.emit('error', err);
947
- })
948
- }
949
- }
950
- }
951
-
952
- if (leaving === 'ok') {
953
- this._log('leaving', this.handleId, 'this is us');
954
- this._removeParticipant(this.handleId);
955
- if (msg['reason'] === 'kicked') {
956
- this.emit('kicked');
957
- this.disconnect().catch(() => {});
958
- }
959
- } else if (leaving) {
960
- //TODO: in 1 PeerConnection case we only unsubscribe from streams, this may not be true, check janus docs
961
- this._log('leaving', leaving);
962
- this._removeFromRemoteUsersCache(leaving);
963
- this._removeParticipant(null, leaving, true);
964
- }
965
-
966
- if (unpublished === 'ok') {
967
- this._log('unpublished', this.handleId, 'this is us');
968
- this._removeParticipant(this.handleId, null, false); // we do just hangup
969
- } else if (unpublished) {
970
- //TODO: in 1 PeerConnection case we only unsubscribe from streams
971
- this._log('unpublished', unpublished);
972
- this._removeFromRemoteUsersCache(unpublished);
973
- this._removeParticipant(null, unpublished, true); // we do hangup and detach
974
- }
975
-
976
- if(kicked === 'ok') {
977
- // this case shouldn't exist
978
- } else if(kicked) {
979
- this._log('kicked', kicked);
980
- this._removeFromRemoteUsersCache(kicked);
981
- this._removeParticipant(null, kicked, true); // we do hangup and detach
982
- }
983
-
984
- if (error) {
985
- this.emit('error', {
986
- type: 'error',
987
- id: 7,
988
- message: 'local participant error',
989
- data: [sender, msg]
990
- });
991
- }
992
- }
993
-
994
- // Streaming related
995
- else if (result && result["status"]) {
996
- this.emit('streamingStatus', result["status"]);
997
- if (result["status"] === 'stopped') {
998
- this.stopStream();
999
- }
1000
- if (result["status"] === 'started') {
1001
- this.emit('streaming', true);
1002
- this.isStreaming = true;
1003
- }
1004
- }
1005
-
1006
- if (jsep !== undefined && jsep !== null) {
1007
- if (this.sessiontype === 'reactooroom') {
1008
- this._webrtcPeer(this.handleId, jsep)
1009
- .catch(err => {
1010
- this.emit('error', err);
1011
- });
1012
- } else if (this.sessiontype === 'streaming') {
1013
- this._publishRemote(this.handleId, jsep)
1014
- .catch(err => {
1015
- this.emit('error', err);
1016
- });
1017
- }
1018
-
1019
- }
1020
- }
1021
- else if (type === "webrtcup") {
1022
-
1023
- if(this.simulcast) {
1024
- return;
1025
- }
1026
-
1027
- this._log('Configuring bitrate: ' + this.initialBitrate);
1028
- if (this.initialBitrate > 0) {
1029
- this.sendMessage(this.handleId, {
1030
- "body": {
1031
- "request": "configure",
1032
- "bitrate": this.initialBitrate
1033
- }
1034
- }).catch(() => null)
1035
- }
1036
- }
1037
- }
1038
-
1039
- //REMOTE
1040
-
1041
- else {
1042
-
1043
- let plugindata = json["plugindata"] || {};
1044
- let msg = plugindata["data"] || {};
1045
- let jsep = json["jsep"];
1046
- let event = msg["videoroom"];
1047
- let error = msg["error"];
1048
- let substream = msg["substream"];
1049
- let mid = msg["mid"];
1050
- let temporal = msg["temporal"];
1051
-
1052
- if(substream !== undefined && substream !== null) {
1053
- this._log('Substream: ', sender, mid, substream);
1054
- this._setSelectedSubstream(sender, mid, substream);
1055
- this._resetStats(sender, mid);
1056
- this.requestKeyFrame(sender, mid)
1057
- }
1058
-
1059
- if(temporal !== undefined && temporal !== null) {
1060
- this._log('Temporal: ', temporal);
1061
- }
1062
-
1063
- if(type === "webrtcup") {
1064
- this.requestKeyFrame(handle.handleId);
1065
- }
1066
-
1067
- if (event === "updated") {
1068
- this._log('Remote has updated tracks', msg);
1069
- if(msg["streams"]) {
1070
- this._updateTransceiverMap(handle.handleId, msg["streams"]);
1071
- }
1072
- }
1073
-
1074
- if (event === "attached") {
1075
- this._log('Remote have successfully joined Room', msg);
1076
- if(msg["streams"]) {
1077
- this._updateTransceiverMap(handle.handleId, msg["streams"] || []);
1078
- }
1079
- this.emit(this._getAddParticipantEventName(handle.handleId), {
1080
- tid: generateUUID(),
1081
- id: handle.handleId,
1082
- userId: decodeJanusDisplay(handle.userId)?.userId,
1083
- role: decodeJanusDisplay(handle.userId)?.role,
1084
- fullUserId: handle.userId,
1085
- stream: null,
1086
- streamMap: structuredClone(handle.webrtcStuff?.streamMap),
1087
- tracksMap: structuredClone(handle.webrtcStuff?.tracksMap),
1088
- transceiverMap: structuredClone(handle.webrtcStuff?.transceiverMap),
1089
- source: null,
1090
- track: null,
1091
- adding: false,
1092
- removing: false,
1093
- constructId: this.constructId,
1094
- hasAudioTrack: false,
1095
- hasVideoTrack: false
1096
- });
1097
- }
1098
-
1099
- if (error) {
1100
- this.emit('error', {type: 'warning', id: 8, message: 'remote participant error', data: [sender, msg]});
1101
- }
1102
-
1103
- if (jsep) {
1104
- this._publishRemote(handle.handleId, jsep)
1105
- .catch(err => {
1106
- this.emit('error', err);
1107
- });
1108
- }
1109
- }
1110
- }
1111
-
1112
- _handleDataEvents(handleId, type, data) {
1113
-
1114
- let handle = this._getHandle(handleId);
1115
-
1116
- if (type === 'state') {
1117
- this._log(` - Data channel status - `, `UID: ${handleId}`, `STATUS: ${JSON.stringify(data)}`, `ME: ${handleId === this.handleId}`)
1118
- if (handle) {
1119
- let config = handle.webrtcStuff;
1120
- config.dataChannelOpen = this.defaultDataChannelLabel === data?.label && data?.state === 'open';
1121
- }
1122
-
1123
- if (handleId === this.handleId && this.defaultDataChannelLabel === data?.label) {
1124
- this._isDataChannelOpen = data?.state === 'open';
1125
- this.emit('dataChannel', data?.state === 'open');
1126
- }
1127
- }
1128
-
1129
- if (type === 'error') {
1130
-
1131
- this.emit('error', {
1132
- type: 'warning',
1133
- id: 9,
1134
- message: 'data event warning',
1135
- data: [handleId, data]
1136
- });
1137
-
1138
- if (handle) {
1139
- let config = handle.webrtcStuff;
1140
- if(this.defaultDataChannelLabel === data.label) {
1141
- config.dataChannelOpen = false;
1142
- }
1143
- }
1144
- if (handleId === this.handleId && this.defaultDataChannelLabel === data.label) {
1145
- this._isDataChannelOpen = false;
1146
- this.emit('dataChannel', false);
1147
- }
1148
- }
1149
-
1150
- if (handleId === this.handleId && type === 'message') {
1151
-
1152
- let d = null;
1153
-
1154
- try {
1155
- d = JSON.parse(data)
1156
- } catch (e) {
1157
- this.emit('error', {type: 'warning', id: 10, message: 'data message parse error', data: [handleId, e]});
1158
- return;
1159
- }
1160
- this.emit('data', d);
1161
- }
1162
-
1163
- }
1164
-
1165
- //removeHandle === true -> hangup, detach, removeHandle === false -> hangup
1166
- _removeParticipant(handleId, rfid, removeHandle = true) {
1167
- let handle = this._getHandle(handleId, rfid);
1168
-
1169
- if (!handle) {
1170
- return Promise.resolve();
1171
- } else {
1172
- handleId = handle.handleId;
1173
- }
1174
-
1175
- return this._send({"janus": "hangup", "handle_id": handleId,}, true)
1176
- .then(() => (removeHandle ? this._send({
1177
- "janus": "detach",
1178
- "handle_id": handleId
1179
- }, true) : Promise.resolve()))
1180
- .finally(() => {
1181
-
1182
- try {
1183
- if (handle.webrtcStuff.stream) {
1184
- if(!this.isRestarting) {
1185
- handle.webrtcStuff.stream?.getTracks().forEach(track => track.stop());
1186
- }
1187
- else {
1188
- handle.webrtcStuff.stream?.getTracks().forEach(track => track.onended = null);
1189
- }
1190
- }
1191
- } catch (e) {
1192
- // Do nothing
1193
- }
1194
-
1195
- if(handle.webrtcStuff.stream) {
1196
- handle.webrtcStuff.stream.onremovetrack = null;
1197
- handle.webrtcStuff.stream = null;
1198
- }
1199
-
1200
- if (handle.webrtcStuff.dataChannel) {
1201
- Object.keys(handle.webrtcStuff.dataChannel).forEach(label => {
1202
- handle.webrtcStuff.dataChannel[label].onmessage = null;
1203
- handle.webrtcStuff.dataChannel[label].onopen = null;
1204
- handle.webrtcStuff.dataChannel[label].onclose = null;
1205
- handle.webrtcStuff.dataChannel[label].onerror = null;
1206
- })
1207
- }
1208
- if (handle.webrtcStuff.pc) {
1209
- handle.webrtcStuff.pc.onicecandidate = null;
1210
- handle.webrtcStuff.pc.ontrack = null;
1211
- handle.webrtcStuff.pc.ondatachannel = null;
1212
- handle.webrtcStuff.pc.onconnectionstatechange = null;
1213
- handle.webrtcStuff.pc.oniceconnectionstatechange = null;
1214
- }
1215
- try {
1216
- handle.webrtcStuff.pc.close();
1217
- } catch (e) {
1218
-
1219
- }
1220
- handle.webrtcStuff = {
1221
- stream: null,
1222
- streamMap: {},
1223
- tracksMap: [],
1224
- transceiverMap: [],
1225
- subscribeMap: [],
1226
- mySdp: null,
1227
- mediaConstraints: null,
1228
- pc: null,
1229
- dataChannelOpen: false,
1230
- dataChannel: null,
1231
- dtmfSender: null,
1232
- trickle: true,
1233
- iceDone: false,
1234
- isIceRestarting: false,
1235
- stats: {},
1236
- selectedSubstream: {},
1237
- initialSimulcastSubstreamBeenSet: {},
1238
- overriddenSimulcastMode: {},
1239
- };
1240
-
1241
- if (handleId === this.handleId) {
1242
- this._isDataChannelOpen = false;
1243
- this.isPublished = false;
1244
- this.emit('published', false);
1245
- this.emit('removeLocalParticipant', {
1246
- id: handleId,
1247
- userId: decodeJanusDisplay(handle.userId)?.userId,
1248
- role: decodeJanusDisplay(handle.userId)?.role,
1249
- fullUserId: handle.userId}
1250
- );
1251
- } else {
1252
- this.emit(this._getRemoveParticipantEventName(handleId), {
1253
- id: handleId,
1254
- userId: decodeJanusDisplay(handle.userId)?.userId,
1255
- role: decodeJanusDisplay(handle.userId)?.role,
1256
- fullUserId: handle.userId}
1257
- );
1258
- }
1259
-
1260
- if (removeHandle) {
1261
- let handleIndex = this._participants.findIndex(p => p.handleId === handleId);
1262
- this._participants.splice(handleIndex, 1);
1263
- }
1264
-
1265
- return true;
1266
- });
1267
-
1268
- }
1269
-
1270
- _createParticipant(userId = null, rfid = null) {
1271
- return this._send({
1272
- "janus": "attach",
1273
- "plugin": this.pluginName,
1274
- }).then(json => {
1275
- let handleId = json.data["id"];
1276
- let handle = {
1277
- handleId,
1278
- rfid, userId,
1279
- webrtcStuff: {
1280
- stream: null,
1281
- streamMap: {},
1282
- tracksMap: [],
1283
- transceiverMap: [],
1284
- subscribeMap: [],
1285
- mySdp: null,
1286
- mediaConstraints: null,
1287
- pc: null,
1288
- dataChannelOpen: false,
1289
- dataChannel: null,
1290
- dtmfSender: null,
1291
- trickle: true,
1292
- iceDone: false,
1293
- isIceRestarting: false,
1294
- stats: {},
1295
- selectedSubstream: {},
1296
- initialSimulcastSubstreamBeenSet: {},
1297
- overriddenSimulcastMode: {},
1298
- }
1299
- };
1300
- this._participants.push(handle);
1301
- return handle;
1302
- })
1303
- }
1304
-
1305
-
1306
- _updateSubscribeMap(handleId, subscribe, unsubscribe) {
1307
- let handle = this._getHandle(handleId);
1308
- if (!handle) {
1309
- this.emit('error', {
1310
- type: 'warning',
1311
- id: 15,
1312
- message: 'id non-existent',
1313
- data: [handleId, 'updateSubscribeMap']
1314
- });
1315
- return;
1316
- }
1317
- let currentSubscribeMap = handle.webrtcStuff.subscribeMap;
1318
- subscribe.forEach(s => {
1319
- if(!currentSubscribeMap.find(c => c.feed === s.feed && c.mid === s.mid)) {
1320
- currentSubscribeMap.push(s);
1321
- }
1322
- });
1323
- unsubscribe.forEach(s => {
1324
- let index = currentSubscribeMap.findIndex(c => c.feed === s.feed && c.mid === s.mid);
1325
- if(index > -1) {
1326
- currentSubscribeMap.splice(index, 1);
1327
- }
1328
- });
1329
- }
1330
-
1331
- _isAlreadySubscribed(handleId, feed, mid) {
1332
- let handle = this._getHandle(handleId);
1333
- if (!handle) {
1334
- this.emit('error', {
1335
- type: 'warning',
1336
- id: 15,
1337
- message: 'id non-existent',
1338
- data: [handleId, 'isAlreadySubscribed']
1339
- });
1340
- return false;
1341
- }
1342
- return handle.webrtcStuff.subscribeMap.findIndex(t => t.mid === mid && t.feed === feed) > -1
1343
- }
1344
-
1345
- _updateTransceiverMap(handleId, streams) {
1346
- this._log('Updating current transceiver map', handleId, streams);
1347
- let handle = this._getHandle(handleId);
1348
- if (!handle) {
1349
- this.emit('error', {
1350
- type: 'warning',
1351
- id: 15,
1352
- message: 'id non-existent',
1353
- data: [handleId, 'updateTransceiverMap']
1354
- });
1355
- return;
1356
- }
1357
- let config = handle.webrtcStuff;
1358
- config.transceiverMap = structuredClone(streams);
1359
- }
1360
-
1361
- _updateParticipantsTrackData(handleId, streams) {
1362
- this._log('Updating participants track data', handleId, streams);
1363
- let handle = this._getHandle(handleId);
1364
- if (!handle) {
1365
- this.emit('error', {
1366
- type: 'warning',
1367
- id: 15,
1368
- message: 'id non-existent',
1369
- data: [handleId, 'updateParticipantsTrackData']
1370
- });
1371
- return;
1372
- }
1373
- let config = handle.webrtcStuff;
1374
- config.tracksMap = structuredClone(streams.map(s => {
1375
- let source = null;
1376
- let simulcastBitrates = null;
1377
- try {
1378
- const description = JSON.parse(s.description);
1379
- source = description?.source;
1380
- simulcastBitrates = description?.simulcastBitrates;
1381
- } catch(e) {}
1382
- return {
1383
- active: !s.disabled,
1384
- description: s.description,
1385
- source: source,
1386
- simulcastBitrates: simulcastBitrates,
1387
- display: s.display,
1388
- id: s.id,
1389
- mid: s.mid,
1390
- mindex: s.mindex,
1391
- codec: s.codec,
1392
- type: s.type,
1393
- }
1394
- }));
1395
- }
1396
-
1397
- _updateRemoteParticipantStreamMap(handleId) {
1398
- this._log('Updating participants stream map', handleId);
1399
- let handle = this._getHandle(handleId);
1400
- if (!handle) {
1401
- this.emit('error', {
1402
- type: 'warning',
1403
- id: 15,
1404
- message: 'id non-existent',
1405
- data: [handleId, 'updateRemoteParticipantStreamMap']
1406
- });
1407
- return;
1408
- }
1409
- let config = handle.webrtcStuff;
1410
- config.streamMap = {};
1411
- config.transceiverMap.forEach(tItem => {
1412
- if(tItem.type === 'data') {
1413
- return;
1414
- }
1415
- if(tItem.active === false) {
1416
- return;
1417
- }
1418
- const source = JSON.parse(tItem.feed_description)?.source;
1419
- if(!config.streamMap[source]) {
1420
- config.streamMap[source] = [];
1421
- }
1422
- let trackId = config.pc.getTransceivers().find(t => t.mid === tItem.mid)?.receiver?.track?.id;
1423
- if(trackId) {
1424
- config.streamMap[source].push(trackId);
1425
- }
1426
- })
1427
- }
1428
-
1429
-
1430
- _joinRoom(roomId, pin, userId, display) {
1431
- return this.sendMessage(this.handleId, {
1432
- body: {
1433
- "request": "join", "room": roomId, "pin": pin, "ptype": "publisher", "display": display, ...(this.webrtcVersion > 1000 ? {id: userId} : {})
1434
- }
1435
- }, false, true)
1436
- }
1437
-
1438
- _watchStream(id) {
1439
- return this.sendMessage(this.handleId, {
1440
- body: {
1441
- "request": "watch", id: String(id)
1442
- }
1443
- }, false, true)
1444
- }
1445
-
1446
- _leaveRoom(dontWait = false) {
1447
- return this.isConnected
1448
- ? this.sendMessage(this.handleId, {body: {"request": "leave"}}, dontWait)
1449
- .finally(() => {
1450
- this.isConnected = false;
1451
- this.isPossibleToPublish = false;
1452
- this.emit('joined', false);
1453
- })
1454
- : Promise.resolve();
1455
- }
1456
-
1457
- _webSocketConnection(reclaim = false) {
1458
-
1459
- if (this.isConnecting) {
1460
- return Promise.reject({type: 'warning', id: 16, message: 'connection establishment already in progress'});
1461
- }
1462
- this.isPossibleToPublish = false;
1463
- this.isConnecting = true;
1464
- this.emit('joining', true);
1465
-
1466
- if (this.ws) {
1467
- this._wipeListeners();
1468
- if (this.ws.readyState === 1) {
1469
- this.ws.close();
1470
- }
1471
- }
1472
- this._stopKeepAlive();
1473
-
1474
- return new Promise((resolve, reject) => {
1475
-
1476
- this.__connectionClosedBoundFn = this._connectionClosed.bind(this);
1477
- this.__handleWsEventsBoundFn = this._handleWsEvents.bind(this);
1478
-
1479
- let abortConnect = () => {
1480
- this._abortController.signal.removeEventListener('abort', abortConnect);
1481
- this.ws.removeEventListener('close', this.__connectionClosedBoundFn);
1482
- this.ws.removeEventListener('message', this.__handleWsEventsBoundFn);
1483
- this.ws.onopen = null;
1484
- this.ws.onerror = null;
1485
- this.isConnecting = false;
1486
- this.emit('joining', false);
1487
- reject({type: 'warning', id: 17, message: 'Connection cancelled'});
1488
- };
1489
-
1490
- this.ws = new WebSocket(this.server, 'janus-protocol');
1491
- this.ws.addEventListener('close', this.__connectionClosedBoundFn);
1492
- this.ws.addEventListener('message', this.__handleWsEventsBoundFn);
1493
-
1494
- this.ws.onopen = () => {
1495
- this._abortController.signal.removeEventListener('abort', abortConnect);
1496
-
1497
- if(!reclaim) {
1498
-
1499
- this._send({"janus": "create"})
1500
- .then(json => {
1501
- this._retries = 0;
1502
- this.sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
1503
- this._startKeepAlive();
1504
- return 1;
1505
- })
1506
- .then(() => this._createParticipant(this.userId))
1507
- .then(handle => {
1508
- this.handleId = handle.handleId;
1509
- return 1
1510
- })
1511
- .then(() => this._joinRoom(this.roomId, this.pin, this.userId, this.display))
1512
- .then(() => {
1513
- this._enableStatsWatch();
1514
- this._enableSubstreamAutoSelect();
1515
- this.isConnecting = false;
1516
- this.isPossibleToPublish = true;
1517
- this.emit('joining', false);
1518
- resolve(this);
1519
- })
1520
- .catch(error => {
1521
- this.isConnecting = false;
1522
- this.emit('joining', false);
1523
- reject({type: error?.type === 'warning' ? 'warning' : 'error', id: 13, message: 'connection error', data: error})
1524
- });
1525
-
1526
- }
1527
-
1528
- else {
1529
- this._send({"janus": "claim"})
1530
- .then(json => {
1531
- this._retries = 0;
1532
- this.sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
1533
- this._startKeepAlive();
1534
- this.isConnecting = false;
1535
- this.emit('joining', false);
1536
- resolve(json);
1537
- })
1538
- .catch(error => {
1539
- this.isConnecting = false;
1540
- this.emit('joining', false);
1541
- reject({type: 'error', id: 11, message: 'reconnection error', data: error})
1542
- });
1543
- }
1544
-
1545
- };
1546
-
1547
- this.ws.onerror = (e) => {
1548
- this._abortController.signal.removeEventListener('abort', abortConnect);
1549
- this.isConnecting = false;
1550
- this.emit('joining', false);
1551
- reject({type: 'error', id: 14, message: 'ws connection error', data: e});
1552
- }
1553
-
1554
- this._abortController.signal.addEventListener('abort', abortConnect);
1555
-
1556
- });
1557
- }
1558
-
1559
- _httpConnection(reclaim = false) {
1560
-
1561
- if (this.isConnecting) {
1562
- return Promise.reject({type: 'warning', id: 16, message: 'connection establishment already in progress'});
1563
- }
1564
- this.isPossibleToPublish = false;
1565
- this.isConnecting = true;
1566
- this.emit('joining', true);
1567
-
1568
- if (this.ws) {
1569
- this._wipeListeners();
1570
- if (this.ws.readyState === 1) {
1571
- this.ws.close();
1572
- }
1573
- }
1574
- this._stopKeepAlive();
1575
-
1576
- if(!reclaim) {
1577
- return this._send({"janus": "create"})
1578
- .then(json => {
1579
- this._retries = 0;
1580
- this.sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
1581
- this._startKeepAlive();
1582
- return 1;
1583
- })
1584
- .then(() => this._createParticipant(this.userId))
1585
- .then(handle => {
1586
- this.handleId = handle.handleId;
1587
- return 1
1588
- })
1589
- .then(() => this._joinRoom(this.roomId, this.pin, this.userId, this.display))
1590
- .then(() => {
1591
- this._enableStatsWatch();
1592
- this._enableSubstreamAutoSelect();
1593
- this._longPoll();
1594
- this.isConnecting = false;
1595
- this.isPossibleToPublish = true;
1596
- this.emit('joining', false);
1597
- return Promise.resolve(this);
1598
- })
1599
- .catch(error => {
1600
- this.isConnecting = false;
1601
- this.emit('joining', false);
1602
- return Promise.reject({type: error?.type === 'warning' ? 'warning' : 'error', id: 13, message: 'connection error', data: error})
1603
- });
1604
- }
1605
- else {
1606
- return this._send({"janus": "claim"})
1607
- .then(json => {
1608
- this._retries = 0;
1609
- this.sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
1610
- this._startKeepAlive();
1611
- this.isConnecting = false;
1612
- this.emit('joining', false);
1613
- return Promise.resolve(json);
1614
- })
1615
- .catch(error => {
1616
- this.isConnecting = false;
1617
- this.emit('joining', false);
1618
- return Promise.reject({type: 'error', id: 11, message: 'reconnection error', data: error})
1619
- });
1620
- }
1621
- }
1622
-
1623
-
1624
- // internal reconnect
1625
- _reconnect() {
1626
- if(this.useWebsockets) {
1627
- return this._webSocketConnection(true)
1628
- }
1629
- else {
1630
- return this._httpConnection(true)
1631
- }
1632
- }
1633
- connect(
1634
- roomId,
1635
- pin,
1636
- server,
1637
- protocol,
1638
- iceServers,
1639
- token,
1640
- display,
1641
- userId,
1642
- webrtcVersion = 0,
1643
- initialBitrate = 0,
1644
- recordingFilename,
1645
- simulcast = false,
1646
- simulcastSettings = this.defaultSimulcastSettings,
1647
- enableDtx = false
1648
- ) {
1649
-
1650
- this.isSupposeToBeConnected = true;
1651
-
1652
- if (this.isConnecting) {
1653
- return Promise.reject({type: 'warning', id: 16, message: 'connection already in progress'});
1654
- }
1655
-
1656
- this._abortController = new AbortController();
1657
-
1658
- this.sessionId = null;
1659
- this.server = server;
1660
- this.protocol = protocol;
1661
- this.iceServers = iceServers;
1662
- this.token = token;
1663
- this.roomId = roomId;
1664
- this.pin = pin;
1665
- this.display = display;
1666
- this.userId = userId;
1667
- this.webrtcVersion = webrtcVersion;
1668
- this.initialBitrate = initialBitrate;
1669
- this.recordingFilename = recordingFilename;
1670
- this.enableDtx = enableDtx;
1671
- this.simulcast = simulcast;
1672
- this.simulcastSettings = structuredClone(simulcastSettings);
1673
- this.useWebsockets = this.protocol === 'ws' || this.protocol === 'wss';
1674
-
1675
- // sort simulcast bitrates
1676
- if(this.simulcastSettings && typeof this.simulcastSettings === 'object' && Object.keys(this.simulcastSettings).length) {
1677
- Object.keys(this.simulcastSettings).forEach(k => {
1678
- this.simulcastSettings[k].bitrates = this.simulcastSettings[k].bitrates.sort((a, b) => {
1679
- if(a.maxBitrate === b.maxBitrate) {
1680
- return a.maxFramerate - b.maxFramerate;
1681
- }
1682
- return a.maxBitrate - b.maxBitrate;
1683
- });
1684
- });
1685
- }
1686
-
1687
- if(this.useWebsockets) {
1688
- return this._webSocketConnection(false)
1689
- }
1690
- else {
1691
- return this._httpConnection(false)
1692
- }
1693
- }
1694
-
1695
- disconnect() {
1696
-
1697
- this.isSupposeToBeConnected = false;
1698
-
1699
- if (this.isDisconnecting) {
1700
- return Promise.resolve();
1701
- }
1702
-
1703
- this._abortController?.abort?.();
1704
- this.isPossibleToPublish = false;
1705
- this.isDisconnecting = true;
1706
- this._stopKeepAlive();
1707
- this._disableStatsWatch();
1708
- this._disableSubstreamAutoSelect();
1709
- let isConnected = this.isConnected;
1710
- return Promise.all(this._participants.map(p => this._removeParticipant(p.handleId)))
1711
- .finally(() => {
1712
- this._wipeListeners();
1713
- if (this.ws && this.ws.readyState === 1) {
1714
- this._send({"janus": "destroy"}, true);
1715
- this.ws.close();
1716
- }
1717
- this.sessionId = null;
1718
- this.isPublished = false;
1719
- this.isConnected = false;
1720
- this.isDisconnecting = false;
1721
- this.emit('publishing', false);
1722
- this.emit('published', false);
1723
- this.emit('joining', false);
1724
- this.emit('joined', false);
1725
- this.emit('disconnect', isConnected);
1726
- return Promise.resolve('Disconnected');
1727
- })
1728
- }
1729
-
1730
- startStream(streamId, server, iceServers, token, userId) {
1731
-
1732
- this.isSupposeToBeConnected = true;
1733
-
1734
- if (this.isConnecting) {
1735
- return Promise.reject({type: 'warning', id: 16, message: 'connection error', data: 'Connection is in progress'});
1736
- }
1737
-
1738
- if (this.ws) {
1739
- this._wipeListeners();
1740
- }
1741
-
1742
- this._stopKeepAlive();
1743
- this._abortController = new AbortController();
1744
- this.server = server;
1745
- this.iceServers = iceServers;
1746
- this.token = token;
1747
- this.streamId = streamId;
1748
- this.userId = userId;
1749
- this.sessionId = null;
1750
- this.isConnecting = true;
1751
- this.emit('streamStarting', true);
1752
- return new Promise((resolve, reject) => {
1753
-
1754
- this.ws = new WebSocket(this.server, 'janus-protocol');
1755
- this.__connectionClosedBoundFn = this._connectionClosed.bind(this);
1756
- this.__handleWsEventsBoundFn = this._handleWsEvents.bind(this);
1757
-
1758
- let abortConnect = () => {
1759
- this._abortController.signal.removeEventListener('abort', abortConnect);
1760
- this.ws.removeEventListener('close', this.__connectionClosedBoundFn);
1761
- this.ws.removeEventListener('message', this.__handleWsEventsBoundFn);
1762
- this.ws.onopen = null;
1763
- this.ws.onerror = null;
1764
- this.isConnecting = false;
1765
- this.emit('streamStarting', false);
1766
- reject({type: 'warning', id: 17, message: 'Connection cancelled'});
1767
- };
1768
-
1769
- this.ws.addEventListener('close', this.__connectionClosedBoundFn);
1770
- this.ws.addEventListener('message', this.__handleWsEventsBoundFn);
1771
- this.ws.onopen = () => {
1772
- this._abortController.signal.removeEventListener('abort', abortConnect);
1773
- this._retries = 0;
1774
- this._send({"janus": "create"})
1775
- .then(json => {
1776
- this.sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
1777
- this._startKeepAlive();
1778
- return 1;
1779
- })
1780
- .then(() => this._createParticipant(userId))
1781
- .then(handle => {
1782
- this.handleId = handle.handleId;
1783
- return 1
1784
- })
1785
- .then(() => this._watchStream(streamId))
1786
- .then(() => {
1787
- this.isConnecting = false;
1788
- this.emit('streamStarting', false);
1789
- resolve(this);
1790
- })
1791
- .catch(error => {
1792
- this.isConnecting = false;
1793
- this.emit('streamStarting', false);
1794
- reject({type: 'error', id: 13, message: 'connection error', data: error})
1795
- });
1796
- };
1797
- this.ws.onerror = (e) => {
1798
- this._abortController.signal.removeEventListener('abort', abortConnect);
1799
- this.isConnecting = false;
1800
- this.emit('streamStarting', false);
1801
- reject({type: 'error', id: 14, message: 'ws connection error', data: e});
1802
- }
1803
-
1804
- this._abortController.signal.addEventListener('abort', abortConnect);
1805
- });
1806
- }
1807
-
1808
- stopStream() {
1809
-
1810
- if (this.isDisconnecting) {
1811
- return Promise.resolve();
1812
- }
1813
-
1814
- this._abortController?.abort?.();
1815
- this._stopKeepAlive();
1816
-
1817
- let isStreaming = this.isStreaming;
1818
- this.isPossibleToPublish = false;
1819
- this.isDisconnecting = true;
1820
- return this._removeParticipant(this.handleId)
1821
- .finally(() => {
1822
- this.sendMessage(this.handleId, {body: {"request": "stop"}}, true);
1823
- this._wipeListeners();
1824
- this._send({"janus": "destroy"}, true);
1825
- if (this.ws && this.ws.readyState === 1) {
1826
- this.ws.close();
1827
- }
1828
- this.sessionId = null;
1829
- this.isDisconnecting = false;
1830
- this.isStreaming = false;
1831
- this.emit('streamStarting', false);
1832
- this.emit('streaming', false);
1833
- this.emit('disconnect', isStreaming);
1834
- return Promise.resolve('Disconnected');
1835
- });
1836
- }
1837
-
1838
- destroy() {
1839
-
1840
- if (this.sessiontype === 'reactooroom') {
1841
- return this.disconnect()
1842
- .then(() => {
1843
- this.clear();
1844
- return true;
1845
- });
1846
- } else if (this.sessiontype === 'streaming') {
1847
- return this.stopStream()
1848
- .then(() => {
1849
- this.clear();
1850
- return true
1851
- })
1852
- }
1853
- }
1854
-
1855
- _enableDebug() {
1856
- this._log = console.log.bind(console);
1857
- }
1858
-
1859
- _getHandle(handleId, rfid = null, userId = null, fullUserId = null) {
1860
- return this._participants.find(p => p.handleId === handleId || (rfid && p.rfid === rfid) || (userId && decodeJanusDisplay(p.userId)?.userId === userId) || (fullUserId && p.userId === fullUserId));
1861
- }
1862
-
1863
-
1864
- _findSimulcastConfig(source, settings) {
1865
- return Object.keys(settings).reduce((acc, key) => {
1866
- if(settings[source]) {
1867
- return settings[source];
1868
- } else if(source.indexOf(key.match(/\*(.*?)\*/)?.[1]) > -1) {
1869
- return settings[key];
1870
- } else return acc;
1871
- }, settings['default']);
1872
- }
1873
-
1874
- _disableStatsWatch() {
1875
- if (this._statsTimeoutId) {
1876
- clearTimeout(this._statsTimeoutId);
1877
- this._statsTimeoutStopped = true;
1878
- this._statsTimeoutId = null;
1879
- }
1880
- }
1881
- _enableStatsWatch() {
1882
- if (this._statsTimeoutId) {
1883
- clearTimeout(this._statsTimeoutId);
1884
- this._statsTimeoutId = null;
1885
- }
1886
- this._statsTimeoutStopped = false;
1887
- const loop = () => {
1888
- this._getStats('video')
1889
- .then(participantsStats => {
1890
- this._parseVideoStats(participantsStats);
1891
- })
1892
- .finally(() => {
1893
- if (!this._statsTimeoutStopped) {
1894
- this._statsTimeoutId = setTimeout(loop, this._statsInterval);
1895
- }
1896
- })
1897
- };
1898
- loop()
1899
- }
1900
-
1901
- // This method completely ignores temporal layers
1902
- // We prefer higher fps and lower resolution so if the fps in not in the range of 0.7 of the max fps we go to the next lower resolution
1903
-
1904
- _enableSubstreamAutoSelect() {
1905
-
1906
- if(!this.simulcast) {
1907
- return;
1908
- }
1909
-
1910
- if(this._aqTimeoutId) {
1911
- clearTimeout(this._aqTimeoutId);
1912
- this._aqTimeoutId = null;
1913
- this._aqIntervalCounter = 0;
1914
- }
1915
-
1916
-
1917
- const checkStats = () => {
1918
- this._participants.forEach(p => {
1919
- if(p.handleId !== this.handleId) {
1920
-
1921
- const transceivers = p.webrtcStuff?.pc?.getTransceivers();
1922
- const mids = transceivers?.filter(t => t.receiver.track.kind === "video")?.map(t => t.mid) || [];
1923
- mids.forEach(mid => {
1924
-
1925
- const {source, simulcastBitrates} = p.webrtcStuff.tracksMap.find(t => t.mid === mid) || {};
1926
-
1927
- // track is gone
1928
- if(!simulcastBitrates) {
1929
- return;
1930
- }
1931
-
1932
- const simulcastConfigForSource = this._findSimulcastConfig(source, this.simulcastSettings);
1933
- const initialSubstreamBeenSet = !!p.webrtcStuff.initialSimulcastSubstreamBeenSet[mid];
1934
- const defaultSelectedSubstream = p.webrtcStuff?.overriddenSimulcastMode[mid]?.defaultSubstream ?? simulcastConfigForSource?.defaultSubstream;
1935
- const simulcastMode = p.webrtcStuff?.overriddenSimulcastMode[mid]?.mode ?? simulcastConfigForSource?.mode;
1936
-
1937
- if((simulcastMode === 'browserControlled')) {
1938
- // do nothing
1939
- }
1940
-
1941
- else if((simulcastMode === 'manual' && this._aqIntervalCounter % this._aqIntervalDivisor === 0) || !initialSubstreamBeenSet) {
1942
- p.webrtcStuff.initialSimulcastSubstreamBeenSet[mid] = true;
1943
- const currentSubstream = p.webrtcStuff.selectedSubstream[mid];
1944
- if(defaultSelectedSubstream !== undefined && defaultSelectedSubstream !== null && defaultSelectedSubstream !== currentSubstream) {
1945
- this._log('Attempting to force substream quality', defaultSelectedSubstream);
1946
- this.selectSubStream(p.handleId, defaultSelectedSubstream, undefined, mid, false)
1947
- .catch((reason) => this._log(`Changing substream for mid: ${mid} failed. Reason: ${reason}`));
1948
- }
1949
- }
1950
-
1951
- else if(simulcastMode === 'controlled') {
1952
- const currentSubstream = p.webrtcStuff.selectedSubstream[mid];
1953
- const settingsForCurrentSubstream = simulcastBitrates?.[simulcastBitrates.length - 1 - currentSubstream];
1954
-
1955
- let directionDecision = 0;
1956
- if(p.webrtcStuff?.stats?.[mid]?.length > this._upStatsLength) {
1957
- const upMedianStats = this._calculateMedianStats(p.webrtcStuff.stats[mid].slice(this._upStatsLength * -1));
1958
- if(upMedianStats?.freezeDurationSinceLast < (this._upStatsLength * this._statsInterval * 0.33) / 1000 /* && upMedianStats?.freezeCountSinceLast < 3 */) {
1959
- directionDecision = 1;
1960
- }
1961
- }
1962
-
1963
- if(p.webrtcStuff?.stats?.[mid]?.length > this._downStatsLength) {
1964
- const downMedianStats = this._calculateMedianStats(p.webrtcStuff.stats[mid].slice(this._downStatsLength * -1));
1965
- if(downMedianStats?.freezeDurationSinceLast > (this._downStatsLength * this._statsInterval * 0.33) / 1000 /* || downMedianStats?.freezeCountSinceLast > 5 || downMedianStats?.jitter > maxJitter(settingsForCurrentSubstream.maxFramerate) */) {
1966
- directionDecision = -1;
1967
- }
1968
- }
1969
-
1970
- if(directionDecision === -1) {
1971
- if(currentSubstream < simulcastBitrates.length - 1) {
1972
- this._log('Attempting to down the quality for mid: ', mid, ' quality:', currentSubstream + 1);
1973
- this._resetStats(p.handleId, mid);
1974
- this.selectSubStream(p.handleId, currentSubstream + 1, undefined, mid, false)
1975
- .catch((reason) => this._log(`Changing substream for mid: ${mid} failed. Reason: ${reason}`));
1976
- }
1977
- }
1978
- else if (directionDecision === 1) {
1979
- if(currentSubstream > 0) {
1980
- this._log('Attempting to up the quality for mid: ', mid, ' quality:', currentSubstream - 1);
1981
- this._resetStats(p.handleId, mid);
1982
- this.selectSubStream(p.handleId, currentSubstream - 1, undefined, mid, false)
1983
- .catch((reason) => this._log(`Changing substream for mid: ${mid} failed. Reason: ${reason}`));
1984
- }
1985
- }
1986
- else {
1987
- this._log('No quality change for mid: ', mid);
1988
- }
1989
- }
1990
- });
1991
- }
1992
- })
1993
-
1994
- this._aqIntervalCounter++;
1995
- this._aqTimeoutId = setTimeout(checkStats, this._aqInterval);
1996
- }
1997
-
1998
- checkStats();
1999
- }
2000
-
2001
- _disableSubstreamAutoSelect() {
2002
- if(this._aqTimeoutId) {
2003
- clearTimeout(this._aqTimeoutId);
2004
- this._aqTimeoutId = null;
2005
- this._aqIntervalCounter = 0;
2006
- }
2007
- }
2008
-
2009
- _calculateMedianStats(stats) {
2010
- let medianStats = {
2011
- framesPerSecond: null,
2012
- jitter: null,
2013
- roundTripTime: null,
2014
- freezeDurationSinceLast: null,
2015
- freezeCountSinceLast: null,
2016
- };
2017
- let keys = Object.keys(medianStats);
2018
- keys.forEach(key => {
2019
- if(key === 'freezeDurationSinceLast' || key ==='freezeCountSinceLast') {
2020
- medianStats[key] = stats.reduce((acc, cur) => acc + cur[key], 0);
2021
- }
2022
- // median but ignore first value of stats array
2023
- else {
2024
- let values = stats.map(s => s[key]);
2025
- medianStats[key] = median(values)
2026
- }
2027
-
2028
- });
2029
- medianStats.statsLength = stats.length;
2030
- return medianStats;
2031
- }
2032
-
2033
-
2034
- _parseVideoStats(participantsStats) {
2035
-
2036
- let statsToEmit = [];
2037
-
2038
- for (const sourceStats of participantsStats) {
2039
-
2040
- for (const participantStats of sourceStats) {
2041
-
2042
- if (!participantStats?.handleId || participantStats.handleId === this.handleId) continue;
2043
-
2044
- const handle = this._getHandle(participantStats.handleId);
2045
- if (!handle) continue;
2046
-
2047
- const mid = participantStats.mid;
2048
- handle.webrtcStuff.stats[mid] ??= [];
2049
-
2050
- const lastStats = handle.webrtcStuff.stats[mid][handle.webrtcStuff.stats[mid].length - 1];
2051
-
2052
- const stats = {
2053
- framesPerSecond: null,
2054
- framesDropped: null,
2055
- totalFreezesDuration: null,
2056
- freezeDurationSinceLast: null,
2057
- freezeCount: null,
2058
- freezeCountSinceLast: null,
2059
- jitter: null,
2060
- packetsLost: null,
2061
- nackCount: null,
2062
- roundTripTime: null,
2063
- width: null,
2064
- height: null,
2065
- networkType: null,
2066
- powerEfficientDecoder: null,
2067
- selectedSubstream: null,
2068
- desiredSubstream: null,
2069
- simulcastMode: null
2070
- };
2071
-
2072
- const simulcastConfig = this._findSimulcastConfig(participantStats.source, this.simulcastSettings);
2073
- const defaultSelectedSubstream = handle.webrtcStuff?.overriddenSimulcastMode[mid]?.defaultSubstream ?? simulcastConfig?.defaultSubstream;
2074
- const simulcastMode = handle.webrtcStuff?.overriddenSimulcastMode[mid]?.mode ?? simulcastConfig?.mode;
2075
-
2076
- participantStats.stats.forEach(report => {
2077
-
2078
- if (report.type === 'inbound-rtp' && report.kind === 'video') {
2079
- stats.framesPerSecond = report.framesPerSecond || 0;
2080
- stats.framesDropped = report.framesDropped || 0;
2081
- stats.totalFreezesDuration = report.totalFreezesDuration || 0;
2082
- stats.freezeDurationSinceLast = (report.totalFreezesDuration || 0) - (lastStats?.totalFreezesDuration || 0);
2083
- stats.freezeCount = report.freezeCount || 0;
2084
- stats.freezeCountSinceLast = (report.freezeCount || 0) - (lastStats?.freezeCount || 0);
2085
- stats.jitter = report.jitter;
2086
- stats.packetsLost = report.packetsLost;
2087
- stats.nackCount = report.nackCount;
2088
- stats.width = report.frameWidth;
2089
- stats.height = report.frameHeight;
2090
- stats.powerEfficientDecoder = report.powerEfficientDecoder;
2091
- }
2092
-
2093
- if (report.type === 'candidate-pair') {
2094
- stats.roundTripTime = report.currentRoundTripTime;
2095
- }
2096
-
2097
- if (report.type === 'local-candidate') {
2098
- stats.networkType = report.networkType;
2099
- }
2100
- });
2101
-
2102
- stats.selectedSubstream = handle.webrtcStuff.selectedSubstream[mid];
2103
- stats.desiredSubstream = defaultSelectedSubstream;
2104
- stats.simulcastMode = simulcastMode;
2105
-
2106
- handle.webrtcStuff.stats[mid].push(stats);
2107
-
2108
- if (handle.webrtcStuff.stats[mid].length > this._statsMaxLength) {
2109
- handle.webrtcStuff.stats[mid].shift();
2110
- }
2111
-
2112
- statsToEmit.push({
2113
- handleId: participantStats.handleId,
2114
- stats,
2115
- userId: decodeJanusDisplay(handle.userId)?.userId,
2116
- source: participantStats.source,
2117
- mid
2118
- });
2119
- }
2120
- }
2121
-
2122
- this.emit('rtcStats', statsToEmit);
2123
- }
2124
-
2125
- _getStats(type = null) {
2126
- return this._participants.reduce((promise, participant) => {
2127
- return promise.then(results => {
2128
- let mediaTrack = [];
2129
- if (type === 'video') {
2130
- mediaTrack = participant?.webrtcStuff?.stream?.getVideoTracks() || [];
2131
- } else if (type === 'audio') {
2132
- mediaTrack = participant?.webrtcStuff?.stream?.getAudioTracks() || [];
2133
- }
2134
-
2135
- if (type !== null) {
2136
- const transceivers = participant?.webrtcStuff?.pc?.getTransceivers();
2137
- return Promise.all(mediaTrack.map(track => {
2138
- const source = Object.keys(participant.webrtcStuff.streamMap).find(s =>
2139
- participant.webrtcStuff.streamMap[s].find(t => t === track.id)
2140
- );
2141
- const mid = transceivers.find(t =>
2142
- t.receiver?.track?.id === track.id || t.sender?.track?.id === track.id
2143
- )?.mid;
2144
-
2145
- return participant.webrtcStuff.pc.getStats(track)
2146
- .then(r => ({
2147
- stats: r,
2148
- source,
2149
- mid,
2150
- handleId: participant.handleId
2151
- }))
2152
- .catch(e => Promise.reject({
2153
- stats: null,
2154
- error: e,
2155
- handleId: participant.handleId,
2156
- source,
2157
- mid
2158
- }));
2159
- })).then(participantResults => [...results, participantResults]);
2160
- } else {
2161
- return participant?.webrtcStuff?.pc?.getStats(null)
2162
- .then(r => [...results, {
2163
- handleId: participant.handleId,
2164
- stats: r
2165
- }])
2166
- .catch(e => [...results, {
2167
- handleId: participant.handleId,
2168
- error: e
2169
- }]);
2170
- }
2171
- });
2172
- }, Promise.resolve([]));
2173
- }
2174
-
2175
- _resetStats(handleId, mid) {
2176
- let handle = this._getHandle(handleId);
2177
- if(handle) {
2178
- let config = handle.webrtcStuff;
2179
- if(!mid) {
2180
- Object.keys(config.stats).forEach(mid => {
2181
- config.stats[mid] = [config.stats[mid][config.stats[mid].length - 1]];
2182
- })
2183
- }
2184
- else {
2185
- // clearing stats for the new substream
2186
- if(config.stats[mid]) {
2187
- config.stats[mid] = [config.stats[mid][config.stats[mid].length - 1]];
2188
- }
2189
- }
2190
- }
2191
- }
2192
-
2193
- _sendTrickleCandidate(handleId, candidate) {
2194
- return this._send({
2195
- "janus": "trickle",
2196
- "candidate": candidate,
2197
- "handle_id": handleId
2198
- }, false, false, 5)
2199
- }
2200
-
2201
- _webrtc(handleId, enableOntrack = false) {
2202
-
2203
- let handle = this._getHandle(handleId);
2204
- if (!handle) {
2205
- this.emit('error', {
2206
- type: 'error',
2207
- id: 15,
2208
- message: 'id non-existent',
2209
- data: [handleId, 'create rtc connection']
2210
- });
2211
- return;
2212
- }
2213
-
2214
- let config = handle.webrtcStuff;
2215
- if (!config.pc) {
2216
- let pc_config = {"iceServers": this.iceServers, "iceTransportPolicy": 'all', "bundlePolicy": undefined};
2217
-
2218
- pc_config["sdpSemantics"] = "unified-plan";
2219
-
2220
- let pc_constraints = {};
2221
-
2222
- if (adapter.browserDetails.browser === "edge") {
2223
- // This is Edge, enable BUNDLE explicitly
2224
- pc_config.bundlePolicy = "max-bundle";
2225
- }
2226
-
2227
- // pc_config.bundlePolicy = 'balanced';
2228
- // pc_config.iceTransportPolicy = 'relay';
2229
- // pc_config.rtcpMuxPolicy = "negotiate";
2230
-
2231
- this._log('new RTCPeerConnection', pc_config, pc_constraints);
2232
-
2233
- config.pc = new RTCPeerConnection(pc_config, pc_constraints);
2234
-
2235
- config.pc.onnegotiationneeded = () => {
2236
- this._log('onnegotiationneeded');
2237
- };
2238
-
2239
- config.pc.onconnectionstatechange = () => {
2240
- if (config.pc.connectionState === 'failed') {
2241
- this._log('connectionState failed');
2242
- this._iceRestart(handleId);
2243
- }
2244
- this.emit('connectionState', [handleId, handleId === this.handleId, config.pc.connectionState]);
2245
- if(handleId !== this.handleId && config.pc.connectionState === 'connected') {
2246
-
2247
- this.emit(this._getAddParticipantEventName(handle.handleId), {
2248
- tid: generateUUID(),
2249
- id: handle.handleId,
2250
- userId: decodeJanusDisplay(handle.userId)?.userId,
2251
- role: decodeJanusDisplay(handle.userId)?.role,
2252
- fullUserId: handle.userId,
2253
- stream: config.stream,
2254
- streamMap: structuredClone(config.streamMap),
2255
- tracksMap: structuredClone(config.tracksMap),
2256
- transceiverMap: structuredClone(config.transceiverMap),
2257
- track: null,
2258
- source: null,
2259
- constructId: this.constructId,
2260
- adding: false,
2261
- removing: false,
2262
- hasAudioTrack: !!(config.stream && config.stream.getAudioTracks().length),
2263
- hasVideoTrack: !!(config.stream && config.stream.getVideoTracks().length)
2264
- })
2265
- }
2266
- };
2267
- config.pc.oniceconnectionstatechange = () => {
2268
- if (config.pc.iceConnectionState === 'failed') {
2269
- this._log('iceConnectionState failed');
2270
- this._iceRestart(handleId);
2271
- }
2272
- this.emit('iceState', [handleId, handleId === this.handleId, config.pc.iceConnectionState]);
2273
- if(handleId !== this.handleId && (config.pc.iceConnectionState === 'completed' || config.pc.iceConnectionState === 'connected')) {
2274
- this.emit(this._getAddParticipantEventName(handle.handleId), {
2275
- tid: generateUUID(),
2276
- id: handle.handleId,
2277
- userId: decodeJanusDisplay(handle.userId)?.userId,
2278
- role: decodeJanusDisplay(handle.userId)?.role,
2279
- fullUserId: handle.userId,
2280
- stream: config.stream,
2281
- streamMap: structuredClone(config.streamMap),
2282
- tracksMap: structuredClone(config.tracksMap),
2283
- transceiverMap: structuredClone(config.transceiverMap),
2284
- track: null,
2285
- source: null,
2286
- constructId: this.constructId,
2287
- adding: false,
2288
- hasAudioTrack: !!(config.stream && config.stream.getAudioTracks().length),
2289
- hasVideoTrack: !!(config.stream && config.stream.getVideoTracks().length)
2290
- })
2291
- }
2292
- };
2293
- config.pc.onicecandidate = (event) => {
2294
- if (event.candidate == null || (adapter.browserDetails.browser === 'edge' && event.candidate.candidate.indexOf('endOfCandidates') > 0)) {
2295
- config.iceDone = true;
2296
- this._sendTrickleCandidate(handleId, {"completed": true})
2297
- .catch(e => {
2298
- this.emit('error', e);
2299
- });
2300
- } else {
2301
- // JSON.stringify doesn't work on some WebRTC objects anymore
2302
- // See https://code.google.com/p/chromium/issues/detail?id=467366
2303
- var candidate = {
2304
- "candidate": event.candidate.candidate,
2305
- "sdpMid": event.candidate.sdpMid,
2306
- "sdpMLineIndex": event.candidate.sdpMLineIndex
2307
- };
2308
-
2309
- this._sendTrickleCandidate(handleId, candidate)
2310
- .catch(e => {
2311
- this.emit('error', e);
2312
- });
2313
- }
2314
- };
2315
-
2316
- if (enableOntrack) {
2317
- config.pc.ontrack = (event) => {
2318
-
2319
- if(!event.streams)
2320
- return;
2321
-
2322
- if (!config.stream) {
2323
- config.stream = new MediaStream();
2324
- }
2325
-
2326
- if(!event.streams?.[0]?.onremovetrack) {
2327
- event.streams[0].onremovetrack = (ev) => {
2328
- this._log('Remote track removed', ev);
2329
- config.stream?.removeTrack(ev.track);
2330
-
2331
- // check if handle still exists
2332
- if(!this._getHandle(handle.handleId)) {
2333
- return;
2334
- }
2335
- this._updateRemoteParticipantStreamMap(handle.handleId);
2336
-
2337
- let transceiver = config.pc?.getTransceivers()?.find(
2338
- t => t.receiver.track === ev.track);
2339
-
2340
- let mid = transceiver?.mid || ev.track.id;
2341
- let source = Object.keys(config.streamMap).find(key => config.streamMap[key].includes(ev.track.id));
2342
- this.emit(this._getAddParticipantEventName(handle.handleId), {
2343
- tid: generateUUID(),
2344
- id: handle.handleId,
2345
- mid,
2346
- constructId: this.constructId,
2347
- userId: decodeJanusDisplay(handle.userId)?.userId,
2348
- role: decodeJanusDisplay(handle.userId)?.role,
2349
- fullUserId: handle.userId,
2350
- stream: config.stream,
2351
- streamMap: structuredClone(config.streamMap),
2352
- tracksMap: structuredClone(config.tracksMap),
2353
- transceiverMap: structuredClone(config.transceiverMap),
2354
- source,
2355
- track: ev.track,
2356
- adding: false,
2357
- removing: true,
2358
- hasAudioTrack: !!(config.stream && config.stream.getAudioTracks().length),
2359
- hasVideoTrack: !!(config.stream && config.stream.getVideoTracks().length)
2360
- });
2361
- };
2362
- }
2363
-
2364
- if (event.track) {
2365
- config.stream?.addTrack(event.track);
2366
- this._updateRemoteParticipantStreamMap(handle.handleId);
2367
- let mid = event.transceiver ? event.transceiver.mid : event.track.id;
2368
- let source = Object.keys(config.streamMap).find(key => config.streamMap[key].includes(event.track.id));
2369
- if(event.track.kind === 'video') {
2370
- this.requestKeyFrame(handle.handleId, mid);
2371
- }
2372
-
2373
- this.emit(this._getAddParticipantEventName(handle.handleId), {
2374
- tid: generateUUID(),
2375
- mid,
2376
- id: handle.handleId,
2377
- userId: decodeJanusDisplay(handle.userId)?.userId,
2378
- role: decodeJanusDisplay(handle.userId)?.role,
2379
- fullUserId: handle.userId,
2380
- stream: config.stream,
2381
- streamMap: structuredClone(config.streamMap),
2382
- tracksMap: structuredClone(config.tracksMap),
2383
- transceiverMap: structuredClone(config.transceiverMap),
2384
- source,
2385
- track: event.track,
2386
- constructId: this.constructId,
2387
- adding: true,
2388
- removing: false,
2389
- hasAudioTrack: !!(config.stream && config.stream.getAudioTracks().length),
2390
- hasVideoTrack: !!(config.stream && config.stream.getVideoTracks().length)
2391
- });
2392
-
2393
- if (event.track.onended)
2394
- return;
2395
-
2396
- event.track.onended = (ev) => {
2397
- this._log('Remote track ended');
2398
- config.stream?.removeTrack(ev.target);
2399
- // check if handle still exists
2400
- if(!this._getHandle(handle.handleId)) {
2401
- return;
2402
- }
2403
- this._updateRemoteParticipantStreamMap(handle.handleId);
2404
- let transceiver = config.pc?.getTransceivers()?.find(
2405
- t => t.receiver.track === ev.target);
2406
- let mid = transceiver?.mid || ev.target.id;
2407
- let source = Object.keys(config.streamMap).find(key => config.streamMap[key].includes(ev.target.id));
2408
- this.emit(this._getAddParticipantEventName(handle.handleId), {
2409
- tid: generateUUID(),
2410
- id: handle.handleId,
2411
- mid,
2412
- constructId: this.constructId,
2413
- userId: decodeJanusDisplay(handle.userId)?.userId,
2414
- role: decodeJanusDisplay(handle.userId)?.role,
2415
- fullUserId: handle.userId,
2416
- stream: config.stream,
2417
- streamMap: structuredClone(config.streamMap),
2418
- tracksMap: structuredClone(config.tracksMap),
2419
- transceiverMap: structuredClone(config.transceiverMap),
2420
- source,
2421
- track: ev.target,
2422
- adding: false,
2423
- removing: true,
2424
- hasAudioTrack: !!(config.stream && config.stream.getAudioTracks().length),
2425
- hasVideoTrack: !!(config.stream && config.stream.getVideoTracks().length)
2426
- });
2427
- };
2428
-
2429
- event.track.onmute = (ev) => {
2430
- this._log('Remote track muted');
2431
-
2432
- let transceiver = config.pc.getTransceivers().find(
2433
- t => t.receiver.track === ev.target);
2434
- let mid = transceiver.mid || ev.target.id;
2435
- let source = Object.keys(config.streamMap).find(key => config.streamMap[key].includes(ev.target.id));
2436
- this.emit('remoteTrackMuted', {
2437
- id: handle.handleId,
2438
- mid,
2439
- constructId: this.constructId,
2440
- userId: decodeJanusDisplay(handle.userId)?.userId,
2441
- role: decodeJanusDisplay(handle.userId)?.role,
2442
- fullUserId: handle.userId,
2443
- stream: config.stream,
2444
- streamMap: structuredClone(config.streamMap),
2445
- source,
2446
- kind: ev.target.kind,
2447
- track: ev.target,
2448
- muted: true
2449
- });
2450
-
2451
- };
2452
-
2453
- event.track.onunmute = (ev) => {
2454
- this._log('Remote track unmuted');
2455
-
2456
- let transceiver = config.pc.getTransceivers().find(
2457
- t => t.receiver.track === ev.target);
2458
- let mid = transceiver.mid || ev.target.id;
2459
- let source = Object.keys(config.streamMap).find(key => config.streamMap[key].includes(ev.target.id));
2460
-
2461
- this.emit('remoteTrackMuted', {
2462
- id: handle.handleId,
2463
- mid,
2464
- constructId: this.constructId,
2465
- userId: decodeJanusDisplay(handle.userId)?.userId,
2466
- role: decodeJanusDisplay(handle.userId)?.role,
2467
- fullUserId: handle.userId,
2468
- stream: config.stream,
2469
- streamMap: structuredClone(config.streamMap),
2470
- source,
2471
- kind: ev.target.kind,
2472
- track: ev.target,
2473
- muted: false
2474
- });
2475
- };
2476
- }
2477
- };
2478
- }
2479
- }
2480
-
2481
- if (!config.dataChannel || !config.dataChannelOpen) {
2482
-
2483
- config.dataChannel = {};
2484
-
2485
- var onDataChannelMessage = (event) => {
2486
- this._handleDataEvents(handleId, 'message', event.data);
2487
- };
2488
- var onDataChannelStateChange = (event) => {
2489
- let label = event.target.label;
2490
- let protocol = event.target.protocol;
2491
- let state = config.dataChannel[label] ? config.dataChannel[label].readyState : "null";
2492
- this._handleDataEvents(handleId, 'state', {state, label} );
2493
- };
2494
- var onDataChannelError = (error) => {
2495
- this._handleDataEvents(handleId, 'error', {label: error?.channel?.label, error});
2496
- };
2497
-
2498
- const createDataChannel = (label, protocol, incoming) => {
2499
- let options = {ordered: true};
2500
- if(!incoming) {
2501
- if(protocol) {
2502
- options = {...options, protocol}
2503
- }
2504
- config.dataChannel[label] = config.pc.createDataChannel(label, options);
2505
- }
2506
- else {
2507
- config.dataChannel[label] = incoming;
2508
- }
2509
-
2510
- config.dataChannel[label].onmessage = onDataChannelMessage;
2511
- config.dataChannel[label].onopen = onDataChannelStateChange;
2512
- config.dataChannel[label].onclose = onDataChannelStateChange;
2513
- config.dataChannel[label].onerror = onDataChannelError;
2514
- }
2515
-
2516
- createDataChannel(this.defaultDataChannelLabel, null, null)
2517
- config.pc.ondatachannel = function (event) {
2518
- createDataChannel(event.channel.label, event.channel.protocol, event.channel)
2519
- };
2520
- }
2521
- }
2522
-
2523
- _webrtcPeer(handleId, jsep) {
2524
-
2525
- let handle = this._getHandle(handleId);
2526
- if (!handle) {
2527
- return Promise.reject({type: 'warning', id: 15, message: 'id non-existent', data: [handleId, 'rtc peer']});
2528
- }
2529
-
2530
- var config = handle.webrtcStuff;
2531
-
2532
- if (jsep !== undefined && jsep !== null) {
2533
- if (config.pc === null) {
2534
- this._log("No PeerConnection: if this is an answer, use createAnswer and not _webrtcPeer");
2535
- return Promise.resolve(null);
2536
- }
2537
-
2538
- return config.pc.setRemoteDescription(jsep)
2539
- .then(() => {
2540
- config.remoteSdp = jsep.sdp;
2541
- // Any trickle candidate we cached?
2542
- if (config.candidates && config.candidates.length > 0) {
2543
- for (var i = 0; i < config.candidates.length; i++) {
2544
- var candidate = config.candidates[i];
2545
- if (!candidate || candidate.completed === true) {
2546
- config.pc.addIceCandidate(null);
2547
- } else {
2548
- config.pc.addIceCandidate(candidate);
2549
- }
2550
- }
2551
- config.candidates = [];
2552
- }
2553
- // Done
2554
- return true;
2555
- })
2556
- .catch((e) => {
2557
- return Promise.reject({type: 'warning', id: 32, message: 'rtc peer', data: [handleId, e]});
2558
- });
2559
- } else {
2560
- return Promise.reject({type: 'warning', id: 22, message: 'rtc peer', data: [handleId, 'invalid jsep']});
2561
- }
2562
- }
2563
-
2564
- _iceRestart(handleId) {
2565
-
2566
- let handle = this._getHandle(handleId);
2567
- if (!handle) {
2568
- return;
2569
- }
2570
- var config = handle.webrtcStuff;
2571
-
2572
- // Already restarting;
2573
- if (config.isIceRestarting) {
2574
- return;
2575
- }
2576
-
2577
- config.isIceRestarting = true;
2578
-
2579
- if (this.handleId === handleId) {
2580
- this._log('Performing local ICE restart');
2581
- let hasAudio = !!(config.stream && config.stream.getAudioTracks().length > 0);
2582
- let hasVideo = !!(config.stream && config.stream.getVideoTracks().length > 0);
2583
- this._createAO('offer', handleId, true )
2584
- .then((jsep) => {
2585
- if (!jsep) {
2586
- return null;
2587
- }
2588
- return this.sendMessage(handleId, {
2589
- body: {"request": "configure", "keyframe": true, "audio": hasAudio, "video": hasVideo, "data": true, ...(this.recordingFilename ? {filename: this.recordingFilename} : {})},
2590
- jsep
2591
- }, false, false, 5);
2592
- })
2593
- .then(r => {
2594
- config.isIceRestarting = false;
2595
- this._log('ICE restart success');
2596
- })
2597
- .catch((e) => {
2598
- config.isIceRestarting = false;
2599
- this.emit('error', {type: 'warning', id: 28, message: 'iceRestart failed', data: e});
2600
- });
2601
- } else {
2602
- this._log('Performing remote ICE restart', handleId);
2603
- return this.sendMessage(handleId, {
2604
- body: {"request": "configure", "restart": true}
2605
- }, false, false, 5).then(() => {
2606
- }).then(() => {
2607
- config.isIceRestarting = false;
2608
- this._log('ICE restart success');
2609
- }).catch(() => {
2610
- config.isIceRestarting = false;
2611
- });
2612
- }
2613
- }
2614
-
2615
- _setupTransceivers(handleId, [audioSend, audioRecv, videoSend, videoRecv, audioTransceiver = null, videoTransceiver = null]) {
2616
- //TODO: this should be refactored to use handle's trackMap so we dont have to pass any parameters
2617
-
2618
- let handle = this._getHandle(handleId);
2619
- if (!handle) {
2620
- return null;
2621
- }
2622
- let config = handle.webrtcStuff;
2623
-
2624
- const setTransceiver = (transceiver, send, recv, kind = 'audio') => {
2625
- if (!send && !recv) {
2626
- // disabled: have we removed it?
2627
- if (transceiver) {
2628
- if (transceiver.setDirection) {
2629
- transceiver.setDirection("inactive");
2630
- } else {
2631
- transceiver.direction = "inactive";
2632
- }
2633
- }
2634
- } else {
2635
- if (send && recv) {
2636
- if (transceiver) {
2637
- if (transceiver.setDirection) {
2638
- transceiver.setDirection("sendrecv");
2639
- } else {
2640
- transceiver.direction = "sendrecv";
2641
- }
2642
- }
2643
- } else if (send && !recv) {
2644
- if (transceiver) {
2645
- if (transceiver.setDirection) {
2646
- transceiver.setDirection("sendonly");
2647
- } else {
2648
- transceiver.direction = "sendonly";
2649
- }
2650
- }
2651
- } else if (!send && recv) {
2652
- if (transceiver) {
2653
- if (transceiver.setDirection) {
2654
- transceiver.setDirection("recvonly");
2655
- } else {
2656
- transceiver.direction = "recvonly";
2657
- }
2658
- } else {
2659
- // In theory, this is the only case where we might not have a transceiver yet
2660
- config.pc.addTransceiver(kind, {direction: "recvonly"});
2661
- }
2662
- }
2663
- }
2664
- }
2665
-
2666
- // if we're passing any transceivers, we work only on them, doesn't matter if one of them is null
2667
- if(audioTransceiver || videoTransceiver) {
2668
- if(audioTransceiver) {
2669
- setTransceiver(audioTransceiver, audioSend, audioRecv, 'audio');
2670
- }
2671
- if(videoTransceiver) {
2672
- setTransceiver(videoTransceiver, videoSend, videoRecv, 'video');
2673
- }
2674
- }
2675
- // else we work on all transceivers
2676
- else {
2677
- let transceivers = config.pc.getTransceivers();
2678
- if (transceivers && transceivers.length > 0) {
2679
- for (let i in transceivers) {
2680
- let t = transceivers[i];
2681
- if (
2682
- (t.sender && t.sender.track && t.sender.track.kind === "audio") ||
2683
- (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")
2684
- ) {
2685
- setTransceiver(t, audioSend, audioRecv, 'audio');
2686
- }
2687
- if (
2688
- (t.sender && t.sender.track && t.sender.track.kind === "video") ||
2689
- (t.receiver && t.receiver.track && t.receiver.track.kind === "video")
2690
- ) {
2691
- setTransceiver(t, videoSend, videoRecv, 'video');
2692
- }
2693
- }
2694
- }
2695
- }
2696
- }
2697
-
2698
- _createAO(type = 'offer', handleId, iceRestart = false, ) {
2699
-
2700
- let handle = this._getHandle(handleId);
2701
- if (!handle) {
2702
- return Promise.reject({
2703
- type: 'warning',
2704
- id: 15,
2705
- message: 'id non-existent',
2706
- data: [handleId, 'createAO', type]
2707
- });
2708
- }
2709
-
2710
- let methodName = null;
2711
- if (type === 'offer') {
2712
- methodName = 'createOffer'
2713
- } else {
2714
- methodName = 'createAnswer'
2715
- }
2716
-
2717
- let config = handle.webrtcStuff;
2718
- let mediaConstraints = {};
2719
-
2720
- if (iceRestart) {
2721
- mediaConstraints["iceRestart"] = true;
2722
- }
2723
-
2724
- return config.pc[methodName](mediaConstraints)
2725
- .then( (response) => {
2726
-
2727
- // if type offer and its me and we want dtx we mungle the sdp
2728
- if(handleId === this.handleId && type === 'offer' && this.enableDtx) {
2729
- // enable DTX
2730
- response.sdp = response.sdp.replace("useinbandfec=1", "useinbandfec=1;usedtx=1")
2731
- }
2732
-
2733
- config.mySdp = response.sdp;
2734
- let _p = config.pc.setLocalDescription(response)
2735
- .catch((e) => {
2736
- return Promise.reject({
2737
- type: 'warning',
2738
- id: 24,
2739
- message: 'setLocalDescription',
2740
- data: [handleId, e]
2741
- })
2742
- });
2743
- config.mediaConstraints = mediaConstraints;
2744
- if (!config.iceDone && !config.trickle) {
2745
- // Don't do anything until we have all candidates
2746
- return Promise.resolve(null);
2747
- }
2748
-
2749
- // JSON.stringify doesn't work on some WebRTC objects anymore
2750
- // See https://code.google.com/p/chromium/issues/detail?id=467366
2751
- var jsep = {
2752
- "type": response.type,
2753
- "sdp": response.sdp
2754
- };
2755
-
2756
- if(response.e2ee)
2757
- jsep.e2ee = true;
2758
- if(response.rid_order === "hml" || response.rid_order === "lmh")
2759
- jsep.rid_order = response.rid_order;
2760
- if(response.force_relay)
2761
- jsep.force_relay = true;
2762
-
2763
- return _p.then(() => jsep)
2764
- }, (e) => {
2765
- return Promise.reject({type: 'warning', id: 25, message: methodName, data: [handleId, e]})
2766
- });
2767
-
2768
- }
2769
-
2770
- _addSimulcastToSDP(sdp) {
2771
- // Split SDP into lines
2772
- let sdpLines = sdp.split('\n');
2773
-
2774
- // Find the index of the video m-line
2775
- let videoIndex = sdpLines.findIndex(line => line.startsWith('m=video'));
2776
- if (videoIndex === -1) {
2777
- // No video m-line found
2778
- return sdp;
2779
- }
2780
-
2781
- // We need to insert the lines after the mid attribute
2782
- // Find the index of the 'a=mid' line in the video section
2783
- let midIndex = sdpLines.findIndex((line, index) =>
2784
- index > videoIndex && line.startsWith('a=mid:')
2785
- );
2786
-
2787
- if (midIndex === -1) {
2788
- // No 'a=mid' line found in video m-section
2789
- return sdp;
2790
- }
2791
-
2792
- // Prepare the simulcast attributes
2793
- const simulcastLines = [
2794
- 'a=rid:l recv',
2795
- 'a=rid:m recv',
2796
- 'a=rid:h recv',
2797
- 'a=simulcast:recv l;m;h'
2798
- ];
2799
-
2800
- // Prepare the extmap attributes for RID and Repaired RID
2801
- const extmapId = sdpLines.reduce((maxId, line) => {
2802
- const match = line.match(/a=extmap:(\d+)/);
2803
- return match ? Math.max(maxId, parseInt(match[1])) : maxId;
2804
- }, 0);
2805
-
2806
- const extmapLines = [
2807
- `a=extmap:${extmapId + 1} urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id`,
2808
- `a=extmap:${extmapId + 2} urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id`
2809
- ];
2810
-
2811
- // Insert the extmap lines after the last existing extmap in the video section
2812
- let lastExtmapIndex = midIndex;
2813
- for (let i = midIndex + 1; i < sdpLines.length; i++) {
2814
- if (sdpLines[i].startsWith('a=extmap:')) {
2815
- lastExtmapIndex = i;
2816
- } else if (sdpLines[i].startsWith('a=')) {
2817
- // Reached another attribute
2818
- break;
2819
- }
2820
- }
2821
-
2822
- // Check if the attributes are already present
2823
- const hasSimulcast = sdpLines.some(line => line.startsWith('a=simulcast'));
2824
- const hasRidExtmap = sdpLines.some(line => line.includes('urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id'));
2825
-
2826
- if (!hasRidExtmap) {
2827
- // Insert the extmap lines
2828
- sdpLines.splice(lastExtmapIndex + 1, 0, ...extmapLines);
2829
- }
2830
-
2831
- if (!hasSimulcast) {
2832
- // Insert the simulcast lines after the 'a=mid' line
2833
- sdpLines.splice(midIndex + 1, 0, ...simulcastLines);
2834
- }
2835
-
2836
- // Return the modified SDP
2837
- return sdpLines.join('\n');
2838
- }
2839
-
2840
-
2841
- _publishRemote(handleId, jsep) {
2842
- let handle = this._getHandle(handleId);
2843
- if (!handle) {
2844
- return Promise.reject({
2845
- type: 'warning',
2846
- id: 15,
2847
- message: 'id non-existent',
2848
- data: [handleId, 'publish remote participant']
2849
- })
2850
- }
2851
-
2852
- this._webrtc(handleId, true);
2853
-
2854
- let config = handle.webrtcStuff;
2855
-
2856
- if (jsep) {
2857
-
2858
- // Just For Debugging
2859
- // Munge the SDP before setting the remote description
2860
- // if (jsep && jsep.sdp) {
2861
- // jsep.sdp = this._addSimulcastToSDP(jsep.sdp);
2862
- // }
2863
-
2864
- return config.pc.setRemoteDescription(jsep)
2865
- .then(() => {
2866
- config.remoteSdp = jsep.sdp;
2867
- // Any trickle candidate we cached?
2868
- if (config.candidates && config.candidates.length > 0) {
2869
- for (var i = 0; i < config.candidates.length; i++) {
2870
- var candidate = config.candidates[i];
2871
- if (!candidate || candidate.completed === true) {
2872
- // end-of-candidates
2873
- config.pc.addIceCandidate(null);
2874
- } else {
2875
- // New candidate
2876
- config.pc.addIceCandidate(candidate);
2877
- }
2878
- }
2879
- config.candidates = [];
2880
- }
2881
-
2882
- this._setupTransceivers(handleId, [false, true, false, true]);
2883
-
2884
- // Create the answer now
2885
- return this._createAO('answer', handleId, false)
2886
- .then(_jsep => {
2887
- if (!_jsep) {
2888
- this.emit('error', {
2889
- type: 'warning',
2890
- id: 19,
2891
- message: 'publish remote participant',
2892
- data: [handleId, 'no jsep']
2893
- });
2894
- return Promise.resolve();
2895
- }
2896
- return this.sendMessage(handleId, {
2897
- "body": {"request": "start", ...(this.roomId && {"room": this.roomId}), ...(this.pin && {pin: this.pin})},
2898
- "jsep": _jsep
2899
- });
2900
- })
2901
- }, (e) => Promise.reject({
2902
- type: 'warning',
2903
- id: 23,
2904
- message: 'setRemoteDescription',
2905
- data: [handleId, e]
2906
- }));
2907
-
2908
- } else {
2909
- return Promise.resolve();
2910
- }
2911
-
2912
- }
2913
-
2914
- //Public methods
2915
- _republishOnTrackEnded(source) {
2916
- let handle = this._getHandle(this.handleId);
2917
- if (!handle) {
2918
- return;
2919
- }
2920
- let config = handle.webrtcStuff;
2921
- if (!config.stream) {
2922
- return;
2923
- }
2924
- let sourceTrackIds = (config.streamMap[source] || []);
2925
- let remainingTracks = [];
2926
- for(let i = 0; i < sourceTrackIds.length; i++) {
2927
- let foundTrack = config.stream.getTracks().find(t => t.id === sourceTrackIds[i]);
2928
- if(foundTrack) {
2929
- remainingTracks.push(foundTrack);
2930
- }
2931
- }
2932
- if (remainingTracks.length) {
2933
- let stream = new MediaStream();
2934
- remainingTracks.forEach(track => stream.addTrack(track));
2935
- return this.publishLocal(stream, source);
2936
- }
2937
- else {
2938
- return this.publishLocal(null, source);
2939
- }
2940
- };
2941
-
2942
-
2943
- publishLocal(stream = null, source = 'camera0') {
2944
-
2945
- if(!this.isPossibleToPublish) {
2946
- return Promise.reject({
2947
- type: 'warning',
2948
- id: 18,
2949
- message: 'Either not connected or disconnecting',
2950
- })
2951
- }
2952
-
2953
- if(stream?.getVideoTracks()?.length > 1) {
2954
- return Promise.reject({
2955
- type: 'warning',
2956
- id: 30,
2957
- message: 'multiple video tracks not supported',
2958
- data: null
2959
- })
2960
- }
2961
-
2962
- if(stream?.getAudioTracks()?.length > 1) {
2963
- return Promise.reject({
2964
- type: 'warning',
2965
- id: 30,
2966
- message: 'multiple audio tracks not supported',
2967
- data: null
2968
- })
2969
- }
2970
-
2971
- let handle = this._getHandle(this.handleId);
2972
- if (!handle) {
2973
- return Promise.reject({
2974
- type: 'error',
2975
- id: 31,
2976
- message: 'no local id, connect before publishing',
2977
- data: null
2978
- })
2979
- }
2980
-
2981
- this.emit('publishing', true);
2982
-
2983
- this._webrtc(this.handleId);
2984
-
2985
- let config = handle.webrtcStuff;
2986
-
2987
- if (!config.stream) {
2988
- config.stream = new MediaStream();
2989
- }
2990
-
2991
- let needsNegotiation = !this.isPublished;
2992
- let transceivers = config.pc.getTransceivers();
2993
- let existingTracks = [...(config.streamMap[source] || [])];
2994
-
2995
- if(stream?.getTracks().length) {
2996
- config.streamMap[source] = stream?.getTracks()?.map(track => track.id) || [];
2997
- } else {
2998
- delete config.streamMap[source];
2999
- }
3000
-
3001
- // remove old audio track related to this source
3002
- let oldAudioStream = config?.stream?.getAudioTracks()?.find(track => existingTracks.includes(track.id));
3003
- if (oldAudioStream) {
3004
- try {
3005
- oldAudioStream.stop();
3006
-
3007
- } catch (e) {
3008
- this._log(e);
3009
- }
3010
- config.stream.removeTrack(oldAudioStream);
3011
- }
3012
-
3013
- // remove old video track related to this source
3014
- let oldVideoStream = config?.stream?.getVideoTracks()?.find(track => existingTracks.includes(track.id));
3015
- if (oldVideoStream) {
3016
- try {
3017
- oldVideoStream.stop();
3018
- } catch (e) {
3019
- this._log(e);
3020
- }
3021
- config.stream.removeTrack(oldVideoStream);
3022
- }
3023
-
3024
- const simulcastConfigForSource = this._findSimulcastConfig(source, this.simulcastSettings);
3025
- let audioTrackReplacePromise = Promise.resolve();
3026
- let videoTrackReplacePromise = Promise.resolve();
3027
-
3028
- let audioTransceiver = null;
3029
- let videoTransceiver = null;
3030
- let replaceAudio = stream?.getAudioTracks()?.length;
3031
- let replaceVideo = stream?.getVideoTracks()?.length;
3032
-
3033
- for(const transceiver of transceivers) {
3034
- if(['sendonly', 'sendrecv'].includes(transceiver.currentDirection) && transceiver.sender?.track?.kind === 'audio' && existingTracks.includes(transceiver.sender?.track?.id)) {
3035
- audioTransceiver = transceiver;
3036
- }
3037
- else if(['sendonly', 'sendrecv'].includes(transceiver.currentDirection) && transceiver.sender?.track?.kind === 'video' && existingTracks.includes(transceiver.sender?.track?.id)) {
3038
- videoTransceiver = transceiver;
3039
- }
3040
-
3041
- // Reusing existing transceivers
3042
- // TODO: if we start using different codecs for different sources, we need to check for that here
3043
-
3044
- else if(transceiver.currentDirection === 'inactive' && transceiver.sender?.getParameters()?.codecs?.find(c => c.mimeType.indexOf('audio') > -1) && replaceAudio && !audioTransceiver) {
3045
- audioTransceiver = transceiver;
3046
- needsNegotiation = true;
3047
- }
3048
- else if(transceiver.currentDirection === 'inactive' && transceiver.sender?.getParameters()?.codecs?.find(c => c.mimeType.indexOf('video') > -1) && replaceVideo && !videoTransceiver) {
3049
- videoTransceiver = transceiver;
3050
- needsNegotiation = true;
3051
- }
3052
- }
3053
-
3054
- if (replaceAudio) {
3055
- config.stream.addTrack(stream.getAudioTracks()[0]);
3056
- if (audioTransceiver && audioTransceiver.sender) {
3057
- audioTrackReplacePromise = audioTransceiver.sender.replaceTrack(stream.getAudioTracks()[0]);
3058
- } else {
3059
- config.pc.addTrack(stream.getAudioTracks()[0], config.stream);
3060
- needsNegotiation = true;
3061
- }
3062
- }
3063
- else {
3064
- if (audioTransceiver && audioTransceiver.sender) {
3065
- audioTrackReplacePromise = audioTransceiver.sender.replaceTrack(null);
3066
- needsNegotiation = true;
3067
- }
3068
- }
3069
-
3070
- if (replaceVideo) {
3071
- config.stream.addTrack(stream.getVideoTracks()[0]);
3072
- if (videoTransceiver && videoTransceiver.sender) {
3073
- videoTrackReplacePromise = videoTransceiver.sender.replaceTrack(stream.getVideoTracks()[0]);
3074
- } else {
3075
- if(!this.simulcast) {
3076
- config.pc.addTrack(stream.getVideoTracks()[0], config.stream);
3077
- }
3078
- else {
3079
- config.pc.addTransceiver(stream.getVideoTracks()[0], {
3080
- direction: 'sendonly',
3081
- streams: [config.stream],
3082
- sendEncodings: structuredClone(simulcastConfigForSource?.bitrates)
3083
- })
3084
- }
3085
- needsNegotiation = true;
3086
- }
3087
- }
3088
- else {
3089
- if (videoTransceiver && videoTransceiver.sender) {
3090
- videoTrackReplacePromise = videoTransceiver.sender.replaceTrack(null);
3091
- needsNegotiation = true;
3092
- }
3093
- }
3094
-
3095
-
3096
- this.isAudioEnabed = !!(config.stream && config.stream.getAudioTracks().length > 0);
3097
- this.isVideoEnabled = !!(config.stream && config.stream.getVideoTracks().length > 0);
3098
-
3099
- // we possibly created new transceivers, so we need to get them again
3100
- transceivers = config.pc.getTransceivers();
3101
- existingTracks = [...(config.streamMap[source] || [])];
3102
- if(!audioTransceiver) {
3103
- audioTransceiver = transceivers.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'audio' && existingTracks.includes(transceiver.sender.track.id))
3104
- }
3105
- if(!videoTransceiver) {
3106
- videoTransceiver = transceivers.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'video' && existingTracks.includes(transceiver.sender.track.id))
3107
- }
3108
-
3109
- let hasAudio = !!(stream && stream.getAudioTracks().length > 0);
3110
- let hasVideo = !!(stream && stream.getVideoTracks().length > 0);
3111
-
3112
- this._setupTransceivers(this.handleId, [hasAudio, false, hasVideo, false, audioTransceiver, videoTransceiver]);
3113
-
3114
- const emitEvents = () => {
3115
- this.isPublished = true;
3116
-
3117
- if(!config.stream.onremovetrack) {
3118
- config.stream.onremovetrack = (ev) => {};
3119
- }
3120
-
3121
- let republishTimeoutId = null;
3122
- let tracks = config.stream.getTracks();
3123
-
3124
- this.emit('addLocalParticipant', {
3125
- tid: generateUUID(),
3126
- id: handle.handleId,
3127
- userId: decodeJanusDisplay(handle.userId)?.userId,
3128
- role: decodeJanusDisplay(this.display)?.role,
3129
- stream: tracks.length ? config.stream : null, // that null is there due to backward compatibility
3130
- track: null,
3131
- streamMap: structuredClone(config.streamMap),
3132
- source,
3133
- adding: false,
3134
- removing: false,
3135
- constructId: this.constructId,
3136
- hasAudioTrack: hasAudio,
3137
- hasVideoTrack: hasVideo
3138
- });
3139
-
3140
- if(tracks.length) {
3141
- tracks.forEach(track => {
3142
- // used as a flag to not emit tracks that been already emitted
3143
- if(!track.onended) {
3144
-
3145
- this.emit('addLocalParticipant', {
3146
- tid: generateUUID(),
3147
- id: handle.handleId,
3148
- userId: decodeJanusDisplay(handle.userId)?.userId,
3149
- role: decodeJanusDisplay(this.display)?.role,
3150
- track,
3151
- stream: config.stream,
3152
- streamMap: structuredClone(config.streamMap),
3153
- source,
3154
- adding: true,
3155
- removing: false,
3156
- constructId: this.constructId,
3157
- hasAudioTrack: hasAudio,
3158
- hasVideoTrack: hasVideo
3159
- });
3160
-
3161
- track.onended = (ev) => {
3162
- config.stream.removeTrack(track);
3163
- clearTimeout(republishTimeoutId);
3164
- republishTimeoutId = setTimeout(() => {
3165
- this._republishOnTrackEnded(source);
3166
- }, 100);
3167
-
3168
- }
3169
- }
3170
- })
3171
- }
3172
-
3173
- this.isMuted = [];
3174
- for(const source of Object.keys(config.streamMap)) {
3175
- const audioTrack = config.stream?.getAudioTracks()?.find(track => config.streamMap[source].includes(track.id));
3176
- const videoTrack = config.stream?.getVideoTracks()?.find(track => config.streamMap[source].includes(track.id));
3177
- this.isMuted.push({type: 'audio', value: !audioTrack || !audioTrack.enabled, source, mid: transceivers.find(transceiver => transceiver.sender?.track?.kind === 'audio' && transceiver.sender?.track?.id === audioTrack?.id)?.mid});
3178
- this.isMuted.push({type: 'video', value: !videoTrack || !videoTrack.enabled, source, mid: transceivers.find(transceiver => transceiver.sender?.track?.kind === 'video' && transceiver.sender?.track?.id === videoTrack?.id)?.mid});
3179
- this.emit('localHasVideo', !!videoTrack, source);
3180
- this.emit('localHasAudio', !!audioTrack, source);
3181
- }
3182
- for(const val of this.isMuted) {
3183
- this.emit('localMuted', {...val});
3184
- }
3185
- this.emit('published', true);
3186
- this.emit('publishing', false);
3187
- return this;
3188
- };
3189
-
3190
- // this should be enough
3191
- if(!needsNegotiation) {
3192
- return Promise
3193
- .all([audioTrackReplacePromise, videoTrackReplacePromise])
3194
- .then(() => emitEvents())
3195
- }
3196
-
3197
- this._log('Starting negotiation');
3198
-
3199
- return Promise
3200
- .all([audioTrackReplacePromise, videoTrackReplacePromise])
3201
- .then(() => this._createAO('offer', this.handleId, false))
3202
- .then((jsep) => {
3203
- if (!jsep) {
3204
- return null;
3205
- }
3206
- //HOTFIX: Temporary fix for Safari 13
3207
- if (jsep.sdp && jsep.sdp.indexOf("\r\na=ice-ufrag") === -1) {
3208
- jsep.sdp = jsep.sdp.replace("\na=ice-ufrag", "\r\na=ice-ufrag");
3209
- }
3210
-
3211
-
3212
- let descriptions = [];
3213
- Object.keys(config.streamMap).forEach(source => {
3214
- const simulcastConfigForSource = this._findSimulcastConfig(source, this.simulcastSettings);
3215
- config.streamMap[source].forEach(trackId => {
3216
- let t = transceivers.find(transceiver => transceiver.sender.track && transceiver.sender.track.id === trackId)
3217
- if(t) {
3218
- descriptions.push({mid: t.mid, description: JSON.stringify({source, simulcastBitrates: simulcastConfigForSource?.bitrates, intercomGroups: this._talkIntercomChannels})});
3219
- }
3220
- })
3221
- });
3222
-
3223
- return this.sendMessage(this.handleId, {
3224
- body: {
3225
- "request": "configure",
3226
- "audio": this.isAudioEnabed,
3227
- "video": this.isVideoEnabled,
3228
- "data": true,
3229
- ...(this.recordingFilename ? {filename: this.recordingFilename} : {}),
3230
- descriptions: descriptions
3231
- },
3232
- jsep
3233
- });
3234
- })
3235
- .then((r) => {
3236
- if (this._isDataChannelOpen === true) {
3237
- return Promise.resolve(r)
3238
- } else {
3239
- return new Promise((resolve, reject) => {
3240
-
3241
- let dataChannelTimeoutId = null;
3242
-
3243
- let _resolve = (val) => {
3244
- if (val) {
3245
- clearTimeout(dataChannelTimeoutId);
3246
- this._abortController.signal.removeEventListener('abort', _rejectAbort);
3247
- this.off('dataChannel', _resolve, this);
3248
- resolve(this);
3249
- }
3250
- };
3251
-
3252
- let _rejectTimeout = () => {
3253
- this.off('dataChannel', _resolve, this);
3254
- this._abortController.signal.removeEventListener('abort', _rejectAbort);
3255
- reject({type: 'error', id: 27, message: 'Data channel did not open', data: null});
3256
- }
3257
-
3258
- let _rejectAbort = () => {
3259
- this._abortController.signal.removeEventListener('abort', _rejectAbort);
3260
- clearTimeout(dataChannelTimeoutId);
3261
- this.off('dataChannel', _resolve, this);
3262
- reject({type: 'warning', id: 17, message: 'Connection cancelled'})
3263
- }
3264
-
3265
- dataChannelTimeoutId = setTimeout(_rejectTimeout, 10000);
3266
- this._abortController.signal.addEventListener('abort', _rejectAbort);
3267
-
3268
- this.on('dataChannel', _resolve, this);
3269
- });
3270
- }
3271
- })
3272
- .then(() => emitEvents())
3273
- .catch(e => {
3274
- this.emit('publishing', false);
3275
- return Promise.reject(e);
3276
- })
3277
- }
3278
-
3279
- unpublishLocal(dontWait = false) {
3280
- return this.isPublished
3281
- ? this.sendMessage(this.handleId, {body: {"request": "unpublish"}}, dontWait)
3282
- .finally(r => {
3283
- this.isPublished = false;
3284
- this.emit('published', false);
3285
- return r
3286
- })
3287
- : Promise.resolve()
3288
- }
3289
-
3290
- getUserTalkIntercomChannels(userId) {
3291
- let talkIntercomChannels = []
3292
- // TODO: check if we don't need full userId
3293
- let handle = this._getHandle(null, null, userId);
3294
- if (handle) {
3295
- let config = handle.webrtcStuff;
3296
- talkIntercomChannels = config.tracksMap.reduce((acc, val) => {
3297
- if(val.description) {
3298
- try {
3299
- let description = JSON.parse(val.description);
3300
- if(description.intercomGroups) {
3301
- description.intercomGroups.forEach(group => {
3302
- if(!acc.includes(group)) {
3303
- acc.push(group);
3304
- }
3305
- });
3306
- }
3307
- }
3308
- catch(e) {}
3309
- }
3310
- return acc;
3311
- }, []);
3312
- }
3313
- return talkIntercomChannels;
3314
- }
3315
-
3316
- toggleAudio(value = null, source = 'camera0', mid) {
3317
- let handle = this._getHandle(this.handleId);
3318
- if (!handle) {
3319
- this.emit('error', {
3320
- type: 'warning',
3321
- id: 21,
3322
- message: 'no local id, connect first', data: null
3323
- });
3324
- return;
3325
- }
3326
- let config = handle.webrtcStuff;
3327
- let transceivers = config.pc?.getTransceivers();
3328
- let transceiver = null;
3329
- if(source) {
3330
- transceiver = transceivers?.find(t => t.sender && t.sender.track && t.receiver.track.kind === "audio" && (config.streamMap[source] || []).includes(t.sender.track.id));
3331
- }
3332
- else {
3333
- transceiver = transceivers?.find(t => t.sender && t.sender.track && t.receiver.track.kind === "audio" && (mid ? t.mid === mid : true));
3334
- }
3335
- if (transceiver) {
3336
- transceiver.sender.track.enabled = value !== null ? !!value : !transceiver.sender.track.enabled;
3337
- }
3338
-
3339
- this.isMuted = [];
3340
- for(const source of Object.keys(config.streamMap)) {
3341
- const audioTrack = config.stream?.getAudioTracks()?.find(track => config.streamMap[source].includes(track.id));
3342
- const audioTransceiver = transceivers?.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'audio' && transceiver.sender.track.id === audioTrack?.id);
3343
- const videoTrack = config.stream?.getVideoTracks()?.find(track => config.streamMap[source].includes(track.id));
3344
- const videoTransceiver = transceivers?.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'video' && transceiver.sender.track.id === videoTrack?.id);
3345
- this.isMuted.push({type: 'audio', value: !audioTrack || !audioTransceiver || !audioTransceiver?.sender?.track?.enabled , source, mid: audioTransceiver?.mid});
3346
- this.isMuted.push({type: 'video', value: !videoTrack || !videoTransceiver || !videoTransceiver?.sender?.track?.enabled, source, mid: videoTransceiver?.mid});
3347
- }
3348
- for(let val of this.isMuted) {
3349
- this.emit('localMuted', {...val});
3350
- }
3351
- }
3352
-
3353
- toggleVideo(value = null, source = 'camera0', mid) {
3354
- let handle = this._getHandle(this.handleId);
3355
- if (!handle) {
3356
- this.emit('error', {
3357
- type: 'warning',
3358
- id: 21,
3359
- message: 'no local id, connect first', data: null
3360
- });
3361
- return;
3362
- }
3363
- let config = handle.webrtcStuff;
3364
- let transceivers = config.pc?.getTransceivers();
3365
- let transceiver = null;
3366
- if(source) {
3367
- transceiver = transceivers?.find(t => t.sender && t.sender.track && t.receiver.track.kind === "video" && (config.streamMap[source] || []).includes(t.sender.track.id));
3368
- }
3369
- else {
3370
- transceiver = transceivers?.find(t => t.sender && t.sender.track && t.receiver.track.kind === "video" && (mid ? t.mid === mid : true));
3371
- }
3372
- if (transceiver) {
3373
- transceiver.sender.track.enabled = value !== null ? !!value : !transceiver.sender.track.enabled;
3374
- }
3375
- this.isMuted = [];
3376
- for(const source of Object.keys(config.streamMap)) {
3377
- const audioTrack = config.stream?.getAudioTracks()?.find(track => config.streamMap[source].includes(track.id));
3378
- const audioTransceiver = transceivers?.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'audio' && transceiver.sender.track.id === audioTrack?.id);
3379
- const videoTrack = config.stream?.getVideoTracks()?.find(track => config.streamMap[source].includes(track.id));
3380
- const videoTransceiver = transceivers?.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'video' && transceiver.sender.track.id === videoTrack?.id);
3381
- this.isMuted.push({type: 'audio', value: !audioTrack || !audioTransceiver || !audioTransceiver?.sender?.track?.enabled , source, mid: audioTransceiver?.mid});
3382
- this.isMuted.push({type: 'video', value: !videoTrack || !videoTransceiver || !videoTransceiver?.sender?.track?.enabled, source, mid: videoTransceiver?.mid});
3383
- }
3384
- for(let val of this.isMuted) {
3385
- this.emit('localMuted', {...val});
3386
- }
3387
- }
3388
-
3389
- requestKeyFrame(handleId, mid) {
3390
- this.sendMessage(handleId, {
3391
- "body": {
3392
- "request": "configure",
3393
- "keyframe": true,
3394
- ...(mid !== undefined ? {streams: [{mid,keyframe:true}]} : {})
3395
- }
3396
- }).catch(() => null)
3397
- }
3398
-
3399
- _setSelectedSubstream(handleId, mid, substream) {
3400
- let handle = this._getHandle(handleId);
3401
- if(handle) {
3402
- let config = handle.webrtcStuff;
3403
- if(!mid) {
3404
- Object.keys(config.selectedSubstream).forEach(mid => {
3405
- config.selectedSubstream[mid] = substream;
3406
- })
3407
- } else {
3408
- config.selectedSubstream[mid] = substream;
3409
- }
3410
- }
3411
- }
3412
-
3413
- overrideSimulcastSettings(handleId, mid, source, settings = {}) {
3414
- const {mode, defaultSubstream} = settings;
3415
- let handle = this._getHandle(handleId);
3416
- if(!handle) {
3417
- return Promise.resolve();
3418
- }
3419
- let config = handle.webrtcStuff;
3420
- if(source !== undefined || mid !== undefined) {
3421
- if(mid === undefined) {
3422
- let transceivers = config.pc.getTransceivers();
3423
- for(let trackId of config.streamMap[source]) {
3424
- let transceiver = transceivers.find(transceiver => transceiver.receiver.track && transceiver.receiver.track.kind === 'video' && transceiver.receiver.track.id === trackId)
3425
- if(transceiver) {
3426
- mid = transceiver.mid;
3427
- break;
3428
- }
3429
- }
3430
- }
3431
-
3432
- if(mid !== undefined) {
3433
- if(!config.overriddenSimulcastMode[mid]) {
3434
- config.overriddenSimulcastMode[mid] = {};
3435
- }
3436
- config.overriddenSimulcastMode[mid]['defaultSubstream'] = defaultSubstream;
3437
- config.overriddenSimulcastMode[mid]['mode'] = mode;
3438
- return true;
3439
- }
3440
- else {
3441
- return false;
3442
- }
3443
- }
3444
- }
3445
-
3446
- selectSubStream(handleId, substream = 2, source, mid, manual = false) {
3447
- this._log('Select substream called for handle:', handleId, 'Source or mid:', source ? source : mid, 'Substream:', substream);
3448
- let handle = this._getHandle(handleId);
3449
- if(!handle) {
3450
- return Promise.resolve();
3451
- }
3452
- let config = handle.webrtcStuff;
3453
- return new Promise((resolve, reject) => {
3454
- let messageTimeoutId;
3455
-
3456
- let clearListeners = () => {
3457
- clearTimeout(messageTimeoutId);
3458
- this._abortController.signal.removeEventListener('abort', abortResponse);
3459
- this.off('longPollEvent', parseResponse);
3460
- this.ws?.removeEventListener('message', parseResponse);
3461
- };
3462
-
3463
- let abortResponse = () => {
3464
- clearListeners();
3465
- reject('aborted');
3466
- };
3467
-
3468
- let parseResponse = (event) => {
3469
- let json = typeof event.data === 'string'
3470
- ? JSON.parse(event.data)
3471
- : event.data;
3472
-
3473
- var sender = json["sender"];
3474
- if(sender === handleId) {
3475
- let plugindata = json["plugindata"] || {};
3476
- let msg = plugindata["data"] || {};
3477
- let substream = msg["substream"];
3478
- if(substream !== undefined && substream !== null && (mid !== undefined ? msg["mid"] === mid : true)) {
3479
- clearListeners();
3480
- resolve({substream, sender});
3481
- }
3482
- }
3483
- }
3484
-
3485
- if(source !== undefined || mid !== undefined) {
3486
- if(mid === undefined) {
3487
- let transceivers = config.pc.getTransceivers();
3488
- for(let trackId of config.streamMap[source]) {
3489
- let transceiver = transceivers.find(transceiver => transceiver.receiver.track && transceiver.receiver.track.kind === 'video' && transceiver.receiver.track.id === trackId)
3490
- if(transceiver) {
3491
- mid = transceiver.mid;
3492
- break;
3493
- }
3494
- }
3495
- }
3496
- if(mid !== undefined) {
3497
-
3498
- if(!config.overriddenSimulcastMode[mid]) {
3499
- config.overriddenSimulcastMode[mid] = {};
3500
- }
3501
-
3502
- if(substream === null) {
3503
-
3504
- if(manual) {
3505
- // reset to previous state
3506
- config.overriddenSimulcastMode[mid]['defaultSubstream'] = null;
3507
- config.overriddenSimulcastMode[mid]['mode'] = null;
3508
- }
3509
-
3510
- resolve({substream, sender: handleId});
3511
- return;
3512
- }
3513
-
3514
- if(manual) {
3515
- config.overriddenSimulcastMode[mid]['defaultSubstream'] = substream;
3516
- config.overriddenSimulcastMode[mid]['mode'] = "manual";
3517
- }
3518
-
3519
- this.on('longPollEvent', parseResponse);
3520
- this.ws?.addEventListener('message', parseResponse);
3521
- this._abortController.signal.addEventListener('abort', abortResponse);
3522
- messageTimeoutId = setTimeout(() => {
3523
- clearListeners();
3524
- reject('timeout');
3525
- }, 10000);
3526
-
3527
- this.sendMessage(handleId, {
3528
- "body": {
3529
- "request": "configure",
3530
- "streams": [
3531
- {
3532
- mid, substream: parseInt(substream)
3533
- }
3534
- ]
3535
- }
3536
- })
3537
- } else {
3538
- reject('no mid found');
3539
- }
3540
- }
3541
- else {
3542
- reject('no source or mid');
3543
- }
3544
- });
3545
- }
3546
-
3547
- setTalkIntercomChannels(groups = ['participants']) {
3548
-
3549
- if(typeof groups !== 'object' || !("length" in groups)) {
3550
- this._log('setTalkIntercomChannels: groups must be an array');
3551
- groups = [groups];
3552
- }
3553
-
3554
- this._talkIntercomChannels = structuredClone(groups);
3555
- let handle = this._getHandle(this.handleId);
3556
- if (!handle) {
3557
- return Promise.resolve();
3558
- }
3559
- let config = handle.webrtcStuff;
3560
- let transceivers = config.pc.getTransceivers();
3561
- let descriptions = [];
3562
- Object.keys(config.streamMap).forEach(source => {
3563
- const simulcastConfigForSource = this._findSimulcastConfig(source, this.simulcastSettings);
3564
- config.streamMap[source].forEach(trackId => {
3565
- let t = transceivers.find(transceiver => transceiver.sender.track && transceiver.sender.track.id === trackId)
3566
- if(t) {
3567
- descriptions.push({mid: t.mid, description: JSON.stringify({simulcastBitrates: simulcastConfigForSource?.bitrates, intercomGroups: groups, source:source})});
3568
- }
3569
- })
3570
- });
3571
- return this.sendMessage(this.handleId, {
3572
- body: {
3573
- "request": "configure",
3574
- descriptions: descriptions,
3575
- }
3576
- })
3577
- }
3578
-
3579
- setListenIntercomChannels(groups = ['participants']) {
3580
-
3581
- if(typeof groups !== 'object' || !("length" in groups)) {
3582
- this._log('setListenIntercomChannels: groups must be an array');
3583
- groups = [groups];
3584
- }
3585
-
3586
- this._listenIntercomChannels = structuredClone(groups);
3587
- let handle = this._getHandle(this.handleId);
3588
- if (!handle) {
3589
- return Promise.resolve();
3590
- }
3591
-
3592
- let promises = [];
3593
- this._participants.forEach(participant => {
3594
-
3595
- if(participant.handleId === this.handleId) {
3596
- return;
3597
- }
3598
-
3599
-
3600
- let handle = this._getHandle(participant.handleId);
3601
- if(handle) {
3602
-
3603
-
3604
- const tracksMap = handle.webrtcStuff.tracksMap.filter(t => t.active);
3605
- const subscribe = [];
3606
- const unsubscribe = [];
3607
-
3608
- tracksMap.forEach(track => {
3609
- if(track.type === 'data') {
3610
- return;
3611
- }
3612
- const description = JSON.parse(track.description);
3613
- const intercomGroups = description?.intercomGroups || [];
3614
- if(this._listenIntercomChannels.some(g => intercomGroups.includes(g))) {
3615
- if(!this._isAlreadySubscribed(participant.handleId, track.id, track.mid)) {
3616
- subscribe.push({feed: track.id, mid: track.mid})
3617
- }
3618
- }
3619
- else {
3620
- unsubscribe.push({feed: track.id, mid: track.mid})
3621
- }
3622
- });
3623
-
3624
- this._updateSubscribeMap(participant.handleId, subscribe, unsubscribe);
3625
-
3626
- if(subscribe.length || unsubscribe.length) {
3627
- promises.push(this.sendMessage(handle.handleId, {
3628
- body: {
3629
- "request": "update",
3630
- ...(subscribe.length ? {subscribe}: {}),
3631
- ...(unsubscribe.length ? {unsubscribe}: {})
3632
- }
3633
- }));
3634
- }
3635
- }
3636
- });
3637
- return Promise.all(promises);
3638
- }
3639
-
3640
- setRoomType(type = 'watchparty') {
3641
- this._roomType = type;
3642
- return this._roomType;
3643
- }
3644
-
3645
- setRestrictSubscribeToUserIds(userIds = []) {
3646
- this._restrictSubscribeToUserIds = structuredClone(userIds);
3647
- if(!this.isConnected) {
3648
- return Promise.resolve(this._restrictSubscribeToUserIds);
3649
- }
3650
- // sanity check by getting listparticipants and comparing it to _remoteUsersCache
3651
- this.sendMessage(this.handleId, {body: {request: 'listparticipants', room: this.roomId}})
3652
- .then(r => {
3653
- let participants = r.participants;
3654
- let remoteUsersCache = this._remoteUsersCache;
3655
- let handle = this._getHandle(this.handleId);
3656
- // filter out my user id from response and compare it to remoteUsersCache
3657
- participants = participants.filter(p => p.id !== handle.userId);
3658
- // get rid of participants that are in participants but not in remoteUsersCache
3659
- remoteUsersCache = remoteUsersCache.filter(r => participants.find(p => p.id === r.id));
3660
- remoteUsersCache.forEach(r => {
3661
- const handle = this._getHandle(null, null, null, r.userId);
3662
- if(this._participantShouldSubscribe(r.userId)) {
3663
- // todo: do a nicer flag to indicate inactive handle than just checking for pc
3664
- if(!handle || !handle.webrtcStuff?.pc) {
3665
- this._log('Subscribing to ', r.userId)
3666
- this._createParticipant(r.userId, r.id)
3667
- .then(handle => {
3668
- this._updateParticipantsTrackData(handle.handleId, r.streams);
3669
- const subscribe = r.streams.filter(s => !s.disabled && this._intercomSubscribe(s)).map(stream => ({feed: stream.id, mid: stream.mid}));
3670
- handle.webrtcStuff.subscribeMap = structuredClone(subscribe);
3671
- return this.sendMessage(handle.handleId, {
3672
- body: {
3673
- "request": "join",
3674
- "room": this.roomId,
3675
- "ptype": "subscriber",
3676
- "private_id": this.privateId,
3677
- streams: subscribe,
3678
- //"feed": id,
3679
- ...(this.webrtcVersion > 1000 ? {id: this.userId} : {}),
3680
- pin: this.pin
3681
- }
3682
- })
3683
- })
3684
- .catch(err => {
3685
- this.emit('error', err);
3686
- })
3687
-
3688
- }
3689
-
3690
- else {
3691
- this._log('Already subscribed to ', r.userId);
3692
- }
3693
-
3694
- } else if(handle) {
3695
- this._log('Unsubscribing from ', r.userId)
3696
- this._removeParticipant(handle.handleId);
3697
- }
3698
- })
3699
-
3700
- // hotfix
3701
-
3702
- setTimeout(() => {
3703
- if(this.isConnected) {
3704
- this.emit('requestMuteStatus');
3705
- }
3706
- }, 2000)
3707
-
3708
- });
3709
-
3710
-
3711
- }
3712
-
3713
-
3714
- }
3715
-
3716
- export default Room;