@webex/plugin-meetings 3.12.0-next.9 → 3.12.0-next.91

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 (213) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +15 -2
  3. package/dist/aiEnableRequest/index.js.map +1 -1
  4. package/dist/breakouts/breakout.js +8 -3
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +26 -2
  7. package/dist/breakouts/index.js.map +1 -1
  8. package/dist/config.js +2 -2
  9. package/dist/config.js.map +1 -1
  10. package/dist/constants.js +35 -9
  11. package/dist/constants.js.map +1 -1
  12. package/dist/controls-options-manager/constants.js +11 -1
  13. package/dist/controls-options-manager/constants.js.map +1 -1
  14. package/dist/controls-options-manager/index.js +67 -29
  15. package/dist/controls-options-manager/index.js.map +1 -1
  16. package/dist/controls-options-manager/util.js +91 -0
  17. package/dist/controls-options-manager/util.js.map +1 -1
  18. package/dist/hashTree/constants.js +13 -1
  19. package/dist/hashTree/constants.js.map +1 -1
  20. package/dist/hashTree/hashTreeParser.js +880 -382
  21. package/dist/hashTree/hashTreeParser.js.map +1 -1
  22. package/dist/hashTree/utils.js +42 -0
  23. package/dist/hashTree/utils.js.map +1 -1
  24. package/dist/index.js +7 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/interceptors/dataChannelAuthToken.js +75 -15
  27. package/dist/interceptors/dataChannelAuthToken.js.map +1 -1
  28. package/dist/interceptors/locusRetry.js +23 -8
  29. package/dist/interceptors/locusRetry.js.map +1 -1
  30. package/dist/interpretation/index.js +10 -1
  31. package/dist/interpretation/index.js.map +1 -1
  32. package/dist/interpretation/interpretation.types.js +7 -0
  33. package/dist/interpretation/interpretation.types.js.map +1 -0
  34. package/dist/interpretation/siLanguage.js +1 -1
  35. package/dist/locus-info/controlsUtils.js +4 -1
  36. package/dist/locus-info/controlsUtils.js.map +1 -1
  37. package/dist/locus-info/index.js +303 -88
  38. package/dist/locus-info/index.js.map +1 -1
  39. package/dist/locus-info/infoUtils.js +36 -5
  40. package/dist/locus-info/infoUtils.js.map +1 -1
  41. package/dist/locus-info/types.js +19 -0
  42. package/dist/locus-info/types.js.map +1 -1
  43. package/dist/media/index.js +3 -1
  44. package/dist/media/index.js.map +1 -1
  45. package/dist/media/properties.js +1 -0
  46. package/dist/media/properties.js.map +1 -1
  47. package/dist/meeting/in-meeting-actions.js +5 -1
  48. package/dist/meeting/in-meeting-actions.js.map +1 -1
  49. package/dist/meeting/index.js +1233 -793
  50. package/dist/meeting/index.js.map +1 -1
  51. package/dist/meeting/locusMediaRequest.js +229 -82
  52. package/dist/meeting/locusMediaRequest.js.map +1 -1
  53. package/dist/meeting/muteState.js +10 -1
  54. package/dist/meeting/muteState.js.map +1 -1
  55. package/dist/meeting/request.js +5 -2
  56. package/dist/meeting/request.js.map +1 -1
  57. package/dist/meeting/util.js +35 -2
  58. package/dist/meeting/util.js.map +1 -1
  59. package/dist/meeting-info/meeting-info-v2.js +2 -2
  60. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  61. package/dist/meetings/index.js +231 -118
  62. package/dist/meetings/index.js.map +1 -1
  63. package/dist/meetings/meetings.types.js +6 -1
  64. package/dist/meetings/meetings.types.js.map +1 -1
  65. package/dist/meetings/request.js +39 -0
  66. package/dist/meetings/request.js.map +1 -1
  67. package/dist/meetings/util.js +79 -5
  68. package/dist/meetings/util.js.map +1 -1
  69. package/dist/member/index.js +10 -0
  70. package/dist/member/index.js.map +1 -1
  71. package/dist/member/types.js.map +1 -1
  72. package/dist/member/util.js +30 -0
  73. package/dist/member/util.js.map +1 -1
  74. package/dist/members/index.js +101 -43
  75. package/dist/members/index.js.map +1 -1
  76. package/dist/members/util.js +8 -1
  77. package/dist/members/util.js.map +1 -1
  78. package/dist/metrics/constants.js +5 -1
  79. package/dist/metrics/constants.js.map +1 -1
  80. package/dist/multistream/codec/constants.js +63 -0
  81. package/dist/multistream/codec/constants.js.map +1 -0
  82. package/dist/multistream/mediaRequestManager.js +62 -15
  83. package/dist/multistream/mediaRequestManager.js.map +1 -1
  84. package/dist/multistream/receiveSlot.js +9 -0
  85. package/dist/multistream/receiveSlot.js.map +1 -1
  86. package/dist/reachability/index.js +10 -13
  87. package/dist/reachability/index.js.map +1 -1
  88. package/dist/reachability/reachability.types.js +2 -0
  89. package/dist/reachability/reachability.types.js.map +1 -1
  90. package/dist/reachability/util.js +45 -0
  91. package/dist/reachability/util.js.map +1 -1
  92. package/dist/reactions/reactions.type.js.map +1 -1
  93. package/dist/recording-controller/index.js +1 -3
  94. package/dist/recording-controller/index.js.map +1 -1
  95. package/dist/types/config.d.ts +2 -2
  96. package/dist/types/constants.d.ts +12 -1
  97. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  98. package/dist/types/controls-options-manager/index.d.ts +16 -0
  99. package/dist/types/hashTree/constants.d.ts +2 -0
  100. package/dist/types/hashTree/hashTreeParser.d.ts +146 -17
  101. package/dist/types/hashTree/utils.d.ts +18 -0
  102. package/dist/types/index.d.ts +3 -0
  103. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  104. package/dist/types/interpretation/interpretation.types.d.ts +10 -0
  105. package/dist/types/locus-info/index.d.ts +50 -6
  106. package/dist/types/locus-info/infoUtils.d.ts +8 -0
  107. package/dist/types/locus-info/types.d.ts +21 -1
  108. package/dist/types/media/properties.d.ts +1 -0
  109. package/dist/types/meeting/in-meeting-actions.d.ts +4 -0
  110. package/dist/types/meeting/index.d.ts +94 -7
  111. package/dist/types/meeting/locusMediaRequest.d.ts +12 -0
  112. package/dist/types/meeting/request.d.ts +1 -0
  113. package/dist/types/meeting/util.d.ts +9 -0
  114. package/dist/types/meetings/index.d.ts +30 -18
  115. package/dist/types/meetings/meetings.types.d.ts +15 -0
  116. package/dist/types/meetings/request.d.ts +14 -0
  117. package/dist/types/member/index.d.ts +1 -0
  118. package/dist/types/member/types.d.ts +1 -0
  119. package/dist/types/member/util.d.ts +9 -0
  120. package/dist/types/members/index.d.ts +15 -2
  121. package/dist/types/members/util.d.ts +1 -5
  122. package/dist/types/metrics/constants.d.ts +4 -0
  123. package/dist/types/multistream/codec/constants.d.ts +7 -0
  124. package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
  125. package/dist/types/reachability/index.d.ts +1 -1
  126. package/dist/types/reachability/reachability.types.d.ts +7 -0
  127. package/dist/types/reachability/util.d.ts +17 -0
  128. package/dist/types/reactions/reactions.type.d.ts +3 -0
  129. package/dist/webinar/index.js +305 -159
  130. package/dist/webinar/index.js.map +1 -1
  131. package/package.json +22 -22
  132. package/src/aiEnableRequest/index.ts +16 -0
  133. package/src/breakouts/breakout.ts +3 -1
  134. package/src/breakouts/index.ts +31 -0
  135. package/src/config.ts +2 -2
  136. package/src/constants.ts +20 -3
  137. package/src/controls-options-manager/constants.ts +14 -1
  138. package/src/controls-options-manager/index.ts +87 -28
  139. package/src/controls-options-manager/util.ts +81 -1
  140. package/src/hashTree/constants.ts +16 -0
  141. package/src/hashTree/hashTreeParser.ts +580 -196
  142. package/src/hashTree/utils.ts +36 -0
  143. package/src/index.ts +6 -0
  144. package/src/interceptors/dataChannelAuthToken.ts +88 -12
  145. package/src/interceptors/locusRetry.ts +25 -4
  146. package/src/interpretation/index.ts +27 -9
  147. package/src/interpretation/interpretation.types.ts +11 -0
  148. package/src/locus-info/controlsUtils.ts +3 -1
  149. package/src/locus-info/index.ts +304 -100
  150. package/src/locus-info/infoUtils.ts +41 -6
  151. package/src/locus-info/types.ts +25 -1
  152. package/src/media/index.ts +3 -0
  153. package/src/media/properties.ts +1 -0
  154. package/src/meeting/in-meeting-actions.ts +8 -0
  155. package/src/meeting/index.ts +494 -67
  156. package/src/meeting/locusMediaRequest.ts +106 -6
  157. package/src/meeting/muteState.ts +10 -1
  158. package/src/meeting/request.ts +11 -0
  159. package/src/meeting/util.ts +47 -5
  160. package/src/meeting-info/meeting-info-v2.ts +4 -2
  161. package/src/meetings/index.ts +133 -78
  162. package/src/meetings/meetings.types.ts +19 -0
  163. package/src/meetings/request.ts +43 -0
  164. package/src/meetings/util.ts +97 -1
  165. package/src/member/index.ts +10 -0
  166. package/src/member/types.ts +1 -0
  167. package/src/member/util.ts +29 -0
  168. package/src/members/index.ts +70 -7
  169. package/src/members/util.ts +12 -4
  170. package/src/metrics/constants.ts +4 -0
  171. package/src/multistream/codec/constants.ts +58 -0
  172. package/src/multistream/mediaRequestManager.ts +119 -28
  173. package/src/multistream/receiveSlot.ts +18 -0
  174. package/src/reachability/index.ts +19 -15
  175. package/src/reachability/reachability.types.ts +11 -0
  176. package/src/reachability/util.ts +36 -0
  177. package/src/reactions/reactions.type.ts +3 -0
  178. package/src/recording-controller/index.ts +1 -2
  179. package/src/webinar/index.ts +214 -36
  180. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  181. package/test/unit/spec/breakouts/breakout.ts +9 -3
  182. package/test/unit/spec/breakouts/index.ts +49 -0
  183. package/test/unit/spec/controls-options-manager/index.js +244 -29
  184. package/test/unit/spec/controls-options-manager/util.js +165 -0
  185. package/test/unit/spec/hashTree/hashTreeParser.ts +1838 -180
  186. package/test/unit/spec/hashTree/utils.ts +125 -1
  187. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +196 -0
  188. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  189. package/test/unit/spec/interpretation/index.ts +26 -4
  190. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  191. package/test/unit/spec/locus-info/index.js +499 -81
  192. package/test/unit/spec/locus-info/infoUtils.js +184 -1
  193. package/test/unit/spec/media/index.ts +31 -0
  194. package/test/unit/spec/meeting/in-meeting-actions.ts +4 -0
  195. package/test/unit/spec/meeting/index.js +1615 -42
  196. package/test/unit/spec/meeting/locusMediaRequest.ts +302 -4
  197. package/test/unit/spec/meeting/muteState.js +81 -0
  198. package/test/unit/spec/meeting/request.js +12 -0
  199. package/test/unit/spec/meeting/utils.js +157 -1
  200. package/test/unit/spec/meeting-info/meetinginfov2.js +19 -10
  201. package/test/unit/spec/meetings/index.js +363 -36
  202. package/test/unit/spec/meetings/request.js +141 -0
  203. package/test/unit/spec/meetings/utils.js +189 -0
  204. package/test/unit/spec/member/index.js +7 -0
  205. package/test/unit/spec/member/util.js +89 -0
  206. package/test/unit/spec/members/index.js +216 -0
  207. package/test/unit/spec/members/request.js +37 -1
  208. package/test/unit/spec/members/utils.js +63 -0
  209. package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
  210. package/test/unit/spec/reachability/index.ts +59 -46
  211. package/test/unit/spec/reachability/util.ts +31 -0
  212. package/test/unit/spec/recording-controller/index.js +9 -8
  213. package/test/unit/spec/webinar/index.ts +329 -28
@@ -13,11 +13,15 @@ import {
13
13
  import testUtils from '../../../utils/testUtils';
14
14
  import {Defer} from '@webex/common';
15
15
  import {IP_VERSION} from '../../../../src/constants';
16
+ import Metrics from '../../../../src/metrics';
17
+ import BEHAVIORAL_METRICS from '../../../../src/metrics/constants';
16
18
 
17
19
  describe('LocusMediaRequest.send()', () => {
18
20
  let locusMediaRequest: LocusMediaRequest;
19
21
  let webexRequestStub;
20
22
  let mockWebex;
23
+ let sendBehavioralMetricStub;
24
+ const waitForSelfUrlChange = () => Promise.resolve();
21
25
 
22
26
  const fakeLocusResponse = {
23
27
  locus: {something: 'whatever'},
@@ -50,8 +54,16 @@ describe('LocusMediaRequest.send()', () => {
50
54
  },
51
55
  ipver: IP_VERSION.only_ipv4,
52
56
  reachability: {
53
- version: '1',
54
- result: 'some fake reachability result',
57
+ version: 1,
58
+ result: {
59
+ usedDiscoveryOptions: {
60
+ 'early-call-min-clusters': 3,
61
+ },
62
+ metrics: {
63
+ 'total-duration-ms': 10,
64
+ },
65
+ tests: {},
66
+ },
55
67
  },
56
68
  },
57
69
  };
@@ -78,8 +90,16 @@ describe('LocusMediaRequest.send()', () => {
78
90
  timeShot: '2023-05-23T08:03:49Z',
79
91
  },
80
92
  reachability: {
81
- version: '1',
82
- result: 'some fake reachability result',
93
+ version: 1,
94
+ result: {
95
+ usedDiscoveryOptions: {
96
+ 'early-call-min-clusters': 3,
97
+ },
98
+ metrics: {
99
+ 'total-duration-ms': 10,
100
+ },
101
+ tests: {},
102
+ },
83
103
  },
84
104
  },
85
105
  };
@@ -132,6 +152,7 @@ describe('LocusMediaRequest.send()', () => {
132
152
  submitClientEvent: sinon.stub(),
133
153
  },
134
154
  };
155
+ sendBehavioralMetricStub = sinon.stub(Metrics, 'sendBehavioralMetric');
135
156
 
136
157
  locusMediaRequest = new LocusMediaRequest(
137
158
  {
@@ -143,6 +164,8 @@ describe('LocusMediaRequest.send()', () => {
143
164
  correlationId: 'correlationId',
144
165
  meetingId: 'meetingId',
145
166
  preferTranscoding: true,
167
+ getCurrentSelfUrl: () => undefined,
168
+ waitForSelfUrlChange,
146
169
  },
147
170
  {
148
171
  parent: mockWebex,
@@ -151,6 +174,10 @@ describe('LocusMediaRequest.send()', () => {
151
174
  webexRequestStub = sinon.stub(locusMediaRequest, 'request').resolves(fakeLocusResponse);
152
175
  });
153
176
 
177
+ afterEach(() => {
178
+ sendBehavioralMetricStub.restore();
179
+ });
180
+
154
181
  const sendLocalMute = (muteOptions, overrides = {}) =>
155
182
  locusMediaRequest.send({...exampleLocalMuteRequestBody, ...overrides, muteOptions});
156
183
 
@@ -167,6 +194,7 @@ describe('LocusMediaRequest.send()', () => {
167
194
 
168
195
  webexRequestStub.resetHistory();
169
196
  mockWebex.internal.newMetrics.submitClientEvent.resetHistory();
197
+ sendBehavioralMetricStub.resetHistory();
170
198
  };
171
199
 
172
200
  it('sends a roap message', async () => {
@@ -183,6 +211,276 @@ describe('LocusMediaRequest.send()', () => {
183
211
  });
184
212
  });
185
213
 
214
+ it('uses the latest resolved selfUrl when a queued roap message is sent', async () => {
215
+ let currentSelfUrl = 'oldSelfUrl';
216
+ locusMediaRequest = new LocusMediaRequest(
217
+ {
218
+ device: {
219
+ url: 'deviceUrl',
220
+ deviceType: 'deviceType',
221
+ regionCode: 'regionCode',
222
+ },
223
+ correlationId: 'correlationId',
224
+ meetingId: 'meetingId',
225
+ preferTranscoding: true,
226
+ getCurrentSelfUrl: () => currentSelfUrl,
227
+ waitForSelfUrlChange,
228
+ },
229
+ {
230
+ parent: mockWebex,
231
+ }
232
+ );
233
+ webexRequestStub = sinon.stub(locusMediaRequest, 'request').resolves(fakeLocusResponse);
234
+
235
+ const request = cloneDeep(exampleRoapRequestBody);
236
+
237
+ request.selfUrl = 'oldSelfUrl';
238
+ currentSelfUrl = 'newSelfUrl';
239
+
240
+ const result = await locusMediaRequest.send(request);
241
+
242
+ assert.equal(result, fakeLocusResponse);
243
+ assert.calledOnce(webexRequestStub);
244
+ assert.equal(webexRequestStub.getCall(0).args[0].uri, 'newSelfUrl/media');
245
+ assert.calledOnceWithExactly(
246
+ sendBehavioralMetricStub,
247
+ BEHAVIORAL_METRICS.LOCUS_MEDIA_REQUEST_RETRY,
248
+ {
249
+ correlation_id: 'correlationId',
250
+ reason: 'selfUrlUpdatedBeforeMediaRequest',
251
+ }
252
+ );
253
+ });
254
+
255
+ it('retries a roap message with the latest resolved selfUrl after a conflict', async () => {
256
+ let currentSelfUrl = 'oldSelfUrl';
257
+ const conflictError = {statusCode: 409, message: 'conflict'};
258
+
259
+ locusMediaRequest = new LocusMediaRequest(
260
+ {
261
+ device: {
262
+ url: 'deviceUrl',
263
+ deviceType: 'deviceType',
264
+ regionCode: 'regionCode',
265
+ },
266
+ correlationId: 'correlationId',
267
+ meetingId: 'meetingId',
268
+ preferTranscoding: true,
269
+ getCurrentSelfUrl: () => currentSelfUrl,
270
+ waitForSelfUrlChange,
271
+ },
272
+ {
273
+ parent: mockWebex,
274
+ }
275
+ );
276
+ webexRequestStub = sinon.stub(locusMediaRequest, 'request');
277
+ webexRequestStub.onFirstCall().callsFake(() => {
278
+ currentSelfUrl = 'newSelfUrl';
279
+
280
+ return Promise.reject(conflictError);
281
+ });
282
+ webexRequestStub.onSecondCall().resolves(fakeLocusResponse);
283
+
284
+ const request = cloneDeep(exampleRoapRequestBody);
285
+
286
+ request.selfUrl = 'oldSelfUrl';
287
+ request.roapMessage.messageType = 'ANSWER';
288
+
289
+ const result = await locusMediaRequest.send(request);
290
+
291
+ assert.equal(result, fakeLocusResponse);
292
+ assert.calledTwice(webexRequestStub);
293
+ assert.equal(webexRequestStub.getCall(0).args[0].uri, 'oldSelfUrl/media');
294
+ assert.equal(webexRequestStub.getCall(1).args[0].uri, 'newSelfUrl/media');
295
+ assert.calledWith(sendBehavioralMetricStub, BEHAVIORAL_METRICS.LOCUS_MEDIA_REQUEST_RETRY, {
296
+ correlation_id: 'correlationId',
297
+ reason: 'selfUrlUpdatedBeforeMediaRequest',
298
+ });
299
+ // The updated selfUrl is detected twice: once while deciding to retry and
300
+ // again when the request is re-resolved for the actual re-send.
301
+ assert.calledTwice(sendBehavioralMetricStub);
302
+ });
303
+
304
+ it('does not retry a roap conflict when the resolved selfUrl has not changed', async () => {
305
+ locusMediaRequest = new LocusMediaRequest(
306
+ {
307
+ device: {
308
+ url: 'deviceUrl',
309
+ deviceType: 'deviceType',
310
+ regionCode: 'regionCode',
311
+ },
312
+ correlationId: 'correlationId',
313
+ meetingId: 'meetingId',
314
+ preferTranscoding: true,
315
+ getCurrentSelfUrl: () => 'sameSelfUrl',
316
+ waitForSelfUrlChange,
317
+ },
318
+ {
319
+ parent: mockWebex,
320
+ }
321
+ );
322
+ webexRequestStub = sinon.stub(locusMediaRequest, 'request').rejects({
323
+ statusCode: 409,
324
+ message: 'conflict',
325
+ });
326
+
327
+ const request = cloneDeep(exampleRoapRequestBody);
328
+
329
+ request.selfUrl = 'sameSelfUrl';
330
+ request.roapMessage.messageType = 'ANSWER';
331
+
332
+ await assert.isRejected(locusMediaRequest.send(request));
333
+
334
+ assert.calledOnce(webexRequestStub);
335
+ assert.equal(webexRequestStub.getCall(0).args[0].uri, 'sameSelfUrl/media');
336
+ assert.calledWith(sendBehavioralMetricStub, BEHAVIORAL_METRICS.LOCUS_MEDIA_REQUEST_RETRY, {
337
+ correlation_id: 'correlationId',
338
+ reason: 'selfUrlNotChangedAfterWait',
339
+ retryAttempt: 0,
340
+ roapMessageType: 'ANSWER',
341
+ });
342
+ });
343
+
344
+ it('retries a roap conflict when selfUrl changes after waiting', async () => {
345
+ let currentSelfUrl = 'sameSelfUrl';
346
+ const waitForSelfUrlChangeAndUpdate = async () => {
347
+ currentSelfUrl = 'newSelfUrlAfterWait';
348
+ };
349
+ locusMediaRequest = new LocusMediaRequest(
350
+ {
351
+ device: {
352
+ url: 'deviceUrl',
353
+ deviceType: 'deviceType',
354
+ regionCode: 'regionCode',
355
+ },
356
+ correlationId: 'correlationId',
357
+ meetingId: 'meetingId',
358
+ preferTranscoding: true,
359
+ getCurrentSelfUrl: () => currentSelfUrl,
360
+ waitForSelfUrlChange: waitForSelfUrlChangeAndUpdate,
361
+ },
362
+ {
363
+ parent: mockWebex,
364
+ }
365
+ );
366
+ webexRequestStub = sinon.stub(locusMediaRequest, 'request');
367
+ webexRequestStub.onFirstCall().rejects({statusCode: 409, message: 'conflict'});
368
+ webexRequestStub.onSecondCall().resolves(fakeLocusResponse);
369
+
370
+ const request = cloneDeep(exampleRoapRequestBody);
371
+
372
+ request.selfUrl = 'sameSelfUrl';
373
+ request.roapMessage.messageType = 'ANSWER';
374
+
375
+ const result = await locusMediaRequest.send(request);
376
+
377
+ assert.equal(result, fakeLocusResponse);
378
+ assert.calledTwice(webexRequestStub);
379
+ assert.equal(webexRequestStub.getCall(0).args[0].uri, 'sameSelfUrl/media');
380
+ assert.equal(webexRequestStub.getCall(1).args[0].uri, 'newSelfUrlAfterWait/media');
381
+ assert.calledWith(sendBehavioralMetricStub, BEHAVIORAL_METRICS.LOCUS_MEDIA_REQUEST_RETRY, {
382
+ correlation_id: 'correlationId',
383
+ reason: 'selfUrlChangedAfterWait',
384
+ retryAttempt: 0,
385
+ roapMessageType: 'ANSWER',
386
+ });
387
+ });
388
+
389
+ it('retries a local mute request with the latest resolved selfUrl after a conflict', async () => {
390
+ let currentSelfUrl = 'oldSelfUrl';
391
+
392
+ locusMediaRequest = new LocusMediaRequest(
393
+ {
394
+ device: {
395
+ url: 'deviceUrl',
396
+ deviceType: 'deviceType',
397
+ regionCode: 'regionCode',
398
+ },
399
+ correlationId: 'correlationId',
400
+ meetingId: 'meetingId',
401
+ preferTranscoding: true,
402
+ getCurrentSelfUrl: () => currentSelfUrl,
403
+ waitForSelfUrlChange,
404
+ },
405
+ {
406
+ parent: mockWebex,
407
+ }
408
+ );
409
+ webexRequestStub = sinon.stub(locusMediaRequest, 'request').resolves(fakeLocusResponse);
410
+
411
+ // Create the confluence on this instance, otherwise the LocalMute request
412
+ // short-circuits (resolves without sending) while confluence is "not created".
413
+ await ensureConfluenceCreated();
414
+
415
+ webexRequestStub.reset();
416
+ webexRequestStub.onFirstCall().callsFake(() => {
417
+ currentSelfUrl = 'newSelfUrl';
418
+
419
+ return Promise.reject({statusCode: 409, message: 'conflict'});
420
+ });
421
+ webexRequestStub.onSecondCall().resolves(fakeLocusResponse);
422
+
423
+ const result = await locusMediaRequest.send({
424
+ ...exampleLocalMuteRequestBody,
425
+ selfUrl: 'oldSelfUrl',
426
+ muteOptions: {audioMuted: false, videoMuted: true},
427
+ });
428
+
429
+ assert.equal(result, fakeLocusResponse);
430
+ assert.calledTwice(webexRequestStub);
431
+ assert.equal(webexRequestStub.getCall(0).args[0].uri, 'oldSelfUrl/media');
432
+ assert.equal(webexRequestStub.getCall(1).args[0].uri, 'newSelfUrl/media');
433
+ });
434
+
435
+ it('stops retrying roap conflicts after the selfUrl retry limit', async () => {
436
+ let currentSelfUrl = 'oldSelfUrl';
437
+ locusMediaRequest = new LocusMediaRequest(
438
+ {
439
+ device: {
440
+ url: 'deviceUrl',
441
+ deviceType: 'deviceType',
442
+ regionCode: 'regionCode',
443
+ },
444
+ correlationId: 'correlationId',
445
+ meetingId: 'meetingId',
446
+ preferTranscoding: true,
447
+ getCurrentSelfUrl: () => currentSelfUrl,
448
+ waitForSelfUrlChange,
449
+ },
450
+ {
451
+ parent: mockWebex,
452
+ }
453
+ );
454
+ webexRequestStub = sinon.stub(locusMediaRequest, 'request');
455
+ webexRequestStub.onFirstCall().callsFake(() => {
456
+ currentSelfUrl = 'secondSelfUrl';
457
+
458
+ return Promise.reject({statusCode: 409, message: 'conflict'});
459
+ });
460
+ webexRequestStub.onSecondCall().callsFake(() => {
461
+ currentSelfUrl = 'thirdSelfUrl';
462
+
463
+ return Promise.reject({statusCode: 409, message: 'conflict'});
464
+ });
465
+ webexRequestStub.onThirdCall().callsFake(() => {
466
+ currentSelfUrl = 'fourthSelfUrl';
467
+
468
+ return Promise.reject({statusCode: 409, message: 'conflict'});
469
+ });
470
+
471
+ const request = cloneDeep(exampleRoapRequestBody);
472
+
473
+ request.selfUrl = 'oldSelfUrl';
474
+ request.roapMessage.messageType = 'ANSWER';
475
+
476
+ await assert.isRejected(locusMediaRequest.send(request));
477
+
478
+ assert.calledThrice(webexRequestStub);
479
+ assert.equal(webexRequestStub.getCall(0).args[0].uri, 'oldSelfUrl/media');
480
+ assert.equal(webexRequestStub.getCall(1).args[0].uri, 'secondSelfUrl/media');
481
+ assert.equal(webexRequestStub.getCall(2).args[0].uri, 'thirdSelfUrl/media');
482
+ });
483
+
186
484
  it('sends correct metric event when roap message fails', async () => {
187
485
  webexRequestStub.rejects({code: 300, message: 'fake error'});
188
486
  await assert.isRejected(sendRoapMessage('OFFER'));
@@ -11,6 +11,7 @@ describe('plugin-meetings', () => {
11
11
  let audio;
12
12
  let video;
13
13
  let originalRemoteUpdateAudioVideo;
14
+ let originalUpdateLocusFromApiResponse;
14
15
 
15
16
  const fakeLocusResponse = {body: {locus: {info: 'this is a fake locus'}}};
16
17
 
@@ -45,6 +46,7 @@ describe('plugin-meetings', () => {
45
46
  };
46
47
 
47
48
  originalRemoteUpdateAudioVideo = MeetingUtil.remoteUpdateAudioVideo;
49
+ originalUpdateLocusFromApiResponse = MeetingUtil.updateLocusFromApiResponse;
48
50
 
49
51
  MeetingUtil.remoteUpdateAudioVideo = sinon.stub().resolves(fakeLocusResponse);
50
52
  MeetingUtil.updateLocusFromApiResponse = sinon.stub();
@@ -57,6 +59,7 @@ describe('plugin-meetings', () => {
57
59
 
58
60
  afterEach(() => {
59
61
  MeetingUtil.remoteUpdateAudioVideo = originalRemoteUpdateAudioVideo;
62
+ MeetingUtil.updateLocusFromApiResponse = originalUpdateLocusFromApiResponse;
60
63
  });
61
64
 
62
65
  describe('mute state library', () => {
@@ -109,6 +112,82 @@ describe('plugin-meetings', () => {
109
112
  assert.isTrue(audio.isRemotelyMuted());
110
113
  });
111
114
 
115
+ it('does not unmute the local stream when server clears remote mute while user is locally muted (breakout -> main regression)', async () => {
116
+ // Panelist is locally muted before joining a breakout.
117
+ meeting.mediaProperties.audioStream.userMuted = true;
118
+ audio.handleLocalStreamChange(meeting);
119
+ await testUtils.flushPromises();
120
+
121
+ // Server applied a remote mute at some point.
122
+ audio.handleServerRemoteMuteUpdate(meeting, true, true);
123
+ assert.isTrue(audio.isRemotelyMuted());
124
+
125
+ meeting.mediaProperties.audioStream.setServerMuted.resetHistory();
126
+
127
+ // Now the user returns from breakout to main, and Locus clears the remote mute.
128
+ audio.handleServerRemoteMuteUpdate(meeting, false, true);
129
+
130
+ // Stream must NOT be force-unmuted - user's local mute intent must be preserved.
131
+ // The stale remoteMute=false must not trigger setServerMuted again.
132
+ assert.notCalled(meeting.mediaProperties.audioStream.setServerMuted);
133
+ assert.isFalse(audio.isRemotelyMuted());
134
+ assert.isTrue(audio.isMuted());
135
+ });
136
+
137
+ it('does not touch the local stream when remote mute stays false (no transition)', async () => {
138
+ // No remote mute has been applied yet (initial state has server.remoteMute=false).
139
+ meeting.mediaProperties.audioStream.setServerMuted.resetHistory();
140
+
141
+ audio.handleServerRemoteMuteUpdate(meeting, false, true);
142
+
143
+ // setServerMuted must not be called - there is no remoteMute transition to act on.
144
+ assert.notCalled(meeting.mediaProperties.audioStream.setServerMuted);
145
+ });
146
+
147
+ it('keeps isMuted() true when a stale remoteMute=false replays after remote mute', async () => {
148
+ // User was unmuted by host (userMuted=false locally).
149
+ meeting.mediaProperties.audioStream.userMuted = false;
150
+ audio.handleLocalStreamChange(meeting);
151
+ await testUtils.flushPromises();
152
+
153
+ // Host then hard-mutes the user.
154
+ audio.handleServerRemoteMuteUpdate(meeting, true, true);
155
+ await testUtils.flushPromises();
156
+ assert.isTrue(audio.isMuted());
157
+
158
+ meeting.mediaProperties.audioStream.setServerMuted.resetHistory();
159
+
160
+ // BO -> main replays a stale remoteMute=false from the locus cache.
161
+ audio.handleServerRemoteMuteUpdate(meeting, false, true);
162
+
163
+ // isMuted() must remain true (client.localMute is the safety net),
164
+ // and the stream must not be force-unmuted.
165
+ assert.isTrue(audio.isMuted());
166
+ assert.isFalse(audio.isRemotelyMuted());
167
+ assert.notCalled(meeting.mediaProperties.audioStream.setServerMuted);
168
+ });
169
+
170
+ it('syncs client mute intent back to server on stale remoteMute=false', async () => {
171
+ // client wants mute (localMute=true) but server's
172
+ // local-mute is out of sync (=false) - e.g. an attendee promoted to panelist whose
173
+ // server-side mute state got mis-set during promotion. remoteMute is currently true.
174
+ audio.state.client.localMute = true;
175
+ audio.state.server.localMute = false;
176
+ audio.state.server.remoteMute = true;
177
+
178
+ MeetingUtil.remoteUpdateAudioVideo.resetHistory();
179
+
180
+ // Stale event after BO -> main clears remoteMute.
181
+ audio.handleServerRemoteMuteUpdate(meeting, false, true);
182
+ await testUtils.flushPromises();
183
+
184
+ // applyClientStateToServer must detect the mismatch and push local mute back
185
+ // to server, so server.controls.audio.muted stays true for other participants' tiles.
186
+ assert.calledOnce(MeetingUtil.remoteUpdateAudioVideo);
187
+ assert.calledWith(MeetingUtil.remoteUpdateAudioVideo, meeting, true, undefined);
188
+ assert.isTrue(audio.isMuted());
189
+ });
190
+
112
191
  it('does local audio unmute if localAudioUnmuteRequired is received', async () => {
113
192
  // first we need to have the local stream user muted
114
193
  meeting.mediaProperties.audioStream.userMuted = true;
@@ -826,6 +905,8 @@ describe('plugin-meetings', () => {
826
905
  it('does not do anything if current state is already the same', async () => {
827
906
  // set it up so that we are remotely muted (so that a sync to server would do a remote unmute)
828
907
  audio.handleServerRemoteMuteUpdate(meeting, true, true);
908
+ await testUtils.flushPromises();
909
+ resetStubHistory();
829
910
 
830
911
  // audio is already enabled and we call to enable it again
831
912
  audio.enable(meeting, true);
@@ -347,6 +347,18 @@ describe('plugin-meetings', () => {
347
347
  'ANNOTATION_ON_SHARE_SUPPORTED',
348
348
  ]);
349
349
  });
350
+ it('adds deviceCapabilities to request when simultaneous interpretation is enabled', async () => {
351
+ await meetingsRequest.joinMeeting({
352
+ enableSimultaneousInterpretation: true,
353
+ });
354
+ const requestParams = meetingsRequest.request.getCall(0).args[0];
355
+ assert.deepEqual(requestParams.body.deviceCapabilities, [
356
+ 'HOST_CONTROL_SI_SUPPORTED',
357
+ 'INTERPRETER_CONTROL_SI_SUPPORTED',
358
+ 'SI_HANDOVER_SUPPORTED',
359
+ 'SIGN_INTERPRETER_SUPPORTED',
360
+ ]);
361
+ });
350
362
  it('does not add deviceCapabilities to request when breakouts and live annotation are not supported', async () => {
351
363
  await meetingsRequest.joinMeeting({});
352
364