@reactoo/watchtogether-sdk-js 2.7.96 → 2.7.98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3194 @@
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
+ }
104
+
105
+ static userRoleSubscriptionRules = {
106
+ participant: {
107
+ "watchparty": ['participant', 'talkback'],
108
+ "studio": ['participant', 'talkback', 'host', 'observer'],
109
+ "commentary": ['participant', 'talkback', 'host'],
110
+ "intercom": ['host', 'talkback', 'observer', 'observerSolo1', 'observerSolo2', 'observerSolo3', 'observerSolo4', 'observerSolo5'],
111
+ "videowall": ['host', 'talkback', 'observer', 'observerSolo1', 'observerSolo2', 'observerSolo3', 'observerSolo4', 'observerSolo5'],
112
+ "videowall-queue": ['host', 'talkback', 'observer', 'observerSolo1', 'observerSolo2', 'observerSolo3', 'observerSolo4', 'observerSolo5'],
113
+ "videowall-queue-video": ['host', 'talkback', 'observer', 'observerSolo1', 'observerSolo2', 'observerSolo3', 'observerSolo4', 'observerSolo5']
114
+ },
115
+ monitor: {
116
+ "watchparty": ['participant', 'host'],
117
+ "studio": ['participant', 'host', 'observer'],
118
+ "commentary": ['participant', 'host'],
119
+ "intercom": ['host', 'participant'],
120
+ "videowall": ['host', 'participant'],
121
+ "videowall-queue": ['host', 'participant'],
122
+ "videowall-queue-video": ['host', 'participant'],
123
+ },
124
+ talkback: {
125
+ "watchparty": ['participant', 'host', 'talkback'],
126
+ "studio": ['participant', 'host', 'observer', 'talkback'],
127
+ "commentary": ['host', 'participant', 'talkback'],
128
+ "intercom": ['host', 'participant', 'talkback'],
129
+ "videowall": ['host', 'participant', 'talkback'],
130
+ "videowall-queue": ['host', 'participant', 'talkback'],
131
+ "videowall-queue-video": ['host', 'participant', 'talkback'],
132
+ },
133
+ observer: {
134
+ "watchparty": ['participant'],
135
+ "studio": ['participant'],
136
+ "commentary": ['participant'],
137
+ "intercom": ['participant'],
138
+ "videowall": ['participant'],
139
+ "videowall-queue": ['participant'],
140
+ "videowall-queue-video": ['participant'],
141
+ },
142
+ observerSolo1: {
143
+ "watchparty": ['participant'],
144
+ "studio": ['participant'],
145
+ "commentary": ['participant'],
146
+ "intercom": ['participant'],
147
+ "videowall": ['participant'],
148
+ "videowall-queue": ['participant'],
149
+ "videowall-queue-video": ['participant'],
150
+ },
151
+ observerSolo2: {
152
+ "watchparty": ['participant'],
153
+ "studio": ['participant'],
154
+ "commentary": ['participant'],
155
+ "intercom": ['participant'],
156
+ "videowall": ['participant'],
157
+ "videowall-queue": ['participant'],
158
+ "videowall-queue-video": ['participant'],
159
+ },
160
+ observerSolo3: {
161
+ "watchparty": ['participant'],
162
+ "studio": ['participant'],
163
+ "commentary": ['participant'],
164
+ "intercom": ['participant'],
165
+ "videowall": ['participant'],
166
+ "videowall-queue": ['participant'],
167
+ "videowall-queue-video": ['participant'],
168
+ },
169
+ observerSolo4: {
170
+ "watchparty": ['participant'],
171
+ "studio": ['participant'],
172
+ "commentary": ['participant'],
173
+ "intercom": ['participant'],
174
+ "videowall": ['participant'],
175
+ "videowall-queue": ['participant'],
176
+ "videowall-queue-video": ['participant'],
177
+ },
178
+ observerSolo5: {
179
+ "watchparty": ['participant'],
180
+ "studio": ['participant'],
181
+ "commentary": ['participant'],
182
+ "intercom": ['participant'],
183
+ "videowall": ['participant'],
184
+ "videowall-queue": ['participant'],
185
+ "videowall-queue-video": ['participant'],
186
+ },
187
+ host: {
188
+ "watchparty": [],
189
+ "studio": [],
190
+ "commentary": [],
191
+ "intercom": [],
192
+ "videowall": [],
193
+ "videowall-queue": [],
194
+ "videowall-queue-video": [],
195
+ },
196
+ companionTV: {},
197
+ companionPhone: {},
198
+ };
199
+
200
+ #publisherHandle = null;
201
+ #subscriberHandle = null;
202
+ #subscriberJoinPromise = null;
203
+
204
+
205
+ constructor(constructId = null, type = 'reactooroom', options = {}) {
206
+
207
+ Object.assign(this, emitter());
208
+ this.options = {...options};
209
+ this.defaultDataChannelLabel = 'JanusDataChannel'
210
+ this.server = null;
211
+ this.iceServers = null;
212
+ this.token = null;
213
+ this.roomId = null;
214
+ this.pin = null;
215
+ this.userId = null;
216
+ this.initialBitrate = 0;
217
+ this.enableDtx = false;
218
+ this.simulcast = false;
219
+
220
+ this.defaultSimulcastSettings = {
221
+ "default" : {
222
+ mode: "controlled", // controlled, manual, browserControlled
223
+ defaultSubstream: 0, // 2 lowest quality, 0 highest quality
224
+ bitrates: [
225
+ {
226
+ "rid": "l",
227
+ "active": true,
228
+ "maxBitrate": 180000,
229
+ "maxFramerate": 20,
230
+ "scaleResolutionDownBy": 3.3333333333333335,
231
+ "priority": "low"
232
+ },
233
+ {
234
+ "rid": "m",
235
+ "active": true,
236
+ "maxBitrate": 500000,
237
+ "maxFramerate": 25,
238
+ "scaleResolutionDownBy": 1.3333333333333335,
239
+ "priority": "low"
240
+ },
241
+ {
242
+ "rid": "h",
243
+ "active": true,
244
+ "maxBitrate": 2000000,
245
+ "maxFramerate": 30,
246
+ "priority": "low"
247
+ }
248
+ ]
249
+ },
250
+ };
251
+ this.recordingFilename = null;
252
+ this.pluginName = RoomSession.sessionTypes[type];
253
+ this.id = null;
254
+ this.privateId = null;
255
+ this.handleId = null;
256
+ this.constructId = constructId || RoomSession.randomString(16);
257
+ this.sessionId = null;
258
+ this.apisecret = null;
259
+ this.ws = null;
260
+
261
+ // helper flags
262
+
263
+ this.isSupposeToBeConnected = false;
264
+ this.isConnecting = false;
265
+ this.isEstablishingConnection = false; //TODO: get rid of this?
266
+ this.isDisconnecting = false;
267
+ this.isConnected = false;
268
+ this.isPublished = false;
269
+
270
+ this.isMuted = [];
271
+
272
+ this.requestMuteStatusTimeoutId = null;
273
+ this.requestMuteStatusTimeout = 100;
274
+ this._statsInterval = 3000;
275
+ this._statsIntervalId = null;
276
+ this._sendMessageTimeout = 10000;
277
+ this._keepAlivePeriod = 25000;
278
+ this._longPollTimeout = 60000;
279
+ this._maxev = 10;
280
+ this._keepAliveId = null;
281
+ this._participants = [];
282
+ this._restrictSubscribeToUserIds = []; // all if empty
283
+ this._talkIntercomChannels = ['participants'];
284
+ this._listenIntercomChannels = ['participants'];
285
+ this._roomType = 'watchparty';
286
+ this._isDataChannelOpen = false;
287
+ this._abortController = null;
288
+
289
+ this.userRoleSubscriptionRules = {
290
+ ...RoomSession.userRoleSubscriptionRules,
291
+ ...(this.options.userRoleSubscriptionRules || {})
292
+ }
293
+
294
+ this._log = RoomSession.noop;
295
+ if (this.options.debug) {
296
+ this.#enableDebug();
297
+ }
298
+ }
299
+
300
+ #httpAPICall = function(url, options) {
301
+ return new Promise((resolve, reject) => {
302
+ const xhr = new XMLHttpRequest();
303
+
304
+ xhr.open(options.verb || 'POST', url);
305
+
306
+ // Set default headers
307
+ xhr.setRequestHeader('Accept', 'application/json, text/plain, */*');
308
+ if (options.verb === "POST") {
309
+ xhr.setRequestHeader('Content-Type', 'application/json');
310
+ }
311
+
312
+ // Handle credentials
313
+ if (options.withCredentials) {
314
+ xhr.withCredentials = true;
315
+ }
316
+
317
+ // Set timeout if specified
318
+ if (options.timeout) {
319
+ xhr.timeout = options.timeout;
320
+ }
321
+
322
+ // Setup handlers
323
+ xhr.onload = function() {
324
+ if (xhr.status >= 200 && xhr.status < 300) {
325
+ let response;
326
+ try {
327
+ response = JSON.parse(xhr.responseText);
328
+ } catch(e) {
329
+ if (options.error) {
330
+ options.error('Invalid JSON', xhr.responseText);
331
+ }
332
+ reject('Invalid JSON');
333
+ return;
334
+ }
335
+ if (options.success) {
336
+ options.success(response);
337
+ }
338
+ resolve(response);
339
+ } else {
340
+ let errorText = xhr.statusText || 'HTTP Error';
341
+ if (options.error) {
342
+ options.error(errorText, xhr.responseText);
343
+ }
344
+ reject(errorText);
345
+ }
346
+ };
347
+
348
+ xhr.onerror = function() {
349
+ let errorText = 'Network error';
350
+ if (options.error) {
351
+ options.error(errorText, 'Is the server down?');
352
+ }
353
+ reject(errorText);
354
+ };
355
+
356
+ xhr.ontimeout = function() {
357
+ let errorText = 'Request timed out';
358
+ if (options.error) {
359
+ options.error(errorText, 'Is the server down?');
360
+ }
361
+ reject(errorText);
362
+ };
363
+
364
+ // Send request
365
+ try {
366
+ if (options.body) {
367
+ xhr.send(JSON.stringify(options.body));
368
+ } else {
369
+ xhr.send();
370
+ }
371
+ } catch(e) {
372
+ if (options.error) {
373
+ options.error('Error sending request', e);
374
+ }
375
+ reject(e);
376
+ }
377
+ });
378
+ }
379
+
380
+ #cacheAttendee(attendee) {
381
+ if(this.#subscriberHandle) {
382
+ this.#subscriberHandle.webrtcStuff.userIdToDisplay[attendee.id] = attendee.display;
383
+ }
384
+ };
385
+
386
+ #removeAttendeeFromCache(id) {
387
+ if(this.#subscriberHandle) {
388
+ delete this.#subscriberHandle.webrtcStuff.userIdToDisplay[id];
389
+ }
390
+ }
391
+
392
+ #getCachedAttendeeIds() {
393
+ return Object.keys(this.#subscriberHandle?.webrtcStuff?.userIdToDisplay || {});
394
+ }
395
+
396
+ #getDisplayById(id) {
397
+ return this.#subscriberHandle.webrtcStuff.userIdToDisplay[id];
398
+ }
399
+
400
+ #emitLocalFeedUpdate({feedRemoval = false, addingTrack = false, removingTrack = false, track = null, source = null, mid = null} = {}) {
401
+
402
+ if(!this.id) {
403
+ // we're not connected probably at all
404
+ return;
405
+ }
406
+
407
+ const eventName = !feedRemoval ? 'addLocalParticipant' : 'removeLocalParticipant';
408
+ const streamMap = this.#getStreamMapForFeed(this.id);
409
+
410
+ const sourceTrackIds = streamMap[source] ?? [];
411
+ const sourceRelatedTracks = this.#publisherHandle?.webrtcStuff?.tracks?.filter(t => sourceTrackIds.includes(t.id)) ?? [];
412
+
413
+ const tracks = this.#publisherHandle?.webrtcStuff?.tracks ?? [];
414
+
415
+ this.emit(eventName, {
416
+ tid: generateUUID(),
417
+ id: this.id,
418
+ constructId: this.constructId,
419
+ userId: decodeJanusDisplay(this.userId)?.userId,
420
+ fullUserId: this.display,
421
+ role: decodeJanusDisplay(this.display)?.role,
422
+ track: track,
423
+ source: source,
424
+ adding: addingTrack,
425
+ removing: removingTrack,
426
+ streamMap: streamMap,
427
+ sourceRelatedTracks: sourceRelatedTracks,
428
+ tracks: tracks.map(t=>t),
429
+ hasAudioTrack: !!tracks.find(track => track.kind === 'audio'),
430
+ hasVideoTrack: !!tracks.find(track => track.kind === 'video'),
431
+ });
432
+ }
433
+
434
+ #emitRemoteFeedUpdate(mid, {feedRemoval = false, addingTrack = false, removingTrack = false, track = null, source = null, attendee = null} = {}) {
435
+
436
+ const transceiverData = this.#getTransceiverDataByMid(mid);
437
+
438
+ if(transceiverData) {
439
+
440
+ const id = transceiverData.feed_id;
441
+ const display = transceiverData.feed_display;
442
+ const parsedDisplay = decodeJanusDisplay(display);
443
+ let description = {};
444
+ try {
445
+ description = JSON.parse(transceiverData.feed_description);
446
+ } catch (e) {}
447
+
448
+ if(!this.#shouldEmitFeedUpdate(parsedDisplay)) {
449
+ this._log('Not emitting feed update for', display, 'because of subscription rules');
450
+ // we don't want to emit this event
451
+ return;
452
+ }
453
+
454
+ const source = description.source;
455
+ const role = parsedDisplay?.role;
456
+ const userId = parsedDisplay?.userId;
457
+ const eventName = this.#getParticipantEventName(!feedRemoval, role);
458
+ const streamMap = this.#getStreamMapForFeed(id);
459
+
460
+ const sourceTrackIds = streamMap[source] ?? [];
461
+ const sourceRelatedTracks = this.#subscriberHandle?.webrtcStuff?.tracks?.filter(t => sourceTrackIds.includes(t.id)) ?? [];
462
+
463
+ const trackIds = Object.keys(streamMap).reduce((acc, key) => {
464
+ streamMap[key].forEach(trackId => {
465
+ if (!acc.includes(trackId)) {
466
+ acc.push(trackId);
467
+ }
468
+ });
469
+ return acc;
470
+ }, []);
471
+
472
+ const tracks = this.#subscriberHandle?.webrtcStuff?.tracks?.filter(t => trackIds.includes(t.id)) ?? [];
473
+
474
+ this.emit(eventName, {
475
+ tid: generateUUID(),
476
+ id,
477
+ userId,
478
+ role,
479
+ fullUserId: display,
480
+ constructId: this.constructId,
481
+ track: track,
482
+ source: source,
483
+ adding: addingTrack,
484
+ removing: removingTrack,
485
+ streamMap,
486
+ sourceRelatedTracks,
487
+ tracks,
488
+ hasAudioTrack: !!tracks.find(track => track.kind === 'audio'),
489
+ hasVideoTrack: !!tracks.find(track => track.kind === 'video'),
490
+
491
+ });
492
+
493
+ } else if(attendee) {
494
+
495
+ const id = attendee.id;
496
+ const display = this.#getDisplayById(id);
497
+ const parsedDisplay = decodeJanusDisplay(display);
498
+
499
+ if(!this.#shouldEmitFeedUpdate(parsedDisplay)) {
500
+ this._log('Not emitting feed update for', display, 'because of subscription rules');
501
+ // we don't want to emit this event
502
+ return;
503
+ }
504
+
505
+ const role = parsedDisplay?.role;
506
+ const userId = parsedDisplay?.userId;
507
+ const streamMap = this.#getStreamMapForFeed(id);
508
+ const eventName = this.#getParticipantEventName(!feedRemoval, role);
509
+
510
+ this.emit(eventName, {
511
+ tid: generateUUID(),
512
+ id,
513
+ userId,
514
+ role,
515
+ fullUserId: display,
516
+ constructId: this.constructId,
517
+ track: null,
518
+ source: null,
519
+ adding: false,
520
+ removing: false,
521
+ streamMap,
522
+ sourceRelatedTracks: [],
523
+ tracks: [],
524
+ hasAudioTrack: false,
525
+ hasVideoTrack: false,
526
+ });
527
+ }
528
+
529
+ // ask for current mute status to each participant
530
+
531
+ clearTimeout(this.requestMuteStatusTimeoutId);
532
+ this.requestMuteStatusTimeoutId = setTimeout(() => {
533
+ if(this.isConnected) {
534
+ this.emit('requestMuteStatus')
535
+ }
536
+ }, this.requestMuteStatusTimeout)
537
+
538
+ }
539
+
540
+ #emitRemoteTrackMuted(mid, {track, muted} = {}) {
541
+
542
+ const transceiverData = this.#getTransceiverDataByMid(mid);
543
+
544
+ if(transceiverData) {
545
+
546
+ const id = transceiverData.feed_id;
547
+ const display = transceiverData.feed_display;
548
+ const parsedDisplay = decodeJanusDisplay(display);
549
+ let description = {};
550
+ try {
551
+ description = JSON.parse(transceiverData.feed_description);
552
+ } catch (e) {}
553
+
554
+ const source = description.source;
555
+ const role = parsedDisplay?.role;
556
+ const userId = parsedDisplay?.userId;
557
+ const streamMap = this.#getStreamMapForFeed(id);
558
+
559
+ this.emit('remoteTrackMuted', {
560
+ tid: generateUUID(),
561
+ id,
562
+ userId,
563
+ role,
564
+ mid,
565
+ fullUserId: display,
566
+ constructId: this.constructId,
567
+ streamMap,
568
+ source,
569
+ kind: track.kind,
570
+ track:track,
571
+ muted
572
+ });
573
+ }
574
+ }
575
+
576
+ #getStreamMapForFeed(id) {
577
+ if (id === this.id) {
578
+ return this.#publisherHandle?.webrtcStuff?.streamMap?.[id] || {};
579
+ }
580
+ else {
581
+ return this.#subscriberHandle?.webrtcStuff?.streamMap?.[id] || {};
582
+ }
583
+ }
584
+
585
+ #isSubscribedToMid(id, mid) {
586
+ if(this.#subscriberHandle) {
587
+ return !!this.#subscriberHandle.webrtcStuff.subscribeMap?.[id]?.[mid]
588
+ }
589
+ return false;
590
+ }
591
+
592
+ #addToSubscribeMap(id, mid) {
593
+ if(this.#subscriberHandle) {
594
+ this.#subscriberHandle.webrtcStuff.subscribeMap[id] = this.#subscriberHandle.webrtcStuff.subscribeMap[id] || {};
595
+ this.#subscriberHandle.webrtcStuff.subscribeMap[id][mid] = true;
596
+ }
597
+ }
598
+
599
+ #removeFromSubscribeMap(id, mid) {
600
+ if(this.#subscriberHandle) {
601
+ if(id && !mid) {
602
+ delete this.#subscriberHandle.webrtcStuff.subscribeMap[id];
603
+ }
604
+ else if(id && mid && this.#subscriberHandle.webrtcStuff.subscribeMap[id]) {
605
+ delete this.#subscriberHandle.webrtcStuff.subscribeMap[id][mid];
606
+ }
607
+ }
608
+ }
609
+
610
+ // will subscribe to all relevant participants
611
+
612
+ async #updateSubscriptions() {
613
+
614
+ // no subscriber handle, we create one and subscribe to relevant participants
615
+
616
+ if(!this.#subscriberJoinPromise) {
617
+ await this.#joinAsSubscriber(this.roomId, this.pin, this.userId);
618
+ }
619
+
620
+ else {
621
+
622
+ // we have a subscriber handle, we need to check if we need to update subscriptions
623
+ // if subscriber handle is not ready yet, we wait for it to be ready
624
+ // if it fails, we try to create a new one
625
+ // if it succeeds, we check if we need to update subscriptions
626
+
627
+ try {
628
+ await this.#subscriberJoinPromise;
629
+ } catch (e) {
630
+ await this.#joinAsSubscriber(this.roomId, this.pin, this.userId);
631
+ return;
632
+ }
633
+
634
+ const handle = this.#subscriberHandle;
635
+ const subscribedTo = handle.webrtcStuff.subscribeMap || {};
636
+ const publishers = handle.webrtcStuff.availablePublishers;
637
+ const subscribe = [];
638
+ const unsubscribe = [];
639
+ const flatSourceMap = [];
640
+
641
+ for (let index in publishers) {
642
+ if(publishers[index]["dummy"])
643
+ continue;
644
+
645
+ let userId = publishers[index]["display"];
646
+ let id = publishers[index]["id"];
647
+ let streams = publishers[index]["streams"] || [];
648
+ let description = {};
649
+
650
+ try {
651
+ const dString = streams.find(s => !!s.description)?.description;
652
+ if(dString) {
653
+ description = JSON.parse(dString);
654
+ }
655
+ } catch (e) {}
656
+
657
+ this._log('Remote publisher: ', id, userId, streams, description);
658
+
659
+ for (let i in streams) {
660
+ const track = streams[i];
661
+ const mid = track.mid; // source.mid
662
+ flatSourceMap.push({id: id, mid: mid});
663
+
664
+ const isSubscribed = this.#isSubscribedToMid(id, mid);
665
+
666
+ if(track.disabled) {
667
+ if(isSubscribed) {
668
+ unsubscribe.push({feed: id, mid: mid});
669
+ this.#removeFromSubscribeMap(id, mid);
670
+ }
671
+ continue;
672
+ }
673
+
674
+ if(!this.#shouldSubscribeParticipant(id, userId, description)) {
675
+ if(isSubscribed) {
676
+ unsubscribe.push({feed: id, mid: mid});
677
+ this.#removeFromSubscribeMap(id, mid);
678
+ }
679
+ continue;
680
+ }
681
+
682
+ if(!isSubscribed) {
683
+ subscribe.push({feed: id, mid: mid});
684
+ this.#addToSubscribeMap(id, mid);
685
+ }
686
+ }
687
+ }
688
+
689
+ // check if we're subscribed to any mid that is no longer available in sources
690
+
691
+ Object.keys(subscribedTo).forEach(id => {
692
+ const mids = Object.keys(subscribedTo[id]);
693
+ for(let mid of mids) {
694
+ if(!flatSourceMap.find(s => s.id === id && s.mid === mid)) {
695
+ unsubscribe.push({feed: id, mid});
696
+ this.#removeFromSubscribeMap(id, mid);
697
+ }
698
+ }
699
+ })
700
+
701
+ if(subscribe.length || unsubscribe.length) {
702
+ await this.sendMessage(handle.handleId, {
703
+ body: {
704
+ "request": "update",
705
+ ...(subscribe.length ? {subscribe}: {}),
706
+ ...(unsubscribe.length ? {unsubscribe}: {})
707
+ }
708
+ });
709
+ }
710
+ }
711
+ }
712
+
713
+ #updatePublishersStreamMap() {
714
+ if(this.#subscriberHandle) {
715
+ let handle = this.#subscriberHandle;
716
+ handle.webrtcStuff.streamMap = {};
717
+ handle.webrtcStuff.transceiverMap.forEach(tItem => {
718
+ if(tItem.type === 'data') {
719
+ return;
720
+ }
721
+ if(tItem.active === false) {
722
+ return;
723
+ }
724
+ const id = tItem.feed_id;
725
+ const source = JSON.parse(tItem.feed_description)?.source;
726
+
727
+ if(!handle.webrtcStuff.streamMap[id]) {
728
+ handle.webrtcStuff.streamMap[id] = {};
729
+ }
730
+
731
+ if(!handle.webrtcStuff.streamMap[id][source]) {
732
+ handle.webrtcStuff.streamMap[id][source] = [];
733
+ }
734
+ let trackId = handle.webrtcStuff.pc.getTransceivers().find(t => t.mid === tItem.mid)?.receiver?.track?.id;
735
+
736
+ if(trackId) {
737
+ handle.webrtcStuff.streamMap[id][source].push(trackId);
738
+ }
739
+ })
740
+ }
741
+ }
742
+
743
+ #updateAvailablePublishersTrackData(newPublishers, removedPublisher) {
744
+ if(this.#subscriberHandle) {
745
+ for(let publisher of newPublishers) {
746
+ const publisherIndex = this.#subscriberHandle.webrtcStuff.availablePublishers.findIndex(p => p.id === publisher.id);
747
+
748
+ if(publisherIndex > -1) {
749
+ this.#subscriberHandle.webrtcStuff.availablePublishers.splice(publisherIndex, 1, publisher);
750
+ }
751
+ else {
752
+ this.#subscriberHandle.webrtcStuff.availablePublishers.push(publisher);
753
+ }
754
+
755
+ }
756
+ if(removedPublisher !== undefined) {
757
+ this.#subscriberHandle.webrtcStuff.availablePublishers = this.#subscriberHandle.webrtcStuff.availablePublishers.filter(p => p.id !== removedPublisher);
758
+ }
759
+ }
760
+ }
761
+
762
+ #getTransceiverDataByMid(mid) {
763
+ return this.#subscriberHandle.webrtcStuff.transceiverMap.find(t => t.mid === mid);
764
+ }
765
+
766
+ // this is done
767
+ // feed_id === id of participant
768
+
769
+ #updateTransceiverMap(handleId, streams = []) {
770
+ this._log('Updating current transceiver map', 'Is me: ' + handleId === this.#publisherHandle.handleId, handleId, streams);
771
+ let handle = this.#getHandle(handleId);
772
+ if (!handle) {
773
+ this.emit('error', {
774
+ type: 'warning',
775
+ id: 15,
776
+ message: 'id non-existent',
777
+ data: [handleId, 'updateTransceiverMap']
778
+ });
779
+ return;
780
+ }
781
+ let config = handle.webrtcStuff;
782
+
783
+ // we need to add new transceivers, update existing transceivers, remove transceivers that are not in streams anymore
784
+ const mids = []
785
+ for(let stream of streams) {
786
+ mids.push(stream.mid);
787
+ const index = config.transceiverMap.findIndex(t => t.mid === stream.mid);
788
+ if(index > -1) {
789
+ config.transceiverMap[index] = {...config.transceiverMap[index], ...stream};
790
+ }
791
+ else {
792
+ config.transceiverMap.push(stream);
793
+ }
794
+ }
795
+ config.transceiverMap = config.transceiverMap.filter(t => mids.includes(t.mid));
796
+ }
797
+
798
+ #getParticipantEventName(adding = true,participantRole) {
799
+ if(adding) {
800
+ switch (participantRole) {
801
+ case 'participant':
802
+ return 'addRemoteParticipant';
803
+ case 'talkback':
804
+ return 'addRemoteTalkback';
805
+ case 'monitor':
806
+ return 'addRemoteTalkback';
807
+ case 'observer':
808
+ case 'observerSolo1':
809
+ case 'observerSolo2':
810
+ case 'observerSolo3':
811
+ case 'observerSolo4':
812
+ case 'observerSolo5':
813
+ return 'addRemoteObserver';
814
+ case 'host':
815
+ return 'addRemoteInstructor';
816
+ case 'companionTV':
817
+ return 'addRemoteCompanionTV';
818
+ case 'companionPhone':
819
+ return 'addRemoteCompanionPhone';
820
+ default:
821
+ return 'addRemoteParticipant';
822
+ }
823
+ }
824
+ else {
825
+ switch (participantRole) {
826
+ case 'participant':
827
+ return 'removeRemoteParticipant';
828
+ case 'talkback':
829
+ return 'removeRemoteTalkback';
830
+ case 'monitor':
831
+ return 'removeRemoteTalkback';
832
+ case 'observer':
833
+ case 'observerSolo1':
834
+ case 'observerSolo2':
835
+ case 'observerSolo3':
836
+ case 'observerSolo4':
837
+ case 'observerSolo5':
838
+ return 'removeRemoteObserver';
839
+ case 'host':
840
+ return 'removeRemoteInstructor';
841
+ case 'companionTV':
842
+ return 'removeRemoteCompanionTV';
843
+ case 'companionPhone':
844
+ return 'removeRemoteCompanionPhone';
845
+ default:
846
+ return 'removeRemoteParticipant';
847
+ }
848
+ }
849
+ }
850
+
851
+ async sendMessage(handleId, message = {body: 'Example Body'}, dontWait = false, dontResolveOnAck = false, retry = 0) {
852
+ return this.#send({
853
+ "janus": "message",
854
+ "handle_id": handleId,
855
+ ...message
856
+ }, dontWait, dontResolveOnAck, retry)
857
+ .then(json => {
858
+ if (json && json["janus"] === "success") {
859
+ let plugindata = json["plugindata"] || {};
860
+ let data = plugindata["data"];
861
+ return Promise.resolve(data);
862
+ }
863
+ return Promise.resolve();
864
+ })
865
+ .catch(json => {
866
+ if (json && json["error"]) {
867
+ return Promise.reject({type: 'warning', id: 1, message: 'sendMessage failed', data: json["error"]})
868
+ } else {
869
+ return Promise.reject({type: 'warning', id: 1, message: 'sendMessage failed', data: json});
870
+ }
871
+ })
872
+ }
873
+
874
+ async #sendHTTP(request = {}, ignoreResponse = false, dontResolveOnAck = false, retry = 0) {
875
+ let transaction = RoomSession.randomString(12);
876
+ let requestData = {
877
+ ...request,
878
+ transaction,
879
+ token: this.token,
880
+ ...((this.sessionId && {'session_id': this.sessionId}) || {}),
881
+ ...((this.apisecret && {'apisecret': this.apisecret}) || {})
882
+ };
883
+ this._log(requestData);
884
+ return this.#httpAPICall(this.server, {body:requestData})
885
+ .then(json => {
886
+ if(json['janus'] !== 'success' && json['janus'] !== 'ack') {
887
+ // not a success not an ack ... should be error
888
+ return Promise.reject(json["error"])
889
+ }
890
+ return json;
891
+ })
892
+ }
893
+
894
+ async #sendWebsockets(request = {}, ignoreResponse = false, dontResolveOnAck = false, retry = 0) {
895
+ let transaction = RoomSession.randomString(12);
896
+ let requestData = {
897
+ ...request,
898
+ transaction,
899
+ token: this.token,
900
+ ...((this.sessionId && {'session_id': this.sessionId}) || {}),
901
+ ...((this.apisecret && {'apisecret': this.apisecret}) || {})
902
+ };
903
+ this._log(requestData);
904
+ const op = () => new Promise((resolve, reject) => {
905
+ let messageTimeoutId = null;
906
+ let abortResponse = () => {
907
+ this._abortController.signal.removeEventListener('abort', abortResponse);
908
+ clearTimeout(messageTimeoutId);
909
+ this.ws.removeEventListener('message', parseResponse);
910
+ reject({type: 'warning', id: 17, message: 'connection cancelled'})
911
+ };
912
+
913
+ let parseResponse = (event) => {
914
+ let json = JSON.parse(event.data);
915
+ let r_transaction = json['transaction'];
916
+ if (r_transaction === transaction && (!dontResolveOnAck || json['janus'] !== 'ack')) {
917
+ clearTimeout(messageTimeoutId);
918
+ this._abortController.signal.removeEventListener('abort', abortResponse);
919
+ this.ws.removeEventListener('message', parseResponse);
920
+ if (json['janus'] === 'error') {
921
+ if (json?.error?.code == 403) {
922
+ this.disconnect();
923
+ }
924
+ reject({type: 'error', id: 2, message: 'send failed', data: json, requestData});
925
+ } else {
926
+ resolve(json);
927
+ }
928
+ }
929
+ };
930
+
931
+ if (ignoreResponse) {
932
+ if (this.ws && this.ws.readyState === 1) {
933
+ this.ws.send(JSON.stringify(requestData));
934
+ }
935
+ resolve();
936
+ } else {
937
+ if (this.ws && this.ws.readyState === 1) {
938
+ this.ws.addEventListener('message', parseResponse);
939
+ messageTimeoutId = setTimeout(() => {
940
+ this.ws.removeEventListener('message', parseResponse);
941
+ this._abortController.signal.removeEventListener('abort', abortResponse);
942
+ reject({type: 'warning', id: 3, message: 'send timeout', data: requestData});
943
+ }, this._sendMessageTimeout);
944
+ this._abortController.signal.addEventListener('abort', abortResponse);
945
+ this.ws.send(JSON.stringify(requestData));
946
+ } else {
947
+ reject({type: 'warning', id: 29, message: 'No connection to WebSockets', data: requestData});
948
+ }
949
+ }
950
+ })
951
+
952
+ return op().catch(e => {
953
+ if (e.id === 17) {
954
+ return Promise.reject(e);
955
+ }
956
+ else if(e.id === 29 && retry > 0) {
957
+ return wait(this._sendMessageTimeout).then(() => this.#send(request, ignoreResponse, dontResolveOnAck, retry - 1));
958
+ }
959
+ else if(retry > 0) {
960
+ return this.#send(request, ignoreResponse, dontResolveOnAck, retry - 1);
961
+ }
962
+ else {
963
+ return Promise.reject(e);
964
+ }
965
+ })
966
+ }
967
+
968
+ async #send(request = {}, ignoreResponse = false, dontResolveOnAck = false, retry = 0) {
969
+ if(this.useWebsockets) {
970
+ return this.#sendWebsockets(request, ignoreResponse, dontResolveOnAck, retry);
971
+ }
972
+ else {
973
+ return this.#sendHTTP(request, ignoreResponse, dontResolveOnAck, retry)
974
+ }
975
+ }
976
+
977
+ #longPoll() {
978
+
979
+ if(!this.isSupposeToBeConnected) {
980
+ return;
981
+ }
982
+
983
+ let longpoll = this.server + "/" + this.sessionId + "?rid=" + new Date().getTime();
984
+ if(this._maxev)
985
+ longpoll = longpoll + "&maxev=" + this._maxev;
986
+ if(this.token)
987
+ longpoll = longpoll + "&token=" + encodeURIComponent(this.token);
988
+ if(this.apisecret)
989
+ longpoll = longpoll + "&apisecret=" + encodeURIComponent(this.apisecret);
990
+
991
+ this.#httpAPICall(longpoll, {
992
+ verb: 'GET',
993
+ timeout: this._longPollTimeout
994
+ })
995
+ .then(this.#handleWsEvents.bind(this))
996
+ .catch(() => {
997
+ this.#longPoll();
998
+ })
999
+ }
1000
+
1001
+ #connectionClosed() {
1002
+
1003
+ if (!this.isConnected || this.isConnecting || this.isDisconnecting) {
1004
+ return;
1005
+ }
1006
+
1007
+ this.#reconnect()
1008
+ .catch(e => {
1009
+ this.disconnect();
1010
+ this.emit('error', e)
1011
+ });
1012
+ }
1013
+
1014
+ #wipeListeners() {
1015
+ if (this.ws) {
1016
+ this.ws.removeEventListener('close', this.__connectionClosedBoundFn);
1017
+ this.ws.removeEventListener('message', this.__handleWsEventsBoundFn);
1018
+ }
1019
+ }
1020
+
1021
+ #startKeepAlive() {
1022
+ this.#send({"janus": "keepalive"})
1023
+ .then(json => {
1024
+ if (json["janus"] !== 'ack') {
1025
+ this.emit('error', {
1026
+ type: 'warning',
1027
+ id: 5,
1028
+ message: 'keepalive response suspicious',
1029
+ data: json["janus"]
1030
+ });
1031
+ }
1032
+ })
1033
+ .catch((e) => {
1034
+ this.emit('error', {type: 'warning', id: 6, message: 'keepalive dead', data: e});
1035
+ this.#connectionClosed();
1036
+ });
1037
+
1038
+ this._keepAliveId = setTimeout(() => {
1039
+ this.#startKeepAlive();
1040
+ }, this._keepAlivePeriod);
1041
+ }
1042
+
1043
+ #stopKeepAlive() {
1044
+ clearTimeout(this._keepAliveId);
1045
+ }
1046
+
1047
+ #handleWsEvents(event, skipPoll = false) {
1048
+
1049
+ // we have a response from long poll, that means we need to run it again
1050
+ if(!this.useWebsockets && !skipPoll) {
1051
+ this.#longPoll();
1052
+ }
1053
+
1054
+ if(!this.useWebsockets && skipPoll) {
1055
+ // we need to fire those events into the wild
1056
+ this.emit('longPollEvent', event);
1057
+ }
1058
+
1059
+ if(Array.isArray(event)) {
1060
+ event.forEach(ev => this.#handleWsEvents({data:ev}, true));
1061
+ return;
1062
+ }
1063
+
1064
+ let json = typeof event.data === 'string'
1065
+ ? JSON.parse(event.data)
1066
+ : event.data;
1067
+
1068
+ var sender = json["sender"];
1069
+ var type = json["janus"];
1070
+
1071
+ let handle = this.#getHandle(sender);
1072
+ if (!handle) {
1073
+ return;
1074
+ }
1075
+
1076
+ if (type === "trickle") {
1077
+ let candidate = json["candidate"];
1078
+ let config = handle.webrtcStuff;
1079
+ if (config.pc && config.remoteSdp) {
1080
+ if (!candidate || candidate.completed === true) {
1081
+ config.pc.addIceCandidate(null);
1082
+ } else {
1083
+ config.pc.addIceCandidate(candidate);
1084
+ }
1085
+ } else {
1086
+ if (!config.candidates) {
1087
+ config.candidates = [];
1088
+ }
1089
+ config.candidates.push(candidate);
1090
+ }
1091
+ } else if (type === "webrtcup") {
1092
+ //none universal
1093
+ } else if (type === "hangup") {
1094
+ this._log('hangup on', handle.handleId, handle.handleId === this.#publisherHandle.handleId, json);
1095
+ if(this.#getHandle(handle.handleId)) {
1096
+ this.#cleanupHandle(handle.handleId, true).catch(() => {});
1097
+ }
1098
+ } else if (type === "detached") {
1099
+ this._log('detached on', handle.handleId, handle.handleId === this.#publisherHandle.handleId, json);
1100
+ if(this.#getHandle(handle.handleId)) {
1101
+ this.#destroyHandle(handle.handleId).catch(() => {});
1102
+ }
1103
+ } else if (type === "media") {
1104
+ this._log('Media event:', handle.handleId, json["type"], json["receiving"], json["mid"]);
1105
+ } else if (type === "slowlink") {
1106
+ this._log('Slowlink', handle.handleId, json["uplink"], json["lost"], json["mid"]);
1107
+ } else if (type === "event") {
1108
+ //none universal
1109
+ } else if (type === 'timeout') {
1110
+ this._log('WebSockets Gateway timeout', json);
1111
+ this.ws.close(3504, "Gateway timeout");
1112
+ } else if (type === 'success' || type === 'error') {
1113
+ // we're capturing those elsewhere
1114
+ } else {
1115
+ this._log(`Unknown event: ${type} on session: ${this.sessionId}`);
1116
+ }
1117
+
1118
+
1119
+ // LOCAL
1120
+
1121
+ if (sender === this.#publisherHandle.handleId) {
1122
+ if (type === "event") {
1123
+
1124
+ var plugindata = json["plugindata"] || {};
1125
+ var msg = plugindata["data"] || {};
1126
+ var jsep = json["jsep"];
1127
+ let result = msg["result"] || null; // related to streaming plugin
1128
+ let event = msg["videoroom"] || null;
1129
+ let attendees = msg["attendees"] ?? [];
1130
+ let list = msg["publishers"] || [];
1131
+ let leaving = msg["leaving"];
1132
+ let kicked = msg["kicked"];
1133
+ let substream = msg["substream"];
1134
+ let temporal = msg["temporal"];
1135
+ let joining = msg["joining"];
1136
+ let unpublished = msg["unpublished"];
1137
+ let error = msg["error"];
1138
+
1139
+ if (event === "joined") {
1140
+
1141
+ this.id = msg["id"];
1142
+ this.privateId = msg["private_id"];
1143
+ this.isConnected = true;
1144
+
1145
+ this._log('We have successfully joined Room',msg);
1146
+
1147
+ // initial events
1148
+
1149
+ attendees.forEach((attendee) => {
1150
+ this.#cacheAttendee(attendee);
1151
+ this.#emitRemoteFeedUpdate(null, {attendee});
1152
+ })
1153
+
1154
+ list.forEach((publisher) => {
1155
+ this.#cacheAttendee(publisher);
1156
+ this.#emitRemoteFeedUpdate(null, {attendee: publisher});
1157
+ });
1158
+
1159
+ // end of initial events
1160
+
1161
+ this.emit('joined', true, this.constructId);
1162
+ this.#emitLocalFeedUpdate();
1163
+ this.#updateAvailablePublishersTrackData(list);
1164
+ this.#updateSubscriptions()
1165
+ .catch((e) => {
1166
+ this._log(e);
1167
+ });
1168
+
1169
+ }
1170
+ else if (event === "event") {
1171
+
1172
+ if(substream !== undefined && substream !== null) {
1173
+ this._log('Substream event:', substream, sender);
1174
+ }
1175
+
1176
+ if(temporal !== undefined && temporal !== null) {
1177
+ this._log('Temporal event:', temporal);
1178
+ }
1179
+
1180
+ if (msg["streams"] !== undefined && msg["streams"] !== null) {
1181
+ this._log('Got my own streams back', msg["streams"]);
1182
+ this.#updateTransceiverMap(handle.handleId, msg["streams"]);
1183
+ }
1184
+
1185
+ this.#updateAvailablePublishersTrackData(list);
1186
+ this.#updateSubscriptions()
1187
+ .catch((e) => {
1188
+ this._log(e);
1189
+ });
1190
+
1191
+
1192
+ if (leaving === 'ok') {
1193
+ this._log('leaving', this.#publisherHandle.handleId, 'this is us');
1194
+
1195
+ this.#emitLocalFeedUpdate({feedRemoval: true});
1196
+
1197
+ if (msg['reason'] === 'kicked') {
1198
+ this.emit('kicked');
1199
+ this.disconnect().catch(() => {});
1200
+ }
1201
+ } else if (leaving) {
1202
+
1203
+ this._log('leaving', leaving);
1204
+ this.#updateAvailablePublishersTrackData([], leaving);
1205
+ this.#updateSubscriptions()
1206
+ .catch((e) => {
1207
+ this._log(e);
1208
+ });
1209
+
1210
+ this.#emitRemoteFeedUpdate(null, {feedRemoval: true, attendee: {id:leaving}});
1211
+ this.#removeAttendeeFromCache(leaving);
1212
+ }
1213
+
1214
+ if (unpublished === 'ok') {
1215
+ this._log('unpublished', this.#publisherHandle.handleId, 'this is us');
1216
+ this.#cleanupHandle(this.#publisherHandle.handleId, true).catch(() => {});
1217
+
1218
+ } else if (unpublished) {
1219
+ this._log('unpublished', unpublished);
1220
+ this.#updateAvailablePublishersTrackData([], unpublished);
1221
+ this.#updateSubscriptions()
1222
+ .catch((e) => {
1223
+ this._log(e);
1224
+ });
1225
+ }
1226
+
1227
+ if(kicked === 'ok') {
1228
+ // this case shouldn't exist
1229
+ } else if(kicked) {
1230
+ this._log('kicked', kicked);
1231
+ this.#updateAvailablePublishersTrackData([], kicked);
1232
+ this.#updateSubscriptions()
1233
+ .catch((e) => {
1234
+ this._log(e);
1235
+ });
1236
+ }
1237
+
1238
+ if(joining) {
1239
+ this.#cacheAttendee(joining);
1240
+ this.#emitRemoteFeedUpdate(null, {attendee: joining});
1241
+ }
1242
+
1243
+ if (error) {
1244
+ this.emit('error', {
1245
+ type: 'error',
1246
+ id: 7,
1247
+ message: 'local participant error',
1248
+ data: [sender, msg]
1249
+ });
1250
+ }
1251
+ }
1252
+
1253
+ if (jsep !== undefined && jsep !== null) {
1254
+ this.#webrtcPeer(this.#publisherHandle.handleId, jsep)
1255
+ .catch(err => {
1256
+ this.emit('error', err);
1257
+ });
1258
+ }
1259
+ }
1260
+ else if (type === "webrtcup") {
1261
+
1262
+ if(this.simulcast) {
1263
+ return;
1264
+ }
1265
+
1266
+ this._log('Configuring bitrate: ' + this.initialBitrate);
1267
+ if (this.initialBitrate > 0) {
1268
+ this.sendMessage(this.#publisherHandle.handleId, {
1269
+ "body": {
1270
+ "request": "configure",
1271
+ "bitrate": this.initialBitrate
1272
+ }
1273
+ }).catch(() => null)
1274
+ }
1275
+ }
1276
+ }
1277
+
1278
+ //REMOTE
1279
+
1280
+ else {
1281
+
1282
+ let plugindata = json["plugindata"] || {};
1283
+ let msg = plugindata["data"] || {};
1284
+ let jsep = json["jsep"];
1285
+ let event = msg["videoroom"];
1286
+ let error = msg["error"];
1287
+ let substream = msg["substream"];
1288
+ let mid = msg["mid"];
1289
+ let temporal = msg["temporal"];
1290
+
1291
+ if(substream !== undefined && substream !== null) {
1292
+ this._log('Substream: ', sender, mid, substream);
1293
+ this.#setSelectedSubstream(sender, mid, substream);
1294
+ this.requestKeyFrame(sender, mid);
1295
+ }
1296
+
1297
+ if(temporal !== undefined && temporal !== null) {
1298
+ this._log('Temporal: ', temporal);
1299
+ this.#setSelectedTemporal(sender, mid, temporal);
1300
+ }
1301
+
1302
+ if(type === "webrtcup") {
1303
+ this.requestKeyFrame(handle.handleId);
1304
+ }
1305
+
1306
+ if (event === "updated") {
1307
+ this._log('Remote has updated tracks', msg);
1308
+ }
1309
+
1310
+ if (event === "attached") {
1311
+ this._log('Subscriber handle successfuly created', msg);
1312
+ }
1313
+
1314
+ if(msg["streams"]) {
1315
+ this.#updateTransceiverMap(handle.handleId, msg["streams"]);
1316
+ }
1317
+
1318
+ if (error) {
1319
+ this.emit('error', {type: 'warning', id: 8, message: 'remote participant error', data: [sender, msg]});
1320
+ }
1321
+
1322
+ if (jsep) {
1323
+ this.#publishRemote(handle.handleId, jsep)
1324
+ .catch(err => {
1325
+ this.emit('error', err);
1326
+ });
1327
+ }
1328
+ }
1329
+ }
1330
+
1331
+ #handleDataChannelEvents(handleId, type, data) {
1332
+
1333
+ let handle = this.#getHandle(handleId);
1334
+
1335
+ if (type === 'state') {
1336
+ this._log(` - Data channel status - `, `UID: ${handleId}`, `STATUS: ${JSON.stringify(data)}`)
1337
+ if (handle) {
1338
+ let config = handle.webrtcStuff;
1339
+ config.dataChannelOpen = this.defaultDataChannelLabel === data?.label && data?.state === 'open';
1340
+ }
1341
+
1342
+ if (handleId === this.#publisherHandle.handleId && this.defaultDataChannelLabel === data?.label) {
1343
+ this._isDataChannelOpen = data?.state === 'open';
1344
+ this.emit('dataChannel', data?.state === 'open');
1345
+ }
1346
+ }
1347
+
1348
+ if (type === 'error') {
1349
+
1350
+ this.emit('error', {
1351
+ type: 'warning',
1352
+ id: 9,
1353
+ message: 'data event warning',
1354
+ data: [handleId, data]
1355
+ });
1356
+
1357
+ if (handle) {
1358
+ let config = handle.webrtcStuff;
1359
+ if(this.defaultDataChannelLabel === data.label) {
1360
+ config.dataChannelOpen = false;
1361
+ }
1362
+ }
1363
+ if (handleId === this.#publisherHandle.handleId && this.defaultDataChannelLabel === data.label) {
1364
+ this._isDataChannelOpen = false;
1365
+ this.emit('dataChannel', false);
1366
+ }
1367
+ }
1368
+
1369
+ if (handleId === this.#publisherHandle.handleId && type === 'message') {
1370
+
1371
+ let d = null;
1372
+
1373
+ try {
1374
+ d = JSON.parse(data)
1375
+ } catch (e) {
1376
+ this.emit('error', {type: 'warning', id: 10, message: 'data message parse error', data: [handleId, e]});
1377
+ return;
1378
+ }
1379
+ this.emit('data', d);
1380
+ }
1381
+
1382
+ }
1383
+
1384
+ async #createHandle() {
1385
+ return this.#send({
1386
+ "janus": "attach",
1387
+ "plugin": this.pluginName,
1388
+ }).then(json => {
1389
+ let handleId = json.data["id"];
1390
+ return {
1391
+ handleId,
1392
+ webrtcStuff: {
1393
+ mySdp: null,
1394
+ mediaConstraints: null,
1395
+ pc: null,
1396
+ dataChannelOpen: false,
1397
+ dataChannel: null,
1398
+ dtmfSender: null,
1399
+ trickle: true,
1400
+ iceDone: false,
1401
+ isIceRestarting: false,
1402
+ tracks: [],
1403
+ stream: new MediaStream(), // for publisher handle only
1404
+ mids: {},
1405
+ userIdToDisplay: {}, // userId to display
1406
+ transceiverMap: [], // got from msg["streams"]
1407
+ streamMap: {}, // id to sources to mids?
1408
+ availablePublishers: [],
1409
+ subscribeMap: [], // subscribed to [id][mid]
1410
+
1411
+ // figure this out below
1412
+ stats: {}
1413
+ }
1414
+ };
1415
+ })
1416
+ }
1417
+
1418
+ async #destroyHandle(handleId, detachRequest = true) {
1419
+ if(handleId) {
1420
+ let handle = this.#getHandle(handleId);
1421
+ if (handle) {
1422
+
1423
+ await this.#cleanupHandle(handleId);
1424
+
1425
+ if(detachRequest) {
1426
+ await this.#send({
1427
+ "janus": "detach",
1428
+ "handle_id": handleId
1429
+ }, true)
1430
+ }
1431
+
1432
+ if (handleId === this.#publisherHandle?.handleId) {
1433
+ this.#publisherHandle = null;
1434
+ }
1435
+ else if (handleId === this.#subscriberHandle?.handleId) {
1436
+ this.#subscriberHandle = null;
1437
+ }
1438
+ }
1439
+ }
1440
+ }
1441
+
1442
+ async #cleanupHandle(handleId, hangupRequest = false) {
1443
+
1444
+ let handle = this.#getHandle(handleId);
1445
+
1446
+ if(hangupRequest) {
1447
+ await this.#send({"janus": "hangup", "handle_id": handleId,}, true)
1448
+ }
1449
+
1450
+ try {
1451
+ if (handle.webrtcStuff.stream) {
1452
+ if(!this.isConnecting) {
1453
+ handle.webrtcStuff.stream?.getTracks().forEach(track => track.stop());
1454
+ }
1455
+ else {
1456
+ handle.webrtcStuff.stream?.getTracks().forEach(track => track.onended = null);
1457
+ }
1458
+ }
1459
+ if(handle.webrtcStuff?.tracks?.length > 0) {
1460
+ handle.webrtcStuff.tracks.forEach(track => {
1461
+ if (track) {
1462
+ track.stop();
1463
+ track.onended = null;
1464
+ }
1465
+ });
1466
+ }
1467
+ } catch (e) {
1468
+ // Do nothing
1469
+ }
1470
+
1471
+ if(handle.webrtcStuff.stream) {
1472
+ handle.webrtcStuff.stream.onremovetrack = null;
1473
+ handle.webrtcStuff.stream = null;
1474
+ }
1475
+
1476
+ if (handle.webrtcStuff.dataChannel) {
1477
+ Object.keys(handle.webrtcStuff.dataChannel).forEach(label => {
1478
+ handle.webrtcStuff.dataChannel[label].onmessage = null;
1479
+ handle.webrtcStuff.dataChannel[label].onopen = null;
1480
+ handle.webrtcStuff.dataChannel[label].onclose = null;
1481
+ handle.webrtcStuff.dataChannel[label].onerror = null;
1482
+ })
1483
+ }
1484
+ if (handle.webrtcStuff.pc) {
1485
+ handle.webrtcStuff.pc.onicecandidate = null;
1486
+ handle.webrtcStuff.pc.ontrack = null;
1487
+ handle.webrtcStuff.pc.ondatachannel = null;
1488
+ handle.webrtcStuff.pc.onconnectionstatechange = null;
1489
+ handle.webrtcStuff.pc.oniceconnectionstatechange = null;
1490
+ }
1491
+ try {
1492
+ handle.webrtcStuff.pc.close();
1493
+ } catch (e) {
1494
+
1495
+ }
1496
+ handle.webrtcStuff = {
1497
+ mySdp: null,
1498
+ mediaConstraints: null,
1499
+ pc: null,
1500
+ dataChannelOpen: false,
1501
+ dataChannel: null,
1502
+ dtmfSender: null,
1503
+ trickle: true,
1504
+ iceDone: false,
1505
+ isIceRestarting: false,
1506
+ tracks: [],
1507
+ stream: new MediaStream(), // for publisher handle only
1508
+ mids: {},
1509
+ userIdToDisplay: {}, // userId to display
1510
+ transceiverMap: [], // got from msg["streams"]
1511
+ streamMap: {}, // id to sources to mids?
1512
+ availablePublishers: [],
1513
+ subscribeMap: [], // subscribed to [id][mid]
1514
+ // figure this out below
1515
+ stats: {}
1516
+ }
1517
+
1518
+ if(this.#publisherHandle && handleId === this.#publisherHandle.handleId) {
1519
+ this.#emitLocalFeedUpdate({feedRemoval: true});
1520
+ }
1521
+ else if(this.#subscriberHandle && handleId === this.#subscriberHandle.handleId) {
1522
+ [...this.#getCachedAttendeeIds()].forEach(id => {
1523
+ this.#emitRemoteFeedUpdate(null, {feedRemoval: true, attendee: {id}});
1524
+ this.#removeAttendeeFromCache(id);
1525
+ })
1526
+ }
1527
+
1528
+ }
1529
+
1530
+ async #joinAsPublisher(roomId, pin, userId, display) {
1531
+ return this.sendMessage(this.#publisherHandle.handleId, {
1532
+ body: {
1533
+ "request": "join", "room": roomId, "pin": pin, "ptype": "publisher", "display": display, ...(this.webrtcVersion > 1000 ? {id: userId} : {})
1534
+ }
1535
+ }, false, true)
1536
+ }
1537
+
1538
+ async #joinAsSubscriber(roomId, pin, userId) {
1539
+
1540
+ const publishers = this.#subscriberHandle.webrtcStuff.availablePublishers;
1541
+
1542
+ if(!publishers.length) {
1543
+ return Promise.reject('no streams to subscribe to');
1544
+ }
1545
+
1546
+ if(this.#subscriberJoinPromise) {
1547
+ return this.#subscriberJoinPromise;
1548
+ }
1549
+
1550
+ let res_streams = [];
1551
+ for (let f in publishers) {
1552
+
1553
+ if(publishers[f]["dummy"])
1554
+ continue;
1555
+
1556
+ let userId = publishers[f]["display"];
1557
+ let streams = publishers[f]["streams"] || [];
1558
+ let id = publishers[f]["id"];
1559
+ let description = {};
1560
+
1561
+ try {
1562
+ const dString = streams.find(s => !!s.description)?.description;
1563
+ if(dString) {
1564
+ description = JSON.parse(dString);
1565
+ }
1566
+ } catch (e) {}
1567
+
1568
+ this._log('Remote publisher: ', id, userId, streams, description);
1569
+
1570
+ for (let i in streams) {
1571
+ const track = streams[i];
1572
+ const mid = track.mid; // source.mid
1573
+ if(track.disabled) {
1574
+ continue;
1575
+ }
1576
+
1577
+ if(!this.#shouldSubscribeParticipant(id, userId, description)) {
1578
+ continue;
1579
+ }
1580
+
1581
+ if(!this.#isSubscribedToMid(id, mid)) {
1582
+ res_streams.push({
1583
+ feed: id, // This is mandatory
1584
+ mid: mid
1585
+ });
1586
+ this.#addToSubscribeMap(id, mid);
1587
+ }
1588
+ }
1589
+ }
1590
+
1591
+ if(!res_streams.length) {
1592
+ return Promise.reject('no streams to subscribe to');
1593
+ }
1594
+
1595
+ this.#subscriberJoinPromise = this.sendMessage(this.#subscriberHandle.handleId, {
1596
+ body: {
1597
+ "request": "join",
1598
+ "room": roomId,
1599
+ "ptype": "subscriber",
1600
+ "use_msid": true,
1601
+ "private_id": this.privateId,
1602
+ streams: res_streams,
1603
+ ...(this.webrtcVersion > 1000 ? {id: userId} : {}),
1604
+ pin: pin
1605
+ }
1606
+ }, false, true)
1607
+ .catch((e) => {
1608
+ this._log(e);
1609
+ this.#subscriberJoinPromise = null;
1610
+ return Promise.reject(e);
1611
+ })
1612
+
1613
+ return this.#subscriberJoinPromise;
1614
+ }
1615
+
1616
+ async #leaveRoom(dontWait = false) {
1617
+ return this.isConnected
1618
+ ? this.sendMessage(this.#publisherHandle.handleId, {body: {"request": "leave"}}, dontWait)
1619
+ .finally(() => {
1620
+ this.isConnected = false;
1621
+ this.emit('joined', false);
1622
+ })
1623
+ : Promise.resolve();
1624
+ }
1625
+
1626
+ async #webSocketConnection(reclaim = false) {
1627
+
1628
+ if(this.isEstablishingConnection) {
1629
+ this.emit('error', {type: 'warning', id: 16, message: 'connection already in progress'});
1630
+ return;
1631
+ }
1632
+
1633
+ this.isEstablishingConnection = true;
1634
+
1635
+ if (this.ws) {
1636
+ this.#wipeListeners();
1637
+ if (this.ws.readyState === 1) {
1638
+ this.ws.close();
1639
+ }
1640
+ }
1641
+
1642
+ this.#stopKeepAlive();
1643
+
1644
+ return new Promise((resolve, reject) => {
1645
+
1646
+ this.__connectionClosedBoundFn = this.#connectionClosed.bind(this);
1647
+ this.__handleWsEventsBoundFn = this.#handleWsEvents.bind(this);
1648
+
1649
+ let abortConnect = () => {
1650
+ this._abortController.signal.removeEventListener('abort', abortConnect);
1651
+ this.ws.removeEventListener('close', this.__connectionClosedBoundFn);
1652
+ this.ws.removeEventListener('message', this.__handleWsEventsBoundFn);
1653
+ this.ws.onopen = null;
1654
+ this.ws.onerror = null;
1655
+ reject({type: 'warning', id: 17, message: 'Connection cancelled'});
1656
+ };
1657
+
1658
+ this.ws = new WebSocket(this.server, 'janus-protocol');
1659
+ this.ws.addEventListener('close', this.__connectionClosedBoundFn);
1660
+ this.ws.addEventListener('message', this.__handleWsEventsBoundFn);
1661
+
1662
+ this.ws.onopen = () => {
1663
+ this._abortController.signal.removeEventListener('abort', abortConnect);
1664
+ if(!reclaim) {
1665
+ this.#send({"janus": "create"})
1666
+ .then(json => {
1667
+ this.sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
1668
+ this.#startKeepAlive();
1669
+ resolve(this);
1670
+ })
1671
+ .catch(error => {
1672
+ reject({type: error?.type === 'warning' ? 'warning' : 'error', id: 13, message: 'connection error', data: error})
1673
+ })
1674
+ .finally(() => {
1675
+ this.isEstablishingConnection = false;
1676
+ });
1677
+ }
1678
+ else {
1679
+ this.#send({"janus": "claim"})
1680
+ .then(json => {
1681
+ this.sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
1682
+ this.#startKeepAlive();
1683
+ resolve(json);
1684
+ })
1685
+ .catch(error => {
1686
+ reject({type: 'error', id: 11, message: 'reconnection error', data: error})
1687
+ })
1688
+ .finally(() => {
1689
+ this.isEstablishingConnection = false;
1690
+ });
1691
+ }
1692
+ };
1693
+
1694
+ this.ws.onerror = (e) => {
1695
+ this._abortController.signal.removeEventListener('abort', abortConnect);
1696
+ reject({type: 'error', id: 14, message: 'ws connection error', data: e});
1697
+ }
1698
+
1699
+ this._abortController.signal.addEventListener('abort', abortConnect);
1700
+
1701
+ });
1702
+ }
1703
+
1704
+ async #httpConnection(reclaim = false) {
1705
+
1706
+ if(this.isEstablishingConnection) {
1707
+ this.emit('error', {type: 'warning', id: 16, message: 'connection already in progress'});
1708
+ return;
1709
+ }
1710
+
1711
+ this.isEstablishingConnection = true;
1712
+
1713
+ // usable when switching from ws to http
1714
+ if (this.ws) {
1715
+ this.#wipeListeners();
1716
+ if (this.ws.readyState === 1) {
1717
+ this.ws.close();
1718
+ }
1719
+ }
1720
+
1721
+ this.#stopKeepAlive();
1722
+
1723
+ if(!reclaim) {
1724
+ return this.#send({"janus": "create"})
1725
+ .then(json => {
1726
+ this.sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
1727
+ this.#startKeepAlive();
1728
+ this.#longPoll();
1729
+ return this;
1730
+ })
1731
+ .catch(error => {
1732
+ return Promise.reject({type: error?.type === 'warning' ? 'warning' : 'error', id: 13, message: 'connection error', data: error})
1733
+ })
1734
+ .finally(() => {
1735
+ this.isEstablishingConnection = false;
1736
+ });
1737
+ }
1738
+ else {
1739
+ return this.#send({"janus": "claim"})
1740
+ .then(json => {
1741
+ this.sessionId = json["session_id"] ? json["session_id"] : json.data["id"];
1742
+ this.#startKeepAlive();
1743
+ this.#longPoll();
1744
+ return this;
1745
+ })
1746
+ .catch(error => {
1747
+ return Promise.reject({type: 'error', id: 11, message: 'reconnection error', data: error})
1748
+ })
1749
+ .finally(() => {
1750
+ this.isEstablishingConnection = false;
1751
+ });
1752
+ }
1753
+ }
1754
+
1755
+ async #reconnect() {
1756
+
1757
+ if (this.isConnecting) {
1758
+ return Promise.reject({type: 'warning', id: 16, message: 'connection already in progress'});
1759
+ }
1760
+
1761
+ this.isConnecting = true;
1762
+
1763
+ if(this.useWebsockets) {
1764
+ try {
1765
+ await this.#webSocketConnection(true)
1766
+ }
1767
+ finally {
1768
+ this.isConnecting = false;
1769
+ }
1770
+ }
1771
+ else {
1772
+ try {
1773
+ await this.#httpConnection(true)
1774
+ }
1775
+ finally {
1776
+ this.isConnecting = false;
1777
+ }
1778
+ }
1779
+ }
1780
+
1781
+ async connect(
1782
+ roomId,
1783
+ pin,
1784
+ server,
1785
+ protocol,
1786
+ iceServers,
1787
+ token,
1788
+ display,
1789
+ userId,
1790
+ webrtcVersion = 0,
1791
+ initialBitrate = 0,
1792
+ recordingFilename,
1793
+ simulcast = false,
1794
+ simulcastSettings = this.defaultSimulcastSettings,
1795
+ enableDtx = false
1796
+ ) {
1797
+
1798
+ this.isSupposeToBeConnected = true;
1799
+
1800
+ if (this.isConnecting) {
1801
+ this.emit('error', {type: 'warning', id: 16, message: 'connection already in progress'});
1802
+ return
1803
+ }
1804
+
1805
+ if(this.isConnected) {
1806
+ await this.disconnect();
1807
+ }
1808
+
1809
+ this._abortController = new AbortController();
1810
+
1811
+ this.sessionId = null;
1812
+ this.server = server;
1813
+ this.protocol = protocol;
1814
+ this.iceServers = iceServers;
1815
+ this.token = token;
1816
+ this.roomId = roomId;
1817
+ this.pin = pin;
1818
+ this.id = null;
1819
+ this.display = display;
1820
+ this.userId = userId;
1821
+ this.webrtcVersion = webrtcVersion;
1822
+ this.initialBitrate = initialBitrate;
1823
+ this.recordingFilename = recordingFilename;
1824
+ this.enableDtx = enableDtx;
1825
+ this.simulcast = simulcast;
1826
+ this.simulcastSettings = structuredClone(simulcastSettings);
1827
+ this.useWebsockets = this.protocol === 'ws' || this.protocol === 'wss';
1828
+
1829
+ // sort simulcast bitrates
1830
+ if(this.simulcastSettings && typeof this.simulcastSettings === 'object' && Object.keys(this.simulcastSettings).length) {
1831
+ Object.keys(this.simulcastSettings).forEach(k => {
1832
+ this.simulcastSettings[k].bitrates = this.simulcastSettings[k].bitrates.sort((a, b) => {
1833
+ if(a.maxBitrate === b.maxBitrate) {
1834
+ return a.maxFramerate - b.maxFramerate;
1835
+ }
1836
+ return a.maxBitrate - b.maxBitrate;
1837
+ });
1838
+ });
1839
+ }
1840
+
1841
+ this.isConnecting = true;
1842
+ this.emit('joining', true);
1843
+
1844
+ try {
1845
+
1846
+ if(this.useWebsockets) {
1847
+ await this.#webSocketConnection(false);
1848
+ }
1849
+ else {
1850
+ await this.#httpConnection(false);
1851
+ }
1852
+
1853
+ this.#publisherHandle = await this.#createHandle();
1854
+ this.handleId = this.#publisherHandle.handleId;
1855
+
1856
+ this.#subscriberHandle = await this.#createHandle();
1857
+
1858
+ await this.#joinAsPublisher(this.roomId, this.pin, this.userId, this.display);
1859
+
1860
+ } catch (error) {
1861
+ this.emit('error', error);
1862
+ } finally {
1863
+ this.isConnecting = false;
1864
+ this.emit('joining', false);
1865
+ }
1866
+
1867
+ this.#enableStatsWatch();
1868
+ }
1869
+
1870
+ async disconnect() {
1871
+
1872
+ this.isSupposeToBeConnected = false;
1873
+
1874
+ if (this.isDisconnecting) {
1875
+ return
1876
+ }
1877
+
1878
+ this.#disableStatsWatch();
1879
+
1880
+ this._abortController?.abort?.();
1881
+ this.isDisconnecting = true;
1882
+ this.#stopKeepAlive();
1883
+ let isConnected = this.isConnected;
1884
+
1885
+ if(this.#publisherHandle) {
1886
+ this.#emitLocalFeedUpdate({feedRemoval: true});
1887
+ }
1888
+
1889
+ if(this.#subscriberHandle) {
1890
+ [...this.#getCachedAttendeeIds()].forEach(id => {
1891
+ this.#emitRemoteFeedUpdate(null, {feedRemoval: true, attendee: {id}});
1892
+ this.#removeAttendeeFromCache(id);
1893
+ })
1894
+ }
1895
+
1896
+ await this.#destroyHandle(this.#publisherHandle?.handleId, false);
1897
+ await this.#destroyHandle(this.#subscriberHandle?.handleId, false);
1898
+
1899
+ this.#wipeListeners();
1900
+ if (this.ws && this.ws.readyState === 1) {
1901
+ await this.#send({"janus": "destroy"}, true);
1902
+ this.ws.close();
1903
+ }
1904
+
1905
+ this.#subscriberJoinPromise = null;
1906
+ this.sessionId = null;
1907
+ this.isPublished = false;
1908
+ this.isConnected = false;
1909
+ this.isDisconnecting = false;
1910
+ this.emit('publishing', false);
1911
+ this.emit('published', false);
1912
+ this.emit('joining', false);
1913
+ this.emit('joined', false);
1914
+ this.emit('disconnect', isConnected);
1915
+
1916
+ }
1917
+
1918
+ async destroy() {
1919
+ return this.disconnect()
1920
+ .then(() => {
1921
+ this.clear();
1922
+ return true;
1923
+ });
1924
+ }
1925
+
1926
+ #enableDebug() {
1927
+ this._log = console.log.bind(console);
1928
+ }
1929
+
1930
+ #getHandle(handleId, rfid = null, userId = null, fullUserId = null) {
1931
+
1932
+ if(handleId === this.#subscriberHandle?.handleId) {
1933
+ return this.#subscriberHandle;
1934
+ }
1935
+
1936
+ if(handleId === this.#publisherHandle?.handleId) {
1937
+ return this.#publisherHandle;
1938
+ }
1939
+
1940
+ return null;
1941
+ }
1942
+
1943
+
1944
+ #findSimulcastConfig(source, settings) {
1945
+ return Object.keys(settings).reduce((acc, key) => {
1946
+ if(settings[source]) {
1947
+ return settings[source];
1948
+ } else if(source.indexOf(key.match(/\*(.*?)\*/)?.[1]) > -1) {
1949
+ return settings[key];
1950
+ } else return acc;
1951
+ }, settings['default']);
1952
+ }
1953
+
1954
+ #sendTrickleCandidate(handleId, candidate) {
1955
+ return this.#send({
1956
+ "janus": "trickle",
1957
+ "candidate": candidate,
1958
+ "handle_id": handleId
1959
+ }, false, false, 5)
1960
+ }
1961
+
1962
+ #webrtc(handleId, enableOntrack = false, enableOnnegotiationneeded = false) {
1963
+
1964
+ let handle = this.#getHandle(handleId);
1965
+ if (!handle) {
1966
+ this.emit('error', {
1967
+ type: 'error',
1968
+ id: 15,
1969
+ message: 'id non-existent',
1970
+ data: [handleId, 'create rtc connection']
1971
+ });
1972
+ return;
1973
+ }
1974
+
1975
+ let config = handle.webrtcStuff;
1976
+ if (!config.pc) {
1977
+ let pc_config = {"iceServers": this.iceServers, "iceTransportPolicy": 'all', "bundlePolicy": undefined};
1978
+
1979
+ pc_config["sdpSemantics"] = "unified-plan";
1980
+
1981
+ let pc_constraints = {};
1982
+
1983
+ if (adapter.browserDetails.browser === "edge") {
1984
+ // This is Edge, enable BUNDLE explicitly
1985
+ pc_config.bundlePolicy = "max-bundle";
1986
+ }
1987
+
1988
+ // pc_config.bundlePolicy = 'balanced';
1989
+ // pc_config.iceTransportPolicy = 'relay';
1990
+ // pc_config.rtcpMuxPolicy = "negotiate";
1991
+
1992
+ this._log('new RTCPeerConnection', pc_config, pc_constraints);
1993
+
1994
+ config.pc = new RTCPeerConnection(pc_config, pc_constraints);
1995
+
1996
+ if(enableOnnegotiationneeded) {
1997
+ config.pc.onnegotiationneeded = async () => {
1998
+ try {
1999
+ this._log('Starting negotiation from onnegotiationneeded');
2000
+
2001
+ // Create an offer
2002
+ const jsep = await this.#createAO('offer', handle.handleId, false);
2003
+
2004
+ if (!jsep) {
2005
+ return null;
2006
+ }
2007
+
2008
+ //HOTFIX: Temporary fix for Safari 13
2009
+ if (jsep.sdp && jsep.sdp.indexOf("\r\na=ice-ufrag") === -1) {
2010
+ jsep.sdp = jsep.sdp.replace("\na=ice-ufrag", "\r\na=ice-ufrag");
2011
+ }
2012
+
2013
+ let descriptions = [];
2014
+ let transceivers = config.pc.getTransceivers();
2015
+ Object.keys(config.streamMap[this.id]).forEach(source => {
2016
+ const simulcastConfigForSource = this.#findSimulcastConfig(source, this.simulcastSettings);
2017
+ config.streamMap[this.id][source].forEach(trackId => {
2018
+ let t = transceivers.find(transceiver => transceiver.sender.track && transceiver.sender.track.id === trackId)
2019
+ if(t) {
2020
+ descriptions.push({mid: t.mid, description: JSON.stringify({source, simulcastBitrates: simulcastConfigForSource?.bitrates, intercomGroups: this._talkIntercomChannels})});
2021
+ }
2022
+ })
2023
+ });
2024
+
2025
+ // Send the offer to the server
2026
+ await this.sendMessage(handle.handleId, {
2027
+ body: {
2028
+ "request": "configure",
2029
+ "audio": !!(config.stream && config.stream.getAudioTracks().length > 0),
2030
+ "video": !!(config.stream && config.stream.getVideoTracks().length > 0),
2031
+ "data": true,
2032
+ descriptions: descriptions,
2033
+ ...(this.recordingFilename ? {filename: this.recordingFilename} : {}),
2034
+ },
2035
+ jsep
2036
+ });
2037
+
2038
+ // The answer will be processed by #handleWsEvents, so we don't need to do anything else here
2039
+ } catch (error) {
2040
+ this._log('Error during negotiation:', error);
2041
+ }
2042
+ };
2043
+ }
2044
+
2045
+ config.pc.onconnectionstatechange = () => {
2046
+
2047
+ if (config.pc.connectionState === 'failed') {
2048
+ this._log('onconnectionstatechange: connectionState === failed');
2049
+ this.#iceRestart(handleId);
2050
+ }
2051
+ this.emit('connectionState', [handleId, handleId === this.#publisherHandle?.handleId, config.pc.connectionState]);
2052
+ };
2053
+
2054
+ config.pc.oniceconnectionstatechange = () => {
2055
+ if (config.pc.iceConnectionState === 'failed') {
2056
+ this._log('oniceconnectionstatechange: iceConnectionState === failed');
2057
+ this.#iceRestart(handleId);
2058
+ }
2059
+ this.emit('iceState', [handleId, handleId === this.#publisherHandle?.handleId, config.pc.iceConnectionState]);
2060
+ };
2061
+ config.pc.onicecandidate = (event) => {
2062
+ if (event.candidate == null || (adapter.browserDetails.browser === 'edge' && event.candidate.candidate.indexOf('endOfCandidates') > 0)) {
2063
+ config.iceDone = true;
2064
+ this.#sendTrickleCandidate(handleId, {"completed": true})
2065
+ .catch(e => {
2066
+ this.emit('error', e);
2067
+ });
2068
+ } else {
2069
+ // JSON.stringify doesn't work on some WebRTC objects anymore
2070
+ // See https://code.google.com/p/chromium/issues/detail?id=467366
2071
+ var candidate = {
2072
+ "candidate": event.candidate.candidate,
2073
+ "sdpMid": event.candidate.sdpMid,
2074
+ "sdpMLineIndex": event.candidate.sdpMLineIndex
2075
+ };
2076
+
2077
+ this.#sendTrickleCandidate(handleId, candidate)
2078
+ .catch(e => {
2079
+ this.emit('error', e);
2080
+ });
2081
+ }
2082
+ };
2083
+
2084
+ if (enableOntrack) {
2085
+ config.pc.ontrack = (event) => {
2086
+
2087
+ if(!event.streams)
2088
+ return;
2089
+
2090
+ if(!event.streams?.[0]?.onremovetrack) {
2091
+
2092
+ event.streams[0].onremovetrack = (ev) => {
2093
+
2094
+ this._log('Remote track removed', ev);
2095
+
2096
+ const trackIndex = config?.tracks?.findIndex(t => t.id === ev.track.id);
2097
+ if(trackIndex > -1) {
2098
+ config.tracks.splice(trackIndex, 1);
2099
+ }
2100
+
2101
+ // check if handle still exists
2102
+ if(!this.#getHandle(handle.handleId)) {
2103
+ return;
2104
+ }
2105
+
2106
+ this.#updatePublishersStreamMap();
2107
+
2108
+ let transceiver = config.pc?.getTransceivers()?.find(
2109
+ t => t.receiver.track === ev.track);
2110
+
2111
+ let mid = transceiver?.mid || ev.track.id;
2112
+
2113
+ // transceiver may not exist already
2114
+ if(mid === ev.target.id && config.mids?.[event.track.id]) {
2115
+ mid = config.mids[event.track.id];
2116
+ }
2117
+
2118
+ this.#emitRemoteFeedUpdate(mid, {
2119
+ removingTrack: true,
2120
+ track: ev.track,
2121
+ });
2122
+
2123
+ delete config.mids[ev.track.id];
2124
+
2125
+ };
2126
+ }
2127
+
2128
+ if (event.track) {
2129
+
2130
+ this._log('Remote track added', event.track, event.streams[0]);
2131
+
2132
+ config.tracks.push(event.track);
2133
+
2134
+ this.#updatePublishersStreamMap();
2135
+
2136
+ let mid = event.transceiver ? event.transceiver.mid : event.track.id;
2137
+ config.mids[event.track.id] = event.transceiver.mid
2138
+
2139
+ if(event.track.kind === 'video') {
2140
+ this.requestKeyFrame(handle.handleId, mid);
2141
+ }
2142
+
2143
+ this.#emitRemoteFeedUpdate(mid, {
2144
+ addingTrack: true,
2145
+ track: event.track
2146
+ });
2147
+
2148
+ if (event.track.onended)
2149
+ return;
2150
+
2151
+ event.track.onended = (ev) => {
2152
+ this._log('Remote track ended', ev);
2153
+
2154
+ // TODO: check this
2155
+
2156
+ const trackIndex = config?.tracks?.findIndex(t => t.id === ev.target.id);
2157
+ if(trackIndex > -1) {
2158
+ config.tracks.splice(trackIndex, 1);
2159
+ }
2160
+
2161
+ // check if handle still exists
2162
+ if(!this.#getHandle(handle.handleId)) {
2163
+ return;
2164
+ }
2165
+
2166
+ this.#updatePublishersStreamMap();
2167
+
2168
+ let transceiver = config.pc?.getTransceivers()?.find(t => t.receiver.track === ev.target);
2169
+ let mid = transceiver?.mid || ev.target.id;
2170
+
2171
+ // transceiver may not exist already
2172
+ if(mid === ev.target.id && config.mids?.[event.track.id]) {
2173
+ mid = config.mids[event.track.id];
2174
+ }
2175
+
2176
+ this.#emitRemoteFeedUpdate(mid, {
2177
+ removingTrack: true,
2178
+ track: ev.target
2179
+ });
2180
+
2181
+ delete config.mids[event.track.id];
2182
+
2183
+ };
2184
+
2185
+ event.track.onmute = (ev) => {
2186
+ this._log('Remote track muted');
2187
+
2188
+ let transceiver = config.pc.getTransceivers().find(
2189
+ t => t.receiver.track === ev.target);
2190
+ let mid = transceiver.mid || ev.target.id;
2191
+
2192
+ this.#emitRemoteTrackMuted(mid, {track:ev.target, muted: true});
2193
+ };
2194
+
2195
+ event.track.onunmute = (ev) => {
2196
+ this._log('Remote track unmuted');
2197
+
2198
+ let transceiver = config.pc.getTransceivers().find(
2199
+ t => t.receiver.track === ev.target);
2200
+ let mid = transceiver.mid || ev.target.id;
2201
+
2202
+ this.#emitRemoteTrackMuted(mid, {track:ev.target, muted: false});
2203
+ };
2204
+ }
2205
+ };
2206
+ }
2207
+ }
2208
+
2209
+ if (!config.dataChannel || !config.dataChannelOpen) {
2210
+
2211
+ config.dataChannel = {};
2212
+
2213
+ var onDataChannelMessage = (event) => {
2214
+ this.#handleDataChannelEvents(handleId, 'message', event.data);
2215
+ };
2216
+ var onDataChannelStateChange = (event) => {
2217
+ let label = event.target.label;
2218
+ let protocol = event.target.protocol;
2219
+ let state = config.dataChannel[label] ? config.dataChannel[label].readyState : "null";
2220
+ this.#handleDataChannelEvents(handleId, 'state', {state, label} );
2221
+ };
2222
+ var onDataChannelError = (error) => {
2223
+ this.#handleDataChannelEvents(handleId, 'error', {label: error?.channel?.label, error});
2224
+ };
2225
+
2226
+ const createDataChannel = (label, protocol, incoming) => {
2227
+ let options = {ordered: true};
2228
+ if(!incoming) {
2229
+ if(protocol) {
2230
+ options = {...options, protocol}
2231
+ }
2232
+ config.dataChannel[label] = config.pc.createDataChannel(label, options);
2233
+ }
2234
+ else {
2235
+ config.dataChannel[label] = incoming;
2236
+ }
2237
+
2238
+ config.dataChannel[label].onmessage = onDataChannelMessage;
2239
+ config.dataChannel[label].onopen = onDataChannelStateChange;
2240
+ config.dataChannel[label].onclose = onDataChannelStateChange;
2241
+ config.dataChannel[label].onerror = onDataChannelError;
2242
+ }
2243
+
2244
+ createDataChannel(this.defaultDataChannelLabel, null, null);
2245
+
2246
+ config.pc.ondatachannel = function (event) {
2247
+ createDataChannel(event.channel.label, event.channel.protocol, event.channel)
2248
+ };
2249
+ }
2250
+ }
2251
+
2252
+ #webrtcPeer(handleId, jsep) {
2253
+
2254
+ let handle = this.#getHandle(handleId);
2255
+ if (!handle) {
2256
+ return Promise.reject({type: 'warning', id: 15, message: 'id non-existent', data: [handleId, 'rtc peer']});
2257
+ }
2258
+
2259
+ var config = handle.webrtcStuff;
2260
+
2261
+ if (jsep !== undefined && jsep !== null) {
2262
+ if (config.pc === null) {
2263
+ this._log("No PeerConnection: if this is an answer, use createAnswer and not #webrtcPeer");
2264
+ return Promise.resolve(null);
2265
+ }
2266
+
2267
+ return config.pc.setRemoteDescription(jsep)
2268
+ .then(() => {
2269
+ config.remoteSdp = jsep.sdp;
2270
+ // Any trickle candidate we cached?
2271
+ if (config.candidates && config.candidates.length > 0) {
2272
+ for (var i = 0; i < config.candidates.length; i++) {
2273
+ var candidate = config.candidates[i];
2274
+ if (!candidate || candidate.completed === true) {
2275
+ config.pc.addIceCandidate(null);
2276
+ } else {
2277
+ config.pc.addIceCandidate(candidate);
2278
+ }
2279
+ }
2280
+ config.candidates = [];
2281
+ }
2282
+ // Done
2283
+ return true;
2284
+ })
2285
+ .catch((e) => {
2286
+ return Promise.reject({type: 'warning', id: 32, message: 'rtc peer', data: [handleId, e]});
2287
+ });
2288
+ } else {
2289
+ return Promise.reject({type: 'warning', id: 22, message: 'rtc peer', data: [handleId, 'invalid jsep']});
2290
+ }
2291
+ }
2292
+
2293
+ #iceRestart(handleId) {
2294
+
2295
+ let handle = this.#getHandle(handleId);
2296
+ if (!handle) {
2297
+ return;
2298
+ }
2299
+ var config = handle.webrtcStuff;
2300
+
2301
+ // Already restarting;
2302
+ if (config.isIceRestarting) {
2303
+ return;
2304
+ }
2305
+
2306
+ config.isIceRestarting = true;
2307
+
2308
+ if (this.#publisherHandle.handleId === handleId) {
2309
+ this._log('Performing local ICE restart');
2310
+ let hasAudio = !!(config.stream && config.stream.getAudioTracks().length > 0);
2311
+ let hasVideo = !!(config.stream && config.stream.getVideoTracks().length > 0);
2312
+ this.#createAO('offer', handleId, true )
2313
+ .then((jsep) => {
2314
+ if (!jsep) {
2315
+ return null;
2316
+ }
2317
+ return this.sendMessage(handleId, {
2318
+ body: {"request": "configure", "keyframe": true, "audio": hasAudio, "video": hasVideo, "data": true, ...(this.recordingFilename ? {filename: this.recordingFilename} : {})},
2319
+ jsep
2320
+ }, false, false, 5);
2321
+ })
2322
+ .then(r => {
2323
+ config.isIceRestarting = false;
2324
+ this._log('ICE restart success');
2325
+ })
2326
+ .catch((e) => {
2327
+ config.isIceRestarting = false;
2328
+ this.emit('error', {type: 'warning', id: 28, message: 'iceRestart failed', data: e});
2329
+ });
2330
+ } else {
2331
+ this._log('Performing remote ICE restart', handleId);
2332
+ return this.sendMessage(handleId, {
2333
+ body: {"request": "configure", "restart": true}
2334
+ }, false, false, 5).then(() => {
2335
+ }).then(() => {
2336
+ config.isIceRestarting = false;
2337
+ this._log('ICE restart success');
2338
+ }).catch(() => {
2339
+ config.isIceRestarting = false;
2340
+ });
2341
+ }
2342
+ }
2343
+
2344
+ #setupTransceivers(handleId, [audioSend, audioRecv, videoSend, videoRecv, audioTransceiver = null, videoTransceiver = null]) {
2345
+ //TODO: this should be refactored to use handle's trackMap so we dont have to pass any parameters
2346
+
2347
+ let handle = this.#getHandle(handleId);
2348
+ if (!handle) {
2349
+ return null;
2350
+ }
2351
+ let config = handle.webrtcStuff;
2352
+
2353
+ const setTransceiver = (transceiver, send, recv, kind = 'audio') => {
2354
+ if (!send && !recv) {
2355
+ // disabled: have we removed it?
2356
+ if (transceiver) {
2357
+ if (transceiver.setDirection) {
2358
+ transceiver.setDirection("inactive");
2359
+ } else {
2360
+ transceiver.direction = "inactive";
2361
+ }
2362
+ }
2363
+ } else {
2364
+ if (send && recv) {
2365
+ if (transceiver) {
2366
+ if (transceiver.setDirection) {
2367
+ transceiver.setDirection("sendrecv");
2368
+ } else {
2369
+ transceiver.direction = "sendrecv";
2370
+ }
2371
+ }
2372
+ } else if (send && !recv) {
2373
+ if (transceiver) {
2374
+ if (transceiver.setDirection) {
2375
+ transceiver.setDirection("sendonly");
2376
+ } else {
2377
+ transceiver.direction = "sendonly";
2378
+ }
2379
+ }
2380
+ } else if (!send && recv) {
2381
+ if (transceiver) {
2382
+ if (transceiver.setDirection) {
2383
+ transceiver.setDirection("recvonly");
2384
+ } else {
2385
+ transceiver.direction = "recvonly";
2386
+ }
2387
+ } else {
2388
+ // In theory, this is the only case where we might not have a transceiver yet
2389
+ config.pc.addTransceiver(kind, {direction: "recvonly"});
2390
+ }
2391
+ }
2392
+ }
2393
+ }
2394
+
2395
+ // if we're passing any transceivers, we work only on them, doesn't matter if one of them is null
2396
+ if(audioTransceiver || videoTransceiver) {
2397
+ if(audioTransceiver) {
2398
+ setTransceiver(audioTransceiver, audioSend, audioRecv, 'audio');
2399
+ }
2400
+ if(videoTransceiver) {
2401
+ setTransceiver(videoTransceiver, videoSend, videoRecv, 'video');
2402
+ }
2403
+ }
2404
+ // else we work on all transceivers
2405
+ else {
2406
+ let transceivers = config.pc.getTransceivers();
2407
+ if (transceivers && transceivers.length > 0) {
2408
+ for (let i in transceivers) {
2409
+ let t = transceivers[i];
2410
+ if (
2411
+ (t.sender && t.sender.track && t.sender.track.kind === "audio") ||
2412
+ (t.receiver && t.receiver.track && t.receiver.track.kind === "audio")
2413
+ ) {
2414
+ setTransceiver(t, audioSend, audioRecv, 'audio');
2415
+ }
2416
+ if (
2417
+ (t.sender && t.sender.track && t.sender.track.kind === "video") ||
2418
+ (t.receiver && t.receiver.track && t.receiver.track.kind === "video")
2419
+ ) {
2420
+ setTransceiver(t, videoSend, videoRecv, 'video');
2421
+ }
2422
+ }
2423
+ }
2424
+ }
2425
+ }
2426
+
2427
+ #createAO(type = 'offer', handleId, iceRestart = false, ) {
2428
+
2429
+ let handle = this.#getHandle(handleId);
2430
+ if (!handle) {
2431
+ return Promise.reject({
2432
+ type: 'warning',
2433
+ id: 15,
2434
+ message: 'id non-existent',
2435
+ data: [handleId, 'createAO', type]
2436
+ });
2437
+ }
2438
+
2439
+ let methodName = null;
2440
+ if (type === 'offer') {
2441
+ methodName = 'createOffer'
2442
+ } else {
2443
+ methodName = 'createAnswer'
2444
+ }
2445
+
2446
+ let config = handle.webrtcStuff;
2447
+ let mediaConstraints = {};
2448
+
2449
+ if (iceRestart) {
2450
+ mediaConstraints["iceRestart"] = true;
2451
+ }
2452
+
2453
+ return config.pc[methodName](mediaConstraints)
2454
+ .then( (response) => {
2455
+
2456
+ // if type offer and its me and we want dtx we mungle the sdp
2457
+ if(handleId === this.#publisherHandle.handleId && type === 'offer' && this.enableDtx) {
2458
+ // enable DTX
2459
+ response.sdp = response.sdp.replace("useinbandfec=1", "useinbandfec=1;usedtx=1")
2460
+ }
2461
+
2462
+ config.mySdp = response.sdp;
2463
+ let _p = config.pc.setLocalDescription(response)
2464
+ .catch((e) => {
2465
+ return Promise.reject({
2466
+ type: 'warning',
2467
+ id: 24,
2468
+ message: 'setLocalDescription',
2469
+ data: [handleId, e]
2470
+ })
2471
+ });
2472
+ config.mediaConstraints = mediaConstraints;
2473
+ if (!config.iceDone && !config.trickle) {
2474
+ // Don't do anything until we have all candidates
2475
+ return Promise.resolve(null);
2476
+ }
2477
+
2478
+ // JSON.stringify doesn't work on some WebRTC objects anymore
2479
+ // See https://code.google.com/p/chromium/issues/detail?id=467366
2480
+ var jsep = {
2481
+ "type": response.type,
2482
+ "sdp": response.sdp
2483
+ };
2484
+
2485
+ if(response.e2ee)
2486
+ jsep.e2ee = true;
2487
+ if(response.rid_order === "hml" || response.rid_order === "lmh")
2488
+ jsep.rid_order = response.rid_order;
2489
+ if(response.force_relay)
2490
+ jsep.force_relay = true;
2491
+
2492
+ return _p.then(() => jsep)
2493
+ }, (e) => {
2494
+ return Promise.reject({type: 'warning', id: 25, message: methodName, data: [handleId, e]})
2495
+ });
2496
+
2497
+ }
2498
+
2499
+ #publishRemote(handleId, jsep) {
2500
+ let handle = this.#getHandle(handleId);
2501
+ if (!handle) {
2502
+ return Promise.reject({
2503
+ type: 'warning',
2504
+ id: 15,
2505
+ message: 'id non-existent',
2506
+ data: [handleId, 'publish remote participant']
2507
+ })
2508
+ }
2509
+
2510
+ this.#webrtc(handleId, true, false);
2511
+
2512
+ let config = handle.webrtcStuff;
2513
+
2514
+ if (jsep) {
2515
+
2516
+ return config.pc.setRemoteDescription(jsep)
2517
+ .then(() => {
2518
+ config.remoteSdp = jsep.sdp;
2519
+ // Any trickle candidate we cached?
2520
+ if (config.candidates && config.candidates.length > 0) {
2521
+ for (var i = 0; i < config.candidates.length; i++) {
2522
+ var candidate = config.candidates[i];
2523
+ if (!candidate || candidate.completed === true) {
2524
+ // end-of-candidates
2525
+ config.pc.addIceCandidate(null);
2526
+ } else {
2527
+ // New candidate
2528
+ config.pc.addIceCandidate(candidate);
2529
+ }
2530
+ }
2531
+ config.candidates = [];
2532
+ }
2533
+
2534
+ this.#setupTransceivers(handleId, [false, true, false, true]);
2535
+
2536
+ // Create the answer now
2537
+ return this.#createAO('answer', handleId, false)
2538
+ .then(_jsep => {
2539
+ if (!_jsep) {
2540
+ this.emit('error', {
2541
+ type: 'warning',
2542
+ id: 19,
2543
+ message: 'publish remote participant',
2544
+ data: [handleId, 'no jsep']
2545
+ });
2546
+ return Promise.resolve();
2547
+ }
2548
+ return this.sendMessage(handleId, {
2549
+ "body": {"request": "start", ...(this.roomId && {"room": this.roomId}), ...(this.pin && {pin: this.pin})},
2550
+ "jsep": _jsep
2551
+ });
2552
+ })
2553
+ }, (e) => Promise.reject({
2554
+ type: 'warning',
2555
+ id: 23,
2556
+ message: 'setRemoteDescription',
2557
+ data: [handleId, e]
2558
+ }));
2559
+
2560
+ } else {
2561
+ return Promise.resolve();
2562
+ }
2563
+
2564
+ }
2565
+
2566
+ #republishOnTrackEnded(source) {
2567
+ let handle = this.#getHandle(this.#publisherHandle.handleId);
2568
+ if (!handle) {
2569
+ return;
2570
+ }
2571
+ let config = handle.webrtcStuff;
2572
+ if (!config.stream) {
2573
+ return;
2574
+ }
2575
+ let sourceTrackIds = (config.streamMap[this.id][source] || []);
2576
+ let remainingTracks = [];
2577
+ for(let i = 0; i < sourceTrackIds.length; i++) {
2578
+ let foundTrack = config.tracks.find(t => t.id === sourceTrackIds[i]);
2579
+ if(foundTrack) {
2580
+ remainingTracks.push(foundTrack);
2581
+ }
2582
+ }
2583
+ if (remainingTracks.length) {
2584
+ let stream = new MediaStream();
2585
+ remainingTracks.forEach(track => stream.addTrack(track));
2586
+ return this.publishLocal(stream, source);
2587
+ }
2588
+ else {
2589
+ return this.publishLocal(null, source);
2590
+ }
2591
+ };
2592
+
2593
+ async publishLocal(stream = null, source = 'camera0') {
2594
+
2595
+ if(!this.isConnected) {
2596
+ return {
2597
+ type: 'warning',
2598
+ id: 18,
2599
+ message: 'Either not connected or disconnecting',
2600
+ }
2601
+ }
2602
+
2603
+ if(stream?.getVideoTracks()?.length > 1) {
2604
+ return {
2605
+ type: 'warning',
2606
+ id: 30,
2607
+ message: 'multiple video tracks not supported',
2608
+ data: null
2609
+ }
2610
+ }
2611
+
2612
+ if(stream?.getAudioTracks()?.length > 1) {
2613
+ return {
2614
+ type: 'warning',
2615
+ id: 30,
2616
+ message: 'multiple audio tracks not supported',
2617
+ data: null
2618
+ }
2619
+ }
2620
+
2621
+ let handle = this.#publisherHandle;
2622
+ if (!handle) {
2623
+ return {
2624
+ type: 'error',
2625
+ id: 31,
2626
+ message: 'no local handle, connect before publishing',
2627
+ data: null
2628
+ }
2629
+ }
2630
+
2631
+ this.emit('publishing', true);
2632
+
2633
+ this.#webrtc(handle.handleId, false, true);
2634
+
2635
+ let config = handle.webrtcStuff;
2636
+
2637
+ if (!config.stream) {
2638
+ config.stream = new MediaStream();
2639
+ }
2640
+
2641
+ let transceivers = config.pc.getTransceivers();
2642
+ let existingTracks = [...(config.streamMap?.[this.id]?.[source] || [])];
2643
+
2644
+ if(stream?.getTracks().length) {
2645
+ if(!config.streamMap[this.id]) {
2646
+ config.streamMap[this.id] = {};
2647
+ }
2648
+ config.streamMap[this.id][source] = stream?.getTracks()?.map(track => track.id) || [];
2649
+ } else {
2650
+ if(config.streamMap[this.id]) {
2651
+ delete config.streamMap[this.id][source];
2652
+ }
2653
+ }
2654
+
2655
+ // remove old audio track related to this source
2656
+ let oldAudioStreamIndex = config?.tracks?.findIndex(track => track.kind === 'audio' && existingTracks.includes(track.id));
2657
+ let oldAudioStream = config?.tracks[oldAudioStreamIndex];
2658
+ if (oldAudioStream) {
2659
+ try {
2660
+ oldAudioStream.stop();
2661
+
2662
+ } catch (e) {
2663
+ this._log(e);
2664
+ }
2665
+ config.stream.removeTrack(oldAudioStream);
2666
+ config?.tracks.splice(oldAudioStreamIndex, 1);
2667
+ }
2668
+
2669
+ // remove old video track related to this source
2670
+ let oldVideoStreamIndex = config?.tracks?.findIndex(track => track.kind === 'video' && existingTracks.includes(track.id));
2671
+ let oldVideoStream = config?.tracks[oldVideoStreamIndex];
2672
+ if (oldVideoStream) {
2673
+ try {
2674
+ oldVideoStream.stop();
2675
+ } catch (e) {
2676
+ this._log(e);
2677
+ }
2678
+
2679
+ config.stream.removeTrack(oldVideoStream);
2680
+ config?.tracks.splice(oldVideoStreamIndex, 1);
2681
+ }
2682
+
2683
+ const simulcastConfigForSource = this.#findSimulcastConfig(source, this.simulcastSettings);
2684
+ let audioTrackReplacePromise = Promise.resolve();
2685
+ let videoTrackReplacePromise = Promise.resolve();
2686
+
2687
+ let audioTransceiver = null;
2688
+ let videoTransceiver = null;
2689
+ let replaceAudio = stream?.getAudioTracks()?.length;
2690
+ let replaceVideo = stream?.getVideoTracks()?.length;
2691
+
2692
+ for(const transceiver of transceivers) {
2693
+ if(['sendonly', 'sendrecv'].includes(transceiver.currentDirection) && transceiver.sender?.track?.kind === 'audio' && existingTracks.includes(transceiver.sender?.track?.id)) {
2694
+ audioTransceiver = transceiver;
2695
+ }
2696
+ else if(['sendonly', 'sendrecv'].includes(transceiver.currentDirection) && transceiver.sender?.track?.kind === 'video' && existingTracks.includes(transceiver.sender?.track?.id)) {
2697
+ videoTransceiver = transceiver;
2698
+ }
2699
+
2700
+ // Reusing existing transceivers
2701
+ // TODO: if we start using different codecs for different sources, we need to check for that here
2702
+
2703
+ else if(transceiver.currentDirection === 'inactive' && transceiver.sender?.getParameters()?.codecs?.find(c => c.mimeType.indexOf('audio') > -1) && replaceAudio && !audioTransceiver) {
2704
+ audioTransceiver = transceiver;
2705
+ }
2706
+ else if(transceiver.currentDirection === 'inactive' && transceiver.sender?.getParameters()?.codecs?.find(c => c.mimeType.indexOf('video') > -1) && replaceVideo && !videoTransceiver) {
2707
+ videoTransceiver = transceiver;
2708
+ }
2709
+ }
2710
+
2711
+ if (replaceAudio) {
2712
+ config.stream.addTrack(stream.getAudioTracks()[0]);
2713
+ config.tracks.push(stream.getAudioTracks()[0]);
2714
+ if (audioTransceiver && audioTransceiver.sender) {
2715
+ audioTrackReplacePromise = audioTransceiver.sender.replaceTrack(stream.getAudioTracks()[0]);
2716
+ } else {
2717
+ config.pc.addTrack(stream.getAudioTracks()[0], config.stream);
2718
+ }
2719
+ }
2720
+ else {
2721
+ if (audioTransceiver && audioTransceiver.sender) {
2722
+ audioTrackReplacePromise = audioTransceiver.sender.replaceTrack(null);
2723
+ }
2724
+ }
2725
+
2726
+ if (replaceVideo) {
2727
+ config.stream.addTrack(stream.getVideoTracks()[0]);
2728
+ config.tracks.push(stream.getVideoTracks()[0]);
2729
+ if (videoTransceiver && videoTransceiver.sender) {
2730
+ videoTrackReplacePromise = videoTransceiver.sender.replaceTrack(stream.getVideoTracks()[0]);
2731
+ } else {
2732
+ if(!this.simulcast) {
2733
+ config.pc.addTrack(stream.getVideoTracks()[0], config.stream);
2734
+ }
2735
+ else {
2736
+ config.pc.addTransceiver(stream.getVideoTracks()[0], {
2737
+ direction: 'sendonly',
2738
+ streams: [config.stream],
2739
+ sendEncodings: structuredClone(simulcastConfigForSource?.bitrates)
2740
+ })
2741
+ }
2742
+ }
2743
+ }
2744
+ else {
2745
+ if (videoTransceiver && videoTransceiver.sender) {
2746
+ videoTrackReplacePromise = videoTransceiver.sender.replaceTrack(null);
2747
+ }
2748
+ }
2749
+
2750
+ // we possibly created new transceivers, so we need to get them again
2751
+
2752
+ transceivers = config.pc.getTransceivers();
2753
+ existingTracks = [...(config.streamMap[this.id][source] || [])];
2754
+ if(!audioTransceiver) {
2755
+ audioTransceiver = transceivers.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'audio' && existingTracks.includes(transceiver.sender.track.id))
2756
+ }
2757
+ if(!videoTransceiver) {
2758
+ videoTransceiver = transceivers.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'video' && existingTracks.includes(transceiver.sender.track.id))
2759
+ }
2760
+
2761
+ let hasAudio = !!(stream && stream.getAudioTracks().length > 0);
2762
+ let hasVideo = !!(stream && stream.getVideoTracks().length > 0);
2763
+
2764
+ this.#setupTransceivers(this.#publisherHandle.handleId, [hasAudio, false, hasVideo, false, audioTransceiver, videoTransceiver]);
2765
+
2766
+ const emitEvents = () => {
2767
+ this.isPublished = true;
2768
+
2769
+ if(!config.stream.onremovetrack) {
2770
+ config.stream.onremovetrack = (ev) => {};
2771
+ }
2772
+
2773
+ let republishTimeoutId = null;
2774
+ let tracks = config.stream.getTracks();
2775
+
2776
+ // this event is emitted when we publish a new source but more importantly if one of the sources goes away
2777
+ this.#emitLocalFeedUpdate({
2778
+ source
2779
+ });
2780
+
2781
+ if(tracks.length) {
2782
+ tracks.forEach(track => {
2783
+
2784
+ // used as a flag to not emit tracks that been already emitted
2785
+ if(!track.onended) {
2786
+
2787
+ this.#emitLocalFeedUpdate({
2788
+ addingTrack: true,
2789
+ track,
2790
+ source
2791
+ })
2792
+
2793
+ track.onended = (ev) => {
2794
+ config.stream.removeTrack(track);
2795
+ config.tracks = config.tracks.filter(t => t.id !== track.id);
2796
+ this.#emitLocalFeedUpdate({
2797
+ removingTrack: true,
2798
+ track,
2799
+ source
2800
+ })
2801
+ clearTimeout(republishTimeoutId);
2802
+ republishTimeoutId = setTimeout(() => {
2803
+ this.#republishOnTrackEnded(source);
2804
+ }, 100);
2805
+
2806
+ }
2807
+ }
2808
+ })
2809
+ }
2810
+
2811
+ this.isMuted = [];
2812
+ for(const source of Object.keys(config.streamMap[this.id])) {
2813
+ const audioTrack = config.stream?.getAudioTracks()?.find(track => config.streamMap[this.id][source].includes(track.id));
2814
+ const videoTrack = config.stream?.getVideoTracks()?.find(track => config.streamMap[this.id][source].includes(track.id));
2815
+ 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});
2816
+ 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});
2817
+
2818
+ this.emit('localHasVideo', !!videoTrack, source);
2819
+ this.emit('localHasAudio', !!audioTrack, source);
2820
+ }
2821
+ for(const val of this.isMuted) {
2822
+ this.emit('localMuted', {...val});
2823
+ }
2824
+ this.emit('published', true);
2825
+ this.emit('publishing', false);
2826
+
2827
+ };
2828
+
2829
+ try {
2830
+
2831
+ await Promise.all([audioTrackReplacePromise, videoTrackReplacePromise]);
2832
+
2833
+ if(this._isDataChannelOpen !== true) {
2834
+ await new Promise((resolve, reject) => {
2835
+ let dataChannelTimeoutId = null;
2836
+ let _resolve = (val) => {
2837
+ if (val) {
2838
+ clearTimeout(dataChannelTimeoutId);
2839
+ this._abortController.signal.removeEventListener('abort', _rejectAbort);
2840
+ this.off('dataChannel', _resolve, this);
2841
+ resolve(this);
2842
+ }
2843
+ };
2844
+ let _rejectTimeout = () => {
2845
+ this.off('dataChannel', _resolve, this);
2846
+ this._abortController.signal.removeEventListener('abort', _rejectAbort);
2847
+ reject({type: 'error', id: 27, message: 'Data channel did not open', data: null});
2848
+ }
2849
+ let _rejectAbort = () => {
2850
+ this._abortController.signal.removeEventListener('abort', _rejectAbort);
2851
+ clearTimeout(dataChannelTimeoutId);
2852
+ this.off('dataChannel', _resolve, this);
2853
+ reject({type: 'warning', id: 17, message: 'Connection cancelled'})
2854
+ }
2855
+ dataChannelTimeoutId = setTimeout(_rejectTimeout, 10000);
2856
+ this._abortController.signal.addEventListener('abort', _rejectAbort);
2857
+ this.on('dataChannel', _resolve, this);
2858
+ });
2859
+ }
2860
+
2861
+ emitEvents();
2862
+
2863
+ }
2864
+ catch(e) {
2865
+ this.emit('publishing', false);
2866
+ }
2867
+
2868
+
2869
+ }
2870
+
2871
+ async unpublishLocal(dontWait = false) {
2872
+ return this.isPublished
2873
+ ? this.sendMessage(this.#publisherHandle.handleId, {body: {"request": "unpublish"}}, dontWait)
2874
+ .finally(r => {
2875
+ this.isPublished = false;
2876
+ this.emit('published', false);
2877
+ return r
2878
+ })
2879
+ : Promise.resolve()
2880
+ }
2881
+
2882
+ toggleAudio(value = null, source = 'camera0', mid) {
2883
+ let handle = this.#getHandle(this.#publisherHandle.handleId);
2884
+ if (!handle) {
2885
+ this.emit('error', {
2886
+ type: 'warning',
2887
+ id: 21,
2888
+ message: 'no local id, connect first', data: null
2889
+ });
2890
+ return;
2891
+ }
2892
+ let config = handle.webrtcStuff;
2893
+ let transceivers = config.pc?.getTransceivers();
2894
+ let transceiver = null;
2895
+ if(source) {
2896
+ transceiver = transceivers?.find(t => t.sender && t.sender.track && t.receiver.track.kind === "audio" && (config.streamMap[this.id][source] || []).includes(t.sender.track.id));
2897
+ }
2898
+ else {
2899
+ transceiver = transceivers?.find(t => t.sender && t.sender.track && t.receiver.track.kind === "audio" && (mid ? t.mid === mid : true));
2900
+ }
2901
+ if (transceiver) {
2902
+ transceiver.sender.track.enabled = value !== null ? !!value : !transceiver.sender.track.enabled;
2903
+ }
2904
+
2905
+ this.isMuted = [];
2906
+ for(const source of Object.keys(config.streamMap[this.id])) {
2907
+ const audioTrack = config.stream?.getAudioTracks()?.find(track => config.streamMap[this.id][source].includes(track.id));
2908
+ const audioTransceiver = transceivers?.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'audio' && transceiver.sender.track.id === audioTrack?.id);
2909
+ const videoTrack = config.stream?.getVideoTracks()?.find(track => config.streamMap[this.id][source].includes(track.id));
2910
+ const videoTransceiver = transceivers?.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'video' && transceiver.sender.track.id === videoTrack?.id);
2911
+ this.isMuted.push({type: 'audio', value: !audioTrack || !audioTransceiver || !audioTransceiver?.sender?.track?.enabled , source, mid: audioTransceiver?.mid});
2912
+ this.isMuted.push({type: 'video', value: !videoTrack || !videoTransceiver || !videoTransceiver?.sender?.track?.enabled, source, mid: videoTransceiver?.mid});
2913
+ }
2914
+ for(let val of this.isMuted) {
2915
+ this.emit('localMuted', {...val});
2916
+ }
2917
+ }
2918
+
2919
+ toggleVideo(value = null, source = 'camera0', mid) {
2920
+ let handle = this.#getHandle(this.#publisherHandle.handleId);
2921
+ if (!handle) {
2922
+ this.emit('error', {
2923
+ type: 'warning',
2924
+ id: 21,
2925
+ message: 'no local id, connect first', data: null
2926
+ });
2927
+ return;
2928
+ }
2929
+ let config = handle.webrtcStuff;
2930
+ let transceivers = config.pc?.getTransceivers();
2931
+ let transceiver = null;
2932
+ if(source) {
2933
+ transceiver = transceivers?.find(t => t.sender && t.sender.track && t.receiver.track.kind === "video" && (config.streamMap[this.id][source] || []).includes(t.sender.track.id));
2934
+ }
2935
+ else {
2936
+ transceiver = transceivers?.find(t => t.sender && t.sender.track && t.receiver.track.kind === "video" && (mid ? t.mid === mid : true));
2937
+ }
2938
+ if (transceiver) {
2939
+ transceiver.sender.track.enabled = value !== null ? !!value : !transceiver.sender.track.enabled;
2940
+ }
2941
+ this.isMuted = [];
2942
+ for(const source of Object.keys(config.streamMap[this.id])) {
2943
+ const audioTrack = config.stream?.getAudioTracks()?.find(track => config.streamMap[this.id][source].includes(track.id));
2944
+ const audioTransceiver = transceivers?.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'audio' && transceiver.sender.track.id === audioTrack?.id);
2945
+ const videoTrack = config.stream?.getVideoTracks()?.find(track => config.streamMap[this.id][source].includes(track.id));
2946
+ const videoTransceiver = transceivers?.find(transceiver => transceiver.sender.track && transceiver.sender.track.kind === 'video' && transceiver.sender.track.id === videoTrack?.id);
2947
+ this.isMuted.push({type: 'audio', value: !audioTrack || !audioTransceiver || !audioTransceiver?.sender?.track?.enabled , source, mid: audioTransceiver?.mid});
2948
+ this.isMuted.push({type: 'video', value: !videoTrack || !videoTransceiver || !videoTransceiver?.sender?.track?.enabled, source, mid: videoTransceiver?.mid});
2949
+ }
2950
+ for(let val of this.isMuted) {
2951
+ this.emit('localMuted', {...val});
2952
+ }
2953
+ }
2954
+
2955
+ requestKeyFrame(handleId, mid) {
2956
+ this.sendMessage(handleId, {
2957
+ "body": {
2958
+ "request": "configure",
2959
+ "keyframe": true,
2960
+ ...(mid !== undefined ? {streams: [{mid,keyframe:true}]} : {})
2961
+ }
2962
+ }).catch(() => null)
2963
+ }
2964
+
2965
+ setRoomType(type = 'watchparty') {
2966
+ this._roomType = type;
2967
+ return this._roomType;
2968
+ }
2969
+
2970
+ #setSelectedSubstream(sender, mid, substream) {
2971
+
2972
+ }
2973
+
2974
+ #setSelectedTemporal(sender, mid, temporal) {
2975
+
2976
+ }
2977
+
2978
+ #shouldEmitFeedUpdate(parsedDisplay = {}) {
2979
+ const myUserRole = decodeJanusDisplay(this.display)?.role || 'participant';
2980
+ const remoteUserRole = parsedDisplay?.role || 'participant';
2981
+ const remoteUserId = parsedDisplay?.userId;
2982
+ const shouldSubscribeToUserRole = this.userRoleSubscriptionRules[myUserRole][(this._roomType || 'watchparty')].indexOf(remoteUserRole) > -1
2983
+ const shouldSubscribeToUserId = this._restrictSubscribeToUserIds.length === 0 || this._restrictSubscribeToUserIds.indexOf(remoteUserId) > -1;
2984
+ return shouldSubscribeToUserRole && shouldSubscribeToUserId;
2985
+ }
2986
+
2987
+ #shouldSubscribeParticipant(id, userId, description) {
2988
+
2989
+ const myUserRole = decodeJanusDisplay(this.display)?.role || 'participant';
2990
+ const remoteUserRole = decodeJanusDisplay(userId)?.role || 'participant';
2991
+ const remoteUserId = decodeJanusDisplay(userId)?.userId;
2992
+ const remoteIntercomGroups = description?.intercomGroups || [];
2993
+ const shouldSubscribeToUserRole = this.userRoleSubscriptionRules[myUserRole][(this._roomType || 'watchparty')].indexOf(remoteUserRole) > -1
2994
+ const shouldSubscribeToUserId = this._restrictSubscribeToUserIds.length === 0 || this._restrictSubscribeToUserIds.indexOf(remoteUserId) > -1;
2995
+ const shouldSubscribeToIntercomGroups = remoteIntercomGroups.some(g => this._listenIntercomChannels.indexOf(g) > -1);
2996
+
2997
+ return shouldSubscribeToUserRole && shouldSubscribeToUserId && shouldSubscribeToIntercomGroups;
2998
+ }
2999
+
3000
+ getUserTalkIntercomChannels(userId) {
3001
+ let talkIntercomChannels = []
3002
+ let transceiver = this.#subscriberHandle.webrtcStuff.transceiverMap.find(t => t.display && decodeJanusDisplay(t.display)?.userId === userId && t.description);
3003
+
3004
+ if(!transceiver) {
3005
+ return talkIntercomChannels;
3006
+ }
3007
+
3008
+ try {
3009
+ let description = JSON.parse(transceiver.description);
3010
+ if(description.intercomGroups) {
3011
+ description.intercomGroups.forEach(group => {
3012
+ if(!talkIntercomChannels.includes(group)) {
3013
+ talkIntercomChannels.push(group);
3014
+ }
3015
+ });
3016
+ }
3017
+ } catch (e) {}
3018
+
3019
+ return talkIntercomChannels;
3020
+ }
3021
+
3022
+ async setTalkIntercomChannels(groups = ['participants']) {
3023
+
3024
+ if(typeof groups !== 'object' || !("length" in groups)) {
3025
+ this._log('setTalkIntercomChannels: groups must be an array');
3026
+ groups = [groups];
3027
+ }
3028
+
3029
+ this._talkIntercomChannels = structuredClone(groups);
3030
+ let handle = this.#getHandle(this.handleId);
3031
+ if (!handle) {
3032
+ return Promise.resolve();
3033
+ }
3034
+ let config = handle.webrtcStuff;
3035
+ let descriptions = [];
3036
+ let transceivers = config.pc.getTransceivers();
3037
+ Object.keys(config.streamMap[this.id]).forEach(source => {
3038
+ const simulcastConfigForSource = this.#findSimulcastConfig(source, this.simulcastSettings);
3039
+ config.streamMap[this.id][source].forEach(trackId => {
3040
+ let t = transceivers.find(transceiver => transceiver.sender.track && transceiver.sender.track.id === trackId)
3041
+ if(t) {
3042
+ descriptions.push({mid: t.mid, description: JSON.stringify({source, simulcastBitrates: simulcastConfigForSource?.bitrates, intercomGroups: this._talkIntercomChannels})});
3043
+ }
3044
+ })
3045
+ });
3046
+
3047
+ return await this.sendMessage(this.handleId, {
3048
+ body: {
3049
+ "request": "configure",
3050
+ descriptions: descriptions,
3051
+ }
3052
+ })
3053
+ }
3054
+
3055
+ async setListenIntercomChannels(groups = ['participants']) {
3056
+
3057
+ if(typeof groups !== 'object' || !("length" in groups)) {
3058
+ this._log('setListenIntercomChannels: groups must be an array');
3059
+ groups = [groups];
3060
+ }
3061
+
3062
+ this._listenIntercomChannels = structuredClone(groups);
3063
+
3064
+ if(!this.isConnected) {
3065
+ return
3066
+ }
3067
+
3068
+ return await this.#updateSubscriptions();
3069
+ }
3070
+
3071
+ async setRestrictSubscribeToUserIds(userIds = []) {
3072
+
3073
+ if(typeof userIds !== 'object' || !("length" in userIds)) {
3074
+ this._log('setRestrictSubscribeToUserIds: userIds must be an array');
3075
+ userIds = [userIds];
3076
+ }
3077
+
3078
+ this._restrictSubscribeToUserIds = structuredClone(userIds);
3079
+
3080
+ if(!this.isConnected) {
3081
+ return
3082
+ }
3083
+
3084
+ return await this.#updateSubscriptions();
3085
+ }
3086
+
3087
+ async selectSubStream(id, substream = 2, source, mid) {
3088
+ this._log('Select substream called for id:', id, 'Source or mid:', source ? source : mid, 'Substream:', substream);
3089
+
3090
+ let config = this.#subscriberHandle.webrtcStuff;
3091
+ return new Promise((resolve, reject) => {
3092
+ let messageTimeoutId;
3093
+
3094
+ if(!config.streamMap[id]) {
3095
+ reject('no user found');
3096
+ }
3097
+
3098
+ let clearListeners = () => {
3099
+ clearTimeout(messageTimeoutId);
3100
+ this._abortController.signal.removeEventListener('abort', abortResponse);
3101
+ this.off('longPollEvent', parseResponse);
3102
+ this.ws?.removeEventListener('message', parseResponse);
3103
+ };
3104
+
3105
+ let abortResponse = () => {
3106
+ clearListeners();
3107
+ reject('aborted');
3108
+ };
3109
+
3110
+ let parseResponse = (event) => {
3111
+ let json = typeof event.data === 'string'
3112
+ ? JSON.parse(event.data)
3113
+ : event.data;
3114
+
3115
+ var sender = json["sender"];
3116
+ if(sender === this.#subscriberHandle.handleId) {
3117
+ let plugindata = json["plugindata"] || {};
3118
+ let msg = plugindata["data"] || {};
3119
+ let substream = msg["substream"];
3120
+ if(substream !== undefined && substream !== null && (mid !== undefined ? msg["mid"] === mid : true)) {
3121
+ clearListeners();
3122
+ resolve({substream, sender});
3123
+ }
3124
+ }
3125
+ }
3126
+
3127
+ if(source !== undefined || mid !== undefined) {
3128
+ if(mid === undefined) {
3129
+ let transceivers = config.pc.getTransceivers();
3130
+ for(let trackId of config.streamMap[id][source]) {
3131
+ let transceiver = transceivers.find(transceiver => transceiver.receiver.track && transceiver.receiver.track.kind === 'video' && transceiver.receiver.track.id === trackId)
3132
+ if(transceiver) {
3133
+ mid = transceiver.mid;
3134
+ break;
3135
+ }
3136
+ }
3137
+ }
3138
+ if(mid !== undefined) {
3139
+
3140
+ this.on('longPollEvent', parseResponse);
3141
+ this.ws?.addEventListener('message', parseResponse);
3142
+ this._abortController.signal.addEventListener('abort', abortResponse);
3143
+ messageTimeoutId = setTimeout(() => {
3144
+ clearListeners();
3145
+ reject('timeout');
3146
+ }, 10000);
3147
+
3148
+ this.sendMessage(this.#subscriberHandle.handleId, {
3149
+ "body": {
3150
+ "request": "configure",
3151
+ "streams": [
3152
+ {
3153
+ mid, substream: parseInt(substream)
3154
+ }
3155
+ ]
3156
+ }
3157
+ })
3158
+ } else {
3159
+ reject('no mid found');
3160
+ }
3161
+ }
3162
+ else {
3163
+ reject('no source or mid');
3164
+ }
3165
+ });
3166
+ }
3167
+
3168
+ #getStats(type = null) {
3169
+
3170
+ }
3171
+
3172
+ #adjustQualitySettings(stats) {}
3173
+
3174
+ #disableStatsWatch() {
3175
+ if (this._statsIntervalId) {
3176
+ clearInterval(this._statsIntervalId);
3177
+ this._statsIntervalId = null;
3178
+ }
3179
+ }
3180
+ #enableStatsWatch() {
3181
+
3182
+ this.#disableStatsWatch();
3183
+
3184
+ const loop = () => {
3185
+ this.#getStats('video')
3186
+ .then(this.#adjustQualitySettings)
3187
+ };
3188
+
3189
+ this._statsIntervalId = setInterval(loop, this._statsInterval);
3190
+ }
3191
+
3192
+ }
3193
+
3194
+ export default Room;