@whereby.com/core 0.2.0-beta.0 → 0.3.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
 
@@ -80,6 +83,7 @@ const signalEvents = {
80
83
  audioEnabled: createSignalEventAction("audioEnabled"),
81
84
  chatMessage: createSignalEventAction("chatMessage"),
82
85
  clientLeft: createSignalEventAction("clientLeft"),
86
+ clientKicked: createSignalEventAction("clientKicked"),
83
87
  clientMetadataReceived: createSignalEventAction("clientMetadataReceived"),
84
88
  cloudRecordingStarted: createSignalEventAction("cloudRecordingStarted"),
85
89
  cloudRecordingStopped: createSignalEventAction("cloudRecordingStopped"),
@@ -184,13 +188,535 @@ createReactor([selectShouldFetchDeviceCredentials], ({ dispatch }, shouldFetchDe
184
188
  }
185
189
  });
186
190
 
191
+ let peerConnections = [];
192
+ let peerConnectionCounter = 0;
193
+ const peerConnectionData = new WeakMap();
194
+
195
+ const removePeerConnection = (pc) => {
196
+ peerConnections = peerConnections.filter((old) => old !== pc);
197
+ };
198
+
199
+ if (window.RTCPeerConnection) {
200
+ const OriginalRTCPeerConnection = window.RTCPeerConnection;
201
+ function PatchedRTCPeerConnection(rtcConfig) {
202
+ const pc = new OriginalRTCPeerConnection(rtcConfig);
203
+ peerConnections.push(pc);
204
+ peerConnectionData.set(pc, { index: peerConnectionCounter++ });
205
+ const onConnectionStateChange = () => {
206
+ if (pc.connectionState === "closed") {
207
+ removePeerConnection(pc);
208
+ pc.removeEventListener("connectionstatechange", onConnectionStateChange);
209
+ }
210
+ };
211
+ pc.addEventListener("connectionstatechange", onConnectionStateChange);
212
+ return pc;
213
+ }
214
+ PatchedRTCPeerConnection.prototype = OriginalRTCPeerConnection.prototype;
215
+ window.RTCPeerConnection = PatchedRTCPeerConnection;
216
+ }
217
+
218
+ const debugOn = new URLSearchParams(window.location.search).has("debug");
219
+
220
+ class Logger {
221
+ constructor() {
222
+ this._isEnabled = debugOn;
223
+ }
224
+
225
+ isEnabled() {
226
+ return this._isEnabled;
227
+ }
228
+
229
+ enable() {
230
+ this._isEnabled = true;
231
+ }
232
+
233
+ disable() {
234
+ this._isEnabled = false;
235
+ }
236
+
237
+ info(...params) {
238
+ if (!this._isEnabled) {
239
+ return;
240
+ }
241
+ // eslint-disable-next-line no-console
242
+ return console.info(...params);
243
+ }
244
+
245
+ warn(...params) {
246
+ if (!this._isEnabled) {
247
+ return;
248
+ }
249
+ return console.warn(...params);
250
+ }
251
+
252
+ error(...params) {
253
+ if (!this._isEnabled) {
254
+ return;
255
+ }
256
+ return console.error(...params);
257
+ }
258
+
259
+ withDebugLogger(myDebugger = null) {
260
+ this._debugger = myDebugger;
261
+ return this;
262
+ }
263
+
264
+ debug(...params) {
265
+ if (!this._isEnabled || !this._debugger) {
266
+ return;
267
+ }
268
+ const suppliedParams = [];
269
+ params.forEach((param) => {
270
+ if (typeof param === "function") {
271
+ const suppliedParam = param();
272
+ suppliedParams.push(suppliedParam);
273
+ } else {
274
+ suppliedParams.push(param);
275
+ }
276
+ });
277
+ this._debugger.print(...suppliedParams);
278
+ }
279
+ }
280
+
281
+ let currentMonitor = null;
282
+
283
+ const getUpdatedStats = () => currentMonitor?.getUpdatedStats();
284
+
285
+ // Protocol enum used for the CLIENT (Make sure to keep it in sync with its server counterpart)
286
+
287
+ // Requests: messages from the client to the server
288
+ const PROTOCOL_REQUESTS = {
289
+ BLOCK_CLIENT: "block_client",
290
+ CLAIM_ROOM: "claim_room",
291
+ CLEAR_CHAT_HISTORY: "clear_chat_history",
292
+ ENABLE_AUDIO: "enable_audio",
293
+ ENABLE_VIDEO: "enable_video",
294
+ END_STREAM: "end_stream",
295
+ FETCH_MEDIASERVER_CONFIG: "fetch_mediaserver_config",
296
+ HANDLE_KNOCK: "handle_knock",
297
+ IDENTIFY_DEVICE: "identify_device",
298
+ INVITE_CLIENT_AS_MEMBER: "invite_client_as_member",
299
+ JOIN_ROOM: "join_room",
300
+ KICK_CLIENT: "kick_client",
301
+ KNOCK_ROOM: "knock_room",
302
+ LEAVE_ROOM: "leave_room",
303
+ SEND_CLIENT_METADATA: "send_client_metadata",
304
+ SET_LOCK: "set_lock",
305
+ SHARE_MEDIA: "share_media",
306
+ START_NEW_STREAM: "start_new_stream",
307
+ START_SCREENSHARE: "start_screenshare",
308
+ STOP_SCREENSHARE: "stop_screenshare",
309
+ START_URL_EMBED: "start_url_embed",
310
+ STOP_URL_EMBED: "stop_url_embed",
311
+ START_RECORDING: "start_recording",
312
+ STOP_RECORDING: "stop_recording",
313
+ SFU_TOKEN: "sfu_token",
314
+ };
315
+
316
+ // Responses: messages from the server to the client, in response to requests
317
+ const PROTOCOL_RESPONSES = {
318
+ AUDIO_ENABLED: "audio_enabled",
319
+ BACKGROUND_IMAGE_CHANGED: "background_image_changed",
320
+ BLOCK_ADDED: "block_added",
321
+ BLOCK_REMOVED: "block_removed",
322
+ CHAT_HISTORY_CLEARED: "chat_history_cleared",
323
+ CLIENT_BLOCKED: "client_blocked",
324
+ CLIENT_INVITED_AS_MEMBER: "client_invited_as_member",
325
+ CLIENT_KICKED: "client_kicked",
326
+ CLIENT_LEFT: "client_left",
327
+ CLIENT_METADATA_RECEIVED: "client_metadata_received",
328
+ CLIENT_READY: "client_ready",
329
+ CLIENT_ROLE_CHANGED: "client_role_changed",
330
+ CLIENT_USER_ID_CHANGED: "client_user_id_changed",
331
+ CONTACTS_UPDATED: "contacts_updated",
332
+ DEVICE_IDENTIFIED: "device_identified",
333
+ ROOM_ROLES_UPDATED: "room_roles_updated",
334
+ KNOCK_HANDLED: "knock_handled",
335
+ KNOCK_PAGE_BACKGROUND_CHANGED: "knock_page_background_changed",
336
+ KNOCKER_LEFT: "knocker_left",
337
+ MEDIASERVER_CONFIG: "mediaserver_config",
338
+ MEDIA_SHARED: "media_shared",
339
+ MEMBER_INVITE: "member_invite",
340
+ NEW_CLIENT: "new_client",
341
+ NEW_STREAM_STARTED: "new_stream_started",
342
+ SCREENSHARE_STARTED: "screenshare_started",
343
+ SCREENSHARE_STOPPED: "screenshare_stopped",
344
+ OWNER_NOTIFIED: "owner_notified",
345
+ OWNERS_CHANGED: "owners_changed",
346
+ PLAY_CLIENT_STICKER: "play_client_sticker",
347
+ ROOM_INTEGRATION_ENABLED: "room_integration_enabled",
348
+ ROOM_INTEGRATION_DISABLED: "room_integration_disabled",
349
+ ROOM_JOINED: "room_joined",
350
+ ROOM_KNOCKED: "room_knocked",
351
+ ROOM_LEFT: "room_left",
352
+ ROOM_LOCKED: "room_locked",
353
+ ROOM_PERMISSIONS_CHANGED: "room_permissions_changed",
354
+ ROOM_LOGO_CHANGED: "room_logo_changed",
355
+ ROOM_TYPE_CHANGED: "room_type_changed",
356
+ ROOM_MODE_CHANGED: "room_mode_changed",
357
+ SOCKET_USER_ID_CHANGED: "socket_user_id_changed",
358
+ STICKERS_UNLOCKED: "stickers_unlocked",
359
+ STREAM_ENDED: "stream_ended",
360
+ URL_EMBED_STARTED: "url_embed_started",
361
+ URL_EMBED_STOPPED: "url_embed_stopped",
362
+ RECORDING_STARTED: "recording_started",
363
+ RECORDING_STOPPED: "recording_stopped",
364
+ USER_NOTIFIED: "user_notified",
365
+ VIDEO_ENABLED: "video_enabled",
366
+ CLIENT_UNABLE_TO_JOIN: "client_unable_to_join",
367
+ };
368
+
369
+ // Relays: messages between clients, relayed through the server
370
+ const RELAY_MESSAGES = {
371
+ CHAT_MESSAGE: "chat_message",
372
+ CHAT_READ_STATE: "chat_read_state",
373
+ CHAT_STATE: "chat_state",
374
+ ICE_CANDIDATE: "ice_candidate",
375
+ ICE_END_OF_CANDIDATES: "ice_endofcandidates",
376
+ READY_TO_RECEIVE_OFFER: "ready_to_receive_offer",
377
+ REMOTE_CLIENT_MEDIA_REQUEST: "remote_client_media_request",
378
+ SDP_ANSWER: "sdp_answer",
379
+ SDP_OFFER: "sdp_offer",
380
+ VIDEO_STICKER: "video_sticker",
381
+ };
382
+
383
+ // Events: something happened that we want to let the client know about
384
+ const PROTOCOL_EVENTS = {
385
+ PENDING_CLIENT_LEFT: "pending_client_left",
386
+ MEDIA_QUALITY_CHANGED: "media_quality_changed",
387
+ };
388
+
389
+ const logger$9 = new Logger();
390
+
391
+ class ReconnectManager extends EventEmitter {
392
+ constructor(socket) {
393
+ super();
394
+ this._socket = socket;
395
+ this._clients = {};
396
+ this._signalDisconnectTime = undefined;
397
+ this.rtcManager = undefined;
398
+ this.metrics = {
399
+ roomJoinedLate: 0,
400
+ pendingClientCanceled: 0,
401
+ evaluationFailed: 0,
402
+ roomJoined: 0,
403
+ };
404
+
405
+ socket.on("disconnect", () => {
406
+ this._signalDisconnectTime = Date.now();
407
+ });
408
+
409
+ // We intercept these events and take responsiblity for forwarding them
410
+ socket.on(PROTOCOL_RESPONSES.ROOM_JOINED, (payload) => this._onRoomJoined(payload));
411
+ socket.on(PROTOCOL_RESPONSES.NEW_CLIENT, (payload) => this._onNewClient(payload));
412
+ socket.on(PROTOCOL_RESPONSES.CLIENT_LEFT, (payload) => this._onClientLeft(payload));
413
+
414
+ // We intercept these events and handle them without forwarding them
415
+ socket.on(PROTOCOL_EVENTS.PENDING_CLIENT_LEFT, (payload) => this._onPendingClientLeft(payload));
416
+
417
+ // We gather information from these events but they will also be forwarded
418
+ socket.on(PROTOCOL_RESPONSES.AUDIO_ENABLED, (payload) => this._onAudioEnabled(payload));
419
+ socket.on(PROTOCOL_RESPONSES.VIDEO_ENABLED, (payload) => this._onVideoEnabled(payload));
420
+ socket.on(PROTOCOL_RESPONSES.SCREENSHARE_STARTED, (payload) => this._onScreenshareChanged(payload, true));
421
+ socket.on(PROTOCOL_RESPONSES.SCREENSHARE_STOPPED, (payload) => this._onScreenshareChanged(payload, false));
422
+ }
423
+
424
+ async _onRoomJoined(payload) {
425
+ // We might have gotten an error
426
+ if (!payload.room?.clients) {
427
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
428
+ return;
429
+ }
430
+
431
+ if (!payload.selfId) {
432
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
433
+ return;
434
+ }
435
+
436
+ const myDeviceId = payload.room.clients.find((c) => payload.selfId === c.id)?.deviceId;
437
+ if (!myDeviceId) {
438
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
439
+ return;
440
+ }
441
+
442
+ // Try to remove our own pending client if this is a page reload
443
+ // Could also be a first normal room_joined which can never be glitch-free
444
+ if (!this._signalDisconnectTime) {
445
+ this._resetClientState(payload);
446
+ payload.room.clients = payload.room.clients.filter(
447
+ (c) => !(c.deviceId === myDeviceId && c.isPendingToLeave)
448
+ );
449
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
450
+ return;
451
+ }
452
+
453
+ // The threshold for trying glitch-free reconnect should be less than server-side configuration
454
+ const RECONNECT_THRESHOLD = payload.disconnectTimeout * 0.8;
455
+ const timeSinceDisconnect = Date.now() - this._signalDisconnectTime;
456
+ if (timeSinceDisconnect > RECONNECT_THRESHOLD) {
457
+ this._resetClientState(payload);
458
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
459
+ this.metrics.roomJoinedLate++;
460
+ return;
461
+ }
462
+
463
+ // At this point we want to try to attempt glitch-free reconnection experience
464
+
465
+ // Filter out our own pending client after page reload
466
+ payload.room.clients = payload.room.clients.filter((c) => !(c.deviceId === myDeviceId && c.isPendingToLeave));
467
+
468
+ const allStats = await getUpdatedStats();
469
+ payload.room.clients.forEach((client) => {
470
+ try {
471
+ if (client.id === payload.selfId) return;
472
+
473
+ // Maybe add client to state
474
+ if (!this._clients[client.id]) {
475
+ this._addClientToState(client);
476
+ return;
477
+ }
478
+ // Verify that rtcManager knows about the client
479
+ if (!this.rtcManager?.hasClient(client.id)) {
480
+ return;
481
+ }
482
+
483
+ // Verify that the client state hasn't changed
484
+ if (
485
+ this._hasClientStateChanged({
486
+ clientId: client.id,
487
+ webcam: client.isVideoEnabled,
488
+ mic: client.isAudioEnabled,
489
+ screenShare: client.streams.length > 1,
490
+ })
491
+ ) {
492
+ return;
493
+ }
494
+
495
+ if (this._wasClientSendingMedia(client.id)) {
496
+ // Verify the client media is still flowing (not stopped from other end)
497
+ if (!this._isClientMediaActive(allStats, client.id)) {
498
+ return;
499
+ }
500
+ }
501
+
502
+ client.mergeWithOldClientState = true;
503
+ } catch (error) {
504
+ logger$9.error("Failed to evaluate if we should merge client state %o", error);
505
+ this.metrics.evaluationFailed++;
506
+ }
507
+ });
508
+
509
+ // We will try to remove any remote client pending to leave
510
+ payload.room.clients.forEach((c) => {
511
+ if (c.isPendingToLeave) {
512
+ this._onPendingClientLeft({ clientId: c.id });
513
+ }
514
+ });
515
+
516
+ this.metrics.roomJoined++;
517
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
518
+ }
519
+
520
+ _onClientLeft(payload) {
521
+ const { clientId } = payload;
522
+ const client = this._clients[clientId];
523
+
524
+ // Remove client from state and clear timeout if client was pending to leave
525
+ if (client) {
526
+ clearTimeout(client.timeout);
527
+ delete this._clients[clientId];
528
+ }
529
+
530
+ // Old RTCManager only takes one argument, so rest is ignored.
531
+ this.rtcManager?.disconnect(clientId, /* activeBreakout */ null, payload.eventClaim);
532
+
533
+ this.emit(PROTOCOL_RESPONSES.CLIENT_LEFT, payload);
534
+ }
535
+
536
+ _onPendingClientLeft(payload) {
537
+ const { clientId } = payload;
538
+ const client = this._clients[clientId];
539
+
540
+ if (!client) {
541
+ logger$9.warn(`client ${clientId} not found`);
542
+ return;
543
+ }
544
+
545
+ // We've already started the check below, don't do it again
546
+ if (client.isPendingToLeave) {
547
+ return;
548
+ }
549
+
550
+ client.isPendingToLeave = true;
551
+ if (this._wasClientSendingMedia(clientId)) {
552
+ client.checkActiveMediaAttempts = 0;
553
+ this._abortIfNotActive(payload);
554
+ }
555
+ }
556
+
557
+ _onNewClient(payload) {
558
+ const {
559
+ client: { id: clientId, deviceId },
560
+ } = payload;
561
+
562
+ const client = this._clients[clientId];
563
+ if (client && client.isPendingToLeave) {
564
+ clearTimeout(client.timeoutHandler);
565
+ client.isPendingToLeave = false;
566
+ this.metrics.pendingClientCanceled++;
567
+ return;
568
+ }
569
+
570
+ this._getPendingClientsByDeviceId(deviceId).forEach((client) => {
571
+ clearTimeout(client.timeoutHandler);
572
+ client.isPendingToLeave = undefined;
573
+ this.emit(PROTOCOL_RESPONSES.CLIENT_LEFT, { clientId: client.clientId });
574
+ });
575
+
576
+ this._addClientToState(payload.client);
577
+ this.emit(PROTOCOL_RESPONSES.NEW_CLIENT, payload);
578
+ }
579
+
580
+ // Evaluate if we should send send client_left before getting it from signal-server
581
+ async _abortIfNotActive(payload) {
582
+ const { clientId } = payload;
583
+
584
+ let client = this._clients[clientId];
585
+ if (!client?.isPendingToLeave) return;
586
+
587
+ client.checkActiveMediaAttempts++;
588
+ if (client.checkActiveMediaAttempts > 3) {
589
+ return;
590
+ }
591
+
592
+ const stillActive = await this._checkIsActive(clientId);
593
+ if (stillActive) {
594
+ client.timeoutHandler = setTimeout(() => this._abortIfNotActive(payload), 500);
595
+ return;
596
+ }
597
+
598
+ client = this._clients[clientId];
599
+ if (client?.isPendingToLeave) {
600
+ clearTimeout(client.timeoutHandler);
601
+ delete this._clients[clientId];
602
+ this.emit(PROTOCOL_RESPONSES.CLIENT_LEFT, payload);
603
+ }
604
+ }
605
+
606
+ // Check if client is active
607
+ async _checkIsActive(clientId) {
608
+ const allStats = await getUpdatedStats();
609
+ return this._isClientMediaActive(allStats, clientId);
610
+ }
611
+
612
+ // Check if client has bitrates for all tracks
613
+ _isClientMediaActive(stats, clientId) {
614
+ const clientStats = stats?.[clientId];
615
+ let isActive = false;
616
+ if (clientStats) {
617
+ Object.entries(clientStats.tracks).forEach(([trackId, trackStats]) => {
618
+ if (trackId !== "probator")
619
+ Object.values(trackStats.ssrcs).forEach((ssrcStats) => {
620
+ if ((ssrcStats.bitrate || 0) > 0) isActive = true;
621
+ });
622
+ });
623
+ }
624
+ return isActive;
625
+ }
626
+
627
+ _onAudioEnabled(payload) {
628
+ const { clientId, isAudioEnabled } = payload;
629
+ this._clients[clientId] = {
630
+ ...(this._clients[clientId] || {}),
631
+ isAudioEnabled,
632
+ };
633
+ }
634
+
635
+ _onVideoEnabled(payload) {
636
+ const { clientId, isVideoEnabled } = payload;
637
+ this._clients[clientId] = {
638
+ ...(this._clients[clientId] || {}),
639
+ isVideoEnabled,
640
+ };
641
+ }
642
+
643
+ _onScreenshareChanged(payload, action) {
644
+ const { clientId } = payload;
645
+ this._clients[clientId] = {
646
+ ...(this._clients[clientId] || {}),
647
+ isScreenshareEnabled: action,
648
+ };
649
+ }
650
+
651
+ _hasClientStateChanged({ clientId, webcam, mic, screenShare }) {
652
+ const state = this._clients[clientId];
653
+
654
+ if (!state) {
655
+ throw new Error(`Client ${clientId} not found in ReconnectManager state`);
656
+ }
657
+
658
+ if (webcam !== state.isVideoEnabled) {
659
+ return true;
660
+ }
661
+ if (mic !== state.isAudioEnabled) {
662
+ return true;
663
+ }
664
+ if (screenShare !== state.isScreenshareEnabled) {
665
+ return true;
666
+ }
667
+
668
+ return false;
669
+ }
670
+
671
+ _addClientToState(newClient) {
672
+ this._clients[newClient.id] = {
673
+ ...(this._clients[newClient.id] || {}),
674
+ isAudioEnabled: newClient.isAudioEnabled,
675
+ isVideoEnabled: newClient.isVideoEnabled,
676
+ isScreenshareEnabled: newClient.streams.length > 1,
677
+ deviceId: newClient.deviceId,
678
+ isPendingToLeave: newClient.isPendingToLeave,
679
+ clientId: newClient.id,
680
+ };
681
+ }
682
+
683
+ _wasClientSendingMedia(clientId) {
684
+ const client = this._clients[clientId];
685
+
686
+ if (!client) {
687
+ throw new Error(`Client ${clientId} not found in ReconnectManager state`);
688
+ }
689
+
690
+ return client.isAudioEnabled || client.isVideoEnabled || client.isScreenshareEnabled;
691
+ }
692
+
693
+ _getPendingClientsByDeviceId(deviceId) {
694
+ return Object.values(this._clients).filter((clientState) => {
695
+ return clientState.deviceId === deviceId && clientState.isPendingToLeave;
696
+ });
697
+ }
698
+
699
+ _resetClientState(payload) {
700
+ this._clients = {};
701
+ payload.room.clients.forEach((client) => {
702
+ if (client.id === payload.selfId) {
703
+ return;
704
+ } else {
705
+ this._addClientToState(client);
706
+ }
707
+ });
708
+ }
709
+ }
710
+
187
711
  const DEFAULT_SOCKET_PATH = "/protocol/socket.io/v4";
188
712
 
713
+ const NOOP_KEEPALIVE_INTERVAL = 2000;
714
+
189
715
  /**
190
716
  * Wrapper class that extends the Socket.IO client library.
191
717
  */
192
718
  class ServerSocket {
193
- constructor(hostName, optionsOverrides) {
719
+ constructor(hostName, optionsOverrides, glitchFree) {
194
720
  this._socket = io(hostName, {
195
721
  path: DEFAULT_SOCKET_PATH,
196
722
  randomizationFactor: 0.5,
@@ -216,14 +742,41 @@ class ServerSocket {
216
742
  this._socket.io.opts.transports = ["websocket", "polling"];
217
743
  }
218
744
  });
745
+
746
+ if (glitchFree) this._reconnectManager = new ReconnectManager(this._socket);
747
+
219
748
  this._socket.on("connect", () => {
220
749
  const transport = this.getTransport();
221
750
  if (transport === "websocket") {
222
751
  this._wasConnectedUsingWebsocket = true;
752
+
753
+ // start noop keepalive loop to detect client side disconnects fast
754
+ if (!this.noopKeepaliveInterval)
755
+ this.noopKeepaliveInterval = setInterval(() => {
756
+ try {
757
+ // send a noop message if it thinks it is connected (might not be)
758
+ if (this._socket.connected) {
759
+ this._socket.io.engine.sendPacket("noop");
760
+ }
761
+ } catch (ex) {}
762
+ }, NOOP_KEEPALIVE_INTERVAL);
763
+ }
764
+ });
765
+
766
+ this._socket.on("disconnect", () => {
767
+ if (this.noopKeepaliveInterval) {
768
+ clearInterval(this.noopKeepaliveInterval);
769
+ this.noopKeepaliveInterval = null;
223
770
  }
224
771
  });
225
772
  }
226
773
 
774
+ setRtcManager(rtcManager) {
775
+ if (this._reconnectManager) {
776
+ this._reconnectManager.rtcManager = rtcManager;
777
+ }
778
+ }
779
+
227
780
  connect() {
228
781
  if (this.isConnected() || this.isConnecting()) {
229
782
  return;
@@ -282,6 +835,17 @@ class ServerSocket {
282
835
  * @returns {function} Function to deregister the listener.
283
836
  */
284
837
  on(eventName, handler) {
838
+ const relayableEvents = [
839
+ PROTOCOL_RESPONSES.ROOM_JOINED,
840
+ PROTOCOL_RESPONSES.CLIENT_LEFT,
841
+ PROTOCOL_RESPONSES.NEW_CLIENT,
842
+ ];
843
+
844
+ // Intercept certain events if glitch-free is enabled.
845
+ if (this._reconnectManager && relayableEvents.includes(eventName)) {
846
+ return this._interceptEvent(eventName, handler);
847
+ }
848
+
285
849
  this._socket.on(eventName, handler);
286
850
 
287
851
  return () => {
@@ -308,12 +872,30 @@ class ServerSocket {
308
872
  off(eventName, handler) {
309
873
  this._socket.off(eventName, handler);
310
874
  }
875
+
876
+ /**
877
+ * Intercept event and let ReconnectManager handle them.
878
+ */
879
+ _interceptEvent(eventName, handler) {
880
+ if (this._reconnectManager) {
881
+ this._reconnectManager.on(eventName, handler);
882
+ }
883
+
884
+ return () => {
885
+ if (this._reconnectManager) this._reconnectManager.removeListener(eventName, handler);
886
+ };
887
+ }
888
+
889
+ getGlitchFreeMetrics() {
890
+ return this._reconnectManager?.metrics;
891
+ }
311
892
  }
312
893
 
313
894
  function forwardSocketEvents(socket, dispatch) {
314
895
  socket.on("room_joined", (payload) => dispatch(signalEvents.roomJoined(payload)));
315
896
  socket.on("new_client", (payload) => dispatch(signalEvents.newClient(payload)));
316
897
  socket.on("client_left", (payload) => dispatch(signalEvents.clientLeft(payload)));
898
+ socket.on("client_kicked", (payload) => dispatch(signalEvents.clientKicked(payload)));
317
899
  socket.on("audio_enabled", (payload) => dispatch(signalEvents.audioEnabled(payload)));
318
900
  socket.on("video_enabled", (payload) => dispatch(signalEvents.videoEnabled(payload)));
319
901
  socket.on("client_metadata_received", (payload) => dispatch(signalEvents.clientMetadataReceived(payload)));
@@ -537,6 +1119,7 @@ const parseResolution = (res) => res.split(/[^\d]/g).map((n) => parseInt(n, 10))
537
1119
  function getMediaConstraints({
538
1120
  disableAEC,
539
1121
  disableAGC,
1122
+ fps24,
540
1123
  hd,
541
1124
  lax,
542
1125
  lowDataMode,
@@ -547,7 +1130,7 @@ function getMediaConstraints({
547
1130
  }) {
548
1131
  let HIGH_HEIGHT = 480;
549
1132
  let LOW_HEIGHT = 240;
550
- let LOW_FPS = 15;
1133
+ let NON_STANDARD_FPS = 0;
551
1134
 
552
1135
  if (hd) {
553
1136
  // respect user choice, but default to HD for pro, and SD for free
@@ -560,15 +1143,20 @@ function getMediaConstraints({
560
1143
  } else {
561
1144
  LOW_HEIGHT = 360;
562
1145
  }
563
- LOW_FPS = 30; // we still use 30fps because of assumptions about temporal layers
564
1146
  }
565
1147
 
1148
+ // Set framerate to 24 to increase quality/bandwidth
1149
+ if (fps24) NON_STANDARD_FPS = 24;
1150
+
1151
+ // Set framerate for low data, but only for non-simulcast
1152
+ if (lowDataMode && !simulcast) NON_STANDARD_FPS = 15;
1153
+
566
1154
  const constraints = {
567
1155
  audio: { ...(preferredDeviceIds.audioId && { deviceId: preferredDeviceIds.audioId }) },
568
1156
  video: {
569
1157
  ...(preferredDeviceIds.videoId ? { deviceId: preferredDeviceIds.videoId } : { facingMode: "user" }),
570
1158
  height: lowDataMode ? LOW_HEIGHT : HIGH_HEIGHT,
571
- ...(lowDataMode && { frameRate: LOW_FPS }),
1159
+ ...(NON_STANDARD_FPS && { frameRate: NON_STANDARD_FPS }),
572
1160
  },
573
1161
  };
574
1162
  if (lax) {
@@ -701,6 +1289,8 @@ var assert_1 = assert;
701
1289
 
702
1290
  var assert$1 = /*@__PURE__*/getDefaultExportFromCjs(assert_1);
703
1291
 
1292
+ const logger$8 = new Logger();
1293
+
704
1294
  const isMobile = /mobi/i.test(navigator.userAgent);
705
1295
 
706
1296
  class NoDevicesError extends Error {
@@ -720,7 +1310,7 @@ function getUserMedia(constraints) {
720
1310
 
721
1311
  return navigator.mediaDevices.getUserMedia(constraints).catch((error) => {
722
1312
  const message = `${error}, ${JSON.stringify(constraints, null, 2)}`;
723
- console.error(`getUserMedia ${message}`);
1313
+ logger$8.error(`getUserMedia ${message}`);
724
1314
  throw error;
725
1315
  });
726
1316
  }
@@ -906,7 +1496,7 @@ async function getStream(constraintOpt, { replaceStream, fallback = true } = {})
906
1496
  getConstraints({ ...constraintOpt, options: { ...constraintOpt.options, lax: true } })
907
1497
  );
908
1498
  } catch (e2) {
909
- console.warn(`Tried getting stream again with laxer constraints, but failed: ${"" + e2}`);
1499
+ logger$8.warn(`Tried getting stream again with laxer constraints, but failed: ${"" + e2}`);
910
1500
  }
911
1501
  // Message often hints at which was the problem, let's use that
912
1502
  const errMsg = ("" + e).toLowerCase();
@@ -915,7 +1505,7 @@ async function getStream(constraintOpt, { replaceStream, fallback = true } = {})
915
1505
  try {
916
1506
  stream = await getUserMedia(getConstraints({ ...constraintOpt, [problemWith]: null }));
917
1507
  } catch (e2) {
918
- console.warn(`Re-tried ${problemWith} with no constraints, but failed: ${"" + e2}`);
1508
+ logger$8.warn(`Re-tried ${problemWith} with no constraints, but failed: ${"" + e2}`);
919
1509
  }
920
1510
  }
921
1511
  if (!stream) {
@@ -926,7 +1516,7 @@ async function getStream(constraintOpt, { replaceStream, fallback = true } = {})
926
1516
  try {
927
1517
  stream = await getUserMedia(getConstraints({ ...constraintOpt, [kind]: false }));
928
1518
  } catch (e2) {
929
- console.warn(`Re-tried without ${kind}, but failed: ${"" + e2}`);
1519
+ logger$8.warn(`Re-tried without ${kind}, but failed: ${"" + e2}`);
930
1520
  }
931
1521
  if (stream) break;
932
1522
  }
@@ -1917,6 +2507,9 @@ const roomConnectionSlice = createSlice({
1917
2507
  }
1918
2508
  return Object.assign(Object.assign({}, state), { session: null });
1919
2509
  });
2510
+ builder.addCase(signalEvents.clientKicked, (state) => {
2511
+ return Object.assign(Object.assign({}, state), { status: "kicked" });
2512
+ });
1920
2513
  builder.addCase(socketReconnecting, (state) => {
1921
2514
  return Object.assign(Object.assign({}, state), { status: "reconnect" });
1922
2515
  });
@@ -1978,163 +2571,65 @@ const doConnectRoom = createAppThunk(() => (dispatch, getState) => {
1978
2571
  organizationId,
1979
2572
  roomKey,
1980
2573
  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",
2574
+ selfId,
2575
+ userAgent: `browser-sdk:${sdkVersion || "unknown"}`,
2576
+ externalId,
2577
+ });
2578
+ dispatch(connectionStatusChanged("connecting"));
2579
+ });
2580
+ const selectRoomConnectionRaw = (state) => state.roomConnection;
2581
+ const selectRoomConnectionSession = (state) => state.roomConnection.session;
2582
+ const selectRoomConnectionSessionId = (state) => { var _a; return (_a = state.roomConnection.session) === null || _a === void 0 ? void 0 : _a.id; };
2583
+ const selectRoomConnectionStatus = (state) => state.roomConnection.status;
2584
+ const selectShouldConnectRoom = createSelector([selectOrganizationId, selectRoomConnectionStatus, selectSignalConnectionDeviceIdentified, selectLocalMediaStatus], (hasOrganizationIdFetched, roomConnectionStatus, signalConnectionDeviceIdentified, localMediaStatus) => {
2585
+ if (localMediaStatus === "started" &&
2586
+ signalConnectionDeviceIdentified &&
2587
+ !!hasOrganizationIdFetched &&
2588
+ ["initializing", "reconnect"].includes(roomConnectionStatus)) {
2589
+ return true;
2590
+ }
2591
+ return false;
2592
+ });
2593
+ createReactor([selectShouldConnectRoom], ({ dispatch }, shouldConnectRoom) => {
2594
+ if (shouldConnectRoom) {
2595
+ dispatch(doConnectRoom());
2596
+ }
2597
+ });
2598
+ startAppListening({
2599
+ actionCreator: signalEvents.knockHandled,
2600
+ effect: ({ payload }, { dispatch, getState }) => {
2601
+ const { clientId, resolution } = payload;
2602
+ const state = getState();
2603
+ const selfId = selectSelfId(state);
2604
+ if (clientId !== selfId) {
2605
+ return;
2606
+ }
2607
+ if (resolution === "accepted") {
2608
+ dispatch(setRoomKey(payload.metadata.roomKey));
2609
+ dispatch(doConnectRoom());
2610
+ }
2611
+ else if (resolution === "rejected") {
2612
+ dispatch(connectionStatusChanged("knock_rejected"));
2613
+ }
2614
+ },
2615
+ });
2616
+
2617
+ const EVENTS = {
2618
+ CLIENT_CONNECTION_STATUS_CHANGED: "client_connection_status_changed",
2619
+ STREAM_ADDED: "stream_added",
2620
+ RTC_MANAGER_CREATED: "rtc_manager_created",
2621
+ RTC_MANAGER_DESTROYED: "rtc_manager_destroyed",
2622
+ LOCAL_STREAM_TRACK_ADDED: "local_stream_track_added",
2623
+ LOCAL_STREAM_TRACK_REMOVED: "local_stream_track_removed",
2624
+ REMOTE_STREAM_TRACK_ADDED: "remote_stream_track_added",
2625
+ REMOTE_STREAM_TRACK_REMOVED: "remote_stream_track_removed",
2124
2626
  };
2125
2627
 
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",
2628
+ const TYPES = {
2629
+ CONNECTING: "connecting",
2630
+ CONNECTION_FAILED: "connection_failed",
2631
+ CONNECTION_SUCCESSFUL: "connection_successful",
2632
+ CONNECTION_DISCONNECTED: "connection_disconnected",
2138
2633
  };
2139
2634
 
2140
2635
  const CAMERA_STREAM_ID$2 = "0";
@@ -2250,13 +2745,22 @@ function detectMicrophoneNotWorking(pc) {
2250
2745
  var rtcManagerEvents = {
2251
2746
  CAMERA_NOT_WORKING: "camera_not_working",
2252
2747
  CONNECTION_BLOCKED_BY_NETWORK: "connection_blocked_by_network",
2748
+ ICE_IPV6_SEEN: "ice_ipv6_seen",
2749
+ ICE_MDNS_SEEN: "ice_mdns_seen",
2750
+ ICE_NO_PUBLIC_IP_GATHERED: "ice_no_public_ip_gathered",
2751
+ ICE_NO_PUBLIC_IP_GATHERED_3SEC: "ice_no_public_ip_gathered_3sec",
2752
+ ICE_RESTART: "ice_restart",
2253
2753
  MICROPHONE_NOT_WORKING: "microphone_not_working",
2254
2754
  MICROPHONE_STOPPED_WORKING: "microphone_stopped_working",
2755
+ NEW_PC: "new_pc",
2756
+ SFU_CONNECTION_OPEN: "sfu_connection_open",
2255
2757
  SFU_CONNECTION_CLOSED: "sfu_connection_closed",
2256
2758
  COLOCATION_SPEAKER: "colocation_speaker",
2257
2759
  DOMINANT_SPEAKER: "dominant_speaker",
2258
2760
  };
2259
2761
 
2762
+ const logger$7 = new Logger();
2763
+
2260
2764
  const browserName$3 = adapter.browserDetails.browser;
2261
2765
  const browserVersion$1 = adapter.browserDetails.version;
2262
2766
 
@@ -2298,8 +2802,7 @@ function setCodecPreferenceSDP(sdp, vp9On, redOn) {
2298
2802
  const newSdp = sdpTransform.write(sdpObject);
2299
2803
  return newSdp;
2300
2804
  } catch (error) {
2301
- // eslint-disable-next-line no-console
2302
- console.log("setCodecPreferenceSDP error:", error);
2805
+ logger$7.error("setCodecPreferenceSDP error:", error);
2303
2806
  }
2304
2807
  }
2305
2808
 
@@ -2446,10 +2949,19 @@ const MAXIMUM_TURN_BANDWIDTH_UNLIMITED = -1; // kbps;
2446
2949
 
2447
2950
  const MEDIA_JITTER_BUFFER_TARGET = 400; // milliseconds;
2448
2951
 
2952
+ const logger$6 = new Logger();
2953
+
2449
2954
  class Session {
2450
- constructor({ peerConnectionId, bandwidth, maximumTurnBandwidth, deprioritizeH264Encoding, logger = console }) {
2955
+ constructor({ peerConnectionId, bandwidth, maximumTurnBandwidth, deprioritizeH264Encoding }) {
2451
2956
  this.peerConnectionId = peerConnectionId;
2452
2957
  this.relayCandidateSeen = false;
2958
+ this.serverReflexiveCandidateSeen = false;
2959
+ this.publicHostCandidateSeen = false;
2960
+ this.ipv6HostCandidateSeen = false;
2961
+ this.ipv6HostCandidateTeredoSeen = false;
2962
+ this.ipv6HostCandidate6to4Seen = false;
2963
+ this.mdnsHostCandidateSeen = false;
2964
+
2453
2965
  this.pc = null;
2454
2966
  this.wasEverConnected = false;
2455
2967
  this.connectionStatus = null;
@@ -2469,7 +2981,6 @@ class Session {
2469
2981
  });
2470
2982
  this.offerOptions = { offerToReceiveAudio: true, offerToReceiveVideo: true };
2471
2983
  this._deprioritizeH264Encoding = deprioritizeH264Encoding;
2472
- this._logger = logger;
2473
2984
  }
2474
2985
 
2475
2986
  setAndGetPeerConnection({ clientId, constraints, peerConnectionConfig, shouldAddLocalVideo }) {
@@ -2613,7 +3124,7 @@ class Session {
2613
3124
  return setVideoBandwidthUsingSetParameters(this.pc, this.bandwidth);
2614
3125
  },
2615
3126
  (e) => {
2616
- this._logger.warn("Could not set remote description from remote answer: ", e);
3127
+ logger$6.warn("Could not set remote description from remote answer: ", e);
2617
3128
  }
2618
3129
  );
2619
3130
  }
@@ -2634,7 +3145,7 @@ class Session {
2634
3145
  return;
2635
3146
  }
2636
3147
  this.pc.addIceCandidate(candidate).catch((e) => {
2637
- this._logger.warn("Failed to add ICE candidate ('%s'): %s", candidate ? candidate.candidate : null, e);
3148
+ logger$6.warn("Failed to add ICE candidate ('%s'): %s", candidate ? candidate.candidate : null, e);
2638
3149
  });
2639
3150
  });
2640
3151
  }
@@ -2656,7 +3167,7 @@ class Session {
2656
3167
  // do not handle state change events when we close the connection explicitly
2657
3168
  pc.close();
2658
3169
  } catch (e) {
2659
- console.warn("failures during close of session", e);
3170
+ logger$6.warn("failures during close of session", e);
2660
3171
  // we're not interested in errors from RTCPeerConnection.close()
2661
3172
  }
2662
3173
  }
@@ -2672,7 +3183,7 @@ class Session {
2672
3183
  const senders = pc.getSenders();
2673
3184
  function dbg(msg) {
2674
3185
  const tr = (t) => t && `id:${t.id},kind:${t.kind},state:${t.readyState}`;
2675
- this._logger.warn(
3186
+ logger$6.warn(
2676
3187
  `${msg}. newTrack:${tr(newTrack)}, oldTrack:${tr(oldTrack)}, sender tracks: ${JSON.stringify(
2677
3188
  senders.map((s) => `s ${tr(s.track)}`)
2678
3189
  )}, sender first codecs: ${JSON.stringify(senders.map((s) => (s.getParameters().codecs || [])[0]))}`
@@ -2826,6 +3337,21 @@ class Session {
2826
3337
 
2827
3338
  setVideoBandwidthUsingSetParameters(this.pc, this.bandwidth);
2828
3339
  }
3340
+
3341
+ setAudioOnly(enable, excludedTrackIds = []) {
3342
+ this.pc
3343
+ .getTransceivers()
3344
+ .filter(
3345
+ (videoTransceiver) =>
3346
+ videoTransceiver?.direction !== "recvonly" &&
3347
+ videoTransceiver?.receiver?.track?.kind === "video" &&
3348
+ !excludedTrackIds.includes(videoTransceiver?.receiver?.track?.id) &&
3349
+ !excludedTrackIds.includes(videoTransceiver?.sender?.track?.id)
3350
+ )
3351
+ .forEach((videoTransceiver) => {
3352
+ videoTransceiver.direction = enable ? "sendonly" : "sendrecv";
3353
+ });
3354
+ }
2829
3355
  }
2830
3356
 
2831
3357
  // transforms a maplike to an object. Mostly for getStats +
@@ -2924,6 +3450,8 @@ var rtcstats = function(trace, getStatsInterval, prefixesToWrap) {
2924
3450
  var peerconnectioncounter = 0;
2925
3451
  var isFirefox = !!window.mozRTCPeerConnection;
2926
3452
  var isEdge = !!window.RTCIceGatherer;
3453
+ var prevById = {};
3454
+
2927
3455
  prefixesToWrap.forEach(function(prefix) {
2928
3456
  if (!window[prefix + 'RTCPeerConnection']) {
2929
3457
  return;
@@ -2994,13 +3522,12 @@ var rtcstats = function(trace, getStatsInterval, prefixesToWrap) {
2994
3522
  trace('ondatachannel', id, [event.channel.id, event.channel.label]);
2995
3523
  });
2996
3524
 
2997
- var prev = {};
2998
3525
  var getStats = function() {
2999
3526
  pc.getStats(null).then(function(res) {
3000
3527
  var now = map2obj(res);
3001
3528
  var base = JSON.parse(JSON.stringify(now)); // our new prev
3002
- trace('getstats', id, deltaCompression(prev, now));
3003
- prev = base;
3529
+ trace('getstats', id, deltaCompression(prevById[id] || {}, now));
3530
+ prevById[id] = base;
3004
3531
  });
3005
3532
  };
3006
3533
  // TODO: do we want one big interval and all peerconnections
@@ -3220,6 +3747,13 @@ var rtcstats = function(trace, getStatsInterval, prefixesToWrap) {
3220
3747
  }
3221
3748
  });
3222
3749
  */
3750
+
3751
+ return {
3752
+ resetDelta() {
3753
+ prevById = {};
3754
+ }
3755
+ }
3756
+
3223
3757
  };
3224
3758
 
3225
3759
  var rtcstats$1 = /*@__PURE__*/getDefaultExportFromCjs(rtcstats);
@@ -3228,11 +3762,18 @@ var rtcstats$1 = /*@__PURE__*/getDefaultExportFromCjs(rtcstats);
3228
3762
 
3229
3763
  const RTCSTATS_PROTOCOL_VERSION = "1.0";
3230
3764
 
3765
+ // when not connected we need to buffer at least a few getstats reports
3766
+ // as they are delta compressed and we need the initial properties
3767
+ const GETSTATS_BUFFER_SIZE = 20;
3768
+
3231
3769
  const clientInfo = {
3232
- id: v4(), // shared id across rtcstats reconnects
3770
+ id: v4$1(), // shared id across rtcstats reconnects
3233
3771
  connectionNumber: 0,
3234
3772
  };
3235
3773
 
3774
+ const noop = () => {};
3775
+ let resetDelta = noop;
3776
+
3236
3777
  // Inlined version of rtcstats/trace-ws with improved disconnect handling.
3237
3778
  function rtcStatsConnection(wsURL, logger = console) {
3238
3779
  const buffer = [];
@@ -3245,6 +3786,7 @@ function rtcStatsConnection(wsURL, logger = console) {
3245
3786
  let connectionShouldBeOpen;
3246
3787
  let connectionAttempt = 0;
3247
3788
  let hasPassedOnRoomSessionId = false;
3789
+ let getStatsBufferUsed = 0;
3248
3790
 
3249
3791
  const connection = {
3250
3792
  connected: false,
@@ -3282,8 +3824,15 @@ function rtcStatsConnection(wsURL, logger = console) {
3282
3824
  if (ws.readyState === WebSocket.OPEN) {
3283
3825
  connectionAttempt = 0;
3284
3826
  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
3827
+ } else if (args[0] === "getstats") {
3828
+ // only buffer getStats for a while
3829
+ // we don't want this to pile up, but we need at least the initial reports
3830
+ if (getStatsBufferUsed < GETSTATS_BUFFER_SIZE) {
3831
+ getStatsBufferUsed++;
3832
+ buffer.push(args);
3833
+ }
3834
+ } else if (args[0] === "customEvent" && args[2].type === "insightsStats") ; else {
3835
+ // buffer everything else
3287
3836
  buffer.push(args);
3288
3837
  }
3289
3838
 
@@ -3318,6 +3867,7 @@ function rtcStatsConnection(wsURL, logger = console) {
3318
3867
  ws.onclose = (e) => {
3319
3868
  connection.connected = false;
3320
3869
  logger.info(`[RTCSTATS] Closed ${e.code}`);
3870
+ resetDelta();
3321
3871
  };
3322
3872
  ws.onopen = () => {
3323
3873
  // send client info after each connection, so analysis tools can handle reconnections
@@ -3342,10 +3892,11 @@ function rtcStatsConnection(wsURL, logger = console) {
3342
3892
  ws.send(JSON.stringify(userRole));
3343
3893
  }
3344
3894
 
3345
- // send buffered events (non-getStats)
3895
+ // send buffered events
3346
3896
  while (buffer.length) {
3347
3897
  ws.send(JSON.stringify(buffer.shift()));
3348
3898
  }
3899
+ getStatsBufferUsed = 0;
3349
3900
  };
3350
3901
  },
3351
3902
  };
@@ -3354,11 +3905,13 @@ function rtcStatsConnection(wsURL, logger = console) {
3354
3905
  }
3355
3906
 
3356
3907
  const server = rtcStatsConnection("wss://rtcstats.srv.whereby.com" );
3357
- rtcstats$1(
3908
+ const stats = rtcstats$1(
3358
3909
  server.trace,
3359
3910
  10000, // query once every 10 seconds.
3360
3911
  [""] // only shim unprefixed RTCPeerConnecion.
3361
3912
  );
3913
+ // on node clients this function can be undefined
3914
+ resetDelta = stats?.resetDelta || noop;
3362
3915
 
3363
3916
  const rtcStats = {
3364
3917
  sendEvent: (type, value) => {
@@ -3376,6 +3929,8 @@ const rtcStats = {
3376
3929
  server,
3377
3930
  };
3378
3931
 
3932
+ const logger$5 = new Logger();
3933
+
3379
3934
  const CAMERA_STREAM_ID$1 = RtcStream.getCameraId();
3380
3935
  const browserName$2 = adapter.browserDetails.browser;
3381
3936
  const browserVersion = adapter.browserDetails.version;
@@ -3392,7 +3947,7 @@ if (browserName$2 === "chrome") {
3392
3947
  }
3393
3948
 
3394
3949
  class BaseRtcManager {
3395
- constructor({ selfId, room, emitter, serverSocket, webrtcProvider, features, logger = console }) {
3950
+ constructor({ selfId, room, emitter, serverSocket, webrtcProvider, features }) {
3396
3951
  assert$1.ok(selfId, "selfId is required");
3397
3952
  assert$1.ok(room, "room is required");
3398
3953
  assert$1.ok(emitter && emitter.emit, "emitter is required");
@@ -3407,13 +3962,14 @@ class BaseRtcManager {
3407
3962
  this.peerConnections = {};
3408
3963
  this.localStreams = {};
3409
3964
  this.enabledLocalStreamIds = [];
3965
+ this._screenshareVideoTrackIds = [];
3410
3966
  this._socketListenerDeregisterFunctions = [];
3411
3967
  this._localStreamDeregisterFunction = null;
3412
3968
  this._emitter = emitter;
3413
3969
  this._serverSocket = serverSocket;
3414
3970
  this._webrtcProvider = webrtcProvider;
3415
3971
  this._features = features || {};
3416
- this._logger = logger;
3972
+ this._isAudioOnlyMode = false;
3417
3973
 
3418
3974
  this.offerOptions = { offerToReceiveAudio: true, offerToReceiveVideo: true };
3419
3975
  this._pendingActionsForConnectedPeerConnections = [];
@@ -3432,6 +3988,8 @@ class BaseRtcManager {
3432
3988
  iceServers: iceServers.iceServers || [],
3433
3989
  mediaserverConfigTtlSeconds,
3434
3990
  });
3991
+
3992
+ this.totalSessionsCreated = 0;
3435
3993
  }
3436
3994
 
3437
3995
  numberOfPeerconnections() {
@@ -3482,7 +4040,7 @@ class BaseRtcManager {
3482
4040
  receiver.playoutDelayHint = MEDIA_JITTER_BUFFER_TARGET / 1000; // seconds
3483
4041
  });
3484
4042
  } catch (error) {
3485
- this._logger.error("Error during setting jitter buffer target:", error);
4043
+ logger$5.error("Error during setting jitter buffer target:", error);
3486
4044
  }
3487
4045
  }
3488
4046
 
@@ -3537,6 +4095,8 @@ class BaseRtcManager {
3537
4095
  : MAXIMUM_TURN_BANDWIDTH,
3538
4096
  deprioritizeH264Encoding,
3539
4097
  });
4098
+
4099
+ this.totalSessionsCreated++;
3540
4100
  }
3541
4101
  return session;
3542
4102
  }
@@ -3630,6 +4190,8 @@ class BaseRtcManager {
3630
4190
  clientId,
3631
4191
  });
3632
4192
 
4193
+ setTimeout(() => this._emit(rtcManagerEvents.NEW_PC), 0);
4194
+
3633
4195
  pc.ontrack = (event) => {
3634
4196
  const stream = event.streams[0];
3635
4197
  if (stream.id === "default" && stream.getAudioTracks().length === 0) {
@@ -3691,6 +4253,11 @@ class BaseRtcManager {
3691
4253
  this.maybeRestrictRelayBandwidth(session);
3692
4254
  }
3693
4255
  }
4256
+
4257
+ if (this._isAudioOnlyMode) {
4258
+ session.setAudioOnly(true, this._screenshareVideoTrackIds);
4259
+ }
4260
+
3694
4261
  session.registerConnected();
3695
4262
  break;
3696
4263
  case "disconnected":
@@ -3794,7 +4361,7 @@ class BaseRtcManager {
3794
4361
  _cleanup(peerConnectionId) {
3795
4362
  const session = this._getSession(peerConnectionId);
3796
4363
  if (!session) {
3797
- this._logger.warn("No RTCPeerConnection in RTCManager.disconnect()", peerConnectionId);
4364
+ logger$5.warn("No RTCPeerConnection in RTCManager.disconnect()", peerConnectionId);
3798
4365
  return;
3799
4366
  }
3800
4367
  session.close();
@@ -3824,10 +4391,10 @@ class BaseRtcManager {
3824
4391
  const promises = [];
3825
4392
  this._forEachPeerConnection((session) => {
3826
4393
  if (!session.hasConnectedPeerConnection()) {
3827
- this._logger.log("Session doesn't have a connected PeerConnection, adding pending action!");
4394
+ logger$5.info("Session doesn't have a connected PeerConnection, adding pending action!");
3828
4395
  const pendingActions = this._pendingActionsForConnectedPeerConnections;
3829
4396
  if (!pendingActions) {
3830
- this._logger.warn(
4397
+ logger$5.warn(
3831
4398
  `No pending action is created to repalce track, because the pending actions array is null`
3832
4399
  );
3833
4400
  return;
@@ -3836,7 +4403,7 @@ class BaseRtcManager {
3836
4403
  const action = () => {
3837
4404
  const replacedTrackPromise = session.replaceTrack(oldTrack, newTrack);
3838
4405
  if (!replacedTrackPromise) {
3839
- this._logger.error("replaceTrack returned false!");
4406
+ logger$5.error("replaceTrack returned false!");
3840
4407
  reject(`ReplaceTrack returned false`);
3841
4408
  return;
3842
4409
  }
@@ -3849,7 +4416,7 @@ class BaseRtcManager {
3849
4416
  }
3850
4417
  const replacedTrackResult = session.replaceTrack(oldTrack, newTrack);
3851
4418
  if (!replacedTrackResult) {
3852
- this._logger.error("replaceTrack returned false!");
4419
+ logger$5.error("replaceTrack returned false!");
3853
4420
  return;
3854
4421
  }
3855
4422
  promises.push(replacedTrackResult);
@@ -3919,6 +4486,7 @@ class BaseRtcManager {
3919
4486
  }
3920
4487
 
3921
4488
  // at this point it is clearly a screensharing stream.
4489
+ this._screenshareVideoTrackIds.push(stream.getVideoTracks()[0].id);
3922
4490
  this._shareScreen(streamId, stream);
3923
4491
  return;
3924
4492
  }
@@ -4017,7 +4585,7 @@ class BaseRtcManager {
4017
4585
 
4018
4586
  this._serverSocket.on(PROTOCOL_RESPONSES.MEDIASERVER_CONFIG, (data) => {
4019
4587
  if (data.error) {
4020
- this._logger.warn("FETCH_MEDIASERVER_CONFIG failed:", data.error);
4588
+ logger$5.warn("FETCH_MEDIASERVER_CONFIG failed:", data.error);
4021
4589
  return;
4022
4590
  }
4023
4591
  this._updateAndScheduleMediaServersRefresh(data);
@@ -4030,7 +4598,7 @@ class BaseRtcManager {
4030
4598
  this._serverSocket.on(RELAY_MESSAGES.ICE_CANDIDATE, (data) => {
4031
4599
  const session = this._getSession(data.clientId);
4032
4600
  if (!session) {
4033
- this._logger.warn("No RTCPeerConnection on ICE_CANDIDATE", data);
4601
+ logger$5.warn("No RTCPeerConnection on ICE_CANDIDATE", data);
4034
4602
  return;
4035
4603
  }
4036
4604
  session.addIceCandidate(data.message);
@@ -4039,7 +4607,7 @@ class BaseRtcManager {
4039
4607
  this._serverSocket.on(RELAY_MESSAGES.ICE_END_OF_CANDIDATES, (data) => {
4040
4608
  const session = this._getSession(data.clientId);
4041
4609
  if (!session) {
4042
- this._logger.warn("No RTCPeerConnection on ICE_END_OF_CANDIDATES", data);
4610
+ logger$5.warn("No RTCPeerConnection on ICE_END_OF_CANDIDATES", data);
4043
4611
  return;
4044
4612
  }
4045
4613
  session.addIceCandidate(null);
@@ -4049,7 +4617,7 @@ class BaseRtcManager {
4049
4617
  this._serverSocket.on(RELAY_MESSAGES.SDP_OFFER, (data) => {
4050
4618
  const session = this._getSession(data.clientId);
4051
4619
  if (!session) {
4052
- this._logger.warn("No RTCPeerConnection on SDP_OFFER", data);
4620
+ logger$5.warn("No RTCPeerConnection on SDP_OFFER", data);
4053
4621
  return;
4054
4622
  }
4055
4623
  const offer = this._transformIncomingSdp(data.message, session.pc);
@@ -4065,12 +4633,35 @@ class BaseRtcManager {
4065
4633
  this._serverSocket.on(RELAY_MESSAGES.SDP_ANSWER, (data) => {
4066
4634
  const session = this._getSession(data.clientId);
4067
4635
  if (!session) {
4068
- this._logger.warn("No RTCPeerConnection on SDP_ANSWER", data);
4636
+ logger$5.warn("No RTCPeerConnection on SDP_ANSWER", data);
4069
4637
  return;
4070
4638
  }
4071
4639
  const answer = this._transformIncomingSdp(data.message, session.pc);
4072
4640
  session.handleAnswer(answer);
4073
4641
  }),
4642
+
4643
+ // if this is a reconnect to signal-server during screen-share we must let signal-server know
4644
+ this._serverSocket.on(PROTOCOL_RESPONSES.ROOM_JOINED, ({ room: { sfuServer: isSfu } }) => {
4645
+ if (isSfu || !this._wasScreenSharing) return;
4646
+
4647
+ const screenShareStreamId = Object.keys(this.localStreams).find((id) => id !== CAMERA_STREAM_ID$1);
4648
+ if (!screenShareStreamId) {
4649
+ return;
4650
+ }
4651
+
4652
+ const screenshareStream = this.localStreams[screenShareStreamId];
4653
+ if (!screenshareStream) {
4654
+ logger$5.warn("screenshare stream %s not found", screenShareStreamId);
4655
+ return;
4656
+ }
4657
+
4658
+ const hasAudioTrack = screenshareStream.getAudioTracks().length > 0;
4659
+
4660
+ this._emitServerEvent(PROTOCOL_REQUESTS.START_SCREENSHARE, {
4661
+ streamId: screenShareStreamId,
4662
+ hasAudioTrack,
4663
+ });
4664
+ }),
4074
4665
  ];
4075
4666
  }
4076
4667
 
@@ -4104,6 +4695,27 @@ class BaseRtcManager {
4104
4695
  track.removeEventListener("ended", this._audioTrackOnEnded);
4105
4696
  }
4106
4697
 
4698
+ setAudioOnly(audioOnly) {
4699
+ this._isAudioOnlyMode = audioOnly;
4700
+
4701
+ this._forEachPeerConnection((session) => {
4702
+ if (session.hasConnectedPeerConnection()) {
4703
+ this._withForcedRenegotiation(session, () =>
4704
+ session.setAudioOnly(this._isAudioOnlyMode, this._screenshareVideoTrackIds)
4705
+ );
4706
+ }
4707
+ });
4708
+ }
4709
+
4710
+ setRemoteScreenshareVideoTrackIds(remoteScreenshareVideoTrackIds = []) {
4711
+ const localScreenshareStream = this._getFirstLocalNonCameraStream();
4712
+
4713
+ this._screenshareVideoTrackIds = [
4714
+ ...(localScreenshareStream?.track ? [localScreenshareStream.track.id] : []),
4715
+ ...remoteScreenshareVideoTrackIds,
4716
+ ];
4717
+ }
4718
+
4107
4719
  setRoomSessionId(roomSessionId) {
4108
4720
  this._roomSessionId = roomSessionId;
4109
4721
  }
@@ -4137,6 +4749,54 @@ function getOptimalBitrate(width, height, frameRate) {
4137
4749
  return targetBitrate;
4138
4750
  }
4139
4751
 
4752
+ // taken from https://github.com/sindresorhus/ip-regex ^5.0.0
4753
+ // inlined because it's import caused errors in browser-sdk when running tests
4754
+ const word = "[a-fA-F\\d:]";
4755
+
4756
+ const boundry = (options) =>
4757
+ options && options.includeBoundaries ? `(?:(?<=\\s|^)(?=${word})|(?<=${word})(?=\\s|$))` : "";
4758
+
4759
+ 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}";
4760
+
4761
+ const v6segment = "[a-fA-F\\d]{1,4}";
4762
+
4763
+ const v6 = `
4764
+ (?:
4765
+ (?:${v6segment}:){7}(?:${v6segment}|:)| // 1:2:3:4:5:6:7:: 1:2:3:4:5:6:7:8
4766
+ (?:${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
4767
+ (?:${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
4768
+ (?:${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
4769
+ (?:${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
4770
+ (?:${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
4771
+ (?:${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
4772
+ (?::(?:(?::${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
4773
+ )(?:%[0-9a-zA-Z]{1,})? // %eth0 %1
4774
+ `
4775
+ .replace(/\s*\/\/.*$/gm, "")
4776
+ .replace(/\n/g, "")
4777
+ .trim();
4778
+
4779
+ // Pre-compile only the exact regexes because adding a global flag make regexes stateful
4780
+ const v46Exact = new RegExp(`(?:^${v4}$)|(?:^${v6}$)`);
4781
+ const v4exact = new RegExp(`^${v4}$`);
4782
+ const v6exact = new RegExp(`^${v6}$`);
4783
+
4784
+ const ipRegex = (options) =>
4785
+ options && options.exact
4786
+ ? v46Exact
4787
+ : new RegExp(
4788
+ `(?:${boundry(options)}${v4}${boundry(options)})|(?:${boundry(options)}${v6}${boundry(options)})`,
4789
+ "g"
4790
+ );
4791
+
4792
+ ipRegex.v4 = (options) =>
4793
+ options && options.exact ? v4exact : new RegExp(`${boundry(options)}${v4}${boundry(options)}`, "g");
4794
+ ipRegex.v6 = (options) =>
4795
+ options && options.exact ? v6exact : new RegExp(`${boundry(options)}${v6}${boundry(options)}`, "g");
4796
+
4797
+ const logger$4 = new Logger();
4798
+
4799
+ const ICE_PUBLIC_IP_GATHERING_TIMEOUT = 3 * 1000;
4140
4800
  const CAMERA_STREAM_ID = RtcStream.getCameraId();
4141
4801
  const browserName$1 = adapter.browserDetails.browser;
4142
4802
 
@@ -4147,7 +4807,7 @@ class P2pRtcManager extends BaseRtcManager {
4147
4807
  let session = this._getSession(clientId);
4148
4808
  let bandwidth = (session && session.bandwidth) || 0;
4149
4809
  if (session) {
4150
- this._logger.warn("Replacing peer session", clientId);
4810
+ logger$4.warn("Replacing peer session", clientId);
4151
4811
  this._cleanup(clientId);
4152
4812
  } else {
4153
4813
  bandwidth = this._changeBandwidthForAllClients(true);
@@ -4168,6 +4828,14 @@ class P2pRtcManager extends BaseRtcManager {
4168
4828
  // clean up some helpers.
4169
4829
  session.wasEverConnected = false;
4170
4830
  session.relayCandidateSeen = false;
4831
+ session.serverReflexiveCandidateSeen = false;
4832
+ session.publicHostCandidateSeen = false;
4833
+ session.ipv6HostCandidateSeen = false;
4834
+ this.ipv6HostCandidateTeredoSeen = false;
4835
+ this.ipv6HostCandidate6to4Seen = false;
4836
+ this.mdnsHostCandidateSeen = false;
4837
+
4838
+ this._emit(rtcManagerEvents.ICE_RESTART);
4171
4839
 
4172
4840
  this._negotiatePeerConnection(
4173
4841
  clientId,
@@ -4222,13 +4890,13 @@ class P2pRtcManager extends BaseRtcManager {
4222
4890
  videoTransceiver.setCodecPreferences(capabilities.codecs);
4223
4891
  });
4224
4892
  } catch (error) {
4225
- this._logger.error("Error during setting setCodecPreferences:", error);
4893
+ logger$4.error("Error during setting setCodecPreferences:", error);
4226
4894
  }
4227
4895
  }
4228
4896
 
4229
4897
  _negotiatePeerConnection(clientId, session, constraints) {
4230
4898
  if (!session) {
4231
- this._logger.warn("No RTCPeerConnection in negotiatePeerConnection()", clientId);
4899
+ logger$4.warn("No RTCPeerConnection in negotiatePeerConnection()", clientId);
4232
4900
  return;
4233
4901
  }
4234
4902
  const pc = session.pc;
@@ -4270,11 +4938,24 @@ class P2pRtcManager extends BaseRtcManager {
4270
4938
  return;
4271
4939
  }
4272
4940
  pc.setLocalDescription(offer).catch((e) => {
4273
- this._logger.warn("RTCPeerConnection.setLocalDescription() failed with local offer", e);
4941
+ logger$4.warn("RTCPeerConnection.setLocalDescription() failed with local offer", e);
4942
+
4943
+ // we failed to create a valid offer so try having the other side create it, without looping
4944
+ if (this._features.reverseOfferOnFailure) {
4945
+ if (!this._lastReverseDirectionAttemptByClientId)
4946
+ this._lastReverseDirectionAttemptByClientId = {};
4947
+ if (
4948
+ !this._lastReverseDirectionAttemptByClientId[clientId] ||
4949
+ this._lastReverseDirectionAttemptByClientId[clientId] < Date.now() - 10000
4950
+ ) {
4951
+ this.acceptNewStream({ clientId, streamId: clientId, shouldAddLocalVideo: true });
4952
+ this._lastReverseDirectionAttemptByClientId[clientId] = Date.now();
4953
+ }
4954
+ }
4274
4955
  });
4275
4956
  })
4276
4957
  .catch((e) => {
4277
- this._logger.warn("RTCPeerConnection.createOffer() failed to create local offer", e);
4958
+ logger$4.warn("RTCPeerConnection.createOffer() failed to create local offer", e);
4278
4959
  });
4279
4960
  }
4280
4961
 
@@ -4310,7 +4991,7 @@ class P2pRtcManager extends BaseRtcManager {
4310
4991
  let bandwidth = this._features.bandwidth
4311
4992
  ? parseInt(this._features.bandwidth, 10)
4312
4993
  : {
4313
- 1: this._features.cap2pBitrate ? 1000 : 0,
4994
+ 1: 0,
4314
4995
  2: this._features.highP2PBandwidth ? 768 : 384,
4315
4996
  3: this._features.highP2PBandwidth ? 512 : 256,
4316
4997
  4: 192,
@@ -4376,9 +5057,73 @@ class P2pRtcManager extends BaseRtcManager {
4376
5057
  pc.addTrack(this._stoppedVideoTrack, localCameraStream);
4377
5058
  }
4378
5059
 
5060
+ pc.onicegatheringstatechange = (event) => {
5061
+ const connection = event.target;
5062
+
5063
+ switch (connection.iceGatheringState) {
5064
+ case "gathering":
5065
+ if (this.icePublicIPGatheringTimeoutID) clearTimeout(this.icePublicIPGatheringTimeoutID);
5066
+ this.icePublicIPGatheringTimeoutID = setTimeout(() => {
5067
+ if (
5068
+ !session.publicHostCandidateSeen &&
5069
+ !session.relayCandidateSeen &&
5070
+ !session.serverReflexiveCandidateSeen
5071
+ ) {
5072
+ this._emit(rtcManagerEvents.ICE_NO_PUBLIC_IP_GATHERED_3SEC);
5073
+ }
5074
+ }, ICE_PUBLIC_IP_GATHERING_TIMEOUT);
5075
+ break;
5076
+ case "complete":
5077
+ if (this.icePublicIPGatheringTimeoutID) clearTimeout(this.icePublicIPGatheringTimeoutID);
5078
+ this.icePublicIPGatheringTimeoutID = undefined;
5079
+ break;
5080
+ }
5081
+ };
5082
+
4379
5083
  pc.onicecandidate = (event) => {
4380
5084
  if (event.candidate) {
4381
- session.relayCandidateSeen = session.relayCandidateSeen || event.candidate.type === "relay";
5085
+ switch (event.candidate?.type) {
5086
+ case "host":
5087
+ const address = event?.candidate?.address;
5088
+ try {
5089
+ if (ipRegex.v4({ exact: true }).test(address)) {
5090
+ const ipv4 = checkIp(address);
5091
+ if (ipv4.isPublicIp) session.publicHostCandidateSeen = true;
5092
+ } else if (ipRegex.v6({ exact: true }).test(address.replace(/^\[(.*)\]/, "$1"))) {
5093
+ const ipv6 = new Address6(address.replace(/^\[(.*)\]/, "$1"));
5094
+ session.ipv6HostCandidateSeen = true;
5095
+
5096
+ if (ipv6.getScope() === "Global") {
5097
+ session.publicHostCandidateSeen = true;
5098
+ }
5099
+ if (ipv6.isTeredo()) {
5100
+ session.ipv6HostCandidateTeredoSeen = true;
5101
+ }
5102
+ if (ipv6.is6to4()) {
5103
+ session.ipv6HostCandidate6to4Seen = true;
5104
+ }
5105
+ } else {
5106
+ const uuidv4 = address.replace(/.local/, "");
5107
+ if (uuidv4 && validate(uuidv4, 4)) {
5108
+ session.mdnsHostCandidateSeen = true;
5109
+ }
5110
+ }
5111
+ } catch (error) {
5112
+ logger$4.info("Error during parsing candidates! Error: ", { error });
5113
+ }
5114
+ break;
5115
+ case "srflx":
5116
+ if (!session.serverReflexiveCandidateSeen) {
5117
+ session.serverReflexiveCandidateSeen = true;
5118
+ }
5119
+ break;
5120
+ case "relay":
5121
+ case "relayed":
5122
+ if (!session.relayCandidateSeen) {
5123
+ session.relayCandidateSeen = true;
5124
+ }
5125
+ break;
5126
+ }
4382
5127
  this._emitServerEvent(RELAY_MESSAGES.ICE_CANDIDATE, {
4383
5128
  receiverId: clientId,
4384
5129
  message: event.candidate,
@@ -4387,6 +5132,20 @@ class P2pRtcManager extends BaseRtcManager {
4387
5132
  this._emitServerEvent(RELAY_MESSAGES.ICE_END_OF_CANDIDATES, {
4388
5133
  receiverId: clientId,
4389
5134
  });
5135
+ if (
5136
+ !session.publicHostCandidateSeen &&
5137
+ !session.relayCandidateSeen &&
5138
+ !session.serverReflexiveCandidateSeen
5139
+ ) {
5140
+ this._emit(rtcManagerEvents.ICE_NO_PUBLIC_IP_GATHERED);
5141
+ }
5142
+ if (session.ipv6HostCandidateSeen) {
5143
+ this._emit(rtcManagerEvents.ICE_IPV6_SEEN, {
5144
+ teredoSeen: session.ipv6HostCandidateTeredoSeen,
5145
+ sixtofourSeen: session.ipv6HostCandidate6to4Seen,
5146
+ });
5147
+ }
5148
+ if (session.mdnsHostCandidateSeen) this._emit(rtcManagerEvents.ICE_MDNS_SEEN);
4390
5149
  }
4391
5150
  };
4392
5151
 
@@ -4413,7 +5172,7 @@ class P2pRtcManager extends BaseRtcManager {
4413
5172
  if (session) {
4414
5173
  // this will happen on a signal-server reconnect
4415
5174
  // before we tried an ice-restart here, now we recreate the session/pc
4416
- this._logger.warn("Replacing peer session", clientId);
5175
+ logger$4.warn("Replacing peer session", clientId);
4417
5176
  this._cleanup(clientId); // will cleanup and delete session/pc
4418
5177
  } else {
4419
5178
  // we adjust bandwidth based on number of sessions/pcs
@@ -4511,14 +5270,20 @@ class P2pRtcManager extends BaseRtcManager {
4511
5270
  streamId,
4512
5271
  hasAudioTrack: !!stream.getAudioTracks().length,
4513
5272
  });
5273
+ this._wasScreenSharing = true;
4514
5274
  this._addStreamToPeerConnections(stream);
4515
5275
  }
4516
5276
 
4517
5277
  removeStream(streamId, stream, requestedByClientId) {
4518
5278
  super.removeStream(streamId, stream);
4519
5279
  this._removeStreamFromPeerConnections(stream);
5280
+ this._wasScreenSharing = false;
4520
5281
  this._emitServerEvent(PROTOCOL_REQUESTS.STOP_SCREENSHARE, { streamId, requestedByClientId });
4521
5282
  }
5283
+
5284
+ hasClient(clientId) {
5285
+ return Object.keys(this.peerConnections).includes(clientId);
5286
+ }
4522
5287
  }
4523
5288
 
4524
5289
  class SfuV2Parser {
@@ -4643,12 +5408,13 @@ class SfuV2Parser {
4643
5408
  }
4644
5409
  }
4645
5410
 
4646
- class VegaConnection extends EventEmitter {
4647
- constructor(wsUrl, logger, protocol = "whereby-sfu#v4") {
5411
+ const logger$3 = new Logger();
5412
+
5413
+ class VegaConnection extends EventEmitter$1 {
5414
+ constructor(wsUrl, protocol = "whereby-sfu#v4") {
4648
5415
  super();
4649
5416
 
4650
5417
  this.wsUrl = wsUrl;
4651
- this.logger = logger;
4652
5418
  this.protocol = protocol;
4653
5419
 
4654
5420
  // This is the map of sent requests that are waiting for a response
@@ -4681,7 +5447,7 @@ class VegaConnection extends EventEmitter {
4681
5447
  }
4682
5448
 
4683
5449
  _onOpen() {
4684
- this.logger.log("VegaConnectionManager: Connected");
5450
+ logger$3.info("Connected");
4685
5451
 
4686
5452
  this.emit("open");
4687
5453
  }
@@ -4689,7 +5455,7 @@ class VegaConnection extends EventEmitter {
4689
5455
  _onMessage(event) {
4690
5456
  const socketMessage = SfuV2Parser.parse(event.data);
4691
5457
 
4692
- this.logger.log("VegaConnectionManager: Received message", socketMessage);
5458
+ logger$3.info("Received message", socketMessage);
4693
5459
 
4694
5460
  if (socketMessage?.response) {
4695
5461
  this._handleResponse(socketMessage);
@@ -4699,13 +5465,13 @@ class VegaConnection extends EventEmitter {
4699
5465
  }
4700
5466
 
4701
5467
  _onClose() {
4702
- this.logger.log("VegaConnectionManager: Disconnected");
5468
+ logger$3.info("Disconnected");
4703
5469
 
4704
5470
  this._tearDown();
4705
5471
  }
4706
5472
 
4707
5473
  _onError(error) {
4708
- this.logger.log("VegaConnectionManager: Error", error);
5474
+ logger$3.info("Error", error);
4709
5475
  }
4710
5476
 
4711
5477
  _handleResponse(socketMessage) {
@@ -5061,6 +5827,8 @@ const defaultParams = {
5061
5827
  outFormula: "score", // the out/score sent to SFU
5062
5828
  };
5063
5829
 
5830
+ const logger$2 = new Logger();
5831
+
5064
5832
  function createMicAnalyser({ micTrack, params, onScoreUpdated }) {
5065
5833
  // todo: might need to reuse existing in PWA
5066
5834
  const audioCtx = new AudioContext();
@@ -5086,7 +5854,7 @@ function createMicAnalyser({ micTrack, params, onScoreUpdated }) {
5086
5854
  track = stream.getAudioTracks()[0];
5087
5855
  lastTrackWasOurs = true;
5088
5856
  } catch (ex) {
5089
- console.warn("unable to fetch new track for colocation speaker analysis, using current", ex);
5857
+ logger$2.warn("unable to fetch new track for colocation speaker analysis, using current", ex);
5090
5858
  }
5091
5859
  }
5092
5860
 
@@ -5218,18 +5986,207 @@ const maybeTurnOnly = (transportConfig, features) => {
5218
5986
  }
5219
5987
  };
5220
5988
 
5989
+ const logger$1 = new Logger();
5990
+
5991
+ const MEDIA_QUALITY = Object.freeze({
5992
+ ok: "ok",
5993
+ warning: "warning",
5994
+ critical: "critical",
5995
+ });
5996
+
5997
+ const MONITOR_INTERVAL = 600; // ms
5998
+ const TREND_HORIZON = 3; // number of monitor intervals needed for quality to change
5999
+ const WARNING_SCORE = 9;
6000
+ const CRITICAL_SCORE = 7;
6001
+
6002
+ class VegaMediaQualityMonitor extends EventEmitter {
6003
+ constructor() {
6004
+ super();
6005
+ this._clients = {};
6006
+ this._producers = {};
6007
+ this._startMonitor();
6008
+ }
6009
+
6010
+ close() {
6011
+ clearInterval(this._intervalHandle);
6012
+ delete this._intervalHandle;
6013
+ this._producers = {};
6014
+ this._clients = {};
6015
+ }
6016
+
6017
+ _startMonitor() {
6018
+ this._intervalHandle = setInterval(() => {
6019
+ Object.entries(this._producers).forEach(([clientId, producers]) => {
6020
+ this._evaluateClient(clientId, producers);
6021
+ });
6022
+ }, MONITOR_INTERVAL);
6023
+ }
6024
+
6025
+ _evaluateClient(clientId, producers) {
6026
+ if (!this._clients[clientId]) {
6027
+ this._clients[clientId] = {
6028
+ audio: { currentQuality: MEDIA_QUALITY.ok, trend: [] },
6029
+ video: { currentQuality: MEDIA_QUALITY.ok, trend: [] },
6030
+ };
6031
+ }
6032
+
6033
+ this._evaluateProducer(
6034
+ clientId,
6035
+ Object.values(producers).filter((p) => p.kind === "audio"),
6036
+ "audio"
6037
+ );
6038
+ this._evaluateProducer(
6039
+ clientId,
6040
+ Object.values(producers).filter((p) => p.kind === "video"),
6041
+ "video"
6042
+ );
6043
+ }
6044
+
6045
+ _evaluateProducer(clientId, producers, kind) {
6046
+ if (producers.length === 0) {
6047
+ return;
6048
+ }
6049
+
6050
+ const avgScore = producers.reduce((prev, curr) => prev + curr.score, 0) / producers.length;
6051
+ const newQuality = this._evaluateScore(avgScore);
6052
+ const qualityChanged = this._updateTrend(newQuality, this._clients[clientId][kind]);
6053
+ if (qualityChanged) {
6054
+ this.emit(PROTOCOL_EVENTS.MEDIA_QUALITY_CHANGED, {
6055
+ clientId,
6056
+ kind,
6057
+ quality: newQuality,
6058
+ });
6059
+ }
6060
+ }
6061
+
6062
+ _updateTrend(newQuality, state) {
6063
+ state.trend.push(newQuality);
6064
+ if (state.trend.length > TREND_HORIZON) {
6065
+ state.trend.shift();
6066
+ }
6067
+
6068
+ if (newQuality !== state.currentQuality && state.trend.every((t) => t !== state.currentQuality)) {
6069
+ state.currentQuality = newQuality;
6070
+ return true;
6071
+ } else {
6072
+ return false;
6073
+ }
6074
+ }
6075
+
6076
+ addProducer(clientId, producerId) {
6077
+ if (!clientId || !producerId || !(typeof clientId === "string" && typeof producerId === "string")) {
6078
+ logger$1.warn("Missing clientId or producerId");
6079
+ return;
6080
+ }
6081
+
6082
+ if (!this._producers[clientId]) {
6083
+ this._producers[clientId] = {};
6084
+ }
6085
+
6086
+ this._producers[clientId][producerId] = {};
6087
+ }
6088
+
6089
+ removeProducer(clientId, producerId) {
6090
+ delete this._producers[clientId][producerId];
6091
+
6092
+ if (Object.keys(this._producers[clientId]).length === 0) {
6093
+ delete this._producers[clientId];
6094
+ }
6095
+ }
6096
+
6097
+ addConsumer(clientId, consumerId) {
6098
+ if (!clientId || !consumerId) {
6099
+ logger$1.warn("Missing clientId or consumerId");
6100
+ return;
6101
+ }
6102
+
6103
+ if (!this._producers[clientId]) {
6104
+ this._producers[clientId] = {};
6105
+ }
6106
+
6107
+ this._producers[clientId][consumerId] = {};
6108
+ }
6109
+
6110
+ removeConsumer(clientId, consumerId) {
6111
+ delete this._producers[clientId][consumerId];
6112
+
6113
+ if (Object.keys(this._producers[clientId]).length === 0) {
6114
+ delete this._producers[clientId];
6115
+ }
6116
+ }
6117
+
6118
+ addProducerScore(clientId, producerId, kind, score) {
6119
+ if (
6120
+ !Array.isArray(score) ||
6121
+ score.length === 0 ||
6122
+ score.some((s) => !s || !s.hasOwnProperty("score") || typeof s.score !== "number" || isNaN(s.score))
6123
+ ) {
6124
+ logger$1.warn("Unexpected producer score format");
6125
+ return;
6126
+ }
6127
+ this._producers[clientId][producerId] = { kind, score: this._calcAvgProducerScore(score.map((s) => s.score)) };
6128
+ }
6129
+
6130
+ addConsumerScore(clientId, consumerId, kind, score) {
6131
+ if (!score || !score.hasOwnProperty("producerScores") || !Array.isArray(score.producerScores)) {
6132
+ logger$1.warn("Unexpected consumer score format");
6133
+ return;
6134
+ }
6135
+ this._producers[clientId][consumerId] = { kind, score: this._calcAvgProducerScore(score.producerScores) };
6136
+ }
6137
+
6138
+ _evaluateScore(score) {
6139
+ if (score <= WARNING_SCORE && score > CRITICAL_SCORE) {
6140
+ return MEDIA_QUALITY.warning;
6141
+ } else if (score <= CRITICAL_SCORE && score > 0) {
6142
+ return MEDIA_QUALITY.critical;
6143
+ } else {
6144
+ return MEDIA_QUALITY.ok;
6145
+ }
6146
+ }
6147
+
6148
+ _calcAvgProducerScore(scores) {
6149
+ try {
6150
+ if (!Array.isArray(scores) || scores.length === 0) {
6151
+ return 0;
6152
+ }
6153
+
6154
+ let totalScore = 0;
6155
+ let divisor = 0;
6156
+
6157
+ scores.forEach((score) => {
6158
+ if (score > 0) {
6159
+ totalScore += score;
6160
+ divisor++;
6161
+ }
6162
+ });
6163
+
6164
+ if (totalScore === 0 || divisor === 0) {
6165
+ return 0;
6166
+ } else {
6167
+ return totalScore / divisor;
6168
+ }
6169
+ } catch (error) {
6170
+ logger$1.error(error);
6171
+ return 0;
6172
+ }
6173
+ }
6174
+ }
6175
+
6176
+ const logger = new Logger();
6177
+
5221
6178
  const browserName = adapter.browserDetails.browser;
5222
6179
  let unloading = false;
5223
6180
 
5224
6181
  const RESTARTICE_ERROR_RETRY_THRESHOLD_IN_MS = 3500;
5225
6182
  const RESTARTICE_ERROR_MAX_RETRY_COUNT = 5;
5226
- const OUTBOUND_CAM_OUTBOUND_STREAM_ID = v4();
5227
- const OUTBOUND_SCREEN_OUTBOUND_STREAM_ID = v4();
6183
+ const OUTBOUND_CAM_OUTBOUND_STREAM_ID = v4$1();
6184
+ const OUTBOUND_SCREEN_OUTBOUND_STREAM_ID = v4$1();
5228
6185
 
5229
6186
  if (browserName === "chrome") window.document.addEventListener("beforeunload", () => (unloading = true));
5230
6187
 
5231
6188
  class VegaRtcManager {
5232
- constructor({ selfId, room, emitter, serverSocket, webrtcProvider, features, eventClaim, logger = console }) {
6189
+ constructor({ selfId, room, emitter, serverSocket, webrtcProvider, features, eventClaim, deviceHandlerFactory }) {
5233
6190
  assert$1.ok(selfId, "selfId is required");
5234
6191
  assert$1.ok(room, "room is required");
5235
6192
  assert$1.ok(emitter && emitter.emit, "emitter is required");
@@ -5246,14 +6203,18 @@ class VegaRtcManager {
5246
6203
  this._webrtcProvider = webrtcProvider;
5247
6204
  this._features = features || {};
5248
6205
  this._eventClaim = eventClaim;
5249
- this._logger = logger;
5250
6206
 
5251
6207
  this._vegaConnection = null;
5252
6208
 
5253
6209
  this._micAnalyser = null;
5254
6210
  this._micAnalyserDebugger = null;
5255
6211
 
5256
- this._mediasoupDevice = new Device({ handlerName: getHandler() });
6212
+ if (deviceHandlerFactory) {
6213
+ this._mediasoupDevice = new Device({ handlerFactory: deviceHandlerFactory });
6214
+ } else {
6215
+ this._mediasoupDevice = new Device({ handlerName: getHandler() });
6216
+ }
6217
+
5257
6218
  this._routerRtpCapabilities = null;
5258
6219
 
5259
6220
  this._sendTransport = null;
@@ -5316,6 +6277,11 @@ class VegaRtcManager {
5316
6277
  // Retry if connection closed until disconnectAll called;
5317
6278
  this._reconnect = true;
5318
6279
  this._reconnectTimeOut = null;
6280
+
6281
+ this._qualityMonitor = new VegaMediaQualityMonitor();
6282
+ this._qualityMonitor.on(PROTOCOL_EVENTS.MEDIA_QUALITY_CHANGED, (payload) => {
6283
+ this._emitToPWA(PROTOCOL_EVENTS.MEDIA_QUALITY_CHANGED, payload);
6284
+ });
5319
6285
  }
5320
6286
 
5321
6287
  _updateAndScheduleMediaServersRefresh({ iceServers, sfuServer, mediaserverConfigTtlSeconds }) {
@@ -5348,7 +6314,7 @@ class VegaRtcManager {
5348
6314
 
5349
6315
  this._serverSocket.on(PROTOCOL_RESPONSES.MEDIASERVER_CONFIG, (data) => {
5350
6316
  if (data.error) {
5351
- this._logger.warn("FETCH_MEDIASERVER_CONFIG failed:", data.error);
6317
+ logger.warn("FETCH_MEDIASERVER_CONFIG failed:", data.error);
5352
6318
  return;
5353
6319
  }
5354
6320
  this._updateAndScheduleMediaServersRefresh(data);
@@ -5380,14 +6346,14 @@ class VegaRtcManager {
5380
6346
  const queryString = searchParams.toString();
5381
6347
  const wsUrl = `wss://${host}?${queryString}`;
5382
6348
 
5383
- this._vegaConnection = new VegaConnection(wsUrl, this._logger);
6349
+ this._vegaConnection = new VegaConnection(wsUrl);
5384
6350
  this._vegaConnection.on("open", () => this._join());
5385
6351
  this._vegaConnection.on("close", () => this._onClose());
5386
6352
  this._vegaConnection.on("message", (message) => this._onMessage(message));
5387
6353
  }
5388
6354
 
5389
6355
  _onClose() {
5390
- this._logger.debug("_onClose()");
6356
+ logger.info("_onClose()");
5391
6357
 
5392
6358
  // These will clean up any and all producers/consumers
5393
6359
  this._sendTransport?.close();
@@ -5400,10 +6366,14 @@ class VegaRtcManager {
5400
6366
  if (this._reconnect) {
5401
6367
  this._reconnectTimeOut = setTimeout(() => this._connect(), 1000);
5402
6368
  }
6369
+
6370
+ this._qualityMonitor.close();
6371
+ this._emitToPWA(rtcManagerEvents.SFU_CONNECTION_CLOSED);
5403
6372
  }
5404
6373
 
5405
6374
  async _join() {
5406
- this._logger.debug("join()");
6375
+ logger.info("join()");
6376
+ this._emitToPWA(rtcManagerEvents.SFU_CONNECTION_OPEN);
5407
6377
 
5408
6378
  try {
5409
6379
  // We need to always do this, as this triggers the join logic on the SFU
@@ -5437,7 +6407,7 @@ class VegaRtcManager {
5437
6407
 
5438
6408
  await Promise.all(mediaPromises);
5439
6409
  } catch (error) {
5440
- this._logger.error("_join() [error:%o]", error);
6410
+ logger.error("_join() [error:%o]", error);
5441
6411
  // TODO: handle this error, rejoin?
5442
6412
  }
5443
6413
  }
@@ -5468,7 +6438,7 @@ class VegaRtcManager {
5468
6438
 
5469
6439
  const transport = this._mediasoupDevice[creator](transportOptions);
5470
6440
  const onConnectionStateListener = async (connectionState) => {
5471
- this._logger.debug(`Transport ConnectionStateChanged ${connectionState}`);
6441
+ logger.info(`Transport ConnectionStateChanged ${connectionState}`);
5472
6442
  if (connectionState !== "disconnected" && connectionState !== "failed") {
5473
6443
  return;
5474
6444
  }
@@ -5542,17 +6512,17 @@ class VegaRtcManager {
5542
6512
 
5543
6513
  async _restartIce(transport, retried = 0) {
5544
6514
  if (!transport || !("closed" in transport) || !("connectionState" in transport)) {
5545
- this._logger.debug("_restartIce: No transport or property closed or connectionState!");
6515
+ logger.info("_restartIce: No transport or property closed or connectionState!");
5546
6516
  return;
5547
6517
  }
5548
6518
 
5549
6519
  if (transport.closed) {
5550
- this._logger.debug("_restartIce: Transport is closed!");
6520
+ logger.info("_restartIce: Transport is closed!");
5551
6521
  return;
5552
6522
  }
5553
6523
 
5554
6524
  if (transport.connectionState !== "disconnected" && transport.connectionState !== "failed") {
5555
- this._logger.debug("_restartIce: Connection is healthy ICE restart no loneger needed!");
6525
+ logger.info("_restartIce: Connection is healthy ICE restart no loneger needed!");
5556
6526
  return;
5557
6527
  }
5558
6528
 
@@ -5565,24 +6535,24 @@ class VegaRtcManager {
5565
6535
  transport.appData.iceRestartStarted = now;
5566
6536
 
5567
6537
  if (RESTARTICE_ERROR_MAX_RETRY_COUNT <= retried) {
5568
- this._logger.debug("_restartIce: Reached restart ICE maximum retry count!");
6538
+ logger.info("_restartIce: Reached restart ICE maximum retry count!");
5569
6539
  return;
5570
6540
  }
5571
6541
 
5572
6542
  if (!this._vegaConnection) {
5573
- this._logger.debug(`_restartIce: Connection is undefined`);
6543
+ logger.info(`_restartIce: Connection is undefined`);
5574
6544
  return;
5575
6545
  }
5576
6546
  const { iceParameters } = await this._vegaConnection.request("restartIce", { transportId: transport.id });
5577
6547
 
5578
- this._logger.debug("_restartIce: ICE restart iceParameters received from SFU: ", iceParameters);
6548
+ logger.info("_restartIce: ICE restart iceParameters received from SFU: ", iceParameters);
5579
6549
  const error = await transport
5580
6550
  .restartIce({ iceParameters })
5581
6551
  .then(() => null)
5582
6552
  .catch((err) => err);
5583
6553
 
5584
6554
  if (error) {
5585
- this._logger.error(`_restartIce: ICE restart failed: ${error}`);
6555
+ logger.error(`_restartIce: ICE restart failed: ${error}`);
5586
6556
  switch (error.message) {
5587
6557
  case "missing transportId":
5588
6558
  case "no such transport":
@@ -5618,7 +6588,7 @@ class VegaRtcManager {
5618
6588
  }
5619
6589
 
5620
6590
  async _internalSendMic() {
5621
- this._logger.debug("_internalSendMic()");
6591
+ logger.info("_internalSendMic()");
5622
6592
 
5623
6593
  this._micProducerPromise = (async () => {
5624
6594
  try {
@@ -5647,21 +6617,23 @@ class VegaRtcManager {
5647
6617
  currentPaused ? producer.pause() : producer.resume();
5648
6618
 
5649
6619
  this._micProducer = producer;
6620
+ this._qualityMonitor.addProducer(this._selfId, producer.id);
5650
6621
 
5651
6622
  producer.observer.once("close", () => {
5652
- this._logger.debug('micProducer "close" event');
6623
+ logger.info('micProducer "close" event');
5653
6624
 
5654
6625
  if (producer.appData.localClosed)
5655
6626
  this._vegaConnection?.message("closeProducers", { producerIds: [producer.id] });
5656
6627
 
5657
6628
  this._micProducer = null;
5658
6629
  this._micProducerPromise = null;
6630
+ this._qualityMonitor.removeProducer(this._selfId, producer.id);
5659
6631
  });
5660
6632
 
5661
6633
  if (this._micTrack !== this._micProducer.track) await this._replaceMicTrack();
5662
6634
  if (this._micPaused !== this._micProducer.paused) this._pauseResumeMic();
5663
6635
  } catch (error) {
5664
- this._logger.error("micProducer failed:%o", error);
6636
+ logger.error("micProducer failed:%o", error);
5665
6637
  } finally {
5666
6638
  this._micProducerPromise = null;
5667
6639
 
@@ -5675,7 +6647,7 @@ class VegaRtcManager {
5675
6647
  }
5676
6648
 
5677
6649
  async _internalSetupMicScore() {
5678
- this._logger.debug("_internalSetupMicScore()");
6650
+ logger.info("_internalSetupMicScore()");
5679
6651
 
5680
6652
  this._micScoreProducerPromise = (async () => {
5681
6653
  try {
@@ -5698,7 +6670,7 @@ class VegaRtcManager {
5698
6670
  this._micScoreProducer = producer;
5699
6671
 
5700
6672
  producer.observer.once("close", () => {
5701
- this._logger.debug('micScoreProducer "close" event');
6673
+ logger.info('micScoreProducer "close" event');
5702
6674
  if (producer.appData.localClosed) {
5703
6675
  this._vegaConnection?.message("closeDataProducers", { dataProducerIds: [producer.id] });
5704
6676
  }
@@ -5707,7 +6679,7 @@ class VegaRtcManager {
5707
6679
  this._micScoreProducerPromise = null;
5708
6680
  });
5709
6681
  } catch (error) {
5710
- this._logger.error("micScoreProducer failed:%o", error);
6682
+ logger.error("micScoreProducer failed:%o", error);
5711
6683
  } finally {
5712
6684
  this._micScoreProducerPromise = null;
5713
6685
 
@@ -5725,7 +6697,7 @@ class VegaRtcManager {
5725
6697
  }
5726
6698
 
5727
6699
  async _replaceMicTrack() {
5728
- this._logger.debug("_replaceMicTrack()");
6700
+ logger.info("_replaceMicTrack()");
5729
6701
 
5730
6702
  if (!this._micTrack || !this._micProducer || this._micProducer.closed) return;
5731
6703
 
@@ -5741,7 +6713,7 @@ class VegaRtcManager {
5741
6713
  }
5742
6714
 
5743
6715
  _pauseResumeMic() {
5744
- this._logger.debug("_pauseResumeMic()");
6716
+ logger.info("_pauseResumeMic()");
5745
6717
 
5746
6718
  if (!this._micProducer || this._micProducer.closed) return;
5747
6719
 
@@ -5768,7 +6740,7 @@ class VegaRtcManager {
5768
6740
  }
5769
6741
 
5770
6742
  async _sendMic(track) {
5771
- this._logger.debug("_sendMic() [track:%o]", track);
6743
+ logger.info("_sendMic() [track:%o]", track);
5772
6744
 
5773
6745
  this._micTrack = track;
5774
6746
 
@@ -5800,7 +6772,7 @@ class VegaRtcManager {
5800
6772
  try {
5801
6773
  this._micScoreProducer.send(JSON.stringify({ score }));
5802
6774
  } catch (ex) {
5803
- console.error("_sendMicScore failed [error:%o]", ex);
6775
+ logger.error("_sendMicScore failed [error:%o]", ex);
5804
6776
  }
5805
6777
  return;
5806
6778
  }
@@ -5812,7 +6784,7 @@ class VegaRtcManager {
5812
6784
  }
5813
6785
 
5814
6786
  async _internalSendWebcam() {
5815
- this._logger.debug("_internalSendWebcam()");
6787
+ logger.info("_internalSendWebcam()");
5816
6788
 
5817
6789
  this._webcamProducerPromise = (async () => {
5818
6790
  try {
@@ -5841,21 +6813,23 @@ class VegaRtcManager {
5841
6813
  currentPaused ? producer.pause() : producer.resume();
5842
6814
 
5843
6815
  this._webcamProducer = producer;
6816
+ this._qualityMonitor.addProducer(this._selfId, producer.id);
5844
6817
  producer.observer.once("close", () => {
5845
- this._logger.debug('webcamProducer "close" event');
6818
+ logger.info('webcamProducer "close" event');
5846
6819
 
5847
6820
  if (producer.appData.localClosed)
5848
6821
  this._vegaConnection?.message("closeProducers", { producerIds: [producer.id] });
5849
6822
 
5850
6823
  this._webcamProducer = null;
5851
6824
  this._webcamProducerPromise = null;
6825
+ this._qualityMonitor.removeProducer(this._selfId, producer.id);
5852
6826
  });
5853
6827
 
5854
6828
  // Has someone replaced the track?
5855
6829
  if (this._webcamTrack !== this._webcamProducer.track) await this._replaceWebcamTrack();
5856
6830
  if (this._webcamPaused !== this._webcamProducer.paused) this._pauseResumeWebcam();
5857
6831
  } catch (error) {
5858
- this._logger.error("webcamProducer failed:%o", error);
6832
+ logger.error("webcamProducer failed:%o", error);
5859
6833
  } finally {
5860
6834
  this._webcamProducerPromise = null;
5861
6835
 
@@ -5869,7 +6843,7 @@ class VegaRtcManager {
5869
6843
  }
5870
6844
 
5871
6845
  async _replaceWebcamTrack() {
5872
- this._logger.debug("_replaceWebcamTrack()");
6846
+ logger.info("_replaceWebcamTrack()");
5873
6847
 
5874
6848
  if (!this._webcamTrack || !this._webcamProducer || this._webcamProducer.closed) return;
5875
6849
 
@@ -5880,7 +6854,7 @@ class VegaRtcManager {
5880
6854
  }
5881
6855
 
5882
6856
  _pauseResumeWebcam() {
5883
- this._logger.debug("_pauseResumeWebcam()");
6857
+ logger.info("_pauseResumeWebcam()");
5884
6858
 
5885
6859
  if (!this._webcamProducer || this._webcamProducer.closed) return;
5886
6860
 
@@ -5904,7 +6878,7 @@ class VegaRtcManager {
5904
6878
  }
5905
6879
 
5906
6880
  async _sendWebcam(track) {
5907
- this._logger.debug("_sendWebcam() [track:%o]", track);
6881
+ logger.info("_sendWebcam() [track:%o]", track);
5908
6882
 
5909
6883
  this._webcamTrack = track;
5910
6884
 
@@ -5918,7 +6892,7 @@ class VegaRtcManager {
5918
6892
  }
5919
6893
 
5920
6894
  async _internalSendScreenVideo() {
5921
- this._logger.debug("_internalSendScreenVideo()");
6895
+ logger.info("_internalSendScreenVideo()");
5922
6896
 
5923
6897
  this._screenVideoProducerPromise = (async () => {
5924
6898
  try {
@@ -5943,20 +6917,22 @@ class VegaRtcManager {
5943
6917
  });
5944
6918
 
5945
6919
  this._screenVideoProducer = producer;
6920
+ this._qualityMonitor.addProducer(this._selfId, producer.id);
5946
6921
  producer.observer.once("close", () => {
5947
- this._logger.debug('screenVideoProducer "close" event');
6922
+ logger.info('screenVideoProducer "close" event');
5948
6923
 
5949
6924
  if (producer.appData.localClosed)
5950
6925
  this._vegaConnection?.message("closeProducers", { producerIds: [producer.id] });
5951
6926
 
5952
6927
  this._screenVideoProducer = null;
5953
6928
  this._screenVideoProducerPromise = null;
6929
+ this._qualityMonitor.removeProducer(this._selfId, producer.id);
5954
6930
  });
5955
6931
 
5956
6932
  // Has someone replaced the track?
5957
6933
  if (this._screenVideoTrack !== this._screenVideoProducer.track) await this._replaceScreenVideoTrack();
5958
6934
  } catch (error) {
5959
- this._logger.error("screenVideoProducer failed:%o", error);
6935
+ logger.error("screenVideoProducer failed:%o", error);
5960
6936
  } finally {
5961
6937
  this._screenVideoProducerPromise = null;
5962
6938
 
@@ -5970,7 +6946,7 @@ class VegaRtcManager {
5970
6946
  }
5971
6947
 
5972
6948
  async _replaceScreenVideoTrack() {
5973
- this._logger.debug("_replaceScreenVideoTrack()");
6949
+ logger.info("_replaceScreenVideoTrack()");
5974
6950
 
5975
6951
  if (!this._screenVideoTrack || !this._screenVideoProducer || this._screenVideoProducer.closed) return;
5976
6952
 
@@ -5981,7 +6957,7 @@ class VegaRtcManager {
5981
6957
  }
5982
6958
 
5983
6959
  async _sendScreenVideo(track) {
5984
- this._logger.debug("_sendScreenVideo() [track:%o]", track);
6960
+ logger.info("_sendScreenVideo() [track:%o]", track);
5985
6961
 
5986
6962
  this._screenVideoTrack = track;
5987
6963
 
@@ -5995,7 +6971,7 @@ class VegaRtcManager {
5995
6971
  }
5996
6972
 
5997
6973
  async _internalSendScreenAudio() {
5998
- this._logger.debug("_internalSendScreenAudio()");
6974
+ logger.info("_internalSendScreenAudio()");
5999
6975
 
6000
6976
  this._screenAudioProducerPromise = (async () => {
6001
6977
  try {
@@ -6020,20 +6996,22 @@ class VegaRtcManager {
6020
6996
  });
6021
6997
 
6022
6998
  this._screenAudioProducer = producer;
6999
+ this._qualityMonitor.addProducer(this._selfId, producer.id);
6023
7000
  producer.observer.once("close", () => {
6024
- this._logger.debug('screenAudioProducer "close" event');
7001
+ logger.info('screenAudioProducer "close" event');
6025
7002
 
6026
7003
  if (producer.appData.localClosed)
6027
7004
  this._vegaConnection?.message("closeProducers", { producerIds: [producer.id] });
6028
7005
 
6029
7006
  this._screenAudioProducer = null;
6030
7007
  this._screenAudioProducerPromise = null;
7008
+ this._qualityMonitor.removeProducer(this._selfId, producer.id);
6031
7009
  });
6032
7010
 
6033
7011
  // Has someone replaced the track?
6034
7012
  if (this._screenAudioTrack !== this._screenAudioProducer.track) await this._replaceScreenAudioTrack();
6035
7013
  } catch (error) {
6036
- this._logger.error("screenAudioProducer failed:%o", error);
7014
+ logger.error("screenAudioProducer failed:%o", error);
6037
7015
  } finally {
6038
7016
  this._screenAudioProducerPromise = null;
6039
7017
 
@@ -6047,7 +7025,7 @@ class VegaRtcManager {
6047
7025
  }
6048
7026
 
6049
7027
  async _replaceScreenAudioTrack() {
6050
- this._logger.debug("_replaceScreenAudioTrack()");
7028
+ logger.info("_replaceScreenAudioTrack()");
6051
7029
 
6052
7030
  if (!this._screenAudioTrack || !this._screenAudioProducer || this._screenAudioProducer.closed) return;
6053
7031
 
@@ -6058,7 +7036,7 @@ class VegaRtcManager {
6058
7036
  }
6059
7037
 
6060
7038
  async _sendScreenAudio(track) {
6061
- this._logger.debug("_sendScreenAudio() [track:%o]", track);
7039
+ logger.info("_sendScreenAudio() [track:%o]", track);
6062
7040
 
6063
7041
  this._screenAudioTrack = track;
6064
7042
 
@@ -6072,7 +7050,7 @@ class VegaRtcManager {
6072
7050
  }
6073
7051
 
6074
7052
  _stopProducer(producer) {
6075
- this._logger.debug("_stopProducer()");
7053
+ logger.info("_stopProducer()");
6076
7054
 
6077
7055
  if (!producer || producer.closed) return;
6078
7056
 
@@ -6127,6 +7105,18 @@ class VegaRtcManager {
6127
7105
  rtcStats.sendEvent("colocation_changed", { colocation });
6128
7106
  }
6129
7107
 
7108
+ /**
7109
+ * This sends a signal to the SFU to pause all incoming video streams to the client.
7110
+ *
7111
+ * @param {boolean} audioOnly
7112
+ */
7113
+ setAudioOnly(audioOnly) {
7114
+ this._vegaConnection?.message(audioOnly ? "enableAudioOnly" : "disableAudioOnly");
7115
+ }
7116
+
7117
+ // the track ids send by signal server for remote-initiated screenshares
7118
+ setRemoteScreenshareVideoTrackIds(/*remoteScreenshareVideoTrackIds*/) {}
7119
+
6130
7120
  /**
6131
7121
  * The unique identifier for this room session.
6132
7122
  *
@@ -6146,7 +7136,7 @@ class VegaRtcManager {
6146
7136
  * @param {string} eventClaim
6147
7137
  */
6148
7138
  disconnect(clientIdOrStreamId, _activeBreakout, eventClaim) {
6149
- this._logger.debug("disconnect() [clientIdOrStreamId:%s, eventClaim:%s]", clientIdOrStreamId, eventClaim);
7139
+ logger.info("disconnect() [clientIdOrStreamId:%s, eventClaim:%s]", clientIdOrStreamId, eventClaim);
6150
7140
 
6151
7141
  if (this._clientStates.has(clientIdOrStreamId)) {
6152
7142
  // In this case this is a disconnect from an actual client, not just a screen share.
@@ -6204,7 +7194,7 @@ class VegaRtcManager {
6204
7194
  * @param {string} requestedByClientId
6205
7195
  */
6206
7196
  removeStream(streamId, _stream, requestedByClientId) {
6207
- this._logger.debug("removeStream() [streamId:%s, requestedByClientId:%s]", streamId, requestedByClientId);
7197
+ logger.info("removeStream() [streamId:%s, requestedByClientId:%s]", streamId, requestedByClientId);
6208
7198
 
6209
7199
  this._emitToSignal(PROTOCOL_REQUESTS.STOP_SCREENSHARE, {
6210
7200
  streamId: OUTBOUND_SCREEN_OUTBOUND_STREAM_ID,
@@ -6308,7 +7298,7 @@ class VegaRtcManager {
6308
7298
  * @param {boolean} enabled
6309
7299
  */
6310
7300
  stopOrResumeAudio(stream, enabled) {
6311
- this._logger.debug("stopOrResumeAudio() [enabled:%s]", enabled);
7301
+ logger.info("stopOrResumeAudio() [enabled:%s]", enabled);
6312
7302
 
6313
7303
  this._micPaused = !enabled;
6314
7304
 
@@ -6334,7 +7324,7 @@ class VegaRtcManager {
6334
7324
  * @param {boolean} enabled
6335
7325
  */
6336
7326
  stopOrResumeVideo(localStream, enable) {
6337
- this._logger.debug("stopOrResumeVideo() [enable:%s]", enable);
7327
+ logger.info("stopOrResumeVideo() [enable:%s]", enable);
6338
7328
 
6339
7329
  this._webcamPaused = !enable;
6340
7330
 
@@ -6397,7 +7387,7 @@ class VegaRtcManager {
6397
7387
  * }} streamOptions
6398
7388
  */
6399
7389
  acceptNewStream({ streamId, clientId }) {
6400
- this._logger.debug("acceptNewStream()", { streamId, clientId });
7390
+ logger.info("acceptNewStream()", { streamId, clientId });
6401
7391
 
6402
7392
  // make sure we have rtcStats connection
6403
7393
  this.rtcStatsReconnect();
@@ -6428,7 +7418,7 @@ class VegaRtcManager {
6428
7418
  * }} size
6429
7419
  */
6430
7420
  updateStreamResolution(streamId, _ignored, { width, height }) {
6431
- this._logger.debug("updateStreamResolution()", { streamId, width, height });
7421
+ logger.info("updateStreamResolution()", { streamId, width, height });
6432
7422
 
6433
7423
  const consumerId = this._streamIdToVideoConsumerId.get(streamId);
6434
7424
  const consumer = this._consumers.get(consumerId);
@@ -6561,18 +7551,22 @@ class VegaRtcManager {
6561
7551
  return this._onDataConsumerClosed(data);
6562
7552
  case "dominantSpeaker":
6563
7553
  return this._onDominantSpeaker(data);
7554
+ case "consumerScore":
7555
+ return this._onConsumerScore(data);
7556
+ case "producerScore":
7557
+ return this._onProducerScore(data);
6564
7558
  default:
6565
- this._logger.debug(`unknown message method "${method}"`);
7559
+ logger.info(`unknown message method "${method}"`);
6566
7560
  return;
6567
7561
  }
6568
7562
  })
6569
7563
  .catch((error) => {
6570
- console.error('"message" failed [error:%o]', error);
7564
+ logger.error('"message" failed [error:%o]', error);
6571
7565
  });
6572
7566
  }
6573
7567
 
6574
7568
  async _onConsumerReady(options) {
6575
- this._logger.debug("_onConsumerReady()", { id: options.id, producerId: options.producerId });
7569
+ logger.info("_onConsumerReady()", { id: options.id, producerId: options.producerId });
6576
7570
 
6577
7571
  const consumer = await this._receiveTransport.consume(options);
6578
7572
 
@@ -6581,8 +7575,10 @@ class VegaRtcManager {
6581
7575
  consumer.appData.spatialLayer = 2;
6582
7576
 
6583
7577
  this._consumers.set(consumer.id, consumer);
7578
+ this._qualityMonitor.addConsumer(consumer.appData.sourceClientId, consumer.id);
6584
7579
  consumer.observer.once("close", () => {
6585
7580
  this._consumers.delete(consumer.id);
7581
+ this._qualityMonitor.removeConsumer(consumer.appData.sourceClientId, consumer.id);
6586
7582
 
6587
7583
  this._consumerClosedCleanup(consumer);
6588
7584
  });
@@ -6593,7 +7589,7 @@ class VegaRtcManager {
6593
7589
  // Legacy Chrome API
6594
7590
  consumer.rtpReceiver.playoutDelayHint = MEDIA_JITTER_BUFFER_TARGET / 1000; // seconds
6595
7591
  } catch (error) {
6596
- this._logger.error("Error during setting jitter buffer target:", error);
7592
+ logger.error("Error during setting jitter buffer target:", error);
6597
7593
  }
6598
7594
  }
6599
7595
 
@@ -6626,13 +7622,13 @@ class VegaRtcManager {
6626
7622
  }
6627
7623
 
6628
7624
  async _onConsumerClosed({ consumerId, reason }) {
6629
- this._logger.debug("_onConsumerClosed()", { consumerId, reason });
7625
+ logger.info("_onConsumerClosed()", { consumerId, reason });
6630
7626
 
6631
7627
  this._consumers.get(consumerId)?.close();
6632
7628
  }
6633
7629
 
6634
7630
  _onConsumerPaused({ consumerId }) {
6635
- this._logger.debug("_onConsumerPaused()", { consumerId });
7631
+ logger.info("_onConsumerPaused()", { consumerId });
6636
7632
 
6637
7633
  const consumer = this._consumers.get(consumerId);
6638
7634
 
@@ -6643,7 +7639,7 @@ class VegaRtcManager {
6643
7639
  }
6644
7640
 
6645
7641
  _onConsumerResumed({ consumerId }) {
6646
- this._logger.debug("_onConsumerResumed()", { consumerId });
7642
+ logger.info("_onConsumerResumed()", { consumerId });
6647
7643
 
6648
7644
  const consumer = this._consumers.get(consumerId);
6649
7645
 
@@ -6656,8 +7652,30 @@ class VegaRtcManager {
6656
7652
  }
6657
7653
  }
6658
7654
 
7655
+ _onConsumerScore({ consumerId, kind, score }) {
7656
+ logger.info("_onConsumerScore()", { consumerId, kind, score });
7657
+ const {
7658
+ appData: { sourceClientId },
7659
+ } = this._consumers.get(consumerId) || { appData: {} };
7660
+
7661
+ if (sourceClientId) {
7662
+ this._qualityMonitor.addConsumerScore(sourceClientId, consumerId, kind, score);
7663
+ }
7664
+ }
7665
+
7666
+ _onProducerScore({ producerId, kind, score }) {
7667
+ logger.info("_onProducerScore()", { producerId, kind, score });
7668
+ [this._micProducer, this._webcamProducer, this._screenVideoProducer, this._screenAudioProducer].forEach(
7669
+ (producer) => {
7670
+ if (producer?.id === producerId) {
7671
+ this._qualityMonitor.addProducerScore(this._selfId, producerId, kind, score);
7672
+ }
7673
+ }
7674
+ );
7675
+ }
7676
+
6659
7677
  async _onDataConsumerReady(options) {
6660
- this._logger.debug("_onDataConsumerReady()", { id: options.id, producerId: options.producerId });
7678
+ logger.info("_onDataConsumerReady()", { id: options.id, producerId: options.producerId });
6661
7679
  const consumer = await this._receiveTransport.consumeData(options);
6662
7680
 
6663
7681
  this._dataConsumers.set(consumer.id, consumer);
@@ -6683,7 +7701,7 @@ class VegaRtcManager {
6683
7701
  }
6684
7702
 
6685
7703
  async _onDataConsumerClosed({ dataConsumerId, reason }) {
6686
- this._logger.debug("_onDataConsumerClosed()", { dataConsumerId, reason });
7704
+ logger.info("_onDataConsumerClosed()", { dataConsumerId, reason });
6687
7705
  const consumer = this._dataConsumers.get(dataConsumerId);
6688
7706
  consumer?.close();
6689
7707
  }
@@ -6739,7 +7757,7 @@ class VegaRtcManager {
6739
7757
  } = clientState;
6740
7758
 
6741
7759
  // Need to pause/resume any consumers that are part of a stream that has been
6742
- // accepted or dosconnected by the PWA
7760
+ // accepted or disconnected by the PWA
6743
7761
  const toPauseConsumers = [];
6744
7762
  const toResumeConsumers = [];
6745
7763
 
@@ -6841,6 +7859,10 @@ class VegaRtcManager {
6841
7859
  setMicAnalyserParams(params) {
6842
7860
  this._micAnalyser?.setParams(params);
6843
7861
  }
7862
+
7863
+ hasClient(clientId) {
7864
+ return this._clientStates.has(clientId);
7865
+ }
6844
7866
  }
6845
7867
 
6846
7868
  class RtcManagerDispatcher {
@@ -6849,7 +7871,16 @@ class RtcManagerDispatcher {
6849
7871
  this.currentManager = null;
6850
7872
  serverSocket.on(PROTOCOL_RESPONSES.ROOM_JOINED, ({ room, selfId, error, eventClaim }) => {
6851
7873
  if (error) return; // ignore error responses which lack room
6852
- const config = { selfId, room, emitter, serverSocket, webrtcProvider, features, eventClaim };
7874
+ const config = {
7875
+ selfId,
7876
+ room,
7877
+ emitter,
7878
+ serverSocket,
7879
+ webrtcProvider,
7880
+ features,
7881
+ eventClaim,
7882
+ deviceHandlerFactory: features?.deviceHandlerFactory,
7883
+ };
6853
7884
  const isSfu = !!room.sfuServer;
6854
7885
  if (this.currentManager) {
6855
7886
  if (this.currentManager.isInitializedWith({ selfId, roomName: room.name, isSfu })) {
@@ -6870,6 +7901,7 @@ class RtcManagerDispatcher {
6870
7901
  rtcManager.setupSocketListeners();
6871
7902
  emitter.emit(EVENTS.RTC_MANAGER_CREATED, { rtcManager });
6872
7903
  this.currentManager = rtcManager;
7904
+ serverSocket.setRtcManager(rtcManager);
6873
7905
  });
6874
7906
  }
6875
7907
 
@@ -7794,7 +8826,7 @@ var localStorage$1 = localStorage;
7794
8826
  const events = {
7795
8827
  CREDENTIALS_SAVED: "credentials_saved",
7796
8828
  };
7797
- class CredentialsService extends EventEmitter$1 {
8829
+ class CredentialsService extends EventEmitter {
7798
8830
  constructor({ deviceService, credentialsStore, }) {
7799
8831
  super();
7800
8832
  this._deviceService = deviceService;
@@ -8432,7 +9464,7 @@ class LocalParticipant extends RoomParticipant {
8432
9464
  }
8433
9465
  }
8434
9466
 
8435
- const sdkVersion = "0.2.0-beta.0";
9467
+ const sdkVersion = "0.3.0";
8436
9468
 
8437
9469
  const defaultSubdomainPattern = /^(?:([^.]+)[.])?((:?[^.]+[.]){1,}[^.]+)$/;
8438
9470
  const localstackPattern = /^(?:([^.]+)-)?(ip-[^.]*[.](?:hereby[.]dev|rfc1918[.]disappear[.]at)(?::\d+|))$/;