@webex/plugin-meetings 3.0.0-beta.15 → 3.0.0-beta.151

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 (480) hide show
  1. package/README.md +45 -1
  2. package/dist/annotation/annotation.types.js +7 -0
  3. package/dist/annotation/annotation.types.js.map +1 -0
  4. package/dist/annotation/constants.js +49 -0
  5. package/dist/annotation/constants.js.map +1 -0
  6. package/dist/annotation/index.js +359 -0
  7. package/dist/annotation/index.js.map +1 -0
  8. package/dist/breakouts/breakout.js +193 -0
  9. package/dist/breakouts/breakout.js.map +1 -0
  10. package/dist/breakouts/collection.js +23 -0
  11. package/dist/breakouts/collection.js.map +1 -0
  12. package/dist/breakouts/edit-lock-error.js +52 -0
  13. package/dist/breakouts/edit-lock-error.js.map +1 -0
  14. package/dist/breakouts/events.js +43 -0
  15. package/dist/breakouts/events.js.map +1 -0
  16. package/dist/breakouts/index.js +1046 -0
  17. package/dist/breakouts/index.js.map +1 -0
  18. package/dist/breakouts/request.js +78 -0
  19. package/dist/breakouts/request.js.map +1 -0
  20. package/dist/breakouts/utils.js +67 -0
  21. package/dist/breakouts/utils.js.map +1 -0
  22. package/dist/common/browser-detection.js +1 -21
  23. package/dist/common/browser-detection.js.map +1 -1
  24. package/dist/common/collection.js +5 -20
  25. package/dist/common/collection.js.map +1 -1
  26. package/dist/common/config.js +0 -7
  27. package/dist/common/config.js.map +1 -1
  28. package/dist/common/errors/captcha-error.js +0 -21
  29. package/dist/common/errors/captcha-error.js.map +1 -1
  30. package/dist/common/errors/intent-to-join.js +0 -21
  31. package/dist/common/errors/intent-to-join.js.map +1 -1
  32. package/dist/common/errors/join-meeting.js +0 -21
  33. package/dist/common/errors/join-meeting.js.map +1 -1
  34. package/dist/common/errors/media.js +0 -21
  35. package/dist/common/errors/media.js.map +1 -1
  36. package/dist/common/errors/parameter.js +0 -28
  37. package/dist/common/errors/parameter.js.map +1 -1
  38. package/dist/common/errors/password-error.js +0 -21
  39. package/dist/common/errors/password-error.js.map +1 -1
  40. package/dist/common/errors/permission.js +0 -21
  41. package/dist/common/errors/permission.js.map +1 -1
  42. package/dist/common/errors/reconnection-in-progress.js +0 -17
  43. package/dist/common/errors/reconnection-in-progress.js.map +1 -1
  44. package/dist/common/errors/reconnection.js +0 -21
  45. package/dist/common/errors/reconnection.js.map +1 -1
  46. package/dist/common/errors/stats.js +0 -21
  47. package/dist/common/errors/stats.js.map +1 -1
  48. package/dist/common/errors/webex-errors.js +9 -43
  49. package/dist/common/errors/webex-errors.js.map +1 -1
  50. package/dist/common/errors/webex-meetings-error.js +1 -24
  51. package/dist/common/errors/webex-meetings-error.js.map +1 -1
  52. package/dist/common/events/events-scope.js +0 -22
  53. package/dist/common/events/events-scope.js.map +1 -1
  54. package/dist/common/events/events.js +0 -23
  55. package/dist/common/events/events.js.map +1 -1
  56. package/dist/common/events/trigger-proxy.js +0 -12
  57. package/dist/common/events/trigger-proxy.js.map +1 -1
  58. package/dist/common/events/util.js +0 -15
  59. package/dist/common/events/util.js.map +1 -1
  60. package/dist/common/logs/logger-config.js +0 -4
  61. package/dist/common/logs/logger-config.js.map +1 -1
  62. package/dist/common/logs/logger-proxy.js +1 -8
  63. package/dist/common/logs/logger-proxy.js.map +1 -1
  64. package/dist/common/logs/request.js +35 -61
  65. package/dist/common/logs/request.js.map +1 -1
  66. package/dist/common/queue.js +4 -14
  67. package/dist/common/queue.js.map +1 -1
  68. package/dist/config.js +7 -13
  69. package/dist/config.js.map +1 -1
  70. package/dist/constants.js +208 -64
  71. package/dist/constants.js.map +1 -1
  72. package/dist/controls-options-manager/constants.js +14 -0
  73. package/dist/controls-options-manager/constants.js.map +1 -0
  74. package/dist/controls-options-manager/enums.js +27 -0
  75. package/dist/controls-options-manager/enums.js.map +1 -0
  76. package/dist/controls-options-manager/index.js +297 -0
  77. package/dist/controls-options-manager/index.js.map +1 -0
  78. package/dist/controls-options-manager/types.js +7 -0
  79. package/dist/controls-options-manager/types.js.map +1 -0
  80. package/dist/controls-options-manager/util.js +300 -0
  81. package/dist/controls-options-manager/util.js.map +1 -0
  82. package/dist/index.js +78 -17
  83. package/dist/index.js.map +1 -1
  84. package/dist/locus-info/controlsUtils.js +100 -29
  85. package/dist/locus-info/controlsUtils.js.map +1 -1
  86. package/dist/locus-info/embeddedAppsUtils.js +3 -26
  87. package/dist/locus-info/embeddedAppsUtils.js.map +1 -1
  88. package/dist/locus-info/fullState.js +0 -15
  89. package/dist/locus-info/fullState.js.map +1 -1
  90. package/dist/locus-info/hostUtils.js +4 -12
  91. package/dist/locus-info/hostUtils.js.map +1 -1
  92. package/dist/locus-info/index.js +387 -208
  93. package/dist/locus-info/index.js.map +1 -1
  94. package/dist/locus-info/infoUtils.js +0 -38
  95. package/dist/locus-info/infoUtils.js.map +1 -1
  96. package/dist/locus-info/mediaSharesUtils.js +54 -38
  97. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  98. package/dist/locus-info/parser.js +90 -126
  99. package/dist/locus-info/parser.js.map +1 -1
  100. package/dist/locus-info/selfUtils.js +93 -92
  101. package/dist/locus-info/selfUtils.js.map +1 -1
  102. package/dist/media/index.js +70 -219
  103. package/dist/media/index.js.map +1 -1
  104. package/dist/media/properties.js +74 -198
  105. package/dist/media/properties.js.map +1 -1
  106. package/dist/media/util.js +1 -8
  107. package/dist/media/util.js.map +1 -1
  108. package/dist/mediaQualityMetrics/config.js +505 -495
  109. package/dist/mediaQualityMetrics/config.js.map +1 -1
  110. package/dist/meeting/in-meeting-actions.js +79 -14
  111. package/dist/meeting/in-meeting-actions.js.map +1 -1
  112. package/dist/meeting/index.js +2685 -3324
  113. package/dist/meeting/index.js.map +1 -1
  114. package/dist/meeting/locusMediaRequest.js +291 -0
  115. package/dist/meeting/locusMediaRequest.js.map +1 -0
  116. package/dist/meeting/muteState.js +243 -185
  117. package/dist/meeting/muteState.js.map +1 -1
  118. package/dist/meeting/request.js +296 -342
  119. package/dist/meeting/request.js.map +1 -1
  120. package/dist/meeting/request.type.js +0 -1
  121. package/dist/meeting/request.type.js.map +1 -1
  122. package/dist/meeting/state.js +16 -26
  123. package/dist/meeting/state.js.map +1 -1
  124. package/dist/meeting/util.js +446 -585
  125. package/dist/meeting/util.js.map +1 -1
  126. package/dist/meeting-info/collection.js +3 -25
  127. package/dist/meeting-info/collection.js.map +1 -1
  128. package/dist/meeting-info/index.js +8 -31
  129. package/dist/meeting-info/index.js.map +1 -1
  130. package/dist/meeting-info/meeting-info-v2.js +261 -242
  131. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  132. package/dist/meeting-info/request.js +1 -16
  133. package/dist/meeting-info/request.js.map +1 -1
  134. package/dist/meeting-info/util.js +98 -183
  135. package/dist/meeting-info/util.js.map +1 -1
  136. package/dist/meeting-info/utilv2.js +156 -232
  137. package/dist/meeting-info/utilv2.js.map +1 -1
  138. package/dist/meetings/collection.js +24 -20
  139. package/dist/meetings/collection.js.map +1 -1
  140. package/dist/meetings/index.js +526 -372
  141. package/dist/meetings/index.js.map +1 -1
  142. package/dist/meetings/meetings.types.js +7 -0
  143. package/dist/meetings/meetings.types.js.map +1 -0
  144. package/dist/meetings/request.js +21 -40
  145. package/dist/meetings/request.js.map +1 -1
  146. package/dist/meetings/util.js +172 -141
  147. package/dist/meetings/util.js.map +1 -1
  148. package/dist/member/index.js +58 -57
  149. package/dist/member/index.js.map +1 -1
  150. package/dist/member/types.js +15 -0
  151. package/dist/member/types.js.map +1 -0
  152. package/dist/member/util.js +101 -69
  153. package/dist/member/util.js.map +1 -1
  154. package/dist/members/collection.js +12 -12
  155. package/dist/members/collection.js.map +1 -1
  156. package/dist/members/index.js +123 -162
  157. package/dist/members/index.js.map +1 -1
  158. package/dist/members/request.js +120 -85
  159. package/dist/members/request.js.map +1 -1
  160. package/dist/members/types.js +15 -0
  161. package/dist/members/types.js.map +1 -0
  162. package/dist/members/util.js +314 -260
  163. package/dist/members/util.js.map +1 -1
  164. package/dist/metrics/config.js +50 -16
  165. package/dist/metrics/config.js.map +1 -1
  166. package/dist/metrics/constants.js +4 -7
  167. package/dist/metrics/constants.js.map +1 -1
  168. package/dist/metrics/index.js +75 -147
  169. package/dist/metrics/index.js.map +1 -1
  170. package/dist/multistream/mediaRequestManager.js +170 -50
  171. package/dist/multistream/mediaRequestManager.js.map +1 -1
  172. package/dist/multistream/receiveSlot.js +58 -65
  173. package/dist/multistream/receiveSlot.js.map +1 -1
  174. package/dist/multistream/receiveSlotManager.js +73 -94
  175. package/dist/multistream/receiveSlotManager.js.map +1 -1
  176. package/dist/multistream/remoteMedia.js +55 -74
  177. package/dist/multistream/remoteMedia.js.map +1 -1
  178. package/dist/multistream/remoteMediaGroup.js +66 -43
  179. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  180. package/dist/multistream/remoteMediaManager.js +502 -442
  181. package/dist/multistream/remoteMediaManager.js.map +1 -1
  182. package/dist/networkQualityMonitor/index.js +24 -51
  183. package/dist/networkQualityMonitor/index.js.map +1 -1
  184. package/dist/personal-meeting-room/index.js +3 -38
  185. package/dist/personal-meeting-room/index.js.map +1 -1
  186. package/dist/personal-meeting-room/request.js +2 -33
  187. package/dist/personal-meeting-room/request.js.map +1 -1
  188. package/dist/personal-meeting-room/util.js +0 -13
  189. package/dist/personal-meeting-room/util.js.map +1 -1
  190. package/dist/reachability/index.js +190 -199
  191. package/dist/reachability/index.js.map +1 -1
  192. package/dist/reachability/request.js +14 -23
  193. package/dist/reachability/request.js.map +1 -1
  194. package/dist/reactions/constants.js +13 -0
  195. package/dist/reactions/constants.js.map +1 -0
  196. package/dist/reactions/reactions.js +2 -4
  197. package/dist/reactions/reactions.js.map +1 -1
  198. package/dist/reactions/reactions.type.js +18 -24
  199. package/dist/reactions/reactions.type.js.map +1 -1
  200. package/dist/reconnection-manager/index.js +356 -476
  201. package/dist/reconnection-manager/index.js.map +1 -1
  202. package/dist/recording-controller/enums.js +17 -0
  203. package/dist/recording-controller/enums.js.map +1 -0
  204. package/dist/recording-controller/index.js +343 -0
  205. package/dist/recording-controller/index.js.map +1 -0
  206. package/dist/recording-controller/util.js +63 -0
  207. package/dist/recording-controller/util.js.map +1 -0
  208. package/dist/roap/index.js +32 -75
  209. package/dist/roap/index.js.map +1 -1
  210. package/dist/roap/request.js +129 -136
  211. package/dist/roap/request.js.map +1 -1
  212. package/dist/roap/turnDiscovery.js +143 -103
  213. package/dist/roap/turnDiscovery.js.map +1 -1
  214. package/dist/statsAnalyzer/global.js +1 -95
  215. package/dist/statsAnalyzer/global.js.map +1 -1
  216. package/dist/statsAnalyzer/index.js +369 -462
  217. package/dist/statsAnalyzer/index.js.map +1 -1
  218. package/dist/statsAnalyzer/mqaUtil.js +144 -94
  219. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  220. package/dist/transcription/index.js +9 -44
  221. package/dist/transcription/index.js.map +1 -1
  222. package/dist/types/annotation/annotation.types.d.ts +43 -0
  223. package/dist/types/annotation/constants.d.ts +31 -0
  224. package/dist/types/annotation/index.d.ts +124 -0
  225. package/dist/types/breakouts/breakout.d.ts +8 -0
  226. package/dist/types/breakouts/collection.d.ts +5 -0
  227. package/dist/types/breakouts/edit-lock-error.d.ts +15 -0
  228. package/dist/types/breakouts/events.d.ts +2 -0
  229. package/dist/types/breakouts/index.d.ts +5 -0
  230. package/dist/types/breakouts/request.d.ts +22 -0
  231. package/dist/types/breakouts/utils.d.ts +15 -0
  232. package/dist/types/common/browser-detection.d.ts +9 -0
  233. package/dist/types/common/collection.d.ts +48 -0
  234. package/dist/types/common/config.d.ts +2 -0
  235. package/dist/types/common/errors/captcha-error.d.ts +15 -0
  236. package/dist/types/common/errors/intent-to-join.d.ts +16 -0
  237. package/dist/types/common/errors/join-meeting.d.ts +17 -0
  238. package/dist/types/common/errors/media.d.ts +15 -0
  239. package/dist/types/common/errors/parameter.d.ts +15 -0
  240. package/dist/types/common/errors/password-error.d.ts +15 -0
  241. package/dist/types/common/errors/permission.d.ts +14 -0
  242. package/dist/types/common/errors/reconnection-in-progress.d.ts +9 -0
  243. package/dist/types/common/errors/reconnection.d.ts +15 -0
  244. package/dist/types/common/errors/stats.d.ts +15 -0
  245. package/dist/types/common/errors/webex-errors.d.ts +69 -0
  246. package/dist/types/common/errors/webex-meetings-error.d.ts +20 -0
  247. package/dist/types/common/events/events-scope.d.ts +17 -0
  248. package/dist/types/common/events/events.d.ts +12 -0
  249. package/dist/types/common/events/trigger-proxy.d.ts +2 -0
  250. package/dist/types/common/events/util.d.ts +2 -0
  251. package/dist/types/common/logs/logger-config.d.ts +2 -0
  252. package/dist/types/common/logs/logger-proxy.d.ts +2 -0
  253. package/dist/types/common/logs/request.d.ts +34 -0
  254. package/dist/types/common/queue.d.ts +32 -0
  255. package/dist/types/config.d.ts +72 -0
  256. package/dist/types/constants.d.ts +978 -0
  257. package/dist/types/controls-options-manager/constants.d.ts +4 -0
  258. package/dist/types/controls-options-manager/enums.d.ts +15 -0
  259. package/dist/types/controls-options-manager/index.d.ts +136 -0
  260. package/dist/types/controls-options-manager/types.d.ts +43 -0
  261. package/dist/types/controls-options-manager/util.d.ts +1 -0
  262. package/dist/types/index.d.ts +7 -0
  263. package/dist/types/locus-info/controlsUtils.d.ts +2 -0
  264. package/dist/types/locus-info/embeddedAppsUtils.d.ts +2 -0
  265. package/dist/types/locus-info/fullState.d.ts +2 -0
  266. package/dist/types/locus-info/hostUtils.d.ts +2 -0
  267. package/dist/types/locus-info/index.d.ts +315 -0
  268. package/dist/types/locus-info/infoUtils.d.ts +2 -0
  269. package/dist/types/locus-info/mediaSharesUtils.d.ts +2 -0
  270. package/dist/types/locus-info/parser.d.ts +212 -0
  271. package/dist/types/locus-info/selfUtils.d.ts +2 -0
  272. package/dist/types/media/index.d.ts +34 -0
  273. package/dist/types/media/properties.d.ts +86 -0
  274. package/dist/types/media/util.d.ts +2 -0
  275. package/dist/types/mediaQualityMetrics/config.d.ts +365 -0
  276. package/dist/types/meeting/in-meeting-actions.d.ts +149 -0
  277. package/dist/types/meeting/index.d.ts +1509 -0
  278. package/dist/types/meeting/locusMediaRequest.d.ts +70 -0
  279. package/dist/types/meeting/muteState.d.ts +184 -0
  280. package/dist/types/meeting/request.d.ts +270 -0
  281. package/dist/types/meeting/request.type.d.ts +11 -0
  282. package/dist/types/meeting/state.d.ts +9 -0
  283. package/dist/types/meeting/util.d.ts +75 -0
  284. package/dist/types/meeting-info/collection.d.ts +20 -0
  285. package/dist/types/meeting-info/index.d.ts +57 -0
  286. package/dist/types/meeting-info/meeting-info-v2.d.ts +122 -0
  287. package/dist/types/meeting-info/request.d.ts +22 -0
  288. package/dist/types/meeting-info/util.d.ts +2 -0
  289. package/dist/types/meeting-info/utilv2.d.ts +2 -0
  290. package/dist/types/meetings/collection.d.ts +31 -0
  291. package/dist/types/meetings/index.d.ts +364 -0
  292. package/dist/types/meetings/meetings.types.d.ts +4 -0
  293. package/dist/types/meetings/request.d.ts +27 -0
  294. package/dist/types/meetings/util.d.ts +18 -0
  295. package/dist/types/member/index.d.ts +157 -0
  296. package/dist/types/member/types.d.ts +21 -0
  297. package/dist/types/member/util.d.ts +2 -0
  298. package/dist/types/members/collection.d.ts +29 -0
  299. package/dist/types/members/index.d.ts +353 -0
  300. package/dist/types/members/request.d.ts +114 -0
  301. package/dist/types/members/types.d.ts +24 -0
  302. package/dist/types/members/util.d.ts +210 -0
  303. package/dist/types/metrics/config.d.ts +195 -0
  304. package/dist/types/metrics/constants.d.ts +55 -0
  305. package/dist/types/metrics/index.d.ts +169 -0
  306. package/dist/types/multistream/mediaRequestManager.d.ts +104 -0
  307. package/dist/types/multistream/receiveSlot.d.ts +68 -0
  308. package/dist/types/multistream/receiveSlotManager.d.ts +56 -0
  309. package/dist/types/multistream/remoteMedia.d.ts +72 -0
  310. package/dist/types/multistream/remoteMediaGroup.d.ts +47 -0
  311. package/dist/types/multistream/remoteMediaManager.d.ts +277 -0
  312. package/dist/types/networkQualityMonitor/index.d.ts +70 -0
  313. package/dist/types/personal-meeting-room/index.d.ts +47 -0
  314. package/dist/types/personal-meeting-room/request.d.ts +14 -0
  315. package/dist/types/personal-meeting-room/util.d.ts +2 -0
  316. package/dist/types/reachability/index.d.ts +152 -0
  317. package/dist/types/reachability/request.d.ts +37 -0
  318. package/dist/types/reactions/constants.d.ts +3 -0
  319. package/dist/types/reactions/reactions.d.ts +4 -0
  320. package/dist/types/reactions/reactions.type.d.ts +52 -0
  321. package/dist/types/reconnection-manager/index.d.ts +126 -0
  322. package/dist/types/recording-controller/enums.d.ts +7 -0
  323. package/dist/types/recording-controller/index.d.ts +193 -0
  324. package/dist/types/recording-controller/util.d.ts +13 -0
  325. package/dist/types/roap/index.d.ts +77 -0
  326. package/dist/types/roap/request.d.ts +36 -0
  327. package/dist/types/roap/turnDiscovery.d.ts +91 -0
  328. package/dist/types/statsAnalyzer/global.d.ts +36 -0
  329. package/dist/types/statsAnalyzer/index.d.ts +200 -0
  330. package/dist/types/statsAnalyzer/mqaUtil.d.ts +24 -0
  331. package/dist/types/transcription/index.d.ts +64 -0
  332. package/package.json +28 -21
  333. package/src/annotation/annotation.types.ts +52 -0
  334. package/src/annotation/constants.ts +36 -0
  335. package/src/annotation/index.ts +343 -0
  336. package/src/breakouts/README.md +220 -0
  337. package/src/breakouts/breakout.ts +163 -0
  338. package/src/breakouts/collection.ts +19 -0
  339. package/src/breakouts/edit-lock-error.ts +25 -0
  340. package/src/breakouts/events.ts +37 -0
  341. package/src/breakouts/index.ts +921 -0
  342. package/src/breakouts/request.ts +55 -0
  343. package/src/breakouts/utils.ts +57 -0
  344. package/src/common/errors/webex-errors.ts +6 -2
  345. package/src/common/logs/logger-proxy.ts +1 -1
  346. package/src/config.ts +5 -7
  347. package/src/constants.ts +155 -20
  348. package/src/controls-options-manager/constants.ts +5 -0
  349. package/src/controls-options-manager/enums.ts +18 -0
  350. package/src/controls-options-manager/index.ts +278 -0
  351. package/src/controls-options-manager/types.ts +59 -0
  352. package/src/controls-options-manager/util.ts +286 -0
  353. package/src/index.ts +34 -0
  354. package/src/locus-info/controlsUtils.ts +108 -0
  355. package/src/locus-info/index.ts +310 -21
  356. package/src/locus-info/mediaSharesUtils.ts +48 -0
  357. package/src/locus-info/parser.ts +2 -1
  358. package/src/locus-info/selfUtils.ts +71 -1
  359. package/src/media/index.ts +70 -142
  360. package/src/media/properties.ts +41 -104
  361. package/src/mediaQualityMetrics/config.ts +379 -377
  362. package/src/meeting/in-meeting-actions.ts +156 -0
  363. package/src/meeting/index.ts +1730 -1768
  364. package/src/meeting/locusMediaRequest.ts +309 -0
  365. package/src/meeting/muteState.ts +228 -132
  366. package/src/meeting/request.ts +100 -91
  367. package/src/meeting/request.type.ts +2 -0
  368. package/src/meeting/util.ts +421 -421
  369. package/src/meeting-info/meeting-info-v2.ts +134 -13
  370. package/src/meeting-info/utilv2.ts +13 -3
  371. package/src/meetings/collection.ts +20 -0
  372. package/src/meetings/index.ts +375 -83
  373. package/src/meetings/meetings.types.ts +9 -0
  374. package/src/meetings/request.ts +3 -1
  375. package/src/meetings/util.ts +103 -4
  376. package/src/member/index.ts +40 -0
  377. package/src/member/types.ts +24 -0
  378. package/src/member/util.ts +81 -1
  379. package/src/members/collection.ts +8 -0
  380. package/src/members/index.ts +108 -6
  381. package/src/members/request.ts +98 -17
  382. package/src/members/types.ts +28 -0
  383. package/src/members/util.ts +319 -240
  384. package/src/metrics/config.ts +49 -10
  385. package/src/metrics/constants.ts +2 -4
  386. package/src/metrics/index.ts +43 -27
  387. package/src/multistream/mediaRequestManager.ts +210 -45
  388. package/src/multistream/receiveSlot.ts +68 -26
  389. package/src/multistream/receiveSlotManager.ts +61 -38
  390. package/src/multistream/remoteMedia.ts +29 -3
  391. package/src/multistream/remoteMediaGroup.ts +61 -2
  392. package/src/multistream/remoteMediaManager.ts +260 -66
  393. package/src/networkQualityMonitor/index.ts +6 -6
  394. package/src/reachability/index.ts +75 -25
  395. package/src/reachability/request.ts +10 -5
  396. package/src/reactions/constants.ts +4 -0
  397. package/src/reactions/reactions.ts +4 -4
  398. package/src/reactions/reactions.type.ts +28 -3
  399. package/src/reconnection-manager/index.ts +53 -32
  400. package/src/recording-controller/enums.ts +8 -0
  401. package/src/recording-controller/index.ts +315 -0
  402. package/src/recording-controller/util.ts +58 -0
  403. package/src/roap/index.ts +21 -30
  404. package/src/roap/request.ts +51 -52
  405. package/src/roap/turnDiscovery.ts +51 -27
  406. package/src/statsAnalyzer/global.ts +1 -94
  407. package/src/statsAnalyzer/index.ts +380 -390
  408. package/src/statsAnalyzer/mqaUtil.ts +106 -99
  409. package/test/integration/spec/converged-space-meetings.js +233 -0
  410. package/test/integration/spec/journey.js +332 -255
  411. package/test/integration/spec/space-meeting.js +78 -5
  412. package/test/integration/spec/transcription.js +1 -1
  413. package/test/unit/spec/annotation/index.ts +436 -0
  414. package/test/unit/spec/breakouts/breakout.ts +203 -0
  415. package/test/unit/spec/breakouts/collection.ts +15 -0
  416. package/test/unit/spec/breakouts/edit-lock-error.ts +30 -0
  417. package/test/unit/spec/breakouts/events.ts +77 -0
  418. package/test/unit/spec/breakouts/index.ts +1790 -0
  419. package/test/unit/spec/breakouts/request.ts +104 -0
  420. package/test/unit/spec/breakouts/utils.js +72 -0
  421. package/test/unit/spec/controls-options-manager/index.js +287 -0
  422. package/test/unit/spec/controls-options-manager/util.js +518 -0
  423. package/test/unit/spec/fixture/locus.js +1 -0
  424. package/test/unit/spec/locus-info/controlsUtils.js +303 -30
  425. package/test/unit/spec/locus-info/index.js +615 -4
  426. package/test/unit/spec/locus-info/mediaSharesUtils.ts +22 -0
  427. package/test/unit/spec/locus-info/selfConstant.js +38 -0
  428. package/test/unit/spec/locus-info/selfUtils.js +200 -0
  429. package/test/unit/spec/media/index.ts +118 -22
  430. package/test/unit/spec/media/properties.ts +9 -9
  431. package/test/unit/spec/meeting/in-meeting-actions.ts +76 -0
  432. package/test/unit/spec/meeting/index.js +2394 -1381
  433. package/test/unit/spec/meeting/locusMediaRequest.ts +436 -0
  434. package/test/unit/spec/meeting/muteState.js +370 -208
  435. package/test/unit/spec/meeting/request.js +354 -42
  436. package/test/unit/spec/meeting/utils.js +268 -156
  437. package/test/unit/spec/meeting-info/meetinginfov2.js +383 -5
  438. package/test/unit/spec/meeting-info/utilv2.js +21 -0
  439. package/test/unit/spec/meetings/collection.js +14 -0
  440. package/test/unit/spec/meetings/index.js +842 -128
  441. package/test/unit/spec/meetings/utils.js +206 -2
  442. package/test/unit/spec/member/index.js +24 -0
  443. package/test/unit/spec/member/util.js +384 -32
  444. package/test/unit/spec/members/index.js +320 -1
  445. package/test/unit/spec/members/request.js +206 -27
  446. package/test/unit/spec/members/utils.js +184 -0
  447. package/test/unit/spec/metrics/index.js +98 -0
  448. package/test/unit/spec/multistream/mediaRequestManager.ts +676 -105
  449. package/test/unit/spec/multistream/receiveSlot.ts +77 -18
  450. package/test/unit/spec/multistream/receiveSlotManager.ts +69 -39
  451. package/test/unit/spec/multistream/remoteMedia.ts +32 -2
  452. package/test/unit/spec/multistream/remoteMediaGroup.ts +271 -5
  453. package/test/unit/spec/multistream/remoteMediaManager.ts +672 -65
  454. package/test/unit/spec/networkQualityMonitor/index.js +4 -4
  455. package/test/unit/spec/reachability/index.ts +176 -25
  456. package/test/unit/spec/reachability/request.js +66 -0
  457. package/test/unit/spec/reconnection-manager/index.js +46 -13
  458. package/test/unit/spec/recording-controller/index.js +231 -0
  459. package/test/unit/spec/recording-controller/util.js +102 -0
  460. package/test/unit/spec/roap/index.ts +21 -51
  461. package/test/unit/spec/roap/request.ts +187 -0
  462. package/test/unit/spec/roap/turnDiscovery.ts +73 -34
  463. package/test/unit/spec/stats-analyzer/index.js +94 -43
  464. package/test/utils/constants.js +9 -0
  465. package/test/utils/integrationTestUtils.js +46 -0
  466. package/test/utils/testUtils.js +0 -45
  467. package/test/utils/webex-config.js +4 -0
  468. package/test/utils/webex-test-users.js +7 -3
  469. package/tsconfig.json +6 -0
  470. package/dist/media/internal-media-core-wrapper.js +0 -22
  471. package/dist/media/internal-media-core-wrapper.js.map +0 -1
  472. package/dist/meeting/effectsState.js +0 -334
  473. package/dist/meeting/effectsState.js.map +0 -1
  474. package/dist/multistream/multistreamMedia.js +0 -117
  475. package/dist/multistream/multistreamMedia.js.map +0 -1
  476. package/src/index.js +0 -15
  477. package/src/media/internal-media-core-wrapper.ts +0 -9
  478. package/src/meeting/effectsState.ts +0 -211
  479. package/src/multistream/multistreamMedia.ts +0 -93
  480. package/test/unit/spec/meeting/effectsState.js +0 -281
@@ -2,14 +2,16 @@
2
2
  * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
3
  */
4
4
  import 'jsdom-global/register';
5
- import {cloneDeep, isEqual} from 'lodash';
5
+ import {cloneDeep, forEach, isEqual} from 'lodash';
6
6
  import sinon from 'sinon';
7
+ import * as internalMediaModule from '@webex/internal-media-core';
7
8
  import StateMachine from 'javascript-state-machine';
8
9
  import uuid from 'uuid';
9
10
  import {assert} from '@webex/test-helper-chai';
10
- import {Credentials} from '@webex/webex-core';
11
+ import {Credentials, Token, WebexPlugin} from '@webex/webex-core';
11
12
  import Support from '@webex/internal-plugin-support';
12
13
  import MockWebex from '@webex/test-helper-mock-webex';
14
+ import StaticConfig from '@webex/plugin-meetings/src/common/config';
13
15
  import {
14
16
  FLOOR_ACTION,
15
17
  SHARE_STATUS,
@@ -19,23 +21,42 @@ import {
19
21
  EVENT_TRIGGERS,
20
22
  _SIP_URI_,
21
23
  _MEETING_ID_,
24
+ MEETING_REMOVED_REASON,
22
25
  LOCUSINFO,
23
26
  PC_BAIL_TIMEOUT,
27
+ DISPLAY_HINTS,
24
28
  } from '@webex/plugin-meetings/src/constants';
25
- import {MediaConnection as MC} from '@webex/internal-media-core';
29
+ import * as InternalMediaCoreModule from '@webex/internal-media-core';
30
+ import {
31
+ ConnectionState,
32
+ Event,
33
+ Errors,
34
+ ErrorType,
35
+ RemoteTrackType,
36
+ MediaType,
37
+ } from '@webex/internal-media-core';
38
+ import {
39
+ LocalTrackEvents,
40
+ } from '@webex/media-helpers';
26
41
  import * as StatsAnalyzerModule from '@webex/plugin-meetings/src/statsAnalyzer';
42
+ import * as MuteStateModule from '@webex/plugin-meetings/src/meeting/muteState';
27
43
  import EventsScope from '@webex/plugin-meetings/src/common/events/events-scope';
28
44
  import Meetings, {CONSTANTS} from '@webex/plugin-meetings';
29
45
  import Meeting from '@webex/plugin-meetings/src/meeting';
30
46
  import Members from '@webex/plugin-meetings/src/members';
47
+ import * as MembersImport from '@webex/plugin-meetings/src/members';
31
48
  import Roap from '@webex/plugin-meetings/src/roap';
49
+ import RoapRequest from '@webex/plugin-meetings/src/roap/request';
32
50
  import MeetingRequest from '@webex/plugin-meetings/src/meeting/request';
51
+ import * as MeetingRequestImport from '@webex/plugin-meetings/src/meeting/request';
33
52
  import LocusInfo from '@webex/plugin-meetings/src/locus-info';
34
53
  import MediaProperties from '@webex/plugin-meetings/src/media/properties';
35
54
  import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
36
55
  import Media from '@webex/plugin-meetings/src/media/index';
37
56
  import ReconnectionManager from '@webex/plugin-meetings/src/reconnection-manager';
38
57
  import MediaUtil from '@webex/plugin-meetings/src/media/util';
58
+ import RecordingUtil from '@webex/plugin-meetings/src/recording-controller/util';
59
+ import ControlsOptionsUtil from '@webex/plugin-meetings/src/controls-options-manager/util';
39
60
  import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
40
61
  import LoggerConfig from '@webex/plugin-meetings/src/common/logs/logger-config';
41
62
  import TriggerProxy from '@webex/plugin-meetings/src/common/events/trigger-proxy';
@@ -43,8 +64,13 @@ import BrowserDetection from '@webex/plugin-meetings/src/common/browser-detectio
43
64
  import Metrics from '@webex/plugin-meetings/src/metrics';
44
65
  import {trigger, eventType} from '@webex/plugin-meetings/src/metrics/config';
45
66
  import BEHAVIORAL_METRICS from '@webex/plugin-meetings/src/metrics/constants';
46
- import {IceGatheringFailed} from '@webex/plugin-meetings/src/common/errors/webex-errors';
67
+ import {MediaRequestManager} from '@webex/plugin-meetings/src/multistream/mediaRequestManager';
68
+ import * as ReceiveSlotManagerModule from '@webex/plugin-meetings/src/multistream/receiveSlotManager';
47
69
 
70
+ import LLM from '@webex/internal-plugin-llm';
71
+ import Mercury from '@webex/internal-plugin-mercury';
72
+ import Breakouts from '@webex/plugin-meetings/src/breakouts';
73
+ import {REACTION_RELAY_TYPES} from '../../../../src/reactions/constants';
48
74
  import locus from '../fixture/locus';
49
75
  import {
50
76
  UserNotJoinedError,
@@ -56,15 +82,18 @@ import WebExMeetingsErrors from '../../../../src/common/errors/webex-meetings-er
56
82
  import ParameterError from '../../../../src/common/errors/parameter';
57
83
  import PasswordError from '../../../../src/common/errors/password-error';
58
84
  import CaptchaError from '../../../../src/common/errors/captcha-error';
85
+ import PermissionError from '../../../../src/common/errors/permission';
59
86
  import IntentToJoinError from '../../../../src/common/errors/intent-to-join';
60
87
  import DefaultSDKConfig from '../../../../src/config';
61
88
  import testUtils from '../../../utils/testUtils';
62
89
  import {
63
90
  MeetingInfoV2CaptchaError,
64
91
  MeetingInfoV2PasswordError,
92
+ MeetingInfoV2PolicyError,
65
93
  } from '../../../../src/meeting-info/meeting-info-v2';
94
+ import {ANNOTATION_POLICY} from "../../../../src/annotation/constants";
66
95
 
67
- const {getBrowserName} = BrowserDetection();
96
+ const {getBrowserName, getOSVersion} = BrowserDetection();
68
97
 
69
98
  // Non-stubbed function
70
99
  const {getDisplayMedia} = Media;
@@ -127,6 +156,15 @@ describe('plugin-meetings', () => {
127
156
  },
128
157
  });
129
158
 
159
+ Object.defineProperty(global.window.navigator, 'permissions', {
160
+ writable: true,
161
+ value: {
162
+ query: sinon.stub().callsFake(async (arg) => {
163
+ return {state: 'granted', name: arg.name};
164
+ }),
165
+ },
166
+ });
167
+
130
168
  Object.defineProperty(global.window, 'MediaStream', {
131
169
  writable: true,
132
170
  value: MediaStream,
@@ -148,6 +186,8 @@ describe('plugin-meetings', () => {
148
186
  let test3;
149
187
  let test4;
150
188
  let testDestination;
189
+ let membersSpy;
190
+ let meetingRequestSpy;
151
191
 
152
192
  beforeEach(() => {
153
193
  webex = new MockWebex({
@@ -155,10 +195,12 @@ describe('plugin-meetings', () => {
155
195
  meetings: Meetings,
156
196
  credentials: Credentials,
157
197
  support: Support,
198
+ llm: LLM,
199
+ mercury: Mercury,
158
200
  },
159
201
  config: {
160
202
  credentials: {
161
- client_id: 'mock-client-id',
203
+ client_id: 'mock-client-id'
162
204
  },
163
205
  meetings: {
164
206
  reconnection: {
@@ -168,6 +210,7 @@ describe('plugin-meetings', () => {
168
210
  metrics: {},
169
211
  stats: {},
170
212
  experimental: {enableUnifiedMeetings: true},
213
+ degradationPreferences: { maxMacroblocksLimit: 8192 },
171
214
  },
172
215
  metrics: {
173
216
  type: ['behavioral'],
@@ -179,11 +222,18 @@ describe('plugin-meetings', () => {
179
222
  webex.credentials.getOrgId = sinon.stub().returns('fake-org-id');
180
223
  webex.internal.metrics.submitClientMetrics = sinon.stub().returns(Promise.resolve());
181
224
  webex.meetings.uploadLogs = sinon.stub().returns(Promise.resolve());
225
+ webex.internal.llm.on = sinon.stub();
226
+ membersSpy = sinon.spy(MembersImport, 'default');
227
+ meetingRequestSpy = sinon.spy(MeetingRequestImport, 'default');
182
228
 
183
229
  TriggerProxy.trigger = sinon.stub().returns(true);
184
230
  Metrics.postEvent = sinon.stub();
185
231
  Metrics.initialSetup(null, webex);
186
- MediaUtil.createMediaStream = sinon.stub().returns(true);
232
+ MediaUtil.createMediaStream = sinon.stub().callsFake((tracks) => {
233
+ return {
234
+ getTracks: () => tracks,
235
+ };
236
+ });
187
237
 
188
238
  uuid1 = uuid.v4();
189
239
  uuid2 = uuid.v4();
@@ -228,6 +278,16 @@ describe('plugin-meetings', () => {
228
278
  assert.equal(meeting.deviceUrl, uuid3);
229
279
  assert.deepEqual(meeting.meetingInfo, {});
230
280
  assert.instanceOf(meeting.members, Members);
281
+ assert.calledOnceWithExactly(
282
+ membersSpy,
283
+ {
284
+ locusUrl: meeting.locusUrl,
285
+ receiveSlotManager: meeting.receiveSlotManager,
286
+ mediaRequestManagers: meeting.mediaRequestManagers,
287
+ meeting,
288
+ },
289
+ {parent: meeting.webex}
290
+ );
231
291
  assert.instanceOf(meeting.roap, Roap);
232
292
  assert.instanceOf(meeting.reconnectionManager, ReconnectionManager);
233
293
  assert.isNull(meeting.audio);
@@ -242,6 +302,13 @@ describe('plugin-meetings', () => {
242
302
  assert.isNull(meeting.hostId);
243
303
  assert.isNull(meeting.policy);
244
304
  assert.instanceOf(meeting.meetingRequest, MeetingRequest);
305
+ assert.calledOnceWithExactly(
306
+ meetingRequestSpy,
307
+ {
308
+ meeting,
309
+ },
310
+ {parent: meeting.webex}
311
+ );
245
312
  assert.instanceOf(meeting.locusInfo, LocusInfo);
246
313
  assert.equal(meeting.fetchMeetingInfoTimeoutId, undefined);
247
314
  assert.instanceOf(meeting.mediaProperties, MediaProperties);
@@ -250,6 +317,98 @@ describe('plugin-meetings', () => {
250
317
  assert.equal(meeting.meetingInfoFailureReason, undefined);
251
318
  assert.equal(meeting.destination, testDestination);
252
319
  assert.equal(meeting.destinationType, _MEETING_ID_);
320
+ assert.instanceOf(meeting.breakouts, Breakouts);
321
+ });
322
+ it('creates MediaRequestManager instances', () => {
323
+ assert.instanceOf(meeting.mediaRequestManagers.audio, MediaRequestManager);
324
+ assert.instanceOf(meeting.mediaRequestManagers.video, MediaRequestManager);
325
+ assert.instanceOf(meeting.mediaRequestManagers.screenShareAudio, MediaRequestManager);
326
+ assert.instanceOf(meeting.mediaRequestManagers.screenShareVideo, MediaRequestManager);
327
+ });
328
+
329
+ describe('creates ReceiveSlot manager instance', () => {
330
+ let mockReceiveSlotManagerCtor;
331
+ let providedCreateSlotCallback;
332
+ let providedFindMemberIdByCsiCallback;
333
+
334
+ beforeEach(() => {
335
+ mockReceiveSlotManagerCtor = sinon
336
+ .stub(ReceiveSlotManagerModule, 'ReceiveSlotManager')
337
+ .callsFake((createSlotCallback, findMemberIdByCsiCallback) => {
338
+ providedCreateSlotCallback = createSlotCallback;
339
+ providedFindMemberIdByCsiCallback = findMemberIdByCsiCallback;
340
+
341
+ return {updateMemberIds: sinon.stub()};
342
+ });
343
+
344
+ meeting = new Meeting(
345
+ {
346
+ userId: uuid1,
347
+ resource: uuid2,
348
+ deviceUrl: uuid3,
349
+ locus: {url: url1},
350
+ destination: testDestination,
351
+ destinationType: _MEETING_ID_,
352
+ },
353
+ {
354
+ parent: webex,
355
+ }
356
+ );
357
+
358
+ meeting.mediaProperties.webrtcMediaConnection = {createReceiveSlot: sinon.stub()};
359
+ });
360
+
361
+ it('calls ReceiveSlotManager constructor', () => {
362
+ assert.calledOnce(mockReceiveSlotManagerCtor);
363
+ assert.isDefined(providedCreateSlotCallback);
364
+ assert.isDefined(providedFindMemberIdByCsiCallback);
365
+ });
366
+
367
+ it('calls createReceiveSlot on the webrtc media connection in the createSlotCallback', async () => {
368
+ assert.isDefined(providedCreateSlotCallback);
369
+
370
+ await providedCreateSlotCallback(MediaType.VideoMain);
371
+
372
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.createReceiveSlot);
373
+ assert.calledWith(
374
+ meeting.mediaProperties.webrtcMediaConnection.createReceiveSlot,
375
+ MediaType.VideoMain
376
+ );
377
+ });
378
+
379
+ it('rejects createSlotCallback if there is no webrtc media connection', () => {
380
+ assert.isDefined(providedCreateSlotCallback);
381
+
382
+ meeting.mediaProperties.webrtcMediaConnection.createReceiveSlot.rejects({});
383
+
384
+ assert.isRejected(providedCreateSlotCallback(MediaType.VideoMain));
385
+ });
386
+
387
+ it('calls findMemberByCsi in findMemberIdByCsiCallback and returns the right value', () => {
388
+ assert.isDefined(providedFindMemberIdByCsiCallback);
389
+
390
+ const fakeMember = {id: 'aaa-bbb'};
391
+
392
+ sinon.stub(meeting.members, 'findMemberByCsi').returns(fakeMember);
393
+
394
+ const memberId = providedFindMemberIdByCsiCallback(123);
395
+
396
+ assert.calledOnce(meeting.members.findMemberByCsi);
397
+ assert.calledWith(meeting.members.findMemberByCsi, 123);
398
+ assert.equal(memberId, fakeMember.id);
399
+ });
400
+
401
+ it('returns undefined if findMemberByCsi does not find the member', () => {
402
+ assert.isDefined(providedFindMemberIdByCsiCallback);
403
+
404
+ sinon.stub(meeting.members, 'findMemberByCsi').returns(undefined);
405
+
406
+ const memberId = providedFindMemberIdByCsiCallback(123);
407
+
408
+ assert.calledOnce(meeting.members.findMemberByCsi);
409
+ assert.calledWith(meeting.members.findMemberByCsi, 123);
410
+ assert.equal(memberId, undefined);
411
+ });
253
412
  });
254
413
  });
255
414
  describe('#invite', () => {
@@ -299,6 +458,21 @@ describe('plugin-meetings', () => {
299
458
  assert.calledOnce(meeting.members.admitMembers);
300
459
  assert.calledWith(meeting.members.admitMembers, [uuid1]);
301
460
  });
461
+ it('should call from a breakout session if caller is in a breakout session', async () => {
462
+ const locusUrls = {
463
+ authorizingLocusUrl: 'authorizingLocusUrl',
464
+ mainLocusUrl: 'mainLocusUrl',
465
+ };
466
+ await meeting.admit([uuid1], locusUrls);
467
+ assert.calledOnce(meeting.members.admitMembers);
468
+ assert.calledWith(meeting.members.admitMembers, [uuid1], locusUrls);
469
+
470
+ meeting.breakouts.set('locusUrl', 'authorizingLocusUrl');
471
+ meeting.breakouts.set('mainLocusUrl', 'mainLocusUrl');
472
+ await meeting.admit([uuid1]);
473
+ const args = meeting.members.admitMembers.getCall(1).args;
474
+ assert.deepEqual(args, [[uuid1], locusUrls]);
475
+ });
302
476
  });
303
477
  describe('#getMembers', () => {
304
478
  it('should have #getMembers', () => {
@@ -310,325 +484,7 @@ describe('plugin-meetings', () => {
310
484
  assert.instanceOf(members, Members);
311
485
  });
312
486
  });
313
- describe('#isAudioMuted', () => {
314
- it('should have #isAudioMuted', () => {
315
- assert.exists(meeting.invite);
316
- });
317
- it('should get the audio muted status and return as a boolean', () => {
318
- const muted = meeting.isAudioMuted();
319
-
320
- assert.isNotOk(muted);
321
- });
322
- });
323
- describe('#isAudioSelf', () => {
324
- it('should have #isAudioSelf', () => {
325
- assert.exists(meeting.invite);
326
- });
327
- it('should get the audio self status and return as a boolean', () => {
328
- const self = meeting.isAudioSelf();
329
-
330
- assert.isNotOk(self);
331
- });
332
- });
333
- describe('#isVideoMuted', () => {
334
- it('should have #isVideoMuted', () => {
335
- assert.exists(meeting.isVideoMuted);
336
- });
337
- it('should get the video muted status and return as a boolean', () => {
338
- const muted = meeting.isVideoMuted();
339
-
340
- assert.isNotOk(muted);
341
- });
342
- });
343
- describe('#isVideoSelf', () => {
344
- it('should have #isVideoSelf', () => {
345
- assert.exists(meeting.invite);
346
- });
347
- it('should get the video self status and return as a boolean', () => {
348
- const self = meeting.isVideoSelf();
349
-
350
- assert.isNotOk(self);
351
- });
352
- });
353
- describe('#muteAudio', () => {
354
- it('should have #muteAudio', () => {
355
- assert.exists(meeting.muteAudio);
356
- });
357
- describe('before audio is defined', () => {
358
- it('should reject and return a promise', async () => {
359
- await meeting.muteAudio().catch((err) => {
360
- assert.instanceOf(err, UserNotJoinedError);
361
- });
362
- });
363
-
364
- it('should reject and return a promise', async () => {
365
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
366
- await meeting.muteAudio().catch((err) => {
367
- assert.instanceOf(err, NoMediaEstablishedYetError);
368
- });
369
- });
370
-
371
- it('should reject and return a promise', async () => {
372
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
373
- meeting.mediaId = 'mediaId';
374
- await meeting.muteAudio().catch((err) => {
375
- assert.instanceOf(err, ParameterError);
376
- });
377
- });
378
- });
379
- describe('after audio is defined', () => {
380
- let handleClientRequest;
381
-
382
- beforeEach(() => {
383
- handleClientRequest = sinon.stub().returns(Promise.resolve());
384
- meeting.audio = {handleClientRequest};
385
- });
386
-
387
- it('should return a promise resolution', async () => {
388
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
389
- meeting.mediaId = 'mediaId';
390
-
391
- const audio = meeting.muteAudio();
392
-
393
- assert.exists(audio.then);
394
- await audio;
395
- assert.calledOnce(handleClientRequest);
396
- assert.calledWith(handleClientRequest, meeting, true);
397
- });
398
- });
399
- });
400
- describe('#unmuteAudio', () => {
401
- it('should have #unmuteAudio', () => {
402
- assert.exists(meeting.unmuteAudio);
403
- });
404
- describe('before audio is defined', () => {
405
- it('should reject when user not joined', async () => {
406
- await meeting.unmuteAudio().catch((err) => {
407
- assert.instanceOf(err, UserNotJoinedError);
408
- });
409
- });
410
-
411
- it('should reject when no media is established yet ', async () => {
412
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
413
- await meeting.unmuteAudio().catch((err) => {
414
- assert.instanceOf(err, NoMediaEstablishedYetError);
415
- });
416
- });
417
-
418
- it('should reject when audio is not there or established', async () => {
419
- meeting.mediaId = 'mediaId';
420
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
421
- await meeting.unmuteAudio().catch((err) => {
422
- assert.instanceOf(err, ParameterError);
423
- });
424
- });
425
- });
426
- describe('after audio is defined', () => {
427
- let handleClientRequest;
428
-
429
- beforeEach(() => {
430
- handleClientRequest = sinon.stub().returns(Promise.resolve());
431
- meeting.mediaId = 'mediaId';
432
- meeting.audio = {handleClientRequest};
433
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
434
- });
435
-
436
- it('should return a promise resolution', async () => {
437
- meeting.audio = {handleClientRequest};
438
-
439
- const audio = meeting.unmuteAudio();
440
-
441
- assert.exists(audio.then);
442
- await audio;
443
- assert.calledOnce(handleClientRequest);
444
- assert.calledWith(handleClientRequest, meeting, false);
445
- });
446
- });
447
- });
448
- describe('BNR', () => {
449
- const fakeMediaTrack = () => ({
450
- id: Date.now().toString(),
451
- stop: () => {},
452
- readyState: 'live',
453
- enabled: true,
454
- getSettings: () => ({
455
- sampleRate: 48000,
456
- }),
457
- });
458
-
459
- beforeEach(() => {
460
- meeting.getMediaStreams = sinon.stub().returns(Promise.resolve());
461
- sinon.replace(meeting, 'addMedia', () => {
462
- sinon.stub(meeting.mediaProperties, 'audioTrack').value(fakeMediaTrack());
463
- sinon.stub(meeting.mediaProperties, 'mediaDirection').value({
464
- receiveAudio: true,
465
- });
466
- });
467
- });
468
- describe('#enableBNR', () => {
469
- it('should have #enableBnr', () => {
470
- assert.exists(meeting.enableBNR);
471
- });
472
-
473
- describe('before audio attached to meeting', () => {
474
- it('should throw no audio error', async () => {
475
- await meeting.enableBNR().catch((err) => {
476
- assert.equal(err.toString(), "Error: Meeting doesn't have an audioTrack attached");
477
- });
478
- });
479
- });
480
-
481
- describe('after audio attached to meeting', () => {
482
- let handleClientRequest;
483
-
484
- beforeEach(async () => {
485
- await meeting.getMediaStreams();
486
- await meeting.addMedia();
487
- });
488
-
489
- it('should throw error if meeting audio is muted', async () => {
490
- const handleClientRequest = (meeting, mute) => {
491
- meeting.mediaProperties.audioTrack.enabled = !mute;
492
-
493
- return Promise.resolve();
494
- };
495
- const isMuted = () => !meeting.mediaProperties.audioTrack.enabled;
496
-
497
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
498
- meeting.mediaId = 'mediaId';
499
- meeting.audio = {handleClientRequest, isMuted};
500
- await meeting.muteAudio();
501
- await meeting.enableBNR().catch((err) => {
502
- assert.equal(err.message, 'Cannot enable BNR while meeting is muted');
503
- });
504
- });
505
-
506
- it('should return true on enable bnr success', async () => {
507
- handleClientRequest = sinon.stub().returns(Promise.resolve(true));
508
- meeting.effects = {handleClientRequest};
509
- const response = await meeting.enableBNR();
510
-
511
- assert.equal(response, true);
512
- });
513
- });
514
- });
515
-
516
- describe('#disableBNR', () => {
517
- describe('before audio attached to meeting', () => {
518
- it('should have #disableBnr', () => {
519
- assert.exists(meeting.disableBNR);
520
- });
521
-
522
- it('should throw no audio error', async () => {
523
- await meeting.disableBNR().catch((err) => {
524
- assert.equal(err.toString(), "Error: Meeting doesn't have an audioTrack attached");
525
- });
526
- });
527
- });
528
- describe('after audio attached to meeting', () => {
529
- beforeEach(async () => {
530
- await meeting.getMediaStreams();
531
- await meeting.addMedia();
532
- });
533
-
534
- let handleClientRequest;
535
- let isBnrEnabled;
536
-
537
- it('should return true on disable bnr success', async () => {
538
- handleClientRequest = sinon.stub().returns(Promise.resolve(true));
539
- isBnrEnabled = sinon.stub().returns(Promise.resolve(true));
540
- meeting.effects = {handleClientRequest, isBnrEnabled};
541
- const response = await meeting.disableBNR();
542
-
543
- assert.equal(response, true);
544
- });
545
- });
546
- });
547
- });
548
- describe('#muteVideo', () => {
549
- it('should have #muteVideo', () => {
550
- assert.exists(meeting.muteVideo);
551
- });
552
- describe('before video is defined', () => {
553
- it('should reject when user not joined', async () => {
554
- await meeting.muteVideo().catch((err) => {
555
- assert.instanceOf(err, UserNotJoinedError);
556
- });
557
- });
558
-
559
- it('should reject when no media is established', async () => {
560
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
561
- await meeting.muteVideo().catch((err) => {
562
- assert.instanceOf(err, NoMediaEstablishedYetError);
563
- });
564
- });
565
-
566
- it('should reject when no video added or established', async () => {
567
- meeting.mediaId = 'mediaId';
568
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
569
- await meeting.muteVideo().catch((err) => {
570
- assert.instanceOf(err, ParameterError);
571
- });
572
- });
573
- });
574
- describe('after video is defined', () => {
575
- it('should return a promise resolution', async () => {
576
- const handleClientRequest = sinon.stub().returns(Promise.resolve());
577
-
578
- meeting.mediaId = 'mediaId';
579
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
580
- meeting.video = {handleClientRequest};
581
- const video = meeting.muteVideo();
582
-
583
- assert.exists(video.then);
584
- await video;
585
- assert.calledOnce(handleClientRequest);
586
- assert.calledWith(handleClientRequest, meeting, true);
587
- });
588
- });
589
- });
590
- describe('#unmuteVideo', () => {
591
- it('should have #unmuteVideo', () => {
592
- assert.exists(meeting.unmuteVideo);
593
- });
594
- describe('before video is defined', () => {
595
- it('should reject no user joined', async () => {
596
- await meeting.unmuteVideo().catch((err) => {
597
- assert.instanceOf(err, Error);
598
- });
599
- });
600
-
601
- it('should reject no media established', async () => {
602
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
603
- await meeting.unmuteVideo().catch((err) => {
604
- assert.instanceOf(err, Error);
605
- });
606
- });
607
-
608
- it('should reject when no video added or established', async () => {
609
- meeting.mediaId = 'mediaId';
610
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
611
- await meeting.unmuteVideo().catch((err) => {
612
- assert.instanceOf(err, Error);
613
- });
614
- });
615
- });
616
- describe('after video is defined', () => {
617
- it('should return a promise resolution', async () => {
618
- const handleClientRequest = sinon.stub().returns(Promise.resolve());
619
487
 
620
- meeting.mediaId = 'mediaId';
621
- meeting.locusInfo.parsedLocus = {self: {state: 'JOINED'}};
622
- meeting.video = {handleClientRequest};
623
- const video = meeting.unmuteVideo();
624
-
625
- assert.exists(video.then);
626
- await video;
627
- assert.calledOnce(handleClientRequest);
628
- assert.calledWith(handleClientRequest, meeting, false);
629
- });
630
- });
631
- });
632
488
  describe('#joinWithMedia', () => {
633
489
  it('should have #joinWithMedia', () => {
634
490
  assert.exists(meeting.joinWithMedia);
@@ -636,125 +492,21 @@ describe('plugin-meetings', () => {
636
492
  describe('resolution', () => {
637
493
  it('should success and return a promise', async () => {
638
494
  meeting.join = sinon.stub().returns(Promise.resolve(test1));
639
- meeting.getMediaStreams = sinon.stub().returns(Promise.resolve([test2, test3]));
640
495
  meeting.addMedia = sinon.stub().returns(Promise.resolve(test4));
641
- await meeting.joinWithMedia({});
496
+ const result = await meeting.joinWithMedia({});
642
497
  assert.calledOnce(meeting.join);
643
- assert.calledOnce(meeting.getMediaStreams);
498
+ assert.calledOnce(meeting.addMedia);
499
+ assert.deepEqual(result, {join: test1, media: test4});
644
500
  });
645
501
  });
646
502
  describe('rejection', () => {
647
503
  it('should error out and return a promise', async () => {
648
504
  meeting.join = sinon.stub().returns(Promise.reject());
649
- meeting.getMediaStreams = sinon.stub().returns(true);
650
505
  assert.isRejected(meeting.joinWithMedia({}));
651
506
  });
652
507
  });
653
508
  });
654
- describe('#getMediaStreams', () => {
655
- beforeEach(() => {
656
- sinon
657
- .stub(Media, 'getSupportedDevice')
658
- .callsFake((options) =>
659
- Promise.resolve({sendAudio: options.sendAudio, sendVideo: options.sendVideo})
660
- );
661
- sinon.stub(Media, 'getUserMedia').returns(Promise.resolve(['stream1', 'stream2']));
662
- });
663
- afterEach(() => {
664
- sinon.restore();
665
- });
666
- it('should have #getMediaStreams', () => {
667
- assert.exists(meeting.getMediaStreams);
668
- });
669
- it('should proxy Media getUserMedia, and return a promise', async () => {
670
- await meeting.getMediaStreams({sendAudio: true, sendVideo: true});
671
-
672
- assert.calledOnce(Media.getUserMedia);
673
- });
674
-
675
- it('uses the preferred video device if set', async () => {
676
- const videoDevice = 'video1';
677
- const mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
678
- const audioVideoSettings = {};
679
-
680
- sinon.stub(meeting.mediaProperties, 'videoDeviceId').value(videoDevice);
681
- sinon.stub(meeting.mediaProperties, 'localQualityLevel').value('480p');
682
- await meeting.getMediaStreams(mediaDirection, audioVideoSettings);
683
-
684
- assert.calledWith(
685
- Media.getUserMedia,
686
- {
687
- ...mediaDirection,
688
- isSharing: false,
689
- },
690
- {
691
- video: {
692
- width: {max: 640, ideal: 640},
693
- height: {max: 480, ideal: 480},
694
- deviceId: videoDevice,
695
- },
696
- }
697
- );
698
- });
699
- it('will set a new preferred video input device if passed in', async () => {
700
- // if audioVideo settings parameter specifies a new video device it
701
- // will store that device as the preferred video device.
702
- // Which is the case with meeting.updateVideo()
703
- const oldVideoDevice = 'video1';
704
- const newVideoDevice = 'video2';
705
- const mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
706
- const audioVideoSettings = {video: {deviceId: newVideoDevice}};
707
-
708
- sinon.stub(meeting.mediaProperties, 'videoDeviceId').value(oldVideoDevice);
709
- sinon.stub(meeting.mediaProperties, 'setVideoDeviceId');
710
-
711
- await meeting.getMediaStreams(mediaDirection, audioVideoSettings);
712
-
713
- assert.calledWith(meeting.mediaProperties.setVideoDeviceId, newVideoDevice);
714
- });
715
-
716
- it('uses the passed custom video resolution', async () => {
717
- const mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
718
- const customAudioVideoSettings = {
719
- video: {
720
- width: {
721
- max: 400,
722
- ideal: 400,
723
- },
724
- height: {
725
- max: 200,
726
- ideal: 200,
727
- },
728
- frameRate: {
729
- ideal: 15,
730
- max: 30,
731
- },
732
- facingMode: {
733
- ideal: 'user',
734
- },
735
- },
736
- };
737
-
738
- sinon.stub(meeting.mediaProperties, 'localQualityLevel').value('200p');
739
- await meeting.getMediaStreams(mediaDirection, customAudioVideoSettings);
740
-
741
- assert.calledWith(
742
- Media.getUserMedia,
743
- {
744
- ...mediaDirection,
745
- isSharing: false,
746
- },
747
- customAudioVideoSettings
748
- );
749
- });
750
- it('should not access camera if sendVideo is false ', async () => {
751
- await meeting.getMediaStreams({sendAudio: true, sendVideo: false});
752
509
 
753
- assert.calledOnce(Media.getUserMedia);
754
-
755
- assert.equal(Media.getUserMedia.args[0][0].sendVideo, false);
756
- });
757
- });
758
510
  describe('#isTranscriptionSupported', () => {
759
511
  it('should return false if the feature is not supported for the meeting', () => {
760
512
  meeting.locusInfo.controls = {transcribe: {transcribing: false}};
@@ -779,7 +531,7 @@ describe('plugin-meetings', () => {
779
531
  });
780
532
  });
781
533
 
782
- it('should throw error', async () => {
534
+ it("should throw error if request doesn't work", async () => {
783
535
  meeting.request = sinon.stub().returns(Promise.reject());
784
536
 
785
537
  try {
@@ -799,6 +551,64 @@ describe('plugin-meetings', () => {
799
551
  assert.calledOnce(meeting.transcription.closeSocket);
800
552
  });
801
553
  });
554
+ describe('#isReactionsSupported', () => {
555
+ it('should return false if the feature is not supported for the meeting', () => {
556
+ meeting.locusInfo.controls = {reactions: {enabled: false}};
557
+
558
+ assert.equal(meeting.isReactionsSupported(), false);
559
+ });
560
+ it('should return true if the feature is not supported for the meeting', () => {
561
+ meeting.locusInfo.controls = {reactions: {enabled: true}};
562
+
563
+ assert.equal(meeting.isReactionsSupported(), true);
564
+ });
565
+ });
566
+ describe('#processRelayEvent', () => {
567
+ it('should process a Reaction event type', () => {
568
+ meeting.isReactionsSupported = sinon.stub().returns(true);
569
+ meeting.config.receiveReactions = true;
570
+ const fakeSendersName = 'Fake reactors name';
571
+ meeting.members.membersCollection.get = sinon.stub().returns({name: fakeSendersName});
572
+ const fakeReactionPayload = {
573
+ type: 'fake_type',
574
+ codepoints: 'fake_codepoints',
575
+ shortcodes: 'fake_shortcodes',
576
+ tone: {
577
+ type: 'fake_tone_type',
578
+ codepoints: 'fake_tone_codepoints',
579
+ shortcodes: 'fake_tone_shortcodes',
580
+ },
581
+ };
582
+ const fakeSenderPayload = {
583
+ participantId: 'fake_participant_id',
584
+ };
585
+ const fakeProcessedReaction = {
586
+ reaction: fakeReactionPayload,
587
+ sender: {
588
+ id: fakeSenderPayload.participantId,
589
+ name: fakeSendersName,
590
+ },
591
+ };
592
+ const fakeRelayEvent = {
593
+ data: {
594
+ relayType: REACTION_RELAY_TYPES.REACTION,
595
+ reaction: fakeReactionPayload,
596
+ sender: fakeSenderPayload,
597
+ },
598
+ };
599
+ meeting.processRelayEvent(fakeRelayEvent);
600
+ assert.calledWith(
601
+ TriggerProxy.trigger,
602
+ sinon.match.instanceOf(Meeting),
603
+ {
604
+ file: 'meeting/index',
605
+ function: 'join',
606
+ },
607
+ EVENT_TRIGGERS.MEETING_RECEIVE_REACTIONS,
608
+ fakeProcessedReaction
609
+ );
610
+ });
611
+ });
802
612
  describe('#join', () => {
803
613
  let sandbox = null;
804
614
  const joinMeetingResult = 'JOIN_MEETINGS_OPTION_RESULT';
@@ -873,8 +683,68 @@ describe('plugin-meetings', () => {
873
683
  await meeting.join();
874
684
  sinon.assert.called(meeting.setCorrelationId);
875
685
  });
876
- });
877
- describe('failure', () => {
686
+
687
+ it('should send Meeting Info CA events if meetingInfo is not empty', async () => {
688
+ meeting.meetingInfo = {info: 'info', meetingLookupUrl: 'url'};
689
+
690
+ const join = meeting.join();
691
+
692
+ assert.calledWithMatch(Metrics.postEvent, {
693
+ event: eventType.CALL_INITIATED,
694
+ data: {trigger: trigger.USER_INTERACTION, isRoapCallEnabled: true},
695
+ });
696
+
697
+ assert.exists(join.then);
698
+ const result = await join;
699
+
700
+ assert.calledOnce(MeetingUtil.joinMeeting);
701
+ assert.calledOnce(meeting.setLocus);
702
+ assert.equal(result, joinMeetingResult);
703
+
704
+ assert.calledThrice(Metrics.postEvent)
705
+
706
+ assert.deepEqual(Metrics.postEvent.getCall(0).args[0].event, 'client.call.initiated');
707
+ assert.deepEqual(Metrics.postEvent.getCall(0).args[0].data, {
708
+ isRoapCallEnabled: true,
709
+ trigger: 'user-interaction',
710
+ });
711
+ assert.deepEqual(
712
+ Metrics.postEvent.getCall(1).args[0].event,
713
+ 'client.meetinginfo.request'
714
+ );
715
+ assert.deepEqual(Metrics.postEvent.getCall(1).args[0].data, undefined);
716
+ assert.deepEqual(
717
+ Metrics.postEvent.getCall(2).args[0].event,
718
+ 'client.meetinginfo.response'
719
+ );
720
+ assert.deepEqual(Metrics.postEvent.getCall(2).args[0].data, {
721
+ meetingLookupUrl: 'url',
722
+ });
723
+ });
724
+
725
+ it('should not send Meeting Info CA events if meetingInfo is empty', async () => {
726
+ meeting.meetingInfo = {};
727
+
728
+ const join = meeting.join();
729
+
730
+ assert.calledWithMatch(Metrics.postEvent, {
731
+ event: eventType.CALL_INITIATED,
732
+ data: {trigger: trigger.USER_INTERACTION, isRoapCallEnabled: true},
733
+ });
734
+
735
+ assert.exists(join.then);
736
+ const result = await join;
737
+
738
+ assert.calledOnce(MeetingUtil.joinMeeting);
739
+ assert.calledOnce(meeting.setLocus);
740
+ assert.equal(result, joinMeetingResult);
741
+
742
+ assert.calledOnce(Metrics.postEvent)
743
+
744
+ assert.equal(Metrics.postEvent.getCall(0).args[0].event, 'client.call.initiated');
745
+ });
746
+ });
747
+ describe('failure', () => {
878
748
  beforeEach(() => {
879
749
  sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.reject());
880
750
  meeting.logger.log = sinon.stub().returns(true);
@@ -952,7 +822,7 @@ describe('plugin-meetings', () => {
952
822
  beforeEach(() => {
953
823
  fakeMediaConnection = {
954
824
  close: sinon.stub(),
955
- getConnectionState: sinon.stub().returns(MC.ConnectionState.Connected),
825
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
956
826
  initiateOffer: sinon.stub().resolves({}),
957
827
  on: sinon.stub(),
958
828
  };
@@ -961,7 +831,7 @@ describe('plugin-meetings', () => {
961
831
  meeting.mediaProperties.getCurrentConnectionType = sinon.stub().resolves('udp');
962
832
  meeting.audio = muteStateStub;
963
833
  meeting.video = muteStateStub;
964
- Media.createMediaConnection = sinon.stub().returns(fakeMediaConnection);
834
+ sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection);
965
835
  meeting.setMercuryListener = sinon.stub().returns(true);
966
836
  meeting.setupMediaConnectionListeners = sinon.stub();
967
837
  meeting.setMercuryListener = sinon.stub();
@@ -1022,6 +892,10 @@ describe('plugin-meetings', () => {
1022
892
  code: error.code,
1023
893
  turnDiscoverySkippedReason: undefined,
1024
894
  turnServerUsed: true,
895
+ isMultistream: false,
896
+ signalingState: 'unknown',
897
+ connectionState: 'unknown',
898
+ iceConnectionState: 'unknown'
1025
899
  });
1026
900
  });
1027
901
 
@@ -1038,8 +912,13 @@ describe('plugin-meetings', () => {
1038
912
  locus_id: meeting.locusUrl.split('/').pop(),
1039
913
  reason: err.message,
1040
914
  stack: err.stack,
915
+ code: err.code,
1041
916
  turnDiscoverySkippedReason: 'config',
1042
917
  turnServerUsed: false,
918
+ isMultistream: false,
919
+ signalingState: 'unknown',
920
+ connectionState: 'unknown',
921
+ iceConnectionState: 'unknown'
1043
922
  });
1044
923
  });
1045
924
  });
@@ -1053,21 +932,111 @@ describe('plugin-meetings', () => {
1053
932
  });
1054
933
  const result = await assert.isRejected(meeting.addMedia());
1055
934
 
935
+
936
+ assert(Metrics.sendBehavioralMetric.calledOnce);
937
+ assert.calledWith(
938
+ Metrics.sendBehavioralMetric,
939
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
940
+ sinon.match({
941
+ correlation_id: meeting.correlationId,
942
+ locus_id: meeting.locusUrl.split('/').pop(),
943
+ reason: result.message,
944
+ turnDiscoverySkippedReason: undefined,
945
+ turnServerUsed: true,
946
+ isMultistream: false,
947
+ signalingState: 'unknown',
948
+ connectionState: 'unknown',
949
+ iceConnectionState: 'unknown'
950
+ })
951
+ );
952
+
1056
953
  assert.instanceOf(result, Error);
1057
954
  assert.isNull(meeting.mediaProperties.webrtcMediaConnection);
1058
955
 
956
+ });
957
+
958
+ it('should include the peer connection properties correctly for multistream', async () => {
959
+ meeting.meetingState = 'ACTIVE';
960
+ // setup the mock to return an incomplete object - this will cause addMedia to fail
961
+ // because some methods (like on() or initiateOffer()) are missing
962
+ Media.createMediaConnection = sinon.stub().returns({
963
+ close: sinon.stub(),
964
+ multistreamConnection :{
965
+ pc: {
966
+ pc: {
967
+ signalingState: 'have-local-offer',
968
+ connectionState: 'connecting',
969
+ iceConnectionState: 'checking',
970
+ }
971
+ }
972
+ }
973
+ });
974
+ // set a statsAnalyzer on the meeting so that we can check that it gets reset to null
975
+ meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
976
+ const error = await assert.isRejected(meeting.addMedia());
977
+
978
+ assert.calledWith(
979
+ Metrics.sendBehavioralMetric,
980
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
981
+ sinon.match({
982
+ correlation_id: meeting.correlationId,
983
+ locus_id: meeting.locusUrl.split('/').pop(),
984
+ reason: error.message,
985
+ stack: error.stack,
986
+ code: error.code,
987
+ turnDiscoverySkippedReason: undefined,
988
+ turnServerUsed: true,
989
+ isMultistream: false,
990
+ signalingState: 'have-local-offer',
991
+ connectionState: 'connecting',
992
+ iceConnectionState: 'checking'
993
+
994
+ })
995
+ );
996
+
997
+ assert.isNull(meeting.statsAnalyzer);
1059
998
  assert(Metrics.sendBehavioralMetric.calledOnce);
999
+ });
1000
+
1001
+ it('should include the peer connection properties correctly for transcoded', async () => {
1002
+ meeting.meetingState = 'ACTIVE';
1003
+ // setup the mock to return an incomplete object - this will cause addMedia to fail
1004
+ // because some methods (like on() or initiateOffer()) are missing
1005
+ Media.createMediaConnection = sinon.stub().returns({
1006
+ close: sinon.stub(),
1007
+ mediaConnection :{
1008
+ pc: {
1009
+ signalingState: 'have-local-offer',
1010
+ connectionState: 'connecting',
1011
+ iceConnectionState: 'checking',
1012
+ }
1013
+ }
1014
+ });
1015
+ // set a statsAnalyzer on the meeting so that we can check that it gets reset to null
1016
+ meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
1017
+ const error = await assert.isRejected(meeting.addMedia());
1018
+
1060
1019
  assert.calledWith(
1061
1020
  Metrics.sendBehavioralMetric,
1062
1021
  BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
1063
1022
  sinon.match({
1064
1023
  correlation_id: meeting.correlationId,
1065
1024
  locus_id: meeting.locusUrl.split('/').pop(),
1066
- reason: result.message,
1025
+ reason: error.message,
1026
+ stack: error.stack,
1027
+ code: error.code,
1067
1028
  turnDiscoverySkippedReason: undefined,
1068
1029
  turnServerUsed: true,
1030
+ isMultistream: false,
1031
+ signalingState: 'have-local-offer',
1032
+ connectionState: 'connecting',
1033
+ iceConnectionState: 'checking'
1034
+
1069
1035
  })
1070
1036
  );
1037
+
1038
+ assert.isNull(meeting.statsAnalyzer);
1039
+ assert(Metrics.sendBehavioralMetric.calledOnce);
1071
1040
  });
1072
1041
 
1073
1042
  it('should work the second time addMedia is called in case the first time fails', async () => {
@@ -1198,9 +1167,7 @@ describe('plugin-meetings', () => {
1198
1167
 
1199
1168
  it('should attach the media and return WebExMeetingsErrors when connection does not reach CONNECTED state', async () => {
1200
1169
  meeting.meetingState = 'ACTIVE';
1201
- fakeMediaConnection.getConnectionState = sinon
1202
- .stub()
1203
- .returns(MC.ConnectionState.Connecting);
1170
+ fakeMediaConnection.getConnectionState = sinon.stub().returns(ConnectionState.Connecting);
1204
1171
  const clock = sinon.useFakeTimers();
1205
1172
  const media = meeting.addMedia({
1206
1173
  mediaSettings: {},
@@ -1230,8 +1197,7 @@ describe('plugin-meetings', () => {
1230
1197
  .addMedia({
1231
1198
  mediaSettings: {},
1232
1199
  })
1233
- .catch((error) => {
1234
- assert.equal(error.code, IceGatheringFailed.CODE);
1200
+ .catch(() => {
1235
1201
  errorThrown = true;
1236
1202
  });
1237
1203
 
@@ -1249,6 +1215,7 @@ describe('plugin-meetings', () => {
1249
1215
  correlation_id: meeting.correlationId,
1250
1216
  locus_id: meeting.locusUrl.split('/').pop(),
1251
1217
  connectionType: 'udp',
1218
+ isMultistream: false
1252
1219
  });
1253
1220
  });
1254
1221
 
@@ -1366,627 +1333,853 @@ describe('plugin-meetings', () => {
1366
1333
  });
1367
1334
  });
1368
1335
  });
1369
- });
1370
- describe('#acknowledge', () => {
1371
- it('should have #acknowledge', () => {
1372
- assert.exists(meeting.acknowledge);
1373
- });
1374
- beforeEach(() => {
1375
- meeting.meetingRequest.acknowledgeMeeting = sinon.stub().returns(Promise.resolve());
1376
- });
1377
- it('should acknowledge incoming and return a promise', async () => {
1378
- const ack = meeting.acknowledge('INCOMING', false);
1379
1336
 
1380
- assert.exists(ack.then);
1381
- await ack;
1382
- assert.calledOnce(meeting.meetingRequest.acknowledgeMeeting);
1383
- });
1384
- it('should acknowledge a non incoming and return a promise', async () => {
1385
- const ack = meeting.acknowledge(test1, false);
1337
+ it('should pass bundlePolicy to createMediaConnection', async () => {
1338
+ const FAKE_TURN_URL = 'turns:webex.com:3478';
1339
+ const FAKE_TURN_USER = 'some-turn-username';
1340
+ const FAKE_TURN_PASSWORD = 'some-password';
1386
1341
 
1387
- assert.exists(ack.then);
1388
- await ack;
1389
- assert.notCalled(meeting.meetingRequest.acknowledgeMeeting);
1390
- });
1391
- });
1392
- describe('#decline', () => {
1393
- it('should have #decline', () => {
1394
- assert.exists(meeting.decline);
1395
- });
1396
- beforeEach(() => {
1397
- meeting.meetingRequest.declineMeeting = sinon.stub().returns(Promise.resolve());
1398
- meeting.meetingFiniteStateMachine.ring();
1399
- });
1400
- it('should decline the meeting and trigger meeting destroy for 1:1', async () => {
1401
- await meeting.decline();
1402
- assert.calledOnce(meeting.meetingRequest.declineMeeting);
1342
+ meeting.meetingState = 'ACTIVE';
1343
+ Media.createMediaConnection.resetHistory();
1344
+
1345
+ meeting.roap.doTurnDiscovery = sinon.stub().resolves({
1346
+ turnServerInfo: {
1347
+ url: FAKE_TURN_URL,
1348
+ username: FAKE_TURN_USER,
1349
+ password: FAKE_TURN_PASSWORD,
1350
+ },
1351
+ turnServerSkippedReason: undefined,
1352
+ });
1353
+ const media = meeting.addMedia({
1354
+ mediaSettings: {},
1355
+ bundlePolicy: 'bundlePolicy-value',
1356
+ });
1357
+
1358
+ assert.exists(media);
1359
+ await media;
1360
+ assert.calledOnce(meeting.roap.doTurnDiscovery);
1361
+ assert.calledWith(meeting.roap.doTurnDiscovery, meeting, false);
1362
+ assert.calledOnce(Media.createMediaConnection);
1363
+ assert.calledWith(
1364
+ Media.createMediaConnection,
1365
+ false,
1366
+ meeting.getMediaConnectionDebugId(),
1367
+ sinon.match({
1368
+ turnServerInfo: {
1369
+ url: FAKE_TURN_URL,
1370
+ username: FAKE_TURN_USER,
1371
+ password: FAKE_TURN_PASSWORD,
1372
+ },
1373
+ bundlePolicy: 'bundlePolicy-value',
1374
+ })
1375
+ );
1376
+ assert.calledOnce(fakeMediaConnection.initiateOffer);
1403
1377
  });
1404
1378
  });
1405
- describe('#leave', () => {
1406
- let sandbox;
1407
1379
 
1408
- it('should have #leave', () => {
1409
- assert.exists(meeting.leave);
1410
- });
1380
+ /* This set of tests are like semi-integration tests, they use real MuteState, Media, LocusMediaRequest and Roap classes.
1381
+ They mock the @webex/internal-media-core and sending of /media http requests to Locus.
1382
+ Their main purpose is to test that we send the right http requests to Locus and make right calls
1383
+ to @webex/internal-media-core when addMedia, updateMedia, publishTracks, unpublishTracks are called
1384
+ in various combinations.
1385
+ */
1386
+ [true,false].forEach((isMultistream) =>
1387
+ describe(`addMedia/updateMedia semi-integration tests (${isMultistream ? 'multistream' : 'transcoded'})`, () => {
1388
+ const webrtcAudioTrack = {
1389
+ id: 'underlying audio track',
1390
+ getSettings: sinon.stub().returns({deviceId: 'fake device id for audio track'}),
1391
+ };
1411
1392
 
1412
- it('should reject if meeting is already inactive', async () => {
1413
- await meeting.leave().catch((err) => {
1414
- assert.instanceOf(err, MeetingNotActiveError);
1415
- });
1416
- });
1393
+ let fakeMicrophoneTrack;
1394
+ let fakeRoapMediaConnection;
1395
+ let fakeMultistreamRoapMediaConnection;
1396
+ let roapMediaConnectionConstructorStub;
1397
+ let multistreamRoapMediaConnectionConstructorStub;
1398
+ let locusMediaRequestStub; // stub for /media requests to Locus
1417
1399
 
1418
- it('should reject if meeting is already left', async () => {
1419
- meeting.meetingState = 'ACTIVE';
1420
- await meeting.leave().catch((err) => {
1421
- assert.instanceOf(err, UserNotJoinedError);
1422
- });
1423
- });
1400
+ const roapOfferMessage = {messageType: 'OFFER', sdp: 'sdp', seq: '1', tieBreaker: '123'};
1401
+
1402
+ let expectedMediaConnectionConfig;
1403
+ let expectedDebugId;
1404
+
1405
+ let clock;
1424
1406
 
1425
1407
  beforeEach(() => {
1426
- sandbox = sinon.createSandbox();
1427
- meeting.meetingFiniteStateMachine.ring();
1428
- meeting.meetingFiniteStateMachine.join();
1429
- meeting.meetingRequest.leaveMeeting = sinon
1430
- .stub()
1431
- .returns(Promise.resolve({body: 'test'}));
1432
- meeting.locusInfo.onFullLocus = sinon.stub().returns(true);
1433
- // the 3 need to be promises because we do closeLocalStream.then(closeLocalShare.then) etc in the src code
1434
- meeting.closeLocalStream = sinon.stub().returns(Promise.resolve());
1435
- meeting.closeLocalShare = sinon.stub().returns(Promise.resolve());
1436
- meeting.closeRemoteStream = sinon.stub().returns(Promise.resolve());
1437
- sandbox.stub(meeting, 'closeRemoteTracks').returns(Promise.resolve());
1438
- meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
1439
- meeting.unsetLocalVideoTrack = sinon.stub().returns(true);
1440
- meeting.unsetLocalShareTrack = sinon.stub().returns(true);
1441
- meeting.unsetRemoteTracks = sinon.stub();
1442
- meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
1443
- meeting.unsetRemoteStream = sinon.stub().returns(true);
1444
- meeting.unsetPeerConnections = sinon.stub().returns(true);
1445
- meeting.logger.error = sinon.stub().returns(true);
1446
- meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
1408
+ clock = sinon.useFakeTimers();
1447
1409
 
1448
- // A meeting needs to be joined to leave
1410
+ meeting.deviceUrl = 'deviceUrl';
1411
+ meeting.config.deviceType = 'web';
1412
+ meeting.isMultistream = isMultistream;
1449
1413
  meeting.meetingState = 'ACTIVE';
1450
- meeting.state = 'JOINED';
1451
- });
1452
- afterEach(() => {
1453
- sandbox.restore();
1454
- sandbox = null;
1455
- });
1456
- it('should leave the meeting and return promise', async () => {
1457
- const leave = meeting.leave();
1414
+ meeting.mediaId = 'fake media id';
1415
+ meeting.selfUrl = 'selfUrl';
1416
+ meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
1417
+ meeting.mediaProperties.getCurrentConnectionType = sinon.stub().resolves('udp');
1418
+ meeting.setMercuryListener = sinon.stub();
1419
+ meeting.locusInfo.onFullLocus = sinon.stub();
1420
+ meeting.webex.meetings.reachability = {
1421
+ isAnyClusterReachable: sinon.stub().resolves(true),
1422
+ };
1423
+ meeting.roap.doTurnDiscovery = sinon
1424
+ .stub()
1425
+ .resolves({turnServerInfo: {}, turnDiscoverySkippedReason: 'reachability'});
1426
+
1427
+ StaticConfig.set({bandwidth: {audio: 1234, video: 5678, startBitrate: 9876}});
1428
+
1429
+ Metrics.postEvent = sinon.stub();
1430
+
1431
+ // setup things that are expected to be the same across all the tests and are actually irrelevant for these tests
1432
+ expectedDebugId = `MC-${meeting.id.substring(0, 4)}`;
1433
+ expectedMediaConnectionConfig = {
1434
+ iceServers: [ { urls: undefined, username: '', credential: '' } ],
1435
+ skipInactiveTransceivers: false,
1436
+ requireH264: true,
1437
+ sdpMunging: {
1438
+ convertPort9to0: false,
1439
+ addContentSlides: true,
1440
+ bandwidthLimits: {
1441
+ audio: StaticConfig.meetings.bandwidth.audio,
1442
+ video: StaticConfig.meetings.bandwidth.video,
1443
+ },
1444
+ startBitrate: StaticConfig.meetings.bandwidth.startBitrate,
1445
+ periodicKeyframes: 20,
1446
+ disableExtmap: !meeting.config.enableExtmap,
1447
+ disableRtx: !meeting.config.enableRtx,
1448
+ },
1449
+ };
1458
1450
 
1459
- assert.exists(leave.then);
1460
- await leave;
1461
- assert.calledOnce(meeting.meetingRequest.leaveMeeting);
1462
- assert.calledOnce(meeting.closeLocalStream);
1463
- assert.calledOnce(meeting.closeLocalShare);
1464
- assert.calledOnce(meeting.closeRemoteTracks);
1465
- assert.calledOnce(meeting.closePeerConnections);
1466
- assert.calledOnce(meeting.unsetLocalVideoTrack);
1467
- assert.calledOnce(meeting.unsetLocalShareTrack);
1468
- assert.calledOnce(meeting.unsetRemoteTracks);
1469
- assert.calledOnce(meeting.unsetPeerConnections);
1470
- });
1471
- describe('after audio/video is defined', () => {
1472
- let handleClientRequest;
1451
+ // setup stubs
1452
+ fakeMicrophoneTrack = {
1453
+ id: 'fake mic',
1454
+ on: sinon.stub(),
1455
+ off: sinon.stub(),
1456
+ setUnmuteAllowed: sinon.stub(),
1457
+ setMuted: sinon.stub(),
1458
+ setPublished: sinon.stub(),
1459
+ muted: false,
1460
+ underlyingTrack: webrtcAudioTrack
1461
+ };
1473
1462
 
1474
- beforeEach(() => {
1475
- handleClientRequest = sinon.stub().returns(Promise.resolve(true));
1463
+ fakeRoapMediaConnection = {
1464
+ id: 'roap media connection',
1465
+ close: sinon.stub(),
1466
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
1467
+ initiateOffer: sinon.stub().resolves({}),
1468
+ update: sinon.stub().resolves({}),
1469
+ on: sinon.stub(),
1470
+ };
1476
1471
 
1477
- meeting.audio = {handleClientRequest};
1478
- meeting.video = {handleClientRequest};
1479
- });
1472
+ fakeMultistreamRoapMediaConnection = {
1473
+ id: 'multistream roap media connection',
1474
+ close: sinon.stub(),
1475
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
1476
+ initiateOffer: sinon.stub().resolves({}),
1477
+ publishTrack: sinon.stub().resolves({}),
1478
+ unpublishTrack: sinon.stub().resolves({}),
1479
+ on: sinon.stub(),
1480
+ requestMedia: sinon.stub(),
1481
+ createReceiveSlot: sinon.stub().resolves({on: sinon.stub()}),
1482
+ enableMultistreamAudio: sinon.stub(),
1483
+ };
1480
1484
 
1481
- it('should delete audio and video state machines when leaving the meeting', async () => {
1482
- const leave = meeting.leave();
1485
+ roapMediaConnectionConstructorStub = sinon
1486
+ .stub(internalMediaModule, 'RoapMediaConnection')
1487
+ .returns(fakeRoapMediaConnection);
1483
1488
 
1484
- assert.exists(leave.then);
1485
- await leave;
1489
+ multistreamRoapMediaConnectionConstructorStub = sinon
1490
+ .stub(internalMediaModule, 'MultistreamRoapMediaConnection')
1491
+ .returns(fakeMultistreamRoapMediaConnection);
1486
1492
 
1487
- assert.isNull(meeting.audio);
1488
- assert.isNull(meeting.video);
1489
- });
1493
+ locusMediaRequestStub = sinon.stub(WebexPlugin.prototype, 'request').resolves({body: {locus: { fullState: {}}}});
1490
1494
  });
1491
- it('should leave the meeting without leaving resource', async () => {
1492
- const leave = meeting.leave({resourceId: null});
1493
1495
 
1494
- assert.exists(leave.then);
1495
- await leave;
1496
- assert.calledWith(meeting.meetingRequest.leaveMeeting, {
1497
- locusUrl: meeting.locusUrl,
1498
- correlationId: meeting.correlationId,
1499
- selfId: meeting.selfId,
1500
- resourceId: null,
1501
- deviceUrl: meeting.deviceUrl,
1502
- });
1496
+ afterEach(() => {
1497
+ clock.restore();
1503
1498
  });
1504
- it('should leave the meeting on the resource', async () => {
1505
- const leave = meeting.leave();
1506
1499
 
1507
- assert.exists(leave.then);
1508
- await leave;
1509
- assert.calledWith(meeting.meetingRequest.leaveMeeting, {
1510
- locusUrl: meeting.locusUrl,
1511
- correlationId: meeting.correlationId,
1512
- selfId: meeting.selfId,
1513
- resourceId: meeting.resourceId,
1514
- deviceUrl: meeting.deviceUrl,
1500
+ // helper function that waits until all promises are resolved and any queued up /media requests to Locus are sent out
1501
+ const stableState = async () => {
1502
+ await testUtils.flushPromises();
1503
+ clock.tick(1); // needed because LocusMediaRequest uses Lodash.defer()
1504
+ }
1505
+
1506
+ const resetHistory = () => {
1507
+ locusMediaRequestStub.resetHistory();
1508
+ fakeRoapMediaConnection.update.resetHistory();
1509
+ fakeMultistreamRoapMediaConnection.publishTrack.resetHistory();
1510
+ fakeMultistreamRoapMediaConnection.unpublishTrack.resetHistory();
1511
+ };
1512
+
1513
+ const getRoapListener = () => {
1514
+ const roapMediaConnectionToCheck = isMultistream ? fakeMultistreamRoapMediaConnection : fakeRoapMediaConnection;
1515
+
1516
+ for(let idx = 0; idx < roapMediaConnectionToCheck.on.callCount; idx+= 1) {
1517
+ if (roapMediaConnectionToCheck.on.getCall(idx).args[0] === Event.ROAP_MESSAGE_TO_SEND) {
1518
+ return roapMediaConnectionToCheck.on.getCall(idx).args[1];
1519
+ }
1520
+ }
1521
+ assert.fail('listener for "roap:messageToSend" (Event.ROAP_MESSAGE_TO_SEND) was not registered')
1522
+ }
1523
+
1524
+ // simulates a Roap offer being generated by the RoapMediaConnection
1525
+ const simulateRoapOffer = async () => {
1526
+ const roapListener = getRoapListener();
1527
+
1528
+ await roapListener({roapMessage: roapOfferMessage});
1529
+ await stableState();
1530
+ }
1531
+
1532
+ const checkSdpOfferSent = ({audioMuted, videoMuted}) => {
1533
+ const {sdp, seq, tieBreaker} = roapOfferMessage;
1534
+
1535
+ assert.calledWith(locusMediaRequestStub,
1536
+ {
1537
+ method: 'PUT',
1538
+ uri: `${meeting.selfUrl}/media`,
1539
+ body: {
1540
+ device: { url: meeting.deviceUrl, deviceType: meeting.config.deviceType },
1541
+ correlationId: meeting.correlationId,
1542
+ localMedias: [
1543
+ {
1544
+ localSdp: `{"audioMuted":${audioMuted},"videoMuted":${videoMuted},"roapMessage":{"messageType":"OFFER","sdps":["${sdp}"],"version":"2","seq":"${seq}","tieBreaker":"${tieBreaker}"}}`,
1545
+ mediaId: 'fake media id'
1546
+ }
1547
+ ],
1548
+ clientMediaPreferences: {
1549
+ preferTranscoding: !meeting.isMultistream,
1550
+ joinCookie: undefined
1551
+ }
1552
+ },
1553
+ });
1554
+ };
1555
+
1556
+ const checkLocalMuteSentToLocus = ({audioMuted, videoMuted}) => {
1557
+ assert.calledWith(locusMediaRequestStub, {
1558
+ method: 'PUT',
1559
+ uri: `${meeting.selfUrl}/media`,
1560
+ body: {
1561
+ device: { url: meeting.deviceUrl, deviceType: meeting.config.deviceType },
1562
+ correlationId: meeting.correlationId,
1563
+ localMedias: [
1564
+ {
1565
+ localSdp: `{"audioMuted":${audioMuted},"videoMuted":${videoMuted}}`,
1566
+ mediaId: 'fake media id'
1567
+ }
1568
+ ],
1569
+ clientMediaPreferences: {
1570
+ preferTranscoding: !meeting.isMultistream,
1571
+ },
1572
+ respOnlySdp: true,
1573
+ usingResource: null,
1574
+ },
1575
+ });
1576
+ };
1577
+
1578
+ const checkMediaConnectionCreated = ({mediaConnectionConfig, localTracks, direction, remoteQualityLevel, expectedDebugId}) => {
1579
+ if (isMultistream) {
1580
+ const {iceServers} = mediaConnectionConfig;
1581
+
1582
+ assert.calledOnceWithExactly(multistreamRoapMediaConnectionConstructorStub, {
1583
+ iceServers,
1584
+ enableMainAudio: direction.audio !== 'inactive',
1585
+ enableMainVideo: true
1586
+ }, expectedDebugId);
1587
+
1588
+ Object.values(localTracks).forEach((track) => {
1589
+ if (track) {
1590
+ assert.calledOnceWithExactly(fakeMultistreamRoapMediaConnection.publishTrack, track);
1591
+ }
1592
+ })
1593
+ } else {
1594
+ assert.calledOnceWithExactly(roapMediaConnectionConstructorStub, mediaConnectionConfig,
1595
+ {
1596
+ localTracks: {
1597
+ audio: localTracks.audio?.underlyingTrack,
1598
+ video: localTracks.video?.underlyingTrack,
1599
+ screenShareVideo: localTracks.screenShareVideo?.underlyingTrack,
1600
+ },
1601
+ direction,
1602
+ remoteQualityLevel,
1603
+ },
1604
+ expectedDebugId);
1605
+ }
1606
+ }
1607
+
1608
+ it('addMedia() works correctly when media is enabled without tracks to publish', async () => {
1609
+ await meeting.addMedia();
1610
+ await simulateRoapOffer();
1611
+
1612
+ // check RoapMediaConnection was created correctly
1613
+ checkMediaConnectionCreated({
1614
+ mediaConnectionConfig: expectedMediaConnectionConfig,
1615
+ localTracks: {
1616
+ audio: undefined,
1617
+ video: undefined,
1618
+ screenShareVideo: undefined,
1619
+ },
1620
+ direction: {
1621
+ audio: 'sendrecv',
1622
+ video: 'sendrecv',
1623
+ screenShareVideo: 'recvonly',
1624
+ },
1625
+ remoteQualityLevel: 'HIGH',
1626
+ expectedDebugId,
1515
1627
  });
1516
- });
1517
- });
1518
- describe('#requestScreenShareFloor', () => {
1519
- it('should have #requestScreenShareFloor', () => {
1520
- assert.exists(meeting.requestScreenShareFloor);
1521
- });
1522
- beforeEach(() => {
1523
- meeting.locusInfo.mediaShares = [{name: 'content', url: url1}];
1524
- meeting.locusInfo.self = {url: url1};
1525
- meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
1526
- });
1527
- it('should send the share', async () => {
1528
- const share = meeting.requestScreenShareFloor();
1529
1628
 
1530
- assert.exists(share.then);
1531
- await share;
1532
- assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
1629
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1630
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1631
+
1632
+ // and that it was the only /media request that was sent
1633
+ assert.calledOnce(locusMediaRequestStub);
1533
1634
  });
1534
- });
1535
1635
 
1536
- describe('#shareScreen', () => {
1537
- let _mediaDirection;
1636
+ it('addMedia() works correctly when media is enabled with tracks to publish', async () => {
1637
+ await meeting.addMedia({localTracks: {microphone: fakeMicrophoneTrack}});
1638
+ await simulateRoapOffer();
1538
1639
 
1539
- beforeEach(() => {
1540
- _mediaDirection = meeting.mediaProperties.mediaDirection || {};
1541
- sinon
1542
- .stub(meeting.mediaProperties, 'mediaDirection')
1543
- .value({sendAudio: true, sendVideo: true, sendShare: false});
1544
- });
1640
+ // check RoapMediaConnection was created correctly
1641
+ checkMediaConnectionCreated({
1642
+ mediaConnectionConfig: expectedMediaConnectionConfig,
1643
+ localTracks: {
1644
+ audio: fakeMicrophoneTrack,
1645
+ video: undefined,
1646
+ screenShareVideo: undefined,
1647
+ },
1648
+ direction: {
1649
+ audio: 'sendrecv',
1650
+ video: 'sendrecv',
1651
+ screenShareVideo: 'recvonly',
1652
+ },
1653
+ remoteQualityLevel: 'HIGH',
1654
+ expectedDebugId
1655
+ });
1545
1656
 
1546
- afterEach(() => {
1547
- meeting.mediaProperties.mediaDirection = _mediaDirection;
1548
- });
1657
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1658
+ checkSdpOfferSent({audioMuted: false, videoMuted: true});
1549
1659
 
1550
- it('should have #shareScreen', () => {
1551
- assert.exists(meeting.shareScreen);
1660
+ // and no other local mute requests were sent to Locus
1661
+ assert.calledOnce(locusMediaRequestStub);
1552
1662
  });
1553
1663
 
1554
- describe('basic functionality', () => {
1555
- beforeEach(() => {
1556
- sinon.stub(Media, 'getDisplayMedia').returns(Promise.resolve());
1557
- sinon.stub(meeting, 'updateShare').returns(Promise.resolve());
1558
- });
1664
+ it('addMedia() works correctly when media is enabled with tracks to publish and track is muted', async () => {
1665
+ fakeMicrophoneTrack.muted = true;
1559
1666
 
1560
- afterEach(() => {
1561
- Media.getDisplayMedia.restore();
1562
- meeting.updateShare.restore();
1667
+ await meeting.addMedia({localTracks: {microphone: fakeMicrophoneTrack}});
1668
+ await simulateRoapOffer();
1669
+
1670
+ // check RoapMediaConnection was created correctly
1671
+ checkMediaConnectionCreated({
1672
+ mediaConnectionConfig: expectedMediaConnectionConfig,
1673
+ localTracks: {
1674
+ audio: fakeMicrophoneTrack,
1675
+ video: undefined,
1676
+ screenShareVideo: undefined,
1677
+ },
1678
+ direction: {
1679
+ audio: 'sendrecv',
1680
+ video: 'sendrecv',
1681
+ screenShareVideo: 'recvonly',
1682
+ },
1683
+ remoteQualityLevel: 'HIGH',
1684
+ expectedDebugId,
1563
1685
  });
1564
1686
 
1565
- it('should call get display media', async () => {
1566
- await meeting.shareScreen();
1687
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1688
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1567
1689
 
1568
- assert.calledOnce(Media.getDisplayMedia);
1569
- });
1690
+ // and no other local mute requests were sent to Locus
1691
+ assert.calledOnce(locusMediaRequestStub);
1692
+ });
1570
1693
 
1571
- it('should call updateShare', async () => {
1572
- await meeting.shareScreen();
1694
+ it('addMedia() works correctly when media is disabled with tracks to publish', async () => {
1695
+ await meeting.addMedia({localTracks: {microphone: fakeMicrophoneTrack}, audioEnabled: false});
1696
+ await simulateRoapOffer();
1573
1697
 
1574
- assert.calledOnce(meeting.updateShare);
1698
+ // check RoapMediaConnection was created correctly
1699
+ checkMediaConnectionCreated({
1700
+ mediaConnectionConfig: expectedMediaConnectionConfig,
1701
+ localTracks: {
1702
+ audio: fakeMicrophoneTrack,
1703
+ video: undefined,
1704
+ screenShareVideo: undefined,
1705
+ },
1706
+ direction: {
1707
+ audio: 'inactive',
1708
+ video: 'sendrecv',
1709
+ screenShareVideo: 'recvonly',
1710
+ },
1711
+ remoteQualityLevel: 'HIGH',
1712
+ expectedDebugId
1575
1713
  });
1576
1714
 
1577
- it('properly assigns default values', async () => {
1578
- await meeting.shareScreen({sharePreferences: {highFrameRate: true}});
1715
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1716
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1579
1717
 
1580
- assert.calledWith(Media.getDisplayMedia, {
1581
- sendShare: true,
1582
- sendAudio: false,
1583
- sharePreferences: {highFrameRate: true},
1584
- });
1585
- });
1718
+ // and no other local mute requests were sent to Locus
1719
+ assert.calledOnce(locusMediaRequestStub);
1586
1720
  });
1587
1721
 
1588
- describe('stops share immediately', () => {
1589
- let sandbox;
1722
+ it('addMedia() works correctly when media is disabled with no tracks to publish', async () => {
1723
+ await meeting.addMedia({audioEnabled: false});
1724
+ await simulateRoapOffer();
1590
1725
 
1591
- beforeEach(() => {
1592
- sandbox = sinon.createSandbox();
1726
+ // check RoapMediaConnection was created correctly
1727
+ checkMediaConnectionCreated({
1728
+ mediaConnectionConfig: expectedMediaConnectionConfig,
1729
+ localTracks: {
1730
+ audio: undefined,
1731
+ video: undefined,
1732
+ screenShareVideo: undefined,
1733
+ },
1734
+ direction: {
1735
+ audio: 'inactive',
1736
+ video: 'sendrecv',
1737
+ screenShareVideo: 'recvonly',
1738
+ },
1739
+ remoteQualityLevel: 'HIGH',
1740
+ expectedDebugId
1593
1741
  });
1594
1742
 
1595
- afterEach(() => {
1596
- sandbox.restore();
1597
- sandbox = null;
1598
- });
1743
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1744
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1599
1745
 
1600
- it('Can bypass canUpdateMedia() check', () => {
1601
- const sendShare = true;
1602
- const receiveShare = false;
1603
- const stream = 'stream';
1746
+ // and no other local mute requests were sent to Locus
1747
+ assert.calledOnce(locusMediaRequestStub);
1748
+ });
1604
1749
 
1605
- sandbox.stub(MeetingUtil, 'getTrack').returns({videoTrack: true});
1606
- MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve(true));
1607
- sandbox.stub(meeting, 'canUpdateMedia').returns(true);
1608
- sandbox.stub(meeting, 'setLocalShareTrack');
1750
+ describe('publishTracks()/unpublishTracks() calls', () => {
1751
+ [
1752
+ {mediaEnabled: true, expected: {direction: 'sendrecv', localMuteSentValue: false}},
1753
+ {mediaEnabled: false, expected: {direction: 'inactive', localMuteSentValue: undefined}}
1754
+ ]
1755
+ .forEach(({mediaEnabled, expected}) => {
1756
+ it(`first publishTracks() call while media is ${mediaEnabled ? 'enabled' : 'disabled'}`, async () => {
1757
+ await meeting.addMedia({audioEnabled: mediaEnabled});
1758
+ await simulateRoapOffer();
1609
1759
 
1610
- meeting.updateShare({
1611
- sendShare,
1612
- receiveShare,
1613
- stream,
1614
- skipSignalingCheck: true,
1615
- });
1760
+ resetHistory();
1616
1761
 
1617
- assert.notCalled(meeting.canUpdateMedia);
1618
- });
1762
+ await meeting.publishTracks({microphone: fakeMicrophoneTrack});
1763
+ await stableState();
1619
1764
 
1620
- it('skips canUpdateMedia() check on contentTracks.onended', () => {
1621
- const {mediaProperties} = meeting;
1622
- const fakeTrack = {
1623
- getSettings: sinon.stub().returns({}),
1624
- onended: sinon.stub(),
1625
- };
1765
+ if (expected.localMuteSentValue !== undefined) {
1766
+ // check local mute was sent and it was the only /media request
1767
+ checkLocalMuteSentToLocus({audioMuted: expected.localMuteSentValue, videoMuted: true});
1768
+ assert.calledOnce(locusMediaRequestStub);
1769
+ } else {
1770
+ assert.notCalled(locusMediaRequestStub);
1771
+ }
1772
+ if (isMultistream) {
1773
+ assert.calledOnceWithExactly(fakeMultistreamRoapMediaConnection.publishTrack, fakeMicrophoneTrack);
1774
+ } else {
1775
+ assert.calledOnceWithExactly(fakeRoapMediaConnection.update, {
1776
+ localTracks: { audio: webrtcAudioTrack, video: null, screenShareVideo: null },
1777
+ direction: {
1778
+ audio: expected.direction,
1779
+ video: 'sendrecv',
1780
+ screenShareVideo: 'recvonly',
1781
+ },
1782
+ remoteQualityLevel: 'HIGH'
1783
+ });
1784
+ }
1785
+ });
1626
1786
 
1627
- sandbox.stub(mediaProperties, 'setLocalShareTrack');
1628
- sandbox.stub(mediaProperties, 'shareTrack').value(fakeTrack);
1629
- sandbox.stub(mediaProperties, 'setMediaSettings');
1630
- sandbox.stub(meeting, 'stopShare').resolves(true);
1631
- meeting.setLocalShareTrack(fakeTrack);
1787
+ it(`second publishTracks() call while media is ${mediaEnabled ? 'enabled' : 'disabled'}`, async () => {
1788
+ await meeting.addMedia({audioEnabled: mediaEnabled});
1789
+ await simulateRoapOffer();
1790
+ await meeting.publishTracks({microphone: fakeMicrophoneTrack});
1791
+ await stableState();
1792
+
1793
+ resetHistory();
1794
+
1795
+ const webrtcAudioTrack2 = {id: 'underlying audio track 2'};
1796
+ const fakeMicrophoneTrack2 = {
1797
+ id: 'fake mic 2',
1798
+ on: sinon.stub(),
1799
+ off: sinon.stub(),
1800
+ setUnmuteAllowed: sinon.stub(),
1801
+ setMuted: sinon.stub(),
1802
+ setPublished: sinon.stub(),
1803
+ muted: false,
1804
+ underlyingTrack: webrtcAudioTrack2
1805
+ };
1806
+
1807
+ await meeting.publishTracks({microphone: fakeMicrophoneTrack2});
1808
+ await stableState();
1809
+
1810
+ // only the roap media connection should be updated
1811
+ if (isMultistream) {
1812
+ assert.calledOnceWithExactly(fakeMultistreamRoapMediaConnection.publishTrack, fakeMicrophoneTrack2);
1813
+ } else {
1814
+ assert.calledOnceWithExactly(fakeRoapMediaConnection.update, {
1815
+ localTracks: { audio: webrtcAudioTrack2, video: null, screenShareVideo: null },
1816
+ direction: {
1817
+ audio: expected.direction,
1818
+ video: 'sendrecv',
1819
+ screenShareVideo: 'recvonly',
1820
+ },
1821
+ remoteQualityLevel: 'HIGH'
1822
+ });
1823
+ }
1632
1824
 
1633
- fakeTrack.onended();
1825
+ // and no other roap messages or local mute requests were sent
1826
+ assert.notCalled(locusMediaRequestStub);
1827
+ });
1634
1828
 
1635
- assert.calledWith(meeting.stopShare, {skipSignalingCheck: true});
1636
- });
1829
+ it(`unpublishTracks() call while media is ${mediaEnabled ? 'enabled' : 'disabled'}`, async () => {
1830
+ await meeting.addMedia({audioEnabled: mediaEnabled});
1831
+ await simulateRoapOffer();
1832
+ await meeting.publishTracks({microphone: fakeMicrophoneTrack});
1833
+ await stableState();
1637
1834
 
1638
- it('stopShare accepts and passes along optional parameters', () => {
1639
- const args = {
1640
- abc: 123,
1641
- receiveShare: false,
1642
- sendShare: false,
1643
- };
1835
+ resetHistory();
1644
1836
 
1645
- sandbox.stub(meeting, 'updateShare').returns(Promise.resolve());
1646
- sandbox.stub(meeting.mediaProperties, 'mediaDirection').value(false);
1837
+ await meeting.unpublishTracks([fakeMicrophoneTrack]);
1838
+ await stableState();
1647
1839
 
1648
- meeting.stopShare(args);
1840
+ // the roap media connection should be updated
1841
+ if (isMultistream) {
1842
+ assert.calledOnceWithExactly(fakeMultistreamRoapMediaConnection.unpublishTrack, fakeMicrophoneTrack);
1843
+ } else {
1844
+ assert.calledOnceWithExactly(fakeRoapMediaConnection.update, {
1845
+ localTracks: { audio: null, video: null, screenShareVideo: null },
1846
+ direction: {
1847
+ audio: expected.direction,
1848
+ video: 'sendrecv',
1849
+ screenShareVideo: 'recvonly',
1850
+ },
1851
+ remoteQualityLevel: 'HIGH'
1852
+ });
1853
+ }
1649
1854
 
1650
- assert.calledWith(meeting.updateShare, args);
1651
- });
1855
+ if (expected.localMuteSentValue !== undefined) {
1856
+ // and local mute sent to Locus
1857
+ checkLocalMuteSentToLocus({audioMuted: !expected.localMuteSentValue /* negation, because we're un-publishing */, videoMuted: true});
1858
+ assert.calledOnce(locusMediaRequestStub);
1859
+ } else {
1860
+ assert.notCalled(locusMediaRequestStub);
1861
+ }
1862
+ });
1863
+ });
1652
1864
  });
1653
1865
 
1654
- describe('out-of-sync sharing', () => {
1655
- let sandbox;
1866
+ describe('updateMedia()', () => {
1656
1867
 
1657
- beforeEach(() => {
1658
- sandbox = sinon.createSandbox();
1868
+ const addMedia = async (enableMedia, track) => {
1869
+ await meeting.addMedia({audioEnabled: enableMedia, localTracks: {microphone: track}});
1870
+ await simulateRoapOffer();
1871
+
1872
+ resetHistory();
1873
+ }
1874
+
1875
+ const checkAudioEnabled = (expectedTrack, expectedDirection) => {
1876
+ if (isMultistream) {
1877
+ assert.calledOnceWithExactly(fakeMultistreamRoapMediaConnection.enableMultistreamAudio, expectedDirection !== 'inactive');
1878
+ } else {
1879
+ assert.calledOnceWithExactly(fakeRoapMediaConnection.update, {
1880
+ localTracks: { audio: expectedTrack, video: null, screenShareVideo: null },
1881
+ direction: {
1882
+ audio: expectedDirection,
1883
+ video: 'sendrecv',
1884
+ screenShareVideo: 'recvonly',
1885
+ },
1886
+ remoteQualityLevel: 'HIGH'
1887
+ });
1888
+ }
1889
+ }
1890
+
1891
+ it('updateMedia() disables media when nothing is published', async () => {
1892
+ await addMedia(true);
1893
+
1894
+ await meeting.updateMedia({audioEnabled: false});
1895
+
1896
+ // the roap media connection should be updated
1897
+ checkAudioEnabled(null, 'inactive');
1898
+
1899
+ // and that would trigger a new offer so we simulate it happening
1900
+ await simulateRoapOffer();
1901
+
1902
+ // check SDP offer was sent with the right audioMuted/videoMuted values
1903
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1904
+
1905
+ // and no other local mute requests were sent to Locus
1906
+ assert.calledOnce(locusMediaRequestStub);
1659
1907
  });
1660
1908
 
1661
- afterEach(() => {
1662
- sandbox.restore();
1663
- sandbox = null;
1909
+ it('updateMedia() enables media when nothing is published', async () => {
1910
+ await addMedia(false);
1911
+
1912
+ await meeting.updateMedia({audioEnabled: true});
1913
+
1914
+ // the roap media connection should be updated
1915
+ checkAudioEnabled(null, 'sendrecv');
1916
+
1917
+ // and that would trigger a new offer so we simulate it happening
1918
+ await simulateRoapOffer();
1919
+
1920
+ // check SDP offer was sent with the right audioMuted/videoMuted values
1921
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1922
+
1923
+ // and no other local mute requests were sent to Locus
1924
+ assert.calledOnce(locusMediaRequestStub);
1664
1925
  });
1665
1926
 
1666
- it('handleShareTrackEnded triggers an event', () => {
1667
- const stream = 'stream';
1668
- const {EVENT_TYPES} = CONSTANTS;
1927
+ it('updateMedia() disables media when track is published', async () => {
1928
+ await addMedia(true, fakeMicrophoneTrack);
1669
1929
 
1670
- sandbox.stub(meeting, 'stopShare').resolves(true);
1930
+ await meeting.updateMedia({audioEnabled: false});
1931
+ await stableState();
1671
1932
 
1672
- meeting.handleShareTrackEnded(stream);
1933
+ // the roap media connection should be updated
1934
+ checkAudioEnabled(webrtcAudioTrack, 'inactive');
1673
1935
 
1674
- assert.calledWith(
1675
- TriggerProxy.trigger,
1676
- sinon.match.instanceOf(Meeting),
1677
- {
1678
- file: 'meeting/index',
1679
- function: 'handleShareTrackEnded',
1680
- },
1681
- EVENT_TRIGGERS.MEETING_STOPPED_SHARING_LOCAL,
1682
- {
1683
- stream,
1684
- type: EVENT_TYPES.LOCAL_SHARE,
1685
- }
1686
- );
1936
+ checkLocalMuteSentToLocus({audioMuted: true, videoMuted: true});
1937
+
1938
+ locusMediaRequestStub.resetHistory();
1939
+
1940
+ // and that would trigger a new offer so we simulate it happening
1941
+ await simulateRoapOffer();
1942
+
1943
+ // check SDP offer was sent with the right audioMuted/videoMuted values
1944
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1945
+
1946
+ // and no other local mute requests were sent to Locus
1947
+ assert.calledOnce(locusMediaRequestStub);
1687
1948
  });
1688
- });
1689
- });
1690
1949
 
1691
- describe('#shareScreen resolutions', () => {
1692
- let _getDisplayMedia = null;
1693
- const config = DefaultSDKConfig.meetings;
1694
- const {resolution} = config;
1695
- const shareOptions = {
1696
- sendShare: true,
1697
- sendAudio: false,
1698
- };
1699
- const fireFoxOptions = {
1700
- audio: false,
1701
- video: {
1702
- audio: shareOptions.sendAudio,
1703
- video: shareOptions.sendShare,
1704
- },
1705
- };
1950
+ it('updateMedia() enables media when track is published', async () => {
1951
+ await addMedia(false, fakeMicrophoneTrack);
1706
1952
 
1707
- const MediaStream = {
1708
- getVideoTracks: () => [
1709
- {
1710
- applyConstraints: () => {},
1711
- },
1712
- ],
1713
- };
1953
+ await meeting.updateMedia({audioEnabled: true});
1954
+ await stableState();
1714
1955
 
1715
- const MediaConstraint = {
1716
- cursor: 'always',
1717
- aspectRatio: config.aspectRatio,
1718
- frameRate: config.screenFrameRate,
1719
- width: null,
1720
- height: null,
1721
- };
1956
+ // the roap media connection should be updated
1957
+ checkAudioEnabled(webrtcAudioTrack, 'sendrecv');
1722
1958
 
1723
- const browserConditionalValue = (value) => {
1724
- const key = getBrowserName().toLowerCase();
1725
- const defaultKey = 'default';
1959
+ checkLocalMuteSentToLocus({audioMuted: false, videoMuted: true});
1726
1960
 
1727
- return value[key] || value[defaultKey];
1728
- };
1961
+ locusMediaRequestStub.resetHistory();
1729
1962
 
1730
- before(() => {
1731
- meeting.updateShare = sinon.stub().returns(Promise.resolve());
1963
+ // and that would trigger a new offer so we simulate it happening
1964
+ await simulateRoapOffer();
1732
1965
 
1733
- if (!global.navigator) {
1734
- global.navigator = {
1735
- mediaDevices: {
1736
- getDisplayMedia: null,
1737
- },
1738
- };
1739
- }
1740
- _getDisplayMedia = global.navigator.mediaDevices.getDisplayMedia;
1741
- Object.defineProperty(global.navigator.mediaDevices, 'getDisplayMedia', {
1742
- value: sinon.stub().returns(Promise.resolve(MediaStream)),
1743
- writable: true,
1966
+ // check SDP offer was sent with the right audioMuted/videoMuted values
1967
+ checkSdpOfferSent({audioMuted: false, videoMuted: true});
1968
+
1969
+ // and no other local mute requests were sent to Locus
1970
+ assert.calledOnce(locusMediaRequestStub);
1744
1971
  });
1745
1972
  });
1746
1973
 
1747
- after(() => {
1748
- // clean up for browser
1749
- Object.defineProperty(global.navigator.mediaDevices, 'getDisplayMedia', {
1750
- value: _getDisplayMedia,
1751
- writable: true,
1752
- });
1974
+ [
1975
+ {mute: true, title: 'muting a track before confluence is created'},
1976
+ {mute: false, title: 'unmuting a track before confluence is created'}
1977
+ ].forEach(({mute, title}) =>
1978
+ it(title, async () => {
1979
+ // initialize the microphone mute state to opposite of what we do in the test
1980
+ fakeMicrophoneTrack.muted = !mute;
1981
+
1982
+ await meeting.addMedia({localTracks: {microphone: fakeMicrophoneTrack}});
1983
+ await stableState();
1984
+
1985
+ resetHistory();
1986
+
1987
+ assert.equal(fakeMicrophoneTrack.on.getCall(0).args[0], LocalTrackEvents.Muted);
1988
+ const mutedListener = fakeMicrophoneTrack.on.getCall(0).args[1];
1989
+ // simulate track being muted
1990
+ mutedListener({trackState: {muted: mute}});
1991
+
1992
+ await stableState();
1993
+
1994
+ // nothing should happen
1995
+ assert.notCalled(locusMediaRequestStub);
1996
+ assert.notCalled(fakeRoapMediaConnection.update);
1997
+
1998
+ // now simulate roap offer
1999
+ await simulateRoapOffer();
2000
+
2001
+ // it should be sent with the right mute status
2002
+ checkSdpOfferSent({audioMuted: mute, videoMuted: true});
2003
+
2004
+ // nothing else should happen
2005
+ assert.calledOnce(locusMediaRequestStub);
2006
+ assert.notCalled(fakeRoapMediaConnection.update);
2007
+ })
2008
+ );
2009
+ }));
2010
+
2011
+ describe('#acknowledge', () => {
2012
+ it('should have #acknowledge', () => {
2013
+ assert.exists(meeting.acknowledge);
1753
2014
  });
2015
+ beforeEach(() => {
2016
+ meeting.meetingRequest.acknowledgeMeeting = sinon.stub().returns(Promise.resolve());
2017
+ });
2018
+ it('should acknowledge incoming and return a promise', async () => {
2019
+ const ack = meeting.acknowledge('INCOMING', false);
1754
2020
 
1755
- // eslint-disable-next-line max-len
1756
- it('will use shareConstraints if defined in provided options', () => {
1757
- const SHARE_WIDTH = 640;
1758
- const SHARE_HEIGHT = 480;
1759
- const shareConstraints = {
1760
- highFrameRate: 2,
1761
- maxWidth: SHARE_WIDTH,
1762
- maxHeight: SHARE_HEIGHT,
1763
- idealWidth: SHARE_WIDTH,
1764
- idealHeight: SHARE_HEIGHT,
1765
- };
2021
+ assert.exists(ack.then);
2022
+ await ack;
2023
+ assert.calledOnce(meeting.meetingRequest.acknowledgeMeeting);
2024
+ });
2025
+ it('should acknowledge a non incoming and return a promise', async () => {
2026
+ const ack = meeting.acknowledge(test1, false);
1766
2027
 
1767
- // If sharePreferences.shareConstraints is defined it ignores
1768
- // default SDK config settings
1769
- getDisplayMedia(
1770
- {
1771
- ...shareOptions,
1772
- sharePreferences: {shareConstraints},
1773
- },
1774
- config
1775
- );
2028
+ assert.exists(ack.then);
2029
+ await ack;
2030
+ assert.notCalled(meeting.meetingRequest.acknowledgeMeeting);
2031
+ });
2032
+ });
2033
+ describe('#decline', () => {
2034
+ it('should have #decline', () => {
2035
+ assert.exists(meeting.decline);
2036
+ });
2037
+ beforeEach(() => {
2038
+ meeting.meetingRequest.declineMeeting = sinon.stub().returns(Promise.resolve());
2039
+ meeting.meetingFiniteStateMachine.ring();
2040
+ });
2041
+ it('should decline the meeting and trigger meeting destroy for 1:1', async () => {
2042
+ await meeting.decline();
2043
+ assert.calledOnce(meeting.meetingRequest.declineMeeting);
2044
+ });
2045
+ });
2046
+ describe('#leave', () => {
2047
+ let sandbox;
1776
2048
 
1777
- // eslint-disable-next-line no-undef
1778
- assert.calledWith(
1779
- navigator.mediaDevices.getDisplayMedia,
1780
- browserConditionalValue({
1781
- default: {
1782
- video: {...shareConstraints},
1783
- },
1784
- // Firefox is being handled differently
1785
- firefox: fireFoxOptions,
1786
- })
1787
- );
2049
+ it('should have #leave', () => {
2050
+ assert.exists(meeting.leave);
1788
2051
  });
1789
2052
 
1790
- // eslint-disable-next-line max-len
1791
- it('will use default resolution if shareConstraints is undefined and highFrameRate is defined', () => {
1792
- // If highFrameRate is defined it ignores default SDK config settings
1793
- getDisplayMedia(
1794
- {
1795
- ...shareOptions,
1796
- sharePreferences: {
1797
- highFrameRate: true,
1798
- },
1799
- },
1800
- config
1801
- );
2053
+ it('should reject if meeting is already inactive', async () => {
2054
+ await meeting.leave().catch((err) => {
2055
+ assert.instanceOf(err, MeetingNotActiveError);
2056
+ });
2057
+ });
1802
2058
 
1803
- // eslint-disable-next-line no-undef
1804
- assert.calledWith(
1805
- navigator.mediaDevices.getDisplayMedia,
1806
- browserConditionalValue({
1807
- default: {
1808
- video: {
1809
- ...MediaConstraint,
1810
- frameRate: config.videoShareFrameRate,
1811
- width: resolution.idealWidth,
1812
- height: resolution.idealHeight,
1813
- maxWidth: resolution.maxWidth,
1814
- maxHeight: resolution.maxHeight,
1815
- idealWidth: resolution.idealWidth,
1816
- idealHeight: resolution.idealHeight,
1817
- },
1818
- },
1819
- firefox: fireFoxOptions,
1820
- })
1821
- );
2059
+ it('should reject if meeting is already left', async () => {
2060
+ meeting.meetingState = 'ACTIVE';
2061
+ await meeting.leave().catch((err) => {
2062
+ assert.instanceOf(err, UserNotJoinedError);
2063
+ });
1822
2064
  });
1823
2065
 
1824
- // eslint-disable-next-line max-len
1825
- it('will use default screenResolution if shareConstraints, highFrameRate, and SDK defaults is undefined', () => {
1826
- getDisplayMedia(shareOptions);
1827
- const {screenResolution} = config;
2066
+ beforeEach(() => {
2067
+ sandbox = sinon.createSandbox();
2068
+ meeting.meetingFiniteStateMachine.ring();
2069
+ meeting.meetingFiniteStateMachine.join();
2070
+ meeting.meetingRequest.leaveMeeting = sinon
2071
+ .stub()
2072
+ .returns(Promise.resolve({body: 'test'}));
2073
+ meeting.locusInfo.onFullLocus = sinon.stub().returns(true);
2074
+ meeting.cleanupLocalTracks = sinon.stub().returns(Promise.resolve());
2075
+ meeting.closeRemoteStream = sinon.stub().returns(Promise.resolve());
2076
+ sandbox.stub(meeting, 'closeRemoteTracks').returns(Promise.resolve());
2077
+ meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
2078
+ meeting.unsetRemoteTracks = sinon.stub();
2079
+ meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2080
+ meeting.unsetPeerConnections = sinon.stub().returns(true);
2081
+ meeting.logger.error = sinon.stub().returns(true);
2082
+ meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
2083
+
2084
+ // A meeting needs to be joined to leave
2085
+ meeting.meetingState = 'ACTIVE';
2086
+ meeting.state = 'JOINED';
2087
+ });
2088
+ afterEach(() => {
2089
+ sandbox.restore();
2090
+ sandbox = null;
2091
+ });
2092
+ it('should leave the meeting and return promise', async () => {
2093
+ const leave = meeting.leave();
1828
2094
 
1829
- // eslint-disable-next-line no-undef
1830
- assert.calledWith(
1831
- navigator.mediaDevices.getDisplayMedia,
1832
- browserConditionalValue({
1833
- default: {
1834
- video: {
1835
- ...MediaConstraint,
1836
- width: screenResolution.idealWidth,
1837
- height: screenResolution.idealHeight,
1838
- },
1839
- },
1840
- firefox: fireFoxOptions,
1841
- })
1842
- );
2095
+ assert.exists(leave.then);
2096
+ await leave;
2097
+ assert.calledOnce(meeting.meetingRequest.leaveMeeting);
2098
+ assert.calledOnce(meeting.cleanupLocalTracks);
2099
+ assert.calledOnce(meeting.closeRemoteTracks);
2100
+ assert.calledOnce(meeting.closePeerConnections);
2101
+ assert.calledOnce(meeting.unsetRemoteTracks);
2102
+ assert.calledOnce(meeting.unsetPeerConnections);
1843
2103
  });
2104
+ describe('after audio/video is defined', () => {
2105
+ let handleClientRequest;
1844
2106
 
1845
- // Test screenResolution
1846
- // eslint-disable-next-line max-len
1847
- it('will use SDK config screenResolution if set, with shareConstraints and highFrameRate being undefined', () => {
1848
- const SHARE_WIDTH = 800;
1849
- const SHARE_HEIGHT = 600;
1850
- const customConfig = {
1851
- screenResolution: {
1852
- maxWidth: SHARE_WIDTH,
1853
- maxHeight: SHARE_HEIGHT,
1854
- idealWidth: SHARE_WIDTH,
1855
- idealHeight: SHARE_HEIGHT,
1856
- },
1857
- };
2107
+ beforeEach(() => {
2108
+ handleClientRequest = sinon.stub().returns(Promise.resolve(true));
1858
2109
 
1859
- getDisplayMedia(shareOptions, customConfig);
2110
+ meeting.audio = {handleClientRequest};
2111
+ meeting.video = {handleClientRequest};
2112
+ });
1860
2113
 
1861
- // eslint-disable-next-line no-undef
1862
- assert.calledWith(
1863
- navigator.mediaDevices.getDisplayMedia,
1864
- browserConditionalValue({
1865
- default: {
1866
- video: {
1867
- ...MediaConstraint,
1868
- width: SHARE_WIDTH,
1869
- height: SHARE_HEIGHT,
1870
- maxWidth: SHARE_WIDTH,
1871
- maxHeight: SHARE_HEIGHT,
1872
- idealWidth: SHARE_WIDTH,
1873
- idealHeight: SHARE_HEIGHT,
1874
- },
1875
- },
1876
- firefox: fireFoxOptions,
1877
- })
1878
- );
2114
+ it('should delete audio and video state machines when leaving the meeting', async () => {
2115
+ const leave = meeting.leave();
2116
+
2117
+ assert.exists(leave.then);
2118
+ await leave;
2119
+
2120
+ assert.isNull(meeting.audio);
2121
+ assert.isNull(meeting.video);
2122
+ });
1879
2123
  });
2124
+ it('should leave the meeting without leaving resource', async () => {
2125
+ const leave = meeting.leave({resourceId: null});
1880
2126
 
1881
- // Test screenFrameRate
1882
- it('will use SDK config screenFrameRate if set, with shareConstraints and highFrameRate being undefined', () => {
1883
- const SHARE_WIDTH = 800;
1884
- const SHARE_HEIGHT = 600;
1885
- const customConfig = {
1886
- screenFrameRate: 999,
1887
- screenResolution: {
1888
- maxWidth: SHARE_WIDTH,
1889
- maxHeight: SHARE_HEIGHT,
1890
- idealWidth: SHARE_WIDTH,
1891
- idealHeight: SHARE_HEIGHT,
1892
- },
1893
- };
2127
+ assert.exists(leave.then);
2128
+ await leave;
2129
+ assert.calledWith(meeting.meetingRequest.leaveMeeting, {
2130
+ locusUrl: meeting.locusUrl,
2131
+ correlationId: meeting.correlationId,
2132
+ selfId: meeting.selfId,
2133
+ resourceId: null,
2134
+ deviceUrl: meeting.deviceUrl,
2135
+ });
2136
+ });
2137
+ it('should leave the meeting on the resource', async () => {
2138
+ const leave = meeting.leave();
1894
2139
 
1895
- getDisplayMedia(shareOptions, customConfig);
2140
+ assert.exists(leave.then);
2141
+ await leave;
2142
+ assert.calledWith(meeting.meetingRequest.leaveMeeting, {
2143
+ locusUrl: meeting.locusUrl,
2144
+ correlationId: meeting.correlationId,
2145
+ selfId: meeting.selfId,
2146
+ resourceId: meeting.resourceId,
2147
+ deviceUrl: meeting.deviceUrl
2148
+ });
2149
+ });
2150
+ it('should leave the meeting on the resource with reason', async () => {
2151
+ const leave = meeting.leave({resourceId: meeting.resourceId, reason: MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST});
1896
2152
 
1897
- // eslint-disable-next-line no-undef
1898
- assert.calledWith(
1899
- navigator.mediaDevices.getDisplayMedia,
1900
- browserConditionalValue({
1901
- default: {
1902
- video: {
1903
- ...MediaConstraint,
1904
- frameRate: customConfig.screenFrameRate,
1905
- width: SHARE_WIDTH,
1906
- height: SHARE_HEIGHT,
1907
- maxWidth: SHARE_WIDTH,
1908
- maxHeight: SHARE_HEIGHT,
1909
- idealWidth: SHARE_WIDTH,
1910
- idealHeight: SHARE_HEIGHT,
1911
- },
1912
- },
1913
- firefox: fireFoxOptions,
1914
- })
1915
- );
2153
+ assert.exists(leave.then);
2154
+ await leave;
2155
+ assert.calledWith(meeting.meetingRequest.leaveMeeting, {
2156
+ locusUrl: meeting.locusUrl,
2157
+ correlationId: meeting.correlationId,
2158
+ selfId: meeting.selfId,
2159
+ resourceId: meeting.resourceId,
2160
+ deviceUrl: meeting.deviceUrl,
2161
+ reason: MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST
2162
+ });
1916
2163
  });
1917
2164
  });
1918
-
1919
- describe('#stopShare', () => {
1920
- it('should have #stopShare', () => {
1921
- assert.exists(meeting.stopShare);
2165
+ describe('#requestScreenShareFloor', () => {
2166
+ it('should have #requestScreenShareFloor', () => {
2167
+ assert.exists(meeting.requestScreenShareFloor);
1922
2168
  });
1923
2169
  beforeEach(() => {
1924
- meeting.mediaProperties.mediaDirection = {receiveShare: true};
1925
- meeting.updateShare = sinon.stub().returns(Promise.resolve());
2170
+ meeting.locusInfo.mediaShares = [{name: 'content', url: url1}];
2171
+ meeting.locusInfo.self = {url: url1};
2172
+ meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
2173
+ meeting.mediaProperties.shareTrack = {}
2174
+ meeting.mediaProperties.mediaDirection.sendShare = true;
2175
+ meeting.state = 'JOINED';
1926
2176
  });
1927
- it('should call updateShare', async () => {
1928
- const share = meeting.stopShare();
2177
+ it('should send the share', async () => {
2178
+ const share = meeting.requestScreenShareFloor();
1929
2179
 
1930
2180
  assert.exists(share.then);
1931
2181
  await share;
1932
- assert.calledOnce(meeting.updateShare);
1933
- });
1934
- });
1935
-
1936
- describe('#updateAudio', () => {
1937
- const FAKE_AUDIO_TRACK = {
1938
- id: 'fake audio track',
1939
- getSettings: sinon.stub().returns({}),
1940
- };
1941
-
1942
- describe('when canUpdateMedia is true', () => {
1943
- beforeEach(() => {
1944
- meeting.canUpdateMedia = sinon.stub().returns(true);
1945
- });
1946
- describe('when options are valid', () => {
1947
- beforeEach(() => {
1948
- MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve());
1949
- meeting.mediaProperties.mediaDirection = {
1950
- sendAudio: false,
1951
- sendVideo: true,
1952
- sendShare: false,
1953
- receiveAudio: false,
1954
- receiveVideo: true,
1955
- receiveShare: true,
1956
- };
1957
- meeting.mediaProperties.webrtcMediaConnection = {
1958
- updateSendReceiveOptions: sinon.stub(),
1959
- };
1960
- sinon.stub(MeetingUtil, 'getTrack').returns({audioTrack: FAKE_AUDIO_TRACK});
1961
- });
1962
- it('calls this.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions', () =>
1963
- meeting
1964
- .updateAudio({
1965
- sendAudio: true,
1966
- receiveAudio: true,
1967
- stream: {id: 'fake stream'},
1968
- })
1969
- .then(() => {
1970
- assert.calledOnce(
1971
- meeting.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions
1972
- );
1973
- assert.calledWith(
1974
- meeting.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions,
1975
- {
1976
- send: {audio: FAKE_AUDIO_TRACK},
1977
- receive: {
1978
- audio: true,
1979
- video: true,
1980
- screenShareVideo: true,
1981
- remoteQualityLevel: 'HIGH',
1982
- },
1983
- }
1984
- );
1985
- }));
1986
- });
1987
- afterEach(() => {
1988
- sinon.restore();
1989
- });
2182
+ assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
1990
2183
  });
1991
2184
  });
1992
2185
 
@@ -2034,37 +2227,25 @@ describe('plugin-meetings', () => {
2034
2227
 
2035
2228
  describe('#updateMedia', () => {
2036
2229
  let sandbox;
2037
- const mockLocalStream = {id: 'mock local stream'};
2038
- const mockLocalShare = {id: 'mock local share stream'};
2039
- const FAKE_TRACKS = {
2040
- audio: {
2041
- id: 'fake audio track',
2042
- getSettings: sinon.stub().returns({}),
2043
- },
2044
- video: {
2045
- id: 'fake video track',
2046
- getSettings: sinon.stub().returns({}),
2047
- },
2048
- screenshareVideo: {
2049
- id: 'fake share track',
2050
- getSettings: sinon.stub().returns({}),
2051
- },
2052
- };
2053
2230
 
2231
+ const createFakeLocalTrack = () => ({
2232
+ underlyingTrack: {id: 'fake underlying track'}
2233
+ });
2054
2234
  beforeEach(() => {
2055
2235
  sandbox = sinon.createSandbox();
2056
- meeting.mediaProperties.mediaDirection = {sendShare: true};
2057
- // setup the stub to return the right tracks
2058
- sandbox.stub(MeetingUtil, 'getTrack').callsFake((stream) => {
2059
- if (stream === mockLocalStream) {
2060
- return {audioTrack: FAKE_TRACKS.audio, videoTrack: FAKE_TRACKS.video};
2061
- }
2062
- if (stream === mockLocalShare) {
2063
- return {audioTrack: null, videoTrack: FAKE_TRACKS.screenshareVideo};
2064
- }
2065
-
2066
- return {audioTrack: null, videoTrack: null};
2067
- });
2236
+ meeting.audio = { enable: sinon.stub()};
2237
+ meeting.video = { enable: sinon.stub()};
2238
+ meeting.mediaProperties.audioTrack = createFakeLocalTrack();
2239
+ meeting.mediaProperties.videoTrack = createFakeLocalTrack();
2240
+ meeting.mediaProperties.shareTrack = createFakeLocalTrack();
2241
+ meeting.mediaProperties.mediaDirection = {
2242
+ sendAudio: true,
2243
+ sendVideo: true,
2244
+ sendShare: true,
2245
+ receiveAudio: true,
2246
+ receiveVideo: true,
2247
+ receiveShare: true,
2248
+ }
2068
2249
  });
2069
2250
 
2070
2251
  afterEach(() => {
@@ -2072,36 +2253,45 @@ describe('plugin-meetings', () => {
2072
2253
  sandbox = null;
2073
2254
  });
2074
2255
 
2075
- it('should use a queue if currently busy', async () => {
2076
- const mediaSettings = {
2077
- sendAudio: true,
2078
- receiveAudio: true,
2079
- sendVideo: true,
2080
- receiveVideo: true,
2081
- sendShare: true,
2082
- receiveShare: true,
2083
- isSharing: true,
2084
- };
2256
+ forEach(
2257
+ [
2258
+ {audioEnabled: true, enableMultistreamAudio: true},
2259
+ {audioEnabled: false, enableMultistreamAudio: false},
2260
+ ],
2261
+ ({audioEnabled, enableMultistreamAudio}) => {
2262
+ it(`should call enableMultistreamAudio with ${enableMultistreamAudio} if it is a multistream connection and audioEnabled: ${audioEnabled}`, async () => {
2263
+ meeting.mediaProperties.webrtcMediaConnection = {
2264
+ enableMultistreamAudio: sinon.stub().resolves({}),
2265
+ };
2266
+ meeting.isMultistream = true;
2267
+
2268
+ await meeting.updateMedia({audioEnabled});
2269
+
2270
+ assert.calledOnceWithExactly(
2271
+ meeting.mediaProperties.webrtcMediaConnection.enableMultistreamAudio,
2272
+ enableMultistreamAudio
2273
+ );
2274
+ assert.calledOnceWithExactly(meeting.audio.enable, meeting, enableMultistreamAudio);
2275
+ });
2276
+ }
2277
+ );
2085
2278
 
2279
+ it('should use a queue if currently busy', async () => {
2086
2280
  sandbox.stub(meeting, 'canUpdateMedia').returns(false);
2087
2281
  meeting.mediaProperties.webrtcMediaConnection = {
2088
- updateSendReceiveOptions: sinon.stub().resolves({}),
2282
+ update: sinon.stub().resolves({}),
2089
2283
  };
2090
2284
 
2091
2285
  let myPromiseResolved = false;
2092
2286
 
2093
2287
  meeting
2094
- .updateMedia({
2095
- localStream: mockLocalStream,
2096
- localShare: mockLocalShare,
2097
- mediaSettings,
2098
- })
2288
+ .updateMedia({audioEnabled: false, videoEnabled: false})
2099
2289
  .then(() => {
2100
2290
  myPromiseResolved = true;
2101
2291
  });
2102
2292
 
2103
2293
  // verify that nothing was done
2104
- assert.notCalled(meeting.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions);
2294
+ assert.notCalled(meeting.mediaProperties.webrtcMediaConnection.update);
2105
2295
 
2106
2296
  // now trigger processing of the queue
2107
2297
  meeting.canUpdateMedia.restore();
@@ -2110,22 +2300,22 @@ describe('plugin-meetings', () => {
2110
2300
  meeting.processNextQueuedMediaUpdate();
2111
2301
  await testUtils.flushPromises();
2112
2302
 
2113
- // and check that updateSendReceiveOptions is called with the original args
2114
- assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions);
2303
+ // and check that update is called with the original args
2304
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.update);
2115
2305
  assert.calledWith(
2116
- meeting.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions,
2306
+ meeting.mediaProperties.webrtcMediaConnection.update,
2117
2307
  {
2118
- send: {
2119
- audio: FAKE_TRACKS.audio,
2120
- video: FAKE_TRACKS.video,
2121
- screenShareVideo: FAKE_TRACKS.screenshareVideo,
2308
+ localTracks: {
2309
+ audio: meeting.mediaProperties.audioTrack.underlyingTrack,
2310
+ video: meeting.mediaProperties.videoTrack.underlyingTrack,
2311
+ screenShareVideo: meeting.mediaProperties.shareTrack.underlyingTrack,
2122
2312
  },
2123
- receive: {
2124
- audio: true,
2125
- video: true,
2126
- screenShareVideo: true,
2127
- remoteQualityLevel: 'HIGH',
2313
+ direction: {
2314
+ audio: 'inactive',
2315
+ video: 'inactive',
2316
+ screenShareVideo: 'sendrecv',
2128
2317
  },
2318
+ remoteQualityLevel: 'HIGH',
2129
2319
  }
2130
2320
  );
2131
2321
  assert.isTrue(myPromiseResolved);
@@ -2144,8 +2334,6 @@ describe('plugin-meetings', () => {
2144
2334
  sendShare: false,
2145
2335
  receiveVideo: true,
2146
2336
  };
2147
- meeting.getMediaStreams = sinon.stub().returns(Promise.resolve([]));
2148
- meeting.updateVideo = sinon.stub().returns(Promise.resolve());
2149
2337
  meeting.mediaProperties.mediaDirection = mediaDirection;
2150
2338
  meeting.mediaProperties.remoteVideoTrack = sinon
2151
2339
  .stub()
@@ -2382,76 +2570,12 @@ describe('plugin-meetings', () => {
2382
2570
  });
2383
2571
  });
2384
2572
 
2385
- describe('#setLocalVideoQuality', () => {
2386
- let mediaDirection;
2387
-
2388
- const fakeTrack = {getSettings: () => ({height: 720})};
2389
- const USER_AGENT_CHROME_MAC =
2390
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ' +
2391
- 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36';
2392
-
2393
- beforeEach(() => {
2394
- mediaDirection = {sendAudio: true, sendVideo: true, sendShare: false};
2395
- meeting.getMediaStreams = sinon.stub().returns(Promise.resolve([]));
2396
- meeting.mediaProperties.mediaDirection = mediaDirection;
2397
- meeting.canUpdateMedia = sinon.stub().returns(true);
2398
- MeetingUtil.validateOptions = sinon.stub().returns(Promise.resolve());
2399
- meeting.updateVideo = sinon.stub().resolves();
2400
- sinon.stub(MeetingUtil, 'getTrack').returns({videoTrack: fakeTrack});
2401
- });
2402
-
2403
- it('should have #setLocalVideoQuality', () => {
2404
- assert.exists(meeting.setLocalVideoQuality);
2405
- });
2406
-
2407
- it('should call getMediaStreams with the proper level', () =>
2408
- meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
2409
- delete mediaDirection.receiveVideo;
2410
- assert.calledWith(
2411
- meeting.getMediaStreams,
2412
- mediaDirection,
2413
- CONSTANTS.VIDEO_RESOLUTIONS[CONSTANTS.QUALITY_LEVELS.LOW]
2414
- );
2415
- }));
2416
-
2417
- it('when browser is chrome then it should stop previous video track', () => {
2418
- meeting.mediaProperties.videoTrack = fakeTrack;
2419
- assert.equal(BrowserDetection(USER_AGENT_CHROME_MAC).getBrowserName(), 'Chrome');
2420
- meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
2421
- assert.calledWith(Media.stopTracks, fakeTrack);
2422
- });
2423
- });
2424
-
2425
- it('should set mediaProperty with the proper level', () =>
2426
- meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
2427
- assert.equal(meeting.mediaProperties.localQualityLevel, CONSTANTS.QUALITY_LEVELS.LOW);
2428
- }));
2429
-
2430
- it('when device does not support 1080p then it should set localQualityLevel with highest possible resolution', () => {
2431
- meeting.setLocalVideoQuality(CONSTANTS.QUALITY_LEVELS['1080p']).then(() => {
2432
- assert.equal(
2433
- meeting.mediaProperties.localQualityLevel,
2434
- CONSTANTS.QUALITY_LEVELS['720p']
2435
- );
2436
- });
2437
- });
2438
-
2439
- it('should error if set to a invalid level', () => {
2440
- assert.isRejected(meeting.setLocalVideoQuality('invalid'));
2441
- });
2442
-
2443
- it('should error if sendVideo is set to false', () => {
2444
- meeting.mediaProperties.mediaDirection = {sendVideo: false};
2445
- assert.isRejected(meeting.setLocalVideoQuality('LOW'));
2446
- });
2447
- });
2448
-
2449
2573
  describe('#setRemoteQualityLevel', () => {
2450
2574
  let mediaDirection;
2451
2575
 
2452
2576
  beforeEach(() => {
2453
2577
  mediaDirection = {receiveAudio: true, receiveVideo: true, receiveShare: false};
2454
- meeting.updateMedia = sinon.stub().returns(Promise.resolve());
2578
+ meeting.updateTranscodedMediaConnection = sinon.stub().returns(Promise.resolve());
2455
2579
  meeting.mediaProperties.mediaDirection = mediaDirection;
2456
2580
  });
2457
2581
 
@@ -2464,9 +2588,9 @@ describe('plugin-meetings', () => {
2464
2588
  assert.equal(meeting.mediaProperties.remoteQualityLevel, CONSTANTS.QUALITY_LEVELS.LOW);
2465
2589
  }));
2466
2590
 
2467
- it('should call updateMedia', () =>
2591
+ it('should call Meeting.updateTranscodedMediaConnection()', () =>
2468
2592
  meeting.setRemoteQualityLevel(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
2469
- assert.calledOnce(meeting.updateMedia);
2593
+ assert.calledOnce(meeting.updateTranscodedMediaConnection);
2470
2594
  }));
2471
2595
 
2472
2596
  it('should error if set to a invalid level', () => {
@@ -2487,7 +2611,6 @@ describe('plugin-meetings', () => {
2487
2611
  meeting.meetingRequest.dialOut = sinon
2488
2612
  .stub()
2489
2613
  .returns(Promise.resolve({body: {locus: 'testData'}}));
2490
- meeting.locusInfo.onFullLocus = sinon.stub().returns(Promise.resolve());
2491
2614
  });
2492
2615
 
2493
2616
  it('with no parameters triggers dial-in, delegating request to meetingRequest correctly', async () => {
@@ -2500,11 +2623,9 @@ describe('plugin-meetings', () => {
2500
2623
  locusUrl: meeting.locusUrl,
2501
2624
  clientUrl: meeting.deviceUrl,
2502
2625
  });
2503
- assert.calledWith(meeting.locusInfo.onFullLocus, 'testData');
2504
2626
  assert.notCalled(meeting.meetingRequest.dialOut);
2505
2627
 
2506
2628
  meeting.meetingRequest.dialIn.resetHistory();
2507
- meeting.locusInfo.onFullLocus.resetHistory();
2508
2629
 
2509
2630
  // try again. the dial in urls should match
2510
2631
  await meeting.usePhoneAudio();
@@ -2515,7 +2636,6 @@ describe('plugin-meetings', () => {
2515
2636
  locusUrl: meeting.locusUrl,
2516
2637
  clientUrl: meeting.deviceUrl,
2517
2638
  });
2518
- assert.calledWith(meeting.locusInfo.onFullLocus, 'testData');
2519
2639
  assert.notCalled(meeting.meetingRequest.dialOut);
2520
2640
  });
2521
2641
 
@@ -2532,11 +2652,9 @@ describe('plugin-meetings', () => {
2532
2652
  clientUrl: meeting.deviceUrl,
2533
2653
  phoneNumber,
2534
2654
  });
2535
- assert.calledWith(meeting.locusInfo.onFullLocus, 'testData');
2536
2655
  assert.notCalled(meeting.meetingRequest.dialIn);
2537
2656
 
2538
2657
  meeting.meetingRequest.dialOut.resetHistory();
2539
- meeting.locusInfo.onFullLocus.resetHistory();
2540
2658
 
2541
2659
  // try again. the dial out urls should match
2542
2660
  await meeting.usePhoneAudio(phoneNumber);
@@ -2548,7 +2666,6 @@ describe('plugin-meetings', () => {
2548
2666
  clientUrl: meeting.deviceUrl,
2549
2667
  phoneNumber,
2550
2668
  });
2551
- assert.calledWith(meeting.locusInfo.onFullLocus, 'testData');
2552
2669
  assert.notCalled(meeting.meetingRequest.dialIn);
2553
2670
  });
2554
2671
 
@@ -2593,6 +2710,9 @@ describe('plugin-meetings', () => {
2593
2710
  const FAKE_CAPTCHA_IMAGE_URL = 'http://captchaimage';
2594
2711
  const FAKE_CAPTCHA_AUDIO_URL = 'http://captchaaudio';
2595
2712
  const FAKE_CAPTCHA_REFRESH_URL = 'http://captcharefresh';
2713
+ const FAKE_INSTALLED_ORG_ID = '123456';
2714
+ const FAKE_EXTRA_PARAMS = {mtid: 'm9fe0afd8c435e892afcce9ea25b97046', joinTXId: 'TSmrX61wNF'};
2715
+ let FAKE_OPTIONS;
2596
2716
  const FAKE_MEETING_INFO = {
2597
2717
  conversationUrl: 'some_convo_url',
2598
2718
  locusUrl: 'some_locus_url',
@@ -2600,6 +2720,8 @@ describe('plugin-meetings', () => {
2600
2720
  meetingNumber: '123456', // this.config.experimental.enableUnifiedMeetings
2601
2721
  hostId: 'some_host_id', // this.owner;
2602
2722
  };
2723
+ const FAKE_MEETING_INFO_LOOKUP_URL = 'meetingLookupUrl';
2724
+
2603
2725
  const FAKE_SDK_CAPTCHA_INFO = {
2604
2726
  captchaId: FAKE_CAPTCHA_ID,
2605
2727
  verificationImageURL: FAKE_CAPTCHA_IMAGE_URL,
@@ -2613,18 +2735,26 @@ describe('plugin-meetings', () => {
2613
2735
  refreshURL: `${FAKE_CAPTCHA_REFRESH_URL}-2`,
2614
2736
  };
2615
2737
 
2738
+ beforeEach(() => {
2739
+ meeting.locusId = 'locus-id';
2740
+ meeting.id = 'meeting-id';
2741
+ FAKE_OPTIONS = {meetingId: meeting.id};
2742
+ });
2743
+
2616
2744
  it('calls meetingInfoProvider with all the right parameters and parses the result', async () => {
2617
2745
  meeting.attrs.meetingInfoProvider = {
2618
- fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO}),
2746
+ fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO, url: FAKE_MEETING_INFO_LOOKUP_URL}),
2619
2747
  };
2620
2748
  meeting.requiredCaptcha = FAKE_SDK_CAPTCHA_INFO;
2621
2749
  meeting.destination = FAKE_DESTINATION;
2622
2750
  meeting.destinationType = FAKE_TYPE;
2751
+ meeting.config.installedOrgID = FAKE_INSTALLED_ORG_ID;
2623
2752
  meeting.parseMeetingInfo = sinon.stub().returns(undefined);
2624
2753
 
2625
2754
  await meeting.fetchMeetingInfo({
2626
2755
  password: FAKE_PASSWORD,
2627
2756
  captchaCode: FAKE_CAPTCHA_CODE,
2757
+ extraParams: FAKE_EXTRA_PARAMS,
2628
2758
  });
2629
2759
 
2630
2760
  assert.calledWith(
@@ -2632,11 +2762,15 @@ describe('plugin-meetings', () => {
2632
2762
  FAKE_DESTINATION,
2633
2763
  FAKE_TYPE,
2634
2764
  FAKE_PASSWORD,
2635
- {code: FAKE_CAPTCHA_CODE, id: FAKE_CAPTCHA_ID}
2765
+ {code: FAKE_CAPTCHA_CODE, id: FAKE_CAPTCHA_ID},
2766
+ FAKE_INSTALLED_ORG_ID,
2767
+ meeting.locusId,
2768
+ FAKE_EXTRA_PARAMS,
2769
+ FAKE_OPTIONS
2636
2770
  );
2637
2771
 
2638
- assert.calledWith(meeting.parseMeetingInfo, {body: FAKE_MEETING_INFO}, FAKE_DESTINATION);
2639
- assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
2772
+ assert.calledWith(meeting.parseMeetingInfo, {body: FAKE_MEETING_INFO, url: FAKE_MEETING_INFO_LOOKUP_URL}, FAKE_DESTINATION);
2773
+ assert.deepEqual(meeting.meetingInfo, {...FAKE_MEETING_INFO, meetingLookupUrl: FAKE_MEETING_INFO_LOOKUP_URL});
2640
2774
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.NOT_REQUIRED);
2641
2775
  assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.NONE);
2642
2776
  assert.equal(meeting.requiredCaptcha, null);
@@ -2651,7 +2785,7 @@ describe('plugin-meetings', () => {
2651
2785
 
2652
2786
  it('calls meetingInfoProvider with all the right parameters and parses the result when random delay is applied', async () => {
2653
2787
  meeting.attrs.meetingInfoProvider = {
2654
- fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO}),
2788
+ fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO, url: FAKE_MEETING_INFO_LOOKUP_URL}),
2655
2789
  };
2656
2790
  meeting.destination = FAKE_DESTINATION;
2657
2791
  meeting.destinationType = FAKE_TYPE;
@@ -2674,13 +2808,17 @@ describe('plugin-meetings', () => {
2674
2808
  FAKE_DESTINATION,
2675
2809
  FAKE_TYPE,
2676
2810
  null,
2677
- null
2811
+ null,
2812
+ undefined,
2813
+ meeting.locusId,
2814
+ {},
2815
+ {meetingId: meeting.id}
2678
2816
  );
2679
2817
 
2680
2818
  // parseMeeting info
2681
- assert.calledWith(meeting.parseMeetingInfo, {body: FAKE_MEETING_INFO}, FAKE_DESTINATION);
2819
+ assert.calledWith(meeting.parseMeetingInfo, {body: FAKE_MEETING_INFO, url: FAKE_MEETING_INFO_LOOKUP_URL}, FAKE_DESTINATION);
2682
2820
 
2683
- assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
2821
+ assert.deepEqual(meeting.meetingInfo, {...FAKE_MEETING_INFO, meetingLookupUrl: FAKE_MEETING_INFO_LOOKUP_URL});
2684
2822
  assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.NONE);
2685
2823
  assert.equal(meeting.requiredCaptcha, null);
2686
2824
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.NOT_REQUIRED);
@@ -2696,7 +2834,7 @@ describe('plugin-meetings', () => {
2696
2834
 
2697
2835
  it('fails if captchaCode is provided when captcha not needed', async () => {
2698
2836
  meeting.attrs.meetingInfoProvider = {
2699
- fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO}),
2837
+ fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO, url: FAKE_MEETING_INFO_LOOKUP_URL}),
2700
2838
  };
2701
2839
  meeting.requiredCaptcha = null;
2702
2840
  meeting.destination = FAKE_DESTINATION;
@@ -2715,7 +2853,7 @@ describe('plugin-meetings', () => {
2715
2853
 
2716
2854
  it('fails if password is provided when not required', async () => {
2717
2855
  meeting.attrs.meetingInfoProvider = {
2718
- fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO}),
2856
+ fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO, url: FAKE_MEETING_INFO_LOOKUP_URL}),
2719
2857
  };
2720
2858
  meeting.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
2721
2859
  meeting.destination = FAKE_DESTINATION;
@@ -2748,10 +2886,15 @@ describe('plugin-meetings', () => {
2748
2886
  FAKE_DESTINATION,
2749
2887
  FAKE_TYPE,
2750
2888
  null,
2751
- null
2889
+ null,
2890
+ undefined,
2891
+ 'locus-id',
2892
+ {},
2893
+ {meetingId: meeting.id},
2752
2894
  );
2753
2895
 
2754
2896
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
2897
+ assert.equal(meeting.meetingInfoFailureCode, 403004);
2755
2898
  assert.equal(
2756
2899
  meeting.meetingInfoFailureReason,
2757
2900
  MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD
@@ -2760,6 +2903,38 @@ describe('plugin-meetings', () => {
2760
2903
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.REQUIRED);
2761
2904
  });
2762
2905
 
2906
+ it('handles meetingInfoProvider policy error', async () => {
2907
+ meeting.destination = FAKE_DESTINATION;
2908
+ meeting.destinationType = FAKE_TYPE;
2909
+ meeting.attrs.meetingInfoProvider = {
2910
+ fetchMeetingInfo: sinon
2911
+ .stub()
2912
+ .throws(new MeetingInfoV2PolicyError(123456, FAKE_MEETING_INFO, 'a message')),
2913
+ };
2914
+
2915
+ await assert.isRejected(meeting.fetchMeetingInfo({}), PermissionError);
2916
+
2917
+ assert.calledWith(
2918
+ meeting.attrs.meetingInfoProvider.fetchMeetingInfo,
2919
+ FAKE_DESTINATION,
2920
+ FAKE_TYPE,
2921
+ null,
2922
+ null,
2923
+ undefined,
2924
+ 'locus-id',
2925
+ {},
2926
+ {meetingId: meeting.id},
2927
+ );
2928
+
2929
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
2930
+ assert.equal(meeting.meetingInfoFailureCode, 123456);
2931
+ assert.equal(
2932
+ meeting.meetingInfoFailureReason,
2933
+ MEETING_INFO_FAILURE_REASON.POLICY
2934
+ );
2935
+ });
2936
+
2937
+
2763
2938
  it('handles meetingInfoProvider requiring captcha because of wrong password', async () => {
2764
2939
  meeting.destination = FAKE_DESTINATION;
2765
2940
  meeting.destinationType = FAKE_TYPE;
@@ -2782,7 +2957,11 @@ describe('plugin-meetings', () => {
2782
2957
  FAKE_DESTINATION,
2783
2958
  FAKE_TYPE,
2784
2959
  'aaa',
2785
- null
2960
+ null,
2961
+ undefined,
2962
+ 'locus-id',
2963
+ {},
2964
+ {meetingId: meeting.id},
2786
2965
  );
2787
2966
 
2788
2967
  assert.deepEqual(meeting.meetingInfo, {});
@@ -2790,6 +2969,7 @@ describe('plugin-meetings', () => {
2790
2969
  meeting.meetingInfoFailureReason,
2791
2970
  MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD
2792
2971
  );
2972
+ assert.equal(meeting.meetingInfoFailureCode, 423005);
2793
2973
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.REQUIRED);
2794
2974
  assert.deepEqual(meeting.requiredCaptcha, {
2795
2975
  captchaId: FAKE_CAPTCHA_ID,
@@ -2822,7 +3002,11 @@ describe('plugin-meetings', () => {
2822
3002
  FAKE_DESTINATION,
2823
3003
  FAKE_TYPE,
2824
3004
  'aaa',
2825
- {code: 'bbb', id: FAKE_CAPTCHA_ID}
3005
+ {code: 'bbb', id: FAKE_CAPTCHA_ID},
3006
+ undefined,
3007
+ 'locus-id',
3008
+ {},
3009
+ {meetingId: meeting.id}
2826
3010
  );
2827
3011
 
2828
3012
  assert.deepEqual(meeting.meetingInfo, {});
@@ -2851,10 +3035,14 @@ describe('plugin-meetings', () => {
2851
3035
  FAKE_DESTINATION,
2852
3036
  FAKE_TYPE,
2853
3037
  'aaa',
2854
- null
3038
+ null,
3039
+ undefined,
3040
+ 'locus-id',
3041
+ {},
3042
+ {meetingId: meeting.id},
2855
3043
  );
2856
3044
 
2857
- assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
3045
+ assert.deepEqual(meeting.meetingInfo, {...FAKE_MEETING_INFO, meetingLookupUrl: undefined});
2858
3046
  assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.NONE);
2859
3047
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.VERIFIED);
2860
3048
  assert.equal(meeting.requiredCaptcha, null);
@@ -2896,7 +3084,11 @@ describe('plugin-meetings', () => {
2896
3084
  FAKE_DESTINATION,
2897
3085
  FAKE_TYPE,
2898
3086
  'aaa',
2899
- {code: 'bbb', id: FAKE_CAPTCHA_ID}
3087
+ {code: 'bbb', id: FAKE_CAPTCHA_ID},
3088
+ undefined,
3089
+ 'locus-id',
3090
+ {},
3091
+ {meetingId: meeting.id},
2900
3092
  );
2901
3093
 
2902
3094
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
@@ -3041,6 +3233,17 @@ describe('plugin-meetings', () => {
3041
3233
  });
3042
3234
  });
3043
3235
 
3236
+ describe('#postMetrics', () => {
3237
+ it('should have #postMetrics', () => {
3238
+ assert.exists(meeting.postMetrics);
3239
+ });
3240
+
3241
+ it('should trigger `postMetrics`', async () => {
3242
+ await meeting.postMetrics(eventType.LEAVE);
3243
+ assert.calledWithMatch(Metrics.postEvent, {event: eventType.LEAVE});
3244
+ });
3245
+ });
3246
+
3044
3247
  describe('#endMeeting for all', () => {
3045
3248
  let sandbox;
3046
3249
 
@@ -3061,16 +3264,12 @@ describe('plugin-meetings', () => {
3061
3264
  .stub()
3062
3265
  .returns(Promise.resolve({body: 'test'}));
3063
3266
  meeting.locusInfo.onFullLocus = sinon.stub().returns(true);
3064
- meeting.closeLocalStream = sinon.stub().returns(Promise.resolve());
3065
- meeting.closeLocalShare = sinon.stub().returns(Promise.resolve());
3267
+ meeting.cleanupLocalTracks = sinon.stub().returns(Promise.resolve());
3066
3268
  meeting.closeRemoteStream = sinon.stub().returns(Promise.resolve());
3067
3269
  sandbox.stub(meeting, 'closeRemoteTracks').returns(Promise.resolve());
3068
3270
  meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
3069
- meeting.unsetLocalVideoTrack = sinon.stub().returns(true);
3070
- meeting.unsetLocalShareTrack = sinon.stub().returns(true);
3071
3271
  meeting.unsetRemoteTracks = sinon.stub();
3072
3272
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
3073
- meeting.unsetRemoteStream = sinon.stub().returns(true);
3074
3273
  meeting.unsetPeerConnections = sinon.stub().returns(true);
3075
3274
  meeting.logger.error = sinon.stub().returns(true);
3076
3275
  meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
@@ -3089,12 +3288,9 @@ describe('plugin-meetings', () => {
3089
3288
  assert.exists(endMeetingForAll.then);
3090
3289
  await endMeetingForAll;
3091
3290
  assert.calledOnce(meeting?.meetingRequest?.endMeetingForAll);
3092
- assert.calledOnce(meeting?.closeLocalStream);
3093
- assert.calledOnce(meeting?.closeLocalShare);
3291
+ assert.calledOnce(meeting?.cleanupLocalTracks);
3094
3292
  assert.calledOnce(meeting?.closeRemoteTracks);
3095
3293
  assert.calledOnce(meeting?.closePeerConnections);
3096
- assert.calledOnce(meeting?.unsetLocalVideoTrack);
3097
- assert.calledOnce(meeting?.unsetLocalShareTrack);
3098
3294
  assert.calledOnce(meeting?.unsetRemoteTracks);
3099
3295
  assert.calledOnce(meeting?.unsetPeerConnections);
3100
3296
  });
@@ -3105,11 +3301,9 @@ describe('plugin-meetings', () => {
3105
3301
 
3106
3302
  beforeEach(() => {
3107
3303
  sandbox = sinon.createSandbox();
3108
- sandbox.stub(meeting, 'closeLocalStream');
3109
- sandbox.stub(meeting, 'closeLocalShare');
3304
+ sandbox.stub(meeting, 'cleanupLocalTracks');
3110
3305
 
3111
3306
  sandbox.stub(meeting.mediaProperties, 'setMediaDirection');
3112
- sandbox.stub(meeting.mediaProperties, 'unsetMediaTracks');
3113
3307
 
3114
3308
  sandbox.stub(meeting.reconnectionManager, 'reconnectMedia').returns(Promise.resolve());
3115
3309
  sandbox
@@ -3182,14 +3376,12 @@ describe('plugin-meetings', () => {
3182
3376
 
3183
3377
  // beacuse we are calling callback so we need to wait
3184
3378
 
3185
- assert.called(meeting.closeLocalStream);
3186
- assert.called(meeting.closeLocalShare);
3379
+ assert.called(meeting.cleanupLocalTracks);
3187
3380
 
3188
3381
  // give queued Promise callbacks a chance to run
3189
3382
  await Promise.resolve();
3190
3383
 
3191
3384
  assert.called(meeting.mediaProperties.setMediaDirection);
3192
- assert.called(meeting.mediaProperties.unsetMediaTracks);
3193
3385
 
3194
3386
  assert.calledWith(meeting.reconnectionManager.reconnectMedia, {
3195
3387
  mediaDirection: {
@@ -3268,41 +3460,319 @@ describe('plugin-meetings', () => {
3268
3460
  }
3269
3461
  });
3270
3462
 
3271
- it('should postEvent on moveFrom ', async () => {
3272
- await meeting.moveFrom('resourceId');
3463
+ it('should postEvent on moveFrom ', async () => {
3464
+ await meeting.moveFrom('resourceId');
3465
+
3466
+ assert.calledWithMatch(Metrics.postEvent, {event: eventType.MOVE_MEDIA});
3467
+ });
3468
+
3469
+ it('should call `MeetingUtil.joinMeetingOptions` with resourceId', async () => {
3470
+ sinon.spy(MeetingUtil, 'joinMeetingOptions');
3471
+ await meeting.moveFrom('resourceId');
3472
+
3473
+ assert.calledWith(MeetingUtil.joinMeetingOptions, meeting);
3474
+ assert.calledWith(MeetingUtil.leaveMeeting, meeting, {
3475
+ resourceId: 'resourceId',
3476
+ correlationId: meeting.correlationId,
3477
+ moveMeeting: true,
3478
+ });
3479
+
3480
+ assert.calledOnce(Metrics.sendBehavioralMetric);
3481
+ assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.MOVE_FROM_SUCCESS);
3482
+ });
3483
+
3484
+ it('should throw an error if moveFrom call fails', async () => {
3485
+ MeetingUtil.joinMeeting = sinon.stub().returns(Promise.reject());
3486
+ try {
3487
+ await meeting.moveFrom('resourceId');
3488
+ } catch {
3489
+ assert.calledOnce(Metrics.sendBehavioralMetric);
3490
+ assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.MOVE_FROM_FAILURE, {
3491
+ correlation_id: meeting.correlationId,
3492
+ locus_id: meeting.locusUrl.split('/').pop(),
3493
+ reason: sinon.match.any,
3494
+ stack: sinon.match.any,
3495
+ });
3496
+ }
3497
+ });
3498
+ });
3499
+ describe('Local tracks publishing', () => {
3500
+ let audioTrack;
3501
+ let videoTrack;
3502
+ let videoShareTrack;
3503
+ let createMuteStateStub;
3504
+ let LocalDisplayTrackConstructorStub;
3505
+ let LocalMicrophoneTrackConstructorStub;
3506
+ let LocalCameraTrackConstructorStub;
3507
+ let fakeLocalDisplayTrack;
3508
+ let fakeLocalMicrophoneTrack;
3509
+ let fakeLocalCameraTrack;
3510
+
3511
+ beforeEach(() => {
3512
+ audioTrack = {
3513
+ id: 'audio track',
3514
+ getSettings: sinon.stub().returns({}),
3515
+ on: sinon.stub(),
3516
+ off: sinon.stub(),
3517
+ };
3518
+ videoTrack = {
3519
+ id: 'video track',
3520
+ getSettings: sinon.stub().returns({}),
3521
+ on: sinon.stub(),
3522
+ off: sinon.stub(),
3523
+ };
3524
+ videoShareTrack = {
3525
+ id: 'share track',
3526
+ on: sinon.stub(),
3527
+ off: sinon.stub(),
3528
+ getSettings: sinon.stub().returns({}),
3529
+ };
3530
+ meeting.requestScreenShareFloor = sinon.stub().resolves({});
3531
+ meeting.releaseScreenShareFloor = sinon.stub().resolves({});
3532
+ meeting.mediaProperties.mediaDirection = {
3533
+ sendAudio: 'fake value', // using non-boolean here so that we can check that these values are untouched in tests
3534
+ sendVideo: 'fake value',
3535
+ sendShare: false,
3536
+ };
3537
+ meeting.isMultistream = true;
3538
+ meeting.mediaProperties.webrtcMediaConnection = {
3539
+ publishTrack: sinon.stub().resolves({}),
3540
+ unpublishTrack: sinon.stub().resolves({}),
3541
+ };
3542
+ meeting.audio = { handleLocalTrackChange: sinon.stub()};
3543
+ meeting.video = { handleLocalTrackChange: sinon.stub()};
3544
+
3545
+ const createFakeLocalTrack = (originalTrack) => ({
3546
+ on: sinon.stub(),
3547
+ off: sinon.stub(),
3548
+ stop: sinon.stub(),
3549
+ originalTrack,
3550
+ });
3551
+
3552
+ // setup mock constructors for webrtc-core local track classes in such a way
3553
+ // that they return the original track correctly (this is needed for unpublish() API tests)
3554
+ LocalDisplayTrackConstructorStub = sinon
3555
+ .stub(InternalMediaCoreModule, 'LocalDisplayTrack')
3556
+ .callsFake((stream) => {
3557
+ fakeLocalDisplayTrack = createFakeLocalTrack(stream.getTracks()[0]);
3558
+ return fakeLocalDisplayTrack;
3559
+ });
3560
+ LocalMicrophoneTrackConstructorStub = sinon
3561
+ .stub(InternalMediaCoreModule, 'LocalMicrophoneTrack')
3562
+ .callsFake((stream) => {
3563
+ fakeLocalMicrophoneTrack = createFakeLocalTrack(stream.getTracks()[0]);
3564
+ return fakeLocalMicrophoneTrack;
3565
+ });
3566
+ LocalCameraTrackConstructorStub = sinon
3567
+ .stub(InternalMediaCoreModule, 'LocalCameraTrack')
3568
+ .callsFake((stream) => {
3569
+ fakeLocalCameraTrack = createFakeLocalTrack(stream.getTracks()[0]);
3570
+ return fakeLocalCameraTrack;
3571
+ });
3572
+
3573
+ createMuteStateStub = sinon
3574
+ .stub(MuteStateModule, 'createMuteState')
3575
+ .returns({id: 'fake mute state instance'});
3576
+ });
3577
+ describe('#publishTracks', () => {
3578
+ it('fails if there is no media connection', async () => {
3579
+ meeting.mediaProperties.webrtcMediaConnection = undefined;
3580
+ await assert.isRejected(meeting.publishTracks({audio: {id: 'some audio track'}}));
3581
+ });
3582
+
3583
+ const checkAudioPublished = (track) => {
3584
+ assert.calledOnceWithExactly(meeting.audio.handleLocalTrackChange, meeting);
3585
+ assert.calledWith(
3586
+ meeting.mediaProperties.webrtcMediaConnection.publishTrack,
3587
+ track
3588
+ );
3589
+ assert.equal(meeting.mediaProperties.audioTrack, track);
3590
+ // check that sendAudio hasn't been touched
3591
+ assert.equal(meeting.mediaProperties.mediaDirection.sendAudio, 'fake value');
3592
+ };
3593
+
3594
+ const checkVideoPublished = (track) => {
3595
+ assert.calledOnceWithExactly(meeting.video.handleLocalTrackChange, meeting);
3596
+ assert.calledWith(
3597
+ meeting.mediaProperties.webrtcMediaConnection.publishTrack,
3598
+ track
3599
+ );
3600
+ assert.equal(meeting.mediaProperties.videoTrack, track);
3601
+ // check that sendVideo hasn't been touched
3602
+ assert.equal(meeting.mediaProperties.mediaDirection.sendVideo, 'fake value');
3603
+ };
3604
+
3605
+ const checkScreenShareVideoPublished = (track) => {
3606
+ assert.calledOnce(meeting.requestScreenShareFloor);
3607
+
3608
+ assert.calledWith(
3609
+ meeting.mediaProperties.webrtcMediaConnection.publishTrack,
3610
+ track
3611
+ );
3612
+ assert.equal(meeting.mediaProperties.shareTrack, track);
3613
+ assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
3614
+ };
3615
+
3616
+ it('requests screen share floor and publishes the screen share video track', async () => {
3617
+ await meeting.publishTracks({screenShare: {video: videoShareTrack}});
3618
+
3619
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.publishTrack);
3620
+ checkScreenShareVideoPublished(videoShareTrack);
3621
+ });
3622
+
3623
+ it('updates MuteState instance and publishes the track for main audio', async () => {
3624
+ await meeting.publishTracks({microphone: audioTrack});
3625
+
3626
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.publishTrack);
3627
+ checkAudioPublished(audioTrack);
3628
+ });
3629
+
3630
+ it('updates MuteState instance and publishes the track for main video', async () => {
3631
+ await meeting.publishTracks({camera: videoTrack});
3632
+
3633
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.publishTrack);
3634
+ checkVideoPublished(videoTrack);
3635
+ });
3636
+
3637
+ it('publishes audio, video and screen share together', async () => {
3638
+ await meeting.publishTracks({
3639
+ microphone: audioTrack,
3640
+ camera: videoTrack,
3641
+ screenShare: {
3642
+ video: videoShareTrack,
3643
+ },
3644
+ });
3645
+
3646
+ assert.calledThrice(meeting.mediaProperties.webrtcMediaConnection.publishTrack);
3647
+ checkAudioPublished(audioTrack);
3648
+ checkVideoPublished(videoTrack);
3649
+ checkScreenShareVideoPublished(videoShareTrack);
3650
+ });
3651
+ });
3652
+ it('creates instance and publishes with annotation info', async () => {
3653
+ const annotationInfo = {
3654
+ version: '1',
3655
+ policy: ANNOTATION_POLICY.APPROVAL,
3656
+ };
3657
+ await meeting.publishTracks({annotationInfo});
3658
+ assert.equal(meeting.annotationInfo, annotationInfo);
3659
+ });
3660
+
3661
+ describe('unpublishTracks', () => {
3662
+ beforeEach(async () => {
3663
+ await meeting.publishTracks({
3664
+ microphone: audioTrack,
3665
+ camera: videoTrack,
3666
+ screenShare: {video: videoShareTrack},
3667
+ });
3668
+ });
3669
+
3670
+ const checkAudioUnpublished = () => {
3671
+ assert.calledWith(
3672
+ meeting.mediaProperties.webrtcMediaConnection.unpublishTrack,
3673
+ audioTrack
3674
+ );
3675
+
3676
+ assert.equal(meeting.mediaProperties.audioTrack, null);
3677
+ assert.equal(meeting.mediaProperties.mediaDirection.sendAudio, 'fake value');
3678
+ };
3679
+
3680
+ const checkVideoUnpublished = () => {
3681
+ assert.calledWith(
3682
+ meeting.mediaProperties.webrtcMediaConnection.unpublishTrack,
3683
+ videoTrack
3684
+ );
3685
+
3686
+ assert.equal(meeting.mediaProperties.videoTrack, null);
3687
+ assert.equal(meeting.mediaProperties.mediaDirection.sendVideo, 'fake value');
3688
+ };
3689
+
3690
+ const checkScreenShareVideoUnpublished = () => {
3691
+ assert.calledWith(
3692
+ meeting.mediaProperties.webrtcMediaConnection.unpublishTrack,
3693
+ videoShareTrack
3694
+ );
3695
+
3696
+ assert.calledOnce(meeting.requestScreenShareFloor);
3697
+
3698
+ assert.equal(meeting.mediaProperties.shareTrack, null);
3699
+ assert.equal(meeting.mediaProperties.mediaDirection.sendShare, false);
3700
+ };
3701
+
3702
+ it('fails if there is no media connection', async () => {
3703
+ meeting.mediaProperties.webrtcMediaConnection = undefined;
3704
+ await assert.isRejected(
3705
+ meeting.unpublishTracks([audioTrack, videoTrack, videoShareTrack])
3706
+ );
3707
+ });
3708
+
3709
+ it('un-publishes the tracks correctly (all 3 together)', async () => {
3710
+ await meeting.unpublishTracks([audioTrack, videoTrack, videoShareTrack]);
3711
+
3712
+ assert.calledThrice(meeting.mediaProperties.webrtcMediaConnection.unpublishTrack);
3713
+ checkAudioUnpublished();
3714
+ checkVideoUnpublished();
3715
+ checkScreenShareVideoUnpublished();
3716
+ });
3717
+
3718
+ it('un-publishes the audio track correctly', async () => {
3719
+ await meeting.unpublishTracks([audioTrack]);
3720
+
3721
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.unpublishTrack);
3722
+ checkAudioUnpublished();
3723
+ });
3724
+
3725
+ it('un-publishes the video track correctly', async () => {
3726
+ await meeting.unpublishTracks([videoTrack]);
3273
3727
 
3274
- assert.calledWithMatch(Metrics.postEvent, {event: eventType.MOVE_MEDIA});
3275
- });
3728
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.unpublishTrack);
3729
+ checkVideoUnpublished();
3730
+ });
3276
3731
 
3277
- it('should call `MeetingUtil.joinMeetingOptions` with resourceId', async () => {
3278
- sinon.spy(MeetingUtil, 'joinMeetingOptions');
3279
- await meeting.moveFrom('resourceId');
3732
+ it('un-publishes the screen share video track correctly', async () => {
3733
+ await meeting.unpublishTracks([videoShareTrack]);
3280
3734
 
3281
- assert.calledWith(MeetingUtil.joinMeetingOptions, meeting);
3282
- assert.calledWith(MeetingUtil.leaveMeeting, meeting, {
3283
- resourceId: 'resourceId',
3284
- correlationId: meeting.correlationId,
3285
- moveMeeting: true,
3735
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.unpublishTrack);
3736
+ checkScreenShareVideoUnpublished();
3286
3737
  });
3738
+ });
3739
+ });
3740
+ });
3287
3741
 
3288
- assert.calledOnce(Metrics.sendBehavioralMetric);
3289
- assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.MOVE_FROM_SUCCESS);
3742
+ describe('#enableMusicMode', () => {
3743
+ beforeEach(() => {
3744
+ meeting.isMultistream = true;
3745
+ meeting.mediaProperties.webrtcMediaConnection = {
3746
+ setCodecParameters: sinon.stub().resolves({}),
3747
+ deleteCodecParameters: sinon.stub().resolves({}),
3748
+ };
3749
+ });
3750
+ [
3751
+ {shouldEnableMusicMode: true},
3752
+ {shouldEnableMusicMode: false},
3753
+ ].forEach(({shouldEnableMusicMode}) => {
3754
+ it(`fails if there is no media connection for shouldEnableMusicMode: ${shouldEnableMusicMode}`, async () => {
3755
+ meeting.mediaProperties.webrtcMediaConnection = undefined;
3756
+ await assert.isRejected(meeting.enableMusicMode(shouldEnableMusicMode));
3290
3757
  });
3758
+ });
3291
3759
 
3292
- it('should throw an error if moveFrom call fails', async () => {
3293
- MeetingUtil.joinMeeting = sinon.stub().returns(Promise.reject());
3294
- try {
3295
- await meeting.moveFrom('resourceId');
3296
- } catch {
3297
- assert.calledOnce(Metrics.sendBehavioralMetric);
3298
- assert.calledWith(Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.MOVE_FROM_FAILURE, {
3299
- correlation_id: meeting.correlationId,
3300
- locus_id: meeting.locusUrl.split('/').pop(),
3301
- reason: sinon.match.any,
3302
- stack: sinon.match.any,
3303
- });
3304
- }
3760
+ it('should set the codec parameters when shouldEnableMusicMode is true', async () => {
3761
+ await meeting.enableMusicMode(true);
3762
+ assert.calledOnceWithExactly(meeting.mediaProperties.webrtcMediaConnection.setCodecParameters, MediaType.AudioMain, {
3763
+ maxaveragebitrate: '64000',
3764
+ maxplaybackrate: '48000',
3305
3765
  });
3766
+ assert.notCalled(meeting.mediaProperties.webrtcMediaConnection.deleteCodecParameters);
3767
+ });
3768
+
3769
+ it('should set the codec parameters when shouldEnableMusicMode is false', async () => {
3770
+ await meeting.enableMusicMode(false);
3771
+ assert.calledOnceWithExactly(meeting.mediaProperties.webrtcMediaConnection.deleteCodecParameters, MediaType.AudioMain, [
3772
+ 'maxaveragebitrate',
3773
+ 'maxplaybackrate',
3774
+ ]);
3775
+ assert.notCalled(meeting.mediaProperties.webrtcMediaConnection.setCodecParameters);
3306
3776
  });
3307
3777
  });
3308
3778
 
@@ -3446,75 +3916,6 @@ describe('plugin-meetings', () => {
3446
3916
  );
3447
3917
  });
3448
3918
  });
3449
- describe('#closeLocalShare', () => {
3450
- it('should stop the stream, and trigger a media:stopped event when the local share stream stops', async () => {
3451
- await meeting.closeLocalShare();
3452
- assert.calledTwice(TriggerProxy.trigger);
3453
-
3454
- assert.equal(TriggerProxy.trigger.getCall(1).args[2], 'media:stopped');
3455
- assert.deepEqual(TriggerProxy.trigger.getCall(1).args[3], {type: 'localShare'});
3456
- });
3457
- });
3458
- describe('#closeLocalStream', () => {
3459
- it('should stop the stream, and trigger a media:stopped event when the local stream stops', async () => {
3460
- await meeting.closeLocalStream();
3461
- assert.calledTwice(TriggerProxy.trigger);
3462
- assert.calledWith(
3463
- TriggerProxy.trigger,
3464
- sinon.match.instanceOf(Meeting),
3465
- {file: 'meeting/index', function: 'closeLocalStream'},
3466
- 'media:stopped',
3467
- {type: 'local'}
3468
- );
3469
- });
3470
- });
3471
- describe('#setLocalTracks', () => {
3472
- it('stores the current video device as the preferred video device', () => {
3473
- const videoDevice = 'video1';
3474
- const fakeTrack = {getSettings: () => ({deviceId: videoDevice})};
3475
- const fakeStream = 'stream1';
3476
-
3477
- sandbox.stub(MeetingUtil, 'getTrack').returns({audioTrack: null, videoTrack: fakeTrack});
3478
- sandbox.stub(meeting.mediaProperties, 'setMediaSettings');
3479
- sandbox.stub(meeting.mediaProperties, 'setVideoDeviceId');
3480
-
3481
- meeting.setLocalTracks(fakeStream);
3482
-
3483
- assert.calledWith(meeting.mediaProperties.setVideoDeviceId, videoDevice);
3484
- });
3485
- });
3486
- describe('#setLocalShareTrack', () => {
3487
- it('should trigger a media:ready event with local share stream', () => {
3488
- const track = {
3489
- getSettings: sinon.stub().returns({
3490
- aspectRatio: '1.7',
3491
- frameRate: 30,
3492
- height: 1980,
3493
- width: 1080,
3494
- displaySurface: true,
3495
- cursor: true,
3496
- }),
3497
- };
3498
- const getVideoTracks = sinon.stub().returns([track]);
3499
-
3500
- meeting.mediaProperties.setLocalShareTrack = sinon.stub().returns(true);
3501
- meeting.mediaProperties.shareTrack = {getVideoTracks, getSettings: track.getSettings};
3502
- meeting.stopShare = sinon.stub().resolves(true);
3503
- meeting.mediaProperties.mediaDirection = {};
3504
- meeting.setLocalShareTrack(test1);
3505
- assert.calledTwice(TriggerProxy.trigger);
3506
- assert.calledWith(
3507
- TriggerProxy.trigger,
3508
- sinon.match.instanceOf(Meeting),
3509
- {file: 'meeting/index', function: 'setLocalShareTrack'},
3510
- 'media:ready'
3511
- );
3512
- assert.calledOnce(meeting.mediaProperties.setLocalShareTrack);
3513
- assert.equal(meeting.mediaProperties.localStream, undefined);
3514
- meeting.mediaProperties.shareTrack.onended();
3515
- assert.calledOnce(meeting.stopShare);
3516
- });
3517
- });
3518
3919
  describe('#setupMediaConnectionListeners', () => {
3519
3920
  let eventListeners;
3520
3921
 
@@ -3527,48 +3928,49 @@ describe('plugin-meetings', () => {
3527
3928
  eventListeners[event] = listener;
3528
3929
  }),
3529
3930
  };
3931
+ MediaUtil.createMediaStream.returns({id: 'stream'});
3530
3932
  });
3531
3933
 
3532
3934
  it('should register for all the correct RoapMediaConnection events', () => {
3533
3935
  meeting.setupMediaConnectionListeners();
3534
- assert.isFunction(eventListeners[MC.Event.ROAP_STARTED]);
3535
- assert.isFunction(eventListeners[MC.Event.ROAP_DONE]);
3536
- assert.isFunction(eventListeners[MC.Event.ROAP_FAILURE]);
3537
- assert.isFunction(eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]);
3538
- assert.isFunction(eventListeners[MC.Event.REMOTE_TRACK_ADDED]);
3539
- assert.isFunction(eventListeners[MC.Event.CONNECTION_STATE_CHANGED]);
3936
+ assert.isFunction(eventListeners[Event.ROAP_STARTED]);
3937
+ assert.isFunction(eventListeners[Event.ROAP_DONE]);
3938
+ assert.isFunction(eventListeners[Event.ROAP_FAILURE]);
3939
+ assert.isFunction(eventListeners[Event.ROAP_MESSAGE_TO_SEND]);
3940
+ assert.isFunction(eventListeners[Event.REMOTE_TRACK_ADDED]);
3941
+ assert.isFunction(eventListeners[Event.CONNECTION_STATE_CHANGED]);
3540
3942
  });
3541
3943
 
3542
3944
  it('should trigger a media:ready event when REMOTE_TRACK_ADDED is fired', () => {
3543
3945
  meeting.setupMediaConnectionListeners();
3544
- eventListeners[MC.Event.REMOTE_TRACK_ADDED]({
3946
+ eventListeners[Event.REMOTE_TRACK_ADDED]({
3545
3947
  track: 'track',
3546
- type: MC.RemoteTrackType.AUDIO,
3948
+ type: RemoteTrackType.AUDIO,
3547
3949
  });
3548
3950
  assert.equal(TriggerProxy.trigger.getCall(1).args[2], 'media:ready');
3549
3951
  assert.deepEqual(TriggerProxy.trigger.getCall(1).args[3], {
3550
3952
  type: 'remoteAudio',
3551
- stream: true,
3953
+ stream: {id: 'stream'},
3552
3954
  });
3553
3955
 
3554
- eventListeners[MC.Event.REMOTE_TRACK_ADDED]({
3956
+ eventListeners[Event.REMOTE_TRACK_ADDED]({
3555
3957
  track: 'track',
3556
- type: MC.RemoteTrackType.VIDEO,
3958
+ type: RemoteTrackType.VIDEO,
3557
3959
  });
3558
3960
  assert.equal(TriggerProxy.trigger.getCall(2).args[2], 'media:ready');
3559
3961
  assert.deepEqual(TriggerProxy.trigger.getCall(2).args[3], {
3560
3962
  type: 'remoteVideo',
3561
- stream: true,
3963
+ stream: {id: 'stream'},
3562
3964
  });
3563
3965
 
3564
- eventListeners[MC.Event.REMOTE_TRACK_ADDED]({
3966
+ eventListeners[Event.REMOTE_TRACK_ADDED]({
3565
3967
  track: 'track',
3566
- type: MC.RemoteTrackType.SCREENSHARE_VIDEO,
3968
+ type: RemoteTrackType.SCREENSHARE_VIDEO,
3567
3969
  });
3568
3970
  assert.equal(TriggerProxy.trigger.getCall(3).args[2], 'media:ready');
3569
3971
  assert.deepEqual(TriggerProxy.trigger.getCall(3).args[3], {
3570
3972
  type: 'remoteShare',
3571
- stream: true,
3973
+ stream: {id: 'stream'},
3572
3974
  });
3573
3975
  });
3574
3976
 
@@ -3613,51 +4015,51 @@ describe('plugin-meetings', () => {
3613
4015
  };
3614
4016
 
3615
4017
  it('should send metrics for SdpOfferCreationError error', () => {
3616
- const fakeError = new MC.Errors.SdpOfferCreationError(fakeErrorMessage, {
4018
+ const fakeError = new Errors.SdpOfferCreationError(fakeErrorMessage, {
3617
4019
  name: fakeErrorName,
3618
4020
  cause: {name: fakeRootCauseName},
3619
4021
  });
3620
4022
 
3621
- eventListeners[MC.Event.ROAP_FAILURE](fakeError);
4023
+ eventListeners[Event.ROAP_FAILURE](fakeError);
3622
4024
 
3623
4025
  checkMetricSent(eventType.LOCAL_SDP_GENERATED);
3624
4026
  checkBehavioralMetricSent(
3625
4027
  BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
3626
- MC.Errors.ErrorCode.SdpOfferCreationError,
4028
+ Errors.ErrorCode.SdpOfferCreationError,
3627
4029
  fakeErrorMessage,
3628
4030
  fakeRootCauseName
3629
4031
  );
3630
4032
  });
3631
4033
 
3632
4034
  it('should send metrics for SdpOfferHandlingError error', () => {
3633
- const fakeError = new MC.Errors.SdpOfferHandlingError(fakeErrorMessage, {
4035
+ const fakeError = new Errors.SdpOfferHandlingError(fakeErrorMessage, {
3634
4036
  name: fakeErrorName,
3635
4037
  cause: {name: fakeRootCauseName},
3636
4038
  });
3637
4039
 
3638
- eventListeners[MC.Event.ROAP_FAILURE](fakeError);
4040
+ eventListeners[Event.ROAP_FAILURE](fakeError);
3639
4041
 
3640
4042
  checkMetricSent(eventType.REMOTE_SDP_RECEIVED);
3641
4043
  checkBehavioralMetricSent(
3642
4044
  BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
3643
- MC.Errors.ErrorCode.SdpOfferHandlingError,
4045
+ Errors.ErrorCode.SdpOfferHandlingError,
3644
4046
  fakeErrorMessage,
3645
4047
  fakeRootCauseName
3646
4048
  );
3647
4049
  });
3648
4050
 
3649
4051
  it('should send metrics for SdpAnswerHandlingError error', () => {
3650
- const fakeError = new MC.Errors.SdpAnswerHandlingError(fakeErrorMessage, {
4052
+ const fakeError = new Errors.SdpAnswerHandlingError(fakeErrorMessage, {
3651
4053
  name: fakeErrorName,
3652
4054
  cause: {name: fakeRootCauseName},
3653
4055
  });
3654
4056
 
3655
- eventListeners[MC.Event.ROAP_FAILURE](fakeError);
4057
+ eventListeners[Event.ROAP_FAILURE](fakeError);
3656
4058
 
3657
4059
  checkMetricSent(eventType.REMOTE_SDP_RECEIVED);
3658
4060
  checkBehavioralMetricSent(
3659
4061
  BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
3660
- MC.Errors.ErrorCode.SdpAnswerHandlingError,
4062
+ Errors.ErrorCode.SdpAnswerHandlingError,
3661
4063
  fakeErrorMessage,
3662
4064
  fakeRootCauseName
3663
4065
  );
@@ -3665,15 +4067,15 @@ describe('plugin-meetings', () => {
3665
4067
 
3666
4068
  it('should send metrics for SdpError error', () => {
3667
4069
  // SdpError is usually without a cause
3668
- const fakeError = new MC.Errors.SdpError(fakeErrorMessage, {name: fakeErrorName});
4070
+ const fakeError = new Errors.SdpError(fakeErrorMessage, {name: fakeErrorName});
3669
4071
 
3670
- eventListeners[MC.Event.ROAP_FAILURE](fakeError);
4072
+ eventListeners[Event.ROAP_FAILURE](fakeError);
3671
4073
 
3672
4074
  checkMetricSent(eventType.LOCAL_SDP_GENERATED);
3673
4075
  // expectedMetadataType is the error name in this case
3674
4076
  checkBehavioralMetricSent(
3675
4077
  BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE,
3676
- MC.Errors.ErrorCode.SdpError,
4078
+ Errors.ErrorCode.SdpError,
3677
4079
  fakeErrorMessage,
3678
4080
  fakeErrorName
3679
4081
  );
@@ -3681,24 +4083,24 @@ describe('plugin-meetings', () => {
3681
4083
 
3682
4084
  it('should send metrics for IceGatheringError error', () => {
3683
4085
  // IceGatheringError is usually without a cause
3684
- const fakeError = new MC.Errors.IceGatheringError(fakeErrorMessage, {
4086
+ const fakeError = new Errors.IceGatheringError(fakeErrorMessage, {
3685
4087
  name: fakeErrorName,
3686
4088
  });
3687
4089
 
3688
- eventListeners[MC.Event.ROAP_FAILURE](fakeError);
4090
+ eventListeners[Event.ROAP_FAILURE](fakeError);
3689
4091
 
3690
4092
  checkMetricSent(eventType.LOCAL_SDP_GENERATED);
3691
4093
  // expectedMetadataType is the error name in this case
3692
4094
  checkBehavioralMetricSent(
3693
4095
  BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE,
3694
- MC.Errors.ErrorCode.IceGatheringError,
4096
+ Errors.ErrorCode.IceGatheringError,
3695
4097
  fakeErrorMessage,
3696
4098
  fakeErrorName
3697
4099
  );
3698
4100
  });
3699
4101
  });
3700
4102
 
3701
- describe('handles MC.Event.ROAP_MESSAGE_TO_SEND correctly', () => {
4103
+ describe('handles Event.ROAP_MESSAGE_TO_SEND correctly', () => {
3702
4104
  let sendRoapOKStub;
3703
4105
  let sendRoapMediaRequestStub;
3704
4106
  let sendRoapAnswerStub;
@@ -3716,7 +4118,7 @@ describe('plugin-meetings', () => {
3716
4118
  });
3717
4119
 
3718
4120
  it('handles OK message correctly', () => {
3719
- eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({
4121
+ eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
3720
4122
  roapMessage: {messageType: 'OK', seq: 1},
3721
4123
  });
3722
4124
 
@@ -3735,7 +4137,7 @@ describe('plugin-meetings', () => {
3735
4137
  });
3736
4138
 
3737
4139
  it('handles OFFER message correctly', () => {
3738
- eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({
4140
+ eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
3739
4141
  roapMessage: {
3740
4142
  messageType: 'OFFER',
3741
4143
  seq: 1,
@@ -3761,7 +4163,7 @@ describe('plugin-meetings', () => {
3761
4163
  });
3762
4164
 
3763
4165
  it('handles ANSWER message correctly', () => {
3764
- eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({
4166
+ eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
3765
4167
  roapMessage: {
3766
4168
  messageType: 'ANSWER',
3767
4169
  seq: 10,
@@ -3788,7 +4190,7 @@ describe('plugin-meetings', () => {
3788
4190
  it('sends metrics if fails to send roap ANSWER message', async () => {
3789
4191
  sendRoapAnswerStub.rejects(new Error('sending answer failed'));
3790
4192
 
3791
- await eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({
4193
+ await eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
3792
4194
  roapMessage: {
3793
4195
  messageType: 'ANSWER',
3794
4196
  seq: 10,
@@ -3810,88 +4212,401 @@ describe('plugin-meetings', () => {
3810
4212
  );
3811
4213
  });
3812
4214
 
3813
- [MC.ErrorType.CONFLICT, MC.ErrorType.DOUBLECONFLICT].forEach((errorType) =>
3814
- it(`handles ERROR message indicating glare condition correctly (errorType=${errorType})`, () => {
3815
- eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({
3816
- roapMessage: {
3817
- messageType: 'ERROR',
3818
- seq: 10,
3819
- errorType,
3820
- tieBreaker: 12345,
3821
- },
3822
- });
4215
+ [ErrorType.CONFLICT, ErrorType.DOUBLECONFLICT].forEach((errorType) =>
4216
+ it(`handles ERROR message indicating glare condition correctly (errorType=${errorType})`, () => {
4217
+ eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
4218
+ roapMessage: {
4219
+ messageType: 'ERROR',
4220
+ seq: 10,
4221
+ errorType,
4222
+ tieBreaker: 12345,
4223
+ },
4224
+ });
4225
+
4226
+ assert.calledOnce(Metrics.sendBehavioralMetric);
4227
+ assert.calledWithMatch(
4228
+ Metrics.sendBehavioralMetric,
4229
+ BEHAVIORAL_METRICS.ROAP_GLARE_CONDITION,
4230
+ {
4231
+ correlation_id: meeting.correlationId,
4232
+ locus_id: meeting.locusUrl.split('/').pop(),
4233
+ sequence: 10,
4234
+ }
4235
+ );
4236
+
4237
+ assert.calledOnce(sendRoapErrorStub);
4238
+ assert.calledWith(sendRoapErrorStub, {
4239
+ seq: 10,
4240
+ errorType,
4241
+ mediaId: meeting.mediaId,
4242
+ correlationId: meeting.correlationId,
4243
+ });
4244
+ })
4245
+ );
4246
+
4247
+ it('handles ERROR message indicating other errors correctly', () => {
4248
+ eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
4249
+ roapMessage: {
4250
+ messageType: 'ERROR',
4251
+ seq: 10,
4252
+ errorType: ErrorType.FAILED,
4253
+ tieBreaker: 12345,
4254
+ },
4255
+ });
4256
+
4257
+ assert.notCalled(Metrics.sendBehavioralMetric);
4258
+
4259
+ assert.calledOnce(sendRoapErrorStub);
4260
+ assert.calledWith(sendRoapErrorStub, {
4261
+ seq: 10,
4262
+ errorType: ErrorType.FAILED,
4263
+ mediaId: meeting.mediaId,
4264
+ correlationId: meeting.correlationId,
4265
+ });
4266
+ });
4267
+ });
4268
+ });
4269
+ describe('#setUpLocusInfoSelfListener', () => {
4270
+ it('listens to the self unadmitted guest event', (done) => {
4271
+ meeting.startKeepAlive = sinon.stub();
4272
+ meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_UNADMITTED_GUEST', test1);
4273
+ assert.calledOnceWithExactly(meeting.startKeepAlive);
4274
+ assert.calledTwice(TriggerProxy.trigger);
4275
+ assert.calledWith(
4276
+ TriggerProxy.trigger,
4277
+ sinon.match.instanceOf(Meeting),
4278
+ {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
4279
+ 'meeting:self:lobbyWaiting',
4280
+ {payload: test1}
4281
+ );
4282
+ done();
4283
+ });
4284
+ it('listens to the self admitted guest event', (done) => {
4285
+ meeting.stopKeepAlive = sinon.stub();
4286
+ meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_ADMITTED_GUEST', test1);
4287
+ assert.calledOnceWithExactly(meeting.stopKeepAlive);
4288
+ assert.calledTwice(TriggerProxy.trigger);
4289
+ assert.calledWith(
4290
+ TriggerProxy.trigger,
4291
+ sinon.match.instanceOf(Meeting),
4292
+ {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
4293
+ 'meeting:self:guestAdmitted',
4294
+ {payload: test1}
4295
+ );
4296
+ done();
4297
+ });
4298
+
4299
+ it('listens to the breakouts changed event', () => {
4300
+ meeting.breakouts.updateBreakoutSessions = sinon.stub();
4301
+
4302
+ const payload = 'payload';
4303
+
4304
+ meeting.locusInfo.emit(
4305
+ {function: 'test', file: 'test'},
4306
+ 'SELF_MEETING_BREAKOUTS_CHANGED',
4307
+ payload
4308
+ );
4309
+
4310
+ assert.calledOnceWithExactly(meeting.breakouts.updateBreakoutSessions, payload);
4311
+ assert.calledWith(
4312
+ TriggerProxy.trigger,
4313
+ meeting,
4314
+ {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
4315
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_UPDATE
4316
+ );
4317
+ });
4318
+
4319
+ it('listens to the self roles changed event', () => {
4320
+ const payload = {oldRoles: [], newRoles: ['COHOST']};
4321
+ meeting.breakouts.updateCanManageBreakouts = sinon.stub();
4322
+
4323
+ meeting.locusInfo.emit(
4324
+ {function: 'test', file: 'test'},
4325
+ 'SELF_ROLES_CHANGED',
4326
+ payload
4327
+ );
4328
+
4329
+ assert.calledOnceWithExactly(meeting.breakouts.updateCanManageBreakouts, true);
4330
+ assert.calledWith(
4331
+ TriggerProxy.trigger,
4332
+ meeting,
4333
+ {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
4334
+ EVENT_TRIGGERS.MEETING_SELF_ROLES_CHANGED,
4335
+ {payload}
4336
+ );
4337
+ });
4338
+ });
4339
+
4340
+ describe('#setUpBreakoutsListener', () => {
4341
+ it('listens to the closing event from breakouts and triggers the closing event', () => {
4342
+ TriggerProxy.trigger.reset();
4343
+ meeting.breakouts.trigger('BREAKOUTS_CLOSING');
4344
+
4345
+ assert.calledWith(
4346
+ TriggerProxy.trigger,
4347
+ meeting,
4348
+ {file: 'meeting/index', function: 'setUpBreakoutsListener'},
4349
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_CLOSING
4350
+ );
4351
+ });
4352
+
4353
+ it('listens to the message event from breakouts and triggers the message event', () => {
4354
+ TriggerProxy.trigger.reset();
4355
+
4356
+ const messageEvent = 'message';
4357
+
4358
+ meeting.breakouts.trigger('MESSAGE', messageEvent);
4359
+
4360
+ assert.calledWith(
4361
+ TriggerProxy.trigger,
4362
+ meeting,
4363
+ {file: 'meeting/index', function: 'setUpBreakoutsListener'},
4364
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_MESSAGE,
4365
+ messageEvent
4366
+ );
4367
+ });
4368
+
4369
+ it('listens to the members update event from breakouts and triggers the breakouts update event', () => {
4370
+ TriggerProxy.trigger.reset();
4371
+ meeting.breakouts.trigger('MEMBERS_UPDATE');
4372
+
4373
+ assert.calledWith(
4374
+ TriggerProxy.trigger,
4375
+ meeting,
4376
+ {file: 'meeting/index', function: 'setUpBreakoutsListener'},
4377
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_UPDATE
4378
+ );
4379
+ });
4380
+
4381
+ it('listens to the ask return to main event from breakouts and triggers the ask return to main event from meeting', () => {
4382
+ TriggerProxy.trigger.reset();
4383
+ meeting.breakouts.trigger('ASK_RETURN_TO_MAIN');
4384
+ assert.calledWith(
4385
+ TriggerProxy.trigger,
4386
+ meeting,
4387
+ {file: 'meeting/index', function: 'setUpBreakoutsListener'},
4388
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_ASK_RETURN_TO_MAIN
4389
+ );
4390
+ });
4391
+
4392
+ it('listens to the leave event from breakouts and triggers the breakout leave event', () => {
4393
+ TriggerProxy.trigger.reset();
4394
+ meeting.breakouts.trigger('LEAVE_BREAKOUT');
4395
+ assert.calledWith(
4396
+ TriggerProxy.trigger,
4397
+ meeting,
4398
+ {file: 'meeting/index', function: 'setUpBreakoutsListener'},
4399
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_LEAVE
4400
+ );
4401
+ });
4402
+
4403
+ it('listens to the breakout ask for help event and triggers the ask for help event', () => {
4404
+ TriggerProxy.trigger.reset();
4405
+ const helpEvent = {sessionId:'sessionId', participant: 'participant'}
4406
+ meeting.breakouts.trigger('ASK_FOR_HELP', helpEvent);
4407
+ assert.calledWith(
4408
+ TriggerProxy.trigger,
4409
+ meeting,
4410
+ {file: 'meeting/index', function: 'setUpBreakoutsListener'},
4411
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_ASK_FOR_HELP,
4412
+ helpEvent
4413
+ );
4414
+ });
4415
+
4416
+ it('listens to the preAssignments update event from breakouts and triggers the update event', () => {
4417
+ TriggerProxy.trigger.reset();
4418
+ meeting.breakouts.trigger('PRE_ASSIGNMENTS_UPDATE');
4419
+
4420
+ assert.calledWith(
4421
+ TriggerProxy.trigger,
4422
+ meeting,
4423
+ {file: 'meeting/index', function: 'setUpBreakoutsListener'},
4424
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_PRE_ASSIGNMENTS_UPDATE
4425
+ );
4426
+ });
4427
+ });
4428
+
4429
+ describe('#setupLocusControlsListener', () => {
4430
+ it('listens to the locus breakouts update event', () => {
4431
+ const locus = {
4432
+ breakout: 'breakout',
4433
+ };
4434
+
4435
+ meeting.breakouts.updateBreakout = sinon.stub();
4436
+ meeting.locusInfo.emit(
4437
+ {function: 'test', file: 'test'},
4438
+ 'CONTROLS_MEETING_BREAKOUT_UPDATED',
4439
+ locus
4440
+ );
4441
+
4442
+ assert.calledOnceWithExactly(meeting.breakouts.updateBreakout, locus.breakout);
4443
+ assert.calledWith(
4444
+ TriggerProxy.trigger,
4445
+ meeting,
4446
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4447
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_UPDATE
4448
+ );
4449
+ });
4450
+
4451
+ it('listens to CONTROLS_MUTE_ON_ENTRY_CHANGED', async () => {
4452
+ const state = {example: 'value'}
4453
+
4454
+ await meeting.locusInfo.emitScoped(
4455
+ {function: 'test', file: 'test'},
4456
+ LOCUSINFO.EVENTS.CONTROLS_MUTE_ON_ENTRY_CHANGED,
4457
+ {state}
4458
+ );
4459
+
4460
+ assert.calledWith(
4461
+ TriggerProxy.trigger,
4462
+ meeting,
4463
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4464
+ EVENT_TRIGGERS.MEETING_CONTROLS_MUTE_ON_ENTRY_UPDATED,
4465
+ {state},
4466
+ );
4467
+ });
4468
+
4469
+ it('listens to MEETING_CONTROLS_SHARE_CONTROL_UPDATED', async () => {
4470
+ const state = {example: 'value'}
4471
+
4472
+ await meeting.locusInfo.emitScoped(
4473
+ {function: 'test', file: 'test'},
4474
+ LOCUSINFO.EVENTS.CONTROLS_SHARE_CONTROL_CHANGED,
4475
+ {state}
4476
+ );
4477
+
4478
+ assert.calledWith(
4479
+ TriggerProxy.trigger,
4480
+ meeting,
4481
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4482
+ EVENT_TRIGGERS.MEETING_CONTROLS_SHARE_CONTROL_UPDATED,
4483
+ {state},
4484
+ );
4485
+ });
4486
+
4487
+ it('listens to MEETING_CONTROLS_DISALLOW_UNMUTE_UPDATED', async () => {
4488
+ const state = {example: 'value'}
3823
4489
 
3824
- assert.calledOnce(Metrics.sendBehavioralMetric);
3825
- assert.calledWithMatch(
3826
- Metrics.sendBehavioralMetric,
3827
- BEHAVIORAL_METRICS.ROAP_GLARE_CONDITION,
3828
- {
3829
- correlation_id: meeting.correlationId,
3830
- locus_id: meeting.locusUrl.split('/').pop(),
3831
- sequence: 10,
3832
- }
3833
- );
4490
+ await meeting.locusInfo.emitScoped(
4491
+ {function: 'test', file: 'test'},
4492
+ LOCUSINFO.EVENTS.CONTROLS_DISALLOW_UNMUTE_CHANGED,
4493
+ {state}
4494
+ );
3834
4495
 
3835
- assert.calledOnce(sendRoapErrorStub);
3836
- assert.calledWith(sendRoapErrorStub, {
3837
- seq: 10,
3838
- errorType,
3839
- mediaId: meeting.mediaId,
3840
- correlationId: meeting.correlationId,
3841
- });
3842
- })
4496
+ assert.calledWith(
4497
+ TriggerProxy.trigger,
4498
+ meeting,
4499
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4500
+ EVENT_TRIGGERS.MEETING_CONTROLS_DISALLOW_UNMUTE_UPDATED,
4501
+ {state},
3843
4502
  );
4503
+ });
3844
4504
 
3845
- it('handles ERROR message indicating other errors correctly', () => {
3846
- eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({
3847
- roapMessage: {
3848
- messageType: 'ERROR',
3849
- seq: 10,
3850
- errorType: MC.ErrorType.FAILED,
3851
- tieBreaker: 12345,
3852
- },
3853
- });
4505
+ it('listens to MEETING_CONTROLS_REACTIONS_UPDATED', async () => {
4506
+ const state = {example: 'value'}
3854
4507
 
3855
- assert.notCalled(Metrics.sendBehavioralMetric);
4508
+ await meeting.locusInfo.emitScoped(
4509
+ {function: 'test', file: 'test'},
4510
+ LOCUSINFO.EVENTS.CONTROLS_REACTIONS_CHANGED,
4511
+ {state}
4512
+ );
3856
4513
 
3857
- assert.calledOnce(sendRoapErrorStub);
3858
- assert.calledWith(sendRoapErrorStub, {
3859
- seq: 10,
3860
- errorType: MC.ErrorType.FAILED,
3861
- mediaId: meeting.mediaId,
3862
- correlationId: meeting.correlationId,
3863
- });
3864
- });
4514
+ assert.calledWith(
4515
+ TriggerProxy.trigger,
4516
+ meeting,
4517
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4518
+ EVENT_TRIGGERS.MEETING_CONTROLS_REACTIONS_UPDATED,
4519
+ {state},
4520
+ );
3865
4521
  });
3866
- });
3867
- describe('#setUpLocusInfoSelfListener', () => {
3868
- it('listens to the self unadmitted guest event', (done) => {
3869
- meeting.startKeepAlive = sinon.stub();
3870
- meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_UNADMITTED_GUEST', test1);
3871
- assert.calledOnceWithExactly(meeting.startKeepAlive);
3872
- assert.calledTwice(TriggerProxy.trigger);
4522
+
4523
+ it('listens to MEETING_CONTROLS_VIEW_THE_PARTICIPANTS_LIST_UPDATED', async () => {
4524
+ const state = {example: 'value'}
4525
+
4526
+ await meeting.locusInfo.emitScoped(
4527
+ {function: 'test', file: 'test'},
4528
+ LOCUSINFO.EVENTS.CONTROLS_VIEW_THE_PARTICIPANTS_LIST_CHANGED,
4529
+ {state}
4530
+ );
4531
+
3873
4532
  assert.calledWith(
3874
4533
  TriggerProxy.trigger,
3875
- sinon.match.instanceOf(Meeting),
3876
- {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
3877
- 'meeting:self:lobbyWaiting',
3878
- {payload: test1}
4534
+ meeting,
4535
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4536
+ EVENT_TRIGGERS.MEETING_CONTROLS_VIEW_THE_PARTICIPANTS_LIST_UPDATED,
4537
+ {state},
3879
4538
  );
3880
- done();
3881
4539
  });
3882
- it('listens to the self admitted guest event', (done) => {
3883
- meeting.stopKeepAlive = sinon.stub();
3884
- meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_ADMITTED_GUEST', test1);
3885
- assert.calledOnceWithExactly(meeting.stopKeepAlive);
3886
- assert.calledTwice(TriggerProxy.trigger);
4540
+
4541
+ it('listens to MEETING_CONTROLS_RAISE_HAND_UPDATED', async () => {
4542
+ const state = {example: 'value'}
4543
+
4544
+ await meeting.locusInfo.emitScoped(
4545
+ {function: 'test', file: 'test'},
4546
+ LOCUSINFO.EVENTS.CONTROLS_RAISE_HAND_CHANGED,
4547
+ {state}
4548
+ );
4549
+
3887
4550
  assert.calledWith(
3888
4551
  TriggerProxy.trigger,
3889
- sinon.match.instanceOf(Meeting),
3890
- {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
3891
- 'meeting:self:guestAdmitted',
3892
- {payload: test1}
4552
+ meeting,
4553
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4554
+ EVENT_TRIGGERS.MEETING_CONTROLS_RAISE_HAND_UPDATED,
4555
+ {state},
3893
4556
  );
3894
- done();
4557
+ });
4558
+
4559
+ it('listens to MEETING_CONTROLS_VIDEO_UPDATED', async () => {
4560
+ const state = {example: 'value'}
4561
+
4562
+ await meeting.locusInfo.emitScoped(
4563
+ {function: 'test', file: 'test'},
4564
+ LOCUSINFO.EVENTS.CONTROLS_VIDEO_CHANGED,
4565
+ {state}
4566
+ );
4567
+
4568
+ assert.calledWith(
4569
+ TriggerProxy.trigger,
4570
+ meeting,
4571
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4572
+ EVENT_TRIGGERS.MEETING_CONTROLS_VIDEO_UPDATED,
4573
+ {state},
4574
+ );
4575
+ });
4576
+
4577
+ it('listens to the timing that user joined into breakout', async () => {
4578
+ const mainLocusUrl = 'mainLocusUrl123';
4579
+
4580
+ meeting.meetingRequest.getLocusStatusByUrl = sinon.stub().returns(Promise.resolve());
4581
+
4582
+ await meeting.locusInfo.emit(
4583
+ {function: 'test', file: 'test'},
4584
+ 'CONTROLS_JOIN_BREAKOUT_FROM_MAIN',
4585
+ {mainLocusUrl}
4586
+ );
4587
+
4588
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusStatusByUrl, mainLocusUrl);
4589
+ const error = {statusCode: 403};
4590
+ meeting.meetingRequest.getLocusStatusByUrl.rejects(error);
4591
+ meeting.locusInfo.clearMainSessionLocusCache = sinon.stub();
4592
+ await meeting.locusInfo.emit(
4593
+ {function: 'test', file: 'test'},
4594
+ 'CONTROLS_JOIN_BREAKOUT_FROM_MAIN',
4595
+ {mainLocusUrl}
4596
+ );
4597
+
4598
+ assert.calledOnce(meeting.locusInfo.clearMainSessionLocusCache);
4599
+
4600
+ const otherError = new Error('something wrong');
4601
+ meeting.meetingRequest.getLocusStatusByUrl.rejects(otherError);
4602
+ meeting.locusInfo.clearMainSessionLocusCache = sinon.stub();
4603
+ await meeting.locusInfo.emit(
4604
+ {function: 'test', file: 'test'},
4605
+ 'CONTROLS_JOIN_BREAKOUT_FROM_MAIN',
4606
+ {mainLocusUrl}
4607
+ );
4608
+
4609
+ assert.notCalled(meeting.locusInfo.clearMainSessionLocusCache);
3895
4610
  });
3896
4611
  });
3897
4612
 
@@ -3900,6 +4615,11 @@ describe('plugin-meetings', () => {
3900
4615
  const newLocusUrl = 'newLocusUrl/12345';
3901
4616
 
3902
4617
  meeting.members = {locusUrlUpdate: sinon.stub().returns(Promise.resolve(test1))};
4618
+ meeting.recordingController = {setLocusUrl: sinon.stub().returns(undefined)};
4619
+ meeting.controlsOptionsManager = {setLocusUrl: sinon.stub().returns(undefined)};
4620
+
4621
+ meeting.breakouts.locusUrlUpdate = sinon.stub();
4622
+ meeting.annotation.locusUrlUpdate = sinon.stub();
3903
4623
 
3904
4624
  meeting.locusInfo.emit(
3905
4625
  {function: 'test', file: 'test'},
@@ -3907,11 +4627,57 @@ describe('plugin-meetings', () => {
3907
4627
  newLocusUrl
3908
4628
  );
3909
4629
  assert.calledWith(meeting.members.locusUrlUpdate, newLocusUrl);
4630
+ assert.calledOnceWithExactly(meeting.breakouts.locusUrlUpdate, newLocusUrl);
4631
+ assert.calledOnceWithExactly(meeting.annotation.locusUrlUpdate, newLocusUrl);
4632
+ assert.calledWith(meeting.members.locusUrlUpdate, newLocusUrl);
4633
+ assert.calledWith(meeting.recordingController.setLocusUrl, newLocusUrl);
4634
+ assert.calledWith(meeting.controlsOptionsManager.setLocusUrl, newLocusUrl);
3910
4635
  assert.equal(meeting.locusUrl, newLocusUrl);
3911
4636
  assert(meeting.locusId, '12345');
3912
4637
  done();
3913
4638
  });
3914
4639
  });
4640
+
4641
+ describe('#setUpLocusServicesListener', () => {
4642
+ it('listens to the locus services update event', (done) => {
4643
+ const newLocusServices = {
4644
+ services: {
4645
+ record: {
4646
+ url: 'url',
4647
+ },
4648
+ approval: {
4649
+ url: 'url',
4650
+ },
4651
+ },
4652
+ };
4653
+
4654
+ meeting.recordingController = {
4655
+ setServiceUrl: sinon.stub().returns(undefined),
4656
+ setSessionId: sinon.stub().returns(undefined),
4657
+ };
4658
+ meeting.annotation = {
4659
+ approvalUrlUpdate: sinon.stub().returns(undefined),
4660
+ };
4661
+
4662
+ meeting.locusInfo.emit(
4663
+ {function: 'test', file: 'test'},
4664
+ 'LINKS_SERVICES',
4665
+ newLocusServices
4666
+ );
4667
+
4668
+ assert.calledWith(
4669
+ meeting.recordingController.setServiceUrl,
4670
+ newLocusServices.services.record.url,
4671
+ );
4672
+ assert.calledWith(
4673
+ meeting.annotation.approvalUrlUpdate,
4674
+ newLocusServices.services.approval.url,
4675
+ );
4676
+ assert.calledOnce(meeting.recordingController.setSessionId);
4677
+ done();
4678
+ });
4679
+ });
4680
+
3915
4681
  describe('#setUpLocusInfoMediaInactiveListener', () => {
3916
4682
  it('listens to disconnect due to un activity ', (done) => {
3917
4683
  TriggerProxy.trigger.reset();
@@ -4057,20 +4823,6 @@ describe('plugin-meetings', () => {
4057
4823
  assert.calledOnce(meeting.mediaProperties.unsetRemoteTracks);
4058
4824
  });
4059
4825
  });
4060
- describe('#unsetLocalVideoTrack', () => {
4061
- it('should unset the local stream and return null', () => {
4062
- meeting.mediaProperties.unsetLocalVideoTrack = sinon.stub().returns(true);
4063
- meeting.unsetLocalVideoTrack();
4064
- assert.calledOnce(meeting.mediaProperties.unsetLocalVideoTrack);
4065
- });
4066
- });
4067
- describe('#unsetLocalShareTrack', () => {
4068
- it('should unset the local share stream and return null', () => {
4069
- meeting.mediaProperties.unsetLocalShareTrack = sinon.stub().returns(true);
4070
- meeting.unsetLocalShareTrack();
4071
- assert.calledOnce(meeting.mediaProperties.unsetLocalShareTrack);
4072
- });
4073
- });
4074
4826
  // TODO: remove
4075
4827
  describe('#setMercuryListener', () => {
4076
4828
  it('should listen to mercury events', () => {
@@ -4237,28 +4989,7 @@ describe('plugin-meetings', () => {
4237
4989
  checkParseMeetingInfo(expectedInfoToParse);
4238
4990
  });
4239
4991
  });
4240
- describe('#parseLocus', () => {
4241
- describe('when CALL and participants', () => {
4242
- beforeEach(() => {
4243
- meeting.setLocus = sinon.stub().returns(true);
4244
- MeetingUtil.getLocusPartner = sinon.stub().returns({person: {sipUrl: uuid3}});
4245
- });
4246
- it('should parse the locus object and set meeting properties and return null', () => {
4247
- meeting.type = 'CALL';
4248
- meeting.parseLocus({url: url1, participants: [{id: uuid1}], self: {id: uuid2}});
4249
- assert.calledOnce(meeting.setLocus);
4250
- assert.calledWith(meeting.setLocus, {
4251
- url: url1,
4252
- participants: [{id: uuid1}],
4253
- self: {id: uuid2},
4254
- });
4255
- assert.calledOnce(MeetingUtil.getLocusPartner);
4256
- assert.calledWith(MeetingUtil.getLocusPartner, [{id: uuid1}], {id: uuid2});
4257
- assert.deepEqual(meeting.partner, {person: {sipUrl: uuid3}});
4258
- assert.equal(meeting.sipUri, uuid3);
4259
- });
4260
- });
4261
- });
4992
+
4262
4993
  describe('#setCorrelationId', () => {
4263
4994
  it('should set the correlationId and return undefined', () => {
4264
4995
  assert.ok(meeting.correlationId);
@@ -4319,25 +5050,37 @@ describe('plugin-meetings', () => {
4319
5050
  let inMeetingActionsSetSpy;
4320
5051
  let canUserLockSpy;
4321
5052
  let canUserUnlockSpy;
4322
- let canUserRecordSpy;
5053
+ let canUserStartSpy;
4323
5054
  let canUserStopSpy;
4324
5055
  let canUserPauseSpy;
4325
5056
  let canUserResumeSpy;
5057
+ let canSetMuteOnEntrySpy;
5058
+ let canUnsetMuteOnEntrySpy;
5059
+ let canSetDisallowUnmuteSpy;
5060
+ let canUnsetDisallowUnmuteSpy;
4326
5061
  let canUserRaiseHandSpy;
4327
5062
  let bothLeaveAndEndMeetingAvailableSpy;
4328
5063
  let canUserLowerAllHandsSpy;
4329
5064
  let canUserLowerSomeoneElsesHandSpy;
4330
5065
  let waitingForOthersToJoinSpy;
4331
5066
  let handleDataChannelUrlChangeSpy;
5067
+ let canSendReactionsSpy;
5068
+ let canUserRenameSelfAndObservedSpy;
5069
+ let canUserRenameOthersSpy;
5070
+ let hasHintsSpy;
4332
5071
 
4333
5072
  beforeEach(() => {
4334
5073
  locusInfoOnSpy = sinon.spy(meeting.locusInfo, 'on');
4335
5074
  canUserLockSpy = sinon.spy(MeetingUtil, 'canUserLock');
4336
5075
  canUserUnlockSpy = sinon.spy(MeetingUtil, 'canUserUnlock');
4337
- canUserRecordSpy = sinon.spy(MeetingUtil, 'canUserRecord');
4338
- canUserStopSpy = sinon.spy(MeetingUtil, 'canUserStop');
4339
- canUserPauseSpy = sinon.spy(MeetingUtil, 'canUserPause');
4340
- canUserResumeSpy = sinon.spy(MeetingUtil, 'canUserResume');
5076
+ canUserStartSpy = sinon.spy(RecordingUtil, 'canUserStart');
5077
+ canUserStopSpy = sinon.spy(RecordingUtil, 'canUserStop');
5078
+ canUserPauseSpy = sinon.spy(RecordingUtil, 'canUserPause');
5079
+ canUserResumeSpy = sinon.spy(RecordingUtil, 'canUserResume');
5080
+ canSetMuteOnEntrySpy = sinon.spy(ControlsOptionsUtil, 'canSetMuteOnEntry');
5081
+ canUnsetMuteOnEntrySpy = sinon.spy(ControlsOptionsUtil, 'canUnsetMuteOnEntry');
5082
+ canSetDisallowUnmuteSpy = sinon.spy(ControlsOptionsUtil, 'canSetDisallowUnmute');
5083
+ canUnsetDisallowUnmuteSpy = sinon.spy(ControlsOptionsUtil, 'canUnsetDisallowUnmute');
4341
5084
  inMeetingActionsSetSpy = sinon.spy(meeting.inMeetingActions, 'set');
4342
5085
  canUserRaiseHandSpy = sinon.spy(MeetingUtil, 'canUserRaiseHand');
4343
5086
  canUserLowerAllHandsSpy = sinon.spy(MeetingUtil, 'canUserLowerAllHands');
@@ -4348,6 +5091,9 @@ describe('plugin-meetings', () => {
4348
5091
  canUserLowerSomeoneElsesHandSpy = sinon.spy(MeetingUtil, 'canUserLowerSomeoneElsesHand');
4349
5092
  waitingForOthersToJoinSpy = sinon.spy(MeetingUtil, 'waitingForOthersToJoin');
4350
5093
  handleDataChannelUrlChangeSpy = sinon.spy(meeting, 'handleDataChannelUrlChange');
5094
+ canSendReactionsSpy = sinon.spy(MeetingUtil, 'canSendReactions');
5095
+ canUserRenameSelfAndObservedSpy = sinon.spy(MeetingUtil, 'canUserRenameSelfAndObserved');
5096
+ canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
4351
5097
  });
4352
5098
 
4353
5099
  afterEach(() => {
@@ -4357,6 +5103,10 @@ describe('plugin-meetings', () => {
4357
5103
  });
4358
5104
 
4359
5105
  it('registers the correct MEETING_INFO_UPDATED event', () => {
5106
+ // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
5107
+ const restorableHasHints = ControlsOptionsUtil.hasHints;
5108
+ ControlsOptionsUtil.hasHints = sinon.stub().returns(true);
5109
+
4360
5110
  meeting.setUpLocusInfoMeetingInfoListener();
4361
5111
 
4362
5112
  assert.calledThrice(locusInfoOnSpy);
@@ -4377,16 +5127,96 @@ describe('plugin-meetings', () => {
4377
5127
 
4378
5128
  assert.calledWith(canUserLockSpy, payload.info.userDisplayHints);
4379
5129
  assert.calledWith(canUserUnlockSpy, payload.info.userDisplayHints);
4380
- assert.calledWith(canUserRecordSpy, payload.info.userDisplayHints);
5130
+ assert.calledWith(canUserStartSpy, payload.info.userDisplayHints);
4381
5131
  assert.calledWith(canUserStopSpy, payload.info.userDisplayHints);
4382
5132
  assert.calledWith(canUserPauseSpy, payload.info.userDisplayHints);
4383
5133
  assert.calledWith(canUserResumeSpy, payload.info.userDisplayHints);
5134
+ assert.calledWith(canSetMuteOnEntrySpy, payload.info.userDisplayHints);
5135
+ assert.calledWith(canUnsetMuteOnEntrySpy, payload.info.userDisplayHints);
5136
+ assert.calledWith(canSetDisallowUnmuteSpy, payload.info.userDisplayHints);
5137
+ assert.calledWith(canUnsetDisallowUnmuteSpy, payload.info.userDisplayHints);
4384
5138
  assert.calledWith(canUserRaiseHandSpy, payload.info.userDisplayHints);
4385
5139
  assert.calledWith(bothLeaveAndEndMeetingAvailableSpy, payload.info.userDisplayHints);
4386
5140
  assert.calledWith(canUserLowerAllHandsSpy, payload.info.userDisplayHints);
4387
5141
  assert.calledWith(canUserLowerSomeoneElsesHandSpy, payload.info.userDisplayHints);
4388
5142
  assert.calledWith(waitingForOthersToJoinSpy, payload.info.userDisplayHints);
4389
5143
  assert.calledWith(handleDataChannelUrlChangeSpy, payload.info.datachannelUrl);
5144
+ assert.calledWith(canSendReactionsSpy, null, payload.info.userDisplayHints);
5145
+ assert.calledWith(canUserRenameSelfAndObservedSpy, payload.info.userDisplayHints);
5146
+ assert.calledWith(canUserRenameOthersSpy, payload.info.userDisplayHints);
5147
+
5148
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5149
+ requiredHints: [DISPLAY_HINTS.MUTE_ALL],
5150
+ displayHints: payload.info.userDisplayHints,
5151
+ });
5152
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5153
+ requiredHints: [DISPLAY_HINTS.UNMUTE_ALL],
5154
+ displayHints: payload.info.userDisplayHints,
5155
+ });
5156
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5157
+ requiredHints: [DISPLAY_HINTS.ENABLE_HARD_MUTE],
5158
+ displayHints: payload.info.userDisplayHints,
5159
+ });
5160
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5161
+ requiredHints: [DISPLAY_HINTS.DISABLE_HARD_MUTE],
5162
+ displayHints: payload.info.userDisplayHints,
5163
+ });
5164
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5165
+ requiredHints: [DISPLAY_HINTS.ENABLE_MUTE_ON_ENTRY],
5166
+ displayHints: payload.info.userDisplayHints,
5167
+ });
5168
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5169
+ requiredHints: [DISPLAY_HINTS.DISABLE_MUTE_ON_ENTRY],
5170
+ displayHints: payload.info.userDisplayHints,
5171
+ });
5172
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5173
+ requiredHints: [DISPLAY_HINTS.ENABLE_REACTIONS],
5174
+ displayHints: payload.info.userDisplayHints,
5175
+ });
5176
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5177
+ requiredHints: [DISPLAY_HINTS.DISABLE_REACTIONS],
5178
+ displayHints: payload.info.userDisplayHints,
5179
+ });
5180
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5181
+ requiredHints: [DISPLAY_HINTS.ENABLE_SHOW_DISPLAY_NAME],
5182
+ displayHints: payload.info.userDisplayHints,
5183
+ });
5184
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5185
+ requiredHints: [DISPLAY_HINTS.DISABLE_SHOW_DISPLAY_NAME],
5186
+ displayHints: payload.info.userDisplayHints,
5187
+ });
5188
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5189
+ requiredHints: [DISPLAY_HINTS.SHARE_CONTROL],
5190
+ displayHints: payload.info.userDisplayHints,
5191
+ });
5192
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5193
+ requiredHints: [DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST],
5194
+ displayHints: payload.info.userDisplayHints,
5195
+ });
5196
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5197
+ requiredHints: [DISPLAY_HINTS.DISABLE_VIEW_THE_PARTICIPANT_LIST],
5198
+ displayHints: payload.info.userDisplayHints,
5199
+ });
5200
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5201
+ requiredHints: [DISPLAY_HINTS.SHARE_FILE],
5202
+ displayHints: payload.info.userDisplayHints,
5203
+ });
5204
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5205
+ requiredHints: [DISPLAY_HINTS.SHARE_APPLICATION],
5206
+ displayHints: payload.info.userDisplayHints,
5207
+ });
5208
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5209
+ requiredHints: [DISPLAY_HINTS.SHARE_CAMERA],
5210
+ displayHints: payload.info.userDisplayHints,
5211
+ });
5212
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5213
+ requiredHints: [DISPLAY_HINTS.SHARE_DESKTOP],
5214
+ displayHints: payload.info.userDisplayHints,
5215
+ });
5216
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5217
+ requiredHints: [DISPLAY_HINTS.SHARE_CONTENT],
5218
+ displayHints: payload.info.userDisplayHints,
5219
+ });
4390
5220
 
4391
5221
  assert.calledWith(
4392
5222
  TriggerProxy.trigger,
@@ -4404,6 +5234,8 @@ describe('plugin-meetings', () => {
4404
5234
  callback(payload);
4405
5235
 
4406
5236
  assert.notCalled(TriggerProxy.trigger);
5237
+
5238
+ ControlsOptionsUtil.hasHints = restorableHasHints;
4407
5239
  });
4408
5240
  });
4409
5241
 
@@ -4451,6 +5283,9 @@ describe('plugin-meetings', () => {
4451
5283
  .stub()
4452
5284
  .returns(Promise.resolve('something'));
4453
5285
  webex.internal.llm.disconnectLLM = sinon.stub().returns(Promise.resolve());
5286
+ meeting.webex.internal.llm.on = sinon.stub();
5287
+ meeting.webex.internal.llm.off = sinon.stub();
5288
+ meeting.processRelayEvent = sinon.stub();
4454
5289
  });
4455
5290
 
4456
5291
  it('does not connect if the call is not joined yet', async () => {
@@ -4464,6 +5299,7 @@ describe('plugin-meetings', () => {
4464
5299
  assert.notCalled(webex.internal.llm.registerAndConnect);
4465
5300
  assert.notCalled(webex.internal.llm.disconnectLLM);
4466
5301
  assert.equal(result, undefined);
5302
+ assert.notCalled(meeting.webex.internal.llm.on);
4467
5303
  });
4468
5304
 
4469
5305
  it('returns undefined if llm is already connected and the locus url is unchanged', async () => {
@@ -4478,17 +5314,29 @@ describe('plugin-meetings', () => {
4478
5314
  assert.notCalled(webex.internal.llm.registerAndConnect);
4479
5315
  assert.notCalled(webex.internal.llm.disconnectLLM);
4480
5316
  assert.equal(result, undefined);
5317
+ assert.notCalled(meeting.webex.internal.llm.on);
4481
5318
  });
4482
5319
 
4483
5320
  it('connects if not already connected', async () => {
4484
5321
  meeting.joinedWith = {state: 'JOINED'};
4485
5322
  meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
4486
5323
 
5324
+
4487
5325
  const result = await meeting.updateLLMConnection();
4488
5326
 
4489
5327
  assert.notCalled(webex.internal.llm.disconnectLLM);
4490
5328
  assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a datachannel url');
4491
5329
  assert.equal(result, 'something');
5330
+ assert.calledOnceWithExactly(
5331
+ meeting.webex.internal.llm.off,
5332
+ 'event:relay.event',
5333
+ meeting.processRelayEvent
5334
+ );
5335
+ assert.calledOnceWithExactly(
5336
+ meeting.webex.internal.llm.on,
5337
+ 'event:relay.event',
5338
+ meeting.processRelayEvent
5339
+ );
4492
5340
  });
4493
5341
 
4494
5342
  it('disconnects if first if the locus url has changed', async () => {
@@ -4507,6 +5355,19 @@ describe('plugin-meetings', () => {
4507
5355
  'a datachannel url'
4508
5356
  );
4509
5357
  assert.equal(result, 'something');
5358
+ assert.calledWithExactly(
5359
+ meeting.webex.internal.llm.off,
5360
+ 'event:relay.event',
5361
+ meeting.processRelayEvent
5362
+ );
5363
+ assert.calledTwice(
5364
+ meeting.webex.internal.llm.off
5365
+ );
5366
+ assert.calledOnceWithExactly(
5367
+ meeting.webex.internal.llm.on,
5368
+ 'event:relay.event',
5369
+ meeting.processRelayEvent
5370
+ );
4510
5371
  });
4511
5372
 
4512
5373
  it('disconnects when the state is not JOINED', async () => {
@@ -4521,15 +5382,22 @@ describe('plugin-meetings', () => {
4521
5382
  assert.calledWith(webex.internal.llm.disconnectLLM);
4522
5383
  assert.notCalled(webex.internal.llm.registerAndConnect);
4523
5384
  assert.equal(result, undefined);
5385
+ assert.calledOnceWithExactly(
5386
+ meeting.webex.internal.llm.off,
5387
+ 'event:relay.event',
5388
+ meeting.processRelayEvent
5389
+ );
4524
5390
  });
5391
+
4525
5392
  });
4526
5393
 
4527
5394
  describe('#setLocus', () => {
4528
5395
  beforeEach(() => {
4529
5396
  meeting.locusInfo.initialSetup = sinon.stub().returns(true);
4530
5397
  });
5398
+
4531
5399
  it('should read the locus object, set on the meeting and return null', () => {
4532
- meeting.parseLocus({
5400
+ meeting.setLocus({
4533
5401
  mediaConnections: [test1],
4534
5402
  locusUrl: url1,
4535
5403
  locusId: uuid1,
@@ -4554,6 +5422,7 @@ describe('plugin-meetings', () => {
4554
5422
  assert.equal(meeting.hostId, uuid4);
4555
5423
  });
4556
5424
  });
5425
+
4557
5426
  describe('preferred video device', () => {
4558
5427
  describe('#getVideoDeviceId', () => {
4559
5428
  it('returns the preferred video device', () => {
@@ -4620,14 +5489,53 @@ describe('plugin-meetings', () => {
4620
5489
  });
4621
5490
  });
4622
5491
  describe('share scenarios', () => {
5492
+
5493
+ describe('triggerAnnotationInfoEvent', () => {
5494
+ it('check triggerAnnotationInfoEvent event', () => {
5495
+
5496
+ TriggerProxy.trigger.reset();
5497
+ const annotationInfo = {version: '1', policy: 'Approval'};
5498
+ meeting.triggerAnnotationInfoEvent({annotation:annotationInfo},{});
5499
+
5500
+ assert.calledWith(
5501
+ TriggerProxy.trigger,
5502
+ meeting,
5503
+ {
5504
+ file: 'meeting/index',
5505
+ function: 'triggerAnnotationInfoEvent',
5506
+ },
5507
+ 'meeting:updateAnnotationInfo',
5508
+ annotationInfo
5509
+ );
5510
+
5511
+ TriggerProxy.trigger.reset();
5512
+ meeting.triggerAnnotationInfoEvent({annotation:annotationInfo},{annotation:annotationInfo});
5513
+ assert.notCalled(TriggerProxy.trigger);
5514
+
5515
+ TriggerProxy.trigger.reset();
5516
+ const annotationInfoUpdated = {version: '1', policy: 'AnnotationNotAllowed'};
5517
+ meeting.triggerAnnotationInfoEvent({annotation:annotationInfoUpdated},{annotation:annotationInfo});
5518
+ assert.calledWith(
5519
+ TriggerProxy.trigger,
5520
+ meeting,
5521
+ {
5522
+ file: 'meeting/index',
5523
+ function: 'triggerAnnotationInfoEvent',
5524
+ },
5525
+ 'meeting:updateAnnotationInfo',
5526
+ annotationInfoUpdated
5527
+ );
5528
+
5529
+ TriggerProxy.trigger.reset();
5530
+ meeting.triggerAnnotationInfoEvent(null,{annotation:annotationInfoUpdated});
5531
+ assert.notCalled(TriggerProxy.trigger);
5532
+
5533
+ });
5534
+ });
5535
+
4623
5536
  describe('setUpLocusMediaSharesListener', () => {
4624
5537
  beforeEach(() => {
4625
5538
  meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
4626
- sinon.stub(meeting, 'updateShare').returns(Promise.resolve());
4627
- });
4628
-
4629
- afterEach(() => {
4630
- meeting.updateShare.restore();
4631
5539
  });
4632
5540
 
4633
5541
  const USER_IDS = {
@@ -4643,7 +5551,7 @@ describe('plugin-meetings', () => {
4643
5551
  'https://board-a.wbx2.com/board/api/v1/channels/977a7330-54f4-11eb-b1ef-91f5eefc7bf3',
4644
5552
  };
4645
5553
 
4646
- const generateContent = (beneficiaryId = null, disposition = null) => ({
5554
+ const generateContent = (beneficiaryId = null, disposition = null,annotation = undefined) => ({
4647
5555
  beneficiaryId,
4648
5556
  disposition,
4649
5557
  });
@@ -4660,7 +5568,10 @@ describe('plugin-meetings', () => {
4660
5568
  beneficiaryId,
4661
5569
  resourceUrl,
4662
5570
  isAccepting,
4663
- otherBeneficiaryId
5571
+ otherBeneficiaryId,
5572
+ annotation,
5573
+ url,
5574
+ shareInstanceId
4664
5575
  ) => {
4665
5576
  const newPayload = cloneDeep(payload);
4666
5577
 
@@ -4686,7 +5597,7 @@ describe('plugin-meetings', () => {
4686
5597
  if (isGranting) {
4687
5598
  if (isContent) {
4688
5599
  activeSharingId.content = beneficiaryId;
4689
- newPayload.current.content = generateContent(beneficiaryId, FLOOR_ACTION.GRANTED);
5600
+ newPayload.current.content = generateContent(beneficiaryId, FLOOR_ACTION.GRANTED,annotation);
4690
5601
 
4691
5602
  if (isEqual(newPayload.current, newPayload.previous)) {
4692
5603
  eventTrigger.member = null;
@@ -4740,7 +5651,7 @@ describe('plugin-meetings', () => {
4740
5651
  eventTrigger.share.push({
4741
5652
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
4742
5653
  functionName: 'remoteShare',
4743
- eventPayload: {memberId: beneficiaryId},
5654
+ eventPayload: {memberId: beneficiaryId, url, shareInstanceId},
4744
5655
  });
4745
5656
  }
4746
5657
  }
@@ -5312,7 +6223,6 @@ describe('plugin-meetings', () => {
5312
6223
  payloadTestHelper([data1, data2, data3]);
5313
6224
  });
5314
6225
  });
5315
-
5316
6226
  describe('Desktop A --> Desktop B', () => {
5317
6227
  it('Scenario #1: you share desktop A and then share desktop B', () => {
5318
6228
  const data1 = generateData(blankPayload, true, true, USER_IDS.ME);
@@ -5529,7 +6439,7 @@ describe('plugin-meetings', () => {
5529
6439
  it('should send reaction with the right data and return a promise', async () => {
5530
6440
  meeting.locusInfo.controls = {reactions: {reactionChannelUrl: 'Fake URL'}};
5531
6441
 
5532
- const reactionPromise = meeting.sendReaction('thumbs_down', 'light');
6442
+ const reactionPromise = meeting.sendReaction('thumb_down', 'light');
5533
6443
 
5534
6444
  assert.exists(reactionPromise.then);
5535
6445
  await reactionPromise;
@@ -5553,7 +6463,7 @@ describe('plugin-meetings', () => {
5553
6463
  meeting.locusInfo.controls = {reactions: {reactionChannelUrl: undefined}};
5554
6464
 
5555
6465
  await assert.isRejected(
5556
- meeting.sendReaction('thumbs_down', 'light'),
6466
+ meeting.sendReaction('thumb_down', 'light'),
5557
6467
  Error,
5558
6468
  'Error sending reaction, service url not found.'
5559
6469
  );
@@ -5576,7 +6486,7 @@ describe('plugin-meetings', () => {
5576
6486
  it('should send a reaction with default skin tone if provided skinToneType is invalid ', async () => {
5577
6487
  meeting.locusInfo.controls = {reactions: {reactionChannelUrl: 'Fake URL'}};
5578
6488
 
5579
- const reactionPromise = meeting.sendReaction('thumbs_down', 'invalid_skin_tone');
6489
+ const reactionPromise = meeting.sendReaction('thumb_down', 'invalid_skin_tone');
5580
6490
 
5581
6491
  assert.exists(reactionPromise.then);
5582
6492
  await reactionPromise;
@@ -5595,7 +6505,7 @@ describe('plugin-meetings', () => {
5595
6505
  it('should send a reaction with default skin tone if none provided', async () => {
5596
6506
  meeting.locusInfo.controls = {reactions: {reactionChannelUrl: 'Fake URL'}};
5597
6507
 
5598
- const reactionPromise = meeting.sendReaction('thumbs_down');
6508
+ const reactionPromise = meeting.sendReaction('thumb_down');
5599
6509
 
5600
6510
  assert.exists(reactionPromise.then);
5601
6511
  await reactionPromise;
@@ -5674,6 +6584,109 @@ describe('plugin-meetings', () => {
5674
6584
  });
5675
6585
  });
5676
6586
  });
6587
+
6588
+ describe('SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED locus event', () => {
6589
+ let spy;
6590
+
6591
+ beforeEach('setup sinon', () => {
6592
+ spy = sinon.spy();
6593
+ });
6594
+
6595
+ const testEmit = async (muted) => {
6596
+ await meeting.locusInfo.emitScoped(
6597
+ {},
6598
+ LOCUSINFO.EVENTS.SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED,
6599
+ {
6600
+ muted,
6601
+ }
6602
+ );
6603
+
6604
+ assert.calledWith(
6605
+ TriggerProxy.trigger,
6606
+ sinon.match.instanceOf(Meeting),
6607
+ {
6608
+ file: 'meeting/index',
6609
+ function: 'setUpLocusInfoSelfListener',
6610
+ },
6611
+ muted
6612
+ ? EVENT_TRIGGERS.MEETING_SELF_VIDEO_MUTED_BY_OTHERS
6613
+ : EVENT_TRIGGERS.MEETING_SELF_VIDEO_UNMUTED_BY_OTHERS,
6614
+ {
6615
+ payload: {
6616
+ muted,
6617
+ },
6618
+ }
6619
+ );
6620
+ };
6621
+
6622
+ it('emits the expected event when muted', async () => {
6623
+ await testEmit(true);
6624
+ });
6625
+
6626
+ it('emits the expected event when not muted', async () => {
6627
+ await testEmit(false);
6628
+ });
6629
+ });
6630
+
6631
+ describe('getAnalyzerMetricsPrePayload', () => {
6632
+ it('should have #getAnalyzerMetricsPrePayload', () => {
6633
+ assert.exists(meeting.getAnalyzerMetricsPrePayload);
6634
+ });
6635
+
6636
+ beforeEach(() => {
6637
+ meeting.meetingRequest.getAnalyzerMetricsPrePayload = sinon
6638
+ .stub()
6639
+ .returns(Promise.resolve());
6640
+ meeting.webex.internal = {services: {get: sinon.stub().returns('Locus URL')}};
6641
+ meeting.correlationId = 'correlation-id';
6642
+ });
6643
+
6644
+ it('it should include meetingLookupUrl if provided', () => {
6645
+ const res = meeting.getAnalyzerMetricsPrePayload({
6646
+ meetingLookupUrl: 'https://service-url.com',
6647
+ event: 'client.meetinginfo.response',
6648
+ });
6649
+
6650
+ assert.deepEqual(res.event, {
6651
+ canProceed: true,
6652
+ eventData: {
6653
+ webClientDomain: '',
6654
+ },
6655
+ identifiers: {
6656
+ correlationId: 'correlation-id',
6657
+ deviceId: uuid3,
6658
+ locusUrl: 'Locus URL',
6659
+ meetingLookupUrl: 'https://service-url.com',
6660
+ orgId: undefined,
6661
+ userId: uuid1,
6662
+ },
6663
+ name: 'client.meetinginfo.response',
6664
+ });
6665
+
6666
+ assert.deepEqual(res.origin, {
6667
+ channel: undefined,
6668
+ loginType: undefined,
6669
+ userType: undefined,
6670
+ clientInfo: {
6671
+ browser: '',
6672
+ browserVersion: '',
6673
+ clientType: undefined,
6674
+ clientVersion: 'webex-js-sdk/undefined',
6675
+ localNetworkPrefix: null,
6676
+ os: 'other',
6677
+ osVersion: getOSVersion() || 'unknown',
6678
+ subClientType: undefined,
6679
+ },
6680
+ name: 'endpoint',
6681
+ networkType: 'unknown',
6682
+ userAgent: 'webex-js-sdk/test-undefined client=undefined; (os=linux/5)',
6683
+ });
6684
+
6685
+ assert.deepEqual(res.senderCountryCode, undefined);
6686
+ assert.deepEqual(res.version, 1);
6687
+
6688
+ });
6689
+ });
5677
6690
  });
5678
6691
  });
5679
6692
  });