@webex/plugin-meetings 3.8.0-next.30 → 3.8.0-next.32

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.
Files changed (36) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/interpretation/index.js +1 -1
  4. package/dist/interpretation/siLanguage.js +1 -1
  5. package/dist/media/index.js +3 -15
  6. package/dist/media/index.js.map +1 -1
  7. package/dist/meeting/index.js +196 -189
  8. package/dist/meeting/index.js.map +1 -1
  9. package/dist/meeting/muteState.js +0 -2
  10. package/dist/meeting/muteState.js.map +1 -1
  11. package/dist/reconnection-manager/index.js +2 -2
  12. package/dist/reconnection-manager/index.js.map +1 -1
  13. package/dist/roap/index.js.map +1 -1
  14. package/dist/roap/turnDiscovery.js +31 -23
  15. package/dist/roap/turnDiscovery.js.map +1 -1
  16. package/dist/roap/types.js +17 -0
  17. package/dist/roap/types.js.map +1 -0
  18. package/dist/types/meeting/index.d.ts +2 -1
  19. package/dist/types/meeting/muteState.d.ts +0 -1
  20. package/dist/types/roap/index.d.ts +3 -2
  21. package/dist/types/roap/turnDiscovery.d.ts +1 -17
  22. package/dist/types/roap/types.d.ts +16 -0
  23. package/dist/webinar/index.js +1 -1
  24. package/package.json +3 -3
  25. package/src/media/index.ts +5 -21
  26. package/src/meeting/index.ts +13 -7
  27. package/src/meeting/muteState.ts +0 -2
  28. package/src/reconnection-manager/index.ts +2 -2
  29. package/src/roap/index.ts +3 -7
  30. package/src/roap/turnDiscovery.ts +21 -35
  31. package/src/roap/types.ts +23 -0
  32. package/test/unit/spec/media/index.ts +6 -16
  33. package/test/unit/spec/meeting/index.js +32 -21
  34. package/test/unit/spec/meeting/muteState.js +0 -2
  35. package/test/unit/spec/reconnection-manager/index.js +4 -4
  36. package/test/unit/spec/roap/turnDiscovery.ts +72 -28
@@ -4,11 +4,11 @@ import {Defer} from '@webex/common';
4
4
  import Metrics from '../metrics';
5
5
  import BEHAVIORAL_METRICS from '../metrics/constants';
6
6
  import LoggerProxy from '../common/logs/logger-proxy';
7
- import {ROAP, Enum} from '../constants';
7
+ import {ROAP} from '../constants';
8
8
 
9
9
  import RoapRequest from './request';
10
10
  import Meeting from '../meeting';
11
- import MeetingUtil from '../meeting/util';
11
+ import {TurnDiscoverySkipReason, TurnServerInfo, TurnDiscoveryResult} from './types';
12
12
 
13
13
  const TURN_DISCOVERY_TIMEOUT = 10; // in seconds
14
14
 
@@ -18,28 +18,6 @@ const TURN_DISCOVERY_TIMEOUT = 10; // in seconds
18
18
  // and do the SDP offer with seq=1
19
19
  const TURN_DISCOVERY_SEQ = 0;
20
20
 
21
- const TurnDiscoverySkipReason = {
22
- missingHttpResponse: 'missing http response', // when we asked for the TURN discovery response to be in the http response, but it wasn't there
23
- reachability: 'reachability', // when udp reachability to public clusters is ok, so we don't need TURN (this doens't apply when joinWithMedia() is used)
24
- alreadyInProgress: 'already in progress', // when we try to start TURN discovery while it's already in progress
25
- } as const;
26
-
27
- export type TurnDiscoverySkipReason =
28
- | Enum<typeof TurnDiscoverySkipReason> // this is a kind of FYI, because in practice typescript will infer the type of TurnDiscoverySkipReason as a string
29
- | string // used in case of errors, contains the error message
30
- | undefined; // used when TURN discovery is not skipped
31
-
32
- export type TurnServerInfo = {
33
- url: string;
34
- username: string;
35
- password: string;
36
- };
37
-
38
- export type TurnDiscoveryResult = {
39
- turnServerInfo?: TurnServerInfo;
40
- turnDiscoverySkippedReason: TurnDiscoverySkipReason;
41
- };
42
-
43
21
  /**
44
22
  * Handles the process of finding out TURN server information from Linus.
45
23
  * This is achieved by sending a TURN_DISCOVERY_REQUEST.
@@ -61,7 +39,7 @@ export default class TurnDiscovery {
61
39
  constructor(roapRequest: RoapRequest) {
62
40
  this.roapRequest = roapRequest;
63
41
  this.turnInfo = {
64
- url: '',
42
+ urls: [],
65
43
  username: '',
66
44
  password: '',
67
45
  };
@@ -134,21 +112,27 @@ export default class TurnDiscovery {
134
112
  }
135
113
 
136
114
  const expectedHeaders = [
137
- {headerName: 'x-cisco-turn-url', field: 'url'},
138
- {headerName: 'x-cisco-turn-username', field: 'username'},
139
- {headerName: 'x-cisco-turn-password', field: 'password'},
115
+ {headerName: 'x-cisco-turn-url', field: 'urls', multipleAllowed: true},
116
+ {headerName: 'x-cisco-turn-username', field: 'username', multipleAllowed: false},
117
+ {headerName: 'x-cisco-turn-password', field: 'password', multipleAllowed: false},
140
118
  ];
141
119
 
142
- let foundHeaders = 0;
120
+ const foundHeaders = {};
143
121
 
144
122
  headers?.forEach((receivedHeader) => {
145
123
  // check if it matches any of our expected headers
146
124
  expectedHeaders.forEach((expectedHeader) => {
147
125
  if (receivedHeader.startsWith(`${expectedHeader.headerName}=`)) {
148
- this.turnInfo[expectedHeader.field] = receivedHeader.substring(
149
- expectedHeader.headerName.length + 1
150
- );
151
- foundHeaders += 1;
126
+ foundHeaders[expectedHeader.headerName] = true;
127
+
128
+ const headerValue = receivedHeader.substring(expectedHeader.headerName.length + 1);
129
+
130
+ if (expectedHeader.multipleAllowed) {
131
+ this.turnInfo[expectedHeader.field].push(headerValue);
132
+ } else {
133
+ // just store the last one we find
134
+ this.turnInfo[expectedHeader.field] = headerValue;
135
+ }
152
136
  }
153
137
  });
154
138
  });
@@ -156,7 +140,7 @@ export default class TurnDiscovery {
156
140
  clearTimeout(this.responseTimer);
157
141
  this.responseTimer = undefined;
158
142
 
159
- if (foundHeaders !== expectedHeaders.length) {
143
+ if (expectedHeaders.some((header) => !foundHeaders[header.headerName])) {
160
144
  LoggerProxy.logger.warn(
161
145
  `Roap:turnDiscovery#handleTurnDiscoveryResponse --> missing some headers, received ${from}: ${JSON.stringify(
162
146
  headers
@@ -169,9 +153,11 @@ export default class TurnDiscovery {
169
153
  );
170
154
  } else {
171
155
  LoggerProxy.logger.info(
172
- `Roap:turnDiscovery#handleTurnDiscoveryResponse --> received a valid response ${from}, url=${this.turnInfo.url}`
156
+ `Roap:turnDiscovery#handleTurnDiscoveryResponse --> received a valid response ${from}, urls=${this.turnInfo.urls}`
173
157
  );
174
158
 
159
+ this.turnInfo.urls = this.turnInfo.urls.filter((url) => url !== ''); // remove empty urls, we might get them if we land on video-mesh nodes (VMN)
160
+
175
161
  this.defer.resolve({isOkRequired: !headers?.includes('noOkInTransaction')});
176
162
  }
177
163
  }
@@ -0,0 +1,23 @@
1
+ import {Enum} from '../constants';
2
+
3
+ export const TurnDiscoverySkipReason = {
4
+ missingHttpResponse: 'missing http response', // when we asked for the TURN discovery response to be in the http response, but it wasn't there
5
+ reachability: 'reachability', // when udp reachability to public clusters is ok, so we don't need TURN (this doens't apply when joinWithMedia() is used)
6
+ alreadyInProgress: 'already in progress', // when we try to start TURN discovery while it's already in progress
7
+ } as const;
8
+
9
+ export type TurnDiscoverySkipReason =
10
+ | Enum<typeof TurnDiscoverySkipReason> // this is a kind of FYI, because in practice typescript will infer the type of TurnDiscoverySkipReason as a string
11
+ | string // used in case of errors, contains the error message
12
+ | undefined; // used when TURN discovery is not skipped
13
+
14
+ export type TurnServerInfo = {
15
+ urls: string[];
16
+ username: string;
17
+ password: string;
18
+ };
19
+
20
+ export type TurnDiscoveryResult = {
21
+ turnServerInfo?: TurnServerInfo;
22
+ turnDiscoverySkippedReason: TurnDiscoverySkipReason;
23
+ };
@@ -79,7 +79,7 @@ describe('createMediaConnection', () => {
79
79
  enableRtx: ENABLE_RTX,
80
80
  enableExtmap: ENABLE_EXTMAP,
81
81
  turnServerInfo: {
82
- url: 'turns:turn-server-url:443?transport=tcp',
82
+ urls: ['turns:turn-server-url-1:443?transport=tcp', 'turns:turn-server-url-2:443?transport=tcp'],
83
83
  username: 'turn username',
84
84
  password: 'turn password',
85
85
  },
@@ -91,12 +91,7 @@ describe('createMediaConnection', () => {
91
91
  {
92
92
  iceServers: [
93
93
  {
94
- urls: 'turn:turn-server-url:5004?transport=tcp',
95
- username: 'turn username',
96
- credential: 'turn password',
97
- },
98
- {
99
- urls: 'turns:turn-server-url:443?transport=tcp',
94
+ urls: ['turns:turn-server-url-1:443?transport=tcp', 'turns:turn-server-url-2:443?transport=tcp'],
100
95
  username: 'turn username',
101
96
  credential: 'turn password',
102
97
  },
@@ -159,7 +154,7 @@ describe('createMediaConnection', () => {
159
154
  },
160
155
  rtcMetrics,
161
156
  turnServerInfo: {
162
- url: 'turns:turn-server-url:443?transport=tcp',
157
+ urls: ['turns:turn-server-url-1:443?transport=tcp', 'turns:turn-server-url-2:443?transport=tcp'],
163
158
  username: 'turn username',
164
159
  password: 'turn password',
165
160
  },
@@ -171,12 +166,7 @@ describe('createMediaConnection', () => {
171
166
  {
172
167
  iceServers: [
173
168
  {
174
- urls: 'turn:turn-server-url:5004?transport=tcp',
175
- username: 'turn username',
176
- credential: 'turn password',
177
- },
178
- {
179
- urls: 'turns:turn-server-url:443?transport=tcp',
169
+ urls: ['turns:turn-server-url-1:443?transport=tcp', 'turns:turn-server-url-2:443?transport=tcp'],
180
170
  username: 'turn username',
181
171
  credential: 'turn password',
182
172
  },
@@ -212,7 +202,7 @@ describe('createMediaConnection', () => {
212
202
  {testCase: 'turnServerInfo is undefined', turnServerInfo: undefined},
213
203
  {
214
204
  testCase: 'turnServerInfo.url is empty string',
215
- turnServerInfo: {url: '', username: 'turn username', password: 'turn password'},
205
+ turnServerInfo: {urls: [], username: 'turn username', password: 'turn password'},
216
206
  },
217
207
  ].forEach(({testCase, turnServerInfo}) => {
218
208
  it(`passes empty ICE servers array to MultistreamRoapMediaConnection if ${testCase} (multistream enabled)`, () => {
@@ -276,7 +266,7 @@ describe('createMediaConnection', () => {
276
266
  {testCase: 'turnServerInfo is undefined', turnServerInfo: undefined},
277
267
  {
278
268
  testCase: 'turnServerInfo.url is empty string',
279
- turnServerInfo: {url: '', username: 'turn username', password: 'turn password'},
269
+ turnServerInfo: {urls: [], username: 'turn username', password: 'turn password'},
280
270
  },
281
271
  ].forEach(({testCase, turnServerInfo}) => {
282
272
  it(`passes empty ICE servers array to RoapMediaConnection if ${testCase} (multistream disabled)`, () => {
@@ -2664,7 +2664,7 @@ describe('plugin-meetings', () => {
2664
2664
 
2665
2665
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
2666
2666
  turnServerInfo: {
2667
- url: FAKE_TURN_URL,
2667
+ urls: [FAKE_TURN_URL],
2668
2668
  username: FAKE_TURN_USER,
2669
2669
  password: FAKE_TURN_PASSWORD,
2670
2670
  },
@@ -2686,7 +2686,7 @@ describe('plugin-meetings', () => {
2686
2686
  meeting.id,
2687
2687
  sinon.match({
2688
2688
  turnServerInfo: {
2689
- url: FAKE_TURN_URL,
2689
+ urls: [FAKE_TURN_URL],
2690
2690
  username: FAKE_TURN_USER,
2691
2691
  password: FAKE_TURN_PASSWORD,
2692
2692
  },
@@ -2744,7 +2744,7 @@ describe('plugin-meetings', () => {
2744
2744
  .onSecondCall()
2745
2745
  .returns({
2746
2746
  turnServerInfo: {
2747
- url: FAKE_TURN_URL,
2747
+ urls: [FAKE_TURN_URL],
2748
2748
  username: FAKE_TURN_USER,
2749
2749
  password: FAKE_TURN_PASSWORD,
2750
2750
  },
@@ -2956,7 +2956,7 @@ describe('plugin-meetings', () => {
2956
2956
  .onSecondCall()
2957
2957
  .returns({
2958
2958
  turnServerInfo: {
2959
- url: FAKE_TURN_URL,
2959
+ urls: [FAKE_TURN_URL],
2960
2960
  username: FAKE_TURN_USER,
2961
2961
  password: FAKE_TURN_PASSWORD,
2962
2962
  },
@@ -3133,7 +3133,7 @@ describe('plugin-meetings', () => {
3133
3133
  .onSecondCall()
3134
3134
  .returns({
3135
3135
  turnServerInfo: {
3136
- url: FAKE_TURN_URL,
3136
+ urls: [FAKE_TURN_URL],
3137
3137
  username: FAKE_TURN_USER,
3138
3138
  password: FAKE_TURN_PASSWORD,
3139
3139
  },
@@ -3185,7 +3185,7 @@ describe('plugin-meetings', () => {
3185
3185
  .onSecondCall()
3186
3186
  .returns({
3187
3187
  turnServerInfo: {
3188
- url: FAKE_TURN_URL,
3188
+ urls: [FAKE_TURN_URL],
3189
3189
  username: FAKE_TURN_USER,
3190
3190
  password: FAKE_TURN_PASSWORD,
3191
3191
  },
@@ -3637,7 +3637,7 @@ describe('plugin-meetings', () => {
3637
3637
 
3638
3638
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3639
3639
  turnServerInfo: {
3640
- url: FAKE_TURN_URL,
3640
+ urls: [FAKE_TURN_URL],
3641
3641
  username: FAKE_TURN_USER,
3642
3642
  password: FAKE_TURN_PASSWORD,
3643
3643
  },
@@ -3663,7 +3663,7 @@ describe('plugin-meetings', () => {
3663
3663
  meeting.id,
3664
3664
  sinon.match({
3665
3665
  turnServerInfo: {
3666
- url: FAKE_TURN_URL,
3666
+ urls: [FAKE_TURN_URL],
3667
3667
  username: FAKE_TURN_USER,
3668
3668
  password: FAKE_TURN_PASSWORD,
3669
3669
  },
@@ -3887,6 +3887,22 @@ describe('plugin-meetings', () => {
3887
3887
  assert.isRejected((Promise.reject()));
3888
3888
  }
3889
3889
  });
3890
+
3891
+ it('updates remote mute state when brb is enabled', async () => {
3892
+ meeting.audio = { handleServerRemoteMuteUpdate: sinon.stub() };
3893
+
3894
+ await meeting.beRightBack(true);
3895
+
3896
+ sinon.assert.calledOnceWithExactly(meeting.audio.handleServerRemoteMuteUpdate, meeting, true);
3897
+ });
3898
+
3899
+ it('does not update remote mute state when brb is disabled', async () => {
3900
+ meeting.audio = { handleServerRemoteMuteUpdate: sinon.stub() };
3901
+
3902
+ await meeting.beRightBack(false);
3903
+
3904
+ assert.notCalled(meeting.audio.handleServerRemoteMuteUpdate);
3905
+ });
3890
3906
  });
3891
3907
  });
3892
3908
 
@@ -3940,7 +3956,7 @@ describe('plugin-meetings', () => {
3940
3956
  .resolves({id: 'fake clientMediaPreferences'});
3941
3957
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3942
3958
  turnServerInfo: {
3943
- url: 'turns:turn-server-url:443?transport=tcp',
3959
+ urls: ['turns:turn-server-url1:443?transport=tcp', 'turns:turn-server-url2:443?transport=tcp'],
3944
3960
  username: 'turn user',
3945
3961
  password: 'turn password',
3946
3962
  },
@@ -3958,12 +3974,7 @@ describe('plugin-meetings', () => {
3958
3974
  expectedMediaConnectionConfig = {
3959
3975
  iceServers: [
3960
3976
  {
3961
- urls: 'turn:turn-server-url:5004?transport=tcp',
3962
- username: 'turn user',
3963
- credential: 'turn password',
3964
- },
3965
- {
3966
- urls: 'turns:turn-server-url:443?transport=tcp',
3977
+ urls: ['turns:turn-server-url1:443?transport=tcp', 'turns:turn-server-url2:443?transport=tcp'],
3967
3978
  username: 'turn user',
3968
3979
  credential: 'turn password',
3969
3980
  },
@@ -5231,7 +5242,7 @@ describe('plugin-meetings', () => {
5231
5242
  // and check that when we fallback to transcoded we still do another TURN discovery
5232
5243
  await runCheck(
5233
5244
  {
5234
- url: 'turns:turn-server-url:443?transport=tcp',
5245
+ urls: ['turns:turn-server-url1:443?transport=tcp', 'turns:turn-server-url2:443?transport=tcp'],
5235
5246
  username: 'turn user',
5236
5247
  password: 'turn password',
5237
5248
  },
@@ -5245,7 +5256,7 @@ describe('plugin-meetings', () => {
5245
5256
  // but doing it just for completeness
5246
5257
  await runCheck(
5247
5258
  {
5248
- url: 'turns:turn-server-url:443?transport=tcp',
5259
+ urls: ['turns:turn-server-url1:443?transport=tcp', 'turns:turn-server-url2:443?transport=tcp'],
5249
5260
  username: 'turn user',
5250
5261
  password: 'turn password',
5251
5262
  },
@@ -7527,19 +7538,19 @@ describe('plugin-meetings', () => {
7527
7538
  });
7528
7539
  });
7529
7540
 
7530
- describe('#setIsoLocalClientMeetingJoinTime', () => {
7541
+ describe('#setIsoLocalClientMeetingJoinTime', () => {
7531
7542
  it('should fallback to system clock ISO string when given an undefined value', () => {
7532
7543
  const currentSystemTime = new Date().toISOString();
7533
7544
  meeting.isoLocalClientMeetingJoinTime = undefined;
7534
7545
  assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
7535
7546
  });
7536
-
7547
+
7537
7548
  it('should fallback to system clock ISO string when given an invalid value', () => {
7538
7549
  const currentSystemTime = new Date().toISOString();
7539
7550
  meeting.isoLocalClientMeetingJoinTime = 'invalid-date';
7540
7551
  assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
7541
7552
  });
7542
-
7553
+
7543
7554
  it('should set the isoLocalClientMeetingJoinTime correctly for a valid date string', () => {
7544
7555
  const validDateString = 'Tue, 01 Apr 2025 13:00:36 GMT';
7545
7556
  const expectedISOString = new Date(validDateString).toISOString();
@@ -8699,7 +8710,7 @@ describe('plugin-meetings', () => {
8699
8710
  meeting.deferSDPAnswer = {
8700
8711
  reject: sinon.stub(),
8701
8712
  };
8702
-
8713
+
8703
8714
  const clearTimeoutSpy = sinon.spy(clock, 'clearTimeout');
8704
8715
 
8705
8716
  const fakeError = new Errors.SdpAnswerHandlingError(fakeErrorMessage, {
@@ -2,7 +2,6 @@ import sinon from 'sinon';
2
2
  import {assert} from '@webex/test-helper-chai';
3
3
  import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
4
4
  import {createMuteState, MuteState} from '@webex/plugin-meetings/src/meeting/muteState';
5
- import PermissionError from '@webex/plugin-meetings/src/common/errors/permission';
6
5
  import {AUDIO, VIDEO} from '@webex/plugin-meetings/src/constants';
7
6
 
8
7
  import testUtils from '../../../utils/testUtils';
@@ -39,7 +38,6 @@ describe('plugin-meetings', () => {
39
38
  unmuteAllowed: true,
40
39
  remoteVideoMuted: false,
41
40
  unmuteVideoAllowed: true,
42
-
43
41
  locusInfo: {
44
42
  handleLocusDelta: sinon.stub(),
45
43
  },
@@ -60,7 +60,7 @@ describe('plugin-meetings', () => {
60
60
  roap: {
61
61
  doTurnDiscovery: sinon.stub().resolves({
62
62
  turnServerInfo: {
63
- url: 'fake_turn_url',
63
+ urls: ['fake_turn_url1', 'fake_turn_url2'],
64
64
  username: 'fake_turn_username',
65
65
  password: 'fake_turn_password',
66
66
  },
@@ -137,7 +137,7 @@ describe('plugin-meetings', () => {
137
137
  assert.calledOnce(fakeMediaConnection.reconnect);
138
138
  assert.calledWith(fakeMediaConnection.reconnect, [
139
139
  {
140
- urls: 'fake_turn_url',
140
+ urls: ['fake_turn_url1', 'fake_turn_url2'],
141
141
  username: 'fake_turn_username',
142
142
  credential: 'fake_turn_password',
143
143
  },
@@ -152,12 +152,12 @@ describe('plugin-meetings', () => {
152
152
  });
153
153
 
154
154
  // this can happen when we land on a video mesh node
155
- it('does not use TURN server if TURN url is an empty string', async () => {
155
+ it('does not use TURN server if TURN urls is an empty array', async () => {
156
156
  const rm = new ReconnectionManager(fakeMeeting);
157
157
 
158
158
  fakeMeeting.roap.doTurnDiscovery.resolves({
159
159
  turnServerInfo: {
160
- url: '',
160
+ urls: [],
161
161
  username: 'whatever',
162
162
  password: 'whatever',
163
163
  },