@whereby.com/core 0.2.0-beta.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,15 @@
1
1
  import { createAsyncThunk, createListenerMiddleware, addListener, createSlice, createAction, createSelector, isAnyOf, combineReducers, configureStore } from '@reduxjs/toolkit';
2
2
  import { io } from 'socket.io-client';
3
3
  import adapter from 'webrtc-adapter';
4
- import { d as debounce } from './debounce-IlgXge5U.js';
4
+ import EventEmitter, { EventEmitter as EventEmitter$1 } from 'events';
5
+ import { d as debounce } from './debounce-B-cWYxqK.js';
5
6
  import SDPUtils from 'sdp';
6
7
  import * as sdpTransform from 'sdp-transform';
7
- import { v4 } from 'uuid';
8
+ import { v4 as v4$1 } from 'uuid';
9
+ import { Address6 } from 'ip-address';
10
+ import checkIp from 'check-ip';
11
+ import validate from 'uuid-validate';
8
12
  import { detectDevice, Device } from 'mediasoup-client';
9
- import EventEmitter$1, { EventEmitter } from 'events';
10
13
  import nodeBtoa from 'btoa';
11
14
  import axios from 'axios';
12
15
 
@@ -184,13 +187,535 @@ createReactor([selectShouldFetchDeviceCredentials], ({ dispatch }, shouldFetchDe
184
187
  }
185
188
  });
186
189
 
190
+ let peerConnections = [];
191
+ let peerConnectionCounter = 0;
192
+ const peerConnectionData = new WeakMap();
193
+
194
+ const removePeerConnection = (pc) => {
195
+ peerConnections = peerConnections.filter((old) => old !== pc);
196
+ };
197
+
198
+ if (window.RTCPeerConnection) {
199
+ const OriginalRTCPeerConnection = window.RTCPeerConnection;
200
+ function PatchedRTCPeerConnection(rtcConfig) {
201
+ const pc = new OriginalRTCPeerConnection(rtcConfig);
202
+ peerConnections.push(pc);
203
+ peerConnectionData.set(pc, { index: peerConnectionCounter++ });
204
+ const onConnectionStateChange = () => {
205
+ if (pc.connectionState === "closed") {
206
+ removePeerConnection(pc);
207
+ pc.removeEventListener("connectionstatechange", onConnectionStateChange);
208
+ }
209
+ };
210
+ pc.addEventListener("connectionstatechange", onConnectionStateChange);
211
+ return pc;
212
+ }
213
+ PatchedRTCPeerConnection.prototype = OriginalRTCPeerConnection.prototype;
214
+ window.RTCPeerConnection = PatchedRTCPeerConnection;
215
+ }
216
+
217
+ const debugOn = new URLSearchParams(window.location.search).has("debug");
218
+
219
+ class Logger {
220
+ constructor() {
221
+ this._isEnabled = debugOn;
222
+ }
223
+
224
+ isEnabled() {
225
+ return this._isEnabled;
226
+ }
227
+
228
+ enable() {
229
+ this._isEnabled = true;
230
+ }
231
+
232
+ disable() {
233
+ this._isEnabled = false;
234
+ }
235
+
236
+ info(...params) {
237
+ if (!this._isEnabled) {
238
+ return;
239
+ }
240
+ // eslint-disable-next-line no-console
241
+ return console.info(...params);
242
+ }
243
+
244
+ warn(...params) {
245
+ if (!this._isEnabled) {
246
+ return;
247
+ }
248
+ return console.warn(...params);
249
+ }
250
+
251
+ error(...params) {
252
+ if (!this._isEnabled) {
253
+ return;
254
+ }
255
+ return console.error(...params);
256
+ }
257
+
258
+ withDebugLogger(myDebugger = null) {
259
+ this._debugger = myDebugger;
260
+ return this;
261
+ }
262
+
263
+ debug(...params) {
264
+ if (!this._isEnabled || !this._debugger) {
265
+ return;
266
+ }
267
+ const suppliedParams = [];
268
+ params.forEach((param) => {
269
+ if (typeof param === "function") {
270
+ const suppliedParam = param();
271
+ suppliedParams.push(suppliedParam);
272
+ } else {
273
+ suppliedParams.push(param);
274
+ }
275
+ });
276
+ this._debugger.print(...suppliedParams);
277
+ }
278
+ }
279
+
280
+ let currentMonitor = null;
281
+
282
+ const getUpdatedStats = () => currentMonitor?.getUpdatedStats();
283
+
284
+ // Protocol enum used for the CLIENT (Make sure to keep it in sync with its server counterpart)
285
+
286
+ // Requests: messages from the client to the server
287
+ const PROTOCOL_REQUESTS = {
288
+ BLOCK_CLIENT: "block_client",
289
+ CLAIM_ROOM: "claim_room",
290
+ CLEAR_CHAT_HISTORY: "clear_chat_history",
291
+ ENABLE_AUDIO: "enable_audio",
292
+ ENABLE_VIDEO: "enable_video",
293
+ END_STREAM: "end_stream",
294
+ FETCH_MEDIASERVER_CONFIG: "fetch_mediaserver_config",
295
+ HANDLE_KNOCK: "handle_knock",
296
+ IDENTIFY_DEVICE: "identify_device",
297
+ INVITE_CLIENT_AS_MEMBER: "invite_client_as_member",
298
+ JOIN_ROOM: "join_room",
299
+ KICK_CLIENT: "kick_client",
300
+ KNOCK_ROOM: "knock_room",
301
+ LEAVE_ROOM: "leave_room",
302
+ SEND_CLIENT_METADATA: "send_client_metadata",
303
+ SET_LOCK: "set_lock",
304
+ SHARE_MEDIA: "share_media",
305
+ START_NEW_STREAM: "start_new_stream",
306
+ START_SCREENSHARE: "start_screenshare",
307
+ STOP_SCREENSHARE: "stop_screenshare",
308
+ START_URL_EMBED: "start_url_embed",
309
+ STOP_URL_EMBED: "stop_url_embed",
310
+ START_RECORDING: "start_recording",
311
+ STOP_RECORDING: "stop_recording",
312
+ SFU_TOKEN: "sfu_token",
313
+ };
314
+
315
+ // Responses: messages from the server to the client, in response to requests
316
+ const PROTOCOL_RESPONSES = {
317
+ AUDIO_ENABLED: "audio_enabled",
318
+ BACKGROUND_IMAGE_CHANGED: "background_image_changed",
319
+ BLOCK_ADDED: "block_added",
320
+ BLOCK_REMOVED: "block_removed",
321
+ CHAT_HISTORY_CLEARED: "chat_history_cleared",
322
+ CLIENT_BLOCKED: "client_blocked",
323
+ CLIENT_INVITED_AS_MEMBER: "client_invited_as_member",
324
+ CLIENT_KICKED: "client_kicked",
325
+ CLIENT_LEFT: "client_left",
326
+ CLIENT_METADATA_RECEIVED: "client_metadata_received",
327
+ CLIENT_READY: "client_ready",
328
+ CLIENT_ROLE_CHANGED: "client_role_changed",
329
+ CLIENT_USER_ID_CHANGED: "client_user_id_changed",
330
+ CONTACTS_UPDATED: "contacts_updated",
331
+ DEVICE_IDENTIFIED: "device_identified",
332
+ ROOM_ROLES_UPDATED: "room_roles_updated",
333
+ KNOCK_HANDLED: "knock_handled",
334
+ KNOCK_PAGE_BACKGROUND_CHANGED: "knock_page_background_changed",
335
+ KNOCKER_LEFT: "knocker_left",
336
+ MEDIASERVER_CONFIG: "mediaserver_config",
337
+ MEDIA_SHARED: "media_shared",
338
+ MEMBER_INVITE: "member_invite",
339
+ NEW_CLIENT: "new_client",
340
+ NEW_STREAM_STARTED: "new_stream_started",
341
+ SCREENSHARE_STARTED: "screenshare_started",
342
+ SCREENSHARE_STOPPED: "screenshare_stopped",
343
+ OWNER_NOTIFIED: "owner_notified",
344
+ OWNERS_CHANGED: "owners_changed",
345
+ PLAY_CLIENT_STICKER: "play_client_sticker",
346
+ ROOM_INTEGRATION_ENABLED: "room_integration_enabled",
347
+ ROOM_INTEGRATION_DISABLED: "room_integration_disabled",
348
+ ROOM_JOINED: "room_joined",
349
+ ROOM_KNOCKED: "room_knocked",
350
+ ROOM_LEFT: "room_left",
351
+ ROOM_LOCKED: "room_locked",
352
+ ROOM_PERMISSIONS_CHANGED: "room_permissions_changed",
353
+ ROOM_LOGO_CHANGED: "room_logo_changed",
354
+ ROOM_TYPE_CHANGED: "room_type_changed",
355
+ ROOM_MODE_CHANGED: "room_mode_changed",
356
+ SOCKET_USER_ID_CHANGED: "socket_user_id_changed",
357
+ STICKERS_UNLOCKED: "stickers_unlocked",
358
+ STREAM_ENDED: "stream_ended",
359
+ URL_EMBED_STARTED: "url_embed_started",
360
+ URL_EMBED_STOPPED: "url_embed_stopped",
361
+ RECORDING_STARTED: "recording_started",
362
+ RECORDING_STOPPED: "recording_stopped",
363
+ USER_NOTIFIED: "user_notified",
364
+ VIDEO_ENABLED: "video_enabled",
365
+ CLIENT_UNABLE_TO_JOIN: "client_unable_to_join",
366
+ };
367
+
368
+ // Relays: messages between clients, relayed through the server
369
+ const RELAY_MESSAGES = {
370
+ CHAT_MESSAGE: "chat_message",
371
+ CHAT_READ_STATE: "chat_read_state",
372
+ CHAT_STATE: "chat_state",
373
+ ICE_CANDIDATE: "ice_candidate",
374
+ ICE_END_OF_CANDIDATES: "ice_endofcandidates",
375
+ READY_TO_RECEIVE_OFFER: "ready_to_receive_offer",
376
+ REMOTE_CLIENT_MEDIA_REQUEST: "remote_client_media_request",
377
+ SDP_ANSWER: "sdp_answer",
378
+ SDP_OFFER: "sdp_offer",
379
+ VIDEO_STICKER: "video_sticker",
380
+ };
381
+
382
+ // Events: something happened that we want to let the client know about
383
+ const PROTOCOL_EVENTS = {
384
+ PENDING_CLIENT_LEFT: "pending_client_left",
385
+ MEDIA_QUALITY_CHANGED: "media_quality_changed",
386
+ };
387
+
388
+ const logger$9 = new Logger();
389
+
390
+ class ReconnectManager extends EventEmitter {
391
+ constructor(socket) {
392
+ super();
393
+ this._socket = socket;
394
+ this._clients = {};
395
+ this._signalDisconnectTime = undefined;
396
+ this.rtcManager = undefined;
397
+ this.metrics = {
398
+ roomJoinedLate: 0,
399
+ pendingClientCanceled: 0,
400
+ evaluationFailed: 0,
401
+ roomJoined: 0,
402
+ };
403
+
404
+ socket.on("disconnect", () => {
405
+ this._signalDisconnectTime = Date.now();
406
+ });
407
+
408
+ // We intercept these events and take responsiblity for forwarding them
409
+ socket.on(PROTOCOL_RESPONSES.ROOM_JOINED, (payload) => this._onRoomJoined(payload));
410
+ socket.on(PROTOCOL_RESPONSES.NEW_CLIENT, (payload) => this._onNewClient(payload));
411
+ socket.on(PROTOCOL_RESPONSES.CLIENT_LEFT, (payload) => this._onClientLeft(payload));
412
+
413
+ // We intercept these events and handle them without forwarding them
414
+ socket.on(PROTOCOL_EVENTS.PENDING_CLIENT_LEFT, (payload) => this._onPendingClientLeft(payload));
415
+
416
+ // We gather information from these events but they will also be forwarded
417
+ socket.on(PROTOCOL_RESPONSES.AUDIO_ENABLED, (payload) => this._onAudioEnabled(payload));
418
+ socket.on(PROTOCOL_RESPONSES.VIDEO_ENABLED, (payload) => this._onVideoEnabled(payload));
419
+ socket.on(PROTOCOL_RESPONSES.SCREENSHARE_STARTED, (payload) => this._onScreenshareChanged(payload, true));
420
+ socket.on(PROTOCOL_RESPONSES.SCREENSHARE_STOPPED, (payload) => this._onScreenshareChanged(payload, false));
421
+ }
422
+
423
+ async _onRoomJoined(payload) {
424
+ // We might have gotten an error
425
+ if (!payload.room?.clients) {
426
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
427
+ return;
428
+ }
429
+
430
+ if (!payload.selfId) {
431
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
432
+ return;
433
+ }
434
+
435
+ const myDeviceId = payload.room.clients.find((c) => payload.selfId === c.id)?.deviceId;
436
+ if (!myDeviceId) {
437
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
438
+ return;
439
+ }
440
+
441
+ // Try to remove our own pending client if this is a page reload
442
+ // Could also be a first normal room_joined which can never be glitch-free
443
+ if (!this._signalDisconnectTime) {
444
+ this._resetClientState(payload);
445
+ payload.room.clients = payload.room.clients.filter(
446
+ (c) => !(c.deviceId === myDeviceId && c.isPendingToLeave)
447
+ );
448
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
449
+ return;
450
+ }
451
+
452
+ // The threshold for trying glitch-free reconnect should be less than server-side configuration
453
+ const RECONNECT_THRESHOLD = payload.disconnectTimeout * 0.8;
454
+ const timeSinceDisconnect = Date.now() - this._signalDisconnectTime;
455
+ if (timeSinceDisconnect > RECONNECT_THRESHOLD) {
456
+ this._resetClientState(payload);
457
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
458
+ this.metrics.roomJoinedLate++;
459
+ return;
460
+ }
461
+
462
+ // At this point we want to try to attempt glitch-free reconnection experience
463
+
464
+ // Filter out our own pending client after page reload
465
+ payload.room.clients = payload.room.clients.filter((c) => !(c.deviceId === myDeviceId && c.isPendingToLeave));
466
+
467
+ const allStats = await getUpdatedStats();
468
+ payload.room.clients.forEach((client) => {
469
+ try {
470
+ if (client.id === payload.selfId) return;
471
+
472
+ // Maybe add client to state
473
+ if (!this._clients[client.id]) {
474
+ this._addClientToState(client);
475
+ return;
476
+ }
477
+ // Verify that rtcManager knows about the client
478
+ if (!this.rtcManager?.hasClient(client.id)) {
479
+ return;
480
+ }
481
+
482
+ // Verify that the client state hasn't changed
483
+ if (
484
+ this._hasClientStateChanged({
485
+ clientId: client.id,
486
+ webcam: client.isVideoEnabled,
487
+ mic: client.isAudioEnabled,
488
+ screenShare: client.streams.length > 1,
489
+ })
490
+ ) {
491
+ return;
492
+ }
493
+
494
+ if (this._wasClientSendingMedia(client.id)) {
495
+ // Verify the client media is still flowing (not stopped from other end)
496
+ if (!this._isClientMediaActive(allStats, client.id)) {
497
+ return;
498
+ }
499
+ }
500
+
501
+ client.mergeWithOldClientState = true;
502
+ } catch (error) {
503
+ logger$9.error("Failed to evaluate if we should merge client state %o", error);
504
+ this.metrics.evaluationFailed++;
505
+ }
506
+ });
507
+
508
+ // We will try to remove any remote client pending to leave
509
+ payload.room.clients.forEach((c) => {
510
+ if (c.isPendingToLeave) {
511
+ this._onPendingClientLeft({ clientId: c.id });
512
+ }
513
+ });
514
+
515
+ this.metrics.roomJoined++;
516
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
517
+ }
518
+
519
+ _onClientLeft(payload) {
520
+ const { clientId } = payload;
521
+ const client = this._clients[clientId];
522
+
523
+ // Remove client from state and clear timeout if client was pending to leave
524
+ if (client) {
525
+ clearTimeout(client.timeout);
526
+ delete this._clients[clientId];
527
+ }
528
+
529
+ // Old RTCManager only takes one argument, so rest is ignored.
530
+ this.rtcManager?.disconnect(clientId, /* activeBreakout */ null, payload.eventClaim);
531
+
532
+ this.emit(PROTOCOL_RESPONSES.CLIENT_LEFT, payload);
533
+ }
534
+
535
+ _onPendingClientLeft(payload) {
536
+ const { clientId } = payload;
537
+ const client = this._clients[clientId];
538
+
539
+ if (!client) {
540
+ logger$9.warn(`client ${clientId} not found`);
541
+ return;
542
+ }
543
+
544
+ // We've already started the check below, don't do it again
545
+ if (client.isPendingToLeave) {
546
+ return;
547
+ }
548
+
549
+ client.isPendingToLeave = true;
550
+ if (this._wasClientSendingMedia(clientId)) {
551
+ client.checkActiveMediaAttempts = 0;
552
+ this._abortIfNotActive(payload);
553
+ }
554
+ }
555
+
556
+ _onNewClient(payload) {
557
+ const {
558
+ client: { id: clientId, deviceId },
559
+ } = payload;
560
+
561
+ const client = this._clients[clientId];
562
+ if (client && client.isPendingToLeave) {
563
+ clearTimeout(client.timeoutHandler);
564
+ client.isPendingToLeave = false;
565
+ this.metrics.pendingClientCanceled++;
566
+ return;
567
+ }
568
+
569
+ this._getPendingClientsByDeviceId(deviceId).forEach((client) => {
570
+ clearTimeout(client.timeoutHandler);
571
+ client.isPendingToLeave = undefined;
572
+ this.emit(PROTOCOL_RESPONSES.CLIENT_LEFT, { clientId: client.clientId });
573
+ });
574
+
575
+ this._addClientToState(payload.client);
576
+ this.emit(PROTOCOL_RESPONSES.NEW_CLIENT, payload);
577
+ }
578
+
579
+ // Evaluate if we should send send client_left before getting it from signal-server
580
+ async _abortIfNotActive(payload) {
581
+ const { clientId } = payload;
582
+
583
+ let client = this._clients[clientId];
584
+ if (!client?.isPendingToLeave) return;
585
+
586
+ client.checkActiveMediaAttempts++;
587
+ if (client.checkActiveMediaAttempts > 3) {
588
+ return;
589
+ }
590
+
591
+ const stillActive = await this._checkIsActive(clientId);
592
+ if (stillActive) {
593
+ client.timeoutHandler = setTimeout(() => this._abortIfNotActive(payload), 500);
594
+ return;
595
+ }
596
+
597
+ client = this._clients[clientId];
598
+ if (client?.isPendingToLeave) {
599
+ clearTimeout(client.timeoutHandler);
600
+ delete this._clients[clientId];
601
+ this.emit(PROTOCOL_RESPONSES.CLIENT_LEFT, payload);
602
+ }
603
+ }
604
+
605
+ // Check if client is active
606
+ async _checkIsActive(clientId) {
607
+ const allStats = await getUpdatedStats();
608
+ return this._isClientMediaActive(allStats, clientId);
609
+ }
610
+
611
+ // Check if client has bitrates for all tracks
612
+ _isClientMediaActive(stats, clientId) {
613
+ const clientStats = stats?.[clientId];
614
+ let isActive = false;
615
+ if (clientStats) {
616
+ Object.entries(clientStats.tracks).forEach(([trackId, trackStats]) => {
617
+ if (trackId !== "probator")
618
+ Object.values(trackStats.ssrcs).forEach((ssrcStats) => {
619
+ if ((ssrcStats.bitrate || 0) > 0) isActive = true;
620
+ });
621
+ });
622
+ }
623
+ return isActive;
624
+ }
625
+
626
+ _onAudioEnabled(payload) {
627
+ const { clientId, isAudioEnabled } = payload;
628
+ this._clients[clientId] = {
629
+ ...(this._clients[clientId] || {}),
630
+ isAudioEnabled,
631
+ };
632
+ }
633
+
634
+ _onVideoEnabled(payload) {
635
+ const { clientId, isVideoEnabled } = payload;
636
+ this._clients[clientId] = {
637
+ ...(this._clients[clientId] || {}),
638
+ isVideoEnabled,
639
+ };
640
+ }
641
+
642
+ _onScreenshareChanged(payload, action) {
643
+ const { clientId } = payload;
644
+ this._clients[clientId] = {
645
+ ...(this._clients[clientId] || {}),
646
+ isScreenshareEnabled: action,
647
+ };
648
+ }
649
+
650
+ _hasClientStateChanged({ clientId, webcam, mic, screenShare }) {
651
+ const state = this._clients[clientId];
652
+
653
+ if (!state) {
654
+ throw new Error(`Client ${clientId} not found in ReconnectManager state`);
655
+ }
656
+
657
+ if (webcam !== state.isVideoEnabled) {
658
+ return true;
659
+ }
660
+ if (mic !== state.isAudioEnabled) {
661
+ return true;
662
+ }
663
+ if (screenShare !== state.isScreenshareEnabled) {
664
+ return true;
665
+ }
666
+
667
+ return false;
668
+ }
669
+
670
+ _addClientToState(newClient) {
671
+ this._clients[newClient.id] = {
672
+ ...(this._clients[newClient.id] || {}),
673
+ isAudioEnabled: newClient.isAudioEnabled,
674
+ isVideoEnabled: newClient.isVideoEnabled,
675
+ isScreenshareEnabled: newClient.streams.length > 1,
676
+ deviceId: newClient.deviceId,
677
+ isPendingToLeave: newClient.isPendingToLeave,
678
+ clientId: newClient.id,
679
+ };
680
+ }
681
+
682
+ _wasClientSendingMedia(clientId) {
683
+ const client = this._clients[clientId];
684
+
685
+ if (!client) {
686
+ throw new Error(`Client ${clientId} not found in ReconnectManager state`);
687
+ }
688
+
689
+ return client.isAudioEnabled || client.isVideoEnabled || client.isScreenshareEnabled;
690
+ }
691
+
692
+ _getPendingClientsByDeviceId(deviceId) {
693
+ return Object.values(this._clients).filter((clientState) => {
694
+ return clientState.deviceId === deviceId && clientState.isPendingToLeave;
695
+ });
696
+ }
697
+
698
+ _resetClientState(payload) {
699
+ this._clients = {};
700
+ payload.room.clients.forEach((client) => {
701
+ if (client.id === payload.selfId) {
702
+ return;
703
+ } else {
704
+ this._addClientToState(client);
705
+ }
706
+ });
707
+ }
708
+ }
709
+
187
710
  const DEFAULT_SOCKET_PATH = "/protocol/socket.io/v4";
188
711
 
712
+ const NOOP_KEEPALIVE_INTERVAL = 2000;
713
+
189
714
  /**
190
715
  * Wrapper class that extends the Socket.IO client library.
191
716
  */
192
717
  class ServerSocket {
193
- constructor(hostName, optionsOverrides) {
718
+ constructor(hostName, optionsOverrides, glitchFree) {
194
719
  this._socket = io(hostName, {
195
720
  path: DEFAULT_SOCKET_PATH,
196
721
  randomizationFactor: 0.5,
@@ -216,14 +741,41 @@ class ServerSocket {
216
741
  this._socket.io.opts.transports = ["websocket", "polling"];
217
742
  }
218
743
  });
744
+
745
+ if (glitchFree) this._reconnectManager = new ReconnectManager(this._socket);
746
+
219
747
  this._socket.on("connect", () => {
220
748
  const transport = this.getTransport();
221
749
  if (transport === "websocket") {
222
750
  this._wasConnectedUsingWebsocket = true;
751
+
752
+ // start noop keepalive loop to detect client side disconnects fast
753
+ if (!this.noopKeepaliveInterval)
754
+ this.noopKeepaliveInterval = setInterval(() => {
755
+ try {
756
+ // send a noop message if it thinks it is connected (might not be)
757
+ if (this._socket.connected) {
758
+ this._socket.io.engine.sendPacket("noop");
759
+ }
760
+ } catch (ex) {}
761
+ }, NOOP_KEEPALIVE_INTERVAL);
762
+ }
763
+ });
764
+
765
+ this._socket.on("disconnect", () => {
766
+ if (this.noopKeepaliveInterval) {
767
+ clearInterval(this.noopKeepaliveInterval);
768
+ this.noopKeepaliveInterval = null;
223
769
  }
224
770
  });
225
771
  }
226
772
 
773
+ setRtcManager(rtcManager) {
774
+ if (this._reconnectManager) {
775
+ this._reconnectManager.rtcManager = rtcManager;
776
+ }
777
+ }
778
+
227
779
  connect() {
228
780
  if (this.isConnected() || this.isConnecting()) {
229
781
  return;
@@ -282,6 +834,17 @@ class ServerSocket {
282
834
  * @returns {function} Function to deregister the listener.
283
835
  */
284
836
  on(eventName, handler) {
837
+ const relayableEvents = [
838
+ PROTOCOL_RESPONSES.ROOM_JOINED,
839
+ PROTOCOL_RESPONSES.CLIENT_LEFT,
840
+ PROTOCOL_RESPONSES.NEW_CLIENT,
841
+ ];
842
+
843
+ // Intercept certain events if glitch-free is enabled.
844
+ if (this._reconnectManager && relayableEvents.includes(eventName)) {
845
+ return this._interceptEvent(eventName, handler);
846
+ }
847
+
285
848
  this._socket.on(eventName, handler);
286
849
 
287
850
  return () => {
@@ -308,6 +871,23 @@ class ServerSocket {
308
871
  off(eventName, handler) {
309
872
  this._socket.off(eventName, handler);
310
873
  }
874
+
875
+ /**
876
+ * Intercept event and let ReconnectManager handle them.
877
+ */
878
+ _interceptEvent(eventName, handler) {
879
+ if (this._reconnectManager) {
880
+ this._reconnectManager.on(eventName, handler);
881
+ }
882
+
883
+ return () => {
884
+ if (this._reconnectManager) this._reconnectManager.removeListener(eventName, handler);
885
+ };
886
+ }
887
+
888
+ getGlitchFreeMetrics() {
889
+ return this._reconnectManager?.metrics;
890
+ }
311
891
  }
312
892
 
313
893
  function forwardSocketEvents(socket, dispatch) {
@@ -537,6 +1117,7 @@ const parseResolution = (res) => res.split(/[^\d]/g).map((n) => parseInt(n, 10))
537
1117
  function getMediaConstraints({
538
1118
  disableAEC,
539
1119
  disableAGC,
1120
+ fps24,
540
1121
  hd,
541
1122
  lax,
542
1123
  lowDataMode,
@@ -547,7 +1128,7 @@ function getMediaConstraints({
547
1128
  }) {
548
1129
  let HIGH_HEIGHT = 480;
549
1130
  let LOW_HEIGHT = 240;
550
- let LOW_FPS = 15;
1131
+ let NON_STANDARD_FPS = 0;
551
1132
 
552
1133
  if (hd) {
553
1134
  // respect user choice, but default to HD for pro, and SD for free
@@ -560,15 +1141,20 @@ function getMediaConstraints({
560
1141
  } else {
561
1142
  LOW_HEIGHT = 360;
562
1143
  }
563
- LOW_FPS = 30; // we still use 30fps because of assumptions about temporal layers
564
1144
  }
565
1145
 
1146
+ // Set framerate to 24 to increase quality/bandwidth
1147
+ if (fps24) NON_STANDARD_FPS = 24;
1148
+
1149
+ // Set framerate for low data, but only for non-simulcast
1150
+ if (lowDataMode && !simulcast) NON_STANDARD_FPS = 15;
1151
+
566
1152
  const constraints = {
567
1153
  audio: { ...(preferredDeviceIds.audioId && { deviceId: preferredDeviceIds.audioId }) },
568
1154
  video: {
569
1155
  ...(preferredDeviceIds.videoId ? { deviceId: preferredDeviceIds.videoId } : { facingMode: "user" }),
570
1156
  height: lowDataMode ? LOW_HEIGHT : HIGH_HEIGHT,
571
- ...(lowDataMode && { frameRate: LOW_FPS }),
1157
+ ...(NON_STANDARD_FPS && { frameRate: NON_STANDARD_FPS }),
572
1158
  },
573
1159
  };
574
1160
  if (lax) {
@@ -701,6 +1287,8 @@ var assert_1 = assert;
701
1287
 
702
1288
  var assert$1 = /*@__PURE__*/getDefaultExportFromCjs(assert_1);
703
1289
 
1290
+ const logger$8 = new Logger();
1291
+
704
1292
  const isMobile = /mobi/i.test(navigator.userAgent);
705
1293
 
706
1294
  class NoDevicesError extends Error {
@@ -720,7 +1308,7 @@ function getUserMedia(constraints) {
720
1308
 
721
1309
  return navigator.mediaDevices.getUserMedia(constraints).catch((error) => {
722
1310
  const message = `${error}, ${JSON.stringify(constraints, null, 2)}`;
723
- console.error(`getUserMedia ${message}`);
1311
+ logger$8.error(`getUserMedia ${message}`);
724
1312
  throw error;
725
1313
  });
726
1314
  }
@@ -906,7 +1494,7 @@ async function getStream(constraintOpt, { replaceStream, fallback = true } = {})
906
1494
  getConstraints({ ...constraintOpt, options: { ...constraintOpt.options, lax: true } })
907
1495
  );
908
1496
  } catch (e2) {
909
- console.warn(`Tried getting stream again with laxer constraints, but failed: ${"" + e2}`);
1497
+ logger$8.warn(`Tried getting stream again with laxer constraints, but failed: ${"" + e2}`);
910
1498
  }
911
1499
  // Message often hints at which was the problem, let's use that
912
1500
  const errMsg = ("" + e).toLowerCase();
@@ -915,7 +1503,7 @@ async function getStream(constraintOpt, { replaceStream, fallback = true } = {})
915
1503
  try {
916
1504
  stream = await getUserMedia(getConstraints({ ...constraintOpt, [problemWith]: null }));
917
1505
  } catch (e2) {
918
- console.warn(`Re-tried ${problemWith} with no constraints, but failed: ${"" + e2}`);
1506
+ logger$8.warn(`Re-tried ${problemWith} with no constraints, but failed: ${"" + e2}`);
919
1507
  }
920
1508
  }
921
1509
  if (!stream) {
@@ -926,7 +1514,7 @@ async function getStream(constraintOpt, { replaceStream, fallback = true } = {})
926
1514
  try {
927
1515
  stream = await getUserMedia(getConstraints({ ...constraintOpt, [kind]: false }));
928
1516
  } catch (e2) {
929
- console.warn(`Re-tried without ${kind}, but failed: ${"" + e2}`);
1517
+ logger$8.warn(`Re-tried without ${kind}, but failed: ${"" + e2}`);
930
1518
  }
931
1519
  if (stream) break;
932
1520
  }
@@ -1978,163 +2566,65 @@ const doConnectRoom = createAppThunk(() => (dispatch, getState) => {
1978
2566
  organizationId,
1979
2567
  roomKey,
1980
2568
  roomName,
1981
- selfId,
1982
- userAgent: `browser-sdk:${sdkVersion || "unknown"}`,
1983
- externalId,
1984
- });
1985
- dispatch(connectionStatusChanged("connecting"));
1986
- });
1987
- const selectRoomConnectionRaw = (state) => state.roomConnection;
1988
- const selectRoomConnectionSession = (state) => state.roomConnection.session;
1989
- const selectRoomConnectionSessionId = (state) => { var _a; return (_a = state.roomConnection.session) === null || _a === void 0 ? void 0 : _a.id; };
1990
- const selectRoomConnectionStatus = (state) => state.roomConnection.status;
1991
- const selectShouldConnectRoom = createSelector([selectOrganizationId, selectRoomConnectionStatus, selectSignalConnectionDeviceIdentified, selectLocalMediaStatus], (hasOrganizationIdFetched, roomConnectionStatus, signalConnectionDeviceIdentified, localMediaStatus) => {
1992
- if (localMediaStatus === "started" &&
1993
- signalConnectionDeviceIdentified &&
1994
- !!hasOrganizationIdFetched &&
1995
- ["initializing", "reconnect"].includes(roomConnectionStatus)) {
1996
- return true;
1997
- }
1998
- return false;
1999
- });
2000
- createReactor([selectShouldConnectRoom], ({ dispatch }, shouldConnectRoom) => {
2001
- if (shouldConnectRoom) {
2002
- dispatch(doConnectRoom());
2003
- }
2004
- });
2005
- startAppListening({
2006
- actionCreator: signalEvents.knockHandled,
2007
- effect: ({ payload }, { dispatch, getState }) => {
2008
- const { clientId, resolution } = payload;
2009
- const state = getState();
2010
- const selfId = selectSelfId(state);
2011
- if (clientId !== selfId) {
2012
- return;
2013
- }
2014
- if (resolution === "accepted") {
2015
- dispatch(setRoomKey(payload.metadata.roomKey));
2016
- dispatch(doConnectRoom());
2017
- }
2018
- else if (resolution === "rejected") {
2019
- dispatch(connectionStatusChanged("knock_rejected"));
2020
- }
2021
- },
2022
- });
2023
-
2024
- const EVENTS = {
2025
- CLIENT_CONNECTION_STATUS_CHANGED: "client_connection_status_changed",
2026
- STREAM_ADDED: "stream_added",
2027
- RTC_MANAGER_CREATED: "rtc_manager_created",
2028
- RTC_MANAGER_DESTROYED: "rtc_manager_destroyed",
2029
- LOCAL_STREAM_TRACK_ADDED: "local_stream_track_added",
2030
- LOCAL_STREAM_TRACK_REMOVED: "local_stream_track_removed",
2031
- REMOTE_STREAM_TRACK_ADDED: "remote_stream_track_added",
2032
- REMOTE_STREAM_TRACK_REMOVED: "remote_stream_track_removed",
2033
- };
2034
-
2035
- const TYPES = {
2036
- CONNECTING: "connecting",
2037
- CONNECTION_FAILED: "connection_failed",
2038
- CONNECTION_SUCCESSFUL: "connection_successful",
2039
- CONNECTION_DISCONNECTED: "connection_disconnected",
2040
- };
2041
-
2042
- // Protocol enum used for the CLIENT (Make sure to keep it in sync with its server counterpart)
2043
-
2044
- // Requests: messages from the client to the server
2045
- const PROTOCOL_REQUESTS = {
2046
- BLOCK_CLIENT: "block_client",
2047
- CLAIM_ROOM: "claim_room",
2048
- CLEAR_CHAT_HISTORY: "clear_chat_history",
2049
- ENABLE_AUDIO: "enable_audio",
2050
- ENABLE_VIDEO: "enable_video",
2051
- END_STREAM: "end_stream",
2052
- FETCH_MEDIASERVER_CONFIG: "fetch_mediaserver_config",
2053
- HANDLE_KNOCK: "handle_knock",
2054
- IDENTIFY_DEVICE: "identify_device",
2055
- INVITE_CLIENT_AS_MEMBER: "invite_client_as_member",
2056
- JOIN_ROOM: "join_room",
2057
- KICK_CLIENT: "kick_client",
2058
- KNOCK_ROOM: "knock_room",
2059
- LEAVE_ROOM: "leave_room",
2060
- SEND_CLIENT_METADATA: "send_client_metadata",
2061
- SET_LOCK: "set_lock",
2062
- SHARE_MEDIA: "share_media",
2063
- START_NEW_STREAM: "start_new_stream",
2064
- START_SCREENSHARE: "start_screenshare",
2065
- STOP_SCREENSHARE: "stop_screenshare",
2066
- START_URL_EMBED: "start_url_embed",
2067
- STOP_URL_EMBED: "stop_url_embed",
2068
- START_RECORDING: "start_recording",
2069
- STOP_RECORDING: "stop_recording",
2070
- SFU_TOKEN: "sfu_token",
2071
- };
2072
-
2073
- // Responses: messages from the server to the client, in response to requests
2074
- const PROTOCOL_RESPONSES = {
2075
- AUDIO_ENABLED: "audio_enabled",
2076
- BACKGROUND_IMAGE_CHANGED: "background_image_changed",
2077
- BLOCK_ADDED: "block_added",
2078
- BLOCK_REMOVED: "block_removed",
2079
- CHAT_HISTORY_CLEARED: "chat_history_cleared",
2080
- CLIENT_BLOCKED: "client_blocked",
2081
- CLIENT_INVITED_AS_MEMBER: "client_invited_as_member",
2082
- CLIENT_KICKED: "client_kicked",
2083
- CLIENT_LEFT: "client_left",
2084
- CLIENT_METADATA_RECEIVED: "client_metadata_received",
2085
- CLIENT_READY: "client_ready",
2086
- CLIENT_ROLE_CHANGED: "client_role_changed",
2087
- CLIENT_USER_ID_CHANGED: "client_user_id_changed",
2088
- CONTACTS_UPDATED: "contacts_updated",
2089
- DEVICE_IDENTIFIED: "device_identified",
2090
- ROOM_ROLES_UPDATED: "room_roles_updated",
2091
- KNOCK_HANDLED: "knock_handled",
2092
- KNOCK_PAGE_BACKGROUND_CHANGED: "knock_page_background_changed",
2093
- KNOCKER_LEFT: "knocker_left",
2094
- MEDIASERVER_CONFIG: "mediaserver_config",
2095
- MEDIA_SHARED: "media_shared",
2096
- MEMBER_INVITE: "member_invite",
2097
- NEW_CLIENT: "new_client",
2098
- NEW_STREAM_STARTED: "new_stream_started",
2099
- SCREENSHARE_STARTED: "screenshare_started",
2100
- SCREENSHARE_STOPPED: "screenshare_stopped",
2101
- OWNER_NOTIFIED: "owner_notified",
2102
- OWNERS_CHANGED: "owners_changed",
2103
- PLAY_CLIENT_STICKER: "play_client_sticker",
2104
- ROOM_INTEGRATION_ENABLED: "room_integration_enabled",
2105
- ROOM_INTEGRATION_DISABLED: "room_integration_disabled",
2106
- ROOM_JOINED: "room_joined",
2107
- ROOM_KNOCKED: "room_knocked",
2108
- ROOM_LEFT: "room_left",
2109
- ROOM_LOCKED: "room_locked",
2110
- ROOM_PERMISSIONS_CHANGED: "room_permissions_changed",
2111
- ROOM_LOGO_CHANGED: "room_logo_changed",
2112
- ROOM_TYPE_CHANGED: "room_type_changed",
2113
- ROOM_MODE_CHANGED: "room_mode_changed",
2114
- SOCKET_USER_ID_CHANGED: "socket_user_id_changed",
2115
- STICKERS_UNLOCKED: "stickers_unlocked",
2116
- STREAM_ENDED: "stream_ended",
2117
- URL_EMBED_STARTED: "url_embed_started",
2118
- URL_EMBED_STOPPED: "url_embed_stopped",
2119
- RECORDING_STARTED: "recording_started",
2120
- RECORDING_STOPPED: "recording_stopped",
2121
- USER_NOTIFIED: "user_notified",
2122
- VIDEO_ENABLED: "video_enabled",
2123
- CLIENT_UNABLE_TO_JOIN: "client_unable_to_join",
2569
+ selfId,
2570
+ userAgent: `browser-sdk:${sdkVersion || "unknown"}`,
2571
+ externalId,
2572
+ });
2573
+ dispatch(connectionStatusChanged("connecting"));
2574
+ });
2575
+ const selectRoomConnectionRaw = (state) => state.roomConnection;
2576
+ const selectRoomConnectionSession = (state) => state.roomConnection.session;
2577
+ const selectRoomConnectionSessionId = (state) => { var _a; return (_a = state.roomConnection.session) === null || _a === void 0 ? void 0 : _a.id; };
2578
+ const selectRoomConnectionStatus = (state) => state.roomConnection.status;
2579
+ const selectShouldConnectRoom = createSelector([selectOrganizationId, selectRoomConnectionStatus, selectSignalConnectionDeviceIdentified, selectLocalMediaStatus], (hasOrganizationIdFetched, roomConnectionStatus, signalConnectionDeviceIdentified, localMediaStatus) => {
2580
+ if (localMediaStatus === "started" &&
2581
+ signalConnectionDeviceIdentified &&
2582
+ !!hasOrganizationIdFetched &&
2583
+ ["initializing", "reconnect"].includes(roomConnectionStatus)) {
2584
+ return true;
2585
+ }
2586
+ return false;
2587
+ });
2588
+ createReactor([selectShouldConnectRoom], ({ dispatch }, shouldConnectRoom) => {
2589
+ if (shouldConnectRoom) {
2590
+ dispatch(doConnectRoom());
2591
+ }
2592
+ });
2593
+ startAppListening({
2594
+ actionCreator: signalEvents.knockHandled,
2595
+ effect: ({ payload }, { dispatch, getState }) => {
2596
+ const { clientId, resolution } = payload;
2597
+ const state = getState();
2598
+ const selfId = selectSelfId(state);
2599
+ if (clientId !== selfId) {
2600
+ return;
2601
+ }
2602
+ if (resolution === "accepted") {
2603
+ dispatch(setRoomKey(payload.metadata.roomKey));
2604
+ dispatch(doConnectRoom());
2605
+ }
2606
+ else if (resolution === "rejected") {
2607
+ dispatch(connectionStatusChanged("knock_rejected"));
2608
+ }
2609
+ },
2610
+ });
2611
+
2612
+ const EVENTS = {
2613
+ CLIENT_CONNECTION_STATUS_CHANGED: "client_connection_status_changed",
2614
+ STREAM_ADDED: "stream_added",
2615
+ RTC_MANAGER_CREATED: "rtc_manager_created",
2616
+ RTC_MANAGER_DESTROYED: "rtc_manager_destroyed",
2617
+ LOCAL_STREAM_TRACK_ADDED: "local_stream_track_added",
2618
+ LOCAL_STREAM_TRACK_REMOVED: "local_stream_track_removed",
2619
+ REMOTE_STREAM_TRACK_ADDED: "remote_stream_track_added",
2620
+ REMOTE_STREAM_TRACK_REMOVED: "remote_stream_track_removed",
2124
2621
  };
2125
2622
 
2126
- // Relays: messages between clients, relayed through the server
2127
- const RELAY_MESSAGES = {
2128
- CHAT_MESSAGE: "chat_message",
2129
- CHAT_READ_STATE: "chat_read_state",
2130
- CHAT_STATE: "chat_state",
2131
- ICE_CANDIDATE: "ice_candidate",
2132
- ICE_END_OF_CANDIDATES: "ice_endofcandidates",
2133
- READY_TO_RECEIVE_OFFER: "ready_to_receive_offer",
2134
- REMOTE_CLIENT_MEDIA_REQUEST: "remote_client_media_request",
2135
- SDP_ANSWER: "sdp_answer",
2136
- SDP_OFFER: "sdp_offer",
2137
- VIDEO_STICKER: "video_sticker",
2623
+ const TYPES = {
2624
+ CONNECTING: "connecting",
2625
+ CONNECTION_FAILED: "connection_failed",
2626
+ CONNECTION_SUCCESSFUL: "connection_successful",
2627
+ CONNECTION_DISCONNECTED: "connection_disconnected",
2138
2628
  };
2139
2629
 
2140
2630
  const CAMERA_STREAM_ID$2 = "0";
@@ -2250,13 +2740,22 @@ function detectMicrophoneNotWorking(pc) {
2250
2740
  var rtcManagerEvents = {
2251
2741
  CAMERA_NOT_WORKING: "camera_not_working",
2252
2742
  CONNECTION_BLOCKED_BY_NETWORK: "connection_blocked_by_network",
2743
+ ICE_IPV6_SEEN: "ice_ipv6_seen",
2744
+ ICE_MDNS_SEEN: "ice_mdns_seen",
2745
+ ICE_NO_PUBLIC_IP_GATHERED: "ice_no_public_ip_gathered",
2746
+ ICE_NO_PUBLIC_IP_GATHERED_3SEC: "ice_no_public_ip_gathered_3sec",
2747
+ ICE_RESTART: "ice_restart",
2253
2748
  MICROPHONE_NOT_WORKING: "microphone_not_working",
2254
2749
  MICROPHONE_STOPPED_WORKING: "microphone_stopped_working",
2750
+ NEW_PC: "new_pc",
2751
+ SFU_CONNECTION_OPEN: "sfu_connection_open",
2255
2752
  SFU_CONNECTION_CLOSED: "sfu_connection_closed",
2256
2753
  COLOCATION_SPEAKER: "colocation_speaker",
2257
2754
  DOMINANT_SPEAKER: "dominant_speaker",
2258
2755
  };
2259
2756
 
2757
+ const logger$7 = new Logger();
2758
+
2260
2759
  const browserName$3 = adapter.browserDetails.browser;
2261
2760
  const browserVersion$1 = adapter.browserDetails.version;
2262
2761
 
@@ -2298,8 +2797,7 @@ function setCodecPreferenceSDP(sdp, vp9On, redOn) {
2298
2797
  const newSdp = sdpTransform.write(sdpObject);
2299
2798
  return newSdp;
2300
2799
  } catch (error) {
2301
- // eslint-disable-next-line no-console
2302
- console.log("setCodecPreferenceSDP error:", error);
2800
+ logger$7.error("setCodecPreferenceSDP error:", error);
2303
2801
  }
2304
2802
  }
2305
2803
 
@@ -2446,10 +2944,19 @@ const MAXIMUM_TURN_BANDWIDTH_UNLIMITED = -1; // kbps;
2446
2944
 
2447
2945
  const MEDIA_JITTER_BUFFER_TARGET = 400; // milliseconds;
2448
2946
 
2947
+ const logger$6 = new Logger();
2948
+
2449
2949
  class Session {
2450
- constructor({ peerConnectionId, bandwidth, maximumTurnBandwidth, deprioritizeH264Encoding, logger = console }) {
2950
+ constructor({ peerConnectionId, bandwidth, maximumTurnBandwidth, deprioritizeH264Encoding }) {
2451
2951
  this.peerConnectionId = peerConnectionId;
2452
2952
  this.relayCandidateSeen = false;
2953
+ this.serverReflexiveCandidateSeen = false;
2954
+ this.publicHostCandidateSeen = false;
2955
+ this.ipv6HostCandidateSeen = false;
2956
+ this.ipv6HostCandidateTeredoSeen = false;
2957
+ this.ipv6HostCandidate6to4Seen = false;
2958
+ this.mdnsHostCandidateSeen = false;
2959
+
2453
2960
  this.pc = null;
2454
2961
  this.wasEverConnected = false;
2455
2962
  this.connectionStatus = null;
@@ -2469,7 +2976,6 @@ class Session {
2469
2976
  });
2470
2977
  this.offerOptions = { offerToReceiveAudio: true, offerToReceiveVideo: true };
2471
2978
  this._deprioritizeH264Encoding = deprioritizeH264Encoding;
2472
- this._logger = logger;
2473
2979
  }
2474
2980
 
2475
2981
  setAndGetPeerConnection({ clientId, constraints, peerConnectionConfig, shouldAddLocalVideo }) {
@@ -2613,7 +3119,7 @@ class Session {
2613
3119
  return setVideoBandwidthUsingSetParameters(this.pc, this.bandwidth);
2614
3120
  },
2615
3121
  (e) => {
2616
- this._logger.warn("Could not set remote description from remote answer: ", e);
3122
+ logger$6.warn("Could not set remote description from remote answer: ", e);
2617
3123
  }
2618
3124
  );
2619
3125
  }
@@ -2634,7 +3140,7 @@ class Session {
2634
3140
  return;
2635
3141
  }
2636
3142
  this.pc.addIceCandidate(candidate).catch((e) => {
2637
- this._logger.warn("Failed to add ICE candidate ('%s'): %s", candidate ? candidate.candidate : null, e);
3143
+ logger$6.warn("Failed to add ICE candidate ('%s'): %s", candidate ? candidate.candidate : null, e);
2638
3144
  });
2639
3145
  });
2640
3146
  }
@@ -2656,7 +3162,7 @@ class Session {
2656
3162
  // do not handle state change events when we close the connection explicitly
2657
3163
  pc.close();
2658
3164
  } catch (e) {
2659
- console.warn("failures during close of session", e);
3165
+ logger$6.warn("failures during close of session", e);
2660
3166
  // we're not interested in errors from RTCPeerConnection.close()
2661
3167
  }
2662
3168
  }
@@ -2672,7 +3178,7 @@ class Session {
2672
3178
  const senders = pc.getSenders();
2673
3179
  function dbg(msg) {
2674
3180
  const tr = (t) => t && `id:${t.id},kind:${t.kind},state:${t.readyState}`;
2675
- this._logger.warn(
3181
+ logger$6.warn(
2676
3182
  `${msg}. newTrack:${tr(newTrack)}, oldTrack:${tr(oldTrack)}, sender tracks: ${JSON.stringify(
2677
3183
  senders.map((s) => `s ${tr(s.track)}`)
2678
3184
  )}, sender first codecs: ${JSON.stringify(senders.map((s) => (s.getParameters().codecs || [])[0]))}`
@@ -2826,6 +3332,21 @@ class Session {
2826
3332
 
2827
3333
  setVideoBandwidthUsingSetParameters(this.pc, this.bandwidth);
2828
3334
  }
3335
+
3336
+ setAudioOnly(enable, excludedTrackIds = []) {
3337
+ this.pc
3338
+ .getTransceivers()
3339
+ .filter(
3340
+ (videoTransceiver) =>
3341
+ videoTransceiver?.direction !== "recvonly" &&
3342
+ videoTransceiver?.receiver?.track?.kind === "video" &&
3343
+ !excludedTrackIds.includes(videoTransceiver?.receiver?.track?.id) &&
3344
+ !excludedTrackIds.includes(videoTransceiver?.sender?.track?.id)
3345
+ )
3346
+ .forEach((videoTransceiver) => {
3347
+ videoTransceiver.direction = enable ? "sendonly" : "sendrecv";
3348
+ });
3349
+ }
2829
3350
  }
2830
3351
 
2831
3352
  // transforms a maplike to an object. Mostly for getStats +
@@ -2924,6 +3445,8 @@ var rtcstats = function(trace, getStatsInterval, prefixesToWrap) {
2924
3445
  var peerconnectioncounter = 0;
2925
3446
  var isFirefox = !!window.mozRTCPeerConnection;
2926
3447
  var isEdge = !!window.RTCIceGatherer;
3448
+ var prevById = {};
3449
+
2927
3450
  prefixesToWrap.forEach(function(prefix) {
2928
3451
  if (!window[prefix + 'RTCPeerConnection']) {
2929
3452
  return;
@@ -2994,13 +3517,12 @@ var rtcstats = function(trace, getStatsInterval, prefixesToWrap) {
2994
3517
  trace('ondatachannel', id, [event.channel.id, event.channel.label]);
2995
3518
  });
2996
3519
 
2997
- var prev = {};
2998
3520
  var getStats = function() {
2999
3521
  pc.getStats(null).then(function(res) {
3000
3522
  var now = map2obj(res);
3001
3523
  var base = JSON.parse(JSON.stringify(now)); // our new prev
3002
- trace('getstats', id, deltaCompression(prev, now));
3003
- prev = base;
3524
+ trace('getstats', id, deltaCompression(prevById[id] || {}, now));
3525
+ prevById[id] = base;
3004
3526
  });
3005
3527
  };
3006
3528
  // TODO: do we want one big interval and all peerconnections
@@ -3220,6 +3742,13 @@ var rtcstats = function(trace, getStatsInterval, prefixesToWrap) {
3220
3742
  }
3221
3743
  });
3222
3744
  */
3745
+
3746
+ return {
3747
+ resetDelta() {
3748
+ prevById = {};
3749
+ }
3750
+ }
3751
+
3223
3752
  };
3224
3753
 
3225
3754
  var rtcstats$1 = /*@__PURE__*/getDefaultExportFromCjs(rtcstats);
@@ -3228,11 +3757,18 @@ var rtcstats$1 = /*@__PURE__*/getDefaultExportFromCjs(rtcstats);
3228
3757
 
3229
3758
  const RTCSTATS_PROTOCOL_VERSION = "1.0";
3230
3759
 
3760
+ // when not connected we need to buffer at least a few getstats reports
3761
+ // as they are delta compressed and we need the initial properties
3762
+ const GETSTATS_BUFFER_SIZE = 20;
3763
+
3231
3764
  const clientInfo = {
3232
- id: v4(), // shared id across rtcstats reconnects
3765
+ id: v4$1(), // shared id across rtcstats reconnects
3233
3766
  connectionNumber: 0,
3234
3767
  };
3235
3768
 
3769
+ const noop = () => {};
3770
+ let resetDelta = noop;
3771
+
3236
3772
  // Inlined version of rtcstats/trace-ws with improved disconnect handling.
3237
3773
  function rtcStatsConnection(wsURL, logger = console) {
3238
3774
  const buffer = [];
@@ -3245,6 +3781,7 @@ function rtcStatsConnection(wsURL, logger = console) {
3245
3781
  let connectionShouldBeOpen;
3246
3782
  let connectionAttempt = 0;
3247
3783
  let hasPassedOnRoomSessionId = false;
3784
+ let getStatsBufferUsed = 0;
3248
3785
 
3249
3786
  const connection = {
3250
3787
  connected: false,
@@ -3282,8 +3819,15 @@ function rtcStatsConnection(wsURL, logger = console) {
3282
3819
  if (ws.readyState === WebSocket.OPEN) {
3283
3820
  connectionAttempt = 0;
3284
3821
  ws.send(JSON.stringify(args));
3285
- } else if (args[0] !== "getstats" && !(args[0] === "customEvent" && args[2].type === "insightsStats")) {
3286
- // buffer all but stats when not connected
3822
+ } else if (args[0] === "getstats") {
3823
+ // only buffer getStats for a while
3824
+ // we don't want this to pile up, but we need at least the initial reports
3825
+ if (getStatsBufferUsed < GETSTATS_BUFFER_SIZE) {
3826
+ getStatsBufferUsed++;
3827
+ buffer.push(args);
3828
+ }
3829
+ } else if (args[0] === "customEvent" && args[2].type === "insightsStats") ; else {
3830
+ // buffer everything else
3287
3831
  buffer.push(args);
3288
3832
  }
3289
3833
 
@@ -3318,6 +3862,7 @@ function rtcStatsConnection(wsURL, logger = console) {
3318
3862
  ws.onclose = (e) => {
3319
3863
  connection.connected = false;
3320
3864
  logger.info(`[RTCSTATS] Closed ${e.code}`);
3865
+ resetDelta();
3321
3866
  };
3322
3867
  ws.onopen = () => {
3323
3868
  // send client info after each connection, so analysis tools can handle reconnections
@@ -3342,10 +3887,11 @@ function rtcStatsConnection(wsURL, logger = console) {
3342
3887
  ws.send(JSON.stringify(userRole));
3343
3888
  }
3344
3889
 
3345
- // send buffered events (non-getStats)
3890
+ // send buffered events
3346
3891
  while (buffer.length) {
3347
3892
  ws.send(JSON.stringify(buffer.shift()));
3348
3893
  }
3894
+ getStatsBufferUsed = 0;
3349
3895
  };
3350
3896
  },
3351
3897
  };
@@ -3354,11 +3900,13 @@ function rtcStatsConnection(wsURL, logger = console) {
3354
3900
  }
3355
3901
 
3356
3902
  const server = rtcStatsConnection("wss://rtcstats.srv.whereby.com" );
3357
- rtcstats$1(
3903
+ const stats = rtcstats$1(
3358
3904
  server.trace,
3359
3905
  10000, // query once every 10 seconds.
3360
3906
  [""] // only shim unprefixed RTCPeerConnecion.
3361
3907
  );
3908
+ // on node clients this function can be undefined
3909
+ resetDelta = stats?.resetDelta || noop;
3362
3910
 
3363
3911
  const rtcStats = {
3364
3912
  sendEvent: (type, value) => {
@@ -3376,6 +3924,8 @@ const rtcStats = {
3376
3924
  server,
3377
3925
  };
3378
3926
 
3927
+ const logger$5 = new Logger();
3928
+
3379
3929
  const CAMERA_STREAM_ID$1 = RtcStream.getCameraId();
3380
3930
  const browserName$2 = adapter.browserDetails.browser;
3381
3931
  const browserVersion = adapter.browserDetails.version;
@@ -3392,7 +3942,7 @@ if (browserName$2 === "chrome") {
3392
3942
  }
3393
3943
 
3394
3944
  class BaseRtcManager {
3395
- constructor({ selfId, room, emitter, serverSocket, webrtcProvider, features, logger = console }) {
3945
+ constructor({ selfId, room, emitter, serverSocket, webrtcProvider, features }) {
3396
3946
  assert$1.ok(selfId, "selfId is required");
3397
3947
  assert$1.ok(room, "room is required");
3398
3948
  assert$1.ok(emitter && emitter.emit, "emitter is required");
@@ -3407,13 +3957,14 @@ class BaseRtcManager {
3407
3957
  this.peerConnections = {};
3408
3958
  this.localStreams = {};
3409
3959
  this.enabledLocalStreamIds = [];
3960
+ this._screenshareVideoTrackIds = [];
3410
3961
  this._socketListenerDeregisterFunctions = [];
3411
3962
  this._localStreamDeregisterFunction = null;
3412
3963
  this._emitter = emitter;
3413
3964
  this._serverSocket = serverSocket;
3414
3965
  this._webrtcProvider = webrtcProvider;
3415
3966
  this._features = features || {};
3416
- this._logger = logger;
3967
+ this._isAudioOnlyMode = false;
3417
3968
 
3418
3969
  this.offerOptions = { offerToReceiveAudio: true, offerToReceiveVideo: true };
3419
3970
  this._pendingActionsForConnectedPeerConnections = [];
@@ -3432,6 +3983,8 @@ class BaseRtcManager {
3432
3983
  iceServers: iceServers.iceServers || [],
3433
3984
  mediaserverConfigTtlSeconds,
3434
3985
  });
3986
+
3987
+ this.totalSessionsCreated = 0;
3435
3988
  }
3436
3989
 
3437
3990
  numberOfPeerconnections() {
@@ -3482,7 +4035,7 @@ class BaseRtcManager {
3482
4035
  receiver.playoutDelayHint = MEDIA_JITTER_BUFFER_TARGET / 1000; // seconds
3483
4036
  });
3484
4037
  } catch (error) {
3485
- this._logger.error("Error during setting jitter buffer target:", error);
4038
+ logger$5.error("Error during setting jitter buffer target:", error);
3486
4039
  }
3487
4040
  }
3488
4041
 
@@ -3537,6 +4090,8 @@ class BaseRtcManager {
3537
4090
  : MAXIMUM_TURN_BANDWIDTH,
3538
4091
  deprioritizeH264Encoding,
3539
4092
  });
4093
+
4094
+ this.totalSessionsCreated++;
3540
4095
  }
3541
4096
  return session;
3542
4097
  }
@@ -3630,6 +4185,8 @@ class BaseRtcManager {
3630
4185
  clientId,
3631
4186
  });
3632
4187
 
4188
+ setTimeout(() => this._emit(rtcManagerEvents.NEW_PC), 0);
4189
+
3633
4190
  pc.ontrack = (event) => {
3634
4191
  const stream = event.streams[0];
3635
4192
  if (stream.id === "default" && stream.getAudioTracks().length === 0) {
@@ -3691,6 +4248,11 @@ class BaseRtcManager {
3691
4248
  this.maybeRestrictRelayBandwidth(session);
3692
4249
  }
3693
4250
  }
4251
+
4252
+ if (this._isAudioOnlyMode) {
4253
+ session.setAudioOnly(true, this._screenshareVideoTrackIds);
4254
+ }
4255
+
3694
4256
  session.registerConnected();
3695
4257
  break;
3696
4258
  case "disconnected":
@@ -3794,7 +4356,7 @@ class BaseRtcManager {
3794
4356
  _cleanup(peerConnectionId) {
3795
4357
  const session = this._getSession(peerConnectionId);
3796
4358
  if (!session) {
3797
- this._logger.warn("No RTCPeerConnection in RTCManager.disconnect()", peerConnectionId);
4359
+ logger$5.warn("No RTCPeerConnection in RTCManager.disconnect()", peerConnectionId);
3798
4360
  return;
3799
4361
  }
3800
4362
  session.close();
@@ -3824,10 +4386,10 @@ class BaseRtcManager {
3824
4386
  const promises = [];
3825
4387
  this._forEachPeerConnection((session) => {
3826
4388
  if (!session.hasConnectedPeerConnection()) {
3827
- this._logger.log("Session doesn't have a connected PeerConnection, adding pending action!");
4389
+ logger$5.info("Session doesn't have a connected PeerConnection, adding pending action!");
3828
4390
  const pendingActions = this._pendingActionsForConnectedPeerConnections;
3829
4391
  if (!pendingActions) {
3830
- this._logger.warn(
4392
+ logger$5.warn(
3831
4393
  `No pending action is created to repalce track, because the pending actions array is null`
3832
4394
  );
3833
4395
  return;
@@ -3836,7 +4398,7 @@ class BaseRtcManager {
3836
4398
  const action = () => {
3837
4399
  const replacedTrackPromise = session.replaceTrack(oldTrack, newTrack);
3838
4400
  if (!replacedTrackPromise) {
3839
- this._logger.error("replaceTrack returned false!");
4401
+ logger$5.error("replaceTrack returned false!");
3840
4402
  reject(`ReplaceTrack returned false`);
3841
4403
  return;
3842
4404
  }
@@ -3849,7 +4411,7 @@ class BaseRtcManager {
3849
4411
  }
3850
4412
  const replacedTrackResult = session.replaceTrack(oldTrack, newTrack);
3851
4413
  if (!replacedTrackResult) {
3852
- this._logger.error("replaceTrack returned false!");
4414
+ logger$5.error("replaceTrack returned false!");
3853
4415
  return;
3854
4416
  }
3855
4417
  promises.push(replacedTrackResult);
@@ -3919,6 +4481,7 @@ class BaseRtcManager {
3919
4481
  }
3920
4482
 
3921
4483
  // at this point it is clearly a screensharing stream.
4484
+ this._screenshareVideoTrackIds.push(stream.getVideoTracks()[0].id);
3922
4485
  this._shareScreen(streamId, stream);
3923
4486
  return;
3924
4487
  }
@@ -4017,7 +4580,7 @@ class BaseRtcManager {
4017
4580
 
4018
4581
  this._serverSocket.on(PROTOCOL_RESPONSES.MEDIASERVER_CONFIG, (data) => {
4019
4582
  if (data.error) {
4020
- this._logger.warn("FETCH_MEDIASERVER_CONFIG failed:", data.error);
4583
+ logger$5.warn("FETCH_MEDIASERVER_CONFIG failed:", data.error);
4021
4584
  return;
4022
4585
  }
4023
4586
  this._updateAndScheduleMediaServersRefresh(data);
@@ -4030,7 +4593,7 @@ class BaseRtcManager {
4030
4593
  this._serverSocket.on(RELAY_MESSAGES.ICE_CANDIDATE, (data) => {
4031
4594
  const session = this._getSession(data.clientId);
4032
4595
  if (!session) {
4033
- this._logger.warn("No RTCPeerConnection on ICE_CANDIDATE", data);
4596
+ logger$5.warn("No RTCPeerConnection on ICE_CANDIDATE", data);
4034
4597
  return;
4035
4598
  }
4036
4599
  session.addIceCandidate(data.message);
@@ -4039,7 +4602,7 @@ class BaseRtcManager {
4039
4602
  this._serverSocket.on(RELAY_MESSAGES.ICE_END_OF_CANDIDATES, (data) => {
4040
4603
  const session = this._getSession(data.clientId);
4041
4604
  if (!session) {
4042
- this._logger.warn("No RTCPeerConnection on ICE_END_OF_CANDIDATES", data);
4605
+ logger$5.warn("No RTCPeerConnection on ICE_END_OF_CANDIDATES", data);
4043
4606
  return;
4044
4607
  }
4045
4608
  session.addIceCandidate(null);
@@ -4049,7 +4612,7 @@ class BaseRtcManager {
4049
4612
  this._serverSocket.on(RELAY_MESSAGES.SDP_OFFER, (data) => {
4050
4613
  const session = this._getSession(data.clientId);
4051
4614
  if (!session) {
4052
- this._logger.warn("No RTCPeerConnection on SDP_OFFER", data);
4615
+ logger$5.warn("No RTCPeerConnection on SDP_OFFER", data);
4053
4616
  return;
4054
4617
  }
4055
4618
  const offer = this._transformIncomingSdp(data.message, session.pc);
@@ -4065,12 +4628,35 @@ class BaseRtcManager {
4065
4628
  this._serverSocket.on(RELAY_MESSAGES.SDP_ANSWER, (data) => {
4066
4629
  const session = this._getSession(data.clientId);
4067
4630
  if (!session) {
4068
- this._logger.warn("No RTCPeerConnection on SDP_ANSWER", data);
4631
+ logger$5.warn("No RTCPeerConnection on SDP_ANSWER", data);
4069
4632
  return;
4070
4633
  }
4071
4634
  const answer = this._transformIncomingSdp(data.message, session.pc);
4072
4635
  session.handleAnswer(answer);
4073
4636
  }),
4637
+
4638
+ // if this is a reconnect to signal-server during screen-share we must let signal-server know
4639
+ this._serverSocket.on(PROTOCOL_RESPONSES.ROOM_JOINED, ({ room: { sfuServer: isSfu } }) => {
4640
+ if (isSfu || !this._wasScreenSharing) return;
4641
+
4642
+ const screenShareStreamId = Object.keys(this.localStreams).find((id) => id !== CAMERA_STREAM_ID$1);
4643
+ if (!screenShareStreamId) {
4644
+ return;
4645
+ }
4646
+
4647
+ const screenshareStream = this.localStreams[screenShareStreamId];
4648
+ if (!screenshareStream) {
4649
+ logger$5.warn("screenshare stream %s not found", screenShareStreamId);
4650
+ return;
4651
+ }
4652
+
4653
+ const hasAudioTrack = screenshareStream.getAudioTracks().length > 0;
4654
+
4655
+ this._emitServerEvent(PROTOCOL_REQUESTS.START_SCREENSHARE, {
4656
+ streamId: screenShareStreamId,
4657
+ hasAudioTrack,
4658
+ });
4659
+ }),
4074
4660
  ];
4075
4661
  }
4076
4662
 
@@ -4104,6 +4690,27 @@ class BaseRtcManager {
4104
4690
  track.removeEventListener("ended", this._audioTrackOnEnded);
4105
4691
  }
4106
4692
 
4693
+ setAudioOnly(audioOnly) {
4694
+ this._isAudioOnlyMode = audioOnly;
4695
+
4696
+ this._forEachPeerConnection((session) => {
4697
+ if (session.hasConnectedPeerConnection()) {
4698
+ this._withForcedRenegotiation(session, () =>
4699
+ session.setAudioOnly(this._isAudioOnlyMode, this._screenshareVideoTrackIds)
4700
+ );
4701
+ }
4702
+ });
4703
+ }
4704
+
4705
+ setRemoteScreenshareVideoTrackIds(remoteScreenshareVideoTrackIds = []) {
4706
+ const localScreenshareStream = this._getFirstLocalNonCameraStream();
4707
+
4708
+ this._screenshareVideoTrackIds = [
4709
+ ...(localScreenshareStream?.track ? [localScreenshareStream.track.id] : []),
4710
+ ...remoteScreenshareVideoTrackIds,
4711
+ ];
4712
+ }
4713
+
4107
4714
  setRoomSessionId(roomSessionId) {
4108
4715
  this._roomSessionId = roomSessionId;
4109
4716
  }
@@ -4137,6 +4744,54 @@ function getOptimalBitrate(width, height, frameRate) {
4137
4744
  return targetBitrate;
4138
4745
  }
4139
4746
 
4747
+ // taken from https://github.com/sindresorhus/ip-regex ^5.0.0
4748
+ // inlined because it's import caused errors in browser-sdk when running tests
4749
+ const word = "[a-fA-F\\d:]";
4750
+
4751
+ const boundry = (options) =>
4752
+ options && options.includeBoundaries ? `(?:(?<=\\s|^)(?=${word})|(?<=${word})(?=\\s|$))` : "";
4753
+
4754
+ const v4 = "(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}";
4755
+
4756
+ const v6segment = "[a-fA-F\\d]{1,4}";
4757
+
4758
+ const v6 = `
4759
+ (?:
4760
+ (?:${v6segment}:){7}(?:${v6segment}|:)| // 1:2:3:4:5:6:7:: 1:2:3:4:5:6:7:8
4761
+ (?:${v6segment}:){6}(?:${v4}|:${v6segment}|:)| // 1:2:3:4:5:6:: 1:2:3:4:5:6::8 1:2:3:4:5:6::8 1:2:3:4:5:6::1.2.3.4
4762
+ (?:${v6segment}:){5}(?::${v4}|(?::${v6segment}){1,2}|:)| // 1:2:3:4:5:: 1:2:3:4:5::7:8 1:2:3:4:5::8 1:2:3:4:5::7:1.2.3.4
4763
+ (?:${v6segment}:){4}(?:(?::${v6segment}){0,1}:${v4}|(?::${v6segment}){1,3}|:)| // 1:2:3:4:: 1:2:3:4::6:7:8 1:2:3:4::8 1:2:3:4::6:7:1.2.3.4
4764
+ (?:${v6segment}:){3}(?:(?::${v6segment}){0,2}:${v4}|(?::${v6segment}){1,4}|:)| // 1:2:3:: 1:2:3::5:6:7:8 1:2:3::8 1:2:3::5:6:7:1.2.3.4
4765
+ (?:${v6segment}:){2}(?:(?::${v6segment}){0,3}:${v4}|(?::${v6segment}){1,5}|:)| // 1:2:: 1:2::4:5:6:7:8 1:2::8 1:2::4:5:6:7:1.2.3.4
4766
+ (?:${v6segment}:){1}(?:(?::${v6segment}){0,4}:${v4}|(?::${v6segment}){1,6}|:)| // 1:: 1::3:4:5:6:7:8 1::8 1::3:4:5:6:7:1.2.3.4
4767
+ (?::(?:(?::${v6segment}){0,5}:${v4}|(?::${v6segment}){1,7}|:)) // ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::1.2.3.4
4768
+ )(?:%[0-9a-zA-Z]{1,})? // %eth0 %1
4769
+ `
4770
+ .replace(/\s*\/\/.*$/gm, "")
4771
+ .replace(/\n/g, "")
4772
+ .trim();
4773
+
4774
+ // Pre-compile only the exact regexes because adding a global flag make regexes stateful
4775
+ const v46Exact = new RegExp(`(?:^${v4}$)|(?:^${v6}$)`);
4776
+ const v4exact = new RegExp(`^${v4}$`);
4777
+ const v6exact = new RegExp(`^${v6}$`);
4778
+
4779
+ const ipRegex = (options) =>
4780
+ options && options.exact
4781
+ ? v46Exact
4782
+ : new RegExp(
4783
+ `(?:${boundry(options)}${v4}${boundry(options)})|(?:${boundry(options)}${v6}${boundry(options)})`,
4784
+ "g"
4785
+ );
4786
+
4787
+ ipRegex.v4 = (options) =>
4788
+ options && options.exact ? v4exact : new RegExp(`${boundry(options)}${v4}${boundry(options)}`, "g");
4789
+ ipRegex.v6 = (options) =>
4790
+ options && options.exact ? v6exact : new RegExp(`${boundry(options)}${v6}${boundry(options)}`, "g");
4791
+
4792
+ const logger$4 = new Logger();
4793
+
4794
+ const ICE_PUBLIC_IP_GATHERING_TIMEOUT = 3 * 1000;
4140
4795
  const CAMERA_STREAM_ID = RtcStream.getCameraId();
4141
4796
  const browserName$1 = adapter.browserDetails.browser;
4142
4797
 
@@ -4147,7 +4802,7 @@ class P2pRtcManager extends BaseRtcManager {
4147
4802
  let session = this._getSession(clientId);
4148
4803
  let bandwidth = (session && session.bandwidth) || 0;
4149
4804
  if (session) {
4150
- this._logger.warn("Replacing peer session", clientId);
4805
+ logger$4.warn("Replacing peer session", clientId);
4151
4806
  this._cleanup(clientId);
4152
4807
  } else {
4153
4808
  bandwidth = this._changeBandwidthForAllClients(true);
@@ -4168,6 +4823,14 @@ class P2pRtcManager extends BaseRtcManager {
4168
4823
  // clean up some helpers.
4169
4824
  session.wasEverConnected = false;
4170
4825
  session.relayCandidateSeen = false;
4826
+ session.serverReflexiveCandidateSeen = false;
4827
+ session.publicHostCandidateSeen = false;
4828
+ session.ipv6HostCandidateSeen = false;
4829
+ this.ipv6HostCandidateTeredoSeen = false;
4830
+ this.ipv6HostCandidate6to4Seen = false;
4831
+ this.mdnsHostCandidateSeen = false;
4832
+
4833
+ this._emit(rtcManagerEvents.ICE_RESTART);
4171
4834
 
4172
4835
  this._negotiatePeerConnection(
4173
4836
  clientId,
@@ -4222,13 +4885,13 @@ class P2pRtcManager extends BaseRtcManager {
4222
4885
  videoTransceiver.setCodecPreferences(capabilities.codecs);
4223
4886
  });
4224
4887
  } catch (error) {
4225
- this._logger.error("Error during setting setCodecPreferences:", error);
4888
+ logger$4.error("Error during setting setCodecPreferences:", error);
4226
4889
  }
4227
4890
  }
4228
4891
 
4229
4892
  _negotiatePeerConnection(clientId, session, constraints) {
4230
4893
  if (!session) {
4231
- this._logger.warn("No RTCPeerConnection in negotiatePeerConnection()", clientId);
4894
+ logger$4.warn("No RTCPeerConnection in negotiatePeerConnection()", clientId);
4232
4895
  return;
4233
4896
  }
4234
4897
  const pc = session.pc;
@@ -4270,11 +4933,24 @@ class P2pRtcManager extends BaseRtcManager {
4270
4933
  return;
4271
4934
  }
4272
4935
  pc.setLocalDescription(offer).catch((e) => {
4273
- this._logger.warn("RTCPeerConnection.setLocalDescription() failed with local offer", e);
4936
+ logger$4.warn("RTCPeerConnection.setLocalDescription() failed with local offer", e);
4937
+
4938
+ // we failed to create a valid offer so try having the other side create it, without looping
4939
+ if (this._features.reverseOfferOnFailure) {
4940
+ if (!this._lastReverseDirectionAttemptByClientId)
4941
+ this._lastReverseDirectionAttemptByClientId = {};
4942
+ if (
4943
+ !this._lastReverseDirectionAttemptByClientId[clientId] ||
4944
+ this._lastReverseDirectionAttemptByClientId[clientId] < Date.now() - 10000
4945
+ ) {
4946
+ this.acceptNewStream({ clientId, streamId: clientId, shouldAddLocalVideo: true });
4947
+ this._lastReverseDirectionAttemptByClientId[clientId] = Date.now();
4948
+ }
4949
+ }
4274
4950
  });
4275
4951
  })
4276
4952
  .catch((e) => {
4277
- this._logger.warn("RTCPeerConnection.createOffer() failed to create local offer", e);
4953
+ logger$4.warn("RTCPeerConnection.createOffer() failed to create local offer", e);
4278
4954
  });
4279
4955
  }
4280
4956
 
@@ -4310,7 +4986,7 @@ class P2pRtcManager extends BaseRtcManager {
4310
4986
  let bandwidth = this._features.bandwidth
4311
4987
  ? parseInt(this._features.bandwidth, 10)
4312
4988
  : {
4313
- 1: this._features.cap2pBitrate ? 1000 : 0,
4989
+ 1: 0,
4314
4990
  2: this._features.highP2PBandwidth ? 768 : 384,
4315
4991
  3: this._features.highP2PBandwidth ? 512 : 256,
4316
4992
  4: 192,
@@ -4376,9 +5052,73 @@ class P2pRtcManager extends BaseRtcManager {
4376
5052
  pc.addTrack(this._stoppedVideoTrack, localCameraStream);
4377
5053
  }
4378
5054
 
5055
+ pc.onicegatheringstatechange = (event) => {
5056
+ const connection = event.target;
5057
+
5058
+ switch (connection.iceGatheringState) {
5059
+ case "gathering":
5060
+ if (this.icePublicIPGatheringTimeoutID) clearTimeout(this.icePublicIPGatheringTimeoutID);
5061
+ this.icePublicIPGatheringTimeoutID = setTimeout(() => {
5062
+ if (
5063
+ !session.publicHostCandidateSeen &&
5064
+ !session.relayCandidateSeen &&
5065
+ !session.serverReflexiveCandidateSeen
5066
+ ) {
5067
+ this._emit(rtcManagerEvents.ICE_NO_PUBLIC_IP_GATHERED_3SEC);
5068
+ }
5069
+ }, ICE_PUBLIC_IP_GATHERING_TIMEOUT);
5070
+ break;
5071
+ case "complete":
5072
+ if (this.icePublicIPGatheringTimeoutID) clearTimeout(this.icePublicIPGatheringTimeoutID);
5073
+ this.icePublicIPGatheringTimeoutID = undefined;
5074
+ break;
5075
+ }
5076
+ };
5077
+
4379
5078
  pc.onicecandidate = (event) => {
4380
5079
  if (event.candidate) {
4381
- session.relayCandidateSeen = session.relayCandidateSeen || event.candidate.type === "relay";
5080
+ switch (event.candidate?.type) {
5081
+ case "host":
5082
+ const address = event?.candidate?.address;
5083
+ try {
5084
+ if (ipRegex.v4({ exact: true }).test(address)) {
5085
+ const ipv4 = checkIp(address);
5086
+ if (ipv4.isPublicIp) session.publicHostCandidateSeen = true;
5087
+ } else if (ipRegex.v6({ exact: true }).test(address.replace(/^\[(.*)\]/, "$1"))) {
5088
+ const ipv6 = new Address6(address.replace(/^\[(.*)\]/, "$1"));
5089
+ session.ipv6HostCandidateSeen = true;
5090
+
5091
+ if (ipv6.getScope() === "Global") {
5092
+ session.publicHostCandidateSeen = true;
5093
+ }
5094
+ if (ipv6.isTeredo()) {
5095
+ session.ipv6HostCandidateTeredoSeen = true;
5096
+ }
5097
+ if (ipv6.is6to4()) {
5098
+ session.ipv6HostCandidate6to4Seen = true;
5099
+ }
5100
+ } else {
5101
+ const uuidv4 = address.replace(/.local/, "");
5102
+ if (uuidv4 && validate(uuidv4, 4)) {
5103
+ session.mdnsHostCandidateSeen = true;
5104
+ }
5105
+ }
5106
+ } catch (error) {
5107
+ logger$4.info("Error during parsing candidates! Error: ", { error });
5108
+ }
5109
+ break;
5110
+ case "srflx":
5111
+ if (!session.serverReflexiveCandidateSeen) {
5112
+ session.serverReflexiveCandidateSeen = true;
5113
+ }
5114
+ break;
5115
+ case "relay":
5116
+ case "relayed":
5117
+ if (!session.relayCandidateSeen) {
5118
+ session.relayCandidateSeen = true;
5119
+ }
5120
+ break;
5121
+ }
4382
5122
  this._emitServerEvent(RELAY_MESSAGES.ICE_CANDIDATE, {
4383
5123
  receiverId: clientId,
4384
5124
  message: event.candidate,
@@ -4387,6 +5127,20 @@ class P2pRtcManager extends BaseRtcManager {
4387
5127
  this._emitServerEvent(RELAY_MESSAGES.ICE_END_OF_CANDIDATES, {
4388
5128
  receiverId: clientId,
4389
5129
  });
5130
+ if (
5131
+ !session.publicHostCandidateSeen &&
5132
+ !session.relayCandidateSeen &&
5133
+ !session.serverReflexiveCandidateSeen
5134
+ ) {
5135
+ this._emit(rtcManagerEvents.ICE_NO_PUBLIC_IP_GATHERED);
5136
+ }
5137
+ if (session.ipv6HostCandidateSeen) {
5138
+ this._emit(rtcManagerEvents.ICE_IPV6_SEEN, {
5139
+ teredoSeen: session.ipv6HostCandidateTeredoSeen,
5140
+ sixtofourSeen: session.ipv6HostCandidate6to4Seen,
5141
+ });
5142
+ }
5143
+ if (session.mdnsHostCandidateSeen) this._emit(rtcManagerEvents.ICE_MDNS_SEEN);
4390
5144
  }
4391
5145
  };
4392
5146
 
@@ -4413,7 +5167,7 @@ class P2pRtcManager extends BaseRtcManager {
4413
5167
  if (session) {
4414
5168
  // this will happen on a signal-server reconnect
4415
5169
  // before we tried an ice-restart here, now we recreate the session/pc
4416
- this._logger.warn("Replacing peer session", clientId);
5170
+ logger$4.warn("Replacing peer session", clientId);
4417
5171
  this._cleanup(clientId); // will cleanup and delete session/pc
4418
5172
  } else {
4419
5173
  // we adjust bandwidth based on number of sessions/pcs
@@ -4511,14 +5265,20 @@ class P2pRtcManager extends BaseRtcManager {
4511
5265
  streamId,
4512
5266
  hasAudioTrack: !!stream.getAudioTracks().length,
4513
5267
  });
5268
+ this._wasScreenSharing = true;
4514
5269
  this._addStreamToPeerConnections(stream);
4515
5270
  }
4516
5271
 
4517
5272
  removeStream(streamId, stream, requestedByClientId) {
4518
5273
  super.removeStream(streamId, stream);
4519
5274
  this._removeStreamFromPeerConnections(stream);
5275
+ this._wasScreenSharing = false;
4520
5276
  this._emitServerEvent(PROTOCOL_REQUESTS.STOP_SCREENSHARE, { streamId, requestedByClientId });
4521
5277
  }
5278
+
5279
+ hasClient(clientId) {
5280
+ return Object.keys(this.peerConnections).includes(clientId);
5281
+ }
4522
5282
  }
4523
5283
 
4524
5284
  class SfuV2Parser {
@@ -4643,12 +5403,13 @@ class SfuV2Parser {
4643
5403
  }
4644
5404
  }
4645
5405
 
4646
- class VegaConnection extends EventEmitter {
4647
- constructor(wsUrl, logger, protocol = "whereby-sfu#v4") {
5406
+ const logger$3 = new Logger();
5407
+
5408
+ class VegaConnection extends EventEmitter$1 {
5409
+ constructor(wsUrl, protocol = "whereby-sfu#v4") {
4648
5410
  super();
4649
5411
 
4650
5412
  this.wsUrl = wsUrl;
4651
- this.logger = logger;
4652
5413
  this.protocol = protocol;
4653
5414
 
4654
5415
  // This is the map of sent requests that are waiting for a response
@@ -4681,7 +5442,7 @@ class VegaConnection extends EventEmitter {
4681
5442
  }
4682
5443
 
4683
5444
  _onOpen() {
4684
- this.logger.log("VegaConnectionManager: Connected");
5445
+ logger$3.info("Connected");
4685
5446
 
4686
5447
  this.emit("open");
4687
5448
  }
@@ -4689,7 +5450,7 @@ class VegaConnection extends EventEmitter {
4689
5450
  _onMessage(event) {
4690
5451
  const socketMessage = SfuV2Parser.parse(event.data);
4691
5452
 
4692
- this.logger.log("VegaConnectionManager: Received message", socketMessage);
5453
+ logger$3.info("Received message", socketMessage);
4693
5454
 
4694
5455
  if (socketMessage?.response) {
4695
5456
  this._handleResponse(socketMessage);
@@ -4699,13 +5460,13 @@ class VegaConnection extends EventEmitter {
4699
5460
  }
4700
5461
 
4701
5462
  _onClose() {
4702
- this.logger.log("VegaConnectionManager: Disconnected");
5463
+ logger$3.info("Disconnected");
4703
5464
 
4704
5465
  this._tearDown();
4705
5466
  }
4706
5467
 
4707
5468
  _onError(error) {
4708
- this.logger.log("VegaConnectionManager: Error", error);
5469
+ logger$3.info("Error", error);
4709
5470
  }
4710
5471
 
4711
5472
  _handleResponse(socketMessage) {
@@ -5061,6 +5822,8 @@ const defaultParams = {
5061
5822
  outFormula: "score", // the out/score sent to SFU
5062
5823
  };
5063
5824
 
5825
+ const logger$2 = new Logger();
5826
+
5064
5827
  function createMicAnalyser({ micTrack, params, onScoreUpdated }) {
5065
5828
  // todo: might need to reuse existing in PWA
5066
5829
  const audioCtx = new AudioContext();
@@ -5086,7 +5849,7 @@ function createMicAnalyser({ micTrack, params, onScoreUpdated }) {
5086
5849
  track = stream.getAudioTracks()[0];
5087
5850
  lastTrackWasOurs = true;
5088
5851
  } catch (ex) {
5089
- console.warn("unable to fetch new track for colocation speaker analysis, using current", ex);
5852
+ logger$2.warn("unable to fetch new track for colocation speaker analysis, using current", ex);
5090
5853
  }
5091
5854
  }
5092
5855
 
@@ -5218,18 +5981,207 @@ const maybeTurnOnly = (transportConfig, features) => {
5218
5981
  }
5219
5982
  };
5220
5983
 
5984
+ const logger$1 = new Logger();
5985
+
5986
+ const MEDIA_QUALITY = Object.freeze({
5987
+ ok: "ok",
5988
+ warning: "warning",
5989
+ critical: "critical",
5990
+ });
5991
+
5992
+ const MONITOR_INTERVAL = 600; // ms
5993
+ const TREND_HORIZON = 3; // number of monitor intervals needed for quality to change
5994
+ const WARNING_SCORE = 9;
5995
+ const CRITICAL_SCORE = 7;
5996
+
5997
+ class VegaMediaQualityMonitor extends EventEmitter {
5998
+ constructor() {
5999
+ super();
6000
+ this._clients = {};
6001
+ this._producers = {};
6002
+ this._startMonitor();
6003
+ }
6004
+
6005
+ close() {
6006
+ clearInterval(this._intervalHandle);
6007
+ delete this._intervalHandle;
6008
+ this._producers = {};
6009
+ this._clients = {};
6010
+ }
6011
+
6012
+ _startMonitor() {
6013
+ this._intervalHandle = setInterval(() => {
6014
+ Object.entries(this._producers).forEach(([clientId, producers]) => {
6015
+ this._evaluateClient(clientId, producers);
6016
+ });
6017
+ }, MONITOR_INTERVAL);
6018
+ }
6019
+
6020
+ _evaluateClient(clientId, producers) {
6021
+ if (!this._clients[clientId]) {
6022
+ this._clients[clientId] = {
6023
+ audio: { currentQuality: MEDIA_QUALITY.ok, trend: [] },
6024
+ video: { currentQuality: MEDIA_QUALITY.ok, trend: [] },
6025
+ };
6026
+ }
6027
+
6028
+ this._evaluateProducer(
6029
+ clientId,
6030
+ Object.values(producers).filter((p) => p.kind === "audio"),
6031
+ "audio"
6032
+ );
6033
+ this._evaluateProducer(
6034
+ clientId,
6035
+ Object.values(producers).filter((p) => p.kind === "video"),
6036
+ "video"
6037
+ );
6038
+ }
6039
+
6040
+ _evaluateProducer(clientId, producers, kind) {
6041
+ if (producers.length === 0) {
6042
+ return;
6043
+ }
6044
+
6045
+ const avgScore = producers.reduce((prev, curr) => prev + curr.score, 0) / producers.length;
6046
+ const newQuality = this._evaluateScore(avgScore);
6047
+ const qualityChanged = this._updateTrend(newQuality, this._clients[clientId][kind]);
6048
+ if (qualityChanged) {
6049
+ this.emit(PROTOCOL_EVENTS.MEDIA_QUALITY_CHANGED, {
6050
+ clientId,
6051
+ kind,
6052
+ quality: newQuality,
6053
+ });
6054
+ }
6055
+ }
6056
+
6057
+ _updateTrend(newQuality, state) {
6058
+ state.trend.push(newQuality);
6059
+ if (state.trend.length > TREND_HORIZON) {
6060
+ state.trend.shift();
6061
+ }
6062
+
6063
+ if (newQuality !== state.currentQuality && state.trend.every((t) => t !== state.currentQuality)) {
6064
+ state.currentQuality = newQuality;
6065
+ return true;
6066
+ } else {
6067
+ return false;
6068
+ }
6069
+ }
6070
+
6071
+ addProducer(clientId, producerId) {
6072
+ if (!clientId || !producerId || !(typeof clientId === "string" && typeof producerId === "string")) {
6073
+ logger$1.warn("Missing clientId or producerId");
6074
+ return;
6075
+ }
6076
+
6077
+ if (!this._producers[clientId]) {
6078
+ this._producers[clientId] = {};
6079
+ }
6080
+
6081
+ this._producers[clientId][producerId] = {};
6082
+ }
6083
+
6084
+ removeProducer(clientId, producerId) {
6085
+ delete this._producers[clientId][producerId];
6086
+
6087
+ if (Object.keys(this._producers[clientId]).length === 0) {
6088
+ delete this._producers[clientId];
6089
+ }
6090
+ }
6091
+
6092
+ addConsumer(clientId, consumerId) {
6093
+ if (!clientId || !consumerId) {
6094
+ logger$1.warn("Missing clientId or consumerId");
6095
+ return;
6096
+ }
6097
+
6098
+ if (!this._producers[clientId]) {
6099
+ this._producers[clientId] = {};
6100
+ }
6101
+
6102
+ this._producers[clientId][consumerId] = {};
6103
+ }
6104
+
6105
+ removeConsumer(clientId, consumerId) {
6106
+ delete this._producers[clientId][consumerId];
6107
+
6108
+ if (Object.keys(this._producers[clientId]).length === 0) {
6109
+ delete this._producers[clientId];
6110
+ }
6111
+ }
6112
+
6113
+ addProducerScore(clientId, producerId, kind, score) {
6114
+ if (
6115
+ !Array.isArray(score) ||
6116
+ score.length === 0 ||
6117
+ score.some((s) => !s || !s.hasOwnProperty("score") || typeof s.score !== "number" || isNaN(s.score))
6118
+ ) {
6119
+ logger$1.warn("Unexpected producer score format");
6120
+ return;
6121
+ }
6122
+ this._producers[clientId][producerId] = { kind, score: this._calcAvgProducerScore(score.map((s) => s.score)) };
6123
+ }
6124
+
6125
+ addConsumerScore(clientId, consumerId, kind, score) {
6126
+ if (!score || !score.hasOwnProperty("producerScores") || !Array.isArray(score.producerScores)) {
6127
+ logger$1.warn("Unexpected consumer score format");
6128
+ return;
6129
+ }
6130
+ this._producers[clientId][consumerId] = { kind, score: this._calcAvgProducerScore(score.producerScores) };
6131
+ }
6132
+
6133
+ _evaluateScore(score) {
6134
+ if (score <= WARNING_SCORE && score > CRITICAL_SCORE) {
6135
+ return MEDIA_QUALITY.warning;
6136
+ } else if (score <= CRITICAL_SCORE && score > 0) {
6137
+ return MEDIA_QUALITY.critical;
6138
+ } else {
6139
+ return MEDIA_QUALITY.ok;
6140
+ }
6141
+ }
6142
+
6143
+ _calcAvgProducerScore(scores) {
6144
+ try {
6145
+ if (!Array.isArray(scores) || scores.length === 0) {
6146
+ return 0;
6147
+ }
6148
+
6149
+ let totalScore = 0;
6150
+ let divisor = 0;
6151
+
6152
+ scores.forEach((score) => {
6153
+ if (score > 0) {
6154
+ totalScore += score;
6155
+ divisor++;
6156
+ }
6157
+ });
6158
+
6159
+ if (totalScore === 0 || divisor === 0) {
6160
+ return 0;
6161
+ } else {
6162
+ return totalScore / divisor;
6163
+ }
6164
+ } catch (error) {
6165
+ logger$1.error(error);
6166
+ return 0;
6167
+ }
6168
+ }
6169
+ }
6170
+
6171
+ const logger = new Logger();
6172
+
5221
6173
  const browserName = adapter.browserDetails.browser;
5222
6174
  let unloading = false;
5223
6175
 
5224
6176
  const RESTARTICE_ERROR_RETRY_THRESHOLD_IN_MS = 3500;
5225
6177
  const RESTARTICE_ERROR_MAX_RETRY_COUNT = 5;
5226
- const OUTBOUND_CAM_OUTBOUND_STREAM_ID = v4();
5227
- const OUTBOUND_SCREEN_OUTBOUND_STREAM_ID = v4();
6178
+ const OUTBOUND_CAM_OUTBOUND_STREAM_ID = v4$1();
6179
+ const OUTBOUND_SCREEN_OUTBOUND_STREAM_ID = v4$1();
5228
6180
 
5229
6181
  if (browserName === "chrome") window.document.addEventListener("beforeunload", () => (unloading = true));
5230
6182
 
5231
6183
  class VegaRtcManager {
5232
- constructor({ selfId, room, emitter, serverSocket, webrtcProvider, features, eventClaim, logger = console }) {
6184
+ constructor({ selfId, room, emitter, serverSocket, webrtcProvider, features, eventClaim, deviceHandlerFactory }) {
5233
6185
  assert$1.ok(selfId, "selfId is required");
5234
6186
  assert$1.ok(room, "room is required");
5235
6187
  assert$1.ok(emitter && emitter.emit, "emitter is required");
@@ -5246,14 +6198,18 @@ class VegaRtcManager {
5246
6198
  this._webrtcProvider = webrtcProvider;
5247
6199
  this._features = features || {};
5248
6200
  this._eventClaim = eventClaim;
5249
- this._logger = logger;
5250
6201
 
5251
6202
  this._vegaConnection = null;
5252
6203
 
5253
6204
  this._micAnalyser = null;
5254
6205
  this._micAnalyserDebugger = null;
5255
6206
 
5256
- this._mediasoupDevice = new Device({ handlerName: getHandler() });
6207
+ if (deviceHandlerFactory) {
6208
+ this._mediasoupDevice = new Device({ handlerFactory: deviceHandlerFactory });
6209
+ } else {
6210
+ this._mediasoupDevice = new Device({ handlerName: getHandler() });
6211
+ }
6212
+
5257
6213
  this._routerRtpCapabilities = null;
5258
6214
 
5259
6215
  this._sendTransport = null;
@@ -5316,6 +6272,11 @@ class VegaRtcManager {
5316
6272
  // Retry if connection closed until disconnectAll called;
5317
6273
  this._reconnect = true;
5318
6274
  this._reconnectTimeOut = null;
6275
+
6276
+ this._qualityMonitor = new VegaMediaQualityMonitor();
6277
+ this._qualityMonitor.on(PROTOCOL_EVENTS.MEDIA_QUALITY_CHANGED, (payload) => {
6278
+ this._emitToPWA(PROTOCOL_EVENTS.MEDIA_QUALITY_CHANGED, payload);
6279
+ });
5319
6280
  }
5320
6281
 
5321
6282
  _updateAndScheduleMediaServersRefresh({ iceServers, sfuServer, mediaserverConfigTtlSeconds }) {
@@ -5348,7 +6309,7 @@ class VegaRtcManager {
5348
6309
 
5349
6310
  this._serverSocket.on(PROTOCOL_RESPONSES.MEDIASERVER_CONFIG, (data) => {
5350
6311
  if (data.error) {
5351
- this._logger.warn("FETCH_MEDIASERVER_CONFIG failed:", data.error);
6312
+ logger.warn("FETCH_MEDIASERVER_CONFIG failed:", data.error);
5352
6313
  return;
5353
6314
  }
5354
6315
  this._updateAndScheduleMediaServersRefresh(data);
@@ -5380,14 +6341,14 @@ class VegaRtcManager {
5380
6341
  const queryString = searchParams.toString();
5381
6342
  const wsUrl = `wss://${host}?${queryString}`;
5382
6343
 
5383
- this._vegaConnection = new VegaConnection(wsUrl, this._logger);
6344
+ this._vegaConnection = new VegaConnection(wsUrl);
5384
6345
  this._vegaConnection.on("open", () => this._join());
5385
6346
  this._vegaConnection.on("close", () => this._onClose());
5386
6347
  this._vegaConnection.on("message", (message) => this._onMessage(message));
5387
6348
  }
5388
6349
 
5389
6350
  _onClose() {
5390
- this._logger.debug("_onClose()");
6351
+ logger.info("_onClose()");
5391
6352
 
5392
6353
  // These will clean up any and all producers/consumers
5393
6354
  this._sendTransport?.close();
@@ -5400,10 +6361,14 @@ class VegaRtcManager {
5400
6361
  if (this._reconnect) {
5401
6362
  this._reconnectTimeOut = setTimeout(() => this._connect(), 1000);
5402
6363
  }
6364
+
6365
+ this._qualityMonitor.close();
6366
+ this._emitToPWA(rtcManagerEvents.SFU_CONNECTION_CLOSED);
5403
6367
  }
5404
6368
 
5405
6369
  async _join() {
5406
- this._logger.debug("join()");
6370
+ logger.info("join()");
6371
+ this._emitToPWA(rtcManagerEvents.SFU_CONNECTION_OPEN);
5407
6372
 
5408
6373
  try {
5409
6374
  // We need to always do this, as this triggers the join logic on the SFU
@@ -5437,7 +6402,7 @@ class VegaRtcManager {
5437
6402
 
5438
6403
  await Promise.all(mediaPromises);
5439
6404
  } catch (error) {
5440
- this._logger.error("_join() [error:%o]", error);
6405
+ logger.error("_join() [error:%o]", error);
5441
6406
  // TODO: handle this error, rejoin?
5442
6407
  }
5443
6408
  }
@@ -5468,7 +6433,7 @@ class VegaRtcManager {
5468
6433
 
5469
6434
  const transport = this._mediasoupDevice[creator](transportOptions);
5470
6435
  const onConnectionStateListener = async (connectionState) => {
5471
- this._logger.debug(`Transport ConnectionStateChanged ${connectionState}`);
6436
+ logger.info(`Transport ConnectionStateChanged ${connectionState}`);
5472
6437
  if (connectionState !== "disconnected" && connectionState !== "failed") {
5473
6438
  return;
5474
6439
  }
@@ -5542,17 +6507,17 @@ class VegaRtcManager {
5542
6507
 
5543
6508
  async _restartIce(transport, retried = 0) {
5544
6509
  if (!transport || !("closed" in transport) || !("connectionState" in transport)) {
5545
- this._logger.debug("_restartIce: No transport or property closed or connectionState!");
6510
+ logger.info("_restartIce: No transport or property closed or connectionState!");
5546
6511
  return;
5547
6512
  }
5548
6513
 
5549
6514
  if (transport.closed) {
5550
- this._logger.debug("_restartIce: Transport is closed!");
6515
+ logger.info("_restartIce: Transport is closed!");
5551
6516
  return;
5552
6517
  }
5553
6518
 
5554
6519
  if (transport.connectionState !== "disconnected" && transport.connectionState !== "failed") {
5555
- this._logger.debug("_restartIce: Connection is healthy ICE restart no loneger needed!");
6520
+ logger.info("_restartIce: Connection is healthy ICE restart no loneger needed!");
5556
6521
  return;
5557
6522
  }
5558
6523
 
@@ -5565,24 +6530,24 @@ class VegaRtcManager {
5565
6530
  transport.appData.iceRestartStarted = now;
5566
6531
 
5567
6532
  if (RESTARTICE_ERROR_MAX_RETRY_COUNT <= retried) {
5568
- this._logger.debug("_restartIce: Reached restart ICE maximum retry count!");
6533
+ logger.info("_restartIce: Reached restart ICE maximum retry count!");
5569
6534
  return;
5570
6535
  }
5571
6536
 
5572
6537
  if (!this._vegaConnection) {
5573
- this._logger.debug(`_restartIce: Connection is undefined`);
6538
+ logger.info(`_restartIce: Connection is undefined`);
5574
6539
  return;
5575
6540
  }
5576
6541
  const { iceParameters } = await this._vegaConnection.request("restartIce", { transportId: transport.id });
5577
6542
 
5578
- this._logger.debug("_restartIce: ICE restart iceParameters received from SFU: ", iceParameters);
6543
+ logger.info("_restartIce: ICE restart iceParameters received from SFU: ", iceParameters);
5579
6544
  const error = await transport
5580
6545
  .restartIce({ iceParameters })
5581
6546
  .then(() => null)
5582
6547
  .catch((err) => err);
5583
6548
 
5584
6549
  if (error) {
5585
- this._logger.error(`_restartIce: ICE restart failed: ${error}`);
6550
+ logger.error(`_restartIce: ICE restart failed: ${error}`);
5586
6551
  switch (error.message) {
5587
6552
  case "missing transportId":
5588
6553
  case "no such transport":
@@ -5618,7 +6583,7 @@ class VegaRtcManager {
5618
6583
  }
5619
6584
 
5620
6585
  async _internalSendMic() {
5621
- this._logger.debug("_internalSendMic()");
6586
+ logger.info("_internalSendMic()");
5622
6587
 
5623
6588
  this._micProducerPromise = (async () => {
5624
6589
  try {
@@ -5647,21 +6612,23 @@ class VegaRtcManager {
5647
6612
  currentPaused ? producer.pause() : producer.resume();
5648
6613
 
5649
6614
  this._micProducer = producer;
6615
+ this._qualityMonitor.addProducer(this._selfId, producer.id);
5650
6616
 
5651
6617
  producer.observer.once("close", () => {
5652
- this._logger.debug('micProducer "close" event');
6618
+ logger.info('micProducer "close" event');
5653
6619
 
5654
6620
  if (producer.appData.localClosed)
5655
6621
  this._vegaConnection?.message("closeProducers", { producerIds: [producer.id] });
5656
6622
 
5657
6623
  this._micProducer = null;
5658
6624
  this._micProducerPromise = null;
6625
+ this._qualityMonitor.removeProducer(this._selfId, producer.id);
5659
6626
  });
5660
6627
 
5661
6628
  if (this._micTrack !== this._micProducer.track) await this._replaceMicTrack();
5662
6629
  if (this._micPaused !== this._micProducer.paused) this._pauseResumeMic();
5663
6630
  } catch (error) {
5664
- this._logger.error("micProducer failed:%o", error);
6631
+ logger.error("micProducer failed:%o", error);
5665
6632
  } finally {
5666
6633
  this._micProducerPromise = null;
5667
6634
 
@@ -5675,7 +6642,7 @@ class VegaRtcManager {
5675
6642
  }
5676
6643
 
5677
6644
  async _internalSetupMicScore() {
5678
- this._logger.debug("_internalSetupMicScore()");
6645
+ logger.info("_internalSetupMicScore()");
5679
6646
 
5680
6647
  this._micScoreProducerPromise = (async () => {
5681
6648
  try {
@@ -5698,7 +6665,7 @@ class VegaRtcManager {
5698
6665
  this._micScoreProducer = producer;
5699
6666
 
5700
6667
  producer.observer.once("close", () => {
5701
- this._logger.debug('micScoreProducer "close" event');
6668
+ logger.info('micScoreProducer "close" event');
5702
6669
  if (producer.appData.localClosed) {
5703
6670
  this._vegaConnection?.message("closeDataProducers", { dataProducerIds: [producer.id] });
5704
6671
  }
@@ -5707,7 +6674,7 @@ class VegaRtcManager {
5707
6674
  this._micScoreProducerPromise = null;
5708
6675
  });
5709
6676
  } catch (error) {
5710
- this._logger.error("micScoreProducer failed:%o", error);
6677
+ logger.error("micScoreProducer failed:%o", error);
5711
6678
  } finally {
5712
6679
  this._micScoreProducerPromise = null;
5713
6680
 
@@ -5725,7 +6692,7 @@ class VegaRtcManager {
5725
6692
  }
5726
6693
 
5727
6694
  async _replaceMicTrack() {
5728
- this._logger.debug("_replaceMicTrack()");
6695
+ logger.info("_replaceMicTrack()");
5729
6696
 
5730
6697
  if (!this._micTrack || !this._micProducer || this._micProducer.closed) return;
5731
6698
 
@@ -5741,7 +6708,7 @@ class VegaRtcManager {
5741
6708
  }
5742
6709
 
5743
6710
  _pauseResumeMic() {
5744
- this._logger.debug("_pauseResumeMic()");
6711
+ logger.info("_pauseResumeMic()");
5745
6712
 
5746
6713
  if (!this._micProducer || this._micProducer.closed) return;
5747
6714
 
@@ -5768,7 +6735,7 @@ class VegaRtcManager {
5768
6735
  }
5769
6736
 
5770
6737
  async _sendMic(track) {
5771
- this._logger.debug("_sendMic() [track:%o]", track);
6738
+ logger.info("_sendMic() [track:%o]", track);
5772
6739
 
5773
6740
  this._micTrack = track;
5774
6741
 
@@ -5800,7 +6767,7 @@ class VegaRtcManager {
5800
6767
  try {
5801
6768
  this._micScoreProducer.send(JSON.stringify({ score }));
5802
6769
  } catch (ex) {
5803
- console.error("_sendMicScore failed [error:%o]", ex);
6770
+ logger.error("_sendMicScore failed [error:%o]", ex);
5804
6771
  }
5805
6772
  return;
5806
6773
  }
@@ -5812,7 +6779,7 @@ class VegaRtcManager {
5812
6779
  }
5813
6780
 
5814
6781
  async _internalSendWebcam() {
5815
- this._logger.debug("_internalSendWebcam()");
6782
+ logger.info("_internalSendWebcam()");
5816
6783
 
5817
6784
  this._webcamProducerPromise = (async () => {
5818
6785
  try {
@@ -5841,21 +6808,23 @@ class VegaRtcManager {
5841
6808
  currentPaused ? producer.pause() : producer.resume();
5842
6809
 
5843
6810
  this._webcamProducer = producer;
6811
+ this._qualityMonitor.addProducer(this._selfId, producer.id);
5844
6812
  producer.observer.once("close", () => {
5845
- this._logger.debug('webcamProducer "close" event');
6813
+ logger.info('webcamProducer "close" event');
5846
6814
 
5847
6815
  if (producer.appData.localClosed)
5848
6816
  this._vegaConnection?.message("closeProducers", { producerIds: [producer.id] });
5849
6817
 
5850
6818
  this._webcamProducer = null;
5851
6819
  this._webcamProducerPromise = null;
6820
+ this._qualityMonitor.removeProducer(this._selfId, producer.id);
5852
6821
  });
5853
6822
 
5854
6823
  // Has someone replaced the track?
5855
6824
  if (this._webcamTrack !== this._webcamProducer.track) await this._replaceWebcamTrack();
5856
6825
  if (this._webcamPaused !== this._webcamProducer.paused) this._pauseResumeWebcam();
5857
6826
  } catch (error) {
5858
- this._logger.error("webcamProducer failed:%o", error);
6827
+ logger.error("webcamProducer failed:%o", error);
5859
6828
  } finally {
5860
6829
  this._webcamProducerPromise = null;
5861
6830
 
@@ -5869,7 +6838,7 @@ class VegaRtcManager {
5869
6838
  }
5870
6839
 
5871
6840
  async _replaceWebcamTrack() {
5872
- this._logger.debug("_replaceWebcamTrack()");
6841
+ logger.info("_replaceWebcamTrack()");
5873
6842
 
5874
6843
  if (!this._webcamTrack || !this._webcamProducer || this._webcamProducer.closed) return;
5875
6844
 
@@ -5880,7 +6849,7 @@ class VegaRtcManager {
5880
6849
  }
5881
6850
 
5882
6851
  _pauseResumeWebcam() {
5883
- this._logger.debug("_pauseResumeWebcam()");
6852
+ logger.info("_pauseResumeWebcam()");
5884
6853
 
5885
6854
  if (!this._webcamProducer || this._webcamProducer.closed) return;
5886
6855
 
@@ -5904,7 +6873,7 @@ class VegaRtcManager {
5904
6873
  }
5905
6874
 
5906
6875
  async _sendWebcam(track) {
5907
- this._logger.debug("_sendWebcam() [track:%o]", track);
6876
+ logger.info("_sendWebcam() [track:%o]", track);
5908
6877
 
5909
6878
  this._webcamTrack = track;
5910
6879
 
@@ -5918,7 +6887,7 @@ class VegaRtcManager {
5918
6887
  }
5919
6888
 
5920
6889
  async _internalSendScreenVideo() {
5921
- this._logger.debug("_internalSendScreenVideo()");
6890
+ logger.info("_internalSendScreenVideo()");
5922
6891
 
5923
6892
  this._screenVideoProducerPromise = (async () => {
5924
6893
  try {
@@ -5943,20 +6912,22 @@ class VegaRtcManager {
5943
6912
  });
5944
6913
 
5945
6914
  this._screenVideoProducer = producer;
6915
+ this._qualityMonitor.addProducer(this._selfId, producer.id);
5946
6916
  producer.observer.once("close", () => {
5947
- this._logger.debug('screenVideoProducer "close" event');
6917
+ logger.info('screenVideoProducer "close" event');
5948
6918
 
5949
6919
  if (producer.appData.localClosed)
5950
6920
  this._vegaConnection?.message("closeProducers", { producerIds: [producer.id] });
5951
6921
 
5952
6922
  this._screenVideoProducer = null;
5953
6923
  this._screenVideoProducerPromise = null;
6924
+ this._qualityMonitor.removeProducer(this._selfId, producer.id);
5954
6925
  });
5955
6926
 
5956
6927
  // Has someone replaced the track?
5957
6928
  if (this._screenVideoTrack !== this._screenVideoProducer.track) await this._replaceScreenVideoTrack();
5958
6929
  } catch (error) {
5959
- this._logger.error("screenVideoProducer failed:%o", error);
6930
+ logger.error("screenVideoProducer failed:%o", error);
5960
6931
  } finally {
5961
6932
  this._screenVideoProducerPromise = null;
5962
6933
 
@@ -5970,7 +6941,7 @@ class VegaRtcManager {
5970
6941
  }
5971
6942
 
5972
6943
  async _replaceScreenVideoTrack() {
5973
- this._logger.debug("_replaceScreenVideoTrack()");
6944
+ logger.info("_replaceScreenVideoTrack()");
5974
6945
 
5975
6946
  if (!this._screenVideoTrack || !this._screenVideoProducer || this._screenVideoProducer.closed) return;
5976
6947
 
@@ -5981,7 +6952,7 @@ class VegaRtcManager {
5981
6952
  }
5982
6953
 
5983
6954
  async _sendScreenVideo(track) {
5984
- this._logger.debug("_sendScreenVideo() [track:%o]", track);
6955
+ logger.info("_sendScreenVideo() [track:%o]", track);
5985
6956
 
5986
6957
  this._screenVideoTrack = track;
5987
6958
 
@@ -5995,7 +6966,7 @@ class VegaRtcManager {
5995
6966
  }
5996
6967
 
5997
6968
  async _internalSendScreenAudio() {
5998
- this._logger.debug("_internalSendScreenAudio()");
6969
+ logger.info("_internalSendScreenAudio()");
5999
6970
 
6000
6971
  this._screenAudioProducerPromise = (async () => {
6001
6972
  try {
@@ -6020,20 +6991,22 @@ class VegaRtcManager {
6020
6991
  });
6021
6992
 
6022
6993
  this._screenAudioProducer = producer;
6994
+ this._qualityMonitor.addProducer(this._selfId, producer.id);
6023
6995
  producer.observer.once("close", () => {
6024
- this._logger.debug('screenAudioProducer "close" event');
6996
+ logger.info('screenAudioProducer "close" event');
6025
6997
 
6026
6998
  if (producer.appData.localClosed)
6027
6999
  this._vegaConnection?.message("closeProducers", { producerIds: [producer.id] });
6028
7000
 
6029
7001
  this._screenAudioProducer = null;
6030
7002
  this._screenAudioProducerPromise = null;
7003
+ this._qualityMonitor.removeProducer(this._selfId, producer.id);
6031
7004
  });
6032
7005
 
6033
7006
  // Has someone replaced the track?
6034
7007
  if (this._screenAudioTrack !== this._screenAudioProducer.track) await this._replaceScreenAudioTrack();
6035
7008
  } catch (error) {
6036
- this._logger.error("screenAudioProducer failed:%o", error);
7009
+ logger.error("screenAudioProducer failed:%o", error);
6037
7010
  } finally {
6038
7011
  this._screenAudioProducerPromise = null;
6039
7012
 
@@ -6047,7 +7020,7 @@ class VegaRtcManager {
6047
7020
  }
6048
7021
 
6049
7022
  async _replaceScreenAudioTrack() {
6050
- this._logger.debug("_replaceScreenAudioTrack()");
7023
+ logger.info("_replaceScreenAudioTrack()");
6051
7024
 
6052
7025
  if (!this._screenAudioTrack || !this._screenAudioProducer || this._screenAudioProducer.closed) return;
6053
7026
 
@@ -6058,7 +7031,7 @@ class VegaRtcManager {
6058
7031
  }
6059
7032
 
6060
7033
  async _sendScreenAudio(track) {
6061
- this._logger.debug("_sendScreenAudio() [track:%o]", track);
7034
+ logger.info("_sendScreenAudio() [track:%o]", track);
6062
7035
 
6063
7036
  this._screenAudioTrack = track;
6064
7037
 
@@ -6072,7 +7045,7 @@ class VegaRtcManager {
6072
7045
  }
6073
7046
 
6074
7047
  _stopProducer(producer) {
6075
- this._logger.debug("_stopProducer()");
7048
+ logger.info("_stopProducer()");
6076
7049
 
6077
7050
  if (!producer || producer.closed) return;
6078
7051
 
@@ -6127,6 +7100,18 @@ class VegaRtcManager {
6127
7100
  rtcStats.sendEvent("colocation_changed", { colocation });
6128
7101
  }
6129
7102
 
7103
+ /**
7104
+ * This sends a signal to the SFU to pause all incoming video streams to the client.
7105
+ *
7106
+ * @param {boolean} audioOnly
7107
+ */
7108
+ setAudioOnly(audioOnly) {
7109
+ this._vegaConnection?.message(audioOnly ? "enableAudioOnly" : "disableAudioOnly");
7110
+ }
7111
+
7112
+ // the track ids send by signal server for remote-initiated screenshares
7113
+ setRemoteScreenshareVideoTrackIds(/*remoteScreenshareVideoTrackIds*/) {}
7114
+
6130
7115
  /**
6131
7116
  * The unique identifier for this room session.
6132
7117
  *
@@ -6146,7 +7131,7 @@ class VegaRtcManager {
6146
7131
  * @param {string} eventClaim
6147
7132
  */
6148
7133
  disconnect(clientIdOrStreamId, _activeBreakout, eventClaim) {
6149
- this._logger.debug("disconnect() [clientIdOrStreamId:%s, eventClaim:%s]", clientIdOrStreamId, eventClaim);
7134
+ logger.info("disconnect() [clientIdOrStreamId:%s, eventClaim:%s]", clientIdOrStreamId, eventClaim);
6150
7135
 
6151
7136
  if (this._clientStates.has(clientIdOrStreamId)) {
6152
7137
  // In this case this is a disconnect from an actual client, not just a screen share.
@@ -6204,7 +7189,7 @@ class VegaRtcManager {
6204
7189
  * @param {string} requestedByClientId
6205
7190
  */
6206
7191
  removeStream(streamId, _stream, requestedByClientId) {
6207
- this._logger.debug("removeStream() [streamId:%s, requestedByClientId:%s]", streamId, requestedByClientId);
7192
+ logger.info("removeStream() [streamId:%s, requestedByClientId:%s]", streamId, requestedByClientId);
6208
7193
 
6209
7194
  this._emitToSignal(PROTOCOL_REQUESTS.STOP_SCREENSHARE, {
6210
7195
  streamId: OUTBOUND_SCREEN_OUTBOUND_STREAM_ID,
@@ -6308,7 +7293,7 @@ class VegaRtcManager {
6308
7293
  * @param {boolean} enabled
6309
7294
  */
6310
7295
  stopOrResumeAudio(stream, enabled) {
6311
- this._logger.debug("stopOrResumeAudio() [enabled:%s]", enabled);
7296
+ logger.info("stopOrResumeAudio() [enabled:%s]", enabled);
6312
7297
 
6313
7298
  this._micPaused = !enabled;
6314
7299
 
@@ -6334,7 +7319,7 @@ class VegaRtcManager {
6334
7319
  * @param {boolean} enabled
6335
7320
  */
6336
7321
  stopOrResumeVideo(localStream, enable) {
6337
- this._logger.debug("stopOrResumeVideo() [enable:%s]", enable);
7322
+ logger.info("stopOrResumeVideo() [enable:%s]", enable);
6338
7323
 
6339
7324
  this._webcamPaused = !enable;
6340
7325
 
@@ -6397,7 +7382,7 @@ class VegaRtcManager {
6397
7382
  * }} streamOptions
6398
7383
  */
6399
7384
  acceptNewStream({ streamId, clientId }) {
6400
- this._logger.debug("acceptNewStream()", { streamId, clientId });
7385
+ logger.info("acceptNewStream()", { streamId, clientId });
6401
7386
 
6402
7387
  // make sure we have rtcStats connection
6403
7388
  this.rtcStatsReconnect();
@@ -6428,7 +7413,7 @@ class VegaRtcManager {
6428
7413
  * }} size
6429
7414
  */
6430
7415
  updateStreamResolution(streamId, _ignored, { width, height }) {
6431
- this._logger.debug("updateStreamResolution()", { streamId, width, height });
7416
+ logger.info("updateStreamResolution()", { streamId, width, height });
6432
7417
 
6433
7418
  const consumerId = this._streamIdToVideoConsumerId.get(streamId);
6434
7419
  const consumer = this._consumers.get(consumerId);
@@ -6561,18 +7546,22 @@ class VegaRtcManager {
6561
7546
  return this._onDataConsumerClosed(data);
6562
7547
  case "dominantSpeaker":
6563
7548
  return this._onDominantSpeaker(data);
7549
+ case "consumerScore":
7550
+ return this._onConsumerScore(data);
7551
+ case "producerScore":
7552
+ return this._onProducerScore(data);
6564
7553
  default:
6565
- this._logger.debug(`unknown message method "${method}"`);
7554
+ logger.info(`unknown message method "${method}"`);
6566
7555
  return;
6567
7556
  }
6568
7557
  })
6569
7558
  .catch((error) => {
6570
- console.error('"message" failed [error:%o]', error);
7559
+ logger.error('"message" failed [error:%o]', error);
6571
7560
  });
6572
7561
  }
6573
7562
 
6574
7563
  async _onConsumerReady(options) {
6575
- this._logger.debug("_onConsumerReady()", { id: options.id, producerId: options.producerId });
7564
+ logger.info("_onConsumerReady()", { id: options.id, producerId: options.producerId });
6576
7565
 
6577
7566
  const consumer = await this._receiveTransport.consume(options);
6578
7567
 
@@ -6581,8 +7570,10 @@ class VegaRtcManager {
6581
7570
  consumer.appData.spatialLayer = 2;
6582
7571
 
6583
7572
  this._consumers.set(consumer.id, consumer);
7573
+ this._qualityMonitor.addConsumer(consumer.appData.sourceClientId, consumer.id);
6584
7574
  consumer.observer.once("close", () => {
6585
7575
  this._consumers.delete(consumer.id);
7576
+ this._qualityMonitor.removeConsumer(consumer.appData.sourceClientId, consumer.id);
6586
7577
 
6587
7578
  this._consumerClosedCleanup(consumer);
6588
7579
  });
@@ -6593,7 +7584,7 @@ class VegaRtcManager {
6593
7584
  // Legacy Chrome API
6594
7585
  consumer.rtpReceiver.playoutDelayHint = MEDIA_JITTER_BUFFER_TARGET / 1000; // seconds
6595
7586
  } catch (error) {
6596
- this._logger.error("Error during setting jitter buffer target:", error);
7587
+ logger.error("Error during setting jitter buffer target:", error);
6597
7588
  }
6598
7589
  }
6599
7590
 
@@ -6626,13 +7617,13 @@ class VegaRtcManager {
6626
7617
  }
6627
7618
 
6628
7619
  async _onConsumerClosed({ consumerId, reason }) {
6629
- this._logger.debug("_onConsumerClosed()", { consumerId, reason });
7620
+ logger.info("_onConsumerClosed()", { consumerId, reason });
6630
7621
 
6631
7622
  this._consumers.get(consumerId)?.close();
6632
7623
  }
6633
7624
 
6634
7625
  _onConsumerPaused({ consumerId }) {
6635
- this._logger.debug("_onConsumerPaused()", { consumerId });
7626
+ logger.info("_onConsumerPaused()", { consumerId });
6636
7627
 
6637
7628
  const consumer = this._consumers.get(consumerId);
6638
7629
 
@@ -6643,7 +7634,7 @@ class VegaRtcManager {
6643
7634
  }
6644
7635
 
6645
7636
  _onConsumerResumed({ consumerId }) {
6646
- this._logger.debug("_onConsumerResumed()", { consumerId });
7637
+ logger.info("_onConsumerResumed()", { consumerId });
6647
7638
 
6648
7639
  const consumer = this._consumers.get(consumerId);
6649
7640
 
@@ -6656,8 +7647,30 @@ class VegaRtcManager {
6656
7647
  }
6657
7648
  }
6658
7649
 
7650
+ _onConsumerScore({ consumerId, kind, score }) {
7651
+ logger.info("_onConsumerScore()", { consumerId, kind, score });
7652
+ const {
7653
+ appData: { sourceClientId },
7654
+ } = this._consumers.get(consumerId) || { appData: {} };
7655
+
7656
+ if (sourceClientId) {
7657
+ this._qualityMonitor.addConsumerScore(sourceClientId, consumerId, kind, score);
7658
+ }
7659
+ }
7660
+
7661
+ _onProducerScore({ producerId, kind, score }) {
7662
+ logger.info("_onProducerScore()", { producerId, kind, score });
7663
+ [this._micProducer, this._webcamProducer, this._screenVideoProducer, this._screenAudioProducer].forEach(
7664
+ (producer) => {
7665
+ if (producer?.id === producerId) {
7666
+ this._qualityMonitor.addProducerScore(this._selfId, producerId, kind, score);
7667
+ }
7668
+ }
7669
+ );
7670
+ }
7671
+
6659
7672
  async _onDataConsumerReady(options) {
6660
- this._logger.debug("_onDataConsumerReady()", { id: options.id, producerId: options.producerId });
7673
+ logger.info("_onDataConsumerReady()", { id: options.id, producerId: options.producerId });
6661
7674
  const consumer = await this._receiveTransport.consumeData(options);
6662
7675
 
6663
7676
  this._dataConsumers.set(consumer.id, consumer);
@@ -6683,7 +7696,7 @@ class VegaRtcManager {
6683
7696
  }
6684
7697
 
6685
7698
  async _onDataConsumerClosed({ dataConsumerId, reason }) {
6686
- this._logger.debug("_onDataConsumerClosed()", { dataConsumerId, reason });
7699
+ logger.info("_onDataConsumerClosed()", { dataConsumerId, reason });
6687
7700
  const consumer = this._dataConsumers.get(dataConsumerId);
6688
7701
  consumer?.close();
6689
7702
  }
@@ -6739,7 +7752,7 @@ class VegaRtcManager {
6739
7752
  } = clientState;
6740
7753
 
6741
7754
  // Need to pause/resume any consumers that are part of a stream that has been
6742
- // accepted or dosconnected by the PWA
7755
+ // accepted or disconnected by the PWA
6743
7756
  const toPauseConsumers = [];
6744
7757
  const toResumeConsumers = [];
6745
7758
 
@@ -6841,6 +7854,10 @@ class VegaRtcManager {
6841
7854
  setMicAnalyserParams(params) {
6842
7855
  this._micAnalyser?.setParams(params);
6843
7856
  }
7857
+
7858
+ hasClient(clientId) {
7859
+ return this._clientStates.has(clientId);
7860
+ }
6844
7861
  }
6845
7862
 
6846
7863
  class RtcManagerDispatcher {
@@ -6849,7 +7866,16 @@ class RtcManagerDispatcher {
6849
7866
  this.currentManager = null;
6850
7867
  serverSocket.on(PROTOCOL_RESPONSES.ROOM_JOINED, ({ room, selfId, error, eventClaim }) => {
6851
7868
  if (error) return; // ignore error responses which lack room
6852
- const config = { selfId, room, emitter, serverSocket, webrtcProvider, features, eventClaim };
7869
+ const config = {
7870
+ selfId,
7871
+ room,
7872
+ emitter,
7873
+ serverSocket,
7874
+ webrtcProvider,
7875
+ features,
7876
+ eventClaim,
7877
+ deviceHandlerFactory: features?.deviceHandlerFactory,
7878
+ };
6853
7879
  const isSfu = !!room.sfuServer;
6854
7880
  if (this.currentManager) {
6855
7881
  if (this.currentManager.isInitializedWith({ selfId, roomName: room.name, isSfu })) {
@@ -6870,6 +7896,7 @@ class RtcManagerDispatcher {
6870
7896
  rtcManager.setupSocketListeners();
6871
7897
  emitter.emit(EVENTS.RTC_MANAGER_CREATED, { rtcManager });
6872
7898
  this.currentManager = rtcManager;
7899
+ serverSocket.setRtcManager(rtcManager);
6873
7900
  });
6874
7901
  }
6875
7902
 
@@ -7794,7 +8821,7 @@ var localStorage$1 = localStorage;
7794
8821
  const events = {
7795
8822
  CREDENTIALS_SAVED: "credentials_saved",
7796
8823
  };
7797
- class CredentialsService extends EventEmitter$1 {
8824
+ class CredentialsService extends EventEmitter {
7798
8825
  constructor({ deviceService, credentialsStore, }) {
7799
8826
  super();
7800
8827
  this._deviceService = deviceService;
@@ -8432,7 +9459,7 @@ class LocalParticipant extends RoomParticipant {
8432
9459
  }
8433
9460
  }
8434
9461
 
8435
- const sdkVersion = "0.2.0-beta.0";
9462
+ const sdkVersion = "0.2.0";
8436
9463
 
8437
9464
  const defaultSubdomainPattern = /^(?:([^.]+)[.])?((:?[^.]+[.]){1,}[^.]+)$/;
8438
9465
  const localstackPattern = /^(?:([^.]+)-)?(ip-[^.]*[.](?:hereby[.]dev|rfc1918[.]disappear[.]at)(?::\d+|))$/;