@whereby.com/browser-sdk 2.1.0-beta2 → 2.1.0-beta3

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.
@@ -3,11 +3,15 @@ import { useState, useEffect, useCallback } from 'react';
3
3
  import { createListenerMiddleware, createSlice, createAsyncThunk, createAction, createSelector, isAnyOf, combineReducers, configureStore } from '@reduxjs/toolkit';
4
4
  import { io } from 'socket.io-client';
5
5
  import adapter from 'webrtc-adapter';
6
+ import EventEmitter, { EventEmitter as EventEmitter$1 } from 'events';
6
7
  import SDPUtils from 'sdp';
7
8
  import * as sdpTransform from 'sdp-transform';
8
- import { v4 } from 'uuid';
9
+ import { v4 as v4$1 } from 'uuid';
10
+ import { Address6 } from 'ip-address';
11
+ import checkIp from 'check-ip';
12
+ import validate from 'uuid-validate';
9
13
  import { detectDevice, Device } from 'mediasoup-client';
10
- import EventEmitter$1, { EventEmitter } from 'events';
14
+ import { Chrome111 } from 'mediasoup-client/lib/handlers/Chrome111';
11
15
  import nodeBtoa from 'btoa';
12
16
  import axios from 'axios';
13
17
 
@@ -173,6 +177,7 @@ const initialState$g = {
173
177
  displayName: null,
174
178
  sdkVersion: null,
175
179
  externalId: null,
180
+ isNodeSdk: typeof process !== "undefined" && process.release.name === "node",
176
181
  };
177
182
  const appSlice = createSlice({
178
183
  name: "app",
@@ -200,7 +205,8 @@ const selectAppRoomUrl = (state) => state.app.roomUrl;
200
205
  const selectAppRoomKey = (state) => state.app.roomKey;
201
206
  const selectAppDisplayName = (state) => state.app.displayName;
202
207
  const selectAppSdkVersion = (state) => state.app.sdkVersion;
203
- const selectAppExternalId = (state) => state.app.externalId;
208
+ const selectAppExternalId = (state) => state.app.externalId;
209
+ const selectAppIsNodeSdk = (state) => state.app.isNodeSdk;
204
210
 
205
211
  function createAppAsyncThunk(typePrefix, payloadCreator) {
206
212
  return createAsyncThunk(typePrefix, payloadCreator);
@@ -285,13 +291,461 @@ createReactor([selectShouldFetchDeviceCredentials], ({ dispatch }, shouldFetchDe
285
291
  }
286
292
  });
287
293
 
294
+ let peerConnections = [];
295
+ let peerConnectionCounter = 0;
296
+ const peerConnectionData = new WeakMap();
297
+
298
+ const removePeerConnection = (pc) => {
299
+ peerConnections = peerConnections.filter((old) => old !== pc);
300
+ };
301
+
302
+ if (window.RTCPeerConnection) {
303
+ const OriginalRTCPeerConnection = window.RTCPeerConnection;
304
+ function PatchedRTCPeerConnection(rtcConfig) {
305
+ const pc = new OriginalRTCPeerConnection(rtcConfig);
306
+ peerConnections.push(pc);
307
+ peerConnectionData.set(pc, { index: peerConnectionCounter++ });
308
+ const onConnectionStateChange = () => {
309
+ if (pc.connectionState === "closed") {
310
+ removePeerConnection(pc);
311
+ pc.removeEventListener("connectionstatechange", onConnectionStateChange);
312
+ }
313
+ };
314
+ pc.addEventListener("connectionstatechange", onConnectionStateChange);
315
+ return pc;
316
+ }
317
+ PatchedRTCPeerConnection.prototype = OriginalRTCPeerConnection.prototype;
318
+ window.RTCPeerConnection = PatchedRTCPeerConnection;
319
+ }
320
+
321
+ let currentMonitor = null;
322
+
323
+ const getUpdatedStats = () => currentMonitor?.getUpdatedStats();
324
+
325
+ // Protocol enum used for the CLIENT (Make sure to keep it in sync with its server counterpart)
326
+
327
+ // Requests: messages from the client to the server
328
+ const PROTOCOL_REQUESTS = {
329
+ BLOCK_CLIENT: "block_client",
330
+ CLAIM_ROOM: "claim_room",
331
+ CLEAR_CHAT_HISTORY: "clear_chat_history",
332
+ ENABLE_AUDIO: "enable_audio",
333
+ ENABLE_VIDEO: "enable_video",
334
+ END_STREAM: "end_stream",
335
+ FETCH_MEDIASERVER_CONFIG: "fetch_mediaserver_config",
336
+ HANDLE_KNOCK: "handle_knock",
337
+ IDENTIFY_DEVICE: "identify_device",
338
+ INVITE_CLIENT_AS_MEMBER: "invite_client_as_member",
339
+ JOIN_ROOM: "join_room",
340
+ KICK_CLIENT: "kick_client",
341
+ KNOCK_ROOM: "knock_room",
342
+ LEAVE_ROOM: "leave_room",
343
+ SEND_CLIENT_METADATA: "send_client_metadata",
344
+ SET_LOCK: "set_lock",
345
+ SHARE_MEDIA: "share_media",
346
+ START_NEW_STREAM: "start_new_stream",
347
+ START_SCREENSHARE: "start_screenshare",
348
+ STOP_SCREENSHARE: "stop_screenshare",
349
+ START_URL_EMBED: "start_url_embed",
350
+ STOP_URL_EMBED: "stop_url_embed",
351
+ START_RECORDING: "start_recording",
352
+ STOP_RECORDING: "stop_recording",
353
+ SFU_TOKEN: "sfu_token",
354
+ };
355
+
356
+ // Responses: messages from the server to the client, in response to requests
357
+ const PROTOCOL_RESPONSES = {
358
+ AUDIO_ENABLED: "audio_enabled",
359
+ BACKGROUND_IMAGE_CHANGED: "background_image_changed",
360
+ BLOCK_ADDED: "block_added",
361
+ BLOCK_REMOVED: "block_removed",
362
+ CHAT_HISTORY_CLEARED: "chat_history_cleared",
363
+ CLIENT_BLOCKED: "client_blocked",
364
+ CLIENT_INVITED_AS_MEMBER: "client_invited_as_member",
365
+ CLIENT_KICKED: "client_kicked",
366
+ CLIENT_LEFT: "client_left",
367
+ CLIENT_METADATA_RECEIVED: "client_metadata_received",
368
+ CLIENT_READY: "client_ready",
369
+ CLIENT_ROLE_CHANGED: "client_role_changed",
370
+ CLIENT_USER_ID_CHANGED: "client_user_id_changed",
371
+ CONTACTS_UPDATED: "contacts_updated",
372
+ DEVICE_IDENTIFIED: "device_identified",
373
+ ROOM_ROLES_UPDATED: "room_roles_updated",
374
+ KNOCK_HANDLED: "knock_handled",
375
+ KNOCK_PAGE_BACKGROUND_CHANGED: "knock_page_background_changed",
376
+ KNOCKER_LEFT: "knocker_left",
377
+ MEDIASERVER_CONFIG: "mediaserver_config",
378
+ MEDIA_SHARED: "media_shared",
379
+ MEMBER_INVITE: "member_invite",
380
+ NEW_CLIENT: "new_client",
381
+ NEW_STREAM_STARTED: "new_stream_started",
382
+ SCREENSHARE_STARTED: "screenshare_started",
383
+ SCREENSHARE_STOPPED: "screenshare_stopped",
384
+ OWNER_NOTIFIED: "owner_notified",
385
+ OWNERS_CHANGED: "owners_changed",
386
+ PLAY_CLIENT_STICKER: "play_client_sticker",
387
+ ROOM_INTEGRATION_ENABLED: "room_integration_enabled",
388
+ ROOM_INTEGRATION_DISABLED: "room_integration_disabled",
389
+ ROOM_JOINED: "room_joined",
390
+ ROOM_KNOCKED: "room_knocked",
391
+ ROOM_LEFT: "room_left",
392
+ ROOM_LOCKED: "room_locked",
393
+ ROOM_PERMISSIONS_CHANGED: "room_permissions_changed",
394
+ ROOM_LOGO_CHANGED: "room_logo_changed",
395
+ ROOM_TYPE_CHANGED: "room_type_changed",
396
+ ROOM_MODE_CHANGED: "room_mode_changed",
397
+ SOCKET_USER_ID_CHANGED: "socket_user_id_changed",
398
+ STICKERS_UNLOCKED: "stickers_unlocked",
399
+ STREAM_ENDED: "stream_ended",
400
+ URL_EMBED_STARTED: "url_embed_started",
401
+ URL_EMBED_STOPPED: "url_embed_stopped",
402
+ RECORDING_STARTED: "recording_started",
403
+ RECORDING_STOPPED: "recording_stopped",
404
+ USER_NOTIFIED: "user_notified",
405
+ VIDEO_ENABLED: "video_enabled",
406
+ CLIENT_UNABLE_TO_JOIN: "client_unable_to_join",
407
+ };
408
+
409
+ // Relays: messages between clients, relayed through the server
410
+ const RELAY_MESSAGES = {
411
+ CHAT_MESSAGE: "chat_message",
412
+ CHAT_READ_STATE: "chat_read_state",
413
+ CHAT_STATE: "chat_state",
414
+ ICE_CANDIDATE: "ice_candidate",
415
+ ICE_END_OF_CANDIDATES: "ice_endofcandidates",
416
+ READY_TO_RECEIVE_OFFER: "ready_to_receive_offer",
417
+ REMOTE_CLIENT_MEDIA_REQUEST: "remote_client_media_request",
418
+ SDP_ANSWER: "sdp_answer",
419
+ SDP_OFFER: "sdp_offer",
420
+ VIDEO_STICKER: "video_sticker",
421
+ };
422
+
423
+ // Events: something happened that we want to let the client know about
424
+ const PROTOCOL_EVENTS = {
425
+ PENDING_CLIENT_LEFT: "pending_client_left",
426
+ MEDIA_QUALITY_CHANGED: "media_quality_changed",
427
+ };
428
+
429
+ class ReconnectManager extends EventEmitter {
430
+ constructor(socket, logger = console) {
431
+ super();
432
+ this._socket = socket;
433
+ this._logger = logger;
434
+ this._clients = {};
435
+ this._signalDisconnectTime = undefined;
436
+ this.rtcManager = undefined;
437
+
438
+ socket.on("disconnect", () => {
439
+ this._signalDisconnectTime = Date.now();
440
+ });
441
+
442
+ // We intercept these events and take responsiblity for forwarding them
443
+ socket.on(PROTOCOL_RESPONSES.ROOM_JOINED, (payload) => this._onRoomJoined(payload));
444
+ socket.on(PROTOCOL_RESPONSES.NEW_CLIENT, (payload) => this._onNewClient(payload));
445
+ socket.on(PROTOCOL_RESPONSES.CLIENT_LEFT, (payload) => this._onClientLeft(payload));
446
+
447
+ // We intercept these events and handle them without forwarding them
448
+ socket.on(PROTOCOL_EVENTS.PENDING_CLIENT_LEFT, (payload) => this._onPendingClientLeft(payload));
449
+
450
+ // We gather information from these events but they will also be forwarded
451
+ socket.on(PROTOCOL_RESPONSES.AUDIO_ENABLED, (payload) => this._onAudioEnabled(payload));
452
+ socket.on(PROTOCOL_RESPONSES.VIDEO_ENABLED, (payload) => this._onVideoEnabled(payload));
453
+ socket.on(PROTOCOL_RESPONSES.SCREENSHARE_STARTED, (payload) => this._onScreenshareChanged(payload, true));
454
+ socket.on(PROTOCOL_RESPONSES.SCREENSHARE_STOPPED, (payload) => this._onScreenshareChanged(payload, false));
455
+ }
456
+
457
+ async _onRoomJoined(payload) {
458
+ // We might have gotten an error
459
+ if (!payload.room?.clients) {
460
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
461
+ return;
462
+ }
463
+
464
+ if (!payload.selfId) {
465
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
466
+ return;
467
+ }
468
+
469
+ const myDeviceId = payload.room.clients.find((c) => payload.selfId === c.id)?.deviceId;
470
+ if (!myDeviceId) {
471
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
472
+ return;
473
+ }
474
+
475
+ // Try to remove our own pending client if this is a page reload
476
+ // Could also be a first normal room_joined which can never be glitch-free
477
+ if (!this._signalDisconnectTime) {
478
+ this._resetClientState(payload);
479
+ payload.room.clients = payload.room.clients.filter(
480
+ (c) => !(c.deviceId === myDeviceId && c.isPendingToLeave)
481
+ );
482
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
483
+ return;
484
+ }
485
+
486
+ // The threshold for trying glitch-free reconnect should be less than server-side configuration
487
+ const RECONNECT_THRESHOLD = payload.disconnectTimeout * 0.8;
488
+ const timeSinceDisconnect = Date.now() - this._signalDisconnectTime;
489
+ if (timeSinceDisconnect > RECONNECT_THRESHOLD) {
490
+ this._resetClientState(payload);
491
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
492
+ return;
493
+ }
494
+
495
+ // At this point we want to try to attempt glitch-free reconnection experience
496
+
497
+ // Filter out our own pending client after page reload
498
+ payload.room.clients = payload.room.clients.filter((c) => !(c.deviceId === myDeviceId && c.isPendingToLeave));
499
+
500
+ const allStats = await getUpdatedStats();
501
+ payload.room.clients.forEach((client) => {
502
+ try {
503
+ if (client.id === payload.selfId) return;
504
+
505
+ // Maybe add client to state
506
+ if (!this._clients[client.id]) {
507
+ this._addClientToState(client);
508
+ return;
509
+ }
510
+ // Verify that rtcManager knows about the client
511
+ if (!this.rtcManager?.hasClient(client.id)) {
512
+ return;
513
+ }
514
+
515
+ // Verify that the client state hasn't changed
516
+ if (
517
+ this._hasClientStateChanged({
518
+ clientId: client.id,
519
+ webcam: client.isVideoEnabled,
520
+ mic: client.isAudioEnabled,
521
+ screenShare: client.streams.length > 1,
522
+ })
523
+ ) {
524
+ return;
525
+ }
526
+
527
+ if (this._wasClientSendingMedia(client.id)) {
528
+ // Verify the client media is still flowing (not stopped from other end)
529
+ if (!this._isClientMediaActive(allStats, client.id)) {
530
+ return;
531
+ }
532
+ }
533
+
534
+ client.mergeWithOldClientState = true;
535
+ } catch (error) {
536
+ this._logger.error("Failed to evaluate if we should merge client state %o", error);
537
+ }
538
+ });
539
+
540
+ // We will try to remove any remote client pending to leave
541
+ payload.room.clients.forEach((c) => {
542
+ if (c.isPendingToLeave) {
543
+ this._onPendingClientLeft({ clientId: c.id });
544
+ }
545
+ });
546
+
547
+ this.emit(PROTOCOL_RESPONSES.ROOM_JOINED, payload);
548
+ }
549
+
550
+ _onClientLeft(payload) {
551
+ const { clientId } = payload;
552
+ const client = this._clients[clientId];
553
+
554
+ // Remove client from state and clear timeout if client was pending to leave
555
+ if (client) {
556
+ clearTimeout(client.timeout);
557
+ delete this._clients[clientId];
558
+ }
559
+
560
+ // Old RTCManager only takes one argument, so rest is ignored.
561
+ this.rtcManager?.disconnect(clientId, /* activeBreakout */ null, payload.eventClaim);
562
+
563
+ this.emit(PROTOCOL_RESPONSES.CLIENT_LEFT, payload);
564
+ }
565
+
566
+ _onPendingClientLeft(payload) {
567
+ const { clientId } = payload;
568
+ const client = this._clients[clientId];
569
+
570
+ if (!client) {
571
+ this._logger.warn(`client ${clientId} not found`);
572
+ return;
573
+ }
574
+
575
+ // We've already started the check below, don't do it again
576
+ if (client.isPendingToLeave) {
577
+ return;
578
+ }
579
+
580
+ client.isPendingToLeave = true;
581
+ if (this._wasClientSendingMedia(clientId)) {
582
+ client.checkActiveMediaAttempts = 0;
583
+ this._abortIfNotActive(payload);
584
+ }
585
+ }
586
+
587
+ _onNewClient(payload) {
588
+ const {
589
+ client: { id: clientId, deviceId },
590
+ } = payload;
591
+
592
+ const client = this._clients[clientId];
593
+ if (client && client.isPendingToLeave) {
594
+ clearTimeout(client.timeoutHandler);
595
+ client.isPendingToLeave = false;
596
+ return;
597
+ }
598
+
599
+ this._getPendingClientsByDeviceId(deviceId).forEach((client) => {
600
+ clearTimeout(client.timeoutHandler);
601
+ client.isPendingToLeave = undefined;
602
+ this.emit(PROTOCOL_RESPONSES.CLIENT_LEFT, { clientId: client.clientId });
603
+ });
604
+
605
+ this._addClientToState(payload.client);
606
+ this.emit(PROTOCOL_RESPONSES.NEW_CLIENT, payload);
607
+ }
608
+
609
+ // Evaluate if we should send send client_left before getting it from signal-server
610
+ async _abortIfNotActive(payload) {
611
+ const { clientId } = payload;
612
+
613
+ let client = this._clients[clientId];
614
+ if (!client?.isPendingToLeave) return;
615
+
616
+ client.checkActiveMediaAttempts++;
617
+ if (client.checkActiveMediaAttempts > 3) {
618
+ return;
619
+ }
620
+
621
+ const stillActive = await this._checkIsActive(clientId);
622
+ if (stillActive) {
623
+ client.timeoutHandler = setTimeout(() => this._abortIfNotActive(payload), 500);
624
+ return;
625
+ }
626
+
627
+ client = this._clients[clientId];
628
+ if (client?.isPendingToLeave) {
629
+ clearTimeout(client.timeoutHandler);
630
+ delete this._clients[clientId];
631
+ this.emit(PROTOCOL_RESPONSES.CLIENT_LEFT, payload);
632
+ }
633
+ }
634
+
635
+ // Check if client is active
636
+ async _checkIsActive(clientId) {
637
+ const allStats = await getUpdatedStats();
638
+ return this._isClientMediaActive(allStats, clientId);
639
+ }
640
+
641
+ // Check if client has bitrates for all tracks
642
+ _isClientMediaActive(stats, clientId) {
643
+ const clientStats = stats?.[clientId];
644
+ let isActive = false;
645
+ if (clientStats) {
646
+ Object.entries(clientStats.tracks).forEach(([trackId, trackStats]) => {
647
+ if (trackId !== "probator")
648
+ Object.values(trackStats.ssrcs).forEach((ssrcStats) => {
649
+ if ((ssrcStats.bitrate || 0) > 0) isActive = true;
650
+ });
651
+ });
652
+ }
653
+ return isActive;
654
+ }
655
+
656
+ _onAudioEnabled(payload) {
657
+ const { clientId, isAudioEnabled } = payload;
658
+ this._clients[clientId] = {
659
+ ...(this._clients[clientId] || {}),
660
+ isAudioEnabled,
661
+ };
662
+ }
663
+
664
+ _onVideoEnabled(payload) {
665
+ const { clientId, isVideoEnabled } = payload;
666
+ this._clients[clientId] = {
667
+ ...(this._clients[clientId] || {}),
668
+ isVideoEnabled,
669
+ };
670
+ }
671
+
672
+ _onScreenshareChanged(payload, action) {
673
+ const { clientId } = payload;
674
+ this._clients[clientId] = {
675
+ ...(this._clients[clientId] || {}),
676
+ isScreenshareEnabled: action,
677
+ };
678
+ }
679
+
680
+ _hasClientStateChanged({ clientId, webcam, mic, screenShare }) {
681
+ const state = this._clients[clientId];
682
+
683
+ if (!state) {
684
+ throw new Error(`Client ${clientId} not found in ReconnectManager state`);
685
+ }
686
+
687
+ if (webcam !== state.isVideoEnabled) {
688
+ return true;
689
+ }
690
+ if (mic !== state.isAudioEnabled) {
691
+ return true;
692
+ }
693
+ if (screenShare !== state.isScreenshareEnabled) {
694
+ return true;
695
+ }
696
+
697
+ return false;
698
+ }
699
+
700
+ _addClientToState(newClient) {
701
+ this._clients[newClient.id] = {
702
+ ...(this._clients[newClient.id] || {}),
703
+ isAudioEnabled: newClient.isAudioEnabled,
704
+ isVideoEnabled: newClient.isVideoEnabled,
705
+ isScreenshareEnabled: newClient.streams.length > 1,
706
+ deviceId: newClient.deviceId,
707
+ isPendingToLeave: newClient.isPendingToLeave,
708
+ clientId: newClient.id,
709
+ };
710
+ }
711
+
712
+ _wasClientSendingMedia(clientId) {
713
+ const client = this._clients[clientId];
714
+
715
+ if (!client) {
716
+ throw new Error(`Client ${clientId} not found in ReconnectManager state`);
717
+ }
718
+
719
+ return client.isAudioEnabled || client.isVideoEnabled || client.isScreenshareEnabled;
720
+ }
721
+
722
+ _getPendingClientsByDeviceId(deviceId) {
723
+ return Object.values(this._clients).filter((clientState) => {
724
+ return clientState.deviceId === deviceId && clientState.isPendingToLeave;
725
+ });
726
+ }
727
+
728
+ _resetClientState(payload) {
729
+ this._clients = {};
730
+ payload.room.clients.forEach((client) => {
731
+ if (client.id === payload.selfId) {
732
+ return;
733
+ } else {
734
+ this._addClientToState(client);
735
+ }
736
+ });
737
+ }
738
+ }
739
+
288
740
  const DEFAULT_SOCKET_PATH = "/protocol/socket.io/v4";
289
741
 
742
+ const NOOP_KEEPALIVE_INTERVAL = 2000;
743
+
290
744
  /**
291
745
  * Wrapper class that extends the Socket.IO client library.
292
746
  */
293
747
  class ServerSocket {
294
- constructor(hostName, optionsOverrides) {
748
+ constructor(hostName, optionsOverrides, glitchFree) {
295
749
  this._socket = io(hostName, {
296
750
  path: DEFAULT_SOCKET_PATH,
297
751
  randomizationFactor: 0.5,
@@ -317,12 +771,39 @@ class ServerSocket {
317
771
  this._socket.io.opts.transports = ["websocket", "polling"];
318
772
  }
319
773
  });
774
+
775
+ if (glitchFree) this._reconnectManager = new ReconnectManager(this._socket);
776
+
320
777
  this._socket.on("connect", () => {
321
778
  const transport = this.getTransport();
322
779
  if (transport === "websocket") {
323
780
  this._wasConnectedUsingWebsocket = true;
781
+
782
+ // start noop keepalive loop to detect client side disconnects fast
783
+ if (!this.noopKeepaliveInterval)
784
+ this.noopKeepaliveInterval = setInterval(() => {
785
+ try {
786
+ // send a noop message if it thinks it is connected (might not be)
787
+ if (this._socket.connected) {
788
+ this._socket.io.engine.sendPacket("noop");
789
+ }
790
+ } catch (ex) {}
791
+ }, NOOP_KEEPALIVE_INTERVAL);
324
792
  }
325
793
  });
794
+
795
+ this._socket.on("disconnect", () => {
796
+ if (this.noopKeepaliveInterval) {
797
+ clearInterval(this.noopKeepaliveInterval);
798
+ this.noopKeepaliveInterval = null;
799
+ }
800
+ });
801
+ }
802
+
803
+ setRtcManager(rtcManager) {
804
+ if (this._reconnectManager) {
805
+ this._reconnectManager.rtcManager = rtcManager;
806
+ }
326
807
  }
327
808
 
328
809
  connect() {
@@ -383,6 +864,17 @@ class ServerSocket {
383
864
  * @returns {function} Function to deregister the listener.
384
865
  */
385
866
  on(eventName, handler) {
867
+ const relayableEvents = [
868
+ PROTOCOL_RESPONSES.ROOM_JOINED,
869
+ PROTOCOL_RESPONSES.CLIENT_LEFT,
870
+ PROTOCOL_RESPONSES.NEW_CLIENT,
871
+ ];
872
+
873
+ // Intercept certain events if glitch-free is enabled.
874
+ if (this._reconnectManager && relayableEvents.includes(eventName)) {
875
+ return this._interceptEvent(eventName, handler);
876
+ }
877
+
386
878
  this._socket.on(eventName, handler);
387
879
 
388
880
  return () => {
@@ -409,6 +901,19 @@ class ServerSocket {
409
901
  off(eventName, handler) {
410
902
  this._socket.off(eventName, handler);
411
903
  }
904
+
905
+ /**
906
+ * Intercept event and let ReconnectManager handle them.
907
+ */
908
+ _interceptEvent(eventName, handler) {
909
+ if (this._reconnectManager) {
910
+ this._reconnectManager.on(eventName, handler);
911
+ }
912
+
913
+ return () => {
914
+ if (this._reconnectManager) this._reconnectManager.removeListener(eventName, handler);
915
+ };
916
+ }
412
917
  }
413
918
 
414
919
  function forwardSocketEvents(socket, dispatch) {
@@ -652,6 +1157,7 @@ const parseResolution = (res) => res.split(/[^\d]/g).map((n) => parseInt(n, 10))
652
1157
  function getMediaConstraints({
653
1158
  disableAEC,
654
1159
  disableAGC,
1160
+ fps24,
655
1161
  hd,
656
1162
  lax,
657
1163
  lowDataMode,
@@ -662,7 +1168,7 @@ function getMediaConstraints({
662
1168
  }) {
663
1169
  let HIGH_HEIGHT = 480;
664
1170
  let LOW_HEIGHT = 240;
665
- let LOW_FPS = 15;
1171
+ let NON_STANDARD_FPS = 0;
666
1172
 
667
1173
  if (hd) {
668
1174
  // respect user choice, but default to HD for pro, and SD for free
@@ -675,15 +1181,20 @@ function getMediaConstraints({
675
1181
  } else {
676
1182
  LOW_HEIGHT = 360;
677
1183
  }
678
- LOW_FPS = 30; // we still use 30fps because of assumptions about temporal layers
679
1184
  }
680
1185
 
1186
+ // Set framerate to 24 to increase quality/bandwidth
1187
+ if (fps24) NON_STANDARD_FPS = 24;
1188
+
1189
+ // Set framerate for low data, but only for non-simulcast
1190
+ if (lowDataMode && !simulcast) NON_STANDARD_FPS = 15;
1191
+
681
1192
  const constraints = {
682
1193
  audio: { ...(preferredDeviceIds.audioId && { deviceId: preferredDeviceIds.audioId }) },
683
1194
  video: {
684
1195
  ...(preferredDeviceIds.videoId ? { deviceId: preferredDeviceIds.videoId } : { facingMode: "user" }),
685
1196
  height: lowDataMode ? LOW_HEIGHT : HIGH_HEIGHT,
686
- ...(lowDataMode && { frameRate: LOW_FPS }),
1197
+ ...(NON_STANDARD_FPS && { frameRate: NON_STANDARD_FPS }),
687
1198
  },
688
1199
  };
689
1200
  if (lax) {
@@ -1519,7 +2030,10 @@ const selectSpeakerDevices = createSelector(selectLocalMediaDevices, (devices) =
1519
2030
  * Reactors
1520
2031
  */
1521
2032
  // Start localMedia unless started when roomConnection is wanted
1522
- const selectLocalMediaShouldStartWithOptions = createSelector(selectAppWantsToJoin, selectLocalMediaStatus, selectLocalMediaOptions, (appWantsToJoin, localMediaStatus, localMediaOptions) => {
2033
+ const selectLocalMediaShouldStartWithOptions = createSelector(selectAppWantsToJoin, selectLocalMediaStatus, selectLocalMediaOptions, selectAppIsNodeSdk, (appWantsToJoin, localMediaStatus, localMediaOptions, isNodeSdk) => {
2034
+ if (isNodeSdk) {
2035
+ return;
2036
+ }
1523
2037
  if (appWantsToJoin && localMediaStatus === "" && localMediaOptions) {
1524
2038
  return localMediaOptions;
1525
2039
  }
@@ -2155,8 +2669,14 @@ const selectRoomConnectionStatus = (state) => state.roomConnection.status;
2155
2669
  /**
2156
2670
  * Reactors
2157
2671
  */
2158
- const selectShouldConnectRoom = createSelector([selectOrganizationId, selectRoomConnectionStatus, selectSignalConnectionDeviceIdentified, selectLocalMediaStatus], (hasOrganizationIdFetched, roomConnectionStatus, signalConnectionDeviceIdentified, localMediaStatus) => {
2159
- if (localMediaStatus === "started" &&
2672
+ const selectShouldConnectRoom = createSelector([
2673
+ selectOrganizationId,
2674
+ selectRoomConnectionStatus,
2675
+ selectSignalConnectionDeviceIdentified,
2676
+ selectLocalMediaStatus,
2677
+ selectAppIsNodeSdk,
2678
+ ], (hasOrganizationIdFetched, roomConnectionStatus, signalConnectionDeviceIdentified, localMediaStatus, isNodeSdk) => {
2679
+ if ((localMediaStatus === "started" || isNodeSdk) && // the node SDK doesn't use LocalMedia, so we can join without
2160
2680
  signalConnectionDeviceIdentified &&
2161
2681
  !!hasOrganizationIdFetched &&
2162
2682
  ["initializing", "reconnect"].includes(roomConnectionStatus)) {
@@ -2183,125 +2703,27 @@ startAppListening({
2183
2703
  dispatch(doConnectRoom());
2184
2704
  }
2185
2705
  else if (resolution === "rejected") {
2186
- dispatch(connectionStatusChanged("knock_rejected"));
2187
- }
2188
- },
2189
- });
2190
-
2191
- const EVENTS = {
2192
- CLIENT_CONNECTION_STATUS_CHANGED: "client_connection_status_changed",
2193
- STREAM_ADDED: "stream_added",
2194
- RTC_MANAGER_CREATED: "rtc_manager_created",
2195
- RTC_MANAGER_DESTROYED: "rtc_manager_destroyed",
2196
- LOCAL_STREAM_TRACK_ADDED: "local_stream_track_added",
2197
- LOCAL_STREAM_TRACK_REMOVED: "local_stream_track_removed",
2198
- REMOTE_STREAM_TRACK_ADDED: "remote_stream_track_added",
2199
- REMOTE_STREAM_TRACK_REMOVED: "remote_stream_track_removed",
2200
- };
2201
-
2202
- const TYPES = {
2203
- CONNECTING: "connecting",
2204
- CONNECTION_FAILED: "connection_failed",
2205
- CONNECTION_SUCCESSFUL: "connection_successful",
2206
- CONNECTION_DISCONNECTED: "connection_disconnected",
2207
- };
2208
-
2209
- // Protocol enum used for the CLIENT (Make sure to keep it in sync with its server counterpart)
2210
-
2211
- // Requests: messages from the client to the server
2212
- const PROTOCOL_REQUESTS = {
2213
- BLOCK_CLIENT: "block_client",
2214
- CLAIM_ROOM: "claim_room",
2215
- CLEAR_CHAT_HISTORY: "clear_chat_history",
2216
- ENABLE_AUDIO: "enable_audio",
2217
- ENABLE_VIDEO: "enable_video",
2218
- END_STREAM: "end_stream",
2219
- FETCH_MEDIASERVER_CONFIG: "fetch_mediaserver_config",
2220
- HANDLE_KNOCK: "handle_knock",
2221
- IDENTIFY_DEVICE: "identify_device",
2222
- INVITE_CLIENT_AS_MEMBER: "invite_client_as_member",
2223
- JOIN_ROOM: "join_room",
2224
- KICK_CLIENT: "kick_client",
2225
- KNOCK_ROOM: "knock_room",
2226
- LEAVE_ROOM: "leave_room",
2227
- SEND_CLIENT_METADATA: "send_client_metadata",
2228
- SET_LOCK: "set_lock",
2229
- SHARE_MEDIA: "share_media",
2230
- START_NEW_STREAM: "start_new_stream",
2231
- START_SCREENSHARE: "start_screenshare",
2232
- STOP_SCREENSHARE: "stop_screenshare",
2233
- START_URL_EMBED: "start_url_embed",
2234
- STOP_URL_EMBED: "stop_url_embed",
2235
- START_RECORDING: "start_recording",
2236
- STOP_RECORDING: "stop_recording",
2237
- SFU_TOKEN: "sfu_token",
2238
- };
2706
+ dispatch(connectionStatusChanged("knock_rejected"));
2707
+ }
2708
+ },
2709
+ });
2239
2710
 
2240
- // Responses: messages from the server to the client, in response to requests
2241
- const PROTOCOL_RESPONSES = {
2242
- AUDIO_ENABLED: "audio_enabled",
2243
- BACKGROUND_IMAGE_CHANGED: "background_image_changed",
2244
- BLOCK_ADDED: "block_added",
2245
- BLOCK_REMOVED: "block_removed",
2246
- CHAT_HISTORY_CLEARED: "chat_history_cleared",
2247
- CLIENT_BLOCKED: "client_blocked",
2248
- CLIENT_INVITED_AS_MEMBER: "client_invited_as_member",
2249
- CLIENT_KICKED: "client_kicked",
2250
- CLIENT_LEFT: "client_left",
2251
- CLIENT_METADATA_RECEIVED: "client_metadata_received",
2252
- CLIENT_READY: "client_ready",
2253
- CLIENT_ROLE_CHANGED: "client_role_changed",
2254
- CLIENT_USER_ID_CHANGED: "client_user_id_changed",
2255
- CONTACTS_UPDATED: "contacts_updated",
2256
- DEVICE_IDENTIFIED: "device_identified",
2257
- ROOM_ROLES_UPDATED: "room_roles_updated",
2258
- KNOCK_HANDLED: "knock_handled",
2259
- KNOCK_PAGE_BACKGROUND_CHANGED: "knock_page_background_changed",
2260
- KNOCKER_LEFT: "knocker_left",
2261
- MEDIASERVER_CONFIG: "mediaserver_config",
2262
- MEDIA_SHARED: "media_shared",
2263
- MEMBER_INVITE: "member_invite",
2264
- NEW_CLIENT: "new_client",
2265
- NEW_STREAM_STARTED: "new_stream_started",
2266
- SCREENSHARE_STARTED: "screenshare_started",
2267
- SCREENSHARE_STOPPED: "screenshare_stopped",
2268
- OWNER_NOTIFIED: "owner_notified",
2269
- OWNERS_CHANGED: "owners_changed",
2270
- PLAY_CLIENT_STICKER: "play_client_sticker",
2271
- ROOM_INTEGRATION_ENABLED: "room_integration_enabled",
2272
- ROOM_INTEGRATION_DISABLED: "room_integration_disabled",
2273
- ROOM_JOINED: "room_joined",
2274
- ROOM_KNOCKED: "room_knocked",
2275
- ROOM_LEFT: "room_left",
2276
- ROOM_LOCKED: "room_locked",
2277
- ROOM_PERMISSIONS_CHANGED: "room_permissions_changed",
2278
- ROOM_LOGO_CHANGED: "room_logo_changed",
2279
- ROOM_TYPE_CHANGED: "room_type_changed",
2280
- ROOM_MODE_CHANGED: "room_mode_changed",
2281
- SOCKET_USER_ID_CHANGED: "socket_user_id_changed",
2282
- STICKERS_UNLOCKED: "stickers_unlocked",
2283
- STREAM_ENDED: "stream_ended",
2284
- URL_EMBED_STARTED: "url_embed_started",
2285
- URL_EMBED_STOPPED: "url_embed_stopped",
2286
- RECORDING_STARTED: "recording_started",
2287
- RECORDING_STOPPED: "recording_stopped",
2288
- USER_NOTIFIED: "user_notified",
2289
- VIDEO_ENABLED: "video_enabled",
2290
- CLIENT_UNABLE_TO_JOIN: "client_unable_to_join",
2711
+ const EVENTS = {
2712
+ CLIENT_CONNECTION_STATUS_CHANGED: "client_connection_status_changed",
2713
+ STREAM_ADDED: "stream_added",
2714
+ RTC_MANAGER_CREATED: "rtc_manager_created",
2715
+ RTC_MANAGER_DESTROYED: "rtc_manager_destroyed",
2716
+ LOCAL_STREAM_TRACK_ADDED: "local_stream_track_added",
2717
+ LOCAL_STREAM_TRACK_REMOVED: "local_stream_track_removed",
2718
+ REMOTE_STREAM_TRACK_ADDED: "remote_stream_track_added",
2719
+ REMOTE_STREAM_TRACK_REMOVED: "remote_stream_track_removed",
2291
2720
  };
2292
2721
 
2293
- // Relays: messages between clients, relayed through the server
2294
- const RELAY_MESSAGES = {
2295
- CHAT_MESSAGE: "chat_message",
2296
- CHAT_READ_STATE: "chat_read_state",
2297
- CHAT_STATE: "chat_state",
2298
- ICE_CANDIDATE: "ice_candidate",
2299
- ICE_END_OF_CANDIDATES: "ice_endofcandidates",
2300
- READY_TO_RECEIVE_OFFER: "ready_to_receive_offer",
2301
- REMOTE_CLIENT_MEDIA_REQUEST: "remote_client_media_request",
2302
- SDP_ANSWER: "sdp_answer",
2303
- SDP_OFFER: "sdp_offer",
2304
- VIDEO_STICKER: "video_sticker",
2722
+ const TYPES = {
2723
+ CONNECTING: "connecting",
2724
+ CONNECTION_FAILED: "connection_failed",
2725
+ CONNECTION_SUCCESSFUL: "connection_successful",
2726
+ CONNECTION_DISCONNECTED: "connection_disconnected",
2305
2727
  };
2306
2728
 
2307
2729
  const CAMERA_STREAM_ID$2 = "0";
@@ -2417,8 +2839,14 @@ function detectMicrophoneNotWorking(pc) {
2417
2839
  var rtcManagerEvents = {
2418
2840
  CAMERA_NOT_WORKING: "camera_not_working",
2419
2841
  CONNECTION_BLOCKED_BY_NETWORK: "connection_blocked_by_network",
2842
+ ICE_IPV6_SEEN: "ice_ipv6_seen",
2843
+ ICE_MDNS_SEEN: "ice_mdns_seen",
2844
+ ICE_NO_PUBLIC_IP_GATHERED: "ice_no_public_ip_gathered",
2845
+ ICE_NO_PUBLIC_IP_GATHERED_3SEC: "ice_no_public_ip_gathered_3sec",
2846
+ ICE_RESTART: "ice_restart",
2420
2847
  MICROPHONE_NOT_WORKING: "microphone_not_working",
2421
2848
  MICROPHONE_STOPPED_WORKING: "microphone_stopped_working",
2849
+ NEW_PC: "new_pc",
2422
2850
  SFU_CONNECTION_CLOSED: "sfu_connection_closed",
2423
2851
  COLOCATION_SPEAKER: "colocation_speaker",
2424
2852
  DOMINANT_SPEAKER: "dominant_speaker",
@@ -2617,6 +3045,13 @@ class Session {
2617
3045
  constructor({ peerConnectionId, bandwidth, maximumTurnBandwidth, deprioritizeH264Encoding, logger = console }) {
2618
3046
  this.peerConnectionId = peerConnectionId;
2619
3047
  this.relayCandidateSeen = false;
3048
+ this.serverReflexiveCandidateSeen = false;
3049
+ this.publicHostCandidateSeen = false;
3050
+ this.ipv6HostCandidateSeen = false;
3051
+ this.ipv6HostCandidateTeredoSeen = false;
3052
+ this.ipv6HostCandidate6to4Seen = false;
3053
+ this.mdnsHostCandidateSeen = false;
3054
+
2620
3055
  this.pc = null;
2621
3056
  this.wasEverConnected = false;
2622
3057
  this.connectionStatus = null;
@@ -2993,6 +3428,21 @@ class Session {
2993
3428
 
2994
3429
  setVideoBandwidthUsingSetParameters(this.pc, this.bandwidth);
2995
3430
  }
3431
+
3432
+ setAudioOnly(enable, excludedTrackIds = []) {
3433
+ this.pc
3434
+ .getTransceivers()
3435
+ .filter(
3436
+ (videoTransceiver) =>
3437
+ videoTransceiver?.direction !== "recvonly" &&
3438
+ videoTransceiver?.receiver?.track?.kind === "video" &&
3439
+ !excludedTrackIds.includes(videoTransceiver?.receiver?.track?.id) &&
3440
+ !excludedTrackIds.includes(videoTransceiver?.sender?.track?.id)
3441
+ )
3442
+ .forEach((videoTransceiver) => {
3443
+ videoTransceiver.direction = enable ? "sendonly" : "sendrecv";
3444
+ });
3445
+ }
2996
3446
  }
2997
3447
 
2998
3448
  // transforms a maplike to an object. Mostly for getStats +
@@ -3091,6 +3541,8 @@ var rtcstats = function(trace, getStatsInterval, prefixesToWrap) {
3091
3541
  var peerconnectioncounter = 0;
3092
3542
  var isFirefox = !!window.mozRTCPeerConnection;
3093
3543
  var isEdge = !!window.RTCIceGatherer;
3544
+ var prevById = {};
3545
+
3094
3546
  prefixesToWrap.forEach(function(prefix) {
3095
3547
  if (!window[prefix + 'RTCPeerConnection']) {
3096
3548
  return;
@@ -3161,13 +3613,12 @@ var rtcstats = function(trace, getStatsInterval, prefixesToWrap) {
3161
3613
  trace('ondatachannel', id, [event.channel.id, event.channel.label]);
3162
3614
  });
3163
3615
 
3164
- var prev = {};
3165
3616
  var getStats = function() {
3166
3617
  pc.getStats(null).then(function(res) {
3167
3618
  var now = map2obj(res);
3168
3619
  var base = JSON.parse(JSON.stringify(now)); // our new prev
3169
- trace('getstats', id, deltaCompression(prev, now));
3170
- prev = base;
3620
+ trace('getstats', id, deltaCompression(prevById[id] || {}, now));
3621
+ prevById[id] = base;
3171
3622
  });
3172
3623
  };
3173
3624
  // TODO: do we want one big interval and all peerconnections
@@ -3387,6 +3838,13 @@ var rtcstats = function(trace, getStatsInterval, prefixesToWrap) {
3387
3838
  }
3388
3839
  });
3389
3840
  */
3841
+
3842
+ return {
3843
+ resetDelta() {
3844
+ prevById = {};
3845
+ }
3846
+ }
3847
+
3390
3848
  };
3391
3849
 
3392
3850
  var rtcstats$1 = /*@__PURE__*/getDefaultExportFromCjs(rtcstats);
@@ -3395,11 +3853,18 @@ var rtcstats$1 = /*@__PURE__*/getDefaultExportFromCjs(rtcstats);
3395
3853
 
3396
3854
  const RTCSTATS_PROTOCOL_VERSION = "1.0";
3397
3855
 
3856
+ // when not connected we need to buffer at least a few getstats reports
3857
+ // as they are delta compressed and we need the initial properties
3858
+ const GETSTATS_BUFFER_SIZE = 20;
3859
+
3398
3860
  const clientInfo = {
3399
- id: v4(), // shared id across rtcstats reconnects
3861
+ id: v4$1(), // shared id across rtcstats reconnects
3400
3862
  connectionNumber: 0,
3401
3863
  };
3402
3864
 
3865
+ const noop = () => {};
3866
+ let resetDelta = noop;
3867
+
3403
3868
  // Inlined version of rtcstats/trace-ws with improved disconnect handling.
3404
3869
  function rtcStatsConnection(wsURL, logger = console) {
3405
3870
  const buffer = [];
@@ -3412,6 +3877,7 @@ function rtcStatsConnection(wsURL, logger = console) {
3412
3877
  let connectionShouldBeOpen;
3413
3878
  let connectionAttempt = 0;
3414
3879
  let hasPassedOnRoomSessionId = false;
3880
+ let getStatsBufferUsed = 0;
3415
3881
 
3416
3882
  const connection = {
3417
3883
  connected: false,
@@ -3449,8 +3915,15 @@ function rtcStatsConnection(wsURL, logger = console) {
3449
3915
  if (ws.readyState === WebSocket.OPEN) {
3450
3916
  connectionAttempt = 0;
3451
3917
  ws.send(JSON.stringify(args));
3452
- } else if (args[0] !== "getstats" && !(args[0] === "customEvent" && args[2].type === "insightsStats")) {
3453
- // buffer all but stats when not connected
3918
+ } else if (args[0] === "getstats") {
3919
+ // only buffer getStats for a while
3920
+ // we don't want this to pile up, but we need at least the initial reports
3921
+ if (getStatsBufferUsed < GETSTATS_BUFFER_SIZE) {
3922
+ getStatsBufferUsed++;
3923
+ buffer.push(args);
3924
+ }
3925
+ } else if (args[0] === "customEvent" && args[2].type === "insightsStats") ; else {
3926
+ // buffer everything else
3454
3927
  buffer.push(args);
3455
3928
  }
3456
3929
 
@@ -3485,6 +3958,7 @@ function rtcStatsConnection(wsURL, logger = console) {
3485
3958
  ws.onclose = (e) => {
3486
3959
  connection.connected = false;
3487
3960
  logger.info(`[RTCSTATS] Closed ${e.code}`);
3961
+ resetDelta();
3488
3962
  };
3489
3963
  ws.onopen = () => {
3490
3964
  // send client info after each connection, so analysis tools can handle reconnections
@@ -3509,10 +3983,11 @@ function rtcStatsConnection(wsURL, logger = console) {
3509
3983
  ws.send(JSON.stringify(userRole));
3510
3984
  }
3511
3985
 
3512
- // send buffered events (non-getStats)
3986
+ // send buffered events
3513
3987
  while (buffer.length) {
3514
3988
  ws.send(JSON.stringify(buffer.shift()));
3515
3989
  }
3990
+ getStatsBufferUsed = 0;
3516
3991
  };
3517
3992
  },
3518
3993
  };
@@ -3521,11 +3996,13 @@ function rtcStatsConnection(wsURL, logger = console) {
3521
3996
  }
3522
3997
 
3523
3998
  const server = rtcStatsConnection("wss://rtcstats.srv.whereby.com" );
3524
- rtcstats$1(
3999
+ const stats = rtcstats$1(
3525
4000
  server.trace,
3526
4001
  10000, // query once every 10 seconds.
3527
4002
  [""] // only shim unprefixed RTCPeerConnecion.
3528
4003
  );
4004
+ // on node clients this function can be undefined
4005
+ resetDelta = stats?.resetDelta || noop;
3529
4006
 
3530
4007
  const rtcStats = {
3531
4008
  sendEvent: (type, value) => {
@@ -3574,6 +4051,7 @@ class BaseRtcManager {
3574
4051
  this.peerConnections = {};
3575
4052
  this.localStreams = {};
3576
4053
  this.enabledLocalStreamIds = [];
4054
+ this._screenshareVideoTrackIds = [];
3577
4055
  this._socketListenerDeregisterFunctions = [];
3578
4056
  this._localStreamDeregisterFunction = null;
3579
4057
  this._emitter = emitter;
@@ -3581,6 +4059,7 @@ class BaseRtcManager {
3581
4059
  this._webrtcProvider = webrtcProvider;
3582
4060
  this._features = features || {};
3583
4061
  this._logger = logger;
4062
+ this._isAudioOnlyMode = false;
3584
4063
 
3585
4064
  this.offerOptions = { offerToReceiveAudio: true, offerToReceiveVideo: true };
3586
4065
  this._pendingActionsForConnectedPeerConnections = [];
@@ -3797,6 +4276,8 @@ class BaseRtcManager {
3797
4276
  clientId,
3798
4277
  });
3799
4278
 
4279
+ setTimeout(() => this._emit(rtcManagerEvents.NEW_PC), 0);
4280
+
3800
4281
  pc.ontrack = (event) => {
3801
4282
  const stream = event.streams[0];
3802
4283
  if (stream.id === "default" && stream.getAudioTracks().length === 0) {
@@ -3858,6 +4339,11 @@ class BaseRtcManager {
3858
4339
  this.maybeRestrictRelayBandwidth(session);
3859
4340
  }
3860
4341
  }
4342
+
4343
+ if (this._isAudioOnlyMode) {
4344
+ session.setAudioOnly(true, this._screenshareVideoTrackIds);
4345
+ }
4346
+
3861
4347
  session.registerConnected();
3862
4348
  break;
3863
4349
  case "disconnected":
@@ -4086,6 +4572,7 @@ class BaseRtcManager {
4086
4572
  }
4087
4573
 
4088
4574
  // at this point it is clearly a screensharing stream.
4575
+ this._screenshareVideoTrackIds.push(stream.getVideoTracks()[0].id);
4089
4576
  this._shareScreen(streamId, stream);
4090
4577
  return;
4091
4578
  }
@@ -4238,6 +4725,29 @@ class BaseRtcManager {
4238
4725
  const answer = this._transformIncomingSdp(data.message, session.pc);
4239
4726
  session.handleAnswer(answer);
4240
4727
  }),
4728
+
4729
+ // if this is a reconnect to signal-server during screen-share we must let signal-server know
4730
+ this._serverSocket.on(PROTOCOL_RESPONSES.ROOM_JOINED, ({ room: { sfuServer: isSfu } }) => {
4731
+ if (isSfu || !this._wasScreenSharing) return;
4732
+
4733
+ const screenShareStreamId = Object.keys(this.localStreams).find((id) => id !== CAMERA_STREAM_ID$1);
4734
+ if (!screenShareStreamId) {
4735
+ return;
4736
+ }
4737
+
4738
+ const screenshareStream = this.localStreams[screenShareStreamId];
4739
+ if (!screenshareStream) {
4740
+ this._logger.warn(`screenshare stream ${screenShareStreamId} not found`);
4741
+ return;
4742
+ }
4743
+
4744
+ const hasAudioTrack = screenshareStream.getAudioTracks().length > 0;
4745
+
4746
+ this._emitServerEvent(PROTOCOL_REQUESTS.START_SCREENSHARE, {
4747
+ streamId: screenShareStreamId,
4748
+ hasAudioTrack,
4749
+ });
4750
+ }),
4241
4751
  ];
4242
4752
  }
4243
4753
 
@@ -4271,6 +4781,27 @@ class BaseRtcManager {
4271
4781
  track.removeEventListener("ended", this._audioTrackOnEnded);
4272
4782
  }
4273
4783
 
4784
+ setAudioOnly(audioOnly) {
4785
+ this._isAudioOnlyMode = audioOnly;
4786
+
4787
+ this._forEachPeerConnection((session) => {
4788
+ if (session.hasConnectedPeerConnection()) {
4789
+ this._withForcedRenegotiation(session, () =>
4790
+ session.setAudioOnly(this._isAudioOnlyMode, this._screenshareVideoTrackIds)
4791
+ );
4792
+ }
4793
+ });
4794
+ }
4795
+
4796
+ setRemoteScreenshareVideoTrackIds(remoteScreenshareVideoTrackIds = []) {
4797
+ const localScreenshareStream = this._getFirstLocalNonCameraStream();
4798
+
4799
+ this._screenshareVideoTrackIds = [
4800
+ ...(localScreenshareStream?.track ? [localScreenshareStream.track.id] : []),
4801
+ ...remoteScreenshareVideoTrackIds,
4802
+ ];
4803
+ }
4804
+
4274
4805
  setRoomSessionId(roomSessionId) {
4275
4806
  this._roomSessionId = roomSessionId;
4276
4807
  }
@@ -4304,6 +4835,52 @@ function getOptimalBitrate(width, height, frameRate) {
4304
4835
  return targetBitrate;
4305
4836
  }
4306
4837
 
4838
+ // taken from https://github.com/sindresorhus/ip-regex ^5.0.0
4839
+ // inlined because it's import caused errors in browser-sdk when running tests
4840
+ const word = "[a-fA-F\\d:]";
4841
+
4842
+ const boundry = (options) =>
4843
+ options && options.includeBoundaries ? `(?:(?<=\\s|^)(?=${word})|(?<=${word})(?=\\s|$))` : "";
4844
+
4845
+ 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}";
4846
+
4847
+ const v6segment = "[a-fA-F\\d]{1,4}";
4848
+
4849
+ const v6 = `
4850
+ (?:
4851
+ (?:${v6segment}:){7}(?:${v6segment}|:)| // 1:2:3:4:5:6:7:: 1:2:3:4:5:6:7:8
4852
+ (?:${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
4853
+ (?:${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
4854
+ (?:${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
4855
+ (?:${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
4856
+ (?:${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
4857
+ (?:${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
4858
+ (?::(?:(?::${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
4859
+ )(?:%[0-9a-zA-Z]{1,})? // %eth0 %1
4860
+ `
4861
+ .replace(/\s*\/\/.*$/gm, "")
4862
+ .replace(/\n/g, "")
4863
+ .trim();
4864
+
4865
+ // Pre-compile only the exact regexes because adding a global flag make regexes stateful
4866
+ const v46Exact = new RegExp(`(?:^${v4}$)|(?:^${v6}$)`);
4867
+ const v4exact = new RegExp(`^${v4}$`);
4868
+ const v6exact = new RegExp(`^${v6}$`);
4869
+
4870
+ const ipRegex = (options) =>
4871
+ options && options.exact
4872
+ ? v46Exact
4873
+ : new RegExp(
4874
+ `(?:${boundry(options)}${v4}${boundry(options)})|(?:${boundry(options)}${v6}${boundry(options)})`,
4875
+ "g"
4876
+ );
4877
+
4878
+ ipRegex.v4 = (options) =>
4879
+ options && options.exact ? v4exact : new RegExp(`${boundry(options)}${v4}${boundry(options)}`, "g");
4880
+ ipRegex.v6 = (options) =>
4881
+ options && options.exact ? v6exact : new RegExp(`${boundry(options)}${v6}${boundry(options)}`, "g");
4882
+
4883
+ const ICE_PUBLIC_IP_GATHERING_TIMEOUT = 3 * 1000;
4307
4884
  const CAMERA_STREAM_ID = RtcStream.getCameraId();
4308
4885
  const browserName$1 = adapter.browserDetails.browser;
4309
4886
 
@@ -4335,6 +4912,14 @@ class P2pRtcManager extends BaseRtcManager {
4335
4912
  // clean up some helpers.
4336
4913
  session.wasEverConnected = false;
4337
4914
  session.relayCandidateSeen = false;
4915
+ session.serverReflexiveCandidateSeen = false;
4916
+ session.publicHostCandidateSeen = false;
4917
+ session.ipv6HostCandidateSeen = false;
4918
+ this.ipv6HostCandidateTeredoSeen = false;
4919
+ this.ipv6HostCandidate6to4Seen = false;
4920
+ this.mdnsHostCandidateSeen = false;
4921
+
4922
+ this._emit(rtcManagerEvents.ICE_RESTART);
4338
4923
 
4339
4924
  this._negotiatePeerConnection(
4340
4925
  clientId,
@@ -4477,7 +5062,7 @@ class P2pRtcManager extends BaseRtcManager {
4477
5062
  let bandwidth = this._features.bandwidth
4478
5063
  ? parseInt(this._features.bandwidth, 10)
4479
5064
  : {
4480
- 1: this._features.cap2pBitrate ? 1000 : 0,
5065
+ 1: 0,
4481
5066
  2: this._features.highP2PBandwidth ? 768 : 384,
4482
5067
  3: this._features.highP2PBandwidth ? 512 : 256,
4483
5068
  4: 192,
@@ -4543,9 +5128,73 @@ class P2pRtcManager extends BaseRtcManager {
4543
5128
  pc.addTrack(this._stoppedVideoTrack, localCameraStream);
4544
5129
  }
4545
5130
 
5131
+ pc.onicegatheringstatechange = (event) => {
5132
+ const connection = event.target;
5133
+
5134
+ switch (connection.iceGatheringState) {
5135
+ case "gathering":
5136
+ if (this.icePublicIPGatheringTimeoutID) clearTimeout(this.icePublicIPGatheringTimeoutID);
5137
+ this.icePublicIPGatheringTimeoutID = setTimeout(() => {
5138
+ if (
5139
+ !session.publicHostCandidateSeen &&
5140
+ !session.relayCandidateSeen &&
5141
+ !session.serverReflexiveCandidateSeen
5142
+ ) {
5143
+ this._emit(rtcManagerEvents.ICE_NO_PUBLIC_IP_GATHERED_3SEC);
5144
+ }
5145
+ }, ICE_PUBLIC_IP_GATHERING_TIMEOUT);
5146
+ break;
5147
+ case "complete":
5148
+ if (this.icePublicIPGatheringTimeoutID) clearTimeout(this.icePublicIPGatheringTimeoutID);
5149
+ this.icePublicIPGatheringTimeoutID = undefined;
5150
+ break;
5151
+ }
5152
+ };
5153
+
4546
5154
  pc.onicecandidate = (event) => {
4547
5155
  if (event.candidate) {
4548
- session.relayCandidateSeen = session.relayCandidateSeen || event.candidate.type === "relay";
5156
+ switch (event.candidate?.type) {
5157
+ case "host":
5158
+ const address = event?.candidate?.address;
5159
+ try {
5160
+ if (ipRegex.v4({ exact: true }).test(address)) {
5161
+ const ipv4 = checkIp(address);
5162
+ if (ipv4.isPublicIp) session.publicHostCandidateSeen = true;
5163
+ } else if (ipRegex.v6({ exact: true }).test(address.replace(/^\[(.*)\]/, "$1"))) {
5164
+ const ipv6 = new Address6(address.replace(/^\[(.*)\]/, "$1"));
5165
+ session.ipv6HostCandidateSeen = true;
5166
+
5167
+ if (ipv6.getScope() === "Global") {
5168
+ session.publicHostCandidateSeen = true;
5169
+ }
5170
+ if (ipv6.isTeredo()) {
5171
+ session.ipv6HostCandidateTeredoSeen = true;
5172
+ }
5173
+ if (ipv6.is6to4()) {
5174
+ session.ipv6HostCandidate6to4Seen = true;
5175
+ }
5176
+ } else {
5177
+ const uuidv4 = address.replace(/.local/, "");
5178
+ if (uuidv4 && validate(uuidv4, 4)) {
5179
+ session.mdnsHostCandidateSeen = true;
5180
+ }
5181
+ }
5182
+ } catch (error) {
5183
+ this._logger.debug("Error during parsing candidates! Error: ", { error });
5184
+ }
5185
+ break;
5186
+ case "srflx":
5187
+ if (!session.serverReflexiveCandidateSeen) {
5188
+ session.serverReflexiveCandidateSeen = true;
5189
+ }
5190
+ break;
5191
+ case "relay":
5192
+ case "relayed":
5193
+ if (!session.relayCandidateSeen) {
5194
+ session.relayCandidateSeen = true;
5195
+ }
5196
+ break;
5197
+ }
4549
5198
  this._emitServerEvent(RELAY_MESSAGES.ICE_CANDIDATE, {
4550
5199
  receiverId: clientId,
4551
5200
  message: event.candidate,
@@ -4554,6 +5203,20 @@ class P2pRtcManager extends BaseRtcManager {
4554
5203
  this._emitServerEvent(RELAY_MESSAGES.ICE_END_OF_CANDIDATES, {
4555
5204
  receiverId: clientId,
4556
5205
  });
5206
+ if (
5207
+ !session.publicHostCandidateSeen &&
5208
+ !session.relayCandidateSeen &&
5209
+ !session.serverReflexiveCandidateSeen
5210
+ ) {
5211
+ this._emit(rtcManagerEvents.ICE_NO_PUBLIC_IP_GATHERED);
5212
+ }
5213
+ if (session.ipv6HostCandidateSeen) {
5214
+ this._emit(rtcManagerEvents.ICE_IPV6_SEEN, {
5215
+ teredoSeen: session.ipv6HostCandidateTeredoSeen,
5216
+ sixtofourSeen: session.ipv6HostCandidate6to4Seen,
5217
+ });
5218
+ }
5219
+ if (session.mdnsHostCandidateSeen) this._emit(rtcManagerEvents.ICE_MDNS_SEEN);
4557
5220
  }
4558
5221
  };
4559
5222
 
@@ -4678,14 +5341,20 @@ class P2pRtcManager extends BaseRtcManager {
4678
5341
  streamId,
4679
5342
  hasAudioTrack: !!stream.getAudioTracks().length,
4680
5343
  });
5344
+ this._wasScreenSharing = true;
4681
5345
  this._addStreamToPeerConnections(stream);
4682
5346
  }
4683
5347
 
4684
5348
  removeStream(streamId, stream, requestedByClientId) {
4685
5349
  super.removeStream(streamId, stream);
4686
5350
  this._removeStreamFromPeerConnections(stream);
5351
+ this._wasScreenSharing = false;
4687
5352
  this._emitServerEvent(PROTOCOL_REQUESTS.STOP_SCREENSHARE, { streamId, requestedByClientId });
4688
5353
  }
5354
+
5355
+ hasClient(clientId) {
5356
+ return Object.keys(this.peerConnections).includes(clientId);
5357
+ }
4689
5358
  }
4690
5359
 
4691
5360
  class SfuV2Parser {
@@ -4810,7 +5479,7 @@ class SfuV2Parser {
4810
5479
  }
4811
5480
  }
4812
5481
 
4813
- class VegaConnection extends EventEmitter {
5482
+ class VegaConnection extends EventEmitter$1 {
4814
5483
  constructor(wsUrl, logger, protocol = "whereby-sfu#v4") {
4815
5484
  super();
4816
5485
 
@@ -5385,18 +6054,214 @@ const maybeTurnOnly = (transportConfig, features) => {
5385
6054
  }
5386
6055
  };
5387
6056
 
6057
+ const MEDIA_QUALITY = Object.freeze({
6058
+ ok: "ok",
6059
+ warning: "warning",
6060
+ critical: "critical",
6061
+ });
6062
+
6063
+ const MONITOR_INTERVAL = 600; // ms
6064
+ const TREND_HORIZON = 3; // number of monitor intervals needed for quality to change
6065
+ const WARNING_SCORE = 9;
6066
+ const CRITICAL_SCORE = 7;
6067
+
6068
+ class VegaMediaQualityMonitor extends EventEmitter {
6069
+ constructor({ logger }) {
6070
+ super();
6071
+ this._logger = logger;
6072
+ this._clients = {};
6073
+ this._producers = {};
6074
+ this._startMonitor();
6075
+ }
6076
+
6077
+ close() {
6078
+ clearInterval(this._intervalHandle);
6079
+ delete this._intervalHandle;
6080
+ this._producers = {};
6081
+ this._clients = {};
6082
+ }
6083
+
6084
+ _startMonitor() {
6085
+ this._intervalHandle = setInterval(() => {
6086
+ Object.entries(this._producers).forEach(([clientId, producers]) => {
6087
+ this._evaluateClient(clientId, producers);
6088
+ });
6089
+ }, MONITOR_INTERVAL);
6090
+ }
6091
+
6092
+ _evaluateClient(clientId, producers) {
6093
+ if (!this._clients[clientId]) {
6094
+ this._clients[clientId] = {
6095
+ audio: { currentQuality: MEDIA_QUALITY.ok, trend: [] },
6096
+ video: { currentQuality: MEDIA_QUALITY.ok, trend: [] },
6097
+ };
6098
+ }
6099
+
6100
+ this._evaluateProducer(
6101
+ clientId,
6102
+ Object.values(producers).filter((p) => p.kind === "audio"),
6103
+ "audio"
6104
+ );
6105
+ this._evaluateProducer(
6106
+ clientId,
6107
+ Object.values(producers).filter((p) => p.kind === "video"),
6108
+ "video"
6109
+ );
6110
+ }
6111
+
6112
+ _evaluateProducer(clientId, producers, kind) {
6113
+ if (producers.length === 0) {
6114
+ return;
6115
+ }
6116
+
6117
+ const avgScore = producers.reduce((prev, curr) => prev + curr.score, 0) / producers.length;
6118
+ const newQuality = this._evaluateScore(avgScore);
6119
+ const qualityChanged = this._updateTrend(newQuality, this._clients[clientId][kind]);
6120
+ if (qualityChanged) {
6121
+ this.emit(PROTOCOL_EVENTS.MEDIA_QUALITY_CHANGED, {
6122
+ clientId,
6123
+ kind,
6124
+ quality: newQuality,
6125
+ });
6126
+ }
6127
+ }
6128
+
6129
+ _updateTrend(newQuality, state) {
6130
+ state.trend.push(newQuality);
6131
+ if (state.trend.length > TREND_HORIZON) {
6132
+ state.trend.shift();
6133
+ }
6134
+
6135
+ if (newQuality !== state.currentQuality && state.trend.every((t) => t !== state.currentQuality)) {
6136
+ state.currentQuality = newQuality;
6137
+ return true;
6138
+ } else {
6139
+ return false;
6140
+ }
6141
+ }
6142
+
6143
+ addProducer(clientId, producerId) {
6144
+ if (!clientId || !producerId || !(typeof clientId === "string" && typeof producerId === "string")) {
6145
+ this._logger.warn("Missing clientId or producerId");
6146
+ return;
6147
+ }
6148
+
6149
+ if (!this._producers[clientId]) {
6150
+ this._producers[clientId] = {};
6151
+ }
6152
+
6153
+ this._producers[clientId][producerId] = {};
6154
+ }
6155
+
6156
+ removeProducer(clientId, producerId) {
6157
+ delete this._producers[clientId][producerId];
6158
+
6159
+ if (Object.keys(this._producers[clientId]).length === 0) {
6160
+ delete this._producers[clientId];
6161
+ }
6162
+ }
6163
+
6164
+ addConsumer(clientId, consumerId) {
6165
+ if (!clientId || !consumerId) {
6166
+ this._logger.warn("Missing clientId or consumerId");
6167
+ return;
6168
+ }
6169
+
6170
+ if (!this._producers[clientId]) {
6171
+ this._producers[clientId] = {};
6172
+ }
6173
+
6174
+ this._producers[clientId][consumerId] = {};
6175
+ }
6176
+
6177
+ removeConsumer(clientId, consumerId) {
6178
+ delete this._producers[clientId][consumerId];
6179
+
6180
+ if (Object.keys(this._producers[clientId]).length === 0) {
6181
+ delete this._producers[clientId];
6182
+ }
6183
+ }
6184
+
6185
+ addProducerScore(clientId, producerId, kind, score) {
6186
+ if (
6187
+ !Array.isArray(score) ||
6188
+ score.length === 0 ||
6189
+ score.some((s) => !s || !s.hasOwnProperty("score") || typeof s.score !== "number" || isNaN(s.score))
6190
+ ) {
6191
+ this._logger.warn("VegaMediaQualityMonitor.addProducerScore(): Unexpected producer score format");
6192
+ return;
6193
+ }
6194
+ this._producers[clientId][producerId] = { kind, score: this._calcAvgProducerScore(score.map((s) => s.score)) };
6195
+ }
6196
+
6197
+ addConsumerScore(clientId, consumerId, kind, score) {
6198
+ if (!score || !score.hasOwnProperty("producerScores") || !Array.isArray(score.producerScores)) {
6199
+ this._logger.warn("VegaMediaQualityMonitor.addConsumerScore(): Unexpected consumer score format");
6200
+ return;
6201
+ }
6202
+ this._producers[clientId][consumerId] = { kind, score: this._calcAvgProducerScore(score.producerScores) };
6203
+ }
6204
+
6205
+ _evaluateScore(score) {
6206
+ if (score <= WARNING_SCORE && score > CRITICAL_SCORE) {
6207
+ return MEDIA_QUALITY.warning;
6208
+ } else if (score <= CRITICAL_SCORE && score > 0) {
6209
+ return MEDIA_QUALITY.critical;
6210
+ } else {
6211
+ return MEDIA_QUALITY.ok;
6212
+ }
6213
+ }
6214
+
6215
+ _calcAvgProducerScore(scores) {
6216
+ try {
6217
+ if (!Array.isArray(scores) || scores.length === 0) {
6218
+ return 0;
6219
+ }
6220
+
6221
+ let totalScore = 0;
6222
+ let divisor = 0;
6223
+
6224
+ scores.forEach((score) => {
6225
+ if (score > 0) {
6226
+ totalScore += score;
6227
+ divisor++;
6228
+ }
6229
+ });
6230
+
6231
+ if (totalScore === 0 || divisor === 0) {
6232
+ return 0;
6233
+ } else {
6234
+ return totalScore / divisor;
6235
+ }
6236
+ } catch (error) {
6237
+ this._logger.error(error);
6238
+ return 0;
6239
+ }
6240
+ }
6241
+ }
6242
+
5388
6243
  const browserName = adapter.browserDetails.browser;
5389
6244
  let unloading = false;
5390
6245
 
5391
6246
  const RESTARTICE_ERROR_RETRY_THRESHOLD_IN_MS = 3500;
5392
6247
  const RESTARTICE_ERROR_MAX_RETRY_COUNT = 5;
5393
- const OUTBOUND_CAM_OUTBOUND_STREAM_ID = v4();
5394
- const OUTBOUND_SCREEN_OUTBOUND_STREAM_ID = v4();
6248
+ const OUTBOUND_CAM_OUTBOUND_STREAM_ID = v4$1();
6249
+ const OUTBOUND_SCREEN_OUTBOUND_STREAM_ID = v4$1();
5395
6250
 
5396
6251
  if (browserName === "chrome") window.document.addEventListener("beforeunload", () => (unloading = true));
5397
6252
 
5398
6253
  class VegaRtcManager {
5399
- constructor({ selfId, room, emitter, serverSocket, webrtcProvider, features, eventClaim, logger = console }) {
6254
+ constructor({
6255
+ selfId,
6256
+ room,
6257
+ emitter,
6258
+ serverSocket,
6259
+ webrtcProvider,
6260
+ features,
6261
+ eventClaim,
6262
+ logger = console,
6263
+ deviceHandlerFactory,
6264
+ }) {
5400
6265
  assert$1.ok(selfId, "selfId is required");
5401
6266
  assert$1.ok(room, "room is required");
5402
6267
  assert$1.ok(emitter && emitter.emit, "emitter is required");
@@ -5420,7 +6285,12 @@ class VegaRtcManager {
5420
6285
  this._micAnalyser = null;
5421
6286
  this._micAnalyserDebugger = null;
5422
6287
 
5423
- this._mediasoupDevice = new Device({ handlerName: getHandler() });
6288
+ if (deviceHandlerFactory) {
6289
+ this._mediasoupDevice = new Device({ handlerFactory: deviceHandlerFactory });
6290
+ } else {
6291
+ this._mediasoupDevice = new Device({ handlerName: getHandler() });
6292
+ }
6293
+
5424
6294
  this._routerRtpCapabilities = null;
5425
6295
 
5426
6296
  this._sendTransport = null;
@@ -5483,6 +6353,11 @@ class VegaRtcManager {
5483
6353
  // Retry if connection closed until disconnectAll called;
5484
6354
  this._reconnect = true;
5485
6355
  this._reconnectTimeOut = null;
6356
+
6357
+ this._qualityMonitor = new VegaMediaQualityMonitor({ logger: this._logger });
6358
+ this._qualityMonitor.on(PROTOCOL_EVENTS.MEDIA_QUALITY_CHANGED, (payload) => {
6359
+ this._emitToPWA(PROTOCOL_EVENTS.MEDIA_QUALITY_CHANGED, payload);
6360
+ });
5486
6361
  }
5487
6362
 
5488
6363
  _updateAndScheduleMediaServersRefresh({ iceServers, sfuServer, mediaserverConfigTtlSeconds }) {
@@ -5567,6 +6442,8 @@ class VegaRtcManager {
5567
6442
  if (this._reconnect) {
5568
6443
  this._reconnectTimeOut = setTimeout(() => this._connect(), 1000);
5569
6444
  }
6445
+
6446
+ this._qualityMonitor.close();
5570
6447
  }
5571
6448
 
5572
6449
  async _join() {
@@ -5814,6 +6691,7 @@ class VegaRtcManager {
5814
6691
  currentPaused ? producer.pause() : producer.resume();
5815
6692
 
5816
6693
  this._micProducer = producer;
6694
+ this._qualityMonitor.addProducer(this._selfId, producer.id);
5817
6695
 
5818
6696
  producer.observer.once("close", () => {
5819
6697
  this._logger.debug('micProducer "close" event');
@@ -5823,6 +6701,7 @@ class VegaRtcManager {
5823
6701
 
5824
6702
  this._micProducer = null;
5825
6703
  this._micProducerPromise = null;
6704
+ this._qualityMonitor.removeProducer(this._selfId, producer.id);
5826
6705
  });
5827
6706
 
5828
6707
  if (this._micTrack !== this._micProducer.track) await this._replaceMicTrack();
@@ -6008,6 +6887,7 @@ class VegaRtcManager {
6008
6887
  currentPaused ? producer.pause() : producer.resume();
6009
6888
 
6010
6889
  this._webcamProducer = producer;
6890
+ this._qualityMonitor.addProducer(this._selfId, producer.id);
6011
6891
  producer.observer.once("close", () => {
6012
6892
  this._logger.debug('webcamProducer "close" event');
6013
6893
 
@@ -6016,6 +6896,7 @@ class VegaRtcManager {
6016
6896
 
6017
6897
  this._webcamProducer = null;
6018
6898
  this._webcamProducerPromise = null;
6899
+ this._qualityMonitor.removeProducer(this._selfId, producer.id);
6019
6900
  });
6020
6901
 
6021
6902
  // Has someone replaced the track?
@@ -6110,6 +6991,7 @@ class VegaRtcManager {
6110
6991
  });
6111
6992
 
6112
6993
  this._screenVideoProducer = producer;
6994
+ this._qualityMonitor.addProducer(this._selfId, producer.id);
6113
6995
  producer.observer.once("close", () => {
6114
6996
  this._logger.debug('screenVideoProducer "close" event');
6115
6997
 
@@ -6118,6 +7000,7 @@ class VegaRtcManager {
6118
7000
 
6119
7001
  this._screenVideoProducer = null;
6120
7002
  this._screenVideoProducerPromise = null;
7003
+ this._qualityMonitor.removeProducer(this._selfId, producer.id);
6121
7004
  });
6122
7005
 
6123
7006
  // Has someone replaced the track?
@@ -6187,6 +7070,7 @@ class VegaRtcManager {
6187
7070
  });
6188
7071
 
6189
7072
  this._screenAudioProducer = producer;
7073
+ this._qualityMonitor.addProducer(this._selfId, producer.id);
6190
7074
  producer.observer.once("close", () => {
6191
7075
  this._logger.debug('screenAudioProducer "close" event');
6192
7076
 
@@ -6195,6 +7079,7 @@ class VegaRtcManager {
6195
7079
 
6196
7080
  this._screenAudioProducer = null;
6197
7081
  this._screenAudioProducerPromise = null;
7082
+ this._qualityMonitor.removeProducer(this._selfId, producer.id);
6198
7083
  });
6199
7084
 
6200
7085
  // Has someone replaced the track?
@@ -6294,6 +7179,18 @@ class VegaRtcManager {
6294
7179
  rtcStats.sendEvent("colocation_changed", { colocation });
6295
7180
  }
6296
7181
 
7182
+ /**
7183
+ * This sends a signal to the SFU to pause all incoming video streams to the client.
7184
+ *
7185
+ * @param {boolean} audioOnly
7186
+ */
7187
+ setAudioOnly(audioOnly) {
7188
+ this._vegaConnection?.message(audioOnly ? "enableAudioOnly" : "disableAudioOnly");
7189
+ }
7190
+
7191
+ // the track ids send by signal server for remote-initiated screenshares
7192
+ setRemoteScreenshareVideoTrackIds(/*remoteScreenshareVideoTrackIds*/) {}
7193
+
6297
7194
  /**
6298
7195
  * The unique identifier for this room session.
6299
7196
  *
@@ -6728,6 +7625,10 @@ class VegaRtcManager {
6728
7625
  return this._onDataConsumerClosed(data);
6729
7626
  case "dominantSpeaker":
6730
7627
  return this._onDominantSpeaker(data);
7628
+ case "consumerScore":
7629
+ return this._onConsumerScore(data);
7630
+ case "producerScore":
7631
+ return this._onProducerScore(data);
6731
7632
  default:
6732
7633
  this._logger.debug(`unknown message method "${method}"`);
6733
7634
  return;
@@ -6748,8 +7649,10 @@ class VegaRtcManager {
6748
7649
  consumer.appData.spatialLayer = 2;
6749
7650
 
6750
7651
  this._consumers.set(consumer.id, consumer);
7652
+ this._qualityMonitor.addConsumer(consumer.appData.sourceClientId, consumer.id);
6751
7653
  consumer.observer.once("close", () => {
6752
7654
  this._consumers.delete(consumer.id);
7655
+ this._qualityMonitor.removeConsumer(consumer.appData.sourceClientId, consumer.id);
6753
7656
 
6754
7657
  this._consumerClosedCleanup(consumer);
6755
7658
  });
@@ -6823,6 +7726,28 @@ class VegaRtcManager {
6823
7726
  }
6824
7727
  }
6825
7728
 
7729
+ _onConsumerScore({ consumerId, kind, score }) {
7730
+ this._logger.debug("_onConsumerScore()", { consumerId, kind, score });
7731
+ const {
7732
+ appData: { sourceClientId },
7733
+ } = this._consumers.get(consumerId) || { appData: {} };
7734
+
7735
+ if (sourceClientId) {
7736
+ this._qualityMonitor.addConsumerScore(sourceClientId, consumerId, kind, score);
7737
+ }
7738
+ }
7739
+
7740
+ _onProducerScore({ producerId, kind, score }) {
7741
+ this._logger.debug("_onProducerScore()", { producerId, kind, score });
7742
+ [this._micProducer, this._webcamProducer, this._screenVideoProducer, this._screenAudioProducer].forEach(
7743
+ (producer) => {
7744
+ if (producer?.id === producerId) {
7745
+ this._qualityMonitor.addProducerScore(this._selfId, producerId, kind, score);
7746
+ }
7747
+ }
7748
+ );
7749
+ }
7750
+
6826
7751
  async _onDataConsumerReady(options) {
6827
7752
  this._logger.debug("_onDataConsumerReady()", { id: options.id, producerId: options.producerId });
6828
7753
  const consumer = await this._receiveTransport.consumeData(options);
@@ -6906,7 +7831,7 @@ class VegaRtcManager {
6906
7831
  } = clientState;
6907
7832
 
6908
7833
  // Need to pause/resume any consumers that are part of a stream that has been
6909
- // accepted or dosconnected by the PWA
7834
+ // accepted or disconnected by the PWA
6910
7835
  const toPauseConsumers = [];
6911
7836
  const toResumeConsumers = [];
6912
7837
 
@@ -7008,6 +7933,10 @@ class VegaRtcManager {
7008
7933
  setMicAnalyserParams(params) {
7009
7934
  this._micAnalyser?.setParams(params);
7010
7935
  }
7936
+
7937
+ hasClient(clientId) {
7938
+ return this._clientStates.has(clientId);
7939
+ }
7011
7940
  }
7012
7941
 
7013
7942
  class RtcManagerDispatcher {
@@ -7016,7 +7945,16 @@ class RtcManagerDispatcher {
7016
7945
  this.currentManager = null;
7017
7946
  serverSocket.on(PROTOCOL_RESPONSES.ROOM_JOINED, ({ room, selfId, error, eventClaim }) => {
7018
7947
  if (error) return; // ignore error responses which lack room
7019
- const config = { selfId, room, emitter, serverSocket, webrtcProvider, features, eventClaim };
7948
+ const config = {
7949
+ selfId,
7950
+ room,
7951
+ emitter,
7952
+ serverSocket,
7953
+ webrtcProvider,
7954
+ features,
7955
+ eventClaim,
7956
+ deviceHandlerFactory: features?.deviceHandlerFactory,
7957
+ };
7020
7958
  const isSfu = !!room.sfuServer;
7021
7959
  if (this.currentManager) {
7022
7960
  if (this.currentManager.isInitializedWith({ selfId, roomName: room.name, isSfu })) {
@@ -7037,6 +7975,7 @@ class RtcManagerDispatcher {
7037
7975
  rtcManager.setupSocketListeners();
7038
7976
  emitter.emit(EVENTS.RTC_MANAGER_CREATED, { rtcManager });
7039
7977
  this.currentManager = rtcManager;
7978
+ serverSocket.setRtcManager(rtcManager);
7040
7979
  });
7041
7980
  }
7042
7981
 
@@ -7121,6 +8060,7 @@ const doConnectRtc = createAppThunk(() => (dispatch, getState) => {
7121
8060
  const dispatcher = selectRtcConnectionRaw(state).rtcManagerDispatcher;
7122
8061
  const isCameraEnabled = selectIsCameraEnabled(state);
7123
8062
  const isMicrophoneEnabled = selectIsMicrophoneEnabled(state);
8063
+ const isNodeSdk = selectAppIsNodeSdk(state);
7124
8064
  if (dispatcher) {
7125
8065
  return;
7126
8066
  }
@@ -7146,6 +8086,7 @@ const doConnectRtc = createAppThunk(() => (dispatch, getState) => {
7146
8086
  vp9On: false,
7147
8087
  h264On: false,
7148
8088
  simulcastScreenshareOn: false,
8089
+ deviceHandlerFactory: isNodeSdk ? Chrome111.createFactory() : undefined,
7149
8090
  },
7150
8091
  });
7151
8092
  dispatch(rtcDispatcherCreated(rtcManagerDispatcher));
@@ -8200,7 +9141,7 @@ var localStorage$1 = localStorage;
8200
9141
  const events = {
8201
9142
  CREDENTIALS_SAVED: "credentials_saved",
8202
9143
  };
8203
- class CredentialsService extends EventEmitter$1 {
9144
+ class CredentialsService extends EventEmitter {
8204
9145
  /**
8205
9146
  * Service to manage Whereby's Rest API credentials.
8206
9147
  *
@@ -8690,7 +9631,7 @@ const selectRoomConnectionState = createSelector(selectChatMessages, selectCloud
8690
9631
  return state;
8691
9632
  });
8692
9633
 
8693
- const sdkVersion = "2.1.0-beta2";
9634
+ const sdkVersion = "2.1.0-beta3";
8694
9635
 
8695
9636
  const initialState$1 = {
8696
9637
  chatMessages: [],