@webex/plugin-meetings 3.7.0-next.57 → 3.7.0-next.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,169 @@
1
+ import {MediaType} from '@webex/internal-media-core';
2
+ import LoggerProxy from '../common/logs/logger-proxy';
3
+ import type Meeting from '.';
4
+ import SendSlotManager from '../multistream/sendSlotManager';
5
+
6
+ export const createBrbState = (meeting: Meeting, enabled: boolean) => {
7
+ LoggerProxy.logger.info(
8
+ `Meeting:brbState#createBrbState: creating BrbState for meeting id ${meeting?.id}`
9
+ );
10
+
11
+ const brbState = new BrbState(meeting, enabled);
12
+
13
+ return brbState;
14
+ };
15
+
16
+ /** The purpose of this class is to manage the local and remote brb state
17
+ * and make sure that the server state always matches the last requested state by the client.
18
+ */
19
+ export class BrbState {
20
+ state: {
21
+ client: {
22
+ enabled: boolean;
23
+ };
24
+ server: {
25
+ enabled: boolean;
26
+ };
27
+ syncToServerInProgress: boolean;
28
+ };
29
+
30
+ meeting: Meeting;
31
+
32
+ /**
33
+ * Constructor
34
+ *
35
+ * @param {Meeting} meeting - the meeting object
36
+ * @param {boolean} enabled - whether the client audio/video is enabled at all
37
+ */
38
+ constructor(meeting: Meeting, enabled: boolean) {
39
+ this.meeting = meeting;
40
+ this.state = {
41
+ client: {
42
+ enabled,
43
+ },
44
+ server: {
45
+ enabled: false,
46
+ },
47
+ syncToServerInProgress: false,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Enables/disables brb
53
+ *
54
+ * @param {boolean} enabled
55
+ * @param {SendSlotManager} sendSlotManager
56
+ * @returns {Promise}
57
+ */
58
+ public enable(enabled: boolean, sendSlotManager: SendSlotManager) {
59
+ this.state.client.enabled = enabled;
60
+
61
+ return this.applyClientStateToServer(sendSlotManager);
62
+ }
63
+
64
+ /**
65
+ * Updates the server local and remote brb values so that they match the current client desired state.
66
+ *
67
+ * @param {SendSlotManager} sendSlotManager
68
+ * @returns {Promise}
69
+ */
70
+ private applyClientStateToServer(sendSlotManager: SendSlotManager) {
71
+ if (this.state.syncToServerInProgress) {
72
+ LoggerProxy.logger.info(
73
+ `Meeting:brbState#applyClientStateToServer: request to server in progress, we need to wait for it to complete`
74
+ );
75
+
76
+ return Promise.resolve();
77
+ }
78
+
79
+ const remoteBrbRequiresSync = this.state.client.enabled !== this.state.server.enabled;
80
+
81
+ LoggerProxy.logger.info(
82
+ `Meeting:brbState#applyClientStateToServer: remoteBrbRequiresSync: ${remoteBrbRequiresSync}`
83
+ );
84
+
85
+ if (!remoteBrbRequiresSync) {
86
+ LoggerProxy.logger.info(
87
+ `Meeting:brbState#applyClientStateToServer: client state already matching server state, nothing to do`
88
+ );
89
+
90
+ return Promise.resolve();
91
+ }
92
+
93
+ this.state.syncToServerInProgress = true;
94
+
95
+ return this.sendLocalBrbStateToServer(sendSlotManager)
96
+ .then(() => {
97
+ this.state.syncToServerInProgress = false;
98
+ LoggerProxy.logger.info(
99
+ `Meeting:brbState#applyClientStateToServer: sync with server completed`
100
+ );
101
+
102
+ // need to check if a new sync is required, because this.state.client may have changed while we were doing the current sync
103
+ this.applyClientStateToServer(sendSlotManager);
104
+ })
105
+ .catch((e) => {
106
+ this.state.syncToServerInProgress = false;
107
+ LoggerProxy.logger.warn(`Meeting:brbState#applyClientStateToServer: error: ${e}`);
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Send the local brb state to the server
113
+ *
114
+ * @param {SendSlotManager} sendSlotManager
115
+ * @returns {Promise}
116
+ */
117
+ private async sendLocalBrbStateToServer(sendSlotManager: SendSlotManager) {
118
+ const {enabled} = this.state.client;
119
+
120
+ if (!this.meeting.isMultistream) {
121
+ const errorMessage = 'Meeting:brbState#sendLocalBrbStateToServer: Not a multistream meeting';
122
+ const error = new Error(errorMessage);
123
+
124
+ LoggerProxy.logger.error(error);
125
+
126
+ return Promise.reject(error);
127
+ }
128
+
129
+ if (!this.meeting.mediaProperties.webrtcMediaConnection) {
130
+ const errorMessage =
131
+ 'Meeting:brbState#sendLocalBrbStateToServer: WebRTC media connection is not defined';
132
+ const error = new Error(errorMessage);
133
+
134
+ LoggerProxy.logger.error(error);
135
+
136
+ return Promise.reject(error);
137
+ }
138
+
139
+ // this logic should be applied only to multistream meetings
140
+ return this.meeting.meetingRequest
141
+ .setBrb({
142
+ enabled,
143
+ locusUrl: this.meeting.locusUrl,
144
+ deviceUrl: this.meeting.deviceUrl,
145
+ selfId: this.meeting.selfId,
146
+ })
147
+ .then(() => {
148
+ sendSlotManager.setSourceStateOverride(MediaType.VideoMain, enabled ? 'away' : null);
149
+ })
150
+ .catch((error) => {
151
+ LoggerProxy.logger.error('Meeting:brbState#sendLocalBrbStateToServer: Error ', error);
152
+
153
+ return Promise.reject(error);
154
+ });
155
+ }
156
+
157
+ /**
158
+ * This method should be called whenever the server brb state is changed
159
+ *
160
+ * @param {Boolean} [enabled] true if user has brb enabled, false otherwise
161
+ * @returns {undefined}
162
+ */
163
+ public handleServerBrbUpdate(enabled?: boolean) {
164
+ LoggerProxy.logger.info(
165
+ `Meeting:brbState#handleServerBrbUpdate: updating server brb to (${enabled})`
166
+ );
167
+ this.state.server.enabled = !!enabled;
168
+ }
169
+ }
@@ -163,6 +163,7 @@ import {LocusMediaRequest} from './locusMediaRequest';
163
163
  import {ConnectionStateHandler, ConnectionStateEvent} from './connectionStateHandler';
164
164
  import JoinWebinarError from '../common/errors/join-webinar-error';
165
165
  import Member from '../member';
166
+ import {BrbState, createBrbState} from './brbState';
166
167
  import MultistreamNotSupportedError from '../common/errors/multistream-not-supported-error';
167
168
  import JoinForbiddenError from '../common/errors/join-forbidden-error';
168
169
 
@@ -649,6 +650,7 @@ export default class Meeting extends StatelessWebexPlugin {
649
650
  turnServerUsed: boolean;
650
651
  areVoiceaEventsSetup = false;
651
652
  isMoveToInProgress = false;
653
+ brbState: BrbState;
652
654
 
653
655
  voiceaListenerCallbacks: object = {
654
656
  [VOICEAEVENTS.VOICEA_ANNOUNCEMENT]: (payload: Transcription['languageOptions']) => {
@@ -3407,6 +3409,7 @@ export default class Meeting extends StatelessWebexPlugin {
3407
3409
  });
3408
3410
 
3409
3411
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED, (payload) => {
3412
+ this.brbState?.handleServerBrbUpdate(payload?.brb?.enabled);
3410
3413
  Trigger.trigger(
3411
3414
  this,
3412
3415
  {
@@ -3650,22 +3653,7 @@ export default class Meeting extends StatelessWebexPlugin {
3650
3653
  return Promise.reject(error);
3651
3654
  }
3652
3655
 
3653
- // this logic should be applied only to multistream meetings
3654
- return this.meetingRequest
3655
- .setBrb({
3656
- enabled,
3657
- locusUrl: this.locusUrl,
3658
- deviceUrl: this.deviceUrl,
3659
- selfId: this.selfId,
3660
- })
3661
- .then(() => {
3662
- this.sendSlotManager.setSourceStateOverride(MediaType.VideoMain, enabled ? 'away' : null);
3663
- })
3664
- .catch((error) => {
3665
- LoggerProxy.logger.error('Meeting:index#beRightBack --> Error ', error);
3666
-
3667
- return Promise.reject(error);
3668
- });
3656
+ return this.brbState.enable(enabled, this.sendSlotManager);
3669
3657
  }
3670
3658
 
3671
3659
  /**
@@ -6719,6 +6707,9 @@ export default class Meeting extends StatelessWebexPlugin {
6719
6707
  new RtcMetrics(this.webex, {meetingId: this.id}, this.correlationId)
6720
6708
  : undefined;
6721
6709
 
6710
+ // ongoing reachability checks slow down new media connections especially on Firefox, so we stop them
6711
+ this.getWebexObject().meetings.reachability.stopReachability();
6712
+
6722
6713
  const mc = Media.createMediaConnection(
6723
6714
  this.isMultistream,
6724
6715
  this.getMediaConnectionDebugId(),
@@ -7435,6 +7426,7 @@ export default class Meeting extends StatelessWebexPlugin {
7435
7426
 
7436
7427
  this.audio = createMuteState(AUDIO, this, audioEnabled);
7437
7428
  this.video = createMuteState(VIDEO, this, videoEnabled);
7429
+ this.brbState = createBrbState(this, false);
7438
7430
 
7439
7431
  try {
7440
7432
  await this.setUpLocalStreamReferences(localStreams);
@@ -259,6 +259,32 @@ export default class Reachability extends EventsScope {
259
259
  }
260
260
  }
261
261
 
262
+ /**
263
+ * Stops all reachability checks that are in progress
264
+ * @public
265
+ * @memberof Reachability
266
+ * @returns {void}
267
+ */
268
+ public stopReachability() {
269
+ // overallTimer is always there only if there is reachability in progress
270
+ if (this.overallTimer) {
271
+ LoggerProxy.logger.log(
272
+ 'Reachability:index#stopReachability --> stopping reachability checks'
273
+ );
274
+ this.abortCurrentChecks();
275
+ this.emit(
276
+ {
277
+ file: 'reachability',
278
+ function: 'stopReachability',
279
+ },
280
+ 'reachability:stopped',
281
+ {}
282
+ );
283
+ this.sendMetric(true);
284
+ this.resolveReachabilityPromise();
285
+ }
286
+ }
287
+
262
288
  /**
263
289
  * Returns statistics about last reachability results. The returned value is an object
264
290
  * with a flat list of properties so that it can be easily sent with metrics
@@ -637,9 +663,10 @@ export default class Reachability extends EventsScope {
637
663
  /**
638
664
  * Sends a metric with all the statistics about how long reachability took
639
665
  *
666
+ * @param {boolean} aborted true if the reachability checks were aborted
640
667
  * @returns {void}
641
668
  */
642
- protected async sendMetric() {
669
+ protected async sendMetric(aborted = false) {
643
670
  const results = [];
644
671
 
645
672
  Object.values(this.clusterReachability).forEach((clusterReachability) => {
@@ -650,6 +677,7 @@ export default class Reachability extends EventsScope {
650
677
  });
651
678
 
652
679
  const stats = {
680
+ aborted,
653
681
  vmn: {
654
682
  udp: this.getStatistics(results, 'udp', true),
655
683
  },
@@ -0,0 +1,114 @@
1
+ import sinon from 'sinon';
2
+ import {assert} from '@webex/test-helper-chai';
3
+
4
+ import testUtils from '../../../utils/testUtils';
5
+ import {BrbState, createBrbState} from '@webex/plugin-meetings/src/meeting/brbState';
6
+
7
+ describe('plugin-meetings', () => {
8
+ let meeting: any;
9
+ let brbState: BrbState;
10
+
11
+ beforeEach(async () => {
12
+ meeting = {
13
+ isMultistream: true,
14
+ locusUrl: 'locus url',
15
+ deviceUrl: 'device url',
16
+ selfId: 'self id',
17
+ mediaProperties: {
18
+ webrtcMediaConnection: true,
19
+ },
20
+ sendSlotManager: {
21
+ setSourceStateOverride: sinon.stub(),
22
+ },
23
+ meetingRequest: {
24
+ setBrb: sinon.stub().resolves(),
25
+ },
26
+ };
27
+
28
+ brbState = new BrbState(meeting, false);
29
+ await testUtils.flushPromises();
30
+ });
31
+
32
+ describe('brbState library', () => {
33
+ it('takes into account current status when instantiated', async () => {
34
+ // create a new BrbState instance
35
+ brbState = createBrbState(meeting, true);
36
+ await testUtils.flushPromises();
37
+
38
+ assert.isTrue(brbState.state.client.enabled);
39
+
40
+ // now check the opposite case
41
+ brbState = createBrbState(meeting, false);
42
+ await testUtils.flushPromises();
43
+
44
+ assert.isFalse(brbState.state.client.enabled);
45
+ });
46
+
47
+ it('can be enabled', async () => {
48
+ brbState.enable(true, meeting.sendSlotManager);
49
+ brbState.handleServerBrbUpdate(true);
50
+ await testUtils.flushPromises();
51
+
52
+ assert.isTrue(brbState.state.client.enabled);
53
+ assert.isTrue(brbState.state.server.enabled);
54
+ });
55
+
56
+ it('can be disabled', async () => {
57
+ brbState.enable(false, meeting.sendSlotManager);
58
+ brbState.handleServerBrbUpdate(false);
59
+ await testUtils.flushPromises();
60
+
61
+ assert.isFalse(brbState.state.client.enabled);
62
+ assert.isFalse(brbState.state.server.enabled);
63
+ });
64
+
65
+ it('does not send local brb state to server if it is not a multistream meeting', async () => {
66
+ meeting.isMultistream = false;
67
+ brbState.enable(true, meeting.sendSlotManager);
68
+ brbState.handleServerBrbUpdate(true);
69
+ await testUtils.flushPromises();
70
+
71
+ assert.isTrue(meeting.meetingRequest.setBrb.notCalled);
72
+ });
73
+
74
+ it('does not send local brb state to server if webrtc media connection is not defined', async () => {
75
+ meeting.mediaProperties.webrtcMediaConnection = undefined;
76
+ brbState.enable(true, meeting.sendSlotManager);
77
+ brbState.handleServerBrbUpdate(true);
78
+ await testUtils.flushPromises();
79
+
80
+ assert.isTrue(meeting.meetingRequest.setBrb.notCalled);
81
+ });
82
+
83
+ it('does not send request twice when in progress', async () => {
84
+ brbState.state.syncToServerInProgress = true;
85
+ brbState.enable(true, meeting.sendSlotManager);
86
+ await testUtils.flushPromises();
87
+
88
+ assert.isTrue(meeting.meetingRequest.setBrb.notCalled);
89
+ });
90
+
91
+ it('syncs with server when client state does not match server state', async () => {
92
+ brbState.enable(true, meeting.sendSlotManager);
93
+ brbState.handleServerBrbUpdate(true);
94
+ await testUtils.flushPromises();
95
+
96
+ assert.isTrue(meeting.meetingRequest.setBrb.calledOnce);
97
+ });
98
+
99
+ it('sets source state override when client state does not match server state', async () => {
100
+ brbState.enable(true, meeting.sendSlotManager);
101
+ brbState.handleServerBrbUpdate(true);
102
+ await testUtils.flushPromises();
103
+
104
+ assert.isTrue(meeting.sendSlotManager.setSourceStateOverride.calledOnce);
105
+ });
106
+
107
+ it('handles server update', async () => {
108
+ brbState.handleServerBrbUpdate(true);
109
+ await testUtils.flushPromises();
110
+
111
+ assert.isTrue(brbState.state.server.enabled);
112
+ });
113
+ });
114
+ });
@@ -114,6 +114,7 @@ import {ERROR_DESCRIPTIONS} from '@webex/internal-plugin-metrics/src/call-diagno
114
114
  import MeetingCollection from '@webex/plugin-meetings/src/meetings/collection';
115
115
 
116
116
  import {EVENT_TRIGGERS as VOICEAEVENTS} from '@webex/internal-plugin-voicea';
117
+ import { createBrbState } from '@webex/plugin-meetings/src/meeting/brbState';
117
118
  import JoinForbiddenError from '../../../../src/common/errors/join-forbidden-error';
118
119
 
119
120
  describe('plugin-meetings', () => {
@@ -246,6 +247,7 @@ describe('plugin-meetings', () => {
246
247
  isAnyPublicClusterReachable: sinon.stub().resolves(true),
247
248
  getReachabilityResults: sinon.stub().resolves(undefined),
248
249
  getReachabilityMetrics: sinon.stub().resolves({}),
250
+ stopReachability: sinon.stub(),
249
251
  };
250
252
  webex.internal.llm.on = sinon.stub();
251
253
  webex.internal.newMetrics.callDiagnosticLatencies = new CallDiagnosticLatencies(
@@ -2095,6 +2097,7 @@ describe('plugin-meetings', () => {
2095
2097
  someReachabilityMetric1: 'some value1',
2096
2098
  someReachabilityMetric2: 'some value2',
2097
2099
  }),
2100
+ stopReachability: sinon.stub(),
2098
2101
  };
2099
2102
 
2100
2103
  const forceRtcMetricsSend = sinon.stub().resolves();
@@ -2514,6 +2517,7 @@ describe('plugin-meetings', () => {
2514
2517
  assert.calledOnce(meeting.setMercuryListener);
2515
2518
  assert.calledOnce(fakeMediaConnection.initiateOffer);
2516
2519
  assert.equal(meeting.allowMediaInLobby, allowMediaInLobby);
2520
+ assert.calledOnce(webex.meetings.reachability.stopReachability);
2517
2521
  };
2518
2522
 
2519
2523
  it('should attach the media and return promise', async () => {
@@ -2709,6 +2713,7 @@ describe('plugin-meetings', () => {
2709
2713
  webex.meetings.reachability = {
2710
2714
  isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
2711
2715
  getReachabilityMetrics: sinon.stub().resolves(),
2716
+ stopReachability: sinon.stub(),
2712
2717
  };
2713
2718
  const MOCK_CLIENT_ERROR_CODE = 2004;
2714
2719
  const generateClientErrorCodeForIceFailureStub = sinon
@@ -2917,6 +2922,7 @@ describe('plugin-meetings', () => {
2917
2922
  .onCall(2)
2918
2923
  .resolves(false),
2919
2924
  getReachabilityMetrics: sinon.stub().resolves({}),
2925
+ stopReachability: sinon.stub(),
2920
2926
  };
2921
2927
  const getErrorPayloadForClientErrorCodeStub =
2922
2928
  (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
@@ -3211,6 +3217,7 @@ describe('plugin-meetings', () => {
3211
3217
  someReachabilityMetric1: 'some value1',
3212
3218
  someReachabilityMetric2: 'some value2',
3213
3219
  }),
3220
+ stopReachability: sinon.stub(),
3214
3221
  };
3215
3222
  meeting.iceCandidatesCount = 3;
3216
3223
  meeting.iceCandidateErrors.set('701_error', 3);
@@ -3715,6 +3722,7 @@ describe('plugin-meetings', () => {
3715
3722
 
3716
3723
  webex.meetings.reachability = {
3717
3724
  isWebexMediaBackendUnreachable: sinon.stub().resolves(unreachable || false),
3725
+ stopReachability: sinon.stub(),
3718
3726
  };
3719
3727
 
3720
3728
  const generateClientErrorCodeForIceFailureStub = sinon
@@ -3812,7 +3820,6 @@ describe('plugin-meetings', () => {
3812
3820
  };
3813
3821
 
3814
3822
  beforeEach(() => {
3815
- meeting.meetingRequest.setBrb = sinon.stub().resolves({body: 'test'});
3816
3823
  meeting.mediaProperties.webrtcMediaConnection = {createSendSlot: sinon.stub()};
3817
3824
  meeting.sendSlotManager.createSlot(
3818
3825
  fakeMultistreamRoapMediaConnection,
@@ -3822,6 +3829,8 @@ describe('plugin-meetings', () => {
3822
3829
  meeting.locusUrl = 'locus url';
3823
3830
  meeting.deviceUrl = 'device url';
3824
3831
  meeting.selfId = 'self id';
3832
+ meeting.brbState = createBrbState(meeting, false);
3833
+ meeting.brbState.enable = sinon.stub().resolves();
3825
3834
  });
3826
3835
 
3827
3836
  afterEach(() => {
@@ -3843,7 +3852,7 @@ describe('plugin-meetings', () => {
3843
3852
 
3844
3853
  await brbResult;
3845
3854
  assert.exists(brbResult.then);
3846
- assert.calledOnce(meeting.meetingRequest.setBrb);
3855
+ assert.calledOnce(meeting.brbState.enable);
3847
3856
  })
3848
3857
 
3849
3858
  it('should disable #beRightBack and return a promise', async () => {
@@ -3851,12 +3860,12 @@ describe('plugin-meetings', () => {
3851
3860
 
3852
3861
  await brbResult;
3853
3862
  assert.exists(brbResult.then);
3854
- assert.calledOnce(meeting.meetingRequest.setBrb);
3863
+ assert.calledOnce(meeting.brbState.enable);
3855
3864
  })
3856
3865
 
3857
3866
  it('should throw an error and reject the promise if setBrb fails', async () => {
3858
3867
  const error = new Error('setBrb failed');
3859
- meeting.meetingRequest.setBrb.rejects(error);
3868
+ meeting.brbState.enable.rejects(error);
3860
3869
 
3861
3870
  try {
3862
3871
  await meeting.beRightBack(true);
@@ -3867,27 +3876,6 @@ describe('plugin-meetings', () => {
3867
3876
  }
3868
3877
  })
3869
3878
  });
3870
-
3871
- describe('when in a transcoded meeting', () => {
3872
-
3873
- beforeEach(() => {
3874
- meeting.isMultistream = false;
3875
- });
3876
-
3877
- it('should ignore enabling #beRightBack', async () => {
3878
- meeting.beRightBack(true);
3879
-
3880
- assert.isRejected((Promise.reject()));
3881
- assert.notCalled(meeting.meetingRequest.setBrb);
3882
- })
3883
-
3884
- it('should ignore disabling #beRightBack', async () => {
3885
- meeting.beRightBack(false);
3886
-
3887
- assert.isRejected((Promise.reject()));
3888
- assert.notCalled(meeting.meetingRequest.setBrb);
3889
- })
3890
- });
3891
3879
  });
3892
3880
 
3893
3881
  /* This set of tests are like semi-integration tests, they use real MuteState, Media, LocusMediaRequest and Roap classes.
@@ -9230,6 +9218,7 @@ describe('plugin-meetings', () => {
9230
9218
 
9231
9219
  it('listens to the brb state changed event', () => {
9232
9220
  const assertBrb = (enabled) => {
9221
+ meeting.brbState = createBrbState(meeting, false);
9233
9222
  meeting.locusInfo.emit(
9234
9223
  { function: 'test', file: 'test' },
9235
9224
  LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,