@webex/plugin-meetings 3.8.1 → 3.9.0-multipleLLM.1

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 (317) hide show
  1. package/README.md +26 -13
  2. package/dist/breakouts/breakout.js +1 -1
  3. package/dist/breakouts/index.js +1 -1
  4. package/dist/constants.js +34 -3
  5. package/dist/constants.js.map +1 -1
  6. package/dist/controls-options-manager/enums.js +1 -0
  7. package/dist/controls-options-manager/enums.js.map +1 -1
  8. package/dist/controls-options-manager/types.js.map +1 -1
  9. package/dist/controls-options-manager/util.js +26 -0
  10. package/dist/controls-options-manager/util.js.map +1 -1
  11. package/dist/interpretation/index.js +1 -1
  12. package/dist/interpretation/siLanguage.js +1 -1
  13. package/dist/locus-info/controlsUtils.js +11 -3
  14. package/dist/locus-info/controlsUtils.js.map +1 -1
  15. package/dist/locus-info/index.js +107 -95
  16. package/dist/locus-info/index.js.map +1 -1
  17. package/dist/locus-info/parser.js +4 -1
  18. package/dist/locus-info/parser.js.map +1 -1
  19. package/dist/media/index.js +2 -2
  20. package/dist/media/index.js.map +1 -1
  21. package/dist/media/properties.js +53 -5
  22. package/dist/media/properties.js.map +1 -1
  23. package/dist/meeting/brbState.js +17 -14
  24. package/dist/meeting/brbState.js.map +1 -1
  25. package/dist/meeting/in-meeting-actions.js +13 -1
  26. package/dist/meeting/in-meeting-actions.js.map +1 -1
  27. package/dist/meeting/index.js +555 -296
  28. package/dist/meeting/index.js.map +1 -1
  29. package/dist/meeting/muteState.js +2 -5
  30. package/dist/meeting/muteState.js.map +1 -1
  31. package/dist/meeting/request.js +44 -0
  32. package/dist/meeting/request.js.map +1 -1
  33. package/dist/meeting/request.type.js.map +1 -1
  34. package/dist/{rtcMetrics/constants.js → meeting/type.js} +1 -5
  35. package/dist/meeting/type.js.map +1 -0
  36. package/dist/meeting/util.js +98 -13
  37. package/dist/meeting/util.js.map +1 -1
  38. package/dist/meeting-info/meeting-info-v2.js +29 -21
  39. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  40. package/dist/meetings/index.js +18 -10
  41. package/dist/meetings/index.js.map +1 -1
  42. package/dist/member/types.js.map +1 -1
  43. package/dist/members/collection.js +13 -0
  44. package/dist/members/collection.js.map +1 -1
  45. package/dist/members/index.js +53 -29
  46. package/dist/members/index.js.map +1 -1
  47. package/dist/members/request.js +3 -3
  48. package/dist/members/request.js.map +1 -1
  49. package/dist/members/util.js +25 -8
  50. package/dist/members/util.js.map +1 -1
  51. package/dist/metrics/constants.js +3 -1
  52. package/dist/metrics/constants.js.map +1 -1
  53. package/dist/multistream/mediaRequestManager.js +1 -1
  54. package/dist/multistream/mediaRequestManager.js.map +1 -1
  55. package/dist/multistream/remoteMedia.js +34 -5
  56. package/dist/multistream/remoteMedia.js.map +1 -1
  57. package/dist/multistream/remoteMediaGroup.js +42 -2
  58. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  59. package/dist/multistream/sendSlotManager.js +32 -2
  60. package/dist/multistream/sendSlotManager.js.map +1 -1
  61. package/dist/reachability/index.js +8 -13
  62. package/dist/reachability/index.js.map +1 -1
  63. package/dist/types/constants.d.ts +30 -0
  64. package/dist/types/controls-options-manager/enums.d.ts +2 -1
  65. package/dist/types/controls-options-manager/types.d.ts +4 -1
  66. package/dist/types/locus-info/index.d.ts +54 -10
  67. package/dist/types/media/properties.d.ts +21 -0
  68. package/dist/types/meeting/brbState.d.ts +0 -1
  69. package/dist/types/meeting/in-meeting-actions.d.ts +12 -0
  70. package/dist/types/meeting/index.d.ts +58 -20
  71. package/dist/types/meeting/request.d.ts +18 -1
  72. package/dist/types/meeting/request.type.d.ts +74 -0
  73. package/dist/types/meeting/type.d.ts +9 -0
  74. package/dist/types/meeting/util.d.ts +13 -3
  75. package/dist/types/meeting-info/meeting-info-v2.d.ts +6 -3
  76. package/dist/types/meetings/index.d.ts +3 -1
  77. package/dist/types/member/types.d.ts +1 -0
  78. package/dist/types/members/collection.d.ts +6 -0
  79. package/dist/types/members/index.d.ts +22 -9
  80. package/dist/types/members/request.d.ts +1 -1
  81. package/dist/types/members/util.d.ts +13 -6
  82. package/dist/types/metrics/constants.d.ts +2 -0
  83. package/dist/types/multistream/remoteMedia.d.ts +20 -1
  84. package/dist/types/multistream/remoteMediaGroup.d.ts +11 -0
  85. package/dist/types/multistream/sendSlotManager.d.ts +16 -0
  86. package/dist/types/reachability/index.d.ts +2 -2
  87. package/dist/webinar/index.js +1 -1
  88. package/package.json +24 -25
  89. package/src/constants.ts +34 -2
  90. package/src/controls-options-manager/enums.ts +1 -0
  91. package/src/controls-options-manager/types.ts +6 -1
  92. package/src/controls-options-manager/util.ts +31 -0
  93. package/src/locus-info/controlsUtils.ts +15 -0
  94. package/src/locus-info/index.ts +174 -96
  95. package/src/locus-info/parser.ts +5 -1
  96. package/src/media/index.ts +2 -2
  97. package/src/media/properties.ts +43 -0
  98. package/src/meeting/brbState.ts +13 -9
  99. package/src/meeting/in-meeting-actions.ts +25 -0
  100. package/src/meeting/index.ts +362 -75
  101. package/src/meeting/muteState.ts +2 -6
  102. package/src/meeting/request.ts +39 -0
  103. package/src/meeting/request.type.ts +64 -0
  104. package/src/meeting/type.ts +9 -0
  105. package/src/meeting/util.ts +114 -22
  106. package/src/meeting-info/meeting-info-v2.ts +24 -5
  107. package/src/meetings/index.ts +12 -5
  108. package/src/member/types.ts +1 -0
  109. package/src/members/collection.ts +11 -0
  110. package/src/members/index.ts +51 -15
  111. package/src/members/request.ts +2 -2
  112. package/src/members/util.ts +34 -6
  113. package/src/metrics/constants.ts +2 -0
  114. package/src/multistream/mediaRequestManager.ts +7 -7
  115. package/src/multistream/remoteMedia.ts +34 -4
  116. package/src/multistream/remoteMediaGroup.ts +37 -2
  117. package/src/multistream/sendSlotManager.ts +34 -2
  118. package/src/reachability/index.ts +8 -16
  119. package/test/unit/spec/controls-options-manager/util.js +58 -0
  120. package/test/unit/spec/locus-info/controlsUtils.js +52 -0
  121. package/test/unit/spec/locus-info/index.js +270 -97
  122. package/test/unit/spec/locus-info/parser.js +3 -2
  123. package/test/unit/spec/media/index.ts +107 -0
  124. package/test/unit/spec/media/properties.ts +137 -0
  125. package/test/unit/spec/meeting/brbState.ts +23 -4
  126. package/test/unit/spec/meeting/in-meeting-actions.ts +12 -0
  127. package/test/unit/spec/meeting/index.js +1194 -97
  128. package/test/unit/spec/meeting/muteState.js +32 -6
  129. package/test/unit/spec/meeting/request.js +92 -0
  130. package/test/unit/spec/meeting/utils.js +167 -17
  131. package/test/unit/spec/meeting-info/meetinginfov2.js +8 -3
  132. package/test/unit/spec/meetings/index.js +12 -1
  133. package/test/unit/spec/members/collection.js +120 -0
  134. package/test/unit/spec/members/index.js +140 -12
  135. package/test/unit/spec/members/request.js +57 -2
  136. package/test/unit/spec/members/utils.js +139 -17
  137. package/test/unit/spec/multistream/mediaRequestManager.ts +19 -6
  138. package/test/unit/spec/multistream/remoteMedia.ts +66 -2
  139. package/test/unit/spec/multistream/sendSlotManager.ts +59 -0
  140. package/test/unit/spec/reachability/index.ts +160 -9
  141. package/dist/annotation/annotation.types.d.ts +0 -42
  142. package/dist/annotation/constants.d.ts +0 -31
  143. package/dist/annotation/index.d.ts +0 -117
  144. package/dist/breakouts/breakout.d.ts +0 -8
  145. package/dist/breakouts/collection.d.ts +0 -5
  146. package/dist/breakouts/edit-lock-error.d.ts +0 -15
  147. package/dist/breakouts/events.d.ts +0 -8
  148. package/dist/breakouts/index.d.ts +0 -5
  149. package/dist/breakouts/request.d.ts +0 -22
  150. package/dist/breakouts/utils.d.ts +0 -15
  151. package/dist/common/browser-detection.d.ts +0 -9
  152. package/dist/common/collection.d.ts +0 -48
  153. package/dist/common/config.d.ts +0 -2
  154. package/dist/common/errors/captcha-error.d.ts +0 -15
  155. package/dist/common/errors/intent-to-join.d.ts +0 -16
  156. package/dist/common/errors/join-meeting.d.ts +0 -17
  157. package/dist/common/errors/media.d.ts +0 -15
  158. package/dist/common/errors/no-meeting-info.d.ts +0 -14
  159. package/dist/common/errors/parameter.d.ts +0 -15
  160. package/dist/common/errors/password-error.d.ts +0 -15
  161. package/dist/common/errors/permission.d.ts +0 -14
  162. package/dist/common/errors/reclaim-host-role-error.d.ts +0 -60
  163. package/dist/common/errors/reclaim-host-role-error.js +0 -158
  164. package/dist/common/errors/reclaim-host-role-error.js.map +0 -1
  165. package/dist/common/errors/reclaim-host-role-errors.d.ts +0 -60
  166. package/dist/common/errors/reconnection-in-progress.d.ts +0 -9
  167. package/dist/common/errors/reconnection-in-progress.js +0 -35
  168. package/dist/common/errors/reconnection-in-progress.js.map +0 -1
  169. package/dist/common/errors/reconnection.d.ts +0 -15
  170. package/dist/common/errors/stats.d.ts +0 -15
  171. package/dist/common/errors/webex-errors.d.ts +0 -81
  172. package/dist/common/errors/webex-meetings-error.d.ts +0 -20
  173. package/dist/common/events/events-scope.d.ts +0 -17
  174. package/dist/common/events/events.d.ts +0 -12
  175. package/dist/common/events/trigger-proxy.d.ts +0 -2
  176. package/dist/common/events/util.d.ts +0 -2
  177. package/dist/common/logs/logger-config.d.ts +0 -2
  178. package/dist/common/logs/logger-proxy.d.ts +0 -2
  179. package/dist/common/logs/request.d.ts +0 -34
  180. package/dist/common/queue.d.ts +0 -32
  181. package/dist/config.d.ts +0 -73
  182. package/dist/constants.d.ts +0 -952
  183. package/dist/controls-options-manager/constants.d.ts +0 -4
  184. package/dist/controls-options-manager/enums.d.ts +0 -5
  185. package/dist/controls-options-manager/index.d.ts +0 -120
  186. package/dist/controls-options-manager/types.d.ts +0 -43
  187. package/dist/controls-options-manager/util.d.ts +0 -7
  188. package/dist/index.d.ts +0 -4
  189. package/dist/interceptors/index.d.ts +0 -2
  190. package/dist/interceptors/locusRetry.d.ts +0 -27
  191. package/dist/interpretation/collection.d.ts +0 -5
  192. package/dist/interpretation/index.d.ts +0 -5
  193. package/dist/interpretation/siLanguage.d.ts +0 -5
  194. package/dist/locus-info/controlsUtils.d.ts +0 -2
  195. package/dist/locus-info/embeddedAppsUtils.d.ts +0 -2
  196. package/dist/locus-info/fullState.d.ts +0 -2
  197. package/dist/locus-info/hostUtils.d.ts +0 -2
  198. package/dist/locus-info/index.d.ts +0 -269
  199. package/dist/locus-info/infoUtils.d.ts +0 -2
  200. package/dist/locus-info/mediaSharesUtils.d.ts +0 -2
  201. package/dist/locus-info/parser.d.ts +0 -212
  202. package/dist/locus-info/selfUtils.d.ts +0 -2
  203. package/dist/media/index.d.ts +0 -32
  204. package/dist/media/properties.d.ts +0 -108
  205. package/dist/media/util.d.ts +0 -2
  206. package/dist/mediaQualityMetrics/config.d.ts +0 -233
  207. package/dist/mediaQualityMetrics/config.js +0 -513
  208. package/dist/mediaQualityMetrics/config.js.map +0 -1
  209. package/dist/meeting/effectsState.d.ts +0 -42
  210. package/dist/meeting/effectsState.js +0 -260
  211. package/dist/meeting/effectsState.js.map +0 -1
  212. package/dist/meeting/in-meeting-actions.d.ts +0 -79
  213. package/dist/meeting/index.d.ts +0 -1622
  214. package/dist/meeting/locusMediaRequest.d.ts +0 -74
  215. package/dist/meeting/muteState.d.ts +0 -116
  216. package/dist/meeting/request.d.ts +0 -257
  217. package/dist/meeting/request.type.d.ts +0 -11
  218. package/dist/meeting/state.d.ts +0 -9
  219. package/dist/meeting/util.d.ts +0 -2
  220. package/dist/meeting/voicea-meeting.d.ts +0 -16
  221. package/dist/meeting-info/collection.d.ts +0 -20
  222. package/dist/meeting-info/index.d.ts +0 -57
  223. package/dist/meeting-info/meeting-info-v2.d.ts +0 -93
  224. package/dist/meeting-info/request.d.ts +0 -22
  225. package/dist/meeting-info/util.d.ts +0 -2
  226. package/dist/meeting-info/utilv2.d.ts +0 -2
  227. package/dist/meetings/collection.d.ts +0 -23
  228. package/dist/meetings/index.d.ts +0 -296
  229. package/dist/meetings/meetings.types.d.ts +0 -4
  230. package/dist/meetings/request.d.ts +0 -27
  231. package/dist/meetings/util.d.ts +0 -18
  232. package/dist/member/index.d.ts +0 -148
  233. package/dist/member/member.types.d.ts +0 -11
  234. package/dist/member/member.types.js +0 -18
  235. package/dist/member/member.types.js.map +0 -1
  236. package/dist/member/types.d.ts +0 -32
  237. package/dist/member/util.d.ts +0 -2
  238. package/dist/members/collection.d.ts +0 -24
  239. package/dist/members/index.d.ts +0 -308
  240. package/dist/members/request.d.ts +0 -58
  241. package/dist/members/types.d.ts +0 -25
  242. package/dist/members/util.d.ts +0 -2
  243. package/dist/metrics/config.d.ts +0 -169
  244. package/dist/metrics/config.js +0 -289
  245. package/dist/metrics/config.js.map +0 -1
  246. package/dist/metrics/constants.d.ts +0 -59
  247. package/dist/metrics/index.d.ts +0 -152
  248. package/dist/multistream/mediaRequestManager.d.ts +0 -119
  249. package/dist/multistream/receiveSlot.d.ts +0 -68
  250. package/dist/multistream/receiveSlotManager.d.ts +0 -56
  251. package/dist/multistream/remoteMedia.d.ts +0 -72
  252. package/dist/multistream/remoteMediaGroup.d.ts +0 -49
  253. package/dist/multistream/remoteMediaManager.d.ts +0 -300
  254. package/dist/multistream/sendSlotManager.d.ts +0 -69
  255. package/dist/networkQualityMonitor/index.d.ts +0 -70
  256. package/dist/networkQualityMonitor/index.js +0 -226
  257. package/dist/networkQualityMonitor/index.js.map +0 -1
  258. package/dist/peer-connection-manager/index.d.ts +0 -6
  259. package/dist/peer-connection-manager/index.js +0 -671
  260. package/dist/peer-connection-manager/index.js.map +0 -1
  261. package/dist/peer-connection-manager/util.d.ts +0 -6
  262. package/dist/peer-connection-manager/util.js +0 -110
  263. package/dist/peer-connection-manager/util.js.map +0 -1
  264. package/dist/personal-meeting-room/index.d.ts +0 -47
  265. package/dist/personal-meeting-room/request.d.ts +0 -14
  266. package/dist/personal-meeting-room/util.d.ts +0 -2
  267. package/dist/reachability/clusterReachability.d.ts +0 -109
  268. package/dist/reachability/index.d.ts +0 -139
  269. package/dist/reachability/request.d.ts +0 -35
  270. package/dist/reachability/util.d.ts +0 -8
  271. package/dist/reactions/constants.d.ts +0 -3
  272. package/dist/reactions/reactions.d.ts +0 -4
  273. package/dist/reactions/reactions.type.d.ts +0 -32
  274. package/dist/reconnection-manager/index.d.ts +0 -112
  275. package/dist/recording-controller/enums.d.ts +0 -7
  276. package/dist/recording-controller/index.d.ts +0 -193
  277. package/dist/recording-controller/util.d.ts +0 -13
  278. package/dist/roap/collection.d.ts +0 -10
  279. package/dist/roap/collection.js +0 -63
  280. package/dist/roap/collection.js.map +0 -1
  281. package/dist/roap/handler.d.ts +0 -47
  282. package/dist/roap/handler.js +0 -279
  283. package/dist/roap/handler.js.map +0 -1
  284. package/dist/roap/index.d.ts +0 -116
  285. package/dist/roap/request.d.ts +0 -35
  286. package/dist/roap/state.d.ts +0 -9
  287. package/dist/roap/state.js +0 -127
  288. package/dist/roap/state.js.map +0 -1
  289. package/dist/roap/turnDiscovery.d.ts +0 -81
  290. package/dist/roap/util.d.ts +0 -2
  291. package/dist/roap/util.js +0 -76
  292. package/dist/roap/util.js.map +0 -1
  293. package/dist/rtcMetrics/constants.d.ts +0 -4
  294. package/dist/rtcMetrics/constants.js.map +0 -1
  295. package/dist/rtcMetrics/index.d.ts +0 -61
  296. package/dist/rtcMetrics/index.js +0 -197
  297. package/dist/rtcMetrics/index.js.map +0 -1
  298. package/dist/statsAnalyzer/global.d.ts +0 -118
  299. package/dist/statsAnalyzer/global.js +0 -127
  300. package/dist/statsAnalyzer/global.js.map +0 -1
  301. package/dist/statsAnalyzer/index.d.ts +0 -193
  302. package/dist/statsAnalyzer/index.js +0 -1019
  303. package/dist/statsAnalyzer/index.js.map +0 -1
  304. package/dist/statsAnalyzer/mqaUtil.d.ts +0 -22
  305. package/dist/statsAnalyzer/mqaUtil.js +0 -181
  306. package/dist/statsAnalyzer/mqaUtil.js.map +0 -1
  307. package/dist/transcription/index.d.ts +0 -64
  308. package/dist/types/common/errors/reconnection-in-progress.d.ts +0 -9
  309. package/dist/types/mediaQualityMetrics/config.d.ts +0 -241
  310. package/dist/types/networkQualityMonitor/index.d.ts +0 -70
  311. package/dist/types/rtcMetrics/constants.d.ts +0 -4
  312. package/dist/types/rtcMetrics/index.d.ts +0 -71
  313. package/dist/types/statsAnalyzer/global.d.ts +0 -36
  314. package/dist/types/statsAnalyzer/index.d.ts +0 -217
  315. package/dist/types/statsAnalyzer/mqaUtil.d.ts +0 -48
  316. package/dist/webinar/collection.d.ts +0 -16
  317. package/dist/webinar/index.d.ts +0 -5
@@ -39,6 +39,7 @@ import {
39
39
  ConnectionState,
40
40
  MediaConnectionEventNames,
41
41
  StatsAnalyzerEventNames,
42
+ StatsMonitorEventNames,
42
43
  Errors,
43
44
  ErrorType,
44
45
  RemoteTrackType,
@@ -56,6 +57,7 @@ import * as MeetingRequestImport from '@webex/plugin-meetings/src/meeting/reques
56
57
  import LocusInfo from '@webex/plugin-meetings/src/locus-info';
57
58
  import MediaProperties from '@webex/plugin-meetings/src/media/properties';
58
59
  import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
60
+ import MembersUtil from '@webex/plugin-meetings/src/members/util';
59
61
  import MeetingsUtil from '@webex/plugin-meetings/src/meetings/util';
60
62
  import Media from '@webex/plugin-meetings/src/media/index';
61
63
  import ReconnectionManager from '@webex/plugin-meetings/src/reconnection-manager';
@@ -244,6 +246,7 @@ describe('plugin-meetings', () => {
244
246
  });
245
247
 
246
248
  webex.internal.newMetrics.callDiagnosticMetrics.clearErrorCache = sinon.stub();
249
+ webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId = sinon.stub();
247
250
  webex.internal.support.submitLogs = sinon.stub().returns(Promise.resolve());
248
251
  webex.internal.services = {get: sinon.stub().returns('locus-url')};
249
252
  webex.credentials.getOrgId = sinon.stub().returns('fake-org-id');
@@ -368,6 +371,35 @@ describe('plugin-meetings', () => {
368
371
  assert.instanceOf(meeting.simultaneousInterpretation, SimultaneousInterpretation);
369
372
  assert.instanceOf(meeting.webinar, Webinar);
370
373
  });
374
+
375
+ it('should call the callback with the meeting that has id already set', () => {
376
+ let meetingIdFromCallback;
377
+ // check that the meeting id is already set correctly at the time when the callback is called
378
+ const meetingCreationCallback = sinon.stub().callsFake((meeting) => {
379
+ meetingIdFromCallback = meeting.id;
380
+ });
381
+
382
+ meeting = new Meeting(
383
+ {
384
+ userId: uuid1,
385
+ resource: uuid2,
386
+ deviceUrl: uuid3,
387
+ locus: {url: url1},
388
+ destination: testDestination,
389
+ destinationType: DESTINATION_TYPE.MEETING_ID,
390
+ correlationId,
391
+ selfId: uuid1,
392
+ },
393
+ {
394
+ parent: webex,
395
+ },
396
+ meetingCreationCallback
397
+ );
398
+ assert.exists(meeting.id);
399
+ assert.calledOnceWithExactly(meetingCreationCallback, meeting);
400
+ assert.equal(meeting.id, meetingIdFromCallback);
401
+ });
402
+
371
403
  it('creates MediaRequestManager instances', () => {
372
404
  assert.instanceOf(meeting.mediaRequestManagers.audio, MediaRequestManager);
373
405
  assert.instanceOf(meeting.mediaRequestManagers.video, MediaRequestManager);
@@ -454,6 +486,18 @@ describe('plugin-meetings', () => {
454
486
  });
455
487
  });
456
488
 
489
+ it('pstnCorrelationId getter/setter should work correctly', () => {
490
+ const testPstnCorrelationId = uuid.v4();
491
+
492
+ meeting.pstnCorrelationId = testPstnCorrelationId;
493
+ assert.equal(meeting.pstnCorrelationId, testPstnCorrelationId);
494
+ assert.equal(meeting.callStateForMetrics.pstnCorrelationId, testPstnCorrelationId);
495
+
496
+ meeting.pstnCorrelationId = undefined;
497
+ assert.equal(meeting.pstnCorrelationId, undefined);
498
+ assert.equal(meeting.callStateForMetrics.pstnCorrelationId, undefined);
499
+ });
500
+
457
501
  describe('creates ReceiveSlot manager instance', () => {
458
502
  let mockReceiveSlotManagerCtor;
459
503
  let providedCreateSlotCallback;
@@ -581,7 +625,6 @@ describe('plugin-meetings', () => {
581
625
  assert.isFalse(meeting.isLocusCall());
582
626
  });
583
627
  });
584
-
585
628
  describe('#invite', () => {
586
629
  it('should have #invite', () => {
587
630
  assert.exists(meeting.invite);
@@ -592,8 +635,6 @@ describe('plugin-meetings', () => {
592
635
  it('should proxy members #addMember and return a promise', async () => {
593
636
  const invite = meeting.invite(uuid1, false);
594
637
 
595
- assert.exists(invite.then);
596
- await invite;
597
638
  assert.calledOnce(meeting.members.addMember);
598
639
  assert.calledWith(meeting.members.addMember, uuid1, false);
599
640
  });
@@ -614,20 +655,20 @@ describe('plugin-meetings', () => {
614
655
  assert.calledWith(meeting.members.cancelPhoneInvite, uuid1);
615
656
  });
616
657
  });
617
- describe('#cancelSIPInvite', () => {
618
- it('should have #cancelSIPInvite', () => {
619
- assert.exists(meeting.cancelSIPInvite);
658
+ describe('#cancelInviteByMemberId', () => {
659
+ it('should have #cancelInviteByMemberId', () => {
660
+ assert.exists(meeting.cancelInviteByMemberId);
620
661
  });
621
662
  beforeEach(() => {
622
- meeting.members.cancelSIPInvite = sinon.stub().returns(Promise.resolve(test1));
663
+ meeting.members.cancelInviteByMemberId = sinon.stub().returns(Promise.resolve(test1));
623
664
  });
624
- it('should proxy members #cancelSIPInvite and return a promise', async () => {
625
- const cancel = meeting.cancelSIPInvite({memberId: uuid1});
665
+ it('should proxy members #cancelInviteByMemberId and return a promise', async () => {
666
+ const cancel = meeting.cancelInviteByMemberId({memberId: uuid1});
626
667
 
627
668
  assert.exists(cancel.then);
628
669
  await cancel;
629
- assert.calledOnce(meeting.members.cancelSIPInvite);
630
- assert.calledWith(meeting.members.cancelSIPInvite, {memberId: uuid1});
670
+ assert.calledOnce(meeting.members.cancelInviteByMemberId);
671
+ assert.calledWith(meeting.members.cancelInviteByMemberId, {memberId: uuid1});
631
672
  });
632
673
  });
633
674
  describe('#admit', () => {
@@ -1208,8 +1249,73 @@ describe('plugin-meetings', () => {
1208
1249
  reason: 'joinWithMedia failure',
1209
1250
  });
1210
1251
  });
1211
- });
1212
1252
 
1253
+ it('should ignore sendVideo/receiveVideo when videoEnabled is false', async () => {
1254
+ await meeting.joinWithMedia({
1255
+ joinOptions,
1256
+ mediaOptions: {
1257
+ videoEnabled: false,
1258
+ sendVideo: true,
1259
+ receiveVideo: true,
1260
+ allowMediaInLobby: true,
1261
+ },
1262
+ });
1263
+
1264
+ assert.calledWithMatch(
1265
+ meeting.addMediaInternal,
1266
+ sinon.match.any,
1267
+ sinon.match.any,
1268
+ sinon.match.any,
1269
+ sinon.match.has('videoEnabled', false).and(sinon.match.has('allowMediaInLobby', true))
1270
+ );
1271
+ });
1272
+
1273
+ it('should ignore sendAudio/receiveAudio when audioEnabled is false', async () => {
1274
+ await meeting.joinWithMedia({
1275
+ joinOptions,
1276
+ mediaOptions: {
1277
+ audioEnabled: false,
1278
+ sendAudio: true,
1279
+ receiveAudio: false,
1280
+ allowMediaInLobby: true,
1281
+ },
1282
+ });
1283
+
1284
+ assert.calledWithMatch(
1285
+ meeting.addMediaInternal,
1286
+ sinon.match.any,
1287
+ sinon.match.any,
1288
+ sinon.match.any,
1289
+ sinon.match.has('audioEnabled', false).and(sinon.match.has('allowMediaInLobby', true))
1290
+ );
1291
+ });
1292
+
1293
+ it('should use provided send/receive values when videoEnabled/audioEnabled are true or not set', async () => {
1294
+ await meeting.joinWithMedia({
1295
+ joinOptions,
1296
+ mediaOptions: {
1297
+ sendVideo: true,
1298
+ receiveVideo: false,
1299
+ sendAudio: false,
1300
+ receiveAudio: true,
1301
+ allowMediaInLobby: true,
1302
+ },
1303
+ });
1304
+
1305
+ assert.calledWith(
1306
+ meeting.addMediaInternal,
1307
+ sinon.match.any,
1308
+ sinon.match.any,
1309
+ sinon.match.any,
1310
+ sinon.match({
1311
+ sendVideo: true,
1312
+ receiveVideo: false,
1313
+ sendAudio: false,
1314
+ receiveAudio: true,
1315
+ })
1316
+ );
1317
+ });
1318
+ });
1213
1319
  describe('#isTranscriptionSupported', () => {
1214
1320
  it('should return false if the feature is not supported for the meeting', () => {
1215
1321
  meeting.locusInfo.controls = {transcribe: {caption: false}};
@@ -1223,6 +1329,44 @@ describe('plugin-meetings', () => {
1223
1329
  });
1224
1330
  });
1225
1331
 
1332
+ describe('#update spoken language', () => {
1333
+ beforeEach(() => {
1334
+ webex.internal.voicea.onSpokenLanguageUpdate = sinon.stub();
1335
+ meeting.transcription = {languageOptions: {currentSpokenLanguage: 'en'}};
1336
+ });
1337
+ afterEach(() => {
1338
+ // Restore the original methods after each test
1339
+ sinon.restore();
1340
+ });
1341
+ it('should call voicea.onSpokenLanguageUpdate when joined', async () => {
1342
+ meeting.joinedWith = {state: 'JOINED'};
1343
+ await meeting.locusInfo.emitScoped(
1344
+ {function: 'test', file: 'test'},
1345
+ LOCUSINFO.EVENTS.CONTROLS_MEETING_TRANSCRIPTION_SPOKEN_LANGUAGE_UPDATED,
1346
+ {spokenLanguage: 'fr'}
1347
+ );
1348
+ assert.calledWith(webex.internal.voicea.onSpokenLanguageUpdate, 'fr', meeting.id);
1349
+ assert.equal(meeting.transcription.languageOptions.currentSpokenLanguage, 'fr');
1350
+ assert.calledWith(
1351
+ TriggerProxy.trigger,
1352
+ meeting,
1353
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
1354
+ EVENT_TRIGGERS.MEETING_TRANSCRIPTION_SPOKEN_LANGUAGE_UPDATED
1355
+ );
1356
+ });
1357
+
1358
+ it('should also call voicea.onSpokenLanguageUpdate when not joined', async () => {
1359
+ meeting.joinedWith = {state: 'NOT_JOINED'};
1360
+ await meeting.locusInfo.emitScoped(
1361
+ {function: 'test', file: 'test'},
1362
+ LOCUSINFO.EVENTS.CONTROLS_MEETING_TRANSCRIPTION_SPOKEN_LANGUAGE_UPDATED,
1363
+ {spokenLanguage: 'de'}
1364
+ );
1365
+ assert.calledWith(webex.internal.voicea.onSpokenLanguageUpdate, 'de');
1366
+ assert.equal(meeting.transcription.languageOptions.currentSpokenLanguage, 'de');
1367
+ });
1368
+ });
1369
+
1226
1370
  describe('#startTranscription', () => {
1227
1371
  beforeEach(() => {
1228
1372
  webex.internal.voicea.on = sinon.stub();
@@ -1846,21 +1990,25 @@ describe('plugin-meetings', () => {
1846
1990
  });
1847
1991
  });
1848
1992
 
1849
- it('should post error event if failed', async () => {
1993
+ it('should handle join failure', async () => {
1850
1994
  MeetingUtil.isPinOrGuest = sinon.stub().returns(false);
1995
+ webex.internal.newMetrics.submitClientEvent = sinon.stub();
1996
+
1851
1997
  await meeting.join().catch(() => {
1852
- assert.deepEqual(
1853
- webex.internal.newMetrics.submitClientEvent.getCall(1).args[0].name,
1854
- 'client.locus.join.response'
1855
- );
1856
- assert.match(
1857
- webex.internal.newMetrics.submitClientEvent.getCall(1).args[0].options.rawError,
1998
+ assert.calledOnce(MeetingUtil.joinMeeting);
1999
+
2000
+ // Assert that client.locus.join.response error event is not sent from this function, it is now emitted from MeetingUtil.joinMeeting
2001
+ assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
2002
+ assert.calledWithMatch(
2003
+ webex.internal.newMetrics.submitClientEvent,
1858
2004
  {
1859
- code: 2,
1860
- error: null,
1861
- joinOptions: {},
1862
- sdkMessage:
1863
- 'There was an issue joining the meeting, meeting could be in a bad state.',
2005
+ name: 'client.call.initiated',
2006
+ payload: {
2007
+ trigger: 'user-interaction',
2008
+ isRoapCallEnabled: true,
2009
+ pstnAudioType: undefined
2010
+ },
2011
+ options: {meetingId: meeting.id},
1864
2012
  }
1865
2013
  );
1866
2014
  });
@@ -2061,16 +2209,15 @@ describe('plugin-meetings', () => {
2061
2209
  };
2062
2210
  meeting.mediaProperties.setMediaDirection = sinon.stub().returns(true);
2063
2211
  meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
2064
- meeting.mediaProperties.getCurrentConnectionInfo = sinon
2065
- .stub()
2066
- .resolves({
2067
- connectionType: 'udp',
2068
- selectedCandidatePairChanges: 2,
2069
- numTransports: 1,
2070
- ipVersion: 'IPv6',
2071
- });
2212
+ meeting.mediaProperties.getCurrentConnectionInfo = sinon.stub().resolves({
2213
+ connectionType: 'udp',
2214
+ selectedCandidatePairChanges: 2,
2215
+ numTransports: 1,
2216
+ ipVersion: 'IPv6',
2217
+ });
2072
2218
  meeting.audio = muteStateStub;
2073
2219
  meeting.video = muteStateStub;
2220
+ sinon.stub(MeetingUtil, 'getIpVersion').returns(IP_VERSION.ipv4_and_ipv6);
2074
2221
  sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
2075
2222
  sinon.stub(meeting, 'setupMediaConnectionListeners');
2076
2223
  sinon.stub(meeting, 'setMercuryListener');
@@ -2142,13 +2289,24 @@ describe('plugin-meetings', () => {
2142
2289
  close: sinon.stub(),
2143
2290
  forceRtcMetricsSend,
2144
2291
  });
2145
- // set a statsAnalyzer on the meeting so that we can check that it gets reset to null
2292
+
2293
+ const mockStatsMonitor = {removeAllListeners: sinon.stub()};
2294
+ const mockNetworkQualityMonitor = {removeAllListeners: sinon.stub()};
2295
+
2296
+ // set a statsAnalyzer and statsMonitor on the meeting so that we can check that they get reset to null
2146
2297
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2298
+ meeting.statsMonitor = mockStatsMonitor;
2299
+ meeting.networkQualityMonitor = mockNetworkQualityMonitor;
2147
2300
  const error = await assert.isRejected(meeting.addMedia());
2148
2301
 
2149
2302
  assert.calledOnce(forceRtcMetricsSend);
2303
+ assert.calledOnce(mockStatsMonitor.removeAllListeners);
2304
+ assert.calledOnce(mockNetworkQualityMonitor.removeAllListeners);
2150
2305
 
2151
2306
  assert.isNull(meeting.statsAnalyzer);
2307
+ assert.isNull(meeting.statsMonitor);
2308
+ assert.isNull(meeting.networkQualityMonitor);
2309
+
2152
2310
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
2153
2311
  assert.calledWith(webex.internal.newMetrics.submitInternalEvent.firstCall, {
2154
2312
  name: 'internal.client.add-media.turn-discovery.start',
@@ -2187,10 +2345,12 @@ describe('plugin-meetings', () => {
2187
2345
  someReachabilityMetric1: 'some value1',
2188
2346
  someReachabilityMetric2: 'some value2',
2189
2347
  selectedCandidatePairChanges: 2,
2190
- isSubnetReachable: null,
2191
- selectedCluster: null,
2348
+ subnet_reachable: null,
2349
+ selected_cluster: null,
2350
+ selected_subnet: null,
2192
2351
  numTransports: 1,
2193
2352
  iceCandidatesCount: 0,
2353
+ ipver: 1,
2194
2354
  }
2195
2355
  );
2196
2356
  });
@@ -2235,8 +2395,10 @@ describe('plugin-meetings', () => {
2235
2395
  signalingState: 'unknown',
2236
2396
  connectionState: 'unknown',
2237
2397
  iceConnectionState: 'unknown',
2238
- isSubnetReachable: null,
2239
- selectedCluster: null,
2398
+ subnet_reachable: null,
2399
+ selected_cluster: null,
2400
+ selected_subnet: null,
2401
+ ipver: 1,
2240
2402
  })
2241
2403
  );
2242
2404
 
@@ -2256,12 +2418,23 @@ describe('plugin-meetings', () => {
2256
2418
 
2257
2419
  meeting.waitForRemoteSDPAnswer = sinon.stub().rejects();
2258
2420
 
2259
- // set a statsAnalyzer on the meeting so that we can check that it gets reset to null
2421
+ const mockStatsMonitor = {removeAllListeners: sinon.stub()};
2422
+ const mockNetworkQualityMonitor = {removeAllListeners: sinon.stub()};
2423
+
2424
+ // set a statsAnalyzer and statsMonitor on the meeting so that we can check that they get reset to null
2260
2425
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2426
+ meeting.statsMonitor = mockStatsMonitor;
2427
+ meeting.networkQualityMonitor = mockNetworkQualityMonitor;
2261
2428
 
2262
2429
  const error = await assert.isRejected(meeting.addMedia());
2263
2430
 
2264
2431
  assert.isNull(meeting.statsAnalyzer);
2432
+ assert.isNull(meeting.statsMonitor);
2433
+ assert.isNull(meeting.networkQualityMonitor);
2434
+
2435
+ assert.calledOnce(mockStatsMonitor.removeAllListeners);
2436
+ assert.calledOnce(mockNetworkQualityMonitor.removeAllListeners);
2437
+
2265
2438
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
2266
2439
  assert.calledWith(webex.internal.newMetrics.submitInternalEvent.firstCall, {
2267
2440
  name: 'internal.client.add-media.turn-discovery.start',
@@ -2302,8 +2475,10 @@ describe('plugin-meetings', () => {
2302
2475
  selectedCandidatePairChanges: 2,
2303
2476
  numTransports: 1,
2304
2477
  iceCandidatesCount: 0,
2305
- isSubnetReachable: null,
2306
- selectedCluster: null,
2478
+ subnet_reachable: null,
2479
+ selected_cluster: null,
2480
+ selected_subnet: null,
2481
+ ipver: 1,
2307
2482
  }
2308
2483
  );
2309
2484
  });
@@ -2324,8 +2499,9 @@ describe('plugin-meetings', () => {
2324
2499
  },
2325
2500
  },
2326
2501
  });
2327
- // set a statsAnalyzer on the meeting so that we can check that it gets reset to null
2502
+ // set a statsAnalyzer and statsMonitor on the meeting so that we can check that they get reset to null
2328
2503
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2504
+ meeting.statsMonitor = {removeAllListeners: sinon.stub()};
2329
2505
  const error = await assert.isRejected(meeting.addMedia());
2330
2506
 
2331
2507
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
@@ -2361,12 +2537,15 @@ describe('plugin-meetings', () => {
2361
2537
  signalingState: 'have-local-offer',
2362
2538
  connectionState: 'connecting',
2363
2539
  iceConnectionState: 'checking',
2364
- isSubnetReachable: null,
2365
- selectedCluster: null,
2540
+ subnet_reachable: null,
2541
+ selected_cluster: null,
2542
+ selected_subnet: null,
2543
+ ipver: 1,
2366
2544
  })
2367
2545
  );
2368
2546
 
2369
2547
  assert.isNull(meeting.statsAnalyzer);
2548
+ assert.isNull(meeting.statsMonitor);
2370
2549
  });
2371
2550
 
2372
2551
  it('should include the peer connection properties correctly for transcoded', async () => {
@@ -2383,8 +2562,14 @@ describe('plugin-meetings', () => {
2383
2562
  },
2384
2563
  },
2385
2564
  });
2386
- // set a statsAnalyzer on the meeting so that we can check that it gets reset to null
2565
+
2566
+ const mockStatsMonitor = {removeAllListeners: sinon.stub()};
2567
+ const mockNetworkQualityMonitor = {removeAllListeners: sinon.stub()};
2568
+
2569
+ // set a statsAnalyzer and statsMonitor on the meeting so that we can check that they get reset to null
2387
2570
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2571
+ meeting.statsMonitor = mockStatsMonitor;
2572
+ meeting.networkQualityMonitor = mockNetworkQualityMonitor;
2388
2573
  const error = await assert.isRejected(meeting.addMedia());
2389
2574
 
2390
2575
  assert(webex.internal.newMetrics.submitInternalEvent.calledTwice);
@@ -2420,12 +2605,18 @@ describe('plugin-meetings', () => {
2420
2605
  signalingState: 'have-local-offer',
2421
2606
  connectionState: 'connecting',
2422
2607
  iceConnectionState: 'checking',
2423
- isSubnetReachable: null,
2424
- selectedCluster: null,
2608
+ subnet_reachable: null,
2609
+ selected_cluster: null,
2610
+ selected_subnet: null,
2611
+ ipver: 1,
2425
2612
  })
2426
2613
  );
2427
2614
 
2428
2615
  assert.isNull(meeting.statsAnalyzer);
2616
+ assert.isNull(meeting.statsMonitor);
2617
+ assert.isNull(meeting.networkQualityMonitor);
2618
+ assert.calledOnce(mockStatsMonitor.removeAllListeners);
2619
+ assert.calledOnce(mockNetworkQualityMonitor.removeAllListeners);
2429
2620
  });
2430
2621
 
2431
2622
  it('should work the second time addMedia is called in case the first time fails', async () => {
@@ -2943,8 +3134,10 @@ describe('plugin-meetings', () => {
2943
3134
  selectedCandidatePairChanges: 2,
2944
3135
  numTransports: 1,
2945
3136
  iceCandidatesCount: 0,
2946
- isSubnetReachable: null,
2947
- selectedCluster: null,
3137
+ subnet_reachable: null,
3138
+ selected_cluster: null,
3139
+ selected_subnet: null,
3140
+ ipver: 1,
2948
3141
  },
2949
3142
  ]);
2950
3143
 
@@ -3146,13 +3339,15 @@ describe('plugin-meetings', () => {
3146
3339
  connectionType: 'udp',
3147
3340
  selectedCandidatePairChanges: 2,
3148
3341
  ipVersion: 'IPv6',
3342
+ ipver: 1,
3149
3343
  numTransports: 1,
3150
3344
  isMultistream: false,
3151
3345
  retriedWithTurnServer: true,
3152
3346
  isJoinWithMediaRetry: false,
3153
3347
  iceCandidatesCount: 0,
3154
- isSubnetReachable: null,
3155
- selectedCluster: null,
3348
+ subnet_reachable: null,
3349
+ selected_cluster: null,
3350
+ selected_subnet: null,
3156
3351
  },
3157
3352
  ]);
3158
3353
  meeting.roap.doTurnDiscovery;
@@ -3286,11 +3481,12 @@ describe('plugin-meetings', () => {
3286
3481
  meeting.mediaConnections = [
3287
3482
  {
3288
3483
  mediaAgentCluster: 'some.cluster',
3289
- }
3290
- ]
3484
+ },
3485
+ ];
3291
3486
  meeting.iceCandidatesCount = 3;
3292
3487
  meeting.iceCandidateErrors.set('701_error', 3);
3293
3488
  meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
3489
+ MeetingUtil.getIpVersion.returns(IP_VERSION.only_ipv6);
3294
3490
 
3295
3491
  await meeting.addMedia({
3296
3492
  mediaSettings: {},
@@ -3306,6 +3502,7 @@ describe('plugin-meetings', () => {
3306
3502
  connectionType: 'udp',
3307
3503
  selectedCandidatePairChanges: 2,
3308
3504
  ipVersion: 'IPv6',
3505
+ ipver: 6,
3309
3506
  numTransports: 1,
3310
3507
  isMultistream: false,
3311
3508
  retriedWithTurnServer: false,
@@ -3315,8 +3512,9 @@ describe('plugin-meetings', () => {
3315
3512
  iceCandidatesCount: 3,
3316
3513
  '701_error': 3,
3317
3514
  '701_turn_host_lookup_received_error': 1,
3318
- isSubnetReachable: null,
3319
- selectedCluster: 'some.cluster',
3515
+ subnet_reachable: null,
3516
+ selected_cluster: 'some.cluster',
3517
+ selected_subnet: null,
3320
3518
  }
3321
3519
  );
3322
3520
 
@@ -3379,9 +3577,11 @@ describe('plugin-meetings', () => {
3379
3577
  iceConnectionState: 'unknown',
3380
3578
  selectedCandidatePairChanges: 2,
3381
3579
  numTransports: 1,
3382
- isSubnetReachable: null,
3383
- selectedCluster: null,
3580
+ subnet_reachable: null,
3581
+ selected_cluster: null,
3582
+ selected_subnet: null,
3384
3583
  iceCandidatesCount: 0,
3584
+ ipver: 1,
3385
3585
  }
3386
3586
  );
3387
3587
 
@@ -3442,16 +3642,18 @@ describe('plugin-meetings', () => {
3442
3642
  numTransports: 1,
3443
3643
  '701_error': 2,
3444
3644
  '701_turn_host_lookup_received_error': 1,
3445
- isSubnetReachable: null,
3446
- selectedCluster: null,
3645
+ subnet_reachable: null,
3646
+ selected_cluster: null,
3647
+ selected_subnet: null,
3447
3648
  iceCandidatesCount: 0,
3649
+ ipver: 1,
3448
3650
  }
3449
3651
  );
3450
3652
 
3451
3653
  assert.isOk(errorThrown);
3452
3654
  });
3453
3655
 
3454
- it('should send valid isSubnetReachability if media connection success', async () => {
3656
+ it('should send subnet reachablity metrics if media connection success', async () => {
3455
3657
  meeting.roap.doTurnDiscovery = sinon.stub().returns({
3456
3658
  turnServerInfo: undefined,
3457
3659
  turnDiscoverySkippedReason: undefined,
@@ -3465,6 +3667,12 @@ describe('plugin-meetings', () => {
3465
3667
  stopReachability: sinon.stub(),
3466
3668
  isSubnetReachable: sinon.stub().returns(false),
3467
3669
  };
3670
+ meeting.mediaServerIp = '1.2.3.4';
3671
+ meeting.mediaConnections = [
3672
+ {
3673
+ mediaAgentCluster: 'some.cluster',
3674
+ },
3675
+ ];
3468
3676
 
3469
3677
  const forceRtcMetricsSend = sinon.stub().resolves();
3470
3678
  const closeMediaConnectionStub = sinon.stub();
@@ -3485,6 +3693,7 @@ describe('plugin-meetings', () => {
3485
3693
  locus_id: meeting.locusUrl.split('/').pop(),
3486
3694
  connectionType: 'udp',
3487
3695
  ipVersion: 'IPv6',
3696
+ ipver: 1,
3488
3697
  selectedCandidatePairChanges: 2,
3489
3698
  numTransports: 1,
3490
3699
  isMultistream: false,
@@ -3492,12 +3701,13 @@ describe('plugin-meetings', () => {
3492
3701
  isJoinWithMediaRetry: false,
3493
3702
  iceCandidatesCount: 0,
3494
3703
  reachability_public_udp_success: 5,
3495
- isSubnetReachable: false,
3496
- selectedCluster: null,
3704
+ subnet_reachable: false,
3705
+ selected_cluster: 'some.cluster',
3706
+ selected_subnet: '1.X.X.X',
3497
3707
  });
3498
3708
  });
3499
3709
 
3500
- it('should send valid isSubnetReachability if media connection fails', async () => {
3710
+ it('should send subnet reachablity metrics if media connection fails', async () => {
3501
3711
  let errorThrown = undefined;
3502
3712
 
3503
3713
  meeting.roap.doTurnDiscovery = sinon.stub().returns({
@@ -3513,6 +3723,12 @@ describe('plugin-meetings', () => {
3513
3723
  stopReachability: sinon.stub(),
3514
3724
  isSubnetReachable: sinon.stub().returns(true),
3515
3725
  };
3726
+ meeting.mediaServerIp = '1.2.3.4';
3727
+ meeting.mediaConnections = [
3728
+ {
3729
+ mediaAgentCluster: 'some.cluster',
3730
+ },
3731
+ ];
3516
3732
 
3517
3733
  const forceRtcMetricsSend = sinon.stub().resolves();
3518
3734
  const closeMediaConnectionStub = sinon.stub();
@@ -3554,9 +3770,11 @@ describe('plugin-meetings', () => {
3554
3770
  selectedCandidatePairChanges: 2,
3555
3771
  numTransports: 1,
3556
3772
  reachability_public_udp_success: 5,
3557
- isSubnetReachable: true,
3558
- selectedCluster: null,
3773
+ subnet_reachable: true,
3774
+ selected_cluster: 'some.cluster',
3775
+ selected_subnet: '1.X.X.X',
3559
3776
  iceCandidatesCount: 0,
3777
+ ipver: 1,
3560
3778
  }
3561
3779
  );
3562
3780
 
@@ -3862,13 +4080,14 @@ describe('plugin-meetings', () => {
3862
4080
  });
3863
4081
  });
3864
4082
 
3865
- it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
4083
+ it('counts the number of members that are in the meeting or lobby for MEDIA_QUALITY event', async () => {
3866
4084
  let fakeMembersCollection = {
3867
4085
  members: {
3868
- member1: {isInMeeting: true},
3869
- member2: {isInMeeting: true},
3870
- member3: {isInMeeting: false},
3871
- },
4086
+ member1: {isInMeeting: true, isInLobby: false},
4087
+ member2: {isInMeeting: false, isInLobby: true},
4088
+ member3: {isInMeeting: false, isInLobby: false},
4089
+ member4: {isInMeeting: true, isInLobby: false},
4090
+ }
3872
4091
  };
3873
4092
  sinon.stub(meeting, 'getMembers').returns({membersCollection: fakeMembersCollection});
3874
4093
  const fakeData = {intervalMetadata: {}};
@@ -3886,11 +4105,12 @@ describe('plugin-meetings', () => {
3886
4105
  },
3887
4106
  payload: {
3888
4107
  intervals: [
3889
- sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2)),
4108
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 3)),
3890
4109
  ],
3891
4110
  },
3892
4111
  });
3893
- fakeMembersCollection.members.member2.isInMeeting = false;
4112
+ // Move member2 from lobby to neither in meeting nor lobby
4113
+ fakeMembersCollection.members.member2.isInLobby = false;
3894
4114
 
3895
4115
  statsAnalyzerStub.emit(
3896
4116
  {file: 'test', function: 'test'},
@@ -3905,7 +4125,7 @@ describe('plugin-meetings', () => {
3905
4125
  },
3906
4126
  payload: {
3907
4127
  intervals: [
3908
- sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1)),
4128
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2)),
3909
4129
  ],
3910
4130
  },
3911
4131
  });
@@ -3932,6 +4152,132 @@ describe('plugin-meetings', () => {
3932
4152
  });
3933
4153
  });
3934
4154
 
4155
+ describe('handles StatsMonitor events', () => {
4156
+ let statsMonitorStub;
4157
+ let prevConfigValue;
4158
+ let listeners;
4159
+
4160
+ beforeEach(async () => {
4161
+ meeting.meetingState = 'ACTIVE';
4162
+ prevConfigValue = meeting.config.stats.enableStatsAnalyzer;
4163
+
4164
+ meeting.config.stats.enableStatsAnalyzer = true;
4165
+
4166
+ listeners = {};
4167
+
4168
+ statsMonitorStub = {
4169
+ on: sinon.stub().callsFake((event, callback) => {
4170
+ listeners[event] = callback;
4171
+ }),
4172
+ removeAllListeners: sinon.stub(),
4173
+ };
4174
+
4175
+ sinon.stub(meeting.mediaProperties, 'sendMediaIssueMetric');
4176
+
4177
+ // mock the StatsMonitor constructor
4178
+ sinon.stub(InternalMediaCoreModule, 'StatsMonitor').returns(statsMonitorStub);
4179
+
4180
+ await meeting.addMedia({
4181
+ mediaSettings: {},
4182
+ });
4183
+ });
4184
+
4185
+ afterEach(() => {
4186
+ meeting.config.stats.enableStatsAnalyzer = prevConfigValue;
4187
+ sinon.restore();
4188
+ });
4189
+
4190
+ describe('INBOUND_AUDIO_ISSUE event', () => {
4191
+ it('should not trigger event when no unmuted members exist', () => {
4192
+ const fakeEventData = {issueSubType: 'DECODE_RESULTS_IN_ZERO_AUDIO_LEVEL'};
4193
+
4194
+ // Setup members that are either self or muted
4195
+ const mutedMember = {
4196
+ isSelf: false,
4197
+ isPairedWithSelf: false,
4198
+ isAudioMuted: true,
4199
+ };
4200
+ const selfMember = {
4201
+ isSelf: true,
4202
+ isPairedWithSelf: false,
4203
+ isAudioMuted: false,
4204
+ };
4205
+ const pairedMember = {
4206
+ isSelf: false,
4207
+ isPairedWithSelf: true,
4208
+ isAudioMuted: false,
4209
+ };
4210
+ meeting.members.membersCollection.getAll = sinon.stub().returns({
4211
+ member1: mutedMember,
4212
+ member2: selfMember,
4213
+ member3: pairedMember,
4214
+ });
4215
+
4216
+ // Reset the stub to clear any previous calls
4217
+ TriggerProxy.trigger.resetHistory();
4218
+
4219
+ // Emit the event from statsMonitor
4220
+ listeners[StatsMonitorEventNames.INBOUND_AUDIO_ISSUE](fakeEventData);
4221
+
4222
+ assert.neverCalledWith(
4223
+ TriggerProxy.trigger,
4224
+ meeting,
4225
+ sinon.match.object,
4226
+ EVENT_TRIGGERS.MEDIA_INBOUND_AUDIO_ISSUE_DETECTED,
4227
+ fakeEventData
4228
+ );
4229
+ assert.notCalled(meeting.mediaProperties.sendMediaIssueMetric);
4230
+ });
4231
+
4232
+ it('should trigger event and metric when there are multiple members and at least one is unmuted', () => {
4233
+ const fakeEventData = {issueSubType: 'DECODE_RESULTS_IN_ZERO_AUDIO_LEVEL'};
4234
+
4235
+ // Setup mixed members - some muted, one unmuted
4236
+ const mutedMember = {
4237
+ isSelf: false,
4238
+ isPairedWithSelf: false,
4239
+ isAudioMuted: true,
4240
+ };
4241
+ const unmutedMember = {
4242
+ isSelf: false,
4243
+ isPairedWithSelf: false,
4244
+ isAudioMuted: false,
4245
+ };
4246
+ const selfMember = {
4247
+ isSelf: true,
4248
+ isPairedWithSelf: false,
4249
+ isAudioMuted: false,
4250
+ };
4251
+ meeting.members.membersCollection.getAll = sinon.stub().returns({
4252
+ member1: mutedMember,
4253
+ member2: unmutedMember,
4254
+ member3: selfMember,
4255
+ });
4256
+
4257
+ // Reset the stub to clear any previous calls
4258
+ TriggerProxy.trigger.resetHistory();
4259
+
4260
+ // Emit the event from statsMonitor
4261
+ listeners[StatsMonitorEventNames.INBOUND_AUDIO_ISSUE](fakeEventData);
4262
+
4263
+ assert.calledWith(
4264
+ TriggerProxy.trigger,
4265
+ meeting,
4266
+ sinon.match.object,
4267
+ EVENT_TRIGGERS.MEDIA_INBOUND_AUDIO_ISSUE_DETECTED,
4268
+ fakeEventData
4269
+ );
4270
+
4271
+ assert.calledOnceWithExactly(
4272
+ meeting.mediaProperties.sendMediaIssueMetric,
4273
+ 'inbound_audio',
4274
+ fakeEventData.issueSubType,
4275
+ meeting.correlationId
4276
+ );
4277
+ });
4278
+ });
4279
+ });
4280
+
3935
4281
  describe('bundlePolicy', () => {
3936
4282
  const FAKE_TURN_URL = 'turns:webex.com:3478';
3937
4283
  const FAKE_TURN_USER = 'some-turn-username';
@@ -4152,7 +4498,7 @@ describe('plugin-meetings', () => {
4152
4498
  meeting.deviceUrl = 'device url';
4153
4499
  meeting.selfId = 'self id';
4154
4500
  meeting.brbState = createBrbState(meeting, false);
4155
- meeting.brbState.enable = sinon.stub().resolves();
4501
+ sinon.stub(meeting.brbState, 'enable').resolves();
4156
4502
  });
4157
4503
 
4158
4504
  afterEach(() => {
@@ -4216,6 +4562,17 @@ describe('plugin-meetings', () => {
4216
4562
 
4217
4563
  assert.notCalled(meeting.audio.handleServerRemoteMuteUpdate);
4218
4564
  });
4565
+
4566
+ it('should reject when brb enable fails', async () => {
4567
+ meeting.brbState.enable.restore();
4568
+
4569
+ const error = new Error();
4570
+ meeting.meetingRequest.setBrb = sinon.stub().rejects(error);
4571
+
4572
+ await expect(meeting.beRightBack(true)).to.be.rejectedWith(error);
4573
+
4574
+ assert.isFalse(meeting.brbState.state.syncToServerInProgress);
4575
+ });
4219
4576
  });
4220
4577
  });
4221
4578
 
@@ -5387,6 +5744,7 @@ describe('plugin-meetings', () => {
5387
5744
  let multistreamEventListeners;
5388
5745
  let transcodedEventListeners;
5389
5746
  let mockStatsAnalyzerCtor;
5747
+ let statsMonitorStub;
5390
5748
 
5391
5749
  const setupFakeRoapMediaConnection = (fakeRoapMediaConnection, eventListeners) => {
5392
5750
  fakeRoapMediaConnection.on.callsFake((eventName, cb) => {
@@ -5418,6 +5776,14 @@ describe('plugin-meetings', () => {
5418
5776
  return {on: sinon.stub(), stopAnalyzer: sinon.stub()};
5419
5777
  });
5420
5778
 
5779
+ statsMonitorStub = {
5780
+ on: sinon.stub(),
5781
+ removeAllListeners: sinon.stub(),
5782
+ };
5783
+
5784
+ // mock the StatsMonitor constructor
5785
+ sinon.stub(InternalMediaCoreModule, 'StatsMonitor').returns(statsMonitorStub);
5786
+
5421
5787
  webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
5422
5788
  sinon.stub();
5423
5789
 
@@ -5480,6 +5846,7 @@ describe('plugin-meetings', () => {
5480
5846
  mockStatsAnalyzerCtor,
5481
5847
  sinon.match({
5482
5848
  isMultistream: true,
5849
+ statsMonitor: statsMonitorStub,
5483
5850
  })
5484
5851
  );
5485
5852
  const initialStatsAnalyzer = mockStatsAnalyzerCtor.returnValues[0];
@@ -6362,25 +6729,36 @@ describe('plugin-meetings', () => {
6362
6729
  const DIAL_IN_URL = meeting.dialInUrl;
6363
6730
 
6364
6731
  assert.calledWith(meeting.meetingRequest.dialIn, {
6365
- correlationId: meeting.correlationId,
6732
+ correlationId: meeting.pstnCorrelationId,
6366
6733
  dialInUrl: DIAL_IN_URL,
6367
6734
  locusUrl: meeting.locusUrl,
6368
6735
  clientUrl: meeting.deviceUrl,
6369
6736
  });
6370
6737
  assert.notCalled(meeting.meetingRequest.dialOut);
6371
6738
 
6739
+ // Verify pstnCorrelationId was set
6740
+ assert.exists(meeting.pstnCorrelationId);
6741
+ assert.notEqual(meeting.pstnCorrelationId, meeting.correlationId);
6742
+ const firstPstnCorrelationId = meeting.pstnCorrelationId
6743
+
6372
6744
  meeting.meetingRequest.dialIn.resetHistory();
6373
6745
 
6374
6746
  // try again. the dial in urls should match
6375
6747
  await meeting.usePhoneAudio();
6376
6748
 
6377
6749
  assert.calledWith(meeting.meetingRequest.dialIn, {
6378
- correlationId: meeting.correlationId,
6750
+ correlationId: meeting.pstnCorrelationId,
6379
6751
  dialInUrl: DIAL_IN_URL,
6380
6752
  locusUrl: meeting.locusUrl,
6381
6753
  clientUrl: meeting.deviceUrl,
6382
6754
  });
6383
6755
  assert.notCalled(meeting.meetingRequest.dialOut);
6756
+ // A new PSTN correlationId should be generated for the second attempt
6757
+ assert.notEqual(
6758
+ meeting.pstnCorrelationId,
6759
+ firstPstnCorrelationId,
6760
+ 'pstnCorrelationId should be regenerated on each dial-in attempt'
6761
+ );
6384
6762
  });
6385
6763
 
6386
6764
  it('given a phone number, triggers dial-out, delegating request to meetingRequest correctly', async () => {
@@ -6390,7 +6768,7 @@ describe('plugin-meetings', () => {
6390
6768
  const DIAL_OUT_URL = meeting.dialOutUrl;
6391
6769
 
6392
6770
  assert.calledWith(meeting.meetingRequest.dialOut, {
6393
- correlationId: meeting.correlationId,
6771
+ correlationId: meeting.pstnCorrelationId,
6394
6772
  dialOutUrl: DIAL_OUT_URL,
6395
6773
  locusUrl: meeting.locusUrl,
6396
6774
  clientUrl: meeting.deviceUrl,
@@ -6398,49 +6776,126 @@ describe('plugin-meetings', () => {
6398
6776
  });
6399
6777
  assert.notCalled(meeting.meetingRequest.dialIn);
6400
6778
 
6779
+ // Verify pstnCorrelationId was set
6780
+ assert.exists(meeting.pstnCorrelationId);
6781
+ assert.notEqual(meeting.pstnCorrelationId, meeting.correlationId);
6782
+ const firstPstnCorrelationId = meeting.pstnCorrelationId;
6783
+
6401
6784
  meeting.meetingRequest.dialOut.resetHistory();
6402
6785
 
6403
6786
  // try again. the dial out urls should match
6404
6787
  await meeting.usePhoneAudio(phoneNumber);
6405
6788
 
6406
6789
  assert.calledWith(meeting.meetingRequest.dialOut, {
6407
- correlationId: meeting.correlationId,
6790
+ correlationId: meeting.pstnCorrelationId,
6408
6791
  dialOutUrl: DIAL_OUT_URL,
6409
6792
  locusUrl: meeting.locusUrl,
6410
6793
  clientUrl: meeting.deviceUrl,
6411
6794
  phoneNumber,
6412
6795
  });
6413
6796
  assert.notCalled(meeting.meetingRequest.dialIn);
6797
+ // A new PSTN correlationId should be generated for the second attempt
6798
+ assert.notEqual(
6799
+ meeting.pstnCorrelationId,
6800
+ firstPstnCorrelationId,
6801
+ 'pstnCorrelationId should be regenerated on each dial-out attempt'
6802
+ );
6414
6803
  });
6415
6804
 
6416
- it('rejects if the request failed (dial in)', () => {
6417
- const error = 'something bad happened';
6805
+ it('rejects if the request failed (dial in)', async () => {
6806
+ const error = {error: {message: 'dial in failed'}, stack: 'error stack'};
6418
6807
 
6419
6808
  meeting.meetingRequest.dialIn = sinon.stub().returns(Promise.reject(error));
6420
6809
 
6421
- return meeting
6422
- .usePhoneAudio()
6423
- .then(() => Promise.reject(new Error('Promise resolved when it should have rejected')))
6424
- .catch((e) => {
6425
- assert.equal(e, error);
6810
+ try {
6811
+ await meeting.usePhoneAudio();
6812
+ throw new Error('Promise resolved when it should have rejected');
6813
+ } catch (e) {
6814
+ assert.equal(e, error);
6426
6815
 
6427
- return Promise.resolve();
6816
+ // Verify behavioral metric was sent with dial_in_correlation_id
6817
+ assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_DIAL_IN_FAILURE, {
6818
+ correlation_id: meeting.correlationId,
6819
+ dial_in_url: meeting.dialInUrl,
6820
+ dial_in_correlation_id: sinon.match.string,
6821
+ locus_id: meeting.locusUrl.split('/').pop(),
6822
+ client_url: meeting.deviceUrl,
6823
+ reason: error.error.message,
6824
+ stack: error.stack,
6428
6825
  });
6826
+
6827
+ // Verify pstnCorrelationId was cleared after error
6828
+ assert.equal(meeting.pstnCorrelationId, undefined);
6829
+ }
6429
6830
  });
6430
6831
 
6431
6832
  it('rejects if the request failed (dial out)', async () => {
6432
- const error = 'something bad happened';
6833
+ const error = {error: {message: 'dial out failed'}, stack: 'error stack'};
6433
6834
 
6434
6835
  meeting.meetingRequest.dialOut = sinon.stub().returns(Promise.reject(error));
6435
6836
 
6436
- return meeting
6437
- .usePhoneAudio('+441234567890')
6438
- .then(() => Promise.reject(new Error('Promise resolved when it should have rejected')))
6439
- .catch((e) => {
6440
- assert.equal(e, error);
6837
+ try {
6838
+ await meeting.usePhoneAudio('+441234567890');
6839
+ throw new Error('Promise resolved when it should have rejected');
6840
+ } catch (e) {
6841
+ assert.equal(e, error);
6441
6842
 
6442
- return Promise.resolve();
6843
+ // Verify behavioral metric was sent with dial_out_correlation_id
6844
+ assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.ADD_DIAL_OUT_FAILURE, {
6845
+ correlation_id: meeting.correlationId,
6846
+ dial_out_url: meeting.dialOutUrl,
6847
+ dial_out_correlation_id: sinon.match.string,
6848
+ locus_id: meeting.locusUrl.split('/').pop(),
6849
+ client_url: meeting.deviceUrl,
6850
+ reason: error.error.message,
6851
+ stack: error.stack,
6443
6852
  });
6853
+
6854
+ // Verify pstnCorrelationId was cleared after error
6855
+ assert.equal(meeting.pstnCorrelationId, undefined);
6856
+ }
6857
+ });
6858
+ });
6859
+
6860
+ describe('#disconnectPhoneAudio', () => {
6861
+ beforeEach(() => {
6862
+ // Mock the MeetingUtil.disconnectPhoneAudio method
6863
+ sinon.stub(MeetingUtil, 'disconnectPhoneAudio').resolves();
6864
+ meeting.dialInUrl = 'dialin:///test-dial-in-url';
6865
+ meeting.dialOutUrl = 'dialout:///test-dial-out-url';
6866
+ meeting.dialInDeviceStatus = 'JOINED';
6867
+ meeting.dialOutDeviceStatus = 'JOINED';
6868
+ });
6869
+
6870
+ afterEach(() => {
6871
+ MeetingUtil.disconnectPhoneAudio.restore();
6872
+ });
6873
+
6874
+ it('should disconnect phone audio and clear pstnCorrelationId', async () => {
6875
+ meeting.pstnCorrelationId = 'test-pstn-correlation-id';
6876
+
6877
+ await meeting.disconnectPhoneAudio();
6878
+
6879
+ // Verify that pstnCorrelationId is cleared
6880
+ assert.equal(meeting.pstnCorrelationId, undefined);
6881
+
6882
+ // Verify that MeetingUtil.disconnectPhoneAudio was called for both dial-in and dial-out
6883
+ assert.calledTwice(MeetingUtil.disconnectPhoneAudio);
6884
+ assert.calledWith(MeetingUtil.disconnectPhoneAudio, meeting, meeting.dialInUrl);
6885
+ assert.calledWith(MeetingUtil.disconnectPhoneAudio, meeting, meeting.dialOutUrl);
6886
+ });
6887
+
6888
+ it('should handle case when no PSTN connection is active', async () => {
6889
+ meeting.dialInDeviceStatus = 'IDLE';
6890
+ meeting.dialOutDeviceStatus = 'IDLE';
6891
+ meeting.pstnCorrelationId = 'test-pstn-correlation-id';
6892
+
6893
+ await meeting.disconnectPhoneAudio();
6894
+
6895
+ // Verify that pstnCorrelationId is still cleared even when no phone connection is active
6896
+ assert.equal(meeting.pstnCorrelationId, undefined);
6897
+ // And verify no disconnect was attempted
6898
+ assert.notCalled(MeetingUtil.disconnectPhoneAudio);
6444
6899
  });
6445
6900
  });
6446
6901
 
@@ -7194,6 +7649,8 @@ describe('plugin-meetings', () => {
7194
7649
  'locus-id',
7195
7650
  {extraParam1: 'value1', permissionToken: FAKE_PERMISSION_TOKEN},
7196
7651
  {meetingId: meeting.id, sendCAevents: true},
7652
+ null,
7653
+ null,
7197
7654
  null
7198
7655
  );
7199
7656
  assert.deepEqual(meeting.meetingInfo, {
@@ -7240,6 +7697,8 @@ describe('plugin-meetings', () => {
7240
7697
  'locus-id',
7241
7698
  {extraParam1: 'value1', permissionToken: FAKE_PERMISSION_TOKEN},
7242
7699
  {meetingId: meeting.id, sendCAevents: true},
7700
+ null,
7701
+ null,
7243
7702
  null
7244
7703
  );
7245
7704
  assert.deepEqual(meeting.meetingInfo, {
@@ -7295,6 +7754,8 @@ describe('plugin-meetings', () => {
7295
7754
  permissionToken: FAKE_PERMISSION_TOKEN,
7296
7755
  },
7297
7756
  {meetingId: meeting.id, sendCAevents: true},
7757
+ null,
7758
+ null,
7298
7759
  null
7299
7760
  );
7300
7761
  assert.deepEqual(meeting.meetingInfo, {
@@ -7957,6 +8418,7 @@ describe('plugin-meetings', () => {
7957
8418
 
7958
8419
  meeting.requestScreenShareFloor = sinon.stub().resolves({});
7959
8420
  meeting.releaseScreenShareFloor = sinon.stub().resolves({});
8421
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
7960
8422
  meeting.mediaProperties.mediaDirection = {
7961
8423
  sendAudio: 'fake value', // using non-boolean here so that we can check that these values are untouched in tests
7962
8424
  sendVideo: 'fake value',
@@ -8038,6 +8500,12 @@ describe('plugin-meetings', () => {
8038
8500
  payload: {mediaType: 'share', shareInstanceId: meeting.localShareInstanceId},
8039
8501
  options: {meetingId: meeting.id},
8040
8502
  });
8503
+
8504
+ // ensure the share start timestamp is saved
8505
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
8506
+ key: 'internal.client.share.initiated',
8507
+ });
8508
+
8041
8509
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
8042
8510
 
8043
8511
  assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
@@ -8056,6 +8524,11 @@ describe('plugin-meetings', () => {
8056
8524
  options: {meetingId: meeting.id},
8057
8525
  });
8058
8526
 
8527
+ // ensure the share start timestamp is saved
8528
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
8529
+ key: 'internal.client.share.initiated',
8530
+ });
8531
+
8059
8532
  assert.calledWith(
8060
8533
  meeting.sendSlotManager.getSlot(MediaType.AudioSlides).publishStream,
8061
8534
  stream
@@ -8724,11 +9197,16 @@ describe('plugin-meetings', () => {
8724
9197
  meeting.hasMediaConnectionConnectedAtLeastOnce = false;
8725
9198
  meeting.setupMediaConnectionListeners();
8726
9199
 
9200
+ sinon.stub(MeetingUtil, 'getCaEventLabelsForIpVersion').returns(['fake labels']);
9201
+
8727
9202
  simulateConnectionStateChange(ConnectionState.Connecting);
8728
9203
 
8729
9204
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
8730
9205
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
8731
9206
  name: 'client.ice.start',
9207
+ payload: {
9208
+ labels: ['fake labels'],
9209
+ },
8732
9210
  options: {
8733
9211
  meetingId: meeting.id,
8734
9212
  },
@@ -10011,6 +10489,24 @@ describe('plugin-meetings', () => {
10011
10489
  );
10012
10490
  });
10013
10491
 
10492
+ it('listens to CONTROLS_POLLING_QA_CHANGED', async () => {
10493
+ const state = {example: 'value'};
10494
+
10495
+ await meeting.locusInfo.emitScoped(
10496
+ {function: 'test', file: 'test'},
10497
+ LOCUSINFO.EVENTS.CONTROLS_POLLING_QA_CHANGED,
10498
+ {state}
10499
+ );
10500
+
10501
+ assert.calledWith(
10502
+ TriggerProxy.trigger,
10503
+ meeting,
10504
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
10505
+ EVENT_TRIGGERS.MEETING_CONTROLS_POLLING_QA_UPDATED,
10506
+ {state}
10507
+ );
10508
+ });
10509
+
10014
10510
  it('listens to the locus interpretation update event', () => {
10015
10511
  const interpretation = {
10016
10512
  siLanguages: [{languageCode: 20, languageName: 'en'}],
@@ -10311,6 +10807,8 @@ describe('plugin-meetings', () => {
10311
10807
  meeting.mediaProperties = {mediaDirection: {sendShare: true}};
10312
10808
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
10313
10809
  (meeting.deviceUrl = 'deviceUrl.com'), (meeting.localShareInstanceId = '1234-5678');
10810
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
10811
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1000);
10314
10812
  });
10315
10813
  it('should call changeMeetingFloor()', async () => {
10316
10814
  meeting.screenShareFloorState = 'GRANTED';
@@ -10328,6 +10826,22 @@ describe('plugin-meetings', () => {
10328
10826
  assert.exists(share.then);
10329
10827
  await share;
10330
10828
  assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
10829
+
10830
+ // ensure the share stop timestamp is saved
10831
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
10832
+ key: 'internal.client.share.stopped',
10833
+ });
10834
+
10835
+ // ensure the CA share stopped metric is submitted with duration
10836
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
10837
+ name: 'client.share.stopped',
10838
+ payload: {
10839
+ mediaType: 'share',
10840
+ shareInstanceId: meeting.localShareInstanceId,
10841
+ shareDuration: 1000,
10842
+ },
10843
+ options: {meetingId: meeting.id},
10844
+ });
10331
10845
  });
10332
10846
  it('should not call changeMeetingFloor() if someone else already has the floor', async () => {
10333
10847
  // change selfId so that it doesn't match the beneficiary id from meeting.locusInfo.mediaShares
@@ -10911,6 +11425,7 @@ describe('plugin-meetings', () => {
10911
11425
  let canUserRenameOthersSpy;
10912
11426
  let canShareWhiteBoardSpy;
10913
11427
  let canMoveToLobbySpy;
11428
+ let isSpokenLanguageAutoDetectionEnabledSpy;
10914
11429
  // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
10915
11430
 
10916
11431
  beforeEach(() => {
@@ -10942,6 +11457,8 @@ describe('plugin-meetings', () => {
10942
11457
  canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
10943
11458
  canShareWhiteBoardSpy = sinon.spy(MeetingUtil, 'canShareWhiteBoard');
10944
11459
  canMoveToLobbySpy = sinon.spy(MeetingUtil, 'canMoveToLobby');
11460
+ isSpokenLanguageAutoDetectionEnabledSpy = sinon.spy(MeetingUtil, 'isSpokenLanguageAutoDetectionEnabled');
11461
+
10945
11462
  });
10946
11463
 
10947
11464
  afterEach(() => {
@@ -11494,6 +12011,7 @@ describe('plugin-meetings', () => {
11494
12011
  assert.calledWith(canUserRenameOthersSpy, userDisplayHints);
11495
12012
  assert.calledWith(canShareWhiteBoardSpy, userDisplayHints, selfUserPolicies);
11496
12013
  assert.calledWith(canMoveToLobbySpy, userDisplayHints);
12014
+ assert.calledWith(isSpokenLanguageAutoDetectionEnabledSpy, userDisplayHints);
11497
12015
 
11498
12016
  assert.calledWith(ControlsOptionsUtil.hasHints, {
11499
12017
  requiredHints: [DISPLAY_HINTS.MUTE_ALL],
@@ -11603,6 +12121,14 @@ describe('plugin-meetings', () => {
11603
12121
  requiredHints: [DISPLAY_HINTS.DISABLE_RDC_MEETING_OPTION],
11604
12122
  displayHints: userDisplayHints,
11605
12123
  });
12124
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
12125
+ requiredHints: [DISPLAY_HINTS.ENABLE_ATTENDEE_START_POLLING_QA],
12126
+ displayHints: userDisplayHints,
12127
+ });
12128
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
12129
+ requiredHints: [DISPLAY_HINTS.DISABLE_ATTENDEE_START_POLLING_QA],
12130
+ displayHints: userDisplayHints,
12131
+ });
11606
12132
 
11607
12133
  assert.calledWith(
11608
12134
  TriggerProxy.trigger,
@@ -11892,6 +12418,7 @@ describe('plugin-meetings', () => {
11892
12418
  meeting.locusInfo.self = {url: url1};
11893
12419
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
11894
12420
  meeting.deviceUrl = 'deviceUrl.com';
12421
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
11895
12422
  });
11896
12423
  it('should have #startWhiteboardShare', () => {
11897
12424
  assert.exists(meeting.startWhiteboardShare);
@@ -11919,6 +12446,11 @@ describe('plugin-meetings', () => {
11919
12446
  payload: {mediaType: 'whiteboard'},
11920
12447
  options: {meetingId: meeting.id},
11921
12448
  });
12449
+
12450
+ // ensure the share start timestamp is saved
12451
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
12452
+ key: 'internal.client.share.initiated',
12453
+ });
11922
12454
  });
11923
12455
  });
11924
12456
  describe('#stopWhiteboardShare', () => {
@@ -11930,6 +12462,9 @@ describe('plugin-meetings', () => {
11930
12462
  meeting.locusInfo.self = {url: url1};
11931
12463
  meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
11932
12464
  meeting.deviceUrl = 'deviceUrl.com';
12465
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
12466
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1000);
12467
+ webex.internal.newMetrics.submitClientEvent = sinon.stub();
11933
12468
  });
11934
12469
  it('should stop the whiteboard share', async () => {
11935
12470
  const whiteboardShare = meeting.stopWhiteboardShare();
@@ -11944,6 +12479,21 @@ describe('plugin-meetings', () => {
11944
12479
  uri: url1,
11945
12480
  });
11946
12481
  assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
12482
+
12483
+ // ensure the share stop timestamp is saved
12484
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
12485
+ key: 'internal.client.share.stopped',
12486
+ });
12487
+
12488
+ // ensure the CA share stopped metric is submitted with duration
12489
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
12490
+ name: 'client.share.stopped',
12491
+ payload: {
12492
+ mediaType: 'whiteboard',
12493
+ shareDuration: 1000,
12494
+ },
12495
+ options: {meetingId: meeting.id},
12496
+ });
11947
12497
  });
11948
12498
  });
11949
12499
  });
@@ -12016,6 +12566,9 @@ describe('plugin-meetings', () => {
12016
12566
  meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
12017
12567
  meeting.deviceUrl = 'my-web-url';
12018
12568
  meeting.locusInfo.info = {isWebinar: false};
12569
+ webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
12570
+ webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration = sinon.stub().returns(1500);
12571
+ webex.internal.newMetrics.submitClientEvent = sinon.stub();
12019
12572
  });
12020
12573
 
12021
12574
  const USER_IDS = {
@@ -12242,12 +12795,12 @@ describe('plugin-meetings', () => {
12242
12795
  activeSharingId.whiteboard = beneficiaryId;
12243
12796
 
12244
12797
  eventTrigger.share.push(
12245
- meeting.webinar.selfIsAttendee
12798
+ meeting.webinar.selfIsAttendee || meeting.guest
12246
12799
  ? {
12247
12800
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12248
12801
  functionName: 'remoteShare',
12249
12802
  eventPayload: {
12250
- memberId: null,
12803
+ memberId: meeting.webinar.selfIsAttendee ? beneficiaryId : null,
12251
12804
  url,
12252
12805
  shareInstanceId,
12253
12806
  annotationInfo: undefined,
@@ -12261,7 +12814,8 @@ describe('plugin-meetings', () => {
12261
12814
  }
12262
12815
  );
12263
12816
 
12264
- shareStatus = meeting.webinar.selfIsAttendee
12817
+ shareStatus =
12818
+ meeting.webinar.selfIsAttendee || meeting.guest
12265
12819
  ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
12266
12820
  : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
12267
12821
  }
@@ -12301,7 +12855,7 @@ describe('plugin-meetings', () => {
12301
12855
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12302
12856
  functionName: 'remoteShare',
12303
12857
  eventPayload: {
12304
- memberId: null,
12858
+ memberId: beneficiaryId,
12305
12859
  url,
12306
12860
  shareInstanceId,
12307
12861
  annotationInfo: undefined,
@@ -12479,6 +13033,36 @@ describe('plugin-meetings', () => {
12479
13033
  });
12480
13034
  });
12481
13035
 
13036
+ describe('Whiteboard Share - User is guest', () => {
13037
+ it('User receives a remote share instead of whiteboard share', () => {
13038
+ // Set the guest flag
13039
+ meeting.guest = true;
13040
+
13041
+ // Step 1: Start sharing whiteboard A
13042
+ const data1 = generateData(
13043
+ blankPayload, // Initial payload
13044
+ true, // isGranting: Granting share
13045
+ false, // isContent: Whiteboard (not content)
13046
+ USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
13047
+ RESOURCE_URLS.WHITEBOARD_A // Resource URL: Whiteboard A
13048
+ );
13049
+
13050
+ // Step 2: Stop sharing whiteboard A
13051
+ const data2 = generateData(
13052
+ data1.payload, // Updated payload from Step 1
13053
+ false, // isGranting: Stopping share
13054
+ false, // isContent: Whiteboard
13055
+ USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
13056
+ );
13057
+
13058
+ // Validate the payload changes and status updates
13059
+ payloadTestHelper([data1]);
13060
+
13061
+ // Specific assertions for guest
13062
+ assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
13063
+ });
13064
+ });
13065
+
12482
13066
  describe('Whiteboard A --> Whiteboard B', () => {
12483
13067
  it('Scenario #1: you share both whiteboards', () => {
12484
13068
  const data1 = generateData(
@@ -13130,7 +13714,54 @@ describe('plugin-meetings', () => {
13130
13714
  payloadTestHelper([data1, data2, data3]);
13131
13715
  });
13132
13716
  });
13133
- });
13717
+
13718
+ it('should send share stopped metric when whiteboard sharing stops', () => {
13719
+ // Start whiteboard sharing (this won't trigger metrics)
13720
+ const data1 = generateData(
13721
+ blankPayload,
13722
+ true, // isGranting: true
13723
+ false, // isContent: false (whiteboard)
13724
+ USER_IDS.ME,
13725
+ RESOURCE_URLS.WHITEBOARD_A
13726
+ );
13727
+
13728
+ // Stop whiteboard sharing (this should trigger metrics)
13729
+ const data2 = generateData(
13730
+ data1.payload,
13731
+ false, // isGranting: false (stopping share)
13732
+ false, // isContent: false (whiteboard)
13733
+ USER_IDS.ME
13734
+ );
13735
+
13736
+ // Trigger the events
13737
+ meeting.locusInfo.emit(
13738
+ {function: 'test', file: 'test'},
13739
+ EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
13740
+ data1.payload
13741
+ );
13742
+
13743
+ meeting.locusInfo.emit(
13744
+ {function: 'test', file: 'test'},
13745
+ EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
13746
+ data2.payload
13747
+ );
13748
+
13749
+ // Verify metrics were called when whiteboard sharing stopped
13750
+ assert.calledWith(webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp, {
13751
+ key: 'internal.client.share.stopped',
13752
+ });
13753
+
13754
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
13755
+ name: 'client.share.stopped',
13756
+ payload: {
13757
+ mediaType: 'whiteboard',
13758
+ shareDuration: 1500, // mocked return value
13759
+ },
13760
+ options: {
13761
+ meetingId: meeting.id,
13762
+ },
13763
+ });
13764
+ });
13134
13765
 
13135
13766
  describe('handleShareVideoStreamMuteStateChange', () => {
13136
13767
  it('should emit MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE event with correct fields', () => {
@@ -13157,6 +13788,7 @@ describe('plugin-meetings', () => {
13157
13788
  });
13158
13789
  });
13159
13790
  });
13791
+ });
13160
13792
 
13161
13793
  describe('#startKeepAlive', () => {
13162
13794
  let clock;
@@ -13923,4 +14555,469 @@ describe('plugin-meetings', () => {
13923
14555
  assert.equal(result.failureReason, MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA);
13924
14556
  });
13925
14557
  });
14558
+
14559
+ describe('#setStage', () => {
14560
+ const check = async (options, expectedVideoLayout) => {
14561
+ const locusUrl = `https://locus-test.wbx2.com/locus/api/v1/loci/${uuidv4()}`;
14562
+ meeting.locusUrl = locusUrl;
14563
+
14564
+ const setStagePromise = meeting.setStage(options);
14565
+
14566
+ assert.exists(setStagePromise.then);
14567
+ await setStagePromise;
14568
+
14569
+ assert.calledOnceWithExactly(
14570
+ meeting.meetingRequest.synchronizeStage,
14571
+ locusUrl,
14572
+ expectedVideoLayout
14573
+ );
14574
+ };
14575
+
14576
+ beforeEach(() => {
14577
+ meeting.meetingRequest.synchronizeStage = sinon.stub().returns(Promise.resolve());
14578
+ });
14579
+
14580
+ it('sends the expected request when no options are provided', async () => {
14581
+ await check(undefined, {
14582
+ overrideDefault: true,
14583
+ lockAttendeeViewOnStageOnly: false,
14584
+ stageParameters: {
14585
+ activeSpeakerProportion: 0.5,
14586
+ showActiveSpeaker: {show: false, order: 0},
14587
+ stageManagerType: 0,
14588
+ },
14589
+ });
14590
+ });
14591
+
14592
+ it('sends the expected request when empty options are provided', async () => {
14593
+ await check(
14594
+ {},
14595
+ {
14596
+ overrideDefault: true,
14597
+ lockAttendeeViewOnStageOnly: false,
14598
+ stageParameters: {
14599
+ activeSpeakerProportion: 0.5,
14600
+ showActiveSpeaker: {show: false, order: 0},
14601
+ stageManagerType: 0,
14602
+ },
14603
+ }
14604
+ );
14605
+ });
14606
+
14607
+ [0.25, 0.5, 0.75].forEach((activeSpeakerProportion) => {
14608
+ it(`sends the expected request when only the active speaker proportion option is provided as ${activeSpeakerProportion}`, async () => {
14609
+ await check(
14610
+ {activeSpeakerProportion},
14611
+ {
14612
+ overrideDefault: true,
14613
+ lockAttendeeViewOnStageOnly: false,
14614
+ stageParameters: {
14615
+ activeSpeakerProportion,
14616
+ showActiveSpeaker: {show: false, order: 0},
14617
+ stageManagerType: 0,
14618
+ },
14619
+ }
14620
+ );
14621
+ });
14622
+ });
14623
+
14624
+ it('sends the expected request when only the custom background option is provided', async () => {
14625
+ const customBackground = {
14626
+ url: `https://test.wbx2.com/background/${uuidv4()}.jpg`,
14627
+ };
14628
+
14629
+ await check(
14630
+ {customBackground},
14631
+ {
14632
+ overrideDefault: true,
14633
+ lockAttendeeViewOnStageOnly: false,
14634
+ stageParameters: {
14635
+ activeSpeakerProportion: 0.5,
14636
+ showActiveSpeaker: {show: false, order: 0},
14637
+ stageManagerType: 2,
14638
+ },
14639
+ customLayouts: {background: customBackground},
14640
+ }
14641
+ );
14642
+ });
14643
+
14644
+ it('sends the expected request when only the custom logo option is provided', async () => {
14645
+ const customLogo = {
14646
+ url: `https://test.wbx2.com/logo/${uuidv4()}.png`,
14647
+ position: 'LowerRight',
14648
+ };
14649
+
14650
+ await check(
14651
+ {customLogo},
14652
+ {
14653
+ overrideDefault: true,
14654
+ lockAttendeeViewOnStageOnly: false,
14655
+ stageParameters: {
14656
+ activeSpeakerProportion: 0.5,
14657
+ showActiveSpeaker: {show: false, order: 0},
14658
+ stageManagerType: 1,
14659
+ },
14660
+ customLayouts: {logo: customLogo},
14661
+ }
14662
+ );
14663
+ });
14664
+
14665
+ it('sends the expected request when only the custom name label option is provided', async () => {
14666
+ const customNameLabel = {
14667
+ accentColor: '#0A7806',
14668
+ background: {color: 'rgba(255, 255, 255, 1)'},
14669
+ border: {color: 'rgba(255, 255, 255, 1)'},
14670
+ content: {
14671
+ displayName: {color: 'rgba(0, 0, 0, 0.95)'},
14672
+ subtitle: {color: 'rgba(0, 0, 0, 0.6)'},
14673
+ },
14674
+ decoration: {color: 'rgba(10, 120, 6, 1)'},
14675
+ fadeOut: {delay: 15},
14676
+ type: 'Primary',
14677
+ };
14678
+
14679
+ await check(
14680
+ {customNameLabel},
14681
+ {
14682
+ overrideDefault: true,
14683
+ lockAttendeeViewOnStageOnly: false,
14684
+ stageParameters: {
14685
+ activeSpeakerProportion: 0.5,
14686
+ showActiveSpeaker: {show: false, order: 0},
14687
+ stageManagerType: 4,
14688
+ },
14689
+ nameLabelStyle: customNameLabel,
14690
+ }
14691
+ );
14692
+ });
14693
+
14694
+ it('sends the expected request when only the custom background and logo options are provided', async () => {
14695
+ const customBackground = {
14696
+ url: `https://test.wbx2.com/background/${uuidv4()}.jpg`,
14697
+ };
14698
+ const customLogo = {
14699
+ url: `https://test.wbx2.com/logo/${uuidv4()}.png`,
14700
+ position: 'UpperRight',
14701
+ };
14702
+
14703
+ await check(
14704
+ {customBackground, customLogo},
14705
+ {
14706
+ overrideDefault: true,
14707
+ lockAttendeeViewOnStageOnly: false,
14708
+ stageParameters: {
14709
+ activeSpeakerProportion: 0.5,
14710
+ showActiveSpeaker: {show: false, order: 0},
14711
+ stageManagerType: 3,
14712
+ },
14713
+ customLayouts: {background: customBackground, logo: customLogo},
14714
+ }
14715
+ );
14716
+ });
14717
+
14718
+ it('sends the expected request when only the custom background and name label options are provided', async () => {
14719
+ const customBackground = {
14720
+ url: `https://test.wbx2.com/background/${uuidv4()}.jpg`,
14721
+ };
14722
+ const customNameLabel = {
14723
+ accentColor: '#00A3FF',
14724
+ background: {color: 'rgba(0, 163, 255, 1)'},
14725
+ border: {color: 'rgba(0, 163, 255, 1)'},
14726
+ content: {
14727
+ displayName: {color: 'rgba(255, 255, 255, 0.95)'},
14728
+ subtitle: {color: 'rgba(255, 255, 255, 0.7)'},
14729
+ },
14730
+ decoration: {color: 'rgba(255, 255, 255, 0.95)'},
14731
+ fadeOut: {delay: 15},
14732
+ type: 'PrimaryInverted',
14733
+ };
14734
+
14735
+ await check(
14736
+ {customBackground, customNameLabel},
14737
+ {
14738
+ overrideDefault: true,
14739
+ lockAttendeeViewOnStageOnly: false,
14740
+ stageParameters: {
14741
+ activeSpeakerProportion: 0.5,
14742
+ showActiveSpeaker: {show: false, order: 0},
14743
+ stageManagerType: 6,
14744
+ },
14745
+ customLayouts: {background: customBackground},
14746
+ nameLabelStyle: customNameLabel,
14747
+ }
14748
+ );
14749
+ });
14750
+
14751
+ it('sends the expected request when only the custom logo and name label options are provided', async () => {
14752
+ const customLogo = {
14753
+ url: `https://test.wbx2.com/logo/${uuidv4()}.png`,
14754
+ position: 'UpperLeft',
14755
+ };
14756
+ const customNameLabel = {
14757
+ accentColor: '#942B2B',
14758
+ background: {color: 'rgba(255, 255, 255, 1)'},
14759
+ border: {color: 'rgba(148, 43, 43, 1)'},
14760
+ content: {
14761
+ displayName: {color: 'rgba(0, 0, 0, 0.95)'},
14762
+ subtitle: {color: 'rgba(0, 0, 0, 0.6)'},
14763
+ },
14764
+ decoration: {color: 'rgba(0, 0, 0, 0)'},
14765
+ fadeOut: {delay: 15},
14766
+ type: 'Secondary',
14767
+ };
14768
+
14769
+ await check(
14770
+ {customLogo, customNameLabel},
14771
+ {
14772
+ overrideDefault: true,
14773
+ lockAttendeeViewOnStageOnly: false,
14774
+ stageParameters: {
14775
+ activeSpeakerProportion: 0.5,
14776
+ showActiveSpeaker: {show: false, order: 0},
14777
+ stageManagerType: 5,
14778
+ },
14779
+ customLayouts: {logo: customLogo},
14780
+ nameLabelStyle: customNameLabel,
14781
+ }
14782
+ );
14783
+ });
14784
+
14785
+ it('sends the expected request when only the custom background, logo, name label options are provided', async () => {
14786
+ const customBackground = {
14787
+ url: `https://test.wbx2.com/background/${uuidv4()}.jpg`,
14788
+ };
14789
+ const customLogo = {
14790
+ url: `https://test.wbx2.com/logo/${uuidv4()}.png`,
14791
+ position: 'LowerLeft',
14792
+ };
14793
+ const customNameLabel = {
14794
+ accentColor: '#EBD960',
14795
+ background: {color: 'rgba(235, 217, 96, 0.55)'},
14796
+ border: {color: 'rgba(235, 217, 96, 0.55)'},
14797
+ content: {
14798
+ displayName: {color: 'rgba(255, 255, 255, 0.95)'},
14799
+ subtitle: {color: 'rgba(255, 255, 255, 0.7)'},
14800
+ },
14801
+ decoration: {color: 'rgba(0, 0, 0, 0)'},
14802
+ fadeOut: {delay: 15},
14803
+ type: 'SecondaryInverted',
14804
+ };
14805
+
14806
+ await check(
14807
+ {customBackground, customLogo, customNameLabel},
14808
+ {
14809
+ overrideDefault: true,
14810
+ lockAttendeeViewOnStageOnly: false,
14811
+ stageParameters: {
14812
+ activeSpeakerProportion: 0.5,
14813
+ showActiveSpeaker: {show: false, order: 0},
14814
+ stageManagerType: 7,
14815
+ },
14816
+ customLayouts: {background: customBackground, logo: customLogo},
14817
+ nameLabelStyle: customNameLabel,
14818
+ }
14819
+ );
14820
+ });
14821
+
14822
+ it('sends the expected request when only the important participants option is provided as empty', async () => {
14823
+ await check(
14824
+ {importantParticipants: []},
14825
+ {
14826
+ overrideDefault: true,
14827
+ lockAttendeeViewOnStageOnly: false,
14828
+ stageParameters: {
14829
+ activeSpeakerProportion: 0.5,
14830
+ showActiveSpeaker: {show: false, order: 0},
14831
+ stageManagerType: 0,
14832
+ },
14833
+ }
14834
+ );
14835
+ });
14836
+
14837
+ it('sends the expected request when only the important participants option is provided as populated', async () => {
14838
+ const importantParticipants = [
14839
+ {mainCsi: 11111111, participantId: uuidv4()},
14840
+ {mainCsi: 22222222, participantId: uuidv4()},
14841
+ {mainCsi: 33333333, participantId: uuidv4()},
14842
+ {mainCsi: 44444444, participantId: uuidv4()},
14843
+ {mainCsi: 55555555, participantId: uuidv4()},
14844
+ {mainCsi: 66666666, participantId: uuidv4()},
14845
+ {mainCsi: 77777777, participantId: uuidv4()},
14846
+ {mainCsi: 88888888, participantId: uuidv4()},
14847
+ ];
14848
+
14849
+ await check(
14850
+ {importantParticipants},
14851
+ {
14852
+ overrideDefault: true,
14853
+ lockAttendeeViewOnStageOnly: false,
14854
+ stageParameters: {
14855
+ activeSpeakerProportion: 0.5,
14856
+ importantParticipants: [
14857
+ {...importantParticipants[0], order: 1},
14858
+ {...importantParticipants[1], order: 2},
14859
+ {...importantParticipants[2], order: 3},
14860
+ {...importantParticipants[3], order: 4},
14861
+ {...importantParticipants[4], order: 5},
14862
+ {...importantParticipants[5], order: 6},
14863
+ {...importantParticipants[6], order: 7},
14864
+ {...importantParticipants[7], order: 8},
14865
+ ],
14866
+ showActiveSpeaker: {show: false, order: 0},
14867
+ stageManagerType: 0,
14868
+ },
14869
+ }
14870
+ );
14871
+ });
14872
+
14873
+ [false, true].forEach((lockAttendeeViewOnStage) => {
14874
+ it(`sends the expected request when only the lock attendee view on stage option is provided as ${lockAttendeeViewOnStage}`, async () => {
14875
+ await check(
14876
+ {lockAttendeeViewOnStage},
14877
+ {
14878
+ overrideDefault: true,
14879
+ lockAttendeeViewOnStageOnly: lockAttendeeViewOnStage,
14880
+ stageParameters: {
14881
+ activeSpeakerProportion: 0.5,
14882
+ showActiveSpeaker: {show: false, order: 0},
14883
+ stageManagerType: 0,
14884
+ },
14885
+ }
14886
+ );
14887
+ });
14888
+ });
14889
+
14890
+ [false, true].forEach((showActiveSpeaker) => {
14891
+ it(`sends the expected request when only the show active speaker option is provided as ${showActiveSpeaker}`, async () => {
14892
+ await check(
14893
+ {showActiveSpeaker},
14894
+ {
14895
+ overrideDefault: true,
14896
+ lockAttendeeViewOnStageOnly: false,
14897
+ stageParameters: {
14898
+ activeSpeakerProportion: 0.5,
14899
+ showActiveSpeaker: {show: showActiveSpeaker, order: 0},
14900
+ stageManagerType: 0,
14901
+ },
14902
+ }
14903
+ );
14904
+ });
14905
+ });
14906
+
14907
+ it('sends the expected request when all options are provided', async () => {
14908
+ const activeSpeakerProportion = 0.6;
14909
+ const customBackground = {
14910
+ url: `https://test.wbx2.com/background/${uuidv4()}.jpg`,
14911
+ };
14912
+ const customLogo = {
14913
+ url: `https://test.wbx2.com/logo/${uuidv4()}.png`,
14914
+ position: 'UpperMiddle',
14915
+ };
14916
+ const customNameLabel = {
14917
+ accentColor: '#0A7806',
14918
+ background: {color: 'rgba(255, 255, 255, 1)'},
14919
+ border: {color: 'rgba(255, 255, 255, 1)'},
14920
+ content: {
14921
+ displayName: {color: 'rgba(0, 0, 0, 0.95)'},
14922
+ subtitle: {color: 'rgba(0, 0, 0, 0.6)'},
14923
+ },
14924
+ decoration: {color: 'rgba(10, 120, 6, 1)'},
14925
+ fadeOut: {delay: 15},
14926
+ type: 'Primary',
14927
+ };
14928
+ const importantParticipants = [
14929
+ {mainCsi: 11111111, participantId: uuidv4()},
14930
+ {mainCsi: 22222222, participantId: uuidv4()},
14931
+ {mainCsi: 33333333, participantId: uuidv4()},
14932
+ {mainCsi: 44444444, participantId: uuidv4()},
14933
+ {mainCsi: 55555555, participantId: uuidv4()},
14934
+ {mainCsi: 66666666, participantId: uuidv4()},
14935
+ {mainCsi: 77777777, participantId: uuidv4()},
14936
+ {mainCsi: 88888888, participantId: uuidv4()},
14937
+ ];
14938
+ const lockAttendeeViewOnStage = true;
14939
+ const showActiveSpeaker = true;
14940
+
14941
+ await check(
14942
+ {
14943
+ activeSpeakerProportion,
14944
+ customBackground,
14945
+ customLogo,
14946
+ customNameLabel,
14947
+ importantParticipants,
14948
+ lockAttendeeViewOnStage,
14949
+ showActiveSpeaker,
14950
+ },
14951
+ {
14952
+ overrideDefault: true,
14953
+ lockAttendeeViewOnStageOnly: lockAttendeeViewOnStage,
14954
+ stageParameters: {
14955
+ activeSpeakerProportion,
14956
+ importantParticipants: [
14957
+ {...importantParticipants[0], order: 1},
14958
+ {...importantParticipants[1], order: 2},
14959
+ {...importantParticipants[2], order: 3},
14960
+ {...importantParticipants[3], order: 4},
14961
+ {...importantParticipants[4], order: 5},
14962
+ {...importantParticipants[5], order: 6},
14963
+ {...importantParticipants[6], order: 7},
14964
+ {...importantParticipants[7], order: 8},
14965
+ ],
14966
+ showActiveSpeaker: {show: showActiveSpeaker, order: 0},
14967
+ stageManagerType: 7,
14968
+ },
14969
+ customLayouts: {background: customBackground, logo: customLogo},
14970
+ nameLabelStyle: customNameLabel,
14971
+ }
14972
+ );
14973
+ });
14974
+ });
14975
+
14976
+ describe('#unsetStage', () => {
14977
+ beforeEach(() => {
14978
+ meeting.meetingRequest.synchronizeStage = sinon.stub().returns(Promise.resolve());
14979
+ });
14980
+
14981
+ it('sends the expected request', async () => {
14982
+ const locusUrl = `https://locus-test.wbx2.com/locus/api/v1/loci/${uuidv4()}`;
14983
+ meeting.locusUrl = locusUrl;
14984
+
14985
+ const unsetStagePromise = meeting.unsetStage();
14986
+
14987
+ assert.exists(unsetStagePromise.then);
14988
+ await unsetStagePromise;
14989
+
14990
+ assert.calledOnceWithExactly(
14991
+ meeting.meetingRequest.synchronizeStage,
14992
+ locusUrl,
14993
+ {overrideDefault: false}
14994
+ );
14995
+ });
14996
+ });
14997
+
14998
+ describe('#notifyHost', () => {
14999
+ beforeEach(() => {
15000
+ meeting.meetingRequest.notifyHost = sinon.stub().returns(Promise.resolve());
15001
+ });
15002
+
15003
+ it('sends the expected request', async () => {
15004
+ meeting.meetingInfo.siteFullUrl = `convergedats.webex.com`;
15005
+ const meetingUuid = 'meeting-uuid';
15006
+ const displayName = ['Test', 'User'];
15007
+ meeting.locusId = 'locusId';
15008
+
15009
+ const notifyHostPromise = meeting.notifyHost(meetingUuid, displayName);
15010
+
15011
+ assert.exists(notifyHostPromise.then);
15012
+ await notifyHostPromise;
15013
+
15014
+ assert.calledOnceWithExactly(
15015
+ meeting.meetingRequest.notifyHost,
15016
+ meeting.meetingInfo.siteFullUrl,
15017
+ meeting.locusId,
15018
+ meetingUuid,
15019
+ displayName,
15020
+ );
15021
+ });
15022
+ });
13926
15023
  });