@webex/plugin-meetings 3.0.0-beta.16 → 3.0.0-beta.160

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 (408) 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 +212 -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/errors/webex-errors.js +3 -2
  23. package/dist/common/errors/webex-errors.js.map +1 -1
  24. package/dist/common/logs/logger-proxy.js +1 -1
  25. package/dist/common/logs/logger-proxy.js.map +1 -1
  26. package/dist/config.js +6 -8
  27. package/dist/config.js.map +1 -1
  28. package/dist/constants.js +165 -26
  29. package/dist/constants.js.map +1 -1
  30. package/dist/controls-options-manager/constants.js +14 -0
  31. package/dist/controls-options-manager/constants.js.map +1 -0
  32. package/dist/controls-options-manager/enums.js +27 -0
  33. package/dist/controls-options-manager/enums.js.map +1 -0
  34. package/dist/controls-options-manager/index.js +297 -0
  35. package/dist/controls-options-manager/index.js.map +1 -0
  36. package/dist/controls-options-manager/types.js +7 -0
  37. package/dist/controls-options-manager/types.js.map +1 -0
  38. package/dist/controls-options-manager/util.js +300 -0
  39. package/dist/controls-options-manager/util.js.map +1 -0
  40. package/dist/index.js +77 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/locus-info/controlsUtils.js +91 -2
  43. package/dist/locus-info/controlsUtils.js.map +1 -1
  44. package/dist/locus-info/index.js +298 -24
  45. package/dist/locus-info/index.js.map +1 -1
  46. package/dist/locus-info/mediaSharesUtils.js +43 -1
  47. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  48. package/dist/locus-info/parser.js +2 -1
  49. package/dist/locus-info/parser.js.map +1 -1
  50. package/dist/locus-info/selfUtils.js +88 -14
  51. package/dist/locus-info/selfUtils.js.map +1 -1
  52. package/dist/media/index.js +39 -134
  53. package/dist/media/index.js.map +1 -1
  54. package/dist/media/properties.js +19 -97
  55. package/dist/media/properties.js.map +1 -1
  56. package/dist/mediaQualityMetrics/config.js +505 -493
  57. package/dist/mediaQualityMetrics/config.js.map +1 -1
  58. package/dist/meeting/in-meeting-actions.js +79 -1
  59. package/dist/meeting/in-meeting-actions.js.map +1 -1
  60. package/dist/meeting/index.js +2275 -2152
  61. package/dist/meeting/index.js.map +1 -1
  62. package/dist/meeting/locusMediaRequest.js +291 -0
  63. package/dist/meeting/locusMediaRequest.js.map +1 -0
  64. package/dist/meeting/muteState.js +229 -124
  65. package/dist/meeting/muteState.js.map +1 -1
  66. package/dist/meeting/request.js +191 -167
  67. package/dist/meeting/request.js.map +1 -1
  68. package/dist/meeting/request.type.js.map +1 -1
  69. package/dist/meeting/util.js +443 -443
  70. package/dist/meeting/util.js.map +1 -1
  71. package/dist/meeting-info/meeting-info-v2.js +157 -49
  72. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  73. package/dist/meeting-info/utilv2.js +20 -5
  74. package/dist/meeting-info/utilv2.js.map +1 -1
  75. package/dist/meetings/collection.js +22 -0
  76. package/dist/meetings/collection.js.map +1 -1
  77. package/dist/meetings/index.js +365 -73
  78. package/dist/meetings/index.js.map +1 -1
  79. package/dist/meetings/meetings.types.js +7 -0
  80. package/dist/meetings/meetings.types.js.map +1 -0
  81. package/dist/meetings/request.js +16 -12
  82. package/dist/meetings/request.js.map +1 -1
  83. package/dist/meetings/util.js +88 -1
  84. package/dist/meetings/util.js.map +1 -1
  85. package/dist/member/index.js +41 -0
  86. package/dist/member/index.js.map +1 -1
  87. package/dist/member/types.js +15 -0
  88. package/dist/member/types.js.map +1 -0
  89. package/dist/member/util.js +86 -3
  90. package/dist/member/util.js.map +1 -1
  91. package/dist/members/collection.js +10 -0
  92. package/dist/members/collection.js.map +1 -1
  93. package/dist/members/index.js +94 -11
  94. package/dist/members/index.js.map +1 -1
  95. package/dist/members/request.js +109 -39
  96. package/dist/members/request.js.map +1 -1
  97. package/dist/members/types.js +15 -0
  98. package/dist/members/types.js.map +1 -0
  99. package/dist/members/util.js +316 -233
  100. package/dist/members/util.js.map +1 -1
  101. package/dist/metrics/config.js +50 -14
  102. package/dist/metrics/config.js.map +1 -1
  103. package/dist/metrics/constants.js +3 -5
  104. package/dist/metrics/constants.js.map +1 -1
  105. package/dist/metrics/index.js +48 -29
  106. package/dist/metrics/index.js.map +1 -1
  107. package/dist/multistream/mediaRequestManager.js +265 -36
  108. package/dist/multistream/mediaRequestManager.js.map +1 -1
  109. package/dist/multistream/receiveSlot.js +52 -19
  110. package/dist/multistream/receiveSlot.js.map +1 -1
  111. package/dist/multistream/receiveSlotManager.js +53 -33
  112. package/dist/multistream/receiveSlotManager.js.map +1 -1
  113. package/dist/multistream/remoteMedia.js +44 -18
  114. package/dist/multistream/remoteMedia.js.map +1 -1
  115. package/dist/multistream/remoteMediaGroup.js +60 -3
  116. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  117. package/dist/multistream/remoteMediaManager.js +322 -103
  118. package/dist/multistream/remoteMediaManager.js.map +1 -1
  119. package/dist/networkQualityMonitor/index.js +4 -2
  120. package/dist/networkQualityMonitor/index.js.map +1 -1
  121. package/dist/reachability/index.js +117 -60
  122. package/dist/reachability/index.js.map +1 -1
  123. package/dist/reachability/request.js +12 -5
  124. package/dist/reachability/request.js.map +1 -1
  125. package/dist/reactions/constants.js +13 -0
  126. package/dist/reactions/constants.js.map +1 -0
  127. package/dist/reactions/reactions.js +2 -2
  128. package/dist/reactions/reactions.js.map +1 -1
  129. package/dist/reactions/reactions.type.js +18 -18
  130. package/dist/reactions/reactions.type.js.map +1 -1
  131. package/dist/reconnection-manager/index.js +190 -145
  132. package/dist/reconnection-manager/index.js.map +1 -1
  133. package/dist/recording-controller/enums.js +17 -0
  134. package/dist/recording-controller/enums.js.map +1 -0
  135. package/dist/recording-controller/index.js +343 -0
  136. package/dist/recording-controller/index.js.map +1 -0
  137. package/dist/recording-controller/util.js +63 -0
  138. package/dist/recording-controller/util.js.map +1 -0
  139. package/dist/roap/index.js +21 -29
  140. package/dist/roap/index.js.map +1 -1
  141. package/dist/roap/request.js +127 -92
  142. package/dist/roap/request.js.map +1 -1
  143. package/dist/roap/turnDiscovery.js +135 -53
  144. package/dist/roap/turnDiscovery.js.map +1 -1
  145. package/dist/statsAnalyzer/global.js +1 -93
  146. package/dist/statsAnalyzer/global.js.map +1 -1
  147. package/dist/statsAnalyzer/index.js +329 -314
  148. package/dist/statsAnalyzer/index.js.map +1 -1
  149. package/dist/statsAnalyzer/mqaUtil.js +103 -54
  150. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  151. package/dist/types/annotation/annotation.types.d.ts +43 -0
  152. package/dist/types/annotation/constants.d.ts +31 -0
  153. package/dist/types/annotation/index.d.ts +124 -0
  154. package/dist/types/breakouts/breakout.d.ts +8 -0
  155. package/dist/types/breakouts/collection.d.ts +5 -0
  156. package/dist/types/breakouts/edit-lock-error.d.ts +15 -0
  157. package/dist/types/breakouts/events.d.ts +2 -0
  158. package/dist/types/breakouts/index.d.ts +5 -0
  159. package/dist/types/breakouts/request.d.ts +22 -0
  160. package/dist/types/breakouts/utils.d.ts +15 -0
  161. package/dist/types/common/browser-detection.d.ts +9 -0
  162. package/dist/types/common/collection.d.ts +48 -0
  163. package/dist/types/common/config.d.ts +2 -0
  164. package/dist/types/common/errors/captcha-error.d.ts +15 -0
  165. package/dist/types/common/errors/intent-to-join.d.ts +16 -0
  166. package/dist/types/common/errors/join-meeting.d.ts +17 -0
  167. package/dist/types/common/errors/media.d.ts +15 -0
  168. package/dist/types/common/errors/parameter.d.ts +15 -0
  169. package/dist/types/common/errors/password-error.d.ts +15 -0
  170. package/dist/types/common/errors/permission.d.ts +14 -0
  171. package/dist/types/common/errors/reconnection-in-progress.d.ts +9 -0
  172. package/dist/types/common/errors/reconnection.d.ts +15 -0
  173. package/dist/types/common/errors/stats.d.ts +15 -0
  174. package/dist/types/common/errors/webex-errors.d.ts +69 -0
  175. package/dist/types/common/errors/webex-meetings-error.d.ts +20 -0
  176. package/dist/types/common/events/events-scope.d.ts +17 -0
  177. package/dist/types/common/events/events.d.ts +12 -0
  178. package/dist/types/common/events/trigger-proxy.d.ts +2 -0
  179. package/dist/types/common/events/util.d.ts +2 -0
  180. package/dist/types/common/logs/logger-config.d.ts +2 -0
  181. package/dist/types/common/logs/logger-proxy.d.ts +2 -0
  182. package/dist/types/common/logs/request.d.ts +34 -0
  183. package/dist/types/common/queue.d.ts +32 -0
  184. package/dist/types/config.d.ts +72 -0
  185. package/dist/types/constants.d.ts +978 -0
  186. package/dist/types/controls-options-manager/constants.d.ts +4 -0
  187. package/dist/types/controls-options-manager/enums.d.ts +15 -0
  188. package/dist/types/controls-options-manager/index.d.ts +136 -0
  189. package/dist/types/controls-options-manager/types.d.ts +43 -0
  190. package/dist/types/controls-options-manager/util.d.ts +1 -0
  191. package/dist/types/index.d.ts +7 -0
  192. package/dist/types/locus-info/controlsUtils.d.ts +2 -0
  193. package/dist/types/locus-info/embeddedAppsUtils.d.ts +2 -0
  194. package/dist/types/locus-info/fullState.d.ts +2 -0
  195. package/dist/types/locus-info/hostUtils.d.ts +2 -0
  196. package/dist/types/locus-info/index.d.ts +315 -0
  197. package/dist/types/locus-info/infoUtils.d.ts +2 -0
  198. package/dist/types/locus-info/mediaSharesUtils.d.ts +2 -0
  199. package/dist/types/locus-info/parser.d.ts +212 -0
  200. package/dist/types/locus-info/selfUtils.d.ts +2 -0
  201. package/dist/types/media/index.d.ts +34 -0
  202. package/dist/types/media/properties.d.ts +86 -0
  203. package/dist/types/media/util.d.ts +2 -0
  204. package/dist/types/mediaQualityMetrics/config.d.ts +365 -0
  205. package/dist/types/meeting/in-meeting-actions.d.ts +149 -0
  206. package/dist/types/meeting/index.d.ts +1516 -0
  207. package/dist/types/meeting/locusMediaRequest.d.ts +70 -0
  208. package/dist/types/meeting/muteState.d.ts +184 -0
  209. package/dist/types/meeting/request.d.ts +270 -0
  210. package/dist/types/meeting/request.type.d.ts +11 -0
  211. package/dist/types/meeting/state.d.ts +9 -0
  212. package/dist/types/meeting/util.d.ts +75 -0
  213. package/dist/types/meeting-info/collection.d.ts +20 -0
  214. package/dist/types/meeting-info/index.d.ts +57 -0
  215. package/dist/types/meeting-info/meeting-info-v2.d.ts +122 -0
  216. package/dist/types/meeting-info/request.d.ts +22 -0
  217. package/dist/types/meeting-info/util.d.ts +2 -0
  218. package/dist/types/meeting-info/utilv2.d.ts +2 -0
  219. package/dist/types/meetings/collection.d.ts +31 -0
  220. package/dist/types/meetings/index.d.ts +364 -0
  221. package/dist/types/meetings/meetings.types.d.ts +4 -0
  222. package/dist/types/meetings/request.d.ts +27 -0
  223. package/dist/types/meetings/util.d.ts +18 -0
  224. package/dist/types/member/index.d.ts +157 -0
  225. package/dist/types/member/types.d.ts +21 -0
  226. package/dist/types/member/util.d.ts +2 -0
  227. package/dist/types/members/collection.d.ts +29 -0
  228. package/dist/types/members/index.d.ts +353 -0
  229. package/dist/types/members/request.d.ts +114 -0
  230. package/dist/types/members/types.d.ts +24 -0
  231. package/dist/types/members/util.d.ts +210 -0
  232. package/dist/types/metrics/config.d.ts +195 -0
  233. package/dist/types/metrics/constants.d.ts +55 -0
  234. package/dist/types/metrics/index.d.ts +169 -0
  235. package/dist/types/multistream/mediaRequestManager.d.ts +118 -0
  236. package/dist/types/multistream/receiveSlot.d.ts +68 -0
  237. package/dist/types/multistream/receiveSlotManager.d.ts +56 -0
  238. package/dist/types/multistream/remoteMedia.d.ts +72 -0
  239. package/dist/types/multistream/remoteMediaGroup.d.ts +47 -0
  240. package/dist/types/multistream/remoteMediaManager.d.ts +277 -0
  241. package/dist/types/networkQualityMonitor/index.d.ts +70 -0
  242. package/dist/types/personal-meeting-room/index.d.ts +47 -0
  243. package/dist/types/personal-meeting-room/request.d.ts +14 -0
  244. package/dist/types/personal-meeting-room/util.d.ts +2 -0
  245. package/dist/types/reachability/index.d.ts +152 -0
  246. package/dist/types/reachability/request.d.ts +37 -0
  247. package/dist/types/reactions/constants.d.ts +3 -0
  248. package/dist/types/reactions/reactions.d.ts +4 -0
  249. package/dist/types/reactions/reactions.type.d.ts +52 -0
  250. package/dist/types/reconnection-manager/index.d.ts +126 -0
  251. package/dist/types/recording-controller/enums.d.ts +7 -0
  252. package/dist/types/recording-controller/index.d.ts +193 -0
  253. package/dist/types/recording-controller/util.d.ts +13 -0
  254. package/dist/types/roap/index.d.ts +77 -0
  255. package/dist/types/roap/request.d.ts +36 -0
  256. package/dist/types/roap/turnDiscovery.d.ts +91 -0
  257. package/dist/types/statsAnalyzer/global.d.ts +36 -0
  258. package/dist/types/statsAnalyzer/index.d.ts +200 -0
  259. package/dist/types/statsAnalyzer/mqaUtil.d.ts +24 -0
  260. package/dist/types/transcription/index.d.ts +64 -0
  261. package/package.json +28 -21
  262. package/src/annotation/annotation.types.ts +52 -0
  263. package/src/annotation/constants.ts +36 -0
  264. package/src/annotation/index.ts +343 -0
  265. package/src/breakouts/README.md +220 -0
  266. package/src/breakouts/breakout.ts +180 -0
  267. package/src/breakouts/collection.ts +19 -0
  268. package/src/breakouts/edit-lock-error.ts +25 -0
  269. package/src/breakouts/events.ts +37 -0
  270. package/src/breakouts/index.ts +921 -0
  271. package/src/breakouts/request.ts +55 -0
  272. package/src/breakouts/utils.ts +57 -0
  273. package/src/common/errors/webex-errors.ts +6 -2
  274. package/src/common/logs/logger-proxy.ts +1 -1
  275. package/src/config.ts +5 -7
  276. package/src/constants.ts +155 -20
  277. package/src/controls-options-manager/constants.ts +5 -0
  278. package/src/controls-options-manager/enums.ts +18 -0
  279. package/src/controls-options-manager/index.ts +278 -0
  280. package/src/controls-options-manager/types.ts +59 -0
  281. package/src/controls-options-manager/util.ts +286 -0
  282. package/src/index.ts +34 -0
  283. package/src/locus-info/controlsUtils.ts +108 -0
  284. package/src/locus-info/index.ts +310 -21
  285. package/src/locus-info/mediaSharesUtils.ts +48 -0
  286. package/src/locus-info/parser.ts +2 -1
  287. package/src/locus-info/selfUtils.ts +80 -2
  288. package/src/media/index.ts +70 -142
  289. package/src/media/properties.ts +41 -104
  290. package/src/mediaQualityMetrics/config.ts +379 -377
  291. package/src/meeting/in-meeting-actions.ts +156 -0
  292. package/src/meeting/index.ts +1744 -1767
  293. package/src/meeting/locusMediaRequest.ts +309 -0
  294. package/src/meeting/muteState.ts +228 -132
  295. package/src/meeting/request.ts +100 -91
  296. package/src/meeting/request.type.ts +2 -0
  297. package/src/meeting/util.ts +421 -421
  298. package/src/meeting-info/meeting-info-v2.ts +134 -13
  299. package/src/meeting-info/utilv2.ts +13 -3
  300. package/src/meetings/collection.ts +20 -0
  301. package/src/meetings/index.ts +385 -83
  302. package/src/meetings/meetings.types.ts +12 -0
  303. package/src/meetings/request.ts +3 -1
  304. package/src/meetings/util.ts +103 -4
  305. package/src/member/index.ts +40 -0
  306. package/src/member/types.ts +24 -0
  307. package/src/member/util.ts +81 -1
  308. package/src/members/collection.ts +8 -0
  309. package/src/members/index.ts +108 -6
  310. package/src/members/request.ts +98 -17
  311. package/src/members/types.ts +28 -0
  312. package/src/members/util.ts +319 -240
  313. package/src/metrics/config.ts +49 -10
  314. package/src/metrics/constants.ts +2 -4
  315. package/src/metrics/index.ts +43 -27
  316. package/src/multistream/mediaRequestManager.ts +337 -63
  317. package/src/multistream/receiveSlot.ts +68 -26
  318. package/src/multistream/receiveSlotManager.ts +61 -38
  319. package/src/multistream/remoteMedia.ts +29 -3
  320. package/src/multistream/remoteMediaGroup.ts +61 -2
  321. package/src/multistream/remoteMediaManager.ts +260 -66
  322. package/src/networkQualityMonitor/index.ts +6 -6
  323. package/src/reachability/index.ts +75 -25
  324. package/src/reachability/request.ts +10 -5
  325. package/src/reactions/constants.ts +4 -0
  326. package/src/reactions/reactions.ts +4 -4
  327. package/src/reactions/reactions.type.ts +28 -3
  328. package/src/reconnection-manager/index.ts +53 -32
  329. package/src/recording-controller/enums.ts +8 -0
  330. package/src/recording-controller/index.ts +315 -0
  331. package/src/recording-controller/util.ts +58 -0
  332. package/src/roap/index.ts +21 -30
  333. package/src/roap/request.ts +51 -52
  334. package/src/roap/turnDiscovery.ts +51 -27
  335. package/src/statsAnalyzer/global.ts +1 -94
  336. package/src/statsAnalyzer/index.ts +380 -390
  337. package/src/statsAnalyzer/mqaUtil.ts +106 -99
  338. package/test/integration/spec/converged-space-meetings.js +233 -0
  339. package/test/integration/spec/journey.js +331 -254
  340. package/test/integration/spec/space-meeting.js +77 -4
  341. package/test/unit/spec/annotation/index.ts +436 -0
  342. package/test/unit/spec/breakouts/breakout.ts +233 -0
  343. package/test/unit/spec/breakouts/collection.ts +15 -0
  344. package/test/unit/spec/breakouts/edit-lock-error.ts +30 -0
  345. package/test/unit/spec/breakouts/events.ts +77 -0
  346. package/test/unit/spec/breakouts/index.ts +1790 -0
  347. package/test/unit/spec/breakouts/request.ts +104 -0
  348. package/test/unit/spec/breakouts/utils.js +72 -0
  349. package/test/unit/spec/controls-options-manager/index.js +287 -0
  350. package/test/unit/spec/controls-options-manager/util.js +518 -0
  351. package/test/unit/spec/fixture/locus.js +1 -0
  352. package/test/unit/spec/locus-info/controlsUtils.js +303 -30
  353. package/test/unit/spec/locus-info/index.js +616 -4
  354. package/test/unit/spec/locus-info/mediaSharesUtils.ts +22 -0
  355. package/test/unit/spec/locus-info/selfConstant.js +38 -0
  356. package/test/unit/spec/locus-info/selfUtils.js +249 -0
  357. package/test/unit/spec/media/index.ts +118 -22
  358. package/test/unit/spec/media/properties.ts +9 -9
  359. package/test/unit/spec/meeting/in-meeting-actions.ts +76 -0
  360. package/test/unit/spec/meeting/index.js +2496 -1375
  361. package/test/unit/spec/meeting/locusMediaRequest.ts +436 -0
  362. package/test/unit/spec/meeting/muteState.js +370 -208
  363. package/test/unit/spec/meeting/request.js +354 -42
  364. package/test/unit/spec/meeting/utils.js +268 -156
  365. package/test/unit/spec/meeting-info/meetinginfov2.js +383 -5
  366. package/test/unit/spec/meeting-info/utilv2.js +21 -0
  367. package/test/unit/spec/meetings/collection.js +14 -0
  368. package/test/unit/spec/meetings/index.js +866 -120
  369. package/test/unit/spec/meetings/utils.js +206 -2
  370. package/test/unit/spec/member/index.js +24 -0
  371. package/test/unit/spec/member/util.js +384 -32
  372. package/test/unit/spec/members/index.js +320 -1
  373. package/test/unit/spec/members/request.js +206 -27
  374. package/test/unit/spec/members/utils.js +184 -0
  375. package/test/unit/spec/metrics/index.js +98 -0
  376. package/test/unit/spec/multistream/mediaRequestManager.ts +1012 -109
  377. package/test/unit/spec/multistream/receiveSlot.ts +77 -18
  378. package/test/unit/spec/multistream/receiveSlotManager.ts +69 -39
  379. package/test/unit/spec/multistream/remoteMedia.ts +32 -2
  380. package/test/unit/spec/multistream/remoteMediaGroup.ts +271 -5
  381. package/test/unit/spec/multistream/remoteMediaManager.ts +672 -65
  382. package/test/unit/spec/networkQualityMonitor/index.js +4 -4
  383. package/test/unit/spec/reachability/index.ts +176 -25
  384. package/test/unit/spec/reachability/request.js +66 -0
  385. package/test/unit/spec/reconnection-manager/index.js +46 -13
  386. package/test/unit/spec/recording-controller/index.js +231 -0
  387. package/test/unit/spec/recording-controller/util.js +102 -0
  388. package/test/unit/spec/roap/index.ts +21 -51
  389. package/test/unit/spec/roap/request.ts +187 -0
  390. package/test/unit/spec/roap/turnDiscovery.ts +73 -34
  391. package/test/unit/spec/stats-analyzer/index.js +94 -43
  392. package/test/utils/constants.js +9 -0
  393. package/test/utils/integrationTestUtils.js +46 -0
  394. package/test/utils/testUtils.js +0 -45
  395. package/test/utils/webex-config.js +4 -0
  396. package/test/utils/webex-test-users.js +7 -3
  397. package/tsconfig.json +6 -0
  398. package/dist/media/internal-media-core-wrapper.js +0 -18
  399. package/dist/media/internal-media-core-wrapper.js.map +0 -1
  400. package/dist/meeting/effectsState.js +0 -262
  401. package/dist/meeting/effectsState.js.map +0 -1
  402. package/dist/multistream/multistreamMedia.js +0 -106
  403. package/dist/multistream/multistreamMedia.js.map +0 -1
  404. package/src/index.js +0 -15
  405. package/src/media/internal-media-core-wrapper.ts +0 -9
  406. package/src/meeting/effectsState.ts +0 -211
  407. package/src/multistream/multistreamMedia.ts +0 -93
  408. 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
-
753
- assert.calledOnce(Media.getUserMedia);
754
509
 
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,7 +683,67 @@ describe('plugin-meetings', () => {
873
683
  await meeting.join();
874
684
  sinon.assert.called(meeting.setCorrelationId);
875
685
  });
876
- });
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
+ });
877
747
  describe('failure', () => {
878
748
  beforeEach(() => {
879
749
  sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.reject());
@@ -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,628 +1333,864 @@ 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
-
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);
1386
1336
 
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);
1403
- });
1404
- });
1405
- describe('#leave', () => {
1406
- let sandbox;
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';
1407
1341
 
1408
- it('should have #leave', () => {
1409
- assert.exists(meeting.leave);
1410
- });
1342
+ meeting.meetingState = 'ACTIVE';
1343
+ Media.createMediaConnection.resetHistory();
1411
1344
 
1412
- it('should reject if meeting is already inactive', async () => {
1413
- await meeting.leave().catch((err) => {
1414
- assert.instanceOf(err, MeetingNotActiveError);
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,
1415
1352
  });
1416
- });
1417
-
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);
1353
+ const media = meeting.addMedia({
1354
+ mediaSettings: {},
1355
+ bundlePolicy: 'bundlePolicy-value',
1422
1356
  });
1423
- });
1424
1357
 
1425
- 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());
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);
1377
+ });
1447
1378
 
1448
- // A meeting needs to be joined to leave
1379
+ it('succeeds even if getDevices() throws', async () => {
1449
1380
  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();
1458
1381
 
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;
1382
+ sinon
1383
+ .stub(internalMediaModule, 'getDevices')
1384
+ .rejects(new Error('fake error'));
1473
1385
 
1474
- beforeEach(() => {
1475
- handleClientRequest = sinon.stub().returns(Promise.resolve(true));
1386
+ await meeting.addMedia();
1387
+ })
1388
+ });
1476
1389
 
1477
- meeting.audio = {handleClientRequest};
1478
- meeting.video = {handleClientRequest};
1479
- });
1390
+ /* This set of tests are like semi-integration tests, they use real MuteState, Media, LocusMediaRequest and Roap classes.
1391
+ They mock the @webex/internal-media-core and sending of /media http requests to Locus.
1392
+ Their main purpose is to test that we send the right http requests to Locus and make right calls
1393
+ to @webex/internal-media-core when addMedia, updateMedia, publishTracks, unpublishTracks are called
1394
+ in various combinations.
1395
+ */
1396
+ [true,false].forEach((isMultistream) =>
1397
+ describe(`addMedia/updateMedia semi-integration tests (${isMultistream ? 'multistream' : 'transcoded'})`, () => {
1398
+ const webrtcAudioTrack = {
1399
+ id: 'underlying audio track',
1400
+ getSettings: sinon.stub().returns({deviceId: 'fake device id for audio track'}),
1401
+ };
1480
1402
 
1481
- it('should delete audio and video state machines when leaving the meeting', async () => {
1482
- const leave = meeting.leave();
1403
+ let fakeMicrophoneTrack;
1404
+ let fakeRoapMediaConnection;
1405
+ let fakeMultistreamRoapMediaConnection;
1406
+ let roapMediaConnectionConstructorStub;
1407
+ let multistreamRoapMediaConnectionConstructorStub;
1408
+ let locusMediaRequestStub; // stub for /media requests to Locus
1483
1409
 
1484
- assert.exists(leave.then);
1485
- await leave;
1410
+ const roapOfferMessage = {messageType: 'OFFER', sdp: 'sdp', seq: '1', tieBreaker: '123'};
1486
1411
 
1487
- assert.isNull(meeting.audio);
1488
- assert.isNull(meeting.video);
1489
- });
1490
- });
1491
- it('should leave the meeting without leaving resource', async () => {
1492
- const leave = meeting.leave({resourceId: null});
1412
+ let expectedMediaConnectionConfig;
1413
+ let expectedDebugId;
1493
1414
 
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
- });
1503
- });
1504
- it('should leave the meeting on the resource', async () => {
1505
- const leave = meeting.leave();
1415
+ let clock;
1506
1416
 
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,
1515
- });
1516
- });
1517
- });
1518
- describe('#requestScreenShareFloor', () => {
1519
- it('should have #requestScreenShareFloor', () => {
1520
- assert.exists(meeting.requestScreenShareFloor);
1521
- });
1522
1417
  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();
1418
+ clock = sinon.useFakeTimers();
1529
1419
 
1530
- assert.exists(share.then);
1531
- await share;
1532
- assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
1533
- });
1534
- });
1420
+ meeting.deviceUrl = 'deviceUrl';
1421
+ meeting.config.deviceType = 'web';
1422
+ meeting.isMultistream = isMultistream;
1423
+ meeting.meetingState = 'ACTIVE';
1424
+ meeting.mediaId = 'fake media id';
1425
+ meeting.selfUrl = 'selfUrl';
1426
+ meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves();
1427
+ meeting.mediaProperties.getCurrentConnectionType = sinon.stub().resolves('udp');
1428
+ meeting.setMercuryListener = sinon.stub();
1429
+ meeting.locusInfo.onFullLocus = sinon.stub();
1430
+ meeting.webex.meetings.reachability = {
1431
+ isAnyClusterReachable: sinon.stub().resolves(true),
1432
+ };
1433
+ meeting.roap.doTurnDiscovery = sinon
1434
+ .stub()
1435
+ .resolves({turnServerInfo: {}, turnDiscoverySkippedReason: 'reachability'});
1436
+
1437
+ StaticConfig.set({bandwidth: {audio: 1234, video: 5678, startBitrate: 9876}});
1438
+
1439
+ Metrics.postEvent = sinon.stub();
1440
+
1441
+ // setup things that are expected to be the same across all the tests and are actually irrelevant for these tests
1442
+ expectedDebugId = `MC-${meeting.id.substring(0, 4)}`;
1443
+ expectedMediaConnectionConfig = {
1444
+ iceServers: [ { urls: undefined, username: '', credential: '' } ],
1445
+ skipInactiveTransceivers: false,
1446
+ requireH264: true,
1447
+ sdpMunging: {
1448
+ convertPort9to0: false,
1449
+ addContentSlides: true,
1450
+ bandwidthLimits: {
1451
+ audio: StaticConfig.meetings.bandwidth.audio,
1452
+ video: StaticConfig.meetings.bandwidth.video,
1453
+ },
1454
+ startBitrate: StaticConfig.meetings.bandwidth.startBitrate,
1455
+ periodicKeyframes: 20,
1456
+ disableExtmap: !meeting.config.enableExtmap,
1457
+ disableRtx: !meeting.config.enableRtx,
1458
+ },
1459
+ };
1535
1460
 
1536
- describe('#shareScreen', () => {
1537
- let _mediaDirection;
1461
+ // setup stubs
1462
+ fakeMicrophoneTrack = {
1463
+ id: 'fake mic',
1464
+ on: sinon.stub(),
1465
+ off: sinon.stub(),
1466
+ setUnmuteAllowed: sinon.stub(),
1467
+ setMuted: sinon.stub(),
1468
+ setPublished: sinon.stub(),
1469
+ muted: false,
1470
+ underlyingTrack: webrtcAudioTrack
1471
+ };
1538
1472
 
1539
- beforeEach(() => {
1540
- _mediaDirection = meeting.mediaProperties.mediaDirection || {};
1541
- sinon
1542
- .stub(meeting.mediaProperties, 'mediaDirection')
1543
- .value({sendAudio: true, sendVideo: true, sendShare: false});
1544
- });
1473
+ fakeRoapMediaConnection = {
1474
+ id: 'roap media connection',
1475
+ close: sinon.stub(),
1476
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
1477
+ initiateOffer: sinon.stub().resolves({}),
1478
+ update: sinon.stub().resolves({}),
1479
+ on: sinon.stub(),
1480
+ };
1545
1481
 
1546
- afterEach(() => {
1547
- meeting.mediaProperties.mediaDirection = _mediaDirection;
1482
+ fakeMultistreamRoapMediaConnection = {
1483
+ id: 'multistream roap media connection',
1484
+ close: sinon.stub(),
1485
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
1486
+ initiateOffer: sinon.stub().resolves({}),
1487
+ publishTrack: sinon.stub().resolves({}),
1488
+ unpublishTrack: sinon.stub().resolves({}),
1489
+ on: sinon.stub(),
1490
+ requestMedia: sinon.stub(),
1491
+ createReceiveSlot: sinon.stub().resolves({on: sinon.stub()}),
1492
+ enableMultistreamAudio: sinon.stub(),
1493
+ };
1494
+
1495
+ roapMediaConnectionConstructorStub = sinon
1496
+ .stub(internalMediaModule, 'RoapMediaConnection')
1497
+ .returns(fakeRoapMediaConnection);
1498
+
1499
+ multistreamRoapMediaConnectionConstructorStub = sinon
1500
+ .stub(internalMediaModule, 'MultistreamRoapMediaConnection')
1501
+ .returns(fakeMultistreamRoapMediaConnection);
1502
+
1503
+ locusMediaRequestStub = sinon.stub(WebexPlugin.prototype, 'request').resolves({body: {locus: { fullState: {}}}});
1548
1504
  });
1549
1505
 
1550
- it('should have #shareScreen', () => {
1551
- assert.exists(meeting.shareScreen);
1506
+ afterEach(() => {
1507
+ clock.restore();
1552
1508
  });
1553
1509
 
1554
- describe('basic functionality', () => {
1555
- beforeEach(() => {
1556
- sinon.stub(Media, 'getDisplayMedia').returns(Promise.resolve());
1557
- sinon.stub(meeting, 'updateShare').returns(Promise.resolve());
1558
- });
1510
+ // helper function that waits until all promises are resolved and any queued up /media requests to Locus are sent out
1511
+ const stableState = async () => {
1512
+ await testUtils.flushPromises();
1513
+ clock.tick(1); // needed because LocusMediaRequest uses Lodash.defer()
1514
+ }
1515
+
1516
+ const resetHistory = () => {
1517
+ locusMediaRequestStub.resetHistory();
1518
+ fakeRoapMediaConnection.update.resetHistory();
1519
+ fakeMultistreamRoapMediaConnection.publishTrack.resetHistory();
1520
+ fakeMultistreamRoapMediaConnection.unpublishTrack.resetHistory();
1521
+ };
1559
1522
 
1560
- afterEach(() => {
1561
- Media.getDisplayMedia.restore();
1562
- meeting.updateShare.restore();
1563
- });
1523
+ const getRoapListener = () => {
1524
+ const roapMediaConnectionToCheck = isMultistream ? fakeMultistreamRoapMediaConnection : fakeRoapMediaConnection;
1564
1525
 
1565
- it('should call get display media', async () => {
1566
- await meeting.shareScreen();
1526
+ for(let idx = 0; idx < roapMediaConnectionToCheck.on.callCount; idx+= 1) {
1527
+ if (roapMediaConnectionToCheck.on.getCall(idx).args[0] === Event.ROAP_MESSAGE_TO_SEND) {
1528
+ return roapMediaConnectionToCheck.on.getCall(idx).args[1];
1529
+ }
1530
+ }
1531
+ assert.fail('listener for "roap:messageToSend" (Event.ROAP_MESSAGE_TO_SEND) was not registered')
1532
+ }
1567
1533
 
1568
- assert.calledOnce(Media.getDisplayMedia);
1569
- });
1534
+ // simulates a Roap offer being generated by the RoapMediaConnection
1535
+ const simulateRoapOffer = async () => {
1536
+ const roapListener = getRoapListener();
1570
1537
 
1571
- it('should call updateShare', async () => {
1572
- await meeting.shareScreen();
1538
+ await roapListener({roapMessage: roapOfferMessage});
1539
+ await stableState();
1540
+ }
1573
1541
 
1574
- assert.calledOnce(meeting.updateShare);
1575
- });
1542
+ const checkSdpOfferSent = ({audioMuted, videoMuted}) => {
1543
+ const {sdp, seq, tieBreaker} = roapOfferMessage;
1576
1544
 
1577
- it('properly assigns default values', async () => {
1578
- await meeting.shareScreen({sharePreferences: {highFrameRate: true}});
1545
+ assert.calledWith(locusMediaRequestStub,
1546
+ {
1547
+ method: 'PUT',
1548
+ uri: `${meeting.selfUrl}/media`,
1549
+ body: {
1550
+ device: { url: meeting.deviceUrl, deviceType: meeting.config.deviceType },
1551
+ correlationId: meeting.correlationId,
1552
+ localMedias: [
1553
+ {
1554
+ localSdp: `{"audioMuted":${audioMuted},"videoMuted":${videoMuted},"roapMessage":{"messageType":"OFFER","sdps":["${sdp}"],"version":"2","seq":"${seq}","tieBreaker":"${tieBreaker}"}}`,
1555
+ mediaId: 'fake media id'
1556
+ }
1557
+ ],
1558
+ clientMediaPreferences: {
1559
+ preferTranscoding: !meeting.isMultistream,
1560
+ joinCookie: undefined
1561
+ }
1562
+ },
1563
+ });
1564
+ };
1579
1565
 
1580
- assert.calledWith(Media.getDisplayMedia, {
1581
- sendShare: true,
1582
- sendAudio: false,
1583
- sharePreferences: {highFrameRate: true},
1566
+ const checkLocalMuteSentToLocus = ({audioMuted, videoMuted}) => {
1567
+ assert.calledWith(locusMediaRequestStub, {
1568
+ method: 'PUT',
1569
+ uri: `${meeting.selfUrl}/media`,
1570
+ body: {
1571
+ device: { url: meeting.deviceUrl, deviceType: meeting.config.deviceType },
1572
+ correlationId: meeting.correlationId,
1573
+ localMedias: [
1574
+ {
1575
+ localSdp: `{"audioMuted":${audioMuted},"videoMuted":${videoMuted}}`,
1576
+ mediaId: 'fake media id'
1577
+ }
1578
+ ],
1579
+ clientMediaPreferences: {
1580
+ preferTranscoding: !meeting.isMultistream,
1581
+ },
1582
+ respOnlySdp: true,
1583
+ usingResource: null,
1584
+ },
1584
1585
  });
1586
+ };
1587
+
1588
+ const checkMediaConnectionCreated = ({mediaConnectionConfig, localTracks, direction, remoteQualityLevel, expectedDebugId}) => {
1589
+ if (isMultistream) {
1590
+ const {iceServers} = mediaConnectionConfig;
1591
+
1592
+ assert.calledOnceWithExactly(multistreamRoapMediaConnectionConstructorStub, {
1593
+ iceServers,
1594
+ enableMainAudio: direction.audio !== 'inactive',
1595
+ enableMainVideo: true
1596
+ }, expectedDebugId);
1597
+
1598
+ Object.values(localTracks).forEach((track) => {
1599
+ if (track) {
1600
+ assert.calledOnceWithExactly(fakeMultistreamRoapMediaConnection.publishTrack, track);
1601
+ }
1602
+ })
1603
+ } else {
1604
+ assert.calledOnceWithExactly(roapMediaConnectionConstructorStub, mediaConnectionConfig,
1605
+ {
1606
+ localTracks: {
1607
+ audio: localTracks.audio?.underlyingTrack,
1608
+ video: localTracks.video?.underlyingTrack,
1609
+ screenShareVideo: localTracks.screenShareVideo?.underlyingTrack,
1610
+ },
1611
+ direction,
1612
+ remoteQualityLevel,
1613
+ },
1614
+ expectedDebugId);
1615
+ }
1616
+ }
1617
+
1618
+ it('addMedia() works correctly when media is enabled without tracks to publish', async () => {
1619
+ await meeting.addMedia();
1620
+ await simulateRoapOffer();
1621
+
1622
+ // check RoapMediaConnection was created correctly
1623
+ checkMediaConnectionCreated({
1624
+ mediaConnectionConfig: expectedMediaConnectionConfig,
1625
+ localTracks: {
1626
+ audio: undefined,
1627
+ video: undefined,
1628
+ screenShareVideo: undefined,
1629
+ },
1630
+ direction: {
1631
+ audio: 'sendrecv',
1632
+ video: 'sendrecv',
1633
+ screenShareVideo: 'recvonly',
1634
+ },
1635
+ remoteQualityLevel: 'HIGH',
1636
+ expectedDebugId,
1585
1637
  });
1638
+
1639
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1640
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1641
+
1642
+ // and that it was the only /media request that was sent
1643
+ assert.calledOnce(locusMediaRequestStub);
1586
1644
  });
1587
1645
 
1588
- describe('stops share immediately', () => {
1589
- let sandbox;
1646
+ it('addMedia() works correctly when media is enabled with tracks to publish', async () => {
1647
+ await meeting.addMedia({localTracks: {microphone: fakeMicrophoneTrack}});
1648
+ await simulateRoapOffer();
1590
1649
 
1591
- beforeEach(() => {
1592
- sandbox = sinon.createSandbox();
1650
+ // check RoapMediaConnection was created correctly
1651
+ checkMediaConnectionCreated({
1652
+ mediaConnectionConfig: expectedMediaConnectionConfig,
1653
+ localTracks: {
1654
+ audio: fakeMicrophoneTrack,
1655
+ video: undefined,
1656
+ screenShareVideo: undefined,
1657
+ },
1658
+ direction: {
1659
+ audio: 'sendrecv',
1660
+ video: 'sendrecv',
1661
+ screenShareVideo: 'recvonly',
1662
+ },
1663
+ remoteQualityLevel: 'HIGH',
1664
+ expectedDebugId
1593
1665
  });
1594
1666
 
1595
- afterEach(() => {
1596
- sandbox.restore();
1597
- sandbox = null;
1598
- });
1667
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1668
+ checkSdpOfferSent({audioMuted: false, videoMuted: true});
1599
1669
 
1600
- it('Can bypass canUpdateMedia() check', () => {
1601
- const sendShare = true;
1602
- const receiveShare = false;
1603
- const stream = 'stream';
1670
+ // and no other local mute requests were sent to Locus
1671
+ assert.calledOnce(locusMediaRequestStub);
1672
+ });
1604
1673
 
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');
1674
+ it('addMedia() works correctly when media is enabled with tracks to publish and track is muted', async () => {
1675
+ fakeMicrophoneTrack.muted = true;
1609
1676
 
1610
- meeting.updateShare({
1611
- sendShare,
1612
- receiveShare,
1613
- stream,
1614
- skipSignalingCheck: true,
1615
- });
1677
+ await meeting.addMedia({localTracks: {microphone: fakeMicrophoneTrack}});
1678
+ await simulateRoapOffer();
1616
1679
 
1617
- assert.notCalled(meeting.canUpdateMedia);
1680
+ // check RoapMediaConnection was created correctly
1681
+ checkMediaConnectionCreated({
1682
+ mediaConnectionConfig: expectedMediaConnectionConfig,
1683
+ localTracks: {
1684
+ audio: fakeMicrophoneTrack,
1685
+ video: undefined,
1686
+ screenShareVideo: undefined,
1687
+ },
1688
+ direction: {
1689
+ audio: 'sendrecv',
1690
+ video: 'sendrecv',
1691
+ screenShareVideo: 'recvonly',
1692
+ },
1693
+ remoteQualityLevel: 'HIGH',
1694
+ expectedDebugId,
1618
1695
  });
1619
1696
 
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
- };
1697
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1698
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1626
1699
 
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);
1700
+ // and no other local mute requests were sent to Locus
1701
+ assert.calledOnce(locusMediaRequestStub);
1702
+ });
1632
1703
 
1633
- fakeTrack.onended();
1704
+ it('addMedia() works correctly when media is disabled with tracks to publish', async () => {
1705
+ await meeting.addMedia({localTracks: {microphone: fakeMicrophoneTrack}, audioEnabled: false});
1706
+ await simulateRoapOffer();
1634
1707
 
1635
- assert.calledWith(meeting.stopShare, {skipSignalingCheck: true});
1708
+ // check RoapMediaConnection was created correctly
1709
+ checkMediaConnectionCreated({
1710
+ mediaConnectionConfig: expectedMediaConnectionConfig,
1711
+ localTracks: {
1712
+ audio: fakeMicrophoneTrack,
1713
+ video: undefined,
1714
+ screenShareVideo: undefined,
1715
+ },
1716
+ direction: {
1717
+ audio: 'inactive',
1718
+ video: 'sendrecv',
1719
+ screenShareVideo: 'recvonly',
1720
+ },
1721
+ remoteQualityLevel: 'HIGH',
1722
+ expectedDebugId
1636
1723
  });
1637
1724
 
1638
- it('stopShare accepts and passes along optional parameters', () => {
1639
- const args = {
1640
- abc: 123,
1641
- receiveShare: false,
1642
- sendShare: false,
1643
- };
1725
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1726
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1644
1727
 
1645
- sandbox.stub(meeting, 'updateShare').returns(Promise.resolve());
1646
- sandbox.stub(meeting.mediaProperties, 'mediaDirection').value(false);
1728
+ // and no other local mute requests were sent to Locus
1729
+ assert.calledOnce(locusMediaRequestStub);
1730
+ });
1647
1731
 
1648
- meeting.stopShare(args);
1732
+ it('addMedia() works correctly when media is disabled with no tracks to publish', async () => {
1733
+ await meeting.addMedia({audioEnabled: false});
1734
+ await simulateRoapOffer();
1649
1735
 
1650
- assert.calledWith(meeting.updateShare, args);
1736
+ // check RoapMediaConnection was created correctly
1737
+ checkMediaConnectionCreated({
1738
+ mediaConnectionConfig: expectedMediaConnectionConfig,
1739
+ localTracks: {
1740
+ audio: undefined,
1741
+ video: undefined,
1742
+ screenShareVideo: undefined,
1743
+ },
1744
+ direction: {
1745
+ audio: 'inactive',
1746
+ video: 'sendrecv',
1747
+ screenShareVideo: 'recvonly',
1748
+ },
1749
+ remoteQualityLevel: 'HIGH',
1750
+ expectedDebugId
1651
1751
  });
1752
+
1753
+ // and SDP offer was sent with the right audioMuted/videoMuted values
1754
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1755
+
1756
+ // and no other local mute requests were sent to Locus
1757
+ assert.calledOnce(locusMediaRequestStub);
1652
1758
  });
1653
1759
 
1654
- describe('out-of-sync sharing', () => {
1655
- let sandbox;
1760
+ describe('publishTracks()/unpublishTracks() calls', () => {
1761
+ [
1762
+ {mediaEnabled: true, expected: {direction: 'sendrecv', localMuteSentValue: false}},
1763
+ {mediaEnabled: false, expected: {direction: 'inactive', localMuteSentValue: undefined}}
1764
+ ]
1765
+ .forEach(({mediaEnabled, expected}) => {
1766
+ it(`first publishTracks() call while media is ${mediaEnabled ? 'enabled' : 'disabled'}`, async () => {
1767
+ await meeting.addMedia({audioEnabled: mediaEnabled});
1768
+ await simulateRoapOffer();
1656
1769
 
1657
- beforeEach(() => {
1658
- sandbox = sinon.createSandbox();
1659
- });
1770
+ resetHistory();
1660
1771
 
1661
- afterEach(() => {
1662
- sandbox.restore();
1663
- sandbox = null;
1772
+ await meeting.publishTracks({microphone: fakeMicrophoneTrack});
1773
+ await stableState();
1774
+
1775
+ if (expected.localMuteSentValue !== undefined) {
1776
+ // check local mute was sent and it was the only /media request
1777
+ checkLocalMuteSentToLocus({audioMuted: expected.localMuteSentValue, videoMuted: true});
1778
+ assert.calledOnce(locusMediaRequestStub);
1779
+ } else {
1780
+ assert.notCalled(locusMediaRequestStub);
1781
+ }
1782
+ if (isMultistream) {
1783
+ assert.calledOnceWithExactly(fakeMultistreamRoapMediaConnection.publishTrack, fakeMicrophoneTrack);
1784
+ } else {
1785
+ assert.calledOnceWithExactly(fakeRoapMediaConnection.update, {
1786
+ localTracks: { audio: webrtcAudioTrack, video: null, screenShareVideo: null },
1787
+ direction: {
1788
+ audio: expected.direction,
1789
+ video: 'sendrecv',
1790
+ screenShareVideo: 'recvonly',
1791
+ },
1792
+ remoteQualityLevel: 'HIGH'
1793
+ });
1794
+ }
1795
+ });
1796
+
1797
+ it(`second publishTracks() call while media is ${mediaEnabled ? 'enabled' : 'disabled'}`, async () => {
1798
+ await meeting.addMedia({audioEnabled: mediaEnabled});
1799
+ await simulateRoapOffer();
1800
+ await meeting.publishTracks({microphone: fakeMicrophoneTrack});
1801
+ await stableState();
1802
+
1803
+ resetHistory();
1804
+
1805
+ const webrtcAudioTrack2 = {id: 'underlying audio track 2'};
1806
+ const fakeMicrophoneTrack2 = {
1807
+ id: 'fake mic 2',
1808
+ on: sinon.stub(),
1809
+ off: sinon.stub(),
1810
+ setUnmuteAllowed: sinon.stub(),
1811
+ setMuted: sinon.stub(),
1812
+ setPublished: sinon.stub(),
1813
+ muted: false,
1814
+ underlyingTrack: webrtcAudioTrack2
1815
+ };
1816
+
1817
+ await meeting.publishTracks({microphone: fakeMicrophoneTrack2});
1818
+ await stableState();
1819
+
1820
+ // only the roap media connection should be updated
1821
+ if (isMultistream) {
1822
+ assert.calledOnceWithExactly(fakeMultistreamRoapMediaConnection.publishTrack, fakeMicrophoneTrack2);
1823
+ } else {
1824
+ assert.calledOnceWithExactly(fakeRoapMediaConnection.update, {
1825
+ localTracks: { audio: webrtcAudioTrack2, video: null, screenShareVideo: null },
1826
+ direction: {
1827
+ audio: expected.direction,
1828
+ video: 'sendrecv',
1829
+ screenShareVideo: 'recvonly',
1830
+ },
1831
+ remoteQualityLevel: 'HIGH'
1832
+ });
1833
+ }
1834
+
1835
+ // and no other roap messages or local mute requests were sent
1836
+ assert.notCalled(locusMediaRequestStub);
1837
+ });
1838
+
1839
+ it(`unpublishTracks() call while media is ${mediaEnabled ? 'enabled' : 'disabled'}`, async () => {
1840
+ await meeting.addMedia({audioEnabled: mediaEnabled});
1841
+ await simulateRoapOffer();
1842
+ await meeting.publishTracks({microphone: fakeMicrophoneTrack});
1843
+ await stableState();
1844
+
1845
+ resetHistory();
1846
+
1847
+ await meeting.unpublishTracks([fakeMicrophoneTrack]);
1848
+ await stableState();
1849
+
1850
+ // the roap media connection should be updated
1851
+ if (isMultistream) {
1852
+ assert.calledOnceWithExactly(fakeMultistreamRoapMediaConnection.unpublishTrack, fakeMicrophoneTrack);
1853
+ } else {
1854
+ assert.calledOnceWithExactly(fakeRoapMediaConnection.update, {
1855
+ localTracks: { audio: null, video: null, screenShareVideo: null },
1856
+ direction: {
1857
+ audio: expected.direction,
1858
+ video: 'sendrecv',
1859
+ screenShareVideo: 'recvonly',
1860
+ },
1861
+ remoteQualityLevel: 'HIGH'
1862
+ });
1863
+ }
1864
+
1865
+ if (expected.localMuteSentValue !== undefined) {
1866
+ // and local mute sent to Locus
1867
+ checkLocalMuteSentToLocus({audioMuted: !expected.localMuteSentValue /* negation, because we're un-publishing */, videoMuted: true});
1868
+ assert.calledOnce(locusMediaRequestStub);
1869
+ } else {
1870
+ assert.notCalled(locusMediaRequestStub);
1871
+ }
1872
+ });
1873
+ });
1874
+ });
1875
+
1876
+ describe('updateMedia()', () => {
1877
+
1878
+ const addMedia = async (enableMedia, track) => {
1879
+ await meeting.addMedia({audioEnabled: enableMedia, localTracks: {microphone: track}});
1880
+ await simulateRoapOffer();
1881
+
1882
+ resetHistory();
1883
+ }
1884
+
1885
+ const checkAudioEnabled = (expectedTrack, expectedDirection) => {
1886
+ if (isMultistream) {
1887
+ assert.calledOnceWithExactly(fakeMultistreamRoapMediaConnection.enableMultistreamAudio, expectedDirection !== 'inactive');
1888
+ } else {
1889
+ assert.calledOnceWithExactly(fakeRoapMediaConnection.update, {
1890
+ localTracks: { audio: expectedTrack, video: null, screenShareVideo: null },
1891
+ direction: {
1892
+ audio: expectedDirection,
1893
+ video: 'sendrecv',
1894
+ screenShareVideo: 'recvonly',
1895
+ },
1896
+ remoteQualityLevel: 'HIGH'
1897
+ });
1898
+ }
1899
+ }
1900
+
1901
+ it('updateMedia() disables media when nothing is published', async () => {
1902
+ await addMedia(true);
1903
+
1904
+ await meeting.updateMedia({audioEnabled: false});
1905
+
1906
+ // the roap media connection should be updated
1907
+ checkAudioEnabled(null, 'inactive');
1908
+
1909
+ // and that would trigger a new offer so we simulate it happening
1910
+ await simulateRoapOffer();
1911
+
1912
+ // check SDP offer was sent with the right audioMuted/videoMuted values
1913
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1914
+
1915
+ // and no other local mute requests were sent to Locus
1916
+ assert.calledOnce(locusMediaRequestStub);
1664
1917
  });
1665
1918
 
1666
- it('handleShareTrackEnded triggers an event', () => {
1667
- const stream = 'stream';
1668
- const {EVENT_TYPES} = CONSTANTS;
1919
+ it('updateMedia() enables media when nothing is published', async () => {
1920
+ await addMedia(false);
1669
1921
 
1670
- sandbox.stub(meeting, 'stopShare').resolves(true);
1922
+ await meeting.updateMedia({audioEnabled: true});
1671
1923
 
1672
- meeting.handleShareTrackEnded(stream);
1924
+ // the roap media connection should be updated
1925
+ checkAudioEnabled(null, 'sendrecv');
1673
1926
 
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
- );
1927
+ // and that would trigger a new offer so we simulate it happening
1928
+ await simulateRoapOffer();
1929
+
1930
+ // check SDP offer was sent with the right audioMuted/videoMuted values
1931
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1932
+
1933
+ // and no other local mute requests were sent to Locus
1934
+ assert.calledOnce(locusMediaRequestStub);
1687
1935
  });
1688
- });
1689
- });
1690
1936
 
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
- };
1937
+ it('updateMedia() disables media when track is published', async () => {
1938
+ await addMedia(true, fakeMicrophoneTrack);
1706
1939
 
1707
- const MediaStream = {
1708
- getVideoTracks: () => [
1709
- {
1710
- applyConstraints: () => {},
1711
- },
1712
- ],
1713
- };
1940
+ await meeting.updateMedia({audioEnabled: false});
1941
+ await stableState();
1714
1942
 
1715
- const MediaConstraint = {
1716
- cursor: 'always',
1717
- aspectRatio: config.aspectRatio,
1718
- frameRate: config.screenFrameRate,
1719
- width: null,
1720
- height: null,
1721
- };
1943
+ // the roap media connection should be updated
1944
+ checkAudioEnabled(webrtcAudioTrack, 'inactive');
1722
1945
 
1723
- const browserConditionalValue = (value) => {
1724
- const key = getBrowserName().toLowerCase();
1725
- const defaultKey = 'default';
1946
+ checkLocalMuteSentToLocus({audioMuted: true, videoMuted: true});
1726
1947
 
1727
- return value[key] || value[defaultKey];
1728
- };
1948
+ locusMediaRequestStub.resetHistory();
1729
1949
 
1730
- before(() => {
1731
- meeting.updateShare = sinon.stub().returns(Promise.resolve());
1950
+ // and that would trigger a new offer so we simulate it happening
1951
+ await simulateRoapOffer();
1732
1952
 
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,
1953
+ // check SDP offer was sent with the right audioMuted/videoMuted values
1954
+ checkSdpOfferSent({audioMuted: true, videoMuted: true});
1955
+
1956
+ // and no other local mute requests were sent to Locus
1957
+ assert.calledOnce(locusMediaRequestStub);
1744
1958
  });
1745
- });
1746
1959
 
1747
- after(() => {
1748
- // clean up for browser
1749
- Object.defineProperty(global.navigator.mediaDevices, 'getDisplayMedia', {
1750
- value: _getDisplayMedia,
1751
- writable: true,
1960
+ it('updateMedia() enables media when track is published', async () => {
1961
+ await addMedia(false, fakeMicrophoneTrack);
1962
+
1963
+ await meeting.updateMedia({audioEnabled: true});
1964
+ await stableState();
1965
+
1966
+ // the roap media connection should be updated
1967
+ checkAudioEnabled(webrtcAudioTrack, 'sendrecv');
1968
+
1969
+ checkLocalMuteSentToLocus({audioMuted: false, videoMuted: true});
1970
+
1971
+ locusMediaRequestStub.resetHistory();
1972
+
1973
+ // and that would trigger a new offer so we simulate it happening
1974
+ await simulateRoapOffer();
1975
+
1976
+ // check SDP offer was sent with the right audioMuted/videoMuted values
1977
+ checkSdpOfferSent({audioMuted: false, videoMuted: true});
1978
+
1979
+ // and no other local mute requests were sent to Locus
1980
+ assert.calledOnce(locusMediaRequestStub);
1752
1981
  });
1753
1982
  });
1754
1983
 
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
- };
1984
+ [
1985
+ {mute: true, title: 'muting a track before confluence is created'},
1986
+ {mute: false, title: 'unmuting a track before confluence is created'}
1987
+ ].forEach(({mute, title}) =>
1988
+ it(title, async () => {
1989
+ // initialize the microphone mute state to opposite of what we do in the test
1990
+ fakeMicrophoneTrack.muted = !mute;
1766
1991
 
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
- );
1992
+ await meeting.addMedia({localTracks: {microphone: fakeMicrophoneTrack}});
1993
+ await stableState();
1776
1994
 
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
- );
1788
- });
1995
+ resetHistory();
1789
1996
 
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
- );
1997
+ assert.equal(fakeMicrophoneTrack.on.getCall(0).args[0], LocalTrackEvents.Muted);
1998
+ const mutedListener = fakeMicrophoneTrack.on.getCall(0).args[1];
1999
+ // simulate track being muted
2000
+ mutedListener({trackState: {muted: mute}});
1802
2001
 
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
- );
1822
- });
2002
+ await stableState();
1823
2003
 
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;
2004
+ // nothing should happen
2005
+ assert.notCalled(locusMediaRequestStub);
2006
+ assert.notCalled(fakeRoapMediaConnection.update);
1828
2007
 
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
- );
1843
- });
2008
+ // now simulate roap offer
2009
+ await simulateRoapOffer();
1844
2010
 
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
- };
2011
+ // it should be sent with the right mute status
2012
+ checkSdpOfferSent({audioMuted: mute, videoMuted: true});
1858
2013
 
1859
- getDisplayMedia(shareOptions, customConfig);
2014
+ // nothing else should happen
2015
+ assert.calledOnce(locusMediaRequestStub);
2016
+ assert.notCalled(fakeRoapMediaConnection.update);
2017
+ })
2018
+ );
2019
+ }));
1860
2020
 
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
- );
2021
+ describe('#acknowledge', () => {
2022
+ it('should have #acknowledge', () => {
2023
+ assert.exists(meeting.acknowledge);
1879
2024
  });
2025
+ beforeEach(() => {
2026
+ meeting.meetingRequest.acknowledgeMeeting = sinon.stub().returns(Promise.resolve());
2027
+ });
2028
+ it('should acknowledge incoming and return a promise', async () => {
2029
+ const ack = meeting.acknowledge('INCOMING', false);
1880
2030
 
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
- };
1894
-
1895
- getDisplayMedia(shareOptions, customConfig);
2031
+ assert.exists(ack.then);
2032
+ await ack;
2033
+ assert.calledOnce(meeting.meetingRequest.acknowledgeMeeting);
2034
+ });
2035
+ it('should acknowledge a non incoming and return a promise', async () => {
2036
+ const ack = meeting.acknowledge(test1, false);
1896
2037
 
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
- );
2038
+ assert.exists(ack.then);
2039
+ await ack;
2040
+ assert.notCalled(meeting.meetingRequest.acknowledgeMeeting);
1916
2041
  });
1917
2042
  });
1918
-
1919
- describe('#stopShare', () => {
1920
- it('should have #stopShare', () => {
1921
- assert.exists(meeting.stopShare);
2043
+ describe('#decline', () => {
2044
+ it('should have #decline', () => {
2045
+ assert.exists(meeting.decline);
1922
2046
  });
1923
2047
  beforeEach(() => {
1924
- meeting.mediaProperties.mediaDirection = {receiveShare: true};
1925
- meeting.updateShare = sinon.stub().returns(Promise.resolve());
2048
+ meeting.meetingRequest.declineMeeting = sinon.stub().returns(Promise.resolve());
2049
+ meeting.meetingFiniteStateMachine.ring();
1926
2050
  });
1927
- it('should call updateShare', async () => {
1928
- const share = meeting.stopShare();
1929
-
1930
- assert.exists(share.then);
1931
- await share;
1932
- assert.calledOnce(meeting.updateShare);
2051
+ it('should decline the meeting and trigger meeting destroy for 1:1', async () => {
2052
+ await meeting.decline();
2053
+ assert.calledOnce(meeting.meetingRequest.declineMeeting);
1933
2054
  });
1934
2055
  });
2056
+ describe('#leave', () => {
2057
+ let sandbox;
1935
2058
 
1936
- describe('#updateAudio', () => {
1937
- const FAKE_AUDIO_TRACK = {
1938
- id: 'fake audio track',
1939
- getSettings: sinon.stub().returns({}),
1940
- };
2059
+ it('should have #leave', () => {
2060
+ assert.exists(meeting.leave);
2061
+ });
2062
+
2063
+ it('should reject if meeting is already inactive', async () => {
2064
+ await meeting.leave().catch((err) => {
2065
+ assert.instanceOf(err, MeetingNotActiveError);
2066
+ });
2067
+ });
2068
+
2069
+ it('should reject if meeting is already left', async () => {
2070
+ meeting.meetingState = 'ACTIVE';
2071
+ await meeting.leave().catch((err) => {
2072
+ assert.instanceOf(err, UserNotJoinedError);
2073
+ });
2074
+ });
2075
+
2076
+ beforeEach(() => {
2077
+ sandbox = sinon.createSandbox();
2078
+ meeting.meetingFiniteStateMachine.ring();
2079
+ meeting.meetingFiniteStateMachine.join();
2080
+ meeting.meetingRequest.leaveMeeting = sinon
2081
+ .stub()
2082
+ .returns(Promise.resolve({body: 'test'}));
2083
+ meeting.locusInfo.onFullLocus = sinon.stub().returns(true);
2084
+ meeting.cleanupLocalTracks = sinon.stub().returns(Promise.resolve());
2085
+ meeting.closeRemoteStream = sinon.stub().returns(Promise.resolve());
2086
+ sandbox.stub(meeting, 'closeRemoteTracks').returns(Promise.resolve());
2087
+ meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
2088
+ meeting.unsetRemoteTracks = sinon.stub();
2089
+ meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
2090
+ meeting.unsetPeerConnections = sinon.stub().returns(true);
2091
+ meeting.logger.error = sinon.stub().returns(true);
2092
+ meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
2093
+
2094
+ // A meeting needs to be joined to leave
2095
+ meeting.meetingState = 'ACTIVE';
2096
+ meeting.state = 'JOINED';
2097
+ });
2098
+ afterEach(() => {
2099
+ sandbox.restore();
2100
+ sandbox = null;
2101
+ });
2102
+ it('should leave the meeting and return promise', async () => {
2103
+ const leave = meeting.leave();
2104
+
2105
+ assert.exists(leave.then);
2106
+ await leave;
2107
+ assert.calledOnce(meeting.meetingRequest.leaveMeeting);
2108
+ assert.calledOnce(meeting.cleanupLocalTracks);
2109
+ assert.calledOnce(meeting.closeRemoteTracks);
2110
+ assert.calledOnce(meeting.closePeerConnections);
2111
+ assert.calledOnce(meeting.unsetRemoteTracks);
2112
+ assert.calledOnce(meeting.unsetPeerConnections);
2113
+ });
2114
+ describe('after audio/video is defined', () => {
2115
+ let handleClientRequest;
1941
2116
 
1942
- describe('when canUpdateMedia is true', () => {
1943
2117
  beforeEach(() => {
1944
- meeting.canUpdateMedia = sinon.stub().returns(true);
2118
+ handleClientRequest = sinon.stub().returns(Promise.resolve(true));
2119
+
2120
+ meeting.audio = {handleClientRequest};
2121
+ meeting.video = {handleClientRequest};
1945
2122
  });
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
- }));
2123
+
2124
+ it('should delete audio and video state machines when leaving the meeting', async () => {
2125
+ const leave = meeting.leave();
2126
+
2127
+ assert.exists(leave.then);
2128
+ await leave;
2129
+
2130
+ assert.isNull(meeting.audio);
2131
+ assert.isNull(meeting.video);
1986
2132
  });
1987
- afterEach(() => {
1988
- sinon.restore();
2133
+ });
2134
+ it('should leave the meeting without leaving resource', async () => {
2135
+ const leave = meeting.leave({resourceId: null});
2136
+
2137
+ assert.exists(leave.then);
2138
+ await leave;
2139
+ assert.calledWith(meeting.meetingRequest.leaveMeeting, {
2140
+ locusUrl: meeting.locusUrl,
2141
+ correlationId: meeting.correlationId,
2142
+ selfId: meeting.selfId,
2143
+ resourceId: null,
2144
+ deviceUrl: meeting.deviceUrl,
1989
2145
  });
1990
2146
  });
2147
+ it('should leave the meeting on the resource', async () => {
2148
+ const leave = meeting.leave();
2149
+
2150
+ assert.exists(leave.then);
2151
+ await leave;
2152
+ assert.calledWith(meeting.meetingRequest.leaveMeeting, {
2153
+ locusUrl: meeting.locusUrl,
2154
+ correlationId: meeting.correlationId,
2155
+ selfId: meeting.selfId,
2156
+ resourceId: meeting.resourceId,
2157
+ deviceUrl: meeting.deviceUrl
2158
+ });
2159
+ });
2160
+ it('should leave the meeting on the resource with reason', async () => {
2161
+ const leave = meeting.leave({resourceId: meeting.resourceId, reason: MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST});
2162
+
2163
+ assert.exists(leave.then);
2164
+ await leave;
2165
+ assert.calledWith(meeting.meetingRequest.leaveMeeting, {
2166
+ locusUrl: meeting.locusUrl,
2167
+ correlationId: meeting.correlationId,
2168
+ selfId: meeting.selfId,
2169
+ resourceId: meeting.resourceId,
2170
+ deviceUrl: meeting.deviceUrl,
2171
+ reason: MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST
2172
+ });
2173
+ });
2174
+ });
2175
+ describe('#requestScreenShareFloor', () => {
2176
+ it('should have #requestScreenShareFloor', () => {
2177
+ assert.exists(meeting.requestScreenShareFloor);
2178
+ });
2179
+ beforeEach(() => {
2180
+ meeting.locusInfo.mediaShares = [{name: 'content', url: url1}];
2181
+ meeting.locusInfo.self = {url: url1};
2182
+ meeting.meetingRequest.changeMeetingFloor = sinon.stub().returns(Promise.resolve());
2183
+ meeting.mediaProperties.shareTrack = {}
2184
+ meeting.mediaProperties.mediaDirection.sendShare = true;
2185
+ meeting.state = 'JOINED';
2186
+ });
2187
+ it('should send the share', async () => {
2188
+ const share = meeting.requestScreenShareFloor();
2189
+
2190
+ assert.exists(share.then);
2191
+ await share;
2192
+ assert.calledOnce(meeting.meetingRequest.changeMeetingFloor);
2193
+ });
1991
2194
  });
1992
2195
 
1993
2196
  describe('#sendDTMF', () => {
@@ -2034,37 +2237,25 @@ describe('plugin-meetings', () => {
2034
2237
 
2035
2238
  describe('#updateMedia', () => {
2036
2239
  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
2240
 
2241
+ const createFakeLocalTrack = () => ({
2242
+ underlyingTrack: {id: 'fake underlying track'}
2243
+ });
2054
2244
  beforeEach(() => {
2055
2245
  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
- });
2246
+ meeting.audio = { enable: sinon.stub()};
2247
+ meeting.video = { enable: sinon.stub()};
2248
+ meeting.mediaProperties.audioTrack = createFakeLocalTrack();
2249
+ meeting.mediaProperties.videoTrack = createFakeLocalTrack();
2250
+ meeting.mediaProperties.shareTrack = createFakeLocalTrack();
2251
+ meeting.mediaProperties.mediaDirection = {
2252
+ sendAudio: true,
2253
+ sendVideo: true,
2254
+ sendShare: true,
2255
+ receiveAudio: true,
2256
+ receiveVideo: true,
2257
+ receiveShare: true,
2258
+ }
2068
2259
  });
2069
2260
 
2070
2261
  afterEach(() => {
@@ -2072,36 +2263,45 @@ describe('plugin-meetings', () => {
2072
2263
  sandbox = null;
2073
2264
  });
2074
2265
 
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
- };
2266
+ forEach(
2267
+ [
2268
+ {audioEnabled: true, enableMultistreamAudio: true},
2269
+ {audioEnabled: false, enableMultistreamAudio: false},
2270
+ ],
2271
+ ({audioEnabled, enableMultistreamAudio}) => {
2272
+ it(`should call enableMultistreamAudio with ${enableMultistreamAudio} if it is a multistream connection and audioEnabled: ${audioEnabled}`, async () => {
2273
+ meeting.mediaProperties.webrtcMediaConnection = {
2274
+ enableMultistreamAudio: sinon.stub().resolves({}),
2275
+ };
2276
+ meeting.isMultistream = true;
2277
+
2278
+ await meeting.updateMedia({audioEnabled});
2279
+
2280
+ assert.calledOnceWithExactly(
2281
+ meeting.mediaProperties.webrtcMediaConnection.enableMultistreamAudio,
2282
+ enableMultistreamAudio
2283
+ );
2284
+ assert.calledOnceWithExactly(meeting.audio.enable, meeting, enableMultistreamAudio);
2285
+ });
2286
+ }
2287
+ );
2085
2288
 
2289
+ it('should use a queue if currently busy', async () => {
2086
2290
  sandbox.stub(meeting, 'canUpdateMedia').returns(false);
2087
2291
  meeting.mediaProperties.webrtcMediaConnection = {
2088
- updateSendReceiveOptions: sinon.stub().resolves({}),
2292
+ update: sinon.stub().resolves({}),
2089
2293
  };
2090
2294
 
2091
2295
  let myPromiseResolved = false;
2092
2296
 
2093
2297
  meeting
2094
- .updateMedia({
2095
- localStream: mockLocalStream,
2096
- localShare: mockLocalShare,
2097
- mediaSettings,
2098
- })
2298
+ .updateMedia({audioEnabled: false, videoEnabled: false})
2099
2299
  .then(() => {
2100
2300
  myPromiseResolved = true;
2101
2301
  });
2102
2302
 
2103
2303
  // verify that nothing was done
2104
- assert.notCalled(meeting.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions);
2304
+ assert.notCalled(meeting.mediaProperties.webrtcMediaConnection.update);
2105
2305
 
2106
2306
  // now trigger processing of the queue
2107
2307
  meeting.canUpdateMedia.restore();
@@ -2110,22 +2310,22 @@ describe('plugin-meetings', () => {
2110
2310
  meeting.processNextQueuedMediaUpdate();
2111
2311
  await testUtils.flushPromises();
2112
2312
 
2113
- // and check that updateSendReceiveOptions is called with the original args
2114
- assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions);
2313
+ // and check that update is called with the original args
2314
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.update);
2115
2315
  assert.calledWith(
2116
- meeting.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions,
2316
+ meeting.mediaProperties.webrtcMediaConnection.update,
2117
2317
  {
2118
- send: {
2119
- audio: FAKE_TRACKS.audio,
2120
- video: FAKE_TRACKS.video,
2121
- screenShareVideo: FAKE_TRACKS.screenshareVideo,
2318
+ localTracks: {
2319
+ audio: meeting.mediaProperties.audioTrack.underlyingTrack,
2320
+ video: meeting.mediaProperties.videoTrack.underlyingTrack,
2321
+ screenShareVideo: meeting.mediaProperties.shareTrack.underlyingTrack,
2122
2322
  },
2123
- receive: {
2124
- audio: true,
2125
- video: true,
2126
- screenShareVideo: true,
2127
- remoteQualityLevel: 'HIGH',
2323
+ direction: {
2324
+ audio: 'inactive',
2325
+ video: 'inactive',
2326
+ screenShareVideo: 'sendrecv',
2128
2327
  },
2328
+ remoteQualityLevel: 'HIGH',
2129
2329
  }
2130
2330
  );
2131
2331
  assert.isTrue(myPromiseResolved);
@@ -2144,8 +2344,6 @@ describe('plugin-meetings', () => {
2144
2344
  sendShare: false,
2145
2345
  receiveVideo: true,
2146
2346
  };
2147
- meeting.getMediaStreams = sinon.stub().returns(Promise.resolve([]));
2148
- meeting.updateVideo = sinon.stub().returns(Promise.resolve());
2149
2347
  meeting.mediaProperties.mediaDirection = mediaDirection;
2150
2348
  meeting.mediaProperties.remoteVideoTrack = sinon
2151
2349
  .stub()
@@ -2382,76 +2580,12 @@ describe('plugin-meetings', () => {
2382
2580
  });
2383
2581
  });
2384
2582
 
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
2583
  describe('#setRemoteQualityLevel', () => {
2450
2584
  let mediaDirection;
2451
2585
 
2452
2586
  beforeEach(() => {
2453
2587
  mediaDirection = {receiveAudio: true, receiveVideo: true, receiveShare: false};
2454
- meeting.updateMedia = sinon.stub().returns(Promise.resolve());
2588
+ meeting.updateTranscodedMediaConnection = sinon.stub().returns(Promise.resolve());
2455
2589
  meeting.mediaProperties.mediaDirection = mediaDirection;
2456
2590
  });
2457
2591
 
@@ -2464,9 +2598,9 @@ describe('plugin-meetings', () => {
2464
2598
  assert.equal(meeting.mediaProperties.remoteQualityLevel, CONSTANTS.QUALITY_LEVELS.LOW);
2465
2599
  }));
2466
2600
 
2467
- it('should call updateMedia', () =>
2601
+ it('should call Meeting.updateTranscodedMediaConnection()', () =>
2468
2602
  meeting.setRemoteQualityLevel(CONSTANTS.QUALITY_LEVELS.LOW).then(() => {
2469
- assert.calledOnce(meeting.updateMedia);
2603
+ assert.calledOnce(meeting.updateTranscodedMediaConnection);
2470
2604
  }));
2471
2605
 
2472
2606
  it('should error if set to a invalid level', () => {
@@ -2487,7 +2621,6 @@ describe('plugin-meetings', () => {
2487
2621
  meeting.meetingRequest.dialOut = sinon
2488
2622
  .stub()
2489
2623
  .returns(Promise.resolve({body: {locus: 'testData'}}));
2490
- meeting.locusInfo.onFullLocus = sinon.stub().returns(Promise.resolve());
2491
2624
  });
2492
2625
 
2493
2626
  it('with no parameters triggers dial-in, delegating request to meetingRequest correctly', async () => {
@@ -2500,11 +2633,9 @@ describe('plugin-meetings', () => {
2500
2633
  locusUrl: meeting.locusUrl,
2501
2634
  clientUrl: meeting.deviceUrl,
2502
2635
  });
2503
- assert.calledWith(meeting.locusInfo.onFullLocus, 'testData');
2504
2636
  assert.notCalled(meeting.meetingRequest.dialOut);
2505
2637
 
2506
2638
  meeting.meetingRequest.dialIn.resetHistory();
2507
- meeting.locusInfo.onFullLocus.resetHistory();
2508
2639
 
2509
2640
  // try again. the dial in urls should match
2510
2641
  await meeting.usePhoneAudio();
@@ -2515,7 +2646,6 @@ describe('plugin-meetings', () => {
2515
2646
  locusUrl: meeting.locusUrl,
2516
2647
  clientUrl: meeting.deviceUrl,
2517
2648
  });
2518
- assert.calledWith(meeting.locusInfo.onFullLocus, 'testData');
2519
2649
  assert.notCalled(meeting.meetingRequest.dialOut);
2520
2650
  });
2521
2651
 
@@ -2532,11 +2662,9 @@ describe('plugin-meetings', () => {
2532
2662
  clientUrl: meeting.deviceUrl,
2533
2663
  phoneNumber,
2534
2664
  });
2535
- assert.calledWith(meeting.locusInfo.onFullLocus, 'testData');
2536
2665
  assert.notCalled(meeting.meetingRequest.dialIn);
2537
2666
 
2538
2667
  meeting.meetingRequest.dialOut.resetHistory();
2539
- meeting.locusInfo.onFullLocus.resetHistory();
2540
2668
 
2541
2669
  // try again. the dial out urls should match
2542
2670
  await meeting.usePhoneAudio(phoneNumber);
@@ -2548,7 +2676,6 @@ describe('plugin-meetings', () => {
2548
2676
  clientUrl: meeting.deviceUrl,
2549
2677
  phoneNumber,
2550
2678
  });
2551
- assert.calledWith(meeting.locusInfo.onFullLocus, 'testData');
2552
2679
  assert.notCalled(meeting.meetingRequest.dialIn);
2553
2680
  });
2554
2681
 
@@ -2583,6 +2710,19 @@ describe('plugin-meetings', () => {
2583
2710
  });
2584
2711
  });
2585
2712
 
2713
+ describe("#isJoined", () => {
2714
+ it("should returns isJoined correctly", () => {
2715
+ meeting.joinedWith = undefined;
2716
+ assert.equal(meeting.isJoined(), false);
2717
+
2718
+ meeting.joinedWith = {state: "NOT_JOINED"};
2719
+ assert.equal(meeting.isJoined(), false);
2720
+
2721
+ meeting.joinedWith = {state: "JOINED"};
2722
+ assert.equal(meeting.isJoined(), true);
2723
+ });
2724
+ });
2725
+
2586
2726
  describe('#fetchMeetingInfo', () => {
2587
2727
  const FAKE_DESTINATION = 'something@somecompany.com';
2588
2728
  const FAKE_TYPE = _SIP_URI_;
@@ -2593,6 +2733,9 @@ describe('plugin-meetings', () => {
2593
2733
  const FAKE_CAPTCHA_IMAGE_URL = 'http://captchaimage';
2594
2734
  const FAKE_CAPTCHA_AUDIO_URL = 'http://captchaaudio';
2595
2735
  const FAKE_CAPTCHA_REFRESH_URL = 'http://captcharefresh';
2736
+ const FAKE_INSTALLED_ORG_ID = '123456';
2737
+ const FAKE_EXTRA_PARAMS = {mtid: 'm9fe0afd8c435e892afcce9ea25b97046', joinTXId: 'TSmrX61wNF'};
2738
+ let FAKE_OPTIONS;
2596
2739
  const FAKE_MEETING_INFO = {
2597
2740
  conversationUrl: 'some_convo_url',
2598
2741
  locusUrl: 'some_locus_url',
@@ -2600,6 +2743,8 @@ describe('plugin-meetings', () => {
2600
2743
  meetingNumber: '123456', // this.config.experimental.enableUnifiedMeetings
2601
2744
  hostId: 'some_host_id', // this.owner;
2602
2745
  };
2746
+ const FAKE_MEETING_INFO_LOOKUP_URL = 'meetingLookupUrl';
2747
+
2603
2748
  const FAKE_SDK_CAPTCHA_INFO = {
2604
2749
  captchaId: FAKE_CAPTCHA_ID,
2605
2750
  verificationImageURL: FAKE_CAPTCHA_IMAGE_URL,
@@ -2613,18 +2758,26 @@ describe('plugin-meetings', () => {
2613
2758
  refreshURL: `${FAKE_CAPTCHA_REFRESH_URL}-2`,
2614
2759
  };
2615
2760
 
2761
+ beforeEach(() => {
2762
+ meeting.locusId = 'locus-id';
2763
+ meeting.id = 'meeting-id';
2764
+ FAKE_OPTIONS = {meetingId: meeting.id};
2765
+ });
2766
+
2616
2767
  it('calls meetingInfoProvider with all the right parameters and parses the result', async () => {
2617
2768
  meeting.attrs.meetingInfoProvider = {
2618
- fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO}),
2769
+ fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO, url: FAKE_MEETING_INFO_LOOKUP_URL}),
2619
2770
  };
2620
2771
  meeting.requiredCaptcha = FAKE_SDK_CAPTCHA_INFO;
2621
2772
  meeting.destination = FAKE_DESTINATION;
2622
2773
  meeting.destinationType = FAKE_TYPE;
2774
+ meeting.config.installedOrgID = FAKE_INSTALLED_ORG_ID;
2623
2775
  meeting.parseMeetingInfo = sinon.stub().returns(undefined);
2624
2776
 
2625
2777
  await meeting.fetchMeetingInfo({
2626
2778
  password: FAKE_PASSWORD,
2627
2779
  captchaCode: FAKE_CAPTCHA_CODE,
2780
+ extraParams: FAKE_EXTRA_PARAMS,
2628
2781
  });
2629
2782
 
2630
2783
  assert.calledWith(
@@ -2632,11 +2785,15 @@ describe('plugin-meetings', () => {
2632
2785
  FAKE_DESTINATION,
2633
2786
  FAKE_TYPE,
2634
2787
  FAKE_PASSWORD,
2635
- {code: FAKE_CAPTCHA_CODE, id: FAKE_CAPTCHA_ID}
2788
+ {code: FAKE_CAPTCHA_CODE, id: FAKE_CAPTCHA_ID},
2789
+ FAKE_INSTALLED_ORG_ID,
2790
+ meeting.locusId,
2791
+ FAKE_EXTRA_PARAMS,
2792
+ FAKE_OPTIONS
2636
2793
  );
2637
2794
 
2638
- assert.calledWith(meeting.parseMeetingInfo, {body: FAKE_MEETING_INFO}, FAKE_DESTINATION);
2639
- assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
2795
+ assert.calledWith(meeting.parseMeetingInfo, {body: FAKE_MEETING_INFO, url: FAKE_MEETING_INFO_LOOKUP_URL}, FAKE_DESTINATION);
2796
+ assert.deepEqual(meeting.meetingInfo, {...FAKE_MEETING_INFO, meetingLookupUrl: FAKE_MEETING_INFO_LOOKUP_URL});
2640
2797
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.NOT_REQUIRED);
2641
2798
  assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.NONE);
2642
2799
  assert.equal(meeting.requiredCaptcha, null);
@@ -2651,7 +2808,7 @@ describe('plugin-meetings', () => {
2651
2808
 
2652
2809
  it('calls meetingInfoProvider with all the right parameters and parses the result when random delay is applied', async () => {
2653
2810
  meeting.attrs.meetingInfoProvider = {
2654
- fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO}),
2811
+ fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO, url: FAKE_MEETING_INFO_LOOKUP_URL}),
2655
2812
  };
2656
2813
  meeting.destination = FAKE_DESTINATION;
2657
2814
  meeting.destinationType = FAKE_TYPE;
@@ -2674,13 +2831,17 @@ describe('plugin-meetings', () => {
2674
2831
  FAKE_DESTINATION,
2675
2832
  FAKE_TYPE,
2676
2833
  null,
2677
- null
2834
+ null,
2835
+ undefined,
2836
+ meeting.locusId,
2837
+ {},
2838
+ {meetingId: meeting.id}
2678
2839
  );
2679
2840
 
2680
2841
  // parseMeeting info
2681
- assert.calledWith(meeting.parseMeetingInfo, {body: FAKE_MEETING_INFO}, FAKE_DESTINATION);
2842
+ assert.calledWith(meeting.parseMeetingInfo, {body: FAKE_MEETING_INFO, url: FAKE_MEETING_INFO_LOOKUP_URL}, FAKE_DESTINATION);
2682
2843
 
2683
- assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
2844
+ assert.deepEqual(meeting.meetingInfo, {...FAKE_MEETING_INFO, meetingLookupUrl: FAKE_MEETING_INFO_LOOKUP_URL});
2684
2845
  assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.NONE);
2685
2846
  assert.equal(meeting.requiredCaptcha, null);
2686
2847
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.NOT_REQUIRED);
@@ -2696,7 +2857,7 @@ describe('plugin-meetings', () => {
2696
2857
 
2697
2858
  it('fails if captchaCode is provided when captcha not needed', async () => {
2698
2859
  meeting.attrs.meetingInfoProvider = {
2699
- fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO}),
2860
+ fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO, url: FAKE_MEETING_INFO_LOOKUP_URL}),
2700
2861
  };
2701
2862
  meeting.requiredCaptcha = null;
2702
2863
  meeting.destination = FAKE_DESTINATION;
@@ -2715,7 +2876,7 @@ describe('plugin-meetings', () => {
2715
2876
 
2716
2877
  it('fails if password is provided when not required', async () => {
2717
2878
  meeting.attrs.meetingInfoProvider = {
2718
- fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO}),
2879
+ fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO, url: FAKE_MEETING_INFO_LOOKUP_URL}),
2719
2880
  };
2720
2881
  meeting.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
2721
2882
  meeting.destination = FAKE_DESTINATION;
@@ -2748,10 +2909,15 @@ describe('plugin-meetings', () => {
2748
2909
  FAKE_DESTINATION,
2749
2910
  FAKE_TYPE,
2750
2911
  null,
2751
- null
2912
+ null,
2913
+ undefined,
2914
+ 'locus-id',
2915
+ {},
2916
+ {meetingId: meeting.id},
2752
2917
  );
2753
2918
 
2754
2919
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
2920
+ assert.equal(meeting.meetingInfoFailureCode, 403004);
2755
2921
  assert.equal(
2756
2922
  meeting.meetingInfoFailureReason,
2757
2923
  MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD
@@ -2760,6 +2926,38 @@ describe('plugin-meetings', () => {
2760
2926
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.REQUIRED);
2761
2927
  });
2762
2928
 
2929
+ it('handles meetingInfoProvider policy error', async () => {
2930
+ meeting.destination = FAKE_DESTINATION;
2931
+ meeting.destinationType = FAKE_TYPE;
2932
+ meeting.attrs.meetingInfoProvider = {
2933
+ fetchMeetingInfo: sinon
2934
+ .stub()
2935
+ .throws(new MeetingInfoV2PolicyError(123456, FAKE_MEETING_INFO, 'a message')),
2936
+ };
2937
+
2938
+ await assert.isRejected(meeting.fetchMeetingInfo({}), PermissionError);
2939
+
2940
+ assert.calledWith(
2941
+ meeting.attrs.meetingInfoProvider.fetchMeetingInfo,
2942
+ FAKE_DESTINATION,
2943
+ FAKE_TYPE,
2944
+ null,
2945
+ null,
2946
+ undefined,
2947
+ 'locus-id',
2948
+ {},
2949
+ {meetingId: meeting.id},
2950
+ );
2951
+
2952
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
2953
+ assert.equal(meeting.meetingInfoFailureCode, 123456);
2954
+ assert.equal(
2955
+ meeting.meetingInfoFailureReason,
2956
+ MEETING_INFO_FAILURE_REASON.POLICY
2957
+ );
2958
+ });
2959
+
2960
+
2763
2961
  it('handles meetingInfoProvider requiring captcha because of wrong password', async () => {
2764
2962
  meeting.destination = FAKE_DESTINATION;
2765
2963
  meeting.destinationType = FAKE_TYPE;
@@ -2782,7 +2980,11 @@ describe('plugin-meetings', () => {
2782
2980
  FAKE_DESTINATION,
2783
2981
  FAKE_TYPE,
2784
2982
  'aaa',
2785
- null
2983
+ null,
2984
+ undefined,
2985
+ 'locus-id',
2986
+ {},
2987
+ {meetingId: meeting.id},
2786
2988
  );
2787
2989
 
2788
2990
  assert.deepEqual(meeting.meetingInfo, {});
@@ -2790,6 +2992,7 @@ describe('plugin-meetings', () => {
2790
2992
  meeting.meetingInfoFailureReason,
2791
2993
  MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD
2792
2994
  );
2995
+ assert.equal(meeting.meetingInfoFailureCode, 423005);
2793
2996
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.REQUIRED);
2794
2997
  assert.deepEqual(meeting.requiredCaptcha, {
2795
2998
  captchaId: FAKE_CAPTCHA_ID,
@@ -2822,7 +3025,11 @@ describe('plugin-meetings', () => {
2822
3025
  FAKE_DESTINATION,
2823
3026
  FAKE_TYPE,
2824
3027
  'aaa',
2825
- {code: 'bbb', id: FAKE_CAPTCHA_ID}
3028
+ {code: 'bbb', id: FAKE_CAPTCHA_ID},
3029
+ undefined,
3030
+ 'locus-id',
3031
+ {},
3032
+ {meetingId: meeting.id}
2826
3033
  );
2827
3034
 
2828
3035
  assert.deepEqual(meeting.meetingInfo, {});
@@ -2851,10 +3058,14 @@ describe('plugin-meetings', () => {
2851
3058
  FAKE_DESTINATION,
2852
3059
  FAKE_TYPE,
2853
3060
  'aaa',
2854
- null
3061
+ null,
3062
+ undefined,
3063
+ 'locus-id',
3064
+ {},
3065
+ {meetingId: meeting.id},
2855
3066
  );
2856
3067
 
2857
- assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
3068
+ assert.deepEqual(meeting.meetingInfo, {...FAKE_MEETING_INFO, meetingLookupUrl: undefined});
2858
3069
  assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.NONE);
2859
3070
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.VERIFIED);
2860
3071
  assert.equal(meeting.requiredCaptcha, null);
@@ -2896,7 +3107,11 @@ describe('plugin-meetings', () => {
2896
3107
  FAKE_DESTINATION,
2897
3108
  FAKE_TYPE,
2898
3109
  'aaa',
2899
- {code: 'bbb', id: FAKE_CAPTCHA_ID}
3110
+ {code: 'bbb', id: FAKE_CAPTCHA_ID},
3111
+ undefined,
3112
+ 'locus-id',
3113
+ {},
3114
+ {meetingId: meeting.id},
2900
3115
  );
2901
3116
 
2902
3117
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
@@ -3041,6 +3256,17 @@ describe('plugin-meetings', () => {
3041
3256
  });
3042
3257
  });
3043
3258
 
3259
+ describe('#postMetrics', () => {
3260
+ it('should have #postMetrics', () => {
3261
+ assert.exists(meeting.postMetrics);
3262
+ });
3263
+
3264
+ it('should trigger `postMetrics`', async () => {
3265
+ await meeting.postMetrics(eventType.LEAVE);
3266
+ assert.calledWithMatch(Metrics.postEvent, {event: eventType.LEAVE});
3267
+ });
3268
+ });
3269
+
3044
3270
  describe('#endMeeting for all', () => {
3045
3271
  let sandbox;
3046
3272
 
@@ -3061,16 +3287,12 @@ describe('plugin-meetings', () => {
3061
3287
  .stub()
3062
3288
  .returns(Promise.resolve({body: 'test'}));
3063
3289
  meeting.locusInfo.onFullLocus = sinon.stub().returns(true);
3064
- meeting.closeLocalStream = sinon.stub().returns(Promise.resolve());
3065
- meeting.closeLocalShare = sinon.stub().returns(Promise.resolve());
3290
+ meeting.cleanupLocalTracks = sinon.stub().returns(Promise.resolve());
3066
3291
  meeting.closeRemoteStream = sinon.stub().returns(Promise.resolve());
3067
3292
  sandbox.stub(meeting, 'closeRemoteTracks').returns(Promise.resolve());
3068
3293
  meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
3069
- meeting.unsetLocalVideoTrack = sinon.stub().returns(true);
3070
- meeting.unsetLocalShareTrack = sinon.stub().returns(true);
3071
3294
  meeting.unsetRemoteTracks = sinon.stub();
3072
3295
  meeting.statsAnalyzer = {stopAnalyzer: sinon.stub().resolves()};
3073
- meeting.unsetRemoteStream = sinon.stub().returns(true);
3074
3296
  meeting.unsetPeerConnections = sinon.stub().returns(true);
3075
3297
  meeting.logger.error = sinon.stub().returns(true);
3076
3298
  meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
@@ -3089,12 +3311,9 @@ describe('plugin-meetings', () => {
3089
3311
  assert.exists(endMeetingForAll.then);
3090
3312
  await endMeetingForAll;
3091
3313
  assert.calledOnce(meeting?.meetingRequest?.endMeetingForAll);
3092
- assert.calledOnce(meeting?.closeLocalStream);
3093
- assert.calledOnce(meeting?.closeLocalShare);
3314
+ assert.calledOnce(meeting?.cleanupLocalTracks);
3094
3315
  assert.calledOnce(meeting?.closeRemoteTracks);
3095
3316
  assert.calledOnce(meeting?.closePeerConnections);
3096
- assert.calledOnce(meeting?.unsetLocalVideoTrack);
3097
- assert.calledOnce(meeting?.unsetLocalShareTrack);
3098
3317
  assert.calledOnce(meeting?.unsetRemoteTracks);
3099
3318
  assert.calledOnce(meeting?.unsetPeerConnections);
3100
3319
  });
@@ -3105,11 +3324,9 @@ describe('plugin-meetings', () => {
3105
3324
 
3106
3325
  beforeEach(() => {
3107
3326
  sandbox = sinon.createSandbox();
3108
- sandbox.stub(meeting, 'closeLocalStream');
3109
- sandbox.stub(meeting, 'closeLocalShare');
3327
+ sandbox.stub(meeting, 'cleanupLocalTracks');
3110
3328
 
3111
3329
  sandbox.stub(meeting.mediaProperties, 'setMediaDirection');
3112
- sandbox.stub(meeting.mediaProperties, 'unsetMediaTracks');
3113
3330
 
3114
3331
  sandbox.stub(meeting.reconnectionManager, 'reconnectMedia').returns(Promise.resolve());
3115
3332
  sandbox
@@ -3182,14 +3399,12 @@ describe('plugin-meetings', () => {
3182
3399
 
3183
3400
  // beacuse we are calling callback so we need to wait
3184
3401
 
3185
- assert.called(meeting.closeLocalStream);
3186
- assert.called(meeting.closeLocalShare);
3402
+ assert.called(meeting.cleanupLocalTracks);
3187
3403
 
3188
3404
  // give queued Promise callbacks a chance to run
3189
3405
  await Promise.resolve();
3190
3406
 
3191
3407
  assert.called(meeting.mediaProperties.setMediaDirection);
3192
- assert.called(meeting.mediaProperties.unsetMediaTracks);
3193
3408
 
3194
3409
  assert.calledWith(meeting.reconnectionManager.reconnectMedia, {
3195
3410
  mediaDirection: {
@@ -3304,6 +3519,284 @@ describe('plugin-meetings', () => {
3304
3519
  }
3305
3520
  });
3306
3521
  });
3522
+ describe('Local tracks publishing', () => {
3523
+ let audioTrack;
3524
+ let videoTrack;
3525
+ let videoShareTrack;
3526
+ let createMuteStateStub;
3527
+ let LocalDisplayTrackConstructorStub;
3528
+ let LocalMicrophoneTrackConstructorStub;
3529
+ let LocalCameraTrackConstructorStub;
3530
+ let fakeLocalDisplayTrack;
3531
+ let fakeLocalMicrophoneTrack;
3532
+ let fakeLocalCameraTrack;
3533
+
3534
+ beforeEach(() => {
3535
+ audioTrack = {
3536
+ id: 'audio track',
3537
+ getSettings: sinon.stub().returns({}),
3538
+ on: sinon.stub(),
3539
+ off: sinon.stub(),
3540
+ };
3541
+ videoTrack = {
3542
+ id: 'video track',
3543
+ getSettings: sinon.stub().returns({}),
3544
+ on: sinon.stub(),
3545
+ off: sinon.stub(),
3546
+ };
3547
+ videoShareTrack = {
3548
+ id: 'share track',
3549
+ on: sinon.stub(),
3550
+ off: sinon.stub(),
3551
+ getSettings: sinon.stub().returns({}),
3552
+ };
3553
+ meeting.requestScreenShareFloor = sinon.stub().resolves({});
3554
+ meeting.releaseScreenShareFloor = sinon.stub().resolves({});
3555
+ meeting.mediaProperties.mediaDirection = {
3556
+ sendAudio: 'fake value', // using non-boolean here so that we can check that these values are untouched in tests
3557
+ sendVideo: 'fake value',
3558
+ sendShare: false,
3559
+ };
3560
+ meeting.isMultistream = true;
3561
+ meeting.mediaProperties.webrtcMediaConnection = {
3562
+ publishTrack: sinon.stub().resolves({}),
3563
+ unpublishTrack: sinon.stub().resolves({}),
3564
+ };
3565
+ meeting.audio = { handleLocalTrackChange: sinon.stub()};
3566
+ meeting.video = { handleLocalTrackChange: sinon.stub()};
3567
+
3568
+ const createFakeLocalTrack = (originalTrack) => ({
3569
+ on: sinon.stub(),
3570
+ off: sinon.stub(),
3571
+ stop: sinon.stub(),
3572
+ originalTrack,
3573
+ });
3574
+
3575
+ // setup mock constructors for webrtc-core local track classes in such a way
3576
+ // that they return the original track correctly (this is needed for unpublish() API tests)
3577
+ LocalDisplayTrackConstructorStub = sinon
3578
+ .stub(InternalMediaCoreModule, 'LocalDisplayTrack')
3579
+ .callsFake((stream) => {
3580
+ fakeLocalDisplayTrack = createFakeLocalTrack(stream.getTracks()[0]);
3581
+ return fakeLocalDisplayTrack;
3582
+ });
3583
+ LocalMicrophoneTrackConstructorStub = sinon
3584
+ .stub(InternalMediaCoreModule, 'LocalMicrophoneTrack')
3585
+ .callsFake((stream) => {
3586
+ fakeLocalMicrophoneTrack = createFakeLocalTrack(stream.getTracks()[0]);
3587
+ return fakeLocalMicrophoneTrack;
3588
+ });
3589
+ LocalCameraTrackConstructorStub = sinon
3590
+ .stub(InternalMediaCoreModule, 'LocalCameraTrack')
3591
+ .callsFake((stream) => {
3592
+ fakeLocalCameraTrack = createFakeLocalTrack(stream.getTracks()[0]);
3593
+ return fakeLocalCameraTrack;
3594
+ });
3595
+
3596
+ createMuteStateStub = sinon
3597
+ .stub(MuteStateModule, 'createMuteState')
3598
+ .returns({id: 'fake mute state instance'});
3599
+ });
3600
+ describe('#publishTracks', () => {
3601
+ it('fails if there is no media connection', async () => {
3602
+ meeting.mediaProperties.webrtcMediaConnection = undefined;
3603
+ await assert.isRejected(meeting.publishTracks({audio: {id: 'some audio track'}}));
3604
+ });
3605
+
3606
+ const checkAudioPublished = (track) => {
3607
+ assert.calledOnceWithExactly(meeting.audio.handleLocalTrackChange, meeting);
3608
+ assert.calledWith(
3609
+ meeting.mediaProperties.webrtcMediaConnection.publishTrack,
3610
+ track
3611
+ );
3612
+ assert.equal(meeting.mediaProperties.audioTrack, track);
3613
+ // check that sendAudio hasn't been touched
3614
+ assert.equal(meeting.mediaProperties.mediaDirection.sendAudio, 'fake value');
3615
+ };
3616
+
3617
+ const checkVideoPublished = (track) => {
3618
+ assert.calledOnceWithExactly(meeting.video.handleLocalTrackChange, meeting);
3619
+ assert.calledWith(
3620
+ meeting.mediaProperties.webrtcMediaConnection.publishTrack,
3621
+ track
3622
+ );
3623
+ assert.equal(meeting.mediaProperties.videoTrack, track);
3624
+ // check that sendVideo hasn't been touched
3625
+ assert.equal(meeting.mediaProperties.mediaDirection.sendVideo, 'fake value');
3626
+ };
3627
+
3628
+ const checkScreenShareVideoPublished = (track) => {
3629
+ assert.calledOnce(meeting.requestScreenShareFloor);
3630
+
3631
+ assert.calledWith(
3632
+ meeting.mediaProperties.webrtcMediaConnection.publishTrack,
3633
+ track
3634
+ );
3635
+ assert.equal(meeting.mediaProperties.shareTrack, track);
3636
+ assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
3637
+ };
3638
+
3639
+ it('requests screen share floor and publishes the screen share video track', async () => {
3640
+ await meeting.publishTracks({screenShare: {video: videoShareTrack}});
3641
+
3642
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.publishTrack);
3643
+ checkScreenShareVideoPublished(videoShareTrack);
3644
+ });
3645
+
3646
+ it('updates MuteState instance and publishes the track for main audio', async () => {
3647
+ await meeting.publishTracks({microphone: audioTrack});
3648
+
3649
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.publishTrack);
3650
+ checkAudioPublished(audioTrack);
3651
+ });
3652
+
3653
+ it('updates MuteState instance and publishes the track for main video', async () => {
3654
+ await meeting.publishTracks({camera: videoTrack});
3655
+
3656
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.publishTrack);
3657
+ checkVideoPublished(videoTrack);
3658
+ });
3659
+
3660
+ it('publishes audio, video and screen share together', async () => {
3661
+ await meeting.publishTracks({
3662
+ microphone: audioTrack,
3663
+ camera: videoTrack,
3664
+ screenShare: {
3665
+ video: videoShareTrack,
3666
+ },
3667
+ });
3668
+
3669
+ assert.calledThrice(meeting.mediaProperties.webrtcMediaConnection.publishTrack);
3670
+ checkAudioPublished(audioTrack);
3671
+ checkVideoPublished(videoTrack);
3672
+ checkScreenShareVideoPublished(videoShareTrack);
3673
+ });
3674
+ });
3675
+ it('creates instance and publishes with annotation info', async () => {
3676
+ const annotationInfo = {
3677
+ version: '1',
3678
+ policy: ANNOTATION_POLICY.APPROVAL,
3679
+ };
3680
+ await meeting.publishTracks({annotationInfo});
3681
+ assert.equal(meeting.annotationInfo, annotationInfo);
3682
+ });
3683
+
3684
+ describe('unpublishTracks', () => {
3685
+ beforeEach(async () => {
3686
+ await meeting.publishTracks({
3687
+ microphone: audioTrack,
3688
+ camera: videoTrack,
3689
+ screenShare: {video: videoShareTrack},
3690
+ });
3691
+ });
3692
+
3693
+ const checkAudioUnpublished = () => {
3694
+ assert.calledWith(
3695
+ meeting.mediaProperties.webrtcMediaConnection.unpublishTrack,
3696
+ audioTrack
3697
+ );
3698
+
3699
+ assert.equal(meeting.mediaProperties.audioTrack, null);
3700
+ assert.equal(meeting.mediaProperties.mediaDirection.sendAudio, 'fake value');
3701
+ };
3702
+
3703
+ const checkVideoUnpublished = () => {
3704
+ assert.calledWith(
3705
+ meeting.mediaProperties.webrtcMediaConnection.unpublishTrack,
3706
+ videoTrack
3707
+ );
3708
+
3709
+ assert.equal(meeting.mediaProperties.videoTrack, null);
3710
+ assert.equal(meeting.mediaProperties.mediaDirection.sendVideo, 'fake value');
3711
+ };
3712
+
3713
+ const checkScreenShareVideoUnpublished = () => {
3714
+ assert.calledWith(
3715
+ meeting.mediaProperties.webrtcMediaConnection.unpublishTrack,
3716
+ videoShareTrack
3717
+ );
3718
+
3719
+ assert.calledOnce(meeting.requestScreenShareFloor);
3720
+
3721
+ assert.equal(meeting.mediaProperties.shareTrack, null);
3722
+ assert.equal(meeting.mediaProperties.mediaDirection.sendShare, false);
3723
+ };
3724
+
3725
+ it('fails if there is no media connection', async () => {
3726
+ meeting.mediaProperties.webrtcMediaConnection = undefined;
3727
+ await assert.isRejected(
3728
+ meeting.unpublishTracks([audioTrack, videoTrack, videoShareTrack])
3729
+ );
3730
+ });
3731
+
3732
+ it('un-publishes the tracks correctly (all 3 together)', async () => {
3733
+ await meeting.unpublishTracks([audioTrack, videoTrack, videoShareTrack]);
3734
+
3735
+ assert.calledThrice(meeting.mediaProperties.webrtcMediaConnection.unpublishTrack);
3736
+ checkAudioUnpublished();
3737
+ checkVideoUnpublished();
3738
+ checkScreenShareVideoUnpublished();
3739
+ });
3740
+
3741
+ it('un-publishes the audio track correctly', async () => {
3742
+ await meeting.unpublishTracks([audioTrack]);
3743
+
3744
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.unpublishTrack);
3745
+ checkAudioUnpublished();
3746
+ });
3747
+
3748
+ it('un-publishes the video track correctly', async () => {
3749
+ await meeting.unpublishTracks([videoTrack]);
3750
+
3751
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.unpublishTrack);
3752
+ checkVideoUnpublished();
3753
+ });
3754
+
3755
+ it('un-publishes the screen share video track correctly', async () => {
3756
+ await meeting.unpublishTracks([videoShareTrack]);
3757
+
3758
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.unpublishTrack);
3759
+ checkScreenShareVideoUnpublished();
3760
+ });
3761
+ });
3762
+ });
3763
+ });
3764
+
3765
+ describe('#enableMusicMode', () => {
3766
+ beforeEach(() => {
3767
+ meeting.isMultistream = true;
3768
+ meeting.mediaProperties.webrtcMediaConnection = {
3769
+ setCodecParameters: sinon.stub().resolves({}),
3770
+ deleteCodecParameters: sinon.stub().resolves({}),
3771
+ };
3772
+ });
3773
+ [
3774
+ {shouldEnableMusicMode: true},
3775
+ {shouldEnableMusicMode: false},
3776
+ ].forEach(({shouldEnableMusicMode}) => {
3777
+ it(`fails if there is no media connection for shouldEnableMusicMode: ${shouldEnableMusicMode}`, async () => {
3778
+ meeting.mediaProperties.webrtcMediaConnection = undefined;
3779
+ await assert.isRejected(meeting.enableMusicMode(shouldEnableMusicMode));
3780
+ });
3781
+ });
3782
+
3783
+ it('should set the codec parameters when shouldEnableMusicMode is true', async () => {
3784
+ await meeting.enableMusicMode(true);
3785
+ assert.calledOnceWithExactly(meeting.mediaProperties.webrtcMediaConnection.setCodecParameters, MediaType.AudioMain, {
3786
+ maxaveragebitrate: '64000',
3787
+ maxplaybackrate: '48000',
3788
+ });
3789
+ assert.notCalled(meeting.mediaProperties.webrtcMediaConnection.deleteCodecParameters);
3790
+ });
3791
+
3792
+ it('should set the codec parameters when shouldEnableMusicMode is false', async () => {
3793
+ await meeting.enableMusicMode(false);
3794
+ assert.calledOnceWithExactly(meeting.mediaProperties.webrtcMediaConnection.deleteCodecParameters, MediaType.AudioMain, [
3795
+ 'maxaveragebitrate',
3796
+ 'maxplaybackrate',
3797
+ ]);
3798
+ assert.notCalled(meeting.mediaProperties.webrtcMediaConnection.setCodecParameters);
3799
+ });
3307
3800
  });
3308
3801
 
3309
3802
  describe('Public Event Triggers', () => {
@@ -3431,88 +3924,19 @@ describe('plugin-meetings', () => {
3431
3924
  {type: 'remoteAudio'}
3432
3925
  );
3433
3926
  assert.calledWith(
3434
- TriggerProxy.trigger,
3435
- sinon.match.instanceOf(Meeting),
3436
- {file: 'meeting/index', function: 'closeRemoteTracks'},
3437
- 'media:stopped',
3438
- {type: 'remoteVideo'}
3439
- );
3440
- assert.calledWith(
3441
- TriggerProxy.trigger,
3442
- sinon.match.instanceOf(Meeting),
3443
- {file: 'meeting/index', function: 'closeRemoteTracks'},
3444
- 'media:stopped',
3445
- {type: 'remoteShare'}
3446
- );
3447
- });
3448
- });
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);
3927
+ TriggerProxy.trigger,
3928
+ sinon.match.instanceOf(Meeting),
3929
+ {file: 'meeting/index', function: 'closeRemoteTracks'},
3930
+ 'media:stopped',
3931
+ {type: 'remoteVideo'}
3932
+ );
3506
3933
  assert.calledWith(
3507
3934
  TriggerProxy.trigger,
3508
3935
  sinon.match.instanceOf(Meeting),
3509
- {file: 'meeting/index', function: 'setLocalShareTrack'},
3510
- 'media:ready'
3936
+ {file: 'meeting/index', function: 'closeRemoteTracks'},
3937
+ 'media:stopped',
3938
+ {type: 'remoteShare'}
3511
3939
  );
3512
- assert.calledOnce(meeting.mediaProperties.setLocalShareTrack);
3513
- assert.equal(meeting.mediaProperties.localStream, undefined);
3514
- meeting.mediaProperties.shareTrack.onended();
3515
- assert.calledOnce(meeting.stopShare);
3516
3940
  });
3517
3941
  });
3518
3942
  describe('#setupMediaConnectionListeners', () => {
@@ -3527,48 +3951,49 @@ describe('plugin-meetings', () => {
3527
3951
  eventListeners[event] = listener;
3528
3952
  }),
3529
3953
  };
3954
+ MediaUtil.createMediaStream.returns({id: 'stream'});
3530
3955
  });
3531
3956
 
3532
3957
  it('should register for all the correct RoapMediaConnection events', () => {
3533
3958
  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]);
3959
+ assert.isFunction(eventListeners[Event.ROAP_STARTED]);
3960
+ assert.isFunction(eventListeners[Event.ROAP_DONE]);
3961
+ assert.isFunction(eventListeners[Event.ROAP_FAILURE]);
3962
+ assert.isFunction(eventListeners[Event.ROAP_MESSAGE_TO_SEND]);
3963
+ assert.isFunction(eventListeners[Event.REMOTE_TRACK_ADDED]);
3964
+ assert.isFunction(eventListeners[Event.CONNECTION_STATE_CHANGED]);
3540
3965
  });
3541
3966
 
3542
3967
  it('should trigger a media:ready event when REMOTE_TRACK_ADDED is fired', () => {
3543
3968
  meeting.setupMediaConnectionListeners();
3544
- eventListeners[MC.Event.REMOTE_TRACK_ADDED]({
3969
+ eventListeners[Event.REMOTE_TRACK_ADDED]({
3545
3970
  track: 'track',
3546
- type: MC.RemoteTrackType.AUDIO,
3971
+ type: RemoteTrackType.AUDIO,
3547
3972
  });
3548
3973
  assert.equal(TriggerProxy.trigger.getCall(1).args[2], 'media:ready');
3549
3974
  assert.deepEqual(TriggerProxy.trigger.getCall(1).args[3], {
3550
3975
  type: 'remoteAudio',
3551
- stream: true,
3976
+ stream: {id: 'stream'},
3552
3977
  });
3553
3978
 
3554
- eventListeners[MC.Event.REMOTE_TRACK_ADDED]({
3979
+ eventListeners[Event.REMOTE_TRACK_ADDED]({
3555
3980
  track: 'track',
3556
- type: MC.RemoteTrackType.VIDEO,
3981
+ type: RemoteTrackType.VIDEO,
3557
3982
  });
3558
3983
  assert.equal(TriggerProxy.trigger.getCall(2).args[2], 'media:ready');
3559
3984
  assert.deepEqual(TriggerProxy.trigger.getCall(2).args[3], {
3560
3985
  type: 'remoteVideo',
3561
- stream: true,
3986
+ stream: {id: 'stream'},
3562
3987
  });
3563
3988
 
3564
- eventListeners[MC.Event.REMOTE_TRACK_ADDED]({
3989
+ eventListeners[Event.REMOTE_TRACK_ADDED]({
3565
3990
  track: 'track',
3566
- type: MC.RemoteTrackType.SCREENSHARE_VIDEO,
3991
+ type: RemoteTrackType.SCREENSHARE_VIDEO,
3567
3992
  });
3568
3993
  assert.equal(TriggerProxy.trigger.getCall(3).args[2], 'media:ready');
3569
3994
  assert.deepEqual(TriggerProxy.trigger.getCall(3).args[3], {
3570
3995
  type: 'remoteShare',
3571
- stream: true,
3996
+ stream: {id: 'stream'},
3572
3997
  });
3573
3998
  });
3574
3999
 
@@ -3613,51 +4038,51 @@ describe('plugin-meetings', () => {
3613
4038
  };
3614
4039
 
3615
4040
  it('should send metrics for SdpOfferCreationError error', () => {
3616
- const fakeError = new MC.Errors.SdpOfferCreationError(fakeErrorMessage, {
4041
+ const fakeError = new Errors.SdpOfferCreationError(fakeErrorMessage, {
3617
4042
  name: fakeErrorName,
3618
4043
  cause: {name: fakeRootCauseName},
3619
4044
  });
3620
4045
 
3621
- eventListeners[MC.Event.ROAP_FAILURE](fakeError);
4046
+ eventListeners[Event.ROAP_FAILURE](fakeError);
3622
4047
 
3623
4048
  checkMetricSent(eventType.LOCAL_SDP_GENERATED);
3624
4049
  checkBehavioralMetricSent(
3625
4050
  BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
3626
- MC.Errors.ErrorCode.SdpOfferCreationError,
4051
+ Errors.ErrorCode.SdpOfferCreationError,
3627
4052
  fakeErrorMessage,
3628
4053
  fakeRootCauseName
3629
4054
  );
3630
4055
  });
3631
4056
 
3632
4057
  it('should send metrics for SdpOfferHandlingError error', () => {
3633
- const fakeError = new MC.Errors.SdpOfferHandlingError(fakeErrorMessage, {
4058
+ const fakeError = new Errors.SdpOfferHandlingError(fakeErrorMessage, {
3634
4059
  name: fakeErrorName,
3635
4060
  cause: {name: fakeRootCauseName},
3636
4061
  });
3637
4062
 
3638
- eventListeners[MC.Event.ROAP_FAILURE](fakeError);
4063
+ eventListeners[Event.ROAP_FAILURE](fakeError);
3639
4064
 
3640
4065
  checkMetricSent(eventType.REMOTE_SDP_RECEIVED);
3641
4066
  checkBehavioralMetricSent(
3642
4067
  BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
3643
- MC.Errors.ErrorCode.SdpOfferHandlingError,
4068
+ Errors.ErrorCode.SdpOfferHandlingError,
3644
4069
  fakeErrorMessage,
3645
4070
  fakeRootCauseName
3646
4071
  );
3647
4072
  });
3648
4073
 
3649
4074
  it('should send metrics for SdpAnswerHandlingError error', () => {
3650
- const fakeError = new MC.Errors.SdpAnswerHandlingError(fakeErrorMessage, {
4075
+ const fakeError = new Errors.SdpAnswerHandlingError(fakeErrorMessage, {
3651
4076
  name: fakeErrorName,
3652
4077
  cause: {name: fakeRootCauseName},
3653
4078
  });
3654
4079
 
3655
- eventListeners[MC.Event.ROAP_FAILURE](fakeError);
4080
+ eventListeners[Event.ROAP_FAILURE](fakeError);
3656
4081
 
3657
4082
  checkMetricSent(eventType.REMOTE_SDP_RECEIVED);
3658
4083
  checkBehavioralMetricSent(
3659
4084
  BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
3660
- MC.Errors.ErrorCode.SdpAnswerHandlingError,
4085
+ Errors.ErrorCode.SdpAnswerHandlingError,
3661
4086
  fakeErrorMessage,
3662
4087
  fakeRootCauseName
3663
4088
  );
@@ -3665,15 +4090,15 @@ describe('plugin-meetings', () => {
3665
4090
 
3666
4091
  it('should send metrics for SdpError error', () => {
3667
4092
  // SdpError is usually without a cause
3668
- const fakeError = new MC.Errors.SdpError(fakeErrorMessage, {name: fakeErrorName});
4093
+ const fakeError = new Errors.SdpError(fakeErrorMessage, {name: fakeErrorName});
3669
4094
 
3670
- eventListeners[MC.Event.ROAP_FAILURE](fakeError);
4095
+ eventListeners[Event.ROAP_FAILURE](fakeError);
3671
4096
 
3672
4097
  checkMetricSent(eventType.LOCAL_SDP_GENERATED);
3673
4098
  // expectedMetadataType is the error name in this case
3674
4099
  checkBehavioralMetricSent(
3675
4100
  BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE,
3676
- MC.Errors.ErrorCode.SdpError,
4101
+ Errors.ErrorCode.SdpError,
3677
4102
  fakeErrorMessage,
3678
4103
  fakeErrorName
3679
4104
  );
@@ -3681,24 +4106,24 @@ describe('plugin-meetings', () => {
3681
4106
 
3682
4107
  it('should send metrics for IceGatheringError error', () => {
3683
4108
  // IceGatheringError is usually without a cause
3684
- const fakeError = new MC.Errors.IceGatheringError(fakeErrorMessage, {
4109
+ const fakeError = new Errors.IceGatheringError(fakeErrorMessage, {
3685
4110
  name: fakeErrorName,
3686
4111
  });
3687
4112
 
3688
- eventListeners[MC.Event.ROAP_FAILURE](fakeError);
4113
+ eventListeners[Event.ROAP_FAILURE](fakeError);
3689
4114
 
3690
4115
  checkMetricSent(eventType.LOCAL_SDP_GENERATED);
3691
4116
  // expectedMetadataType is the error name in this case
3692
4117
  checkBehavioralMetricSent(
3693
4118
  BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE,
3694
- MC.Errors.ErrorCode.IceGatheringError,
4119
+ Errors.ErrorCode.IceGatheringError,
3695
4120
  fakeErrorMessage,
3696
4121
  fakeErrorName
3697
4122
  );
3698
4123
  });
3699
4124
  });
3700
4125
 
3701
- describe('handles MC.Event.ROAP_MESSAGE_TO_SEND correctly', () => {
4126
+ describe('handles Event.ROAP_MESSAGE_TO_SEND correctly', () => {
3702
4127
  let sendRoapOKStub;
3703
4128
  let sendRoapMediaRequestStub;
3704
4129
  let sendRoapAnswerStub;
@@ -3716,7 +4141,7 @@ describe('plugin-meetings', () => {
3716
4141
  });
3717
4142
 
3718
4143
  it('handles OK message correctly', () => {
3719
- eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({
4144
+ eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
3720
4145
  roapMessage: {messageType: 'OK', seq: 1},
3721
4146
  });
3722
4147
 
@@ -3735,7 +4160,7 @@ describe('plugin-meetings', () => {
3735
4160
  });
3736
4161
 
3737
4162
  it('handles OFFER message correctly', () => {
3738
- eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({
4163
+ eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
3739
4164
  roapMessage: {
3740
4165
  messageType: 'OFFER',
3741
4166
  seq: 1,
@@ -3761,7 +4186,7 @@ describe('plugin-meetings', () => {
3761
4186
  });
3762
4187
 
3763
4188
  it('handles ANSWER message correctly', () => {
3764
- eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({
4189
+ eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
3765
4190
  roapMessage: {
3766
4191
  messageType: 'ANSWER',
3767
4192
  seq: 10,
@@ -3788,7 +4213,7 @@ describe('plugin-meetings', () => {
3788
4213
  it('sends metrics if fails to send roap ANSWER message', async () => {
3789
4214
  sendRoapAnswerStub.rejects(new Error('sending answer failed'));
3790
4215
 
3791
- await eventListeners[MC.Event.ROAP_MESSAGE_TO_SEND]({
4216
+ await eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
3792
4217
  roapMessage: {
3793
4218
  messageType: 'ANSWER',
3794
4219
  seq: 10,
@@ -3798,100 +4223,496 @@ describe('plugin-meetings', () => {
3798
4223
  });
3799
4224
  await testUtils.flushPromises();
3800
4225
 
3801
- assert.calledOnce(Metrics.sendBehavioralMetric);
3802
- assert.calledWithMatch(
3803
- Metrics.sendBehavioralMetric,
3804
- BEHAVIORAL_METRICS.ROAP_ANSWER_FAILURE,
3805
- {
3806
- correlation_id: meeting.correlationId,
3807
- locus_id: meeting.locusUrl.split('/').pop(),
3808
- reason: 'sending answer failed',
3809
- }
3810
- );
3811
- });
4226
+ assert.calledOnce(Metrics.sendBehavioralMetric);
4227
+ assert.calledWithMatch(
4228
+ Metrics.sendBehavioralMetric,
4229
+ BEHAVIORAL_METRICS.ROAP_ANSWER_FAILURE,
4230
+ {
4231
+ correlation_id: meeting.correlationId,
4232
+ locus_id: meeting.locusUrl.split('/').pop(),
4233
+ reason: 'sending answer failed',
4234
+ }
4235
+ );
4236
+ });
4237
+
4238
+ [ErrorType.CONFLICT, ErrorType.DOUBLECONFLICT].forEach((errorType) =>
4239
+ it(`handles ERROR message indicating glare condition correctly (errorType=${errorType})`, () => {
4240
+ eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
4241
+ roapMessage: {
4242
+ messageType: 'ERROR',
4243
+ seq: 10,
4244
+ errorType,
4245
+ tieBreaker: 12345,
4246
+ },
4247
+ });
4248
+
4249
+ assert.calledOnce(Metrics.sendBehavioralMetric);
4250
+ assert.calledWithMatch(
4251
+ Metrics.sendBehavioralMetric,
4252
+ BEHAVIORAL_METRICS.ROAP_GLARE_CONDITION,
4253
+ {
4254
+ correlation_id: meeting.correlationId,
4255
+ locus_id: meeting.locusUrl.split('/').pop(),
4256
+ sequence: 10,
4257
+ }
4258
+ );
4259
+
4260
+ assert.calledOnce(sendRoapErrorStub);
4261
+ assert.calledWith(sendRoapErrorStub, {
4262
+ seq: 10,
4263
+ errorType,
4264
+ mediaId: meeting.mediaId,
4265
+ correlationId: meeting.correlationId,
4266
+ });
4267
+ })
4268
+ );
4269
+
4270
+ it('handles ERROR message indicating other errors correctly', () => {
4271
+ eventListeners[Event.ROAP_MESSAGE_TO_SEND]({
4272
+ roapMessage: {
4273
+ messageType: 'ERROR',
4274
+ seq: 10,
4275
+ errorType: ErrorType.FAILED,
4276
+ tieBreaker: 12345,
4277
+ },
4278
+ });
4279
+
4280
+ assert.notCalled(Metrics.sendBehavioralMetric);
4281
+
4282
+ assert.calledOnce(sendRoapErrorStub);
4283
+ assert.calledWith(sendRoapErrorStub, {
4284
+ seq: 10,
4285
+ errorType: ErrorType.FAILED,
4286
+ mediaId: meeting.mediaId,
4287
+ correlationId: meeting.correlationId,
4288
+ });
4289
+ });
4290
+ });
4291
+
4292
+ describe('audio and video source count change events', () => {
4293
+ beforeEach(() => {
4294
+ TriggerProxy.trigger.resetHistory();
4295
+ meeting.setupMediaConnectionListeners();
4296
+ });
4297
+
4298
+ it('registers for audio and video source count changed', () => {
4299
+ assert.isFunction(eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED]);
4300
+ assert.isFunction(eventListeners[Event.AUDIO_SOURCES_COUNT_CHANGED]);
4301
+ })
4302
+
4303
+ it('forwards the VIDEO_SOURCES_COUNT_CHANGED event as "media:remoteVideoSourceCountChanged"', () => {
4304
+ const numTotalSources = 10;
4305
+ const numLiveSources = 6;
4306
+ const mediaContent = 'SLIDES';
4307
+
4308
+ sinon.stub(meeting.mediaRequestManagers.video, 'setNumCurrentSources');
4309
+
4310
+ eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED](numTotalSources, numLiveSources, mediaContent);
4311
+
4312
+ assert.calledOnceWithExactly(TriggerProxy.trigger,
4313
+ meeting,
4314
+ sinon.match.object,
4315
+ 'media:remoteVideoSourceCountChanged',
4316
+ {
4317
+ numTotalSources,
4318
+ numLiveSources,
4319
+ mediaContent,
4320
+ }
4321
+ );
4322
+ });
4323
+
4324
+ it('forwards the AUDIO_SOURCES_COUNT_CHANGED event as "media:remoteAudioSourceCountChanged"', () => {
4325
+ const numTotalSources = 5;
4326
+ const numLiveSources = 2;
4327
+ const mediaContent = 'MAIN';
4328
+
4329
+ eventListeners[Event.AUDIO_SOURCES_COUNT_CHANGED](numTotalSources, numLiveSources, mediaContent);
4330
+
4331
+ assert.calledOnceWithExactly(TriggerProxy.trigger,
4332
+ meeting,
4333
+ sinon.match.object,
4334
+ 'media:remoteAudioSourceCountChanged',
4335
+ {
4336
+ numTotalSources,
4337
+ numLiveSources,
4338
+ mediaContent,
4339
+ }
4340
+ );
4341
+ });
4342
+
4343
+ it('calls setNumCurrentSources() when receives VIDEO_SOURCES_COUNT_CHANGED event for MAIN', () => {
4344
+ const numTotalSources = 20;
4345
+ const numLiveSources = 10;
4346
+
4347
+ const setNumCurrentSourcesSpy = sinon.stub(meeting.mediaRequestManagers.video, 'setNumCurrentSources');
4348
+
4349
+ eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED](numTotalSources, numLiveSources, 'MAIN');
4350
+
4351
+ assert.calledOnceWithExactly(setNumCurrentSourcesSpy, numTotalSources, numLiveSources);
4352
+ });
4353
+
4354
+ it('does not call setNumCurrentSources() when receives VIDEO_SOURCES_COUNT_CHANGED event for SLIDES', () => {
4355
+ const numTotalSources = 20;
4356
+ const numLiveSources = 10;
4357
+
4358
+ const setNumCurrentSourcesSpy = sinon.stub(meeting.mediaRequestManagers.video, 'setNumCurrentSources');
4359
+
4360
+ eventListeners[Event.VIDEO_SOURCES_COUNT_CHANGED](numTotalSources, numLiveSources, 'SLIDES');
4361
+
4362
+ assert.notCalled(setNumCurrentSourcesSpy);
4363
+ });
4364
+
4365
+ })
4366
+ });
4367
+ describe('#setUpLocusInfoSelfListener', () => {
4368
+ it('listens to the self unadmitted guest event', (done) => {
4369
+ meeting.startKeepAlive = sinon.stub();
4370
+ meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_UNADMITTED_GUEST', test1);
4371
+ assert.calledOnceWithExactly(meeting.startKeepAlive);
4372
+ assert.calledTwice(TriggerProxy.trigger);
4373
+ assert.calledWith(
4374
+ TriggerProxy.trigger,
4375
+ sinon.match.instanceOf(Meeting),
4376
+ {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
4377
+ 'meeting:self:lobbyWaiting',
4378
+ {payload: test1}
4379
+ );
4380
+ done();
4381
+ });
4382
+ it('listens to the self admitted guest event', (done) => {
4383
+ meeting.stopKeepAlive = sinon.stub();
4384
+ meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_ADMITTED_GUEST', test1);
4385
+ assert.calledOnceWithExactly(meeting.stopKeepAlive);
4386
+ assert.calledTwice(TriggerProxy.trigger);
4387
+ assert.calledWith(
4388
+ TriggerProxy.trigger,
4389
+ sinon.match.instanceOf(Meeting),
4390
+ {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
4391
+ 'meeting:self:guestAdmitted',
4392
+ {payload: test1}
4393
+ );
4394
+ done();
4395
+ });
4396
+
4397
+ it('listens to the breakouts changed event', () => {
4398
+ meeting.breakouts.updateBreakoutSessions = sinon.stub();
4399
+
4400
+ const payload = 'payload';
4401
+
4402
+ meeting.locusInfo.emit(
4403
+ {function: 'test', file: 'test'},
4404
+ 'SELF_MEETING_BREAKOUTS_CHANGED',
4405
+ payload
4406
+ );
4407
+
4408
+ assert.calledOnceWithExactly(meeting.breakouts.updateBreakoutSessions, payload);
4409
+ assert.calledWith(
4410
+ TriggerProxy.trigger,
4411
+ meeting,
4412
+ {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
4413
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_UPDATE
4414
+ );
4415
+ });
4416
+
4417
+ it('listens to the self roles changed event', () => {
4418
+ const payload = {oldRoles: [], newRoles: ['COHOST']};
4419
+ meeting.breakouts.updateCanManageBreakouts = sinon.stub();
4420
+
4421
+ meeting.locusInfo.emit(
4422
+ {function: 'test', file: 'test'},
4423
+ 'SELF_ROLES_CHANGED',
4424
+ payload
4425
+ );
4426
+
4427
+ assert.calledOnceWithExactly(meeting.breakouts.updateCanManageBreakouts, true);
4428
+ assert.calledWith(
4429
+ TriggerProxy.trigger,
4430
+ meeting,
4431
+ {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
4432
+ EVENT_TRIGGERS.MEETING_SELF_ROLES_CHANGED,
4433
+ {payload}
4434
+ );
4435
+ });
4436
+ });
4437
+
4438
+ describe('#setUpBreakoutsListener', () => {
4439
+ it('listens to the closing event from breakouts and triggers the closing event', () => {
4440
+ TriggerProxy.trigger.reset();
4441
+ meeting.breakouts.trigger('BREAKOUTS_CLOSING');
4442
+
4443
+ assert.calledWith(
4444
+ TriggerProxy.trigger,
4445
+ meeting,
4446
+ {file: 'meeting/index', function: 'setUpBreakoutsListener'},
4447
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_CLOSING
4448
+ );
4449
+ });
4450
+
4451
+ it('listens to the message event from breakouts and triggers the message event', () => {
4452
+ TriggerProxy.trigger.reset();
4453
+
4454
+ const messageEvent = 'message';
4455
+
4456
+ meeting.breakouts.trigger('MESSAGE', messageEvent);
4457
+
4458
+ assert.calledWith(
4459
+ TriggerProxy.trigger,
4460
+ meeting,
4461
+ {file: 'meeting/index', function: 'setUpBreakoutsListener'},
4462
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_MESSAGE,
4463
+ messageEvent
4464
+ );
4465
+ });
4466
+
4467
+ it('listens to the members update event from breakouts and triggers the breakouts update event', () => {
4468
+ TriggerProxy.trigger.reset();
4469
+ meeting.breakouts.trigger('MEMBERS_UPDATE');
4470
+
4471
+ assert.calledWith(
4472
+ TriggerProxy.trigger,
4473
+ meeting,
4474
+ {file: 'meeting/index', function: 'setUpBreakoutsListener'},
4475
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_UPDATE
4476
+ );
4477
+ });
4478
+
4479
+ it('should not trigger ASK_RETURN_TO_MAIN before joined', () => {
4480
+ TriggerProxy.trigger.reset();
4481
+ meeting.joinedWith = {state: "NOT_JOINED"};
4482
+ meeting.breakouts.trigger('ASK_RETURN_TO_MAIN');
4483
+ assert.notCalled(TriggerProxy.trigger);
4484
+ });
4485
+
4486
+ it('listens to the ask return to main event from breakouts and triggers the ask return to main event from meeting', () => {
4487
+ TriggerProxy.trigger.reset();
4488
+ meeting.joinedWith = {state: "JOINED"};
4489
+ meeting.breakouts.trigger('ASK_RETURN_TO_MAIN');
4490
+ assert.calledWith(
4491
+ TriggerProxy.trigger,
4492
+ meeting,
4493
+ {file: 'meeting/index', function: 'setUpBreakoutsListener'},
4494
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_ASK_RETURN_TO_MAIN
4495
+ );
4496
+ });
4497
+
4498
+ it('listens to the leave event from breakouts and triggers the breakout leave event', () => {
4499
+ TriggerProxy.trigger.reset();
4500
+ meeting.breakouts.trigger('LEAVE_BREAKOUT');
4501
+ assert.calledWith(
4502
+ TriggerProxy.trigger,
4503
+ meeting,
4504
+ {file: 'meeting/index', function: 'setUpBreakoutsListener'},
4505
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_LEAVE
4506
+ );
4507
+ });
4508
+
4509
+ it('listens to the breakout ask for help event and triggers the ask for help event', () => {
4510
+ TriggerProxy.trigger.reset();
4511
+ const helpEvent = {sessionId:'sessionId', participant: 'participant'}
4512
+ meeting.breakouts.trigger('ASK_FOR_HELP', helpEvent);
4513
+ assert.calledWith(
4514
+ TriggerProxy.trigger,
4515
+ meeting,
4516
+ {file: 'meeting/index', function: 'setUpBreakoutsListener'},
4517
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_ASK_FOR_HELP,
4518
+ helpEvent
4519
+ );
4520
+ });
4521
+
4522
+ it('listens to the preAssignments update event from breakouts and triggers the update event', () => {
4523
+ TriggerProxy.trigger.reset();
4524
+ meeting.breakouts.trigger('PRE_ASSIGNMENTS_UPDATE');
4525
+
4526
+ assert.calledWith(
4527
+ TriggerProxy.trigger,
4528
+ meeting,
4529
+ {file: 'meeting/index', function: 'setUpBreakoutsListener'},
4530
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_PRE_ASSIGNMENTS_UPDATE
4531
+ );
4532
+ });
4533
+ });
4534
+
4535
+ describe('#setupLocusControlsListener', () => {
4536
+ it('listens to the locus breakouts update event', () => {
4537
+ const locus = {
4538
+ breakout: 'breakout',
4539
+ };
4540
+
4541
+ meeting.breakouts.updateBreakout = sinon.stub();
4542
+ meeting.locusInfo.emit(
4543
+ {function: 'test', file: 'test'},
4544
+ 'CONTROLS_MEETING_BREAKOUT_UPDATED',
4545
+ locus
4546
+ );
4547
+
4548
+ assert.calledOnceWithExactly(meeting.breakouts.updateBreakout, locus.breakout);
4549
+ assert.calledWith(
4550
+ TriggerProxy.trigger,
4551
+ meeting,
4552
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4553
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_UPDATE
4554
+ );
4555
+ });
4556
+
4557
+ it('listens to CONTROLS_MUTE_ON_ENTRY_CHANGED', async () => {
4558
+ const state = {example: 'value'}
4559
+
4560
+ await meeting.locusInfo.emitScoped(
4561
+ {function: 'test', file: 'test'},
4562
+ LOCUSINFO.EVENTS.CONTROLS_MUTE_ON_ENTRY_CHANGED,
4563
+ {state}
4564
+ );
4565
+
4566
+ assert.calledWith(
4567
+ TriggerProxy.trigger,
4568
+ meeting,
4569
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4570
+ EVENT_TRIGGERS.MEETING_CONTROLS_MUTE_ON_ENTRY_UPDATED,
4571
+ {state},
4572
+ );
4573
+ });
4574
+
4575
+ it('listens to MEETING_CONTROLS_SHARE_CONTROL_UPDATED', async () => {
4576
+ const state = {example: 'value'}
4577
+
4578
+ await meeting.locusInfo.emitScoped(
4579
+ {function: 'test', file: 'test'},
4580
+ LOCUSINFO.EVENTS.CONTROLS_SHARE_CONTROL_CHANGED,
4581
+ {state}
4582
+ );
4583
+
4584
+ assert.calledWith(
4585
+ TriggerProxy.trigger,
4586
+ meeting,
4587
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4588
+ EVENT_TRIGGERS.MEETING_CONTROLS_SHARE_CONTROL_UPDATED,
4589
+ {state},
4590
+ );
4591
+ });
4592
+
4593
+ it('listens to MEETING_CONTROLS_DISALLOW_UNMUTE_UPDATED', async () => {
4594
+ const state = {example: 'value'}
4595
+
4596
+ await meeting.locusInfo.emitScoped(
4597
+ {function: 'test', file: 'test'},
4598
+ LOCUSINFO.EVENTS.CONTROLS_DISALLOW_UNMUTE_CHANGED,
4599
+ {state}
4600
+ );
4601
+
4602
+ assert.calledWith(
4603
+ TriggerProxy.trigger,
4604
+ meeting,
4605
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4606
+ EVENT_TRIGGERS.MEETING_CONTROLS_DISALLOW_UNMUTE_UPDATED,
4607
+ {state},
4608
+ );
4609
+ });
3812
4610
 
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
- });
4611
+ it('listens to MEETING_CONTROLS_REACTIONS_UPDATED', async () => {
4612
+ const state = {example: 'value'}
3823
4613
 
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
- );
4614
+ await meeting.locusInfo.emitScoped(
4615
+ {function: 'test', file: 'test'},
4616
+ LOCUSINFO.EVENTS.CONTROLS_REACTIONS_CHANGED,
4617
+ {state}
4618
+ );
3834
4619
 
3835
- assert.calledOnce(sendRoapErrorStub);
3836
- assert.calledWith(sendRoapErrorStub, {
3837
- seq: 10,
3838
- errorType,
3839
- mediaId: meeting.mediaId,
3840
- correlationId: meeting.correlationId,
3841
- });
3842
- })
4620
+ assert.calledWith(
4621
+ TriggerProxy.trigger,
4622
+ meeting,
4623
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4624
+ EVENT_TRIGGERS.MEETING_CONTROLS_REACTIONS_UPDATED,
4625
+ {state},
3843
4626
  );
4627
+ });
3844
4628
 
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
- });
4629
+ it('listens to MEETING_CONTROLS_VIEW_THE_PARTICIPANTS_LIST_UPDATED', async () => {
4630
+ const state = {example: 'value'}
3854
4631
 
3855
- assert.notCalled(Metrics.sendBehavioralMetric);
4632
+ await meeting.locusInfo.emitScoped(
4633
+ {function: 'test', file: 'test'},
4634
+ LOCUSINFO.EVENTS.CONTROLS_VIEW_THE_PARTICIPANTS_LIST_CHANGED,
4635
+ {state}
4636
+ );
3856
4637
 
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
- });
4638
+ assert.calledWith(
4639
+ TriggerProxy.trigger,
4640
+ meeting,
4641
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4642
+ EVENT_TRIGGERS.MEETING_CONTROLS_VIEW_THE_PARTICIPANTS_LIST_UPDATED,
4643
+ {state},
4644
+ );
3865
4645
  });
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);
4646
+
4647
+ it('listens to MEETING_CONTROLS_RAISE_HAND_UPDATED', async () => {
4648
+ const state = {example: 'value'}
4649
+
4650
+ await meeting.locusInfo.emitScoped(
4651
+ {function: 'test', file: 'test'},
4652
+ LOCUSINFO.EVENTS.CONTROLS_RAISE_HAND_CHANGED,
4653
+ {state}
4654
+ );
4655
+
3873
4656
  assert.calledWith(
3874
4657
  TriggerProxy.trigger,
3875
- sinon.match.instanceOf(Meeting),
3876
- {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
3877
- 'meeting:self:lobbyWaiting',
3878
- {payload: test1}
4658
+ meeting,
4659
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4660
+ EVENT_TRIGGERS.MEETING_CONTROLS_RAISE_HAND_UPDATED,
4661
+ {state},
3879
4662
  );
3880
- done();
3881
4663
  });
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);
4664
+
4665
+ it('listens to MEETING_CONTROLS_VIDEO_UPDATED', async () => {
4666
+ const state = {example: 'value'}
4667
+
4668
+ await meeting.locusInfo.emitScoped(
4669
+ {function: 'test', file: 'test'},
4670
+ LOCUSINFO.EVENTS.CONTROLS_VIDEO_CHANGED,
4671
+ {state}
4672
+ );
4673
+
3887
4674
  assert.calledWith(
3888
4675
  TriggerProxy.trigger,
3889
- sinon.match.instanceOf(Meeting),
3890
- {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
3891
- 'meeting:self:guestAdmitted',
3892
- {payload: test1}
4676
+ meeting,
4677
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
4678
+ EVENT_TRIGGERS.MEETING_CONTROLS_VIDEO_UPDATED,
4679
+ {state},
3893
4680
  );
3894
- done();
4681
+ });
4682
+
4683
+ it('listens to the timing that user joined into breakout', async () => {
4684
+ const mainLocusUrl = 'mainLocusUrl123';
4685
+
4686
+ meeting.meetingRequest.getLocusStatusByUrl = sinon.stub().returns(Promise.resolve());
4687
+
4688
+ await meeting.locusInfo.emit(
4689
+ {function: 'test', file: 'test'},
4690
+ 'CONTROLS_JOIN_BREAKOUT_FROM_MAIN',
4691
+ {mainLocusUrl}
4692
+ );
4693
+
4694
+ assert.calledOnceWithExactly(meeting.meetingRequest.getLocusStatusByUrl, mainLocusUrl);
4695
+ const error = {statusCode: 403};
4696
+ meeting.meetingRequest.getLocusStatusByUrl.rejects(error);
4697
+ meeting.locusInfo.clearMainSessionLocusCache = sinon.stub();
4698
+ await meeting.locusInfo.emit(
4699
+ {function: 'test', file: 'test'},
4700
+ 'CONTROLS_JOIN_BREAKOUT_FROM_MAIN',
4701
+ {mainLocusUrl}
4702
+ );
4703
+
4704
+ assert.calledOnce(meeting.locusInfo.clearMainSessionLocusCache);
4705
+
4706
+ const otherError = new Error('something wrong');
4707
+ meeting.meetingRequest.getLocusStatusByUrl.rejects(otherError);
4708
+ meeting.locusInfo.clearMainSessionLocusCache = sinon.stub();
4709
+ await meeting.locusInfo.emit(
4710
+ {function: 'test', file: 'test'},
4711
+ 'CONTROLS_JOIN_BREAKOUT_FROM_MAIN',
4712
+ {mainLocusUrl}
4713
+ );
4714
+
4715
+ assert.notCalled(meeting.locusInfo.clearMainSessionLocusCache);
3895
4716
  });
3896
4717
  });
3897
4718
 
@@ -3900,6 +4721,11 @@ describe('plugin-meetings', () => {
3900
4721
  const newLocusUrl = 'newLocusUrl/12345';
3901
4722
 
3902
4723
  meeting.members = {locusUrlUpdate: sinon.stub().returns(Promise.resolve(test1))};
4724
+ meeting.recordingController = {setLocusUrl: sinon.stub().returns(undefined)};
4725
+ meeting.controlsOptionsManager = {setLocusUrl: sinon.stub().returns(undefined)};
4726
+
4727
+ meeting.breakouts.locusUrlUpdate = sinon.stub();
4728
+ meeting.annotation.locusUrlUpdate = sinon.stub();
3903
4729
 
3904
4730
  meeting.locusInfo.emit(
3905
4731
  {function: 'test', file: 'test'},
@@ -3907,11 +4733,57 @@ describe('plugin-meetings', () => {
3907
4733
  newLocusUrl
3908
4734
  );
3909
4735
  assert.calledWith(meeting.members.locusUrlUpdate, newLocusUrl);
4736
+ assert.calledOnceWithExactly(meeting.breakouts.locusUrlUpdate, newLocusUrl);
4737
+ assert.calledOnceWithExactly(meeting.annotation.locusUrlUpdate, newLocusUrl);
4738
+ assert.calledWith(meeting.members.locusUrlUpdate, newLocusUrl);
4739
+ assert.calledWith(meeting.recordingController.setLocusUrl, newLocusUrl);
4740
+ assert.calledWith(meeting.controlsOptionsManager.setLocusUrl, newLocusUrl);
3910
4741
  assert.equal(meeting.locusUrl, newLocusUrl);
3911
4742
  assert(meeting.locusId, '12345');
3912
4743
  done();
3913
4744
  });
3914
4745
  });
4746
+
4747
+ describe('#setUpLocusServicesListener', () => {
4748
+ it('listens to the locus services update event', (done) => {
4749
+ const newLocusServices = {
4750
+ services: {
4751
+ record: {
4752
+ url: 'url',
4753
+ },
4754
+ approval: {
4755
+ url: 'url',
4756
+ },
4757
+ },
4758
+ };
4759
+
4760
+ meeting.recordingController = {
4761
+ setServiceUrl: sinon.stub().returns(undefined),
4762
+ setSessionId: sinon.stub().returns(undefined),
4763
+ };
4764
+ meeting.annotation = {
4765
+ approvalUrlUpdate: sinon.stub().returns(undefined),
4766
+ };
4767
+
4768
+ meeting.locusInfo.emit(
4769
+ {function: 'test', file: 'test'},
4770
+ 'LINKS_SERVICES',
4771
+ newLocusServices
4772
+ );
4773
+
4774
+ assert.calledWith(
4775
+ meeting.recordingController.setServiceUrl,
4776
+ newLocusServices.services.record.url,
4777
+ );
4778
+ assert.calledWith(
4779
+ meeting.annotation.approvalUrlUpdate,
4780
+ newLocusServices.services.approval.url,
4781
+ );
4782
+ assert.calledOnce(meeting.recordingController.setSessionId);
4783
+ done();
4784
+ });
4785
+ });
4786
+
3915
4787
  describe('#setUpLocusInfoMediaInactiveListener', () => {
3916
4788
  it('listens to disconnect due to un activity ', (done) => {
3917
4789
  TriggerProxy.trigger.reset();
@@ -4057,20 +4929,6 @@ describe('plugin-meetings', () => {
4057
4929
  assert.calledOnce(meeting.mediaProperties.unsetRemoteTracks);
4058
4930
  });
4059
4931
  });
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
4932
  // TODO: remove
4075
4933
  describe('#setMercuryListener', () => {
4076
4934
  it('should listen to mercury events', () => {
@@ -4237,28 +5095,7 @@ describe('plugin-meetings', () => {
4237
5095
  checkParseMeetingInfo(expectedInfoToParse);
4238
5096
  });
4239
5097
  });
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
- });
5098
+
4262
5099
  describe('#setCorrelationId', () => {
4263
5100
  it('should set the correlationId and return undefined', () => {
4264
5101
  assert.ok(meeting.correlationId);
@@ -4319,25 +5156,37 @@ describe('plugin-meetings', () => {
4319
5156
  let inMeetingActionsSetSpy;
4320
5157
  let canUserLockSpy;
4321
5158
  let canUserUnlockSpy;
4322
- let canUserRecordSpy;
5159
+ let canUserStartSpy;
4323
5160
  let canUserStopSpy;
4324
5161
  let canUserPauseSpy;
4325
5162
  let canUserResumeSpy;
5163
+ let canSetMuteOnEntrySpy;
5164
+ let canUnsetMuteOnEntrySpy;
5165
+ let canSetDisallowUnmuteSpy;
5166
+ let canUnsetDisallowUnmuteSpy;
4326
5167
  let canUserRaiseHandSpy;
4327
5168
  let bothLeaveAndEndMeetingAvailableSpy;
4328
5169
  let canUserLowerAllHandsSpy;
4329
5170
  let canUserLowerSomeoneElsesHandSpy;
4330
5171
  let waitingForOthersToJoinSpy;
4331
5172
  let handleDataChannelUrlChangeSpy;
5173
+ let canSendReactionsSpy;
5174
+ let canUserRenameSelfAndObservedSpy;
5175
+ let canUserRenameOthersSpy;
5176
+ let hasHintsSpy;
4332
5177
 
4333
5178
  beforeEach(() => {
4334
5179
  locusInfoOnSpy = sinon.spy(meeting.locusInfo, 'on');
4335
5180
  canUserLockSpy = sinon.spy(MeetingUtil, 'canUserLock');
4336
5181
  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');
5182
+ canUserStartSpy = sinon.spy(RecordingUtil, 'canUserStart');
5183
+ canUserStopSpy = sinon.spy(RecordingUtil, 'canUserStop');
5184
+ canUserPauseSpy = sinon.spy(RecordingUtil, 'canUserPause');
5185
+ canUserResumeSpy = sinon.spy(RecordingUtil, 'canUserResume');
5186
+ canSetMuteOnEntrySpy = sinon.spy(ControlsOptionsUtil, 'canSetMuteOnEntry');
5187
+ canUnsetMuteOnEntrySpy = sinon.spy(ControlsOptionsUtil, 'canUnsetMuteOnEntry');
5188
+ canSetDisallowUnmuteSpy = sinon.spy(ControlsOptionsUtil, 'canSetDisallowUnmute');
5189
+ canUnsetDisallowUnmuteSpy = sinon.spy(ControlsOptionsUtil, 'canUnsetDisallowUnmute');
4341
5190
  inMeetingActionsSetSpy = sinon.spy(meeting.inMeetingActions, 'set');
4342
5191
  canUserRaiseHandSpy = sinon.spy(MeetingUtil, 'canUserRaiseHand');
4343
5192
  canUserLowerAllHandsSpy = sinon.spy(MeetingUtil, 'canUserLowerAllHands');
@@ -4348,6 +5197,9 @@ describe('plugin-meetings', () => {
4348
5197
  canUserLowerSomeoneElsesHandSpy = sinon.spy(MeetingUtil, 'canUserLowerSomeoneElsesHand');
4349
5198
  waitingForOthersToJoinSpy = sinon.spy(MeetingUtil, 'waitingForOthersToJoin');
4350
5199
  handleDataChannelUrlChangeSpy = sinon.spy(meeting, 'handleDataChannelUrlChange');
5200
+ canSendReactionsSpy = sinon.spy(MeetingUtil, 'canSendReactions');
5201
+ canUserRenameSelfAndObservedSpy = sinon.spy(MeetingUtil, 'canUserRenameSelfAndObserved');
5202
+ canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
4351
5203
  });
4352
5204
 
4353
5205
  afterEach(() => {
@@ -4357,6 +5209,10 @@ describe('plugin-meetings', () => {
4357
5209
  });
4358
5210
 
4359
5211
  it('registers the correct MEETING_INFO_UPDATED event', () => {
5212
+ // Due to import tree issues, hasHints must be stubed within the scope of the `it`.
5213
+ const restorableHasHints = ControlsOptionsUtil.hasHints;
5214
+ ControlsOptionsUtil.hasHints = sinon.stub().returns(true);
5215
+
4360
5216
  meeting.setUpLocusInfoMeetingInfoListener();
4361
5217
 
4362
5218
  assert.calledThrice(locusInfoOnSpy);
@@ -4377,16 +5233,96 @@ describe('plugin-meetings', () => {
4377
5233
 
4378
5234
  assert.calledWith(canUserLockSpy, payload.info.userDisplayHints);
4379
5235
  assert.calledWith(canUserUnlockSpy, payload.info.userDisplayHints);
4380
- assert.calledWith(canUserRecordSpy, payload.info.userDisplayHints);
5236
+ assert.calledWith(canUserStartSpy, payload.info.userDisplayHints);
4381
5237
  assert.calledWith(canUserStopSpy, payload.info.userDisplayHints);
4382
5238
  assert.calledWith(canUserPauseSpy, payload.info.userDisplayHints);
4383
5239
  assert.calledWith(canUserResumeSpy, payload.info.userDisplayHints);
5240
+ assert.calledWith(canSetMuteOnEntrySpy, payload.info.userDisplayHints);
5241
+ assert.calledWith(canUnsetMuteOnEntrySpy, payload.info.userDisplayHints);
5242
+ assert.calledWith(canSetDisallowUnmuteSpy, payload.info.userDisplayHints);
5243
+ assert.calledWith(canUnsetDisallowUnmuteSpy, payload.info.userDisplayHints);
4384
5244
  assert.calledWith(canUserRaiseHandSpy, payload.info.userDisplayHints);
4385
5245
  assert.calledWith(bothLeaveAndEndMeetingAvailableSpy, payload.info.userDisplayHints);
4386
5246
  assert.calledWith(canUserLowerAllHandsSpy, payload.info.userDisplayHints);
4387
5247
  assert.calledWith(canUserLowerSomeoneElsesHandSpy, payload.info.userDisplayHints);
4388
5248
  assert.calledWith(waitingForOthersToJoinSpy, payload.info.userDisplayHints);
4389
5249
  assert.calledWith(handleDataChannelUrlChangeSpy, payload.info.datachannelUrl);
5250
+ assert.calledWith(canSendReactionsSpy, null, payload.info.userDisplayHints);
5251
+ assert.calledWith(canUserRenameSelfAndObservedSpy, payload.info.userDisplayHints);
5252
+ assert.calledWith(canUserRenameOthersSpy, payload.info.userDisplayHints);
5253
+
5254
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5255
+ requiredHints: [DISPLAY_HINTS.MUTE_ALL],
5256
+ displayHints: payload.info.userDisplayHints,
5257
+ });
5258
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5259
+ requiredHints: [DISPLAY_HINTS.UNMUTE_ALL],
5260
+ displayHints: payload.info.userDisplayHints,
5261
+ });
5262
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5263
+ requiredHints: [DISPLAY_HINTS.ENABLE_HARD_MUTE],
5264
+ displayHints: payload.info.userDisplayHints,
5265
+ });
5266
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5267
+ requiredHints: [DISPLAY_HINTS.DISABLE_HARD_MUTE],
5268
+ displayHints: payload.info.userDisplayHints,
5269
+ });
5270
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5271
+ requiredHints: [DISPLAY_HINTS.ENABLE_MUTE_ON_ENTRY],
5272
+ displayHints: payload.info.userDisplayHints,
5273
+ });
5274
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5275
+ requiredHints: [DISPLAY_HINTS.DISABLE_MUTE_ON_ENTRY],
5276
+ displayHints: payload.info.userDisplayHints,
5277
+ });
5278
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5279
+ requiredHints: [DISPLAY_HINTS.ENABLE_REACTIONS],
5280
+ displayHints: payload.info.userDisplayHints,
5281
+ });
5282
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5283
+ requiredHints: [DISPLAY_HINTS.DISABLE_REACTIONS],
5284
+ displayHints: payload.info.userDisplayHints,
5285
+ });
5286
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5287
+ requiredHints: [DISPLAY_HINTS.ENABLE_SHOW_DISPLAY_NAME],
5288
+ displayHints: payload.info.userDisplayHints,
5289
+ });
5290
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5291
+ requiredHints: [DISPLAY_HINTS.DISABLE_SHOW_DISPLAY_NAME],
5292
+ displayHints: payload.info.userDisplayHints,
5293
+ });
5294
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5295
+ requiredHints: [DISPLAY_HINTS.SHARE_CONTROL],
5296
+ displayHints: payload.info.userDisplayHints,
5297
+ });
5298
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5299
+ requiredHints: [DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST],
5300
+ displayHints: payload.info.userDisplayHints,
5301
+ });
5302
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5303
+ requiredHints: [DISPLAY_HINTS.DISABLE_VIEW_THE_PARTICIPANT_LIST],
5304
+ displayHints: payload.info.userDisplayHints,
5305
+ });
5306
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5307
+ requiredHints: [DISPLAY_HINTS.SHARE_FILE],
5308
+ displayHints: payload.info.userDisplayHints,
5309
+ });
5310
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5311
+ requiredHints: [DISPLAY_HINTS.SHARE_APPLICATION],
5312
+ displayHints: payload.info.userDisplayHints,
5313
+ });
5314
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5315
+ requiredHints: [DISPLAY_HINTS.SHARE_CAMERA],
5316
+ displayHints: payload.info.userDisplayHints,
5317
+ });
5318
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5319
+ requiredHints: [DISPLAY_HINTS.SHARE_DESKTOP],
5320
+ displayHints: payload.info.userDisplayHints,
5321
+ });
5322
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
5323
+ requiredHints: [DISPLAY_HINTS.SHARE_CONTENT],
5324
+ displayHints: payload.info.userDisplayHints,
5325
+ });
4390
5326
 
4391
5327
  assert.calledWith(
4392
5328
  TriggerProxy.trigger,
@@ -4404,6 +5340,8 @@ describe('plugin-meetings', () => {
4404
5340
  callback(payload);
4405
5341
 
4406
5342
  assert.notCalled(TriggerProxy.trigger);
5343
+
5344
+ ControlsOptionsUtil.hasHints = restorableHasHints;
4407
5345
  });
4408
5346
  });
4409
5347
 
@@ -4451,6 +5389,9 @@ describe('plugin-meetings', () => {
4451
5389
  .stub()
4452
5390
  .returns(Promise.resolve('something'));
4453
5391
  webex.internal.llm.disconnectLLM = sinon.stub().returns(Promise.resolve());
5392
+ meeting.webex.internal.llm.on = sinon.stub();
5393
+ meeting.webex.internal.llm.off = sinon.stub();
5394
+ meeting.processRelayEvent = sinon.stub();
4454
5395
  });
4455
5396
 
4456
5397
  it('does not connect if the call is not joined yet', async () => {
@@ -4464,6 +5405,7 @@ describe('plugin-meetings', () => {
4464
5405
  assert.notCalled(webex.internal.llm.registerAndConnect);
4465
5406
  assert.notCalled(webex.internal.llm.disconnectLLM);
4466
5407
  assert.equal(result, undefined);
5408
+ assert.notCalled(meeting.webex.internal.llm.on);
4467
5409
  });
4468
5410
 
4469
5411
  it('returns undefined if llm is already connected and the locus url is unchanged', async () => {
@@ -4478,17 +5420,29 @@ describe('plugin-meetings', () => {
4478
5420
  assert.notCalled(webex.internal.llm.registerAndConnect);
4479
5421
  assert.notCalled(webex.internal.llm.disconnectLLM);
4480
5422
  assert.equal(result, undefined);
5423
+ assert.notCalled(meeting.webex.internal.llm.on);
4481
5424
  });
4482
5425
 
4483
5426
  it('connects if not already connected', async () => {
4484
5427
  meeting.joinedWith = {state: 'JOINED'};
4485
5428
  meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
4486
5429
 
5430
+
4487
5431
  const result = await meeting.updateLLMConnection();
4488
5432
 
4489
5433
  assert.notCalled(webex.internal.llm.disconnectLLM);
4490
5434
  assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a datachannel url');
4491
5435
  assert.equal(result, 'something');
5436
+ assert.calledOnceWithExactly(
5437
+ meeting.webex.internal.llm.off,
5438
+ 'event:relay.event',
5439
+ meeting.processRelayEvent
5440
+ );
5441
+ assert.calledOnceWithExactly(
5442
+ meeting.webex.internal.llm.on,
5443
+ 'event:relay.event',
5444
+ meeting.processRelayEvent
5445
+ );
4492
5446
  });
4493
5447
 
4494
5448
  it('disconnects if first if the locus url has changed', async () => {
@@ -4507,6 +5461,19 @@ describe('plugin-meetings', () => {
4507
5461
  'a datachannel url'
4508
5462
  );
4509
5463
  assert.equal(result, 'something');
5464
+ assert.calledWithExactly(
5465
+ meeting.webex.internal.llm.off,
5466
+ 'event:relay.event',
5467
+ meeting.processRelayEvent
5468
+ );
5469
+ assert.calledTwice(
5470
+ meeting.webex.internal.llm.off
5471
+ );
5472
+ assert.calledOnceWithExactly(
5473
+ meeting.webex.internal.llm.on,
5474
+ 'event:relay.event',
5475
+ meeting.processRelayEvent
5476
+ );
4510
5477
  });
4511
5478
 
4512
5479
  it('disconnects when the state is not JOINED', async () => {
@@ -4521,15 +5488,22 @@ describe('plugin-meetings', () => {
4521
5488
  assert.calledWith(webex.internal.llm.disconnectLLM);
4522
5489
  assert.notCalled(webex.internal.llm.registerAndConnect);
4523
5490
  assert.equal(result, undefined);
5491
+ assert.calledOnceWithExactly(
5492
+ meeting.webex.internal.llm.off,
5493
+ 'event:relay.event',
5494
+ meeting.processRelayEvent
5495
+ );
4524
5496
  });
5497
+
4525
5498
  });
4526
5499
 
4527
5500
  describe('#setLocus', () => {
4528
5501
  beforeEach(() => {
4529
5502
  meeting.locusInfo.initialSetup = sinon.stub().returns(true);
4530
5503
  });
5504
+
4531
5505
  it('should read the locus object, set on the meeting and return null', () => {
4532
- meeting.parseLocus({
5506
+ meeting.setLocus({
4533
5507
  mediaConnections: [test1],
4534
5508
  locusUrl: url1,
4535
5509
  locusId: uuid1,
@@ -4554,6 +5528,7 @@ describe('plugin-meetings', () => {
4554
5528
  assert.equal(meeting.hostId, uuid4);
4555
5529
  });
4556
5530
  });
5531
+
4557
5532
  describe('preferred video device', () => {
4558
5533
  describe('#getVideoDeviceId', () => {
4559
5534
  it('returns the preferred video device', () => {
@@ -4620,14 +5595,55 @@ describe('plugin-meetings', () => {
4620
5595
  });
4621
5596
  });
4622
5597
  describe('share scenarios', () => {
5598
+
5599
+ describe('triggerAnnotationInfoEvent', () => {
5600
+ it('check triggerAnnotationInfoEvent event', () => {
5601
+
5602
+ TriggerProxy.trigger.reset();
5603
+ const annotationInfo= {version: '1', policy: 'Approval'}
5604
+ const expectAnnotationInfo = {annotationInfo,meetingId:meeting.id };
5605
+ meeting.webex.meetings ={}
5606
+ meeting.triggerAnnotationInfoEvent({annotation:annotationInfo},{});
5607
+ assert.calledWith(
5608
+ TriggerProxy.trigger,
5609
+ {},
5610
+ {
5611
+ file: 'meeting/index',
5612
+ function: 'triggerAnnotationInfoEvent',
5613
+ },
5614
+ 'meeting:updateAnnotationInfo',
5615
+ expectAnnotationInfo
5616
+ );
5617
+
5618
+ TriggerProxy.trigger.reset();
5619
+ meeting.triggerAnnotationInfoEvent({annotation:annotationInfo},{annotation:annotationInfo});
5620
+ assert.notCalled(TriggerProxy.trigger);
5621
+
5622
+ TriggerProxy.trigger.reset();
5623
+ const annotationInfoUpdate = {version: '1', policy: 'AnnotationNotAllowed'}
5624
+ const expectAnnotationInfoUpdated = { annotationInfo: annotationInfoUpdate, meetingId:meeting.id };
5625
+ meeting.triggerAnnotationInfoEvent({annotation: annotationInfoUpdate},{annotation:annotationInfo});
5626
+ assert.calledWith(
5627
+ TriggerProxy.trigger,
5628
+ {},
5629
+ {
5630
+ file: 'meeting/index',
5631
+ function: 'triggerAnnotationInfoEvent',
5632
+ },
5633
+ 'meeting:updateAnnotationInfo',
5634
+ expectAnnotationInfoUpdated
5635
+ );
5636
+
5637
+ TriggerProxy.trigger.reset();
5638
+ meeting.triggerAnnotationInfoEvent(null,{annotation:annotationInfoUpdate});
5639
+ assert.notCalled(TriggerProxy.trigger);
5640
+
5641
+ });
5642
+ });
5643
+
4623
5644
  describe('setUpLocusMediaSharesListener', () => {
4624
5645
  beforeEach(() => {
4625
5646
  meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
4626
- sinon.stub(meeting, 'updateShare').returns(Promise.resolve());
4627
- });
4628
-
4629
- afterEach(() => {
4630
- meeting.updateShare.restore();
4631
5647
  });
4632
5648
 
4633
5649
  const USER_IDS = {
@@ -4643,7 +5659,7 @@ describe('plugin-meetings', () => {
4643
5659
  'https://board-a.wbx2.com/board/api/v1/channels/977a7330-54f4-11eb-b1ef-91f5eefc7bf3',
4644
5660
  };
4645
5661
 
4646
- const generateContent = (beneficiaryId = null, disposition = null) => ({
5662
+ const generateContent = (beneficiaryId = null, disposition = null,annotation = undefined) => ({
4647
5663
  beneficiaryId,
4648
5664
  disposition,
4649
5665
  });
@@ -4660,7 +5676,10 @@ describe('plugin-meetings', () => {
4660
5676
  beneficiaryId,
4661
5677
  resourceUrl,
4662
5678
  isAccepting,
4663
- otherBeneficiaryId
5679
+ otherBeneficiaryId,
5680
+ annotation,
5681
+ url,
5682
+ shareInstanceId
4664
5683
  ) => {
4665
5684
  const newPayload = cloneDeep(payload);
4666
5685
 
@@ -4686,7 +5705,7 @@ describe('plugin-meetings', () => {
4686
5705
  if (isGranting) {
4687
5706
  if (isContent) {
4688
5707
  activeSharingId.content = beneficiaryId;
4689
- newPayload.current.content = generateContent(beneficiaryId, FLOOR_ACTION.GRANTED);
5708
+ newPayload.current.content = generateContent(beneficiaryId, FLOOR_ACTION.GRANTED,annotation);
4690
5709
 
4691
5710
  if (isEqual(newPayload.current, newPayload.previous)) {
4692
5711
  eventTrigger.member = null;
@@ -4740,7 +5759,7 @@ describe('plugin-meetings', () => {
4740
5759
  eventTrigger.share.push({
4741
5760
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
4742
5761
  functionName: 'remoteShare',
4743
- eventPayload: {memberId: beneficiaryId},
5762
+ eventPayload: {memberId: beneficiaryId, url, shareInstanceId},
4744
5763
  });
4745
5764
  }
4746
5765
  }
@@ -5312,7 +6331,6 @@ describe('plugin-meetings', () => {
5312
6331
  payloadTestHelper([data1, data2, data3]);
5313
6332
  });
5314
6333
  });
5315
-
5316
6334
  describe('Desktop A --> Desktop B', () => {
5317
6335
  it('Scenario #1: you share desktop A and then share desktop B', () => {
5318
6336
  const data1 = generateData(blankPayload, true, true, USER_IDS.ME);
@@ -5529,7 +6547,7 @@ describe('plugin-meetings', () => {
5529
6547
  it('should send reaction with the right data and return a promise', async () => {
5530
6548
  meeting.locusInfo.controls = {reactions: {reactionChannelUrl: 'Fake URL'}};
5531
6549
 
5532
- const reactionPromise = meeting.sendReaction('thumbs_down', 'light');
6550
+ const reactionPromise = meeting.sendReaction('thumb_down', 'light');
5533
6551
 
5534
6552
  assert.exists(reactionPromise.then);
5535
6553
  await reactionPromise;
@@ -5553,7 +6571,7 @@ describe('plugin-meetings', () => {
5553
6571
  meeting.locusInfo.controls = {reactions: {reactionChannelUrl: undefined}};
5554
6572
 
5555
6573
  await assert.isRejected(
5556
- meeting.sendReaction('thumbs_down', 'light'),
6574
+ meeting.sendReaction('thumb_down', 'light'),
5557
6575
  Error,
5558
6576
  'Error sending reaction, service url not found.'
5559
6577
  );
@@ -5576,7 +6594,7 @@ describe('plugin-meetings', () => {
5576
6594
  it('should send a reaction with default skin tone if provided skinToneType is invalid ', async () => {
5577
6595
  meeting.locusInfo.controls = {reactions: {reactionChannelUrl: 'Fake URL'}};
5578
6596
 
5579
- const reactionPromise = meeting.sendReaction('thumbs_down', 'invalid_skin_tone');
6597
+ const reactionPromise = meeting.sendReaction('thumb_down', 'invalid_skin_tone');
5580
6598
 
5581
6599
  assert.exists(reactionPromise.then);
5582
6600
  await reactionPromise;
@@ -5595,7 +6613,7 @@ describe('plugin-meetings', () => {
5595
6613
  it('should send a reaction with default skin tone if none provided', async () => {
5596
6614
  meeting.locusInfo.controls = {reactions: {reactionChannelUrl: 'Fake URL'}};
5597
6615
 
5598
- const reactionPromise = meeting.sendReaction('thumbs_down');
6616
+ const reactionPromise = meeting.sendReaction('thumb_down');
5599
6617
 
5600
6618
  assert.exists(reactionPromise.then);
5601
6619
  await reactionPromise;
@@ -5674,6 +6692,109 @@ describe('plugin-meetings', () => {
5674
6692
  });
5675
6693
  });
5676
6694
  });
6695
+
6696
+ describe('SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED locus event', () => {
6697
+ let spy;
6698
+
6699
+ beforeEach('setup sinon', () => {
6700
+ spy = sinon.spy();
6701
+ });
6702
+
6703
+ const testEmit = async (muted) => {
6704
+ await meeting.locusInfo.emitScoped(
6705
+ {},
6706
+ LOCUSINFO.EVENTS.SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED,
6707
+ {
6708
+ muted,
6709
+ }
6710
+ );
6711
+
6712
+ assert.calledWith(
6713
+ TriggerProxy.trigger,
6714
+ sinon.match.instanceOf(Meeting),
6715
+ {
6716
+ file: 'meeting/index',
6717
+ function: 'setUpLocusInfoSelfListener',
6718
+ },
6719
+ muted
6720
+ ? EVENT_TRIGGERS.MEETING_SELF_VIDEO_MUTED_BY_OTHERS
6721
+ : EVENT_TRIGGERS.MEETING_SELF_VIDEO_UNMUTED_BY_OTHERS,
6722
+ {
6723
+ payload: {
6724
+ muted,
6725
+ },
6726
+ }
6727
+ );
6728
+ };
6729
+
6730
+ it('emits the expected event when muted', async () => {
6731
+ await testEmit(true);
6732
+ });
6733
+
6734
+ it('emits the expected event when not muted', async () => {
6735
+ await testEmit(false);
6736
+ });
6737
+ });
6738
+
6739
+ describe('getAnalyzerMetricsPrePayload', () => {
6740
+ it('should have #getAnalyzerMetricsPrePayload', () => {
6741
+ assert.exists(meeting.getAnalyzerMetricsPrePayload);
6742
+ });
6743
+
6744
+ beforeEach(() => {
6745
+ meeting.meetingRequest.getAnalyzerMetricsPrePayload = sinon
6746
+ .stub()
6747
+ .returns(Promise.resolve());
6748
+ meeting.webex.internal = {services: {get: sinon.stub().returns('Locus URL')}};
6749
+ meeting.correlationId = 'correlation-id';
6750
+ });
6751
+
6752
+ it('it should include meetingLookupUrl if provided', () => {
6753
+ const res = meeting.getAnalyzerMetricsPrePayload({
6754
+ meetingLookupUrl: 'https://service-url.com',
6755
+ event: 'client.meetinginfo.response',
6756
+ });
6757
+
6758
+ assert.deepEqual(res.event, {
6759
+ canProceed: true,
6760
+ eventData: {
6761
+ webClientDomain: '',
6762
+ },
6763
+ identifiers: {
6764
+ correlationId: 'correlation-id',
6765
+ deviceId: uuid3,
6766
+ locusUrl: 'Locus URL',
6767
+ meetingLookupUrl: 'https://service-url.com',
6768
+ orgId: undefined,
6769
+ userId: uuid1,
6770
+ },
6771
+ name: 'client.meetinginfo.response',
6772
+ });
6773
+
6774
+ assert.deepEqual(res.origin, {
6775
+ channel: undefined,
6776
+ loginType: undefined,
6777
+ userType: undefined,
6778
+ clientInfo: {
6779
+ browser: '',
6780
+ browserVersion: '',
6781
+ clientType: undefined,
6782
+ clientVersion: 'webex-js-sdk/undefined',
6783
+ localNetworkPrefix: null,
6784
+ os: 'other',
6785
+ osVersion: getOSVersion() || 'unknown',
6786
+ subClientType: undefined,
6787
+ },
6788
+ name: 'endpoint',
6789
+ networkType: 'unknown',
6790
+ userAgent: 'webex-js-sdk/test-undefined client=undefined; (os=linux/5)',
6791
+ });
6792
+
6793
+ assert.deepEqual(res.senderCountryCode, undefined);
6794
+ assert.deepEqual(res.version, 1);
6795
+
6796
+ });
6797
+ });
5677
6798
  });
5678
6799
  });
5679
6800
  });