@webex/plugin-meetings 3.7.0 → 3.8.0

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 (351) hide show
  1. package/dist/annotation/annotation.types.d.ts +42 -0
  2. package/dist/annotation/constants.d.ts +31 -0
  3. package/dist/annotation/index.d.ts +117 -0
  4. package/dist/annotation/index.js +17 -0
  5. package/dist/annotation/index.js.map +1 -1
  6. package/dist/breakouts/breakout.d.ts +8 -0
  7. package/dist/breakouts/breakout.js +1 -1
  8. package/dist/breakouts/collection.d.ts +5 -0
  9. package/dist/breakouts/edit-lock-error.d.ts +15 -0
  10. package/dist/breakouts/events.d.ts +8 -0
  11. package/dist/breakouts/index.d.ts +5 -0
  12. package/dist/breakouts/index.js +1 -1
  13. package/dist/breakouts/request.d.ts +22 -0
  14. package/dist/breakouts/utils.d.ts +15 -0
  15. package/dist/common/browser-detection.d.ts +9 -0
  16. package/dist/common/collection.d.ts +48 -0
  17. package/dist/common/config.d.ts +2 -0
  18. package/dist/common/errors/captcha-error.d.ts +15 -0
  19. package/dist/common/errors/intent-to-join.d.ts +16 -0
  20. package/dist/common/errors/join-forbidden-error.js +52 -0
  21. package/dist/common/errors/join-forbidden-error.js.map +1 -0
  22. package/dist/common/errors/join-meeting.d.ts +17 -0
  23. package/dist/common/errors/{webinar-registration-error.js → join-webinar-error.js} +12 -12
  24. package/dist/common/errors/join-webinar-error.js.map +1 -0
  25. package/dist/common/errors/media.d.ts +15 -0
  26. package/dist/common/errors/multistream-not-supported-error.js +53 -0
  27. package/dist/common/errors/multistream-not-supported-error.js.map +1 -0
  28. package/dist/common/errors/no-meeting-info.d.ts +14 -0
  29. package/dist/common/errors/parameter.d.ts +15 -0
  30. package/dist/common/errors/password-error.d.ts +15 -0
  31. package/dist/common/errors/permission.d.ts +14 -0
  32. package/dist/common/errors/reclaim-host-role-error.js +149 -0
  33. package/dist/common/errors/reclaim-host-role-error.js.map +1 -0
  34. package/dist/common/errors/reclaim-host-role-errors.d.ts +60 -0
  35. package/dist/common/errors/reconnection-in-progress.d.ts +9 -0
  36. package/dist/common/errors/reconnection-in-progress.js +33 -0
  37. package/dist/common/errors/reconnection-in-progress.js.map +1 -0
  38. package/dist/common/errors/reconnection.d.ts +15 -0
  39. package/dist/common/errors/stats.d.ts +15 -0
  40. package/dist/common/errors/webex-errors.d.ts +93 -0
  41. package/dist/common/errors/webex-meetings-error.d.ts +20 -0
  42. package/dist/common/events/events-scope.d.ts +17 -0
  43. package/dist/common/events/events.d.ts +12 -0
  44. package/dist/common/events/trigger-proxy.d.ts +2 -0
  45. package/dist/common/events/util.d.ts +2 -0
  46. package/dist/common/logs/logger-config.d.ts +2 -0
  47. package/dist/common/logs/logger-proxy.d.ts +2 -0
  48. package/dist/common/logs/request.d.ts +36 -0
  49. package/dist/common/queue.d.ts +34 -0
  50. package/dist/config.d.ts +72 -0
  51. package/dist/config.js +2 -1
  52. package/dist/config.js.map +1 -1
  53. package/dist/constants.d.ts +1088 -0
  54. package/dist/constants.js +68 -6
  55. package/dist/constants.js.map +1 -1
  56. package/dist/controls-options-manager/constants.d.ts +4 -0
  57. package/dist/controls-options-manager/enums.d.ts +15 -0
  58. package/dist/controls-options-manager/index.d.ts +136 -0
  59. package/dist/controls-options-manager/types.d.ts +43 -0
  60. package/dist/controls-options-manager/util.d.ts +1 -0
  61. package/dist/index.d.ts +7 -0
  62. package/dist/index.js +16 -11
  63. package/dist/index.js.map +1 -1
  64. package/dist/interceptors/index.d.ts +2 -0
  65. package/dist/interceptors/locusRetry.d.ts +27 -0
  66. package/dist/interpretation/collection.d.ts +5 -0
  67. package/dist/interpretation/index.d.ts +5 -0
  68. package/dist/interpretation/index.js +1 -1
  69. package/dist/interpretation/siLanguage.d.ts +5 -0
  70. package/dist/interpretation/siLanguage.js +1 -1
  71. package/dist/locus-info/controlsUtils.d.ts +2 -0
  72. package/dist/locus-info/embeddedAppsUtils.d.ts +2 -0
  73. package/dist/locus-info/fullState.d.ts +2 -0
  74. package/dist/locus-info/hostUtils.d.ts +2 -0
  75. package/dist/locus-info/index.d.ts +322 -0
  76. package/dist/locus-info/index.js +14 -3
  77. package/dist/locus-info/index.js.map +1 -1
  78. package/dist/locus-info/infoUtils.d.ts +2 -0
  79. package/dist/locus-info/mediaSharesUtils.d.ts +2 -0
  80. package/dist/locus-info/parser.d.ts +272 -0
  81. package/dist/locus-info/selfUtils.d.ts +2 -0
  82. package/dist/locus-info/selfUtils.js +35 -17
  83. package/dist/locus-info/selfUtils.js.map +1 -1
  84. package/dist/media/MediaConnectionAwaiter.js +1 -0
  85. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  86. package/dist/media/index.d.ts +34 -0
  87. package/dist/media/properties.d.ts +93 -0
  88. package/dist/media/properties.js +30 -16
  89. package/dist/media/properties.js.map +1 -1
  90. package/dist/media/util.d.ts +2 -0
  91. package/dist/mediaQualityMetrics/config.d.ts +241 -0
  92. package/dist/mediaQualityMetrics/config.js +502 -0
  93. package/dist/mediaQualityMetrics/config.js.map +1 -0
  94. package/dist/meeting/brbState.js +167 -0
  95. package/dist/meeting/brbState.js.map +1 -0
  96. package/dist/meeting/effectsState.js +260 -0
  97. package/dist/meeting/effectsState.js.map +1 -0
  98. package/dist/meeting/in-meeting-actions.d.ts +167 -0
  99. package/dist/meeting/in-meeting-actions.js +13 -1
  100. package/dist/meeting/in-meeting-actions.js.map +1 -1
  101. package/dist/meeting/index.d.ts +1825 -0
  102. package/dist/meeting/index.js +1331 -1051
  103. package/dist/meeting/index.js.map +1 -1
  104. package/dist/meeting/locusMediaRequest.d.ts +74 -0
  105. package/dist/meeting/locusMediaRequest.js +11 -6
  106. package/dist/meeting/locusMediaRequest.js.map +1 -1
  107. package/dist/meeting/muteState.d.ts +178 -0
  108. package/dist/meeting/muteState.js +1 -6
  109. package/dist/meeting/muteState.js.map +1 -1
  110. package/dist/meeting/request.d.ts +295 -0
  111. package/dist/meeting/request.js +51 -29
  112. package/dist/meeting/request.js.map +1 -1
  113. package/dist/meeting/request.type.d.ts +11 -0
  114. package/dist/meeting/request.type.js.map +1 -1
  115. package/dist/meeting/state.d.ts +9 -0
  116. package/dist/meeting/util.d.ts +119 -0
  117. package/dist/meeting/util.js +103 -67
  118. package/dist/meeting/util.js.map +1 -1
  119. package/dist/meeting/voicea-meeting.d.ts +16 -0
  120. package/dist/meeting-info/collection.d.ts +20 -0
  121. package/dist/meeting-info/index.d.ts +69 -0
  122. package/dist/meeting-info/meeting-info-v2.d.ts +123 -0
  123. package/dist/meeting-info/meeting-info-v2.js +115 -45
  124. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  125. package/dist/meeting-info/request.d.ts +22 -0
  126. package/dist/meeting-info/util.d.ts +2 -0
  127. package/dist/meeting-info/utilv2.d.ts +2 -0
  128. package/dist/meeting-info/utilv2.js +6 -2
  129. package/dist/meeting-info/utilv2.js.map +1 -1
  130. package/dist/meetings/collection.d.ts +40 -0
  131. package/dist/meetings/index.d.ts +390 -0
  132. package/dist/meetings/index.js +107 -55
  133. package/dist/meetings/index.js.map +1 -1
  134. package/dist/meetings/meetings.types.d.ts +4 -0
  135. package/dist/meetings/meetings.types.js +2 -0
  136. package/dist/meetings/meetings.types.js.map +1 -1
  137. package/dist/meetings/request.d.ts +27 -0
  138. package/dist/meetings/util.d.ts +18 -0
  139. package/dist/meetings/util.js +1 -1
  140. package/dist/meetings/util.js.map +1 -1
  141. package/dist/member/index.d.ts +160 -0
  142. package/dist/member/index.js +9 -0
  143. package/dist/member/index.js.map +1 -1
  144. package/dist/member/member.types.js +17 -0
  145. package/dist/member/member.types.js.map +1 -0
  146. package/dist/member/types.d.ts +32 -0
  147. package/dist/member/types.js.map +1 -1
  148. package/dist/member/util.d.ts +2 -0
  149. package/dist/member/util.js +39 -28
  150. package/dist/member/util.js.map +1 -1
  151. package/dist/members/collection.d.ts +29 -0
  152. package/dist/members/index.d.ts +353 -0
  153. package/dist/members/request.d.ts +114 -0
  154. package/dist/members/types.d.ts +25 -0
  155. package/dist/members/util.d.ts +215 -0
  156. package/dist/members/util.js +4 -2
  157. package/dist/members/util.js.map +1 -1
  158. package/dist/metrics/config.js +276 -0
  159. package/dist/metrics/config.js.map +1 -0
  160. package/dist/metrics/constants.d.ts +70 -0
  161. package/dist/metrics/constants.js +6 -1
  162. package/dist/metrics/constants.js.map +1 -1
  163. package/dist/metrics/index.d.ts +45 -0
  164. package/dist/multistream/mediaRequestManager.d.ts +119 -0
  165. package/dist/multistream/receiveSlot.d.ts +68 -0
  166. package/dist/multistream/receiveSlotManager.d.ts +56 -0
  167. package/dist/multistream/remoteMedia.d.ts +72 -0
  168. package/dist/multistream/remoteMedia.js +30 -15
  169. package/dist/multistream/remoteMedia.js.map +1 -1
  170. package/dist/multistream/remoteMediaGroup.d.ts +49 -0
  171. package/dist/multistream/remoteMediaManager.d.ts +300 -0
  172. package/dist/multistream/sendSlotManager.d.ts +69 -0
  173. package/dist/multistream/sendSlotManager.js +24 -0
  174. package/dist/multistream/sendSlotManager.js.map +1 -1
  175. package/dist/networkQualityMonitor/index.d.ts +70 -0
  176. package/dist/networkQualityMonitor/index.js +13 -19
  177. package/dist/networkQualityMonitor/index.js.map +1 -1
  178. package/dist/peer-connection-manager/index.js +671 -0
  179. package/dist/peer-connection-manager/index.js.map +1 -0
  180. package/dist/peer-connection-manager/util.js +109 -0
  181. package/dist/peer-connection-manager/util.js.map +1 -0
  182. package/dist/personal-meeting-room/index.d.ts +47 -0
  183. package/dist/personal-meeting-room/request.d.ts +14 -0
  184. package/dist/personal-meeting-room/util.d.ts +2 -0
  185. package/dist/reachability/clusterReachability.d.ts +109 -0
  186. package/dist/reachability/clusterReachability.js +12 -15
  187. package/dist/reachability/clusterReachability.js.map +1 -1
  188. package/dist/reachability/index.d.ts +105 -0
  189. package/dist/reachability/index.js +461 -136
  190. package/dist/reachability/index.js.map +1 -1
  191. package/dist/reachability/reachability.types.js +7 -0
  192. package/dist/reachability/reachability.types.js.map +1 -0
  193. package/dist/reachability/request.d.ts +39 -0
  194. package/dist/reachability/request.js +21 -8
  195. package/dist/reachability/request.js.map +1 -1
  196. package/dist/reachability/util.d.ts +8 -0
  197. package/dist/reactions/constants.d.ts +3 -0
  198. package/dist/reactions/reactions.d.ts +4 -0
  199. package/dist/reactions/reactions.type.d.ts +52 -0
  200. package/dist/reconnection-manager/index.d.ts +136 -0
  201. package/dist/recording-controller/enums.d.ts +7 -0
  202. package/dist/recording-controller/enums.js +8 -4
  203. package/dist/recording-controller/enums.js.map +1 -1
  204. package/dist/recording-controller/index.d.ts +207 -0
  205. package/dist/recording-controller/index.js +18 -9
  206. package/dist/recording-controller/index.js.map +1 -1
  207. package/dist/recording-controller/util.d.ts +14 -0
  208. package/dist/recording-controller/util.js +13 -9
  209. package/dist/recording-controller/util.js.map +1 -1
  210. package/dist/roap/collection.js +62 -0
  211. package/dist/roap/collection.js.map +1 -0
  212. package/dist/roap/handler.js +275 -0
  213. package/dist/roap/handler.js.map +1 -0
  214. package/dist/roap/index.d.ts +86 -0
  215. package/dist/roap/index.js +15 -15
  216. package/dist/roap/index.js.map +1 -1
  217. package/dist/roap/request.d.ts +39 -0
  218. package/dist/roap/request.js +45 -79
  219. package/dist/roap/request.js.map +1 -1
  220. package/dist/roap/state.js +126 -0
  221. package/dist/roap/state.js.map +1 -0
  222. package/dist/roap/turnDiscovery.d.ts +155 -0
  223. package/dist/roap/turnDiscovery.js +3 -6
  224. package/dist/roap/turnDiscovery.js.map +1 -1
  225. package/dist/roap/util.js +75 -0
  226. package/dist/roap/util.js.map +1 -0
  227. package/dist/rtcMetrics/constants.d.ts +4 -0
  228. package/dist/rtcMetrics/index.d.ts +61 -0
  229. package/dist/statsAnalyzer/global.d.ts +36 -0
  230. package/dist/statsAnalyzer/global.js +126 -0
  231. package/dist/statsAnalyzer/global.js.map +1 -0
  232. package/dist/statsAnalyzer/index.d.ts +217 -0
  233. package/dist/statsAnalyzer/index.js +1013 -0
  234. package/dist/statsAnalyzer/index.js.map +1 -0
  235. package/dist/statsAnalyzer/mqaUtil.d.ts +48 -0
  236. package/dist/statsAnalyzer/mqaUtil.js +179 -0
  237. package/dist/statsAnalyzer/mqaUtil.js.map +1 -0
  238. package/dist/transcription/index.d.ts +64 -0
  239. package/dist/types/annotation/index.d.ts +5 -0
  240. package/dist/types/common/errors/join-forbidden-error.d.ts +15 -0
  241. package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
  242. package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
  243. package/dist/types/common/errors/reconnection-in-progress.d.ts +9 -0
  244. package/dist/types/config.d.ts +1 -0
  245. package/dist/types/constants.d.ts +53 -1
  246. package/dist/types/index.d.ts +3 -3
  247. package/dist/types/locus-info/index.d.ts +2 -1
  248. package/dist/types/mediaQualityMetrics/config.d.ts +241 -0
  249. package/dist/types/meeting/brbState.d.ts +54 -0
  250. package/dist/types/meeting/in-meeting-actions.d.ts +12 -0
  251. package/dist/types/meeting/index.d.ts +64 -14
  252. package/dist/types/meeting/locusMediaRequest.d.ts +6 -3
  253. package/dist/types/meeting/request.d.ts +14 -3
  254. package/dist/types/meeting/request.type.d.ts +6 -0
  255. package/dist/types/meeting/util.d.ts +3 -3
  256. package/dist/types/meeting-info/meeting-info-v2.d.ts +30 -5
  257. package/dist/types/meetings/index.d.ts +20 -2
  258. package/dist/types/meetings/meetings.types.d.ts +8 -0
  259. package/dist/types/member/index.d.ts +1 -0
  260. package/dist/types/member/types.d.ts +7 -0
  261. package/dist/types/members/util.d.ts +2 -0
  262. package/dist/types/metrics/constants.d.ts +6 -1
  263. package/dist/types/multistream/sendSlotManager.d.ts +8 -1
  264. package/dist/types/reachability/clusterReachability.d.ts +1 -10
  265. package/dist/types/reachability/index.d.ts +83 -36
  266. package/dist/types/reachability/reachability.types.d.ts +64 -0
  267. package/dist/types/reachability/request.d.ts +5 -1
  268. package/dist/types/recording-controller/enums.d.ts +5 -2
  269. package/dist/types/recording-controller/index.d.ts +1 -0
  270. package/dist/types/recording-controller/util.d.ts +2 -1
  271. package/dist/types/roap/request.d.ts +1 -13
  272. package/dist/types/statsAnalyzer/global.d.ts +36 -0
  273. package/dist/types/statsAnalyzer/index.d.ts +217 -0
  274. package/dist/types/statsAnalyzer/mqaUtil.d.ts +48 -0
  275. package/dist/webinar/collection.d.ts +16 -0
  276. package/dist/webinar/index.d.ts +5 -0
  277. package/dist/webinar/index.js +390 -7
  278. package/dist/webinar/index.js.map +1 -1
  279. package/package.json +23 -22
  280. package/src/annotation/index.ts +16 -0
  281. package/src/common/errors/join-forbidden-error.ts +26 -0
  282. package/src/common/errors/join-webinar-error.ts +24 -0
  283. package/src/common/errors/multistream-not-supported-error.ts +30 -0
  284. package/src/config.ts +1 -0
  285. package/src/constants.ts +61 -3
  286. package/src/index.ts +5 -3
  287. package/src/locus-info/index.ts +20 -3
  288. package/src/locus-info/selfUtils.ts +24 -6
  289. package/src/media/MediaConnectionAwaiter.ts +2 -0
  290. package/src/media/properties.ts +34 -13
  291. package/src/meeting/brbState.ts +169 -0
  292. package/src/meeting/in-meeting-actions.ts +25 -0
  293. package/src/meeting/index.ts +443 -87
  294. package/src/meeting/locusMediaRequest.ts +11 -8
  295. package/src/meeting/muteState.ts +1 -6
  296. package/src/meeting/request.ts +30 -12
  297. package/src/meeting/request.type.ts +7 -0
  298. package/src/meeting/util.ts +32 -13
  299. package/src/meeting-info/meeting-info-v2.ts +83 -12
  300. package/src/meeting-info/utilv2.ts +17 -3
  301. package/src/meetings/index.ts +79 -20
  302. package/src/meetings/meetings.types.ts +10 -0
  303. package/src/meetings/util.ts +2 -1
  304. package/src/member/index.ts +9 -0
  305. package/src/member/types.ts +8 -0
  306. package/src/member/util.ts +34 -24
  307. package/src/members/util.ts +1 -0
  308. package/src/metrics/constants.ts +6 -1
  309. package/src/multistream/remoteMedia.ts +28 -15
  310. package/src/multistream/sendSlotManager.ts +31 -0
  311. package/src/reachability/clusterReachability.ts +5 -15
  312. package/src/reachability/index.ts +311 -75
  313. package/src/reachability/reachability.types.ts +85 -0
  314. package/src/reachability/request.ts +55 -31
  315. package/src/recording-controller/enums.ts +5 -2
  316. package/src/recording-controller/index.ts +17 -4
  317. package/src/recording-controller/util.ts +20 -5
  318. package/src/roap/index.ts +14 -13
  319. package/src/roap/request.ts +30 -44
  320. package/src/roap/turnDiscovery.ts +2 -4
  321. package/src/webinar/index.ts +235 -9
  322. package/test/unit/spec/annotation/index.ts +46 -1
  323. package/test/unit/spec/locus-info/index.js +292 -60
  324. package/test/unit/spec/locus-info/selfConstant.js +7 -0
  325. package/test/unit/spec/locus-info/selfUtils.js +101 -1
  326. package/test/unit/spec/media/properties.ts +15 -0
  327. package/test/unit/spec/meeting/brbState.ts +114 -0
  328. package/test/unit/spec/meeting/in-meeting-actions.ts +15 -1
  329. package/test/unit/spec/meeting/index.js +851 -107
  330. package/test/unit/spec/meeting/locusMediaRequest.ts +18 -11
  331. package/test/unit/spec/meeting/muteState.js +0 -24
  332. package/test/unit/spec/meeting/request.js +3 -26
  333. package/test/unit/spec/meeting/utils.js +73 -28
  334. package/test/unit/spec/meeting-info/meetinginfov2.js +46 -4
  335. package/test/unit/spec/meeting-info/utilv2.js +26 -0
  336. package/test/unit/spec/meetings/index.js +159 -18
  337. package/test/unit/spec/meetings/utils.js +10 -0
  338. package/test/unit/spec/member/util.js +52 -11
  339. package/test/unit/spec/members/utils.js +95 -0
  340. package/test/unit/spec/multistream/remoteMedia.ts +11 -7
  341. package/test/unit/spec/reachability/clusterReachability.ts +7 -0
  342. package/test/unit/spec/reachability/index.ts +383 -9
  343. package/test/unit/spec/reachability/request.js +48 -12
  344. package/test/unit/spec/recording-controller/index.js +61 -5
  345. package/test/unit/spec/recording-controller/util.js +39 -3
  346. package/test/unit/spec/roap/index.ts +48 -1
  347. package/test/unit/spec/roap/request.ts +51 -109
  348. package/test/unit/spec/roap/turnDiscovery.ts +202 -147
  349. package/test/unit/spec/webinar/index.ts +504 -0
  350. package/dist/common/errors/webinar-registration-error.js.map +0 -1
  351. package/src/common/errors/webinar-registration-error.ts +0 -27
@@ -91,14 +91,15 @@ import ParameterError from '../../../../src/common/errors/parameter';
91
91
  import PasswordError from '../../../../src/common/errors/password-error';
92
92
  import CaptchaError from '../../../../src/common/errors/captcha-error';
93
93
  import PermissionError from '../../../../src/common/errors/permission';
94
- import WebinarRegistrationError from '../../../../src/common/errors/webinar-registration-error';
94
+ import JoinWebinarError from '../../../../src/common/errors/join-webinar-error';
95
95
  import IntentToJoinError from '../../../../src/common/errors/intent-to-join';
96
+ import MultistreamNotSupportedError from '../../../../src/common/errors/multistream-not-supported-error';;
96
97
  import testUtils from '../../../utils/testUtils';
97
98
  import {
98
99
  MeetingInfoV2CaptchaError,
99
100
  MeetingInfoV2PasswordError,
100
101
  MeetingInfoV2PolicyError,
101
- MeetingInfoV2WebinarRegistrationError,
102
+ MeetingInfoV2JoinWebinarError, MeetingInfoV2JoinForbiddenError,
102
103
  } from '../../../../src/meeting-info/meeting-info-v2';
103
104
  import {
104
105
  DTLS_HANDSHAKE_FAILED_CLIENT_CODE,
@@ -113,6 +114,8 @@ import {ERROR_DESCRIPTIONS} from '@webex/internal-plugin-metrics/src/call-diagno
113
114
  import MeetingCollection from '@webex/plugin-meetings/src/meetings/collection';
114
115
 
115
116
  import {EVENT_TRIGGERS as VOICEAEVENTS} from '@webex/internal-plugin-voicea';
117
+ import { createBrbState } from '@webex/plugin-meetings/src/meeting/brbState';
118
+ import JoinForbiddenError from '../../../../src/common/errors/join-forbidden-error';
116
119
 
117
120
  describe('plugin-meetings', () => {
118
121
  const logger = {
@@ -244,6 +247,7 @@ describe('plugin-meetings', () => {
244
247
  isAnyPublicClusterReachable: sinon.stub().resolves(true),
245
248
  getReachabilityResults: sinon.stub().resolves(undefined),
246
249
  getReachabilityMetrics: sinon.stub().resolves({}),
250
+ stopReachability: sinon.stub(),
247
251
  };
248
252
  webex.internal.llm.on = sinon.stub();
249
253
  webex.internal.newMetrics.callDiagnosticLatencies = new CallDiagnosticLatencies(
@@ -649,11 +653,10 @@ describe('plugin-meetings', () => {
649
653
  });
650
654
 
651
655
  const fakeRoapMessage = {id: 'fake TURN discovery message'};
652
- const fakeReachabilityResults = {id: 'fake reachability'};
653
656
  const fakeTurnServerInfo = {id: 'fake turn info'};
654
657
  const fakeJoinResult = {id: 'join result'};
655
658
 
656
- const joinOptions = {correlationId: '12345'};
659
+ const joinOptions = {correlationId: '12345', enableMultistream: true};
657
660
  const mediaOptions = {audioEnabled: true, allowMediaInLobby: true};
658
661
 
659
662
  let generateTurnDiscoveryRequestMessageStub;
@@ -662,13 +665,14 @@ describe('plugin-meetings', () => {
662
665
  let addMediaInternalStub;
663
666
 
664
667
  beforeEach(() => {
665
- meeting.join = sinon.stub().returns(Promise.resolve(fakeJoinResult));
668
+ meeting.join = sinon.stub().callsFake((joinOptions) => {
669
+ meeting.isMultistream = joinOptions.enableMultistream;
670
+ return Promise.resolve(fakeJoinResult)
671
+ });
666
672
  addMediaInternalStub = sinon
667
673
  .stub(meeting, 'addMediaInternal')
668
674
  .returns(Promise.resolve(test4));
669
675
 
670
- webex.meetings.reachability.getReachabilityResults.resolves(fakeReachabilityResults);
671
-
672
676
  generateTurnDiscoveryRequestMessageStub = sinon
673
677
  .stub(meeting.roap, 'generateTurnDiscoveryRequestMessage')
674
678
  .resolves({roapMessage: fakeRoapMessage});
@@ -688,7 +692,6 @@ describe('plugin-meetings', () => {
688
692
  assert.calledOnceWithExactly(meeting.join, {
689
693
  ...joinOptions,
690
694
  roapMessage: fakeRoapMessage,
691
- reachability: fakeReachabilityResults,
692
695
  });
693
696
  assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true);
694
697
  assert.calledOnceWithExactly(
@@ -704,7 +707,7 @@ describe('plugin-meetings', () => {
704
707
  mediaOptions
705
708
  );
706
709
 
707
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
710
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
708
711
 
709
712
  // resets joinWithMediaRetryInfo
710
713
  assert.deepEqual(meeting.joinWithMediaRetryInfo, {
@@ -725,7 +728,6 @@ describe('plugin-meetings', () => {
725
728
  assert.calledOnceWithExactly(meeting.join, {
726
729
  ...joinOptions,
727
730
  roapMessage: undefined,
728
- reachability: fakeReachabilityResults,
729
731
  });
730
732
  assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true);
731
733
  assert.notCalled(handleTurnDiscoveryHttpResponseStub);
@@ -738,7 +740,7 @@ describe('plugin-meetings', () => {
738
740
  mediaOptions
739
741
  );
740
742
 
741
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
743
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
742
744
  assert.equal(meeting.turnServerUsed, false);
743
745
  });
744
746
 
@@ -757,7 +759,6 @@ describe('plugin-meetings', () => {
757
759
  assert.calledOnceWithExactly(meeting.join, {
758
760
  ...joinOptions,
759
761
  roapMessage: fakeRoapMessage,
760
- reachability: fakeReachabilityResults,
761
762
  });
762
763
  assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true);
763
764
  assert.calledOnceWithExactly(
@@ -774,7 +775,7 @@ describe('plugin-meetings', () => {
774
775
  mediaOptions
775
776
  );
776
777
 
777
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
778
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
778
779
  });
779
780
 
780
781
  it('should reject if join() fails', async () => {
@@ -861,7 +862,8 @@ describe('plugin-meetings', () => {
861
862
  }
862
863
  );
863
864
 
864
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
865
+ // expect multistreamEnabled: false, because this test overrides the join meeting.join stub so it doesn't set the isMultistream flag
866
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: false});
865
867
 
866
868
  // resets joinWithMediaRetryInfo
867
869
  assert.deepEqual(meeting.joinWithMediaRetryInfo, {
@@ -950,7 +952,7 @@ describe('plugin-meetings', () => {
950
952
  mediaOptions,
951
953
  });
952
954
 
953
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
955
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
954
956
 
955
957
  assert.calledOnce(meeting.join);
956
958
  assert.notCalled(leaveStub);
@@ -1044,6 +1046,7 @@ describe('plugin-meetings', () => {
1044
1046
  getConnectionState: sinon.stub().returns(ConnectionState.Connected),
1045
1047
  initiateOffer: sinon.stub().resolves({}),
1046
1048
  on: sinon.stub(),
1049
+ createSendSlot: sinon.stub(),
1047
1050
  };
1048
1051
 
1049
1052
  /* Setup the stubs so that the first call to addMediaInternal() fails
@@ -1060,12 +1063,14 @@ describe('plugin-meetings', () => {
1060
1063
 
1061
1064
  sinon.stub(meeting.roap, 'doTurnDiscovery').resolves({turnServerInfo: 'fake turn info'});
1062
1065
 
1066
+ // calling joinWithMedia() with enableMultistream=false, because this test uses real addMediaInternal() implementation
1067
+ // and it requires less stubs when it's without multistream
1063
1068
  const result = await meeting.joinWithMedia({
1064
- joinOptions,
1069
+ joinOptions: {...joinOptions, enableMultistream: false},
1065
1070
  mediaOptions,
1066
1071
  });
1067
1072
 
1068
- assert.deepEqual(result, {join: fakeJoinResult, media: undefined});
1073
+ assert.deepEqual(result, {join: fakeJoinResult, media: undefined, multistreamEnabled: false});
1069
1074
 
1070
1075
  assert.calledOnce(meeting.join);
1071
1076
 
@@ -1140,6 +1145,7 @@ describe('plugin-meetings', () => {
1140
1145
  addMediaError.name = 'SdpOfferCreationError';
1141
1146
 
1142
1147
  meeting.addMediaInternal.rejects(addMediaError);
1148
+ sinon.stub(meeting, 'leave').resolves();
1143
1149
 
1144
1150
  await assert.isRejected(
1145
1151
  meeting.joinWithMedia({
@@ -1168,6 +1174,7 @@ describe('plugin-meetings', () => {
1168
1174
  type: addMediaError.name,
1169
1175
  }
1170
1176
  );
1177
+ assert.calledOnceWithExactly(meeting.leave, {resourceId: undefined, reason: 'joinWithMedia failure'})
1171
1178
  });
1172
1179
  });
1173
1180
 
@@ -1244,6 +1251,7 @@ describe('plugin-meetings', () => {
1244
1251
  webex.internal.voicea.off = sinon.stub();
1245
1252
  webex.internal.voicea.listenToEvents = sinon.stub();
1246
1253
  webex.internal.voicea.turnOnCaptions = sinon.stub();
1254
+ webex.internal.voicea.deregisterEvents = sinon.stub();
1247
1255
  });
1248
1256
 
1249
1257
  it('should stop listening to voicea events and also trigger a stop event', () => {
@@ -1572,6 +1580,55 @@ describe('plugin-meetings', () => {
1572
1580
  fakeProcessedReaction
1573
1581
  );
1574
1582
  });
1583
+
1584
+ it('should fail quietly if participantId does not exist in membersCollection', () => {
1585
+ LoggerProxy.logger.warn = sinon.stub();
1586
+ meeting.isReactionsSupported = sinon.stub().returns(true);
1587
+ meeting.config.receiveReactions = true;
1588
+ const fakeSendersName = 'Fake reactors name';
1589
+ const fakeReactionPayload = {
1590
+ type: 'fake_type',
1591
+ codepoints: 'fake_codepoints',
1592
+ shortcodes: 'fake_shortcodes',
1593
+ tone: {
1594
+ type: 'fake_tone_type',
1595
+ codepoints: 'fake_tone_codepoints',
1596
+ shortcodes: 'fake_tone_shortcodes',
1597
+ },
1598
+ };
1599
+ const fakeSenderPayload = {
1600
+ participantId: 'fake_participant_id',
1601
+ };
1602
+ const fakeProcessedReaction = {
1603
+ reaction: fakeReactionPayload,
1604
+ sender: {
1605
+ id: fakeSenderPayload.participantId,
1606
+ name: fakeSendersName,
1607
+ },
1608
+ };
1609
+ const fakeRelayEvent = {
1610
+ data: {
1611
+ relayType: REACTION_RELAY_TYPES.REACTION,
1612
+ reaction: fakeReactionPayload,
1613
+ sender: fakeSenderPayload,
1614
+ },
1615
+ };
1616
+ meeting.processRelayEvent(fakeRelayEvent);
1617
+ assert.calledWith(
1618
+ LoggerProxy.logger.warn,
1619
+ `Meeting:index#processRelayEvent --> Skipping handling of react for ${meeting.id}. participantId fake_participant_id does not exist in membersCollection.`
1620
+ );
1621
+ assert.neverCalledWith(
1622
+ TriggerProxy.trigger,
1623
+ sinon.match.instanceOf(Meeting),
1624
+ {
1625
+ file: 'meeting/index',
1626
+ function: 'join',
1627
+ },
1628
+ EVENT_TRIGGERS.MEETING_RECEIVE_REACTIONS,
1629
+ fakeProcessedReaction
1630
+ );
1631
+ });
1575
1632
  });
1576
1633
 
1577
1634
  describe('#handleLLMOnline', () => {
@@ -1711,6 +1768,12 @@ describe('plugin-meetings', () => {
1711
1768
  sinon.assert.called(setCorrelationIdSpy);
1712
1769
  assert.equal(meeting.correlationId, '123');
1713
1770
  });
1771
+
1772
+ it('should not send client.call.initiated if told not to', async () => {
1773
+ await meeting.join({sendCallInitiated: false});
1774
+
1775
+ sinon.assert.notCalled(webex.internal.newMetrics.submitClientEvent);
1776
+ });
1714
1777
  });
1715
1778
 
1716
1779
  describe('failure', () => {
@@ -2034,6 +2097,7 @@ describe('plugin-meetings', () => {
2034
2097
  someReachabilityMetric1: 'some value1',
2035
2098
  someReachabilityMetric2: 'some value2',
2036
2099
  }),
2100
+ stopReachability: sinon.stub(),
2037
2101
  };
2038
2102
 
2039
2103
  const forceRtcMetricsSend = sinon.stub().resolves();
@@ -2453,6 +2517,7 @@ describe('plugin-meetings', () => {
2453
2517
  assert.calledOnce(meeting.setMercuryListener);
2454
2518
  assert.calledOnce(fakeMediaConnection.initiateOffer);
2455
2519
  assert.equal(meeting.allowMediaInLobby, allowMediaInLobby);
2520
+ assert.calledOnce(webex.meetings.reachability.stopReachability);
2456
2521
  };
2457
2522
 
2458
2523
  it('should attach the media and return promise', async () => {
@@ -2471,6 +2536,61 @@ describe('plugin-meetings', () => {
2471
2536
  checkWorking();
2472
2537
  });
2473
2538
 
2539
+ it('should upload logs periodically', async () => {
2540
+ const clock = sinon.useFakeTimers();
2541
+
2542
+ meeting.roap.doTurnDiscovery = sinon
2543
+ .stub()
2544
+ .resolves({turnServerInfo: undefined, turnDiscoverySkippedReason: undefined});
2545
+
2546
+ let logUploadCounter = 0;
2547
+
2548
+ TriggerProxy.trigger.callsFake((meetingObject, options, event) => {
2549
+ if (
2550
+ meetingObject === meeting &&
2551
+ options.file === 'meeting/index' &&
2552
+ options.function === 'uploadLogs' &&
2553
+ event === 'REQUEST_UPLOAD_LOGS'
2554
+ ) {
2555
+ logUploadCounter += 1;
2556
+ }
2557
+ });
2558
+
2559
+ meeting.config.logUploadIntervalMultiplicationFactor = 1;
2560
+ meeting.meetingState = 'ACTIVE';
2561
+
2562
+ await meeting.addMedia({
2563
+ mediaSettings: {},
2564
+ });
2565
+
2566
+ const checkLogCounter = (delayInMinutes, expectedCounter) => {
2567
+ const delayInMilliseconds = delayInMinutes * 60 * 1000;
2568
+
2569
+ // first check that the counter is not increased just before the delay
2570
+ clock.tick(delayInMilliseconds - 50);
2571
+ assert.equal(logUploadCounter, expectedCounter - 1);
2572
+
2573
+ // and now check that it has reached expected value after the delay
2574
+ clock.tick(50);
2575
+ assert.equal(logUploadCounter, expectedCounter);
2576
+ };
2577
+
2578
+ checkLogCounter(0.1, 1);
2579
+ checkLogCounter(15, 2);
2580
+ checkLogCounter(30, 3);
2581
+ checkLogCounter(60, 4);
2582
+ checkLogCounter(60, 5);
2583
+
2584
+ // simulate media connection being removed -> 1 more upload should happen, but nothing more afterwards
2585
+ meeting.mediaProperties.webrtcMediaConnection = undefined;
2586
+ checkLogCounter(60, 6);
2587
+
2588
+ clock.tick(120 * 1000 * 60);
2589
+ assert.equal(logUploadCounter, 6);
2590
+
2591
+ clock.restore();
2592
+ });
2593
+
2474
2594
  it('should attach the media and return promise when in the lobby if allowMediaInLobby is set', async () => {
2475
2595
  meeting.roap.doTurnDiscovery = sinon
2476
2596
  .stub()
@@ -2593,6 +2713,7 @@ describe('plugin-meetings', () => {
2593
2713
  webex.meetings.reachability = {
2594
2714
  isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
2595
2715
  getReachabilityMetrics: sinon.stub().resolves(),
2716
+ stopReachability: sinon.stub(),
2596
2717
  };
2597
2718
  const MOCK_CLIENT_ERROR_CODE = 2004;
2598
2719
  const generateClientErrorCodeForIceFailureStub = sinon
@@ -2801,6 +2922,7 @@ describe('plugin-meetings', () => {
2801
2922
  .onCall(2)
2802
2923
  .resolves(false),
2803
2924
  getReachabilityMetrics: sinon.stub().resolves({}),
2925
+ stopReachability: sinon.stub(),
2804
2926
  };
2805
2927
  const getErrorPayloadForClientErrorCodeStub =
2806
2928
  (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
@@ -3095,6 +3217,7 @@ describe('plugin-meetings', () => {
3095
3217
  someReachabilityMetric1: 'some value1',
3096
3218
  someReachabilityMetric2: 'some value2',
3097
3219
  }),
3220
+ stopReachability: sinon.stub(),
3098
3221
  };
3099
3222
  meeting.iceCandidatesCount = 3;
3100
3223
  meeting.iceCandidateErrors.set('701_error', 3);
@@ -3424,6 +3547,51 @@ describe('plugin-meetings', () => {
3424
3547
  });
3425
3548
  });
3426
3549
 
3550
+ it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
3551
+ let fakeMembersCollection = {
3552
+ members: {
3553
+ member1: { isInMeeting: true },
3554
+ member2: { isInMeeting: true },
3555
+ member3: { isInMeeting: false },
3556
+ },
3557
+ };
3558
+ sinon.stub(meeting, 'getMembers').returns({ membersCollection: fakeMembersCollection });
3559
+ const fakeData = { intervalMetadata: {}, networkType: 'wifi' };
3560
+
3561
+ statsAnalyzerStub.emit(
3562
+ { file: 'test', function: 'test' },
3563
+ StatsAnalyzerEventNames.MEDIA_QUALITY,
3564
+ { data: fakeData }
3565
+ );
3566
+
3567
+ assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
3568
+ name: 'client.mediaquality.event',
3569
+ options: {
3570
+ meetingId: meeting.id,
3571
+ },
3572
+ payload: {
3573
+ intervals: [sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2))],
3574
+ },
3575
+ });
3576
+ fakeMembersCollection.members.member2.isInMeeting = false;
3577
+
3578
+ statsAnalyzerStub.emit(
3579
+ { file: 'test', function: 'test' },
3580
+ StatsAnalyzerEventNames.MEDIA_QUALITY,
3581
+ { data: fakeData }
3582
+ );
3583
+
3584
+ assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
3585
+ name: 'client.mediaquality.event',
3586
+ options: {
3587
+ meetingId: meeting.id,
3588
+ },
3589
+ payload: {
3590
+ intervals: [sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1))],
3591
+ },
3592
+ });
3593
+ });
3594
+
3427
3595
  it('calls submitMQE correctly', async () => {
3428
3596
  const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'};
3429
3597
 
@@ -3501,14 +3669,6 @@ describe('plugin-meetings', () => {
3501
3669
  });
3502
3670
  });
3503
3671
 
3504
- it('succeeds even if getDevices() throws', async () => {
3505
- meeting.meetingState = 'ACTIVE';
3506
-
3507
- sinon.stub(InternalMediaCoreModule, 'getDevices').rejects(new Error('fake error'));
3508
-
3509
- await meeting.addMedia();
3510
- });
3511
-
3512
3672
  describe('CA ice failures checks', () => {
3513
3673
  [
3514
3674
  {
@@ -3562,6 +3722,7 @@ describe('plugin-meetings', () => {
3562
3722
 
3563
3723
  webex.meetings.reachability = {
3564
3724
  isWebexMediaBackendUnreachable: sinon.stub().resolves(unreachable || false),
3725
+ stopReachability: sinon.stub(),
3565
3726
  };
3566
3727
 
3567
3728
  const generateClientErrorCodeForIceFailureStub = sinon
@@ -3650,6 +3811,73 @@ describe('plugin-meetings', () => {
3650
3811
  });
3651
3812
  });
3652
3813
 
3814
+ describe(`#beRightBack`, () => {
3815
+ const fakeMultistreamRoapMediaConnection = {
3816
+ createSendSlot: sinon.stub().returns({
3817
+ setSourceStateOverride: sinon.stub().resolves(),
3818
+ clearSourceStateOverride: sinon.stub().resolves(),
3819
+ }),
3820
+ };
3821
+
3822
+ beforeEach(() => {
3823
+ meeting.mediaProperties.webrtcMediaConnection = {createSendSlot: sinon.stub()};
3824
+ meeting.sendSlotManager.createSlot(
3825
+ fakeMultistreamRoapMediaConnection,
3826
+ MediaType.VideoMain
3827
+ );
3828
+
3829
+ meeting.locusUrl = 'locus url';
3830
+ meeting.deviceUrl = 'device url';
3831
+ meeting.selfId = 'self id';
3832
+ meeting.brbState = createBrbState(meeting, false);
3833
+ meeting.brbState.enable = sinon.stub().resolves();
3834
+ });
3835
+
3836
+ afterEach(() => {
3837
+ sinon.restore();
3838
+ });
3839
+
3840
+ it('should have #beRightBack', () => {
3841
+ assert.exists(meeting.beRightBack);
3842
+ });
3843
+
3844
+ describe('when in a multistream meeting', () => {
3845
+
3846
+ beforeEach(() => {
3847
+ meeting.isMultistream = true;
3848
+ });
3849
+
3850
+ it('should enable #beRightBack and return a promise', async () => {
3851
+ const brbResult = meeting.beRightBack(true);
3852
+
3853
+ await brbResult;
3854
+ assert.exists(brbResult.then);
3855
+ assert.calledOnce(meeting.brbState.enable);
3856
+ })
3857
+
3858
+ it('should disable #beRightBack and return a promise', async () => {
3859
+ const brbResult = meeting.beRightBack(false);
3860
+
3861
+ await brbResult;
3862
+ assert.exists(brbResult.then);
3863
+ assert.calledOnce(meeting.brbState.enable);
3864
+ })
3865
+
3866
+ it('should throw an error and reject the promise if setBrb fails', async () => {
3867
+ const error = new Error('setBrb failed');
3868
+ meeting.brbState.enable.rejects(error);
3869
+
3870
+ try {
3871
+ await meeting.beRightBack(true);
3872
+ } catch (err) {
3873
+ assert.instanceOf(err, Error);
3874
+ assert.equal(err.message, 'setBrb failed');
3875
+ assert.isRejected((Promise.reject()));
3876
+ }
3877
+ })
3878
+ });
3879
+ });
3880
+
3653
3881
  /* This set of tests are like semi-integration tests, they use real MuteState, Media, LocusMediaRequest and Roap classes.
3654
3882
  They mock the @webex/internal-media-core and sending of /media http requests to Locus.
3655
3883
  Their main purpose is to test that we send the right http requests to Locus and make right calls
@@ -3692,6 +3920,12 @@ describe('plugin-meetings', () => {
3692
3920
  meeting.setMercuryListener = sinon.stub();
3693
3921
  meeting.locusInfo.onFullLocus = sinon.stub();
3694
3922
  meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
3923
+ meeting.webex.meetings.reachability.getReachabilityReportToAttachToRoap = sinon
3924
+ .stub()
3925
+ .resolves({id: 'fake reachability'});
3926
+ meeting.webex.meetings.reachability.getClientMediaPreferences = sinon
3927
+ .stub()
3928
+ .resolves({id: 'fake clientMediaPreferences'});
3695
3929
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3696
3930
  turnServerInfo: {
3697
3931
  url: 'turns:turn-server-url:443?transport=tcp',
@@ -3772,6 +4006,7 @@ describe('plugin-meetings', () => {
3772
4006
  initiateOffer: sinon.stub().resolves({}),
3773
4007
  update: sinon.stub().resolves({}),
3774
4008
  on: sinon.stub(),
4009
+ roapMessageReceived: sinon.stub()
3775
4010
  };
3776
4011
 
3777
4012
  fakeMultistreamRoapMediaConnection = {
@@ -3858,8 +4093,10 @@ describe('plugin-meetings', () => {
3858
4093
  };
3859
4094
 
3860
4095
  // simulates a Roap offer being generated by the RoapMediaConnection
3861
- const simulateRoapOffer = async () => {
3862
- meeting.deferSDPAnswer = {resolve: sinon.stub()};
4096
+ const simulateRoapOffer = async (stubWaitingForAnswer = true) => {
4097
+ if (stubWaitingForAnswer) {
4098
+ meeting.deferSDPAnswer = {resolve: sinon.stub()};
4099
+ }
3863
4100
  const roapListener = getRoapListener();
3864
4101
 
3865
4102
  await roapListener({roapMessage: roapOfferMessage});
@@ -3877,6 +4114,15 @@ describe('plugin-meetings', () => {
3877
4114
  const checkSdpOfferSent = ({audioMuted, videoMuted}) => {
3878
4115
  const {sdp, seq, tieBreaker} = roapOfferMessage;
3879
4116
 
4117
+ assert.calledWith(
4118
+ meeting.webex.meetings.reachability.getClientMediaPreferences,
4119
+ meeting.isMultistream,
4120
+ 0
4121
+ );
4122
+ assert.calledWith(
4123
+ meeting.webex.meetings.reachability.getReachabilityReportToAttachToRoap
4124
+ );
4125
+
3880
4126
  assert.calledWith(locusMediaRequestStub, {
3881
4127
  method: 'PUT',
3882
4128
  uri: `${meeting.selfUrl}/media`,
@@ -3890,14 +4136,12 @@ describe('plugin-meetings', () => {
3890
4136
  correlationId: meeting.correlationId,
3891
4137
  localMedias: [
3892
4138
  {
3893
- localSdp: `{"audioMuted":${audioMuted},"videoMuted":${videoMuted},"roapMessage":{"messageType":"OFFER","sdps":["${sdp}"],"version":"2","seq":"${seq}","tieBreaker":"${tieBreaker}","headers":["includeAnswerInHttpResponse","noOkInTransaction"]}}`,
4139
+ localSdp: `{"audioMuted":${audioMuted},"videoMuted":${videoMuted},"roapMessage":{"messageType":"OFFER","sdps":["${sdp}"],"version":"2","seq":"${seq}","tieBreaker":"${tieBreaker}","headers":["includeAnswerInHttpResponse","noOkInTransaction"]},"reachability":{"id":"fake reachability"}}`,
3894
4140
  mediaId: 'fake media id',
3895
4141
  },
3896
4142
  ],
3897
4143
  clientMediaPreferences: {
3898
- preferTranscoding: !meeting.isMultistream,
3899
- joinCookie: undefined,
3900
- ipver: 0,
4144
+ id: 'fake clientMediaPreferences',
3901
4145
  },
3902
4146
  },
3903
4147
  });
@@ -3918,13 +4162,11 @@ describe('plugin-meetings', () => {
3918
4162
  },
3919
4163
  correlationId: meeting.correlationId,
3920
4164
  clientMediaPreferences: {
3921
- preferTranscoding: !meeting.isMultistream,
3922
- ipver: undefined,
3923
- joinCookie: undefined,
4165
+ id: 'fake clientMediaPreferences',
3924
4166
  },
3925
4167
  localMedias: [
3926
4168
  {
3927
- localSdp: `{"audioMuted":${audioMuted},"videoMuted":${videoMuted},"roapMessage":{"messageType":"OK","version":"2","seq":"${seq}"}}`,
4169
+ localSdp: `{"audioMuted":${audioMuted},"videoMuted":${videoMuted},"roapMessage":{"messageType":"OK","version":"2","seq":"${seq}"},"reachability":{"id":"fake reachability"}}`,
3928
4170
  mediaId: 'fake media id',
3929
4171
  },
3930
4172
  ],
@@ -3950,10 +4192,6 @@ describe('plugin-meetings', () => {
3950
4192
  mediaId: 'fake media id',
3951
4193
  },
3952
4194
  ],
3953
- clientMediaPreferences: {
3954
- preferTranscoding: !meeting.isMultistream,
3955
- ipver: undefined,
3956
- },
3957
4195
  respOnlySdp: true,
3958
4196
  usingResource: null,
3959
4197
  },
@@ -3967,8 +4205,9 @@ describe('plugin-meetings', () => {
3967
4205
  remoteQualityLevel,
3968
4206
  expectedDebugId,
3969
4207
  meetingId,
4208
+ expectMultistream = isMultistream,
3970
4209
  }) => {
3971
- if (isMultistream) {
4210
+ if (expectMultistream) {
3972
4211
  const {iceServers} = mediaConnectionConfig;
3973
4212
 
3974
4213
  assert.calledOnceWithMatch(
@@ -4128,7 +4367,6 @@ describe('plugin-meetings', () => {
4128
4367
  });
4129
4368
 
4130
4369
  it('addMedia() works correctly when media is enabled with streams to publish', async () => {
4131
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4132
4370
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
4133
4371
  await simulateRoapOffer();
4134
4372
  await simulateRoapOk();
@@ -4159,12 +4397,9 @@ describe('plugin-meetings', () => {
4159
4397
 
4160
4398
  // and that these were the only /media requests that were sent
4161
4399
  assert.calledTwice(locusMediaRequestStub);
4162
-
4163
- assert.calledOnce(handleDeviceLoggingSpy);
4164
4400
  });
4165
4401
 
4166
4402
  it('addMedia() works correctly when media is enabled with streams to publish and stream is user muted', async () => {
4167
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4168
4403
  fakeMicrophoneStream.userMuted = true;
4169
4404
 
4170
4405
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
@@ -4196,7 +4431,6 @@ describe('plugin-meetings', () => {
4196
4431
 
4197
4432
  // and that these were the only /media requests that were sent
4198
4433
  assert.calledTwice(locusMediaRequestStub);
4199
- assert.calledOnce(handleDeviceLoggingSpy);
4200
4434
  });
4201
4435
 
4202
4436
  it('addMedia() works correctly when media is enabled with tracks to publish and track is ended', async () => {
@@ -4268,7 +4502,6 @@ describe('plugin-meetings', () => {
4268
4502
  });
4269
4503
 
4270
4504
  it('addMedia() works correctly when media is disabled with streams to publish', async () => {
4271
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4272
4505
  await meeting.addMedia({
4273
4506
  localStreams: {microphone: fakeMicrophoneStream},
4274
4507
  audioEnabled: false,
@@ -4302,20 +4535,6 @@ describe('plugin-meetings', () => {
4302
4535
 
4303
4536
  // and that these were the only /media requests that were sent
4304
4537
  assert.calledTwice(locusMediaRequestStub);
4305
- assert.calledOnce(handleDeviceLoggingSpy);
4306
- });
4307
-
4308
- it('handleDeviceLogging not called when media is disabled', async () => {
4309
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4310
- await meeting.addMedia({
4311
- localStreams: {microphone: fakeMicrophoneStream},
4312
- audioEnabled: false,
4313
- videoEnabled: false,
4314
- });
4315
- await simulateRoapOffer();
4316
- await simulateRoapOk();
4317
-
4318
- assert.notCalled(handleDeviceLoggingSpy);
4319
4538
  });
4320
4539
 
4321
4540
  it('addMedia() works correctly when media is disabled with no streams to publish', async () => {
@@ -4351,20 +4570,6 @@ describe('plugin-meetings', () => {
4351
4570
  assert.calledTwice(locusMediaRequestStub);
4352
4571
  });
4353
4572
 
4354
- it('addMedia() works correctly when media is disabled with no streams to publish', async () => {
4355
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4356
- await meeting.addMedia({audioEnabled: false});
4357
- //calling handleDeviceLogging with audioEnaled as true adn videoEnabled as false
4358
- assert.calledWith(handleDeviceLoggingSpy, false, true);
4359
- });
4360
-
4361
- it('addMedia() works correctly when video is disabled with no streams to publish', async () => {
4362
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4363
- await meeting.addMedia({videoEnabled: false});
4364
- //calling handleDeviceLogging audioEnabled as true videoEnabled as false
4365
- assert.calledWith(handleDeviceLoggingSpy, true, false);
4366
- });
4367
-
4368
4573
  it('addMedia() works correctly when video is disabled with no streams to publish', async () => {
4369
4574
  await meeting.addMedia({videoEnabled: false});
4370
4575
  await simulateRoapOffer();
@@ -4431,13 +4636,6 @@ describe('plugin-meetings', () => {
4431
4636
  assert.calledTwice(locusMediaRequestStub);
4432
4637
  });
4433
4638
 
4434
- it('addMedia() works correctly when both shareAudio and shareVideo is disabled with no streams publish', async () => {
4435
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4436
- await meeting.addMedia({shareAudioEnabled: false, shareVideoEnabled: false});
4437
- //calling handleDeviceLogging with audioEnabled true and videoEnabled as true
4438
- assert.calledWith(handleDeviceLoggingSpy, true, true);
4439
- });
4440
-
4441
4639
  describe('publishStreams()/unpublishStreams() calls', () => {
4442
4640
  [
4443
4641
  {mediaEnabled: true, expected: {direction: 'sendrecv', localMuteSentValue: false}},
@@ -4833,6 +5031,211 @@ describe('plugin-meetings', () => {
4833
5031
  assert.notCalled(fakeRoapMediaConnection.update);
4834
5032
  })
4835
5033
  );
5034
+
5035
+ if (isMultistream) {
5036
+ describe('fallback from multistream to transcoded', () => {
5037
+ let multistreamEventListeners;
5038
+ let transcodedEventListeners;
5039
+ let mockStatsAnalyzerCtor;
5040
+
5041
+ const setupFakeRoapMediaConnection = (fakeRoapMediaConnection, eventListeners) => {
5042
+ fakeRoapMediaConnection.on.callsFake((eventName, cb) => {
5043
+ eventListeners[eventName] = cb;
5044
+ });
5045
+ fakeRoapMediaConnection.initiateOffer.callsFake(() => {
5046
+ // simulate offer being generated
5047
+ eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED]();
5048
+
5049
+ return Promise.resolve();
5050
+ });
5051
+ };
5052
+
5053
+ beforeEach(() => {
5054
+ multistreamEventListeners = {};
5055
+ transcodedEventListeners = {};
5056
+
5057
+ meeting.config.stats.enableStatsAnalyzer = true;
5058
+
5059
+ setupFakeRoapMediaConnection(fakeRoapMediaConnection, transcodedEventListeners);
5060
+ setupFakeRoapMediaConnection(
5061
+ fakeMultistreamRoapMediaConnection,
5062
+ multistreamEventListeners
5063
+ );
5064
+
5065
+ mockStatsAnalyzerCtor = sinon
5066
+ .stub(InternalMediaCoreModule, 'StatsAnalyzer')
5067
+ .callsFake(() => {
5068
+ return {on: sinon.stub(), stopAnalyzer: sinon.stub()};
5069
+ });
5070
+
5071
+ webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
5072
+ sinon.stub();
5073
+
5074
+ // setup the mock so that we get an SDP answer not from Homer
5075
+ locusMediaRequestStub.callsFake(() => {
5076
+ return Promise.resolve({
5077
+ body: {
5078
+ locus: {},
5079
+ mediaConnections: [
5080
+ {
5081
+ remoteSdp:
5082
+ '{"audioMuted":false,"videoMuted":true,"roapMessage":{"messageType":"ANSWER","version":"2","seq":1,"sdps":["v=0\\r\\no=linus 0 1 IN IP4 23.89.67.4\\r\\ns=-\\r\\nc=IN IP4 23.89.67.4\\r\\n"],"headers":["noOkInTransaction"]},"type":"SDP"}',
5083
+ },
5084
+ ],
5085
+ },
5086
+ });
5087
+ });
5088
+
5089
+ sinon.stub(meeting, 'closePeerConnections');
5090
+ sinon.stub(meeting.mediaProperties, 'unsetPeerConnection');
5091
+ sinon.stub(meeting.locusMediaRequest, 'downgradeFromMultistreamToTranscoded');
5092
+ });
5093
+
5094
+ const runCheck = async (turnServerInfo, forceTurnDiscovery) => {
5095
+ // we're calling addMediaInternal() with mic stream,
5096
+ // so that we also verify that audioMute, videoMute info is correctly sent to backend
5097
+ const addMediaPromise = meeting.addMediaInternal(
5098
+ () => '',
5099
+ turnServerInfo,
5100
+ forceTurnDiscovery,
5101
+ {
5102
+ localStreams: {microphone: fakeMicrophoneStream},
5103
+ }
5104
+ );
5105
+ await testUtils.flushPromises();
5106
+ await simulateRoapOffer(false);
5107
+
5108
+ // check MultistreamRoapMediaConnection was created correctly
5109
+ checkMediaConnectionCreated({
5110
+ expectMultistream: true,
5111
+ mediaConnectionConfig: expectedMediaConnectionConfig,
5112
+ localStreams: {
5113
+ audio: fakeMicrophoneStream,
5114
+ video: undefined,
5115
+ screenShareVideo: undefined,
5116
+ screenShareAudio: undefined,
5117
+ },
5118
+ direction: {
5119
+ audio: 'sendrecv',
5120
+ video: 'sendrecv',
5121
+ screenShare: 'recvonly',
5122
+ },
5123
+ remoteQualityLevel: 'HIGH',
5124
+ expectedDebugId,
5125
+ meetingId: meeting.id,
5126
+ });
5127
+
5128
+ // check that stats analyzer was created with the right config and store the reference to it so that we can later check that it was stopped
5129
+ assert.calledOnceWithExactly(
5130
+ mockStatsAnalyzerCtor,
5131
+ sinon.match({
5132
+ isMultistream: true,
5133
+ })
5134
+ );
5135
+ const initialStatsAnalyzer = mockStatsAnalyzerCtor.returnValues[0];
5136
+ mockStatsAnalyzerCtor.resetHistory();
5137
+
5138
+ // TURN discovery was done (if needed)
5139
+ if (turnServerInfo) {
5140
+ assert.notCalled(meeting.roap.doTurnDiscovery);
5141
+ } else {
5142
+ assert.calledWith(meeting.roap.doTurnDiscovery, meeting, false, false);
5143
+ }
5144
+
5145
+ // and SDP offer was sent with the right audioMuted/videoMuted values
5146
+ checkSdpOfferSent({audioMuted: false, videoMuted: true});
5147
+
5148
+ await testUtils.flushPromises();
5149
+
5150
+ // at this point the meeting should have been downgraded to transcoded
5151
+ assert.equal(meeting.isMultistream, false);
5152
+
5153
+ // old stats analyzer stopped and new one created
5154
+ assert.calledOnce(initialStatsAnalyzer.stopAnalyzer);
5155
+ assert.calledOnceWithExactly(
5156
+ mockStatsAnalyzerCtor,
5157
+ sinon.match({
5158
+ isMultistream: false,
5159
+ })
5160
+ );
5161
+
5162
+ // and correct cleanup of other things should have been done
5163
+ assert.calledOnceWithExactly(meeting.closePeerConnections, false);
5164
+ assert.calledOnceWithExactly(meeting.mediaProperties.unsetPeerConnection);
5165
+ assert.calledOnceWithExactly(
5166
+ meeting.locusMediaRequest.downgradeFromMultistreamToTranscoded
5167
+ );
5168
+
5169
+ // new connection should have been created
5170
+ checkMediaConnectionCreated({
5171
+ expectMultistream: false,
5172
+ mediaConnectionConfig: expectedMediaConnectionConfig,
5173
+ localStreams: {
5174
+ audio: fakeMicrophoneStream,
5175
+ video: undefined,
5176
+ screenShareVideo: undefined,
5177
+ screenShareAudio: undefined,
5178
+ },
5179
+ direction: {
5180
+ audio: 'sendrecv',
5181
+ video: 'sendrecv',
5182
+ screenShare: 'recvonly',
5183
+ },
5184
+ remoteQualityLevel: 'HIGH',
5185
+ expectedDebugId,
5186
+ meetingId: meeting.id,
5187
+ });
5188
+
5189
+ // and new TURN discovery done (no matter if it was being done before or not)
5190
+ assert.calledWith(meeting.roap.doTurnDiscovery, meeting, true, true);
5191
+
5192
+ // simulate new offer
5193
+ await simulateRoapOffer(false);
5194
+ checkSdpOfferSent({audioMuted: false, videoMuted: true});
5195
+
5196
+ // overall there should have been 2 calls to locusMediaRequestStub, because 2 offers were sent
5197
+ assert.calledTwice(locusMediaRequestStub);
5198
+
5199
+ // simulate answer being processed correctly
5200
+ transcodedEventListeners[MediaConnectionEventNames.REMOTE_SDP_ANSWER_PROCESSED]();
5201
+
5202
+ // check that addMedia finally resolved
5203
+ await addMediaPromise;
5204
+ };
5205
+
5206
+ it('addMedia() falls back to transcoded if SDP answer is not from Homer', async () => {
5207
+ // call addMediaInternal like addMedia() does it
5208
+ await runCheck(undefined, false);
5209
+ });
5210
+
5211
+ it('addMediaInternal() correctly falls back to transcoded if SDP answer is not from Homer (joinWithMedia() case)', async () => {
5212
+ // call addMediaInternal the way joinWithMedia() does it - with TURN info already provided
5213
+ // and check that when we fallback to transcoded we still do another TURN discovery
5214
+ await runCheck(
5215
+ {
5216
+ url: 'turns:turn-server-url:443?transport=tcp',
5217
+ username: 'turn user',
5218
+ password: 'turn password',
5219
+ },
5220
+ false
5221
+ );
5222
+ });
5223
+
5224
+ it('addMediaInternal() correctly falls back to transcoded if SDP answer is not from Homer (joinWithMedia() retry case)', async () => {
5225
+ // call addMediaInternal the way joinWithMedia() does it when it does a retry - with TURN info already provided
5226
+ // but also with forceTurnDiscovery=true - this shouldn't affect the flow for fallback to transcoded in any way
5227
+ // but doing it just for completeness
5228
+ await runCheck(
5229
+ {
5230
+ url: 'turns:turn-server-url:443?transport=tcp',
5231
+ username: 'turn user',
5232
+ password: 'turn password',
5233
+ },
5234
+ true
5235
+ );
5236
+ });
5237
+ });
5238
+ }
4836
5239
  })
4837
5240
  );
4838
5241
 
@@ -4910,6 +5313,11 @@ describe('plugin-meetings', () => {
4910
5313
  meeting.logger.error = sinon.stub().returns(true);
4911
5314
  meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
4912
5315
  webex.internal.voicea.off = sinon.stub().returns(true);
5316
+ meeting.stopTranscription = sinon.stub();
5317
+ meeting.transcription = {};
5318
+
5319
+ meeting.annotation.deregisterEvents = sinon.stub();
5320
+ webex.internal.llm.off = sinon.stub();
4913
5321
 
4914
5322
  // A meeting needs to be joined to leave
4915
5323
  meeting.meetingState = 'ACTIVE';
@@ -4930,6 +5338,9 @@ describe('plugin-meetings', () => {
4930
5338
  assert.calledOnce(meeting.closePeerConnections);
4931
5339
  assert.calledOnce(meeting.unsetRemoteStreams);
4932
5340
  assert.calledOnce(meeting.unsetPeerConnections);
5341
+ assert.calledOnce(meeting.stopTranscription);
5342
+ assert.calledOnce(meeting.annotation.deregisterEvents);
5343
+ assert.calledWith(webex.internal.llm.off, 'event:relay.event', meeting.processRelayEvent);
4933
5344
  });
4934
5345
 
4935
5346
  it('should reset call diagnostic latencies correctly', async () => {
@@ -5917,6 +6328,38 @@ describe('plugin-meetings', () => {
5917
6328
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.REQUIRED);
5918
6329
  });
5919
6330
 
6331
+ it('handles meetingInfoProvider not reach JBH', async () => {
6332
+ meeting.destination = FAKE_DESTINATION;
6333
+ meeting.destinationType = FAKE_TYPE;
6334
+ meeting.attrs.meetingInfoProvider = {
6335
+ fetchMeetingInfo: sinon
6336
+ .stub()
6337
+ .throws(new MeetingInfoV2JoinForbiddenError(403003, FAKE_MEETING_INFO)),
6338
+ };
6339
+
6340
+ await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinForbiddenError);
6341
+
6342
+ assert.calledWith(
6343
+ meeting.attrs.meetingInfoProvider.fetchMeetingInfo,
6344
+ FAKE_DESTINATION,
6345
+ FAKE_TYPE,
6346
+ null,
6347
+ null,
6348
+ undefined,
6349
+ 'locus-id',
6350
+ {},
6351
+ {meetingId: meeting.id, sendCAevents: true}
6352
+ );
6353
+
6354
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6355
+ assert.equal(meeting.meetingInfoFailureCode, 403003);
6356
+ assert.equal(
6357
+ meeting.meetingInfoFailureReason,
6358
+ MEETING_INFO_FAILURE_REASON.NOT_REACH_JBH
6359
+ );
6360
+ assert.equal(meeting.requiredCaptcha, null);
6361
+ });
6362
+
5920
6363
  it('handles meetingInfoProvider policy error', async () => {
5921
6364
  meeting.destination = FAKE_DESTINATION;
5922
6365
  meeting.destinationType = FAKE_TYPE;
@@ -6284,29 +6727,74 @@ describe('plugin-meetings', () => {
6284
6727
  assert.equal(meeting.fetchMeetingInfoTimeoutId, undefined);
6285
6728
  });
6286
6729
 
6287
- it('handles meetingInfoProvider webinar need registration error', async () => {
6730
+ it('handles MeetingInfoV2JoinWebinarError webinar need registration', async () => {
6288
6731
  meeting.destination = FAKE_DESTINATION;
6289
6732
  meeting.destinationType = FAKE_TYPE;
6290
6733
  meeting.attrs.meetingInfoProvider = {
6291
6734
  fetchMeetingInfo: sinon
6292
6735
  .stub()
6293
6736
  .throws(
6294
- new MeetingInfoV2WebinarRegistrationError(403021, FAKE_MEETING_INFO, 'a message')
6737
+ new MeetingInfoV2JoinWebinarError(403021, FAKE_MEETING_INFO, 'a message')
6295
6738
  ),
6296
6739
  };
6297
6740
 
6298
6741
  await assert.isRejected(
6299
6742
  meeting.fetchMeetingInfo({sendCAevents: true}),
6300
- WebinarRegistrationError
6743
+ JoinWebinarError
6301
6744
  );
6302
6745
 
6303
6746
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6304
- assert.equal(meeting.meetingInfoFailureCode, 403021);
6305
6747
  assert.equal(
6306
6748
  meeting.meetingInfoFailureReason,
6307
6749
  MEETING_INFO_FAILURE_REASON.WEBINAR_REGISTRATION
6308
6750
  );
6309
6751
  });
6752
+
6753
+ it('handles MeetingInfoV2JoinWebinarError webinar need join with webcast', async () => {
6754
+ meeting.destination = FAKE_DESTINATION;
6755
+ meeting.destinationType = FAKE_TYPE;
6756
+ meeting.attrs.meetingInfoProvider = {
6757
+ fetchMeetingInfo: sinon
6758
+ .stub()
6759
+ .throws(
6760
+ new MeetingInfoV2JoinWebinarError(403026, FAKE_MEETING_INFO, 'a message')
6761
+ ),
6762
+ };
6763
+
6764
+ await assert.isRejected(
6765
+ meeting.fetchMeetingInfo({sendCAevents: true}),
6766
+ JoinWebinarError
6767
+ );
6768
+
6769
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6770
+ assert.equal(
6771
+ meeting.meetingInfoFailureReason,
6772
+ MEETING_INFO_FAILURE_REASON.NEED_JOIN_WITH_WEBCAST
6773
+ );
6774
+ });
6775
+
6776
+ it('handles MeetingInfoV2JoinWebinarError webinar need registrationId', async () => {
6777
+ meeting.destination = FAKE_DESTINATION;
6778
+ meeting.destinationType = FAKE_TYPE;
6779
+ meeting.attrs.meetingInfoProvider = {
6780
+ fetchMeetingInfo: sinon
6781
+ .stub()
6782
+ .throws(
6783
+ new MeetingInfoV2JoinWebinarError(403037, FAKE_MEETING_INFO, 'a message')
6784
+ ),
6785
+ };
6786
+
6787
+ await assert.isRejected(
6788
+ meeting.fetchMeetingInfo({sendCAevents: true}),
6789
+ JoinWebinarError
6790
+ );
6791
+
6792
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6793
+ assert.equal(
6794
+ meeting.meetingInfoFailureReason,
6795
+ MEETING_INFO_FAILURE_REASON.WEBINAR_NEED_REGISTRATION_ID
6796
+ );
6797
+ });
6310
6798
  });
6311
6799
 
6312
6800
  describe('#refreshPermissionToken', () => {
@@ -6364,7 +6852,8 @@ describe('plugin-meetings', () => {
6364
6852
  'fake-installed-org-id',
6365
6853
  'locus-id',
6366
6854
  {extraParam1: 'value1', permissionToken: FAKE_PERMISSION_TOKEN},
6367
- {meetingId: meeting.id, sendCAevents: true}
6855
+ {meetingId: meeting.id, sendCAevents: true},
6856
+ null
6368
6857
  );
6369
6858
  assert.deepEqual(meeting.meetingInfo, {
6370
6859
  ...FAKE_MEETING_INFO,
@@ -6409,7 +6898,8 @@ describe('plugin-meetings', () => {
6409
6898
  'fake-installed-org-id',
6410
6899
  'locus-id',
6411
6900
  {extraParam1: 'value1', permissionToken: FAKE_PERMISSION_TOKEN},
6412
- {meetingId: meeting.id, sendCAevents: true}
6901
+ {meetingId: meeting.id, sendCAevents: true},
6902
+ null
6413
6903
  );
6414
6904
  assert.deepEqual(meeting.meetingInfo, {
6415
6905
  ...FAKE_MEETING_INFO,
@@ -6463,7 +6953,8 @@ describe('plugin-meetings', () => {
6463
6953
  extraParam1: 'value1',
6464
6954
  permissionToken: FAKE_PERMISSION_TOKEN,
6465
6955
  },
6466
- {meetingId: meeting.id, sendCAevents: true}
6956
+ {meetingId: meeting.id, sendCAevents: true},
6957
+ null
6467
6958
  );
6468
6959
  assert.deepEqual(meeting.meetingInfo, {
6469
6960
  ...FAKE_MEETING_INFO,
@@ -6767,6 +7258,9 @@ describe('plugin-meetings', () => {
6767
7258
  meeting.transcription = {};
6768
7259
  meeting.stopTranscription = sinon.stub();
6769
7260
 
7261
+ meeting.annotation.deregisterEvents = sinon.stub();
7262
+ webex.internal.llm.off = sinon.stub();
7263
+
6770
7264
  // A meeting needs to be joined to end
6771
7265
  meeting.meetingState = 'ACTIVE';
6772
7266
  meeting.state = 'JOINED';
@@ -6787,6 +7281,9 @@ describe('plugin-meetings', () => {
6787
7281
  assert.calledOnce(meeting?.unsetRemoteStreams);
6788
7282
  assert.calledOnce(meeting?.unsetPeerConnections);
6789
7283
  assert.calledOnce(meeting?.stopTranscription);
7284
+
7285
+ assert.called(meeting.annotation.deregisterEvents);
7286
+ assert.calledWith(webex.internal.llm.off, 'event:relay.event', meeting.processRelayEvent);
6790
7287
  });
6791
7288
  });
6792
7289
 
@@ -7769,7 +8266,9 @@ describe('plugin-meetings', () => {
7769
8266
  });
7770
8267
 
7771
8268
  it('should collect ice candidates', () => {
7772
- eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: {candidate: 'candidate'}});
8269
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({
8270
+ candidate: {candidate: 'candidate'},
8271
+ });
7773
8272
 
7774
8273
  assert.equal(meeting.iceCandidatesCount, 1);
7775
8274
  });
@@ -8075,10 +8574,10 @@ describe('plugin-meetings', () => {
8075
8574
  meeting.statsAnalyzer.stopAnalyzer = sinon.stub().resolves();
8076
8575
  meeting.reconnectionManager = {
8077
8576
  reconnect: sinon.stub().resolves(),
8078
- resetReconnectionTimer: () => {}
8577
+ resetReconnectionTimer: () => {},
8079
8578
  };
8080
8579
  meeting.currentMediaStatus = {
8081
- video: true
8580
+ video: true,
8082
8581
  };
8083
8582
 
8084
8583
  await mockFailedEvent();
@@ -8360,8 +8859,7 @@ describe('plugin-meetings', () => {
8360
8859
  assert.calledWith(meeting.roapMessageReceived, fakeAnswer);
8361
8860
  });
8362
8861
 
8363
- it('handles OFFER message correctly when request fails', async () => {
8364
- const fakeError = new Error('fake error');
8862
+ const runOfferSendingFailureTest = async (fakeError, canProceed, expectedErrorCode) => {
8365
8863
  const clock = sinon.useFakeTimers();
8366
8864
  sinon.spy(clock, 'clearTimeout');
8367
8865
  meeting.deferSDPAnswer = {reject: sinon.stub()};
@@ -8399,19 +8897,31 @@ describe('plugin-meetings', () => {
8399
8897
  assert.equal(meeting.sdpResponseTimer, undefined);
8400
8898
 
8401
8899
  assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
8402
- clientErrorCode: 2007,
8900
+ clientErrorCode: expectedErrorCode,
8403
8901
  });
8404
8902
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
8405
8903
  name: 'client.media-engine.remote-sdp-received',
8406
8904
  payload: {
8407
- canProceed: false,
8408
- errors: [{errorCode: 2007, fatal: true}],
8905
+ canProceed,
8906
+ errors: [{errorCode: expectedErrorCode, fatal: true}],
8409
8907
  },
8410
8908
  options: {
8411
8909
  meetingId: meeting.id,
8412
8910
  rawError: fakeError,
8413
8911
  },
8414
8912
  });
8913
+ };
8914
+
8915
+ it('handles OFFER message correctly when request fails', async () => {
8916
+ const fakeError = new Error('fake error');
8917
+
8918
+ await runOfferSendingFailureTest(fakeError, false, 2007);
8919
+ });
8920
+
8921
+ it('handles OFFER message correctly when we get a non-homer answer', async () => {
8922
+ const fakeError = new MultistreamNotSupportedError();
8923
+
8924
+ await runOfferSendingFailureTest(fakeError, true, 2012);
8415
8925
  });
8416
8926
 
8417
8927
  it('handles ANSWER message correctly', () => {
@@ -8614,6 +9124,7 @@ describe('plugin-meetings', () => {
8614
9124
  });
8615
9125
  });
8616
9126
  });
9127
+
8617
9128
  describe('#setUpLocusInfoSelfListener', () => {
8618
9129
  it('listens to the self unadmitted guest event', (done) => {
8619
9130
  meeting.startKeepAlive = sinon.stub();
@@ -8629,6 +9140,13 @@ describe('plugin-meetings', () => {
8629
9140
  {payload: test1}
8630
9141
  );
8631
9142
  assert.calledOnce(meeting.updateLLMConnection);
9143
+ assert.calledOnceWithExactly(
9144
+ Metrics.sendBehavioralMetric,
9145
+ BEHAVIORAL_METRICS.GUEST_ENTERED_LOBBY,
9146
+ {
9147
+ correlation_id: meeting.correlationId,
9148
+ }
9149
+ );
8632
9150
  done();
8633
9151
  });
8634
9152
  it('listens to the self admitted guest event', (done) => {
@@ -8650,6 +9168,13 @@ describe('plugin-meetings', () => {
8650
9168
  assert.calledOnce(meeting.updateLLMConnection);
8651
9169
  assert.calledOnceWithExactly(meeting.rtcMetrics.sendNextMetrics);
8652
9170
 
9171
+ assert.calledOnceWithExactly(
9172
+ Metrics.sendBehavioralMetric,
9173
+ BEHAVIORAL_METRICS.GUEST_EXITED_LOBBY,
9174
+ {
9175
+ correlation_id: meeting.correlationId,
9176
+ }
9177
+ );
8653
9178
  done();
8654
9179
  });
8655
9180
 
@@ -8694,6 +9219,27 @@ describe('plugin-meetings', () => {
8694
9219
  );
8695
9220
  });
8696
9221
 
9222
+ it('listens to the brb state changed event', () => {
9223
+ const assertBrb = (enabled) => {
9224
+ meeting.brbState = createBrbState(meeting, false);
9225
+ meeting.locusInfo.emit(
9226
+ { function: 'test', file: 'test' },
9227
+ LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
9228
+ { brb: { enabled } },
9229
+ )
9230
+ assert.calledWithExactly(
9231
+ TriggerProxy.trigger,
9232
+ meeting,
9233
+ {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
9234
+ EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
9235
+ { payload: { brb: { enabled } } },
9236
+ );
9237
+ }
9238
+
9239
+ assertBrb(true);
9240
+ assertBrb(false);
9241
+ })
9242
+
8697
9243
  it('listens to the interpretation changed event', () => {
8698
9244
  meeting.simultaneousInterpretation.updateSelfInterpretation = sinon.stub();
8699
9245
 
@@ -8982,6 +9528,8 @@ describe('plugin-meetings', () => {
8982
9528
  });
8983
9529
 
8984
9530
  it('listens to MEETING_CONTROLS_PRACTICE_SESSION_STATUS_UPDATED', async () => {
9531
+ meeting.webinar.updatePracticeSessionStatus = sinon.stub();
9532
+
8985
9533
  const state = {example: 'value'};
8986
9534
 
8987
9535
  await meeting.locusInfo.emitScoped(
@@ -8990,6 +9538,7 @@ describe('plugin-meetings', () => {
8990
9538
  {state}
8991
9539
  );
8992
9540
 
9541
+ assert.calledOnceWithExactly(meeting.webinar.updatePracticeSessionStatus, state);
8993
9542
  assert.calledWith(
8994
9543
  TriggerProxy.trigger,
8995
9544
  meeting,
@@ -9472,15 +10021,44 @@ describe('plugin-meetings', () => {
9472
10021
  describe('#closePeerConnections', () => {
9473
10022
  it('should close the webrtc media connection, and return a promise', async () => {
9474
10023
  const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
9475
- meeting.mediaProperties.webrtcMediaConnection = {close: sinon.stub()};
10024
+ const fakeWebrtcMediaConnection = {close: sinon.stub()};
10025
+ meeting.mediaProperties.webrtcMediaConnection = fakeWebrtcMediaConnection;
10026
+
10027
+ meeting.audio = {id: 'fakeAudioMuteState'};
10028
+ meeting.video = {id: 'fakeVideoMuteState'};
10029
+
9476
10030
  const pcs = meeting.closePeerConnections();
9477
10031
 
9478
10032
  assert.exists(pcs.then);
9479
10033
  await pcs;
9480
- assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.close);
10034
+ assert.calledOnce(fakeWebrtcMediaConnection.close);
10035
+ assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
10036
+ assert.equal(meeting.audio, null);
10037
+ assert.equal(meeting.video, null);
10038
+ assert.equal(meeting.mediaProperties.webrtcMediaConnection, null);
10039
+ });
10040
+
10041
+ it('should close the webrtc media connection, but keep audio and video props unchanged if called with resetMuteStates=false', async () => {
10042
+ const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
10043
+ const fakeWebrtcMediaConnection = {close: sinon.stub()};
10044
+ meeting.mediaProperties.webrtcMediaConnection = fakeWebrtcMediaConnection;
10045
+
10046
+ const fakeAudio = {id: 'fakeAudioMuteState'};
10047
+ const fakeVideo = {id: 'fakeVideoMuteState'};
10048
+
10049
+ meeting.audio = fakeAudio;
10050
+ meeting.video = fakeVideo;
10051
+
10052
+ await meeting.closePeerConnections(false);
10053
+
10054
+ assert.calledOnce(fakeWebrtcMediaConnection.close);
9481
10055
  assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
10056
+ assert.equal(meeting.audio, fakeAudio);
10057
+ assert.equal(meeting.video, fakeVideo);
10058
+ assert.equal(meeting.mediaProperties.webrtcMediaConnection, null);
9482
10059
  });
9483
10060
  });
10061
+
9484
10062
  describe('#unsetPeerConnections', () => {
9485
10063
  it('should unset the peer connections', () => {
9486
10064
  meeting.mediaProperties.unsetPeerConnection = sinon.stub().returns(true);
@@ -10609,6 +11187,7 @@ describe('plugin-meetings', () => {
10609
11187
  meeting.webex.internal.llm.on = sinon.stub();
10610
11188
  meeting.webex.internal.llm.off = sinon.stub();
10611
11189
  meeting.processRelayEvent = sinon.stub();
11190
+ meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(false);
10612
11191
  });
10613
11192
 
10614
11193
  it('does not connect if the call is not joined yet', async () => {
@@ -10740,6 +11319,19 @@ describe('plugin-meetings', () => {
10740
11319
  meeting.processRelayEvent
10741
11320
  );
10742
11321
  });
11322
+
11323
+
11324
+ it('connect ps data channel if ps started in webinar', async () => {
11325
+ meeting.joinedWith = {state: 'JOINED'};
11326
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url', practiceSessionDatachannelUrl: 'a ps datachannel url'}};
11327
+ meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(true);
11328
+ await meeting.updateLLMConnection();
11329
+
11330
+ assert.notCalled(webex.internal.llm.disconnectLLM);
11331
+ assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a ps datachannel url');
11332
+
11333
+ });
11334
+
10743
11335
  });
10744
11336
 
10745
11337
  describe('#setLocus', () => {
@@ -10931,6 +11523,7 @@ describe('plugin-meetings', () => {
10931
11523
  beforeEach(() => {
10932
11524
  meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
10933
11525
  meeting.deviceUrl = 'my-web-url';
11526
+ meeting.locusInfo.info = {isWebinar: false};
10934
11527
  });
10935
11528
 
10936
11529
  const USER_IDS = {
@@ -11156,13 +11749,24 @@ describe('plugin-meetings', () => {
11156
11749
 
11157
11750
  activeSharingId.whiteboard = beneficiaryId;
11158
11751
 
11159
- eventTrigger.share.push({
11752
+ eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
11753
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11754
+ functionName: 'remoteShare',
11755
+ eventPayload: {
11756
+ memberId: null,
11757
+ url,
11758
+ shareInstanceId,
11759
+ annotationInfo: undefined,
11760
+ resourceType: undefined,
11761
+ },
11762
+ } : {
11160
11763
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11161
11764
  functionName: 'startWhiteboardShare',
11162
11765
  eventPayload: {resourceUrl, memberId: beneficiaryId},
11163
11766
  });
11164
11767
 
11165
- shareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11768
+ shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11769
+
11166
11770
  }
11167
11771
 
11168
11772
  if (eventTrigger.member) {
@@ -11194,13 +11798,24 @@ describe('plugin-meetings', () => {
11194
11798
  newPayload.current.content.disposition = FLOOR_ACTION.ACCEPTED;
11195
11799
  newPayload.current.content.beneficiaryId = otherBeneficiaryId;
11196
11800
 
11197
- eventTrigger.share.push({
11801
+ eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
11802
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11803
+ functionName: 'remoteShare',
11804
+ eventPayload: {
11805
+ memberId: null,
11806
+ url,
11807
+ shareInstanceId,
11808
+ annotationInfo: undefined,
11809
+ resourceType: undefined,
11810
+ },
11811
+ } : {
11198
11812
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11199
11813
  functionName: 'startWhiteboardShare',
11200
11814
  eventPayload: {resourceUrl, memberId: beneficiaryId},
11201
11815
  });
11202
11816
 
11203
- shareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11817
+ shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11818
+
11204
11819
  } else {
11205
11820
  eventTrigger.share.push({
11206
11821
  eventName: EVENT_TRIGGERS.MEETING_STOPPED_SHARING_WHITEBOARD,
@@ -11327,6 +11942,38 @@ describe('plugin-meetings', () => {
11327
11942
  assert.exists(meeting.setUpLocusMediaSharesListener);
11328
11943
  });
11329
11944
 
11945
+ describe('Whiteboard Share - Webinar Attendee', () => {
11946
+ it('Scenario #1: Whiteboard sharing as a webinar attendee', () => {
11947
+ // Set the webinar attendee flag
11948
+ meeting.webinar = { selfIsAttendee: true };
11949
+ meeting.locusInfo.info.isWebinar = true;
11950
+
11951
+ // Step 1: Start sharing whiteboard A
11952
+ const data1 = generateData(
11953
+ blankPayload, // Initial payload
11954
+ true, // isGranting: Granting share
11955
+ false, // isContent: Whiteboard (not content)
11956
+ USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
11957
+ RESOURCE_URLS.WHITEBOARD_A // Resource URL: Whiteboard A
11958
+ );
11959
+
11960
+ // Step 2: Stop sharing whiteboard A
11961
+ const data2 = generateData(
11962
+ data1.payload, // Updated payload from Step 1
11963
+ false, // isGranting: Stopping share
11964
+ false, // isContent: Whiteboard
11965
+ USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
11966
+ );
11967
+
11968
+ // Validate the payload changes and status updates
11969
+ payloadTestHelper([data1]);
11970
+
11971
+ // Specific assertions for webinar attendee status
11972
+ assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
11973
+ });
11974
+ });
11975
+
11976
+
11330
11977
  describe('Whiteboard A --> Whiteboard B', () => {
11331
11978
  it('Scenario #1: you share both whiteboards', () => {
11332
11979
  const data1 = generateData(
@@ -12002,9 +12649,12 @@ describe('plugin-meetings', () => {
12002
12649
  it('startKeepAlive starts the keep alive', async () => {
12003
12650
  meeting.meetingRequest.keepAlive = sinon.stub().returns(Promise.resolve());
12004
12651
 
12652
+ const keepAliveUrl1 = 'keep.alive.url1';
12653
+ const keepAliveUrl2 = 'keep.alive.url2';
12654
+
12005
12655
  assert.isNull(meeting.keepAliveTimerId);
12006
12656
  meeting.joinedWith = {
12007
- keepAliveUrl: defaultKeepAliveUrl,
12657
+ keepAliveUrl: keepAliveUrl1,
12008
12658
  keepAliveSecs: defaultKeepAliveSecs,
12009
12659
  };
12010
12660
  meeting.startKeepAlive();
@@ -12013,12 +12663,15 @@ describe('plugin-meetings', () => {
12013
12663
  assert.notCalled(meeting.meetingRequest.keepAlive);
12014
12664
  await progressTime(defaultExpectedInterval);
12015
12665
  assert.calledOnceWithExactly(meeting.meetingRequest.keepAlive, {
12016
- keepAliveUrl: defaultKeepAliveUrl,
12666
+ keepAliveUrl: keepAliveUrl1,
12017
12667
  });
12668
+ // joinedWith keep alive url might change (when we fallback from multistream to transcoded)
12669
+ meeting.joinedWith.keepAliveUrl = keepAliveUrl2;
12670
+
12018
12671
  await progressTime(defaultExpectedInterval);
12019
12672
  assert.calledTwice(meeting.meetingRequest.keepAlive);
12020
- assert.alwaysCalledWithExactly(meeting.meetingRequest.keepAlive, {
12021
- keepAliveUrl: defaultKeepAliveUrl,
12673
+ assert.calledWith(meeting.meetingRequest.keepAlive, {
12674
+ keepAliveUrl: keepAliveUrl2,
12022
12675
  });
12023
12676
  });
12024
12677
  it('startKeepAlive handles existing keepAliveTimerId', async () => {
@@ -12599,7 +13252,7 @@ describe('plugin-meetings', () => {
12599
13252
 
12600
13253
  describe('#roapMessageReceived', () => {
12601
13254
  it('calls roapMessageReceived on the webrtc media connection', () => {
12602
- const fakeMessage = {messageType: 'fake', sdp: 'fake sdp'};
13255
+ const fakeMessage = {messageType: 'ANSWER', sdp: 'fake sdp'};
12603
13256
 
12604
13257
  const getMediaServer = sinon.stub(MeetingsUtil, 'getMediaServer').returns('homer');
12605
13258
 
@@ -12616,5 +13269,96 @@ describe('plugin-meetings', () => {
12616
13269
  assert.calledOnceWithExactly(getMediaServer, 'fake sdp');
12617
13270
  assert.equal(meeting.mediaProperties.webrtcMediaConnection.mediaServer, 'homer');
12618
13271
  });
13272
+
13273
+ it('throws MultistreamNotSupportedError if we get a non-homer SDP answer', async () => {
13274
+ const fakeMessage = {messageType: 'ANSWER', sdp: 'fake sdp'};
13275
+
13276
+ meeting.isMultistream = true;
13277
+ meeting.mediaProperties.webrtcMediaConnection = {
13278
+ roapMessageReceived: sinon.stub(),
13279
+ };
13280
+
13281
+ sinon.stub(MeetingsUtil, 'getMediaServer').returns('linus');
13282
+
13283
+ try {
13284
+ await meeting.roapMessageReceived(fakeMessage);
13285
+
13286
+ assert.fail('Expected MultistreamNotSupportedError to be thrown');
13287
+ } catch(e) {
13288
+ assert.isTrue(e instanceof MultistreamNotSupportedError);
13289
+ }
13290
+
13291
+ assert.notCalled(meeting.mediaProperties.webrtcMediaConnection.roapMessageReceived);
13292
+ });
13293
+
13294
+ it('does not call getMediaServer for a roap message other than ANSWER', async () => {
13295
+ const fakeMessage = {messageType: 'ERROR', sdp: 'fake sdp'};
13296
+
13297
+ meeting.isMultistream = true;
13298
+ meeting.mediaProperties.webrtcMediaConnection = {
13299
+ roapMessageReceived: sinon.stub(),
13300
+ };
13301
+ meeting.mediaProperties.webrtcMediaConnection.mediaServer = 'linus';
13302
+
13303
+ const getMediaServerStub = sinon.stub(MeetingsUtil, 'getMediaServer').returns('something');
13304
+
13305
+ meeting.roapMessageReceived(fakeMessage);
13306
+
13307
+ assert.calledOnceWithExactly(
13308
+ meeting.mediaProperties.webrtcMediaConnection.roapMessageReceived,
13309
+ fakeMessage
13310
+ );
13311
+ assert.notCalled(getMediaServerStub);
13312
+ assert.equal(meeting.mediaProperties.webrtcMediaConnection.mediaServer, 'linus'); // check that it hasn't been overwritten
13313
+ });
13314
+ });
13315
+
13316
+ describe('#verifyRegistrationId', () => {
13317
+ it('calls fetchMeetingInfo() with the passed registrationId and captcha code', async () => {
13318
+ // simulate successful case
13319
+ meeting.fetchMeetingInfo = sinon.stub().resolves();
13320
+ const result = await meeting.verifyRegistrationId('registrationId', 'captcha id');
13321
+
13322
+ assert(Metrics.sendBehavioralMetric.calledOnce);
13323
+ assert.calledWith(
13324
+ Metrics.sendBehavioralMetric,
13325
+ BEHAVIORAL_METRICS.VERIFY_REGISTRATION_ID_SUCCESS
13326
+ );
13327
+ assert.equal(result.isRegistrationIdValid, true);
13328
+ assert.equal(result.requiredCaptcha, null);
13329
+ assert.equal(result.failureReason, MEETING_INFO_FAILURE_REASON.NONE);
13330
+ assert.calledWith(meeting.fetchMeetingInfo, {
13331
+ registrationId: 'registrationId',
13332
+ captchaCode: 'captcha id',
13333
+ sendCAevents: false,
13334
+ });
13335
+ });
13336
+ it('handles registrationIdError returned by fetchMeetingInfo', async () => {
13337
+ meeting.fetchMeetingInfo = sinon.stub().callsFake(() => {
13338
+ meeting.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_REGISTRATIONID;
13339
+
13340
+ return Promise.reject(new JoinWebinarError());
13341
+ });
13342
+ const result = await meeting.verifyRegistrationId('registrationId', 'captcha id');
13343
+
13344
+ assert.equal(result.isRegistrationIdValid, false);
13345
+ assert.equal(result.requiredCaptcha, null);
13346
+ assert.equal(result.failureReason, MEETING_INFO_FAILURE_REASON.WRONG_REGISTRATION_ID);
13347
+ });
13348
+ it('handles CaptchaError returned by fetchMeetingInfo', async () => {
13349
+ const FAKE_CAPTCHA = {captchaId: 'some catcha id...'};
13350
+
13351
+ meeting.fetchMeetingInfo = sinon.stub().callsFake(() => {
13352
+ meeting.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA;
13353
+ meeting.requiredCaptcha = FAKE_CAPTCHA;
13354
+
13355
+ return Promise.reject(new CaptchaError());
13356
+ });
13357
+ const result = await meeting.verifyRegistrationId('registrationId', 'captcha id');
13358
+
13359
+ assert.equal(result.isRegistrationIdValid, false);
13360
+ assert.deepEqual(result.requiredCaptcha, FAKE_CAPTCHA);
13361
+ assert.equal(result.failureReason, MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA);
13362
+ });
12619
13363
  });
12620
13364
  });