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

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 (424) 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 +175 -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/interpretation/collection.js +23 -0
  43. package/dist/interpretation/collection.js.map +1 -0
  44. package/dist/interpretation/index.js +214 -0
  45. package/dist/interpretation/index.js.map +1 -0
  46. package/dist/interpretation/siLanguage.js +25 -0
  47. package/dist/interpretation/siLanguage.js.map +1 -0
  48. package/dist/locus-info/controlsUtils.js +92 -2
  49. package/dist/locus-info/controlsUtils.js.map +1 -1
  50. package/dist/locus-info/index.js +317 -24
  51. package/dist/locus-info/index.js.map +1 -1
  52. package/dist/locus-info/mediaSharesUtils.js +43 -1
  53. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  54. package/dist/locus-info/parser.js +2 -1
  55. package/dist/locus-info/parser.js.map +1 -1
  56. package/dist/locus-info/selfUtils.js +97 -14
  57. package/dist/locus-info/selfUtils.js.map +1 -1
  58. package/dist/media/index.js +39 -134
  59. package/dist/media/index.js.map +1 -1
  60. package/dist/media/properties.js +19 -97
  61. package/dist/media/properties.js.map +1 -1
  62. package/dist/mediaQualityMetrics/config.js +505 -493
  63. package/dist/mediaQualityMetrics/config.js.map +1 -1
  64. package/dist/meeting/in-meeting-actions.js +79 -1
  65. package/dist/meeting/in-meeting-actions.js.map +1 -1
  66. package/dist/meeting/index.js +2349 -2178
  67. package/dist/meeting/index.js.map +1 -1
  68. package/dist/meeting/locusMediaRequest.js +291 -0
  69. package/dist/meeting/locusMediaRequest.js.map +1 -0
  70. package/dist/meeting/muteState.js +229 -124
  71. package/dist/meeting/muteState.js.map +1 -1
  72. package/dist/meeting/request.js +191 -167
  73. package/dist/meeting/request.js.map +1 -1
  74. package/dist/meeting/request.type.js.map +1 -1
  75. package/dist/meeting/util.js +444 -443
  76. package/dist/meeting/util.js.map +1 -1
  77. package/dist/meeting-info/meeting-info-v2.js +157 -49
  78. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  79. package/dist/meeting-info/utilv2.js +20 -5
  80. package/dist/meeting-info/utilv2.js.map +1 -1
  81. package/dist/meetings/collection.js +22 -0
  82. package/dist/meetings/collection.js.map +1 -1
  83. package/dist/meetings/index.js +365 -73
  84. package/dist/meetings/index.js.map +1 -1
  85. package/dist/meetings/meetings.types.js +7 -0
  86. package/dist/meetings/meetings.types.js.map +1 -0
  87. package/dist/meetings/request.js +16 -12
  88. package/dist/meetings/request.js.map +1 -1
  89. package/dist/meetings/util.js +88 -1
  90. package/dist/meetings/util.js.map +1 -1
  91. package/dist/member/index.js +43 -0
  92. package/dist/member/index.js.map +1 -1
  93. package/dist/member/types.js +15 -0
  94. package/dist/member/types.js.map +1 -0
  95. package/dist/member/util.js +97 -3
  96. package/dist/member/util.js.map +1 -1
  97. package/dist/members/collection.js +10 -0
  98. package/dist/members/collection.js.map +1 -1
  99. package/dist/members/index.js +94 -11
  100. package/dist/members/index.js.map +1 -1
  101. package/dist/members/request.js +109 -39
  102. package/dist/members/request.js.map +1 -1
  103. package/dist/members/types.js +15 -0
  104. package/dist/members/types.js.map +1 -0
  105. package/dist/members/util.js +316 -233
  106. package/dist/members/util.js.map +1 -1
  107. package/dist/metrics/config.js +50 -14
  108. package/dist/metrics/config.js.map +1 -1
  109. package/dist/metrics/constants.js +3 -5
  110. package/dist/metrics/constants.js.map +1 -1
  111. package/dist/metrics/index.js +48 -29
  112. package/dist/metrics/index.js.map +1 -1
  113. package/dist/multistream/mediaRequestManager.js +265 -36
  114. package/dist/multistream/mediaRequestManager.js.map +1 -1
  115. package/dist/multistream/receiveSlot.js +52 -19
  116. package/dist/multistream/receiveSlot.js.map +1 -1
  117. package/dist/multistream/receiveSlotManager.js +53 -33
  118. package/dist/multistream/receiveSlotManager.js.map +1 -1
  119. package/dist/multistream/remoteMedia.js +44 -18
  120. package/dist/multistream/remoteMedia.js.map +1 -1
  121. package/dist/multistream/remoteMediaGroup.js +60 -3
  122. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  123. package/dist/multistream/remoteMediaManager.js +322 -103
  124. package/dist/multistream/remoteMediaManager.js.map +1 -1
  125. package/dist/networkQualityMonitor/index.js +4 -2
  126. package/dist/networkQualityMonitor/index.js.map +1 -1
  127. package/dist/reachability/index.js +117 -60
  128. package/dist/reachability/index.js.map +1 -1
  129. package/dist/reachability/request.js +12 -5
  130. package/dist/reachability/request.js.map +1 -1
  131. package/dist/reactions/constants.js +13 -0
  132. package/dist/reactions/constants.js.map +1 -0
  133. package/dist/reactions/reactions.js +2 -2
  134. package/dist/reactions/reactions.js.map +1 -1
  135. package/dist/reactions/reactions.type.js +18 -18
  136. package/dist/reactions/reactions.type.js.map +1 -1
  137. package/dist/reconnection-manager/index.js +190 -145
  138. package/dist/reconnection-manager/index.js.map +1 -1
  139. package/dist/recording-controller/enums.js +17 -0
  140. package/dist/recording-controller/enums.js.map +1 -0
  141. package/dist/recording-controller/index.js +343 -0
  142. package/dist/recording-controller/index.js.map +1 -0
  143. package/dist/recording-controller/util.js +63 -0
  144. package/dist/recording-controller/util.js.map +1 -0
  145. package/dist/roap/index.js +21 -29
  146. package/dist/roap/index.js.map +1 -1
  147. package/dist/roap/request.js +127 -92
  148. package/dist/roap/request.js.map +1 -1
  149. package/dist/roap/turnDiscovery.js +135 -53
  150. package/dist/roap/turnDiscovery.js.map +1 -1
  151. package/dist/statsAnalyzer/global.js +1 -93
  152. package/dist/statsAnalyzer/global.js.map +1 -1
  153. package/dist/statsAnalyzer/index.js +329 -314
  154. package/dist/statsAnalyzer/index.js.map +1 -1
  155. package/dist/statsAnalyzer/mqaUtil.js +103 -54
  156. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  157. package/dist/types/annotation/annotation.types.d.ts +43 -0
  158. package/dist/types/annotation/constants.d.ts +31 -0
  159. package/dist/types/annotation/index.d.ts +124 -0
  160. package/dist/types/breakouts/breakout.d.ts +8 -0
  161. package/dist/types/breakouts/collection.d.ts +5 -0
  162. package/dist/types/breakouts/edit-lock-error.d.ts +15 -0
  163. package/dist/types/breakouts/events.d.ts +2 -0
  164. package/dist/types/breakouts/index.d.ts +5 -0
  165. package/dist/types/breakouts/request.d.ts +22 -0
  166. package/dist/types/breakouts/utils.d.ts +15 -0
  167. package/dist/types/common/browser-detection.d.ts +9 -0
  168. package/dist/types/common/collection.d.ts +48 -0
  169. package/dist/types/common/config.d.ts +2 -0
  170. package/dist/types/common/errors/captcha-error.d.ts +15 -0
  171. package/dist/types/common/errors/intent-to-join.d.ts +16 -0
  172. package/dist/types/common/errors/join-meeting.d.ts +17 -0
  173. package/dist/types/common/errors/media.d.ts +15 -0
  174. package/dist/types/common/errors/parameter.d.ts +15 -0
  175. package/dist/types/common/errors/password-error.d.ts +15 -0
  176. package/dist/types/common/errors/permission.d.ts +14 -0
  177. package/dist/types/common/errors/reconnection-in-progress.d.ts +9 -0
  178. package/dist/types/common/errors/reconnection.d.ts +15 -0
  179. package/dist/types/common/errors/stats.d.ts +15 -0
  180. package/dist/types/common/errors/webex-errors.d.ts +69 -0
  181. package/dist/types/common/errors/webex-meetings-error.d.ts +20 -0
  182. package/dist/types/common/events/events-scope.d.ts +17 -0
  183. package/dist/types/common/events/events.d.ts +12 -0
  184. package/dist/types/common/events/trigger-proxy.d.ts +2 -0
  185. package/dist/types/common/events/util.d.ts +2 -0
  186. package/dist/types/common/logs/logger-config.d.ts +2 -0
  187. package/dist/types/common/logs/logger-proxy.d.ts +2 -0
  188. package/dist/types/common/logs/request.d.ts +34 -0
  189. package/dist/types/common/queue.d.ts +32 -0
  190. package/dist/types/config.d.ts +72 -0
  191. package/dist/types/constants.d.ts +987 -0
  192. package/dist/types/controls-options-manager/constants.d.ts +4 -0
  193. package/dist/types/controls-options-manager/enums.d.ts +15 -0
  194. package/dist/types/controls-options-manager/index.d.ts +136 -0
  195. package/dist/types/controls-options-manager/types.d.ts +43 -0
  196. package/dist/types/controls-options-manager/util.d.ts +1 -0
  197. package/dist/types/index.d.ts +7 -0
  198. package/dist/types/interpretation/collection.d.ts +5 -0
  199. package/dist/types/interpretation/index.d.ts +5 -0
  200. package/dist/types/interpretation/siLanguage.d.ts +5 -0
  201. package/dist/types/locus-info/controlsUtils.d.ts +2 -0
  202. package/dist/types/locus-info/embeddedAppsUtils.d.ts +2 -0
  203. package/dist/types/locus-info/fullState.d.ts +2 -0
  204. package/dist/types/locus-info/hostUtils.d.ts +2 -0
  205. package/dist/types/locus-info/index.d.ts +315 -0
  206. package/dist/types/locus-info/infoUtils.d.ts +2 -0
  207. package/dist/types/locus-info/mediaSharesUtils.d.ts +2 -0
  208. package/dist/types/locus-info/parser.d.ts +212 -0
  209. package/dist/types/locus-info/selfUtils.d.ts +2 -0
  210. package/dist/types/media/index.d.ts +34 -0
  211. package/dist/types/media/properties.d.ts +86 -0
  212. package/dist/types/media/util.d.ts +2 -0
  213. package/dist/types/mediaQualityMetrics/config.d.ts +365 -0
  214. package/dist/types/meeting/in-meeting-actions.d.ts +149 -0
  215. package/dist/types/meeting/index.d.ts +1524 -0
  216. package/dist/types/meeting/locusMediaRequest.d.ts +70 -0
  217. package/dist/types/meeting/muteState.d.ts +184 -0
  218. package/dist/types/meeting/request.d.ts +270 -0
  219. package/dist/types/meeting/request.type.d.ts +11 -0
  220. package/dist/types/meeting/state.d.ts +9 -0
  221. package/dist/types/meeting/util.d.ts +75 -0
  222. package/dist/types/meeting-info/collection.d.ts +20 -0
  223. package/dist/types/meeting-info/index.d.ts +57 -0
  224. package/dist/types/meeting-info/meeting-info-v2.d.ts +122 -0
  225. package/dist/types/meeting-info/request.d.ts +22 -0
  226. package/dist/types/meeting-info/util.d.ts +2 -0
  227. package/dist/types/meeting-info/utilv2.d.ts +2 -0
  228. package/dist/types/meetings/collection.d.ts +31 -0
  229. package/dist/types/meetings/index.d.ts +364 -0
  230. package/dist/types/meetings/meetings.types.d.ts +4 -0
  231. package/dist/types/meetings/request.d.ts +27 -0
  232. package/dist/types/meetings/util.d.ts +18 -0
  233. package/dist/types/member/index.d.ts +158 -0
  234. package/dist/types/member/types.d.ts +21 -0
  235. package/dist/types/member/util.d.ts +2 -0
  236. package/dist/types/members/collection.d.ts +29 -0
  237. package/dist/types/members/index.d.ts +353 -0
  238. package/dist/types/members/request.d.ts +114 -0
  239. package/dist/types/members/types.d.ts +24 -0
  240. package/dist/types/members/util.d.ts +210 -0
  241. package/dist/types/metrics/config.d.ts +195 -0
  242. package/dist/types/metrics/constants.d.ts +55 -0
  243. package/dist/types/metrics/index.d.ts +169 -0
  244. package/dist/types/multistream/mediaRequestManager.d.ts +118 -0
  245. package/dist/types/multistream/receiveSlot.d.ts +68 -0
  246. package/dist/types/multistream/receiveSlotManager.d.ts +56 -0
  247. package/dist/types/multistream/remoteMedia.d.ts +72 -0
  248. package/dist/types/multistream/remoteMediaGroup.d.ts +47 -0
  249. package/dist/types/multistream/remoteMediaManager.d.ts +277 -0
  250. package/dist/types/networkQualityMonitor/index.d.ts +70 -0
  251. package/dist/types/personal-meeting-room/index.d.ts +47 -0
  252. package/dist/types/personal-meeting-room/request.d.ts +14 -0
  253. package/dist/types/personal-meeting-room/util.d.ts +2 -0
  254. package/dist/types/reachability/index.d.ts +152 -0
  255. package/dist/types/reachability/request.d.ts +37 -0
  256. package/dist/types/reactions/constants.d.ts +3 -0
  257. package/dist/types/reactions/reactions.d.ts +4 -0
  258. package/dist/types/reactions/reactions.type.d.ts +52 -0
  259. package/dist/types/reconnection-manager/index.d.ts +126 -0
  260. package/dist/types/recording-controller/enums.d.ts +7 -0
  261. package/dist/types/recording-controller/index.d.ts +193 -0
  262. package/dist/types/recording-controller/util.d.ts +13 -0
  263. package/dist/types/roap/index.d.ts +77 -0
  264. package/dist/types/roap/request.d.ts +36 -0
  265. package/dist/types/roap/turnDiscovery.d.ts +91 -0
  266. package/dist/types/statsAnalyzer/global.d.ts +36 -0
  267. package/dist/types/statsAnalyzer/index.d.ts +200 -0
  268. package/dist/types/statsAnalyzer/mqaUtil.d.ts +24 -0
  269. package/dist/types/transcription/index.d.ts +64 -0
  270. package/package.json +28 -21
  271. package/src/annotation/annotation.types.ts +52 -0
  272. package/src/annotation/constants.ts +36 -0
  273. package/src/annotation/index.ts +343 -0
  274. package/src/breakouts/README.md +220 -0
  275. package/src/breakouts/breakout.ts +180 -0
  276. package/src/breakouts/collection.ts +19 -0
  277. package/src/breakouts/edit-lock-error.ts +25 -0
  278. package/src/breakouts/events.ts +37 -0
  279. package/src/breakouts/index.ts +921 -0
  280. package/src/breakouts/request.ts +55 -0
  281. package/src/breakouts/utils.ts +57 -0
  282. package/src/common/errors/webex-errors.ts +6 -2
  283. package/src/common/logs/logger-proxy.ts +1 -1
  284. package/src/config.ts +5 -7
  285. package/src/constants.ts +165 -20
  286. package/src/controls-options-manager/constants.ts +5 -0
  287. package/src/controls-options-manager/enums.ts +18 -0
  288. package/src/controls-options-manager/index.ts +278 -0
  289. package/src/controls-options-manager/types.ts +59 -0
  290. package/src/controls-options-manager/util.ts +286 -0
  291. package/src/index.ts +34 -0
  292. package/src/interpretation/README.md +51 -0
  293. package/src/interpretation/collection.ts +19 -0
  294. package/src/interpretation/index.ts +182 -0
  295. package/src/interpretation/siLanguage.ts +18 -0
  296. package/src/locus-info/controlsUtils.ts +110 -0
  297. package/src/locus-info/index.ts +339 -21
  298. package/src/locus-info/mediaSharesUtils.ts +48 -0
  299. package/src/locus-info/parser.ts +2 -1
  300. package/src/locus-info/selfUtils.ts +86 -2
  301. package/src/media/index.ts +70 -142
  302. package/src/media/properties.ts +41 -104
  303. package/src/mediaQualityMetrics/config.ts +379 -377
  304. package/src/meeting/in-meeting-actions.ts +156 -0
  305. package/src/meeting/index.ts +1779 -1741
  306. package/src/meeting/locusMediaRequest.ts +309 -0
  307. package/src/meeting/muteState.ts +228 -132
  308. package/src/meeting/request.ts +100 -91
  309. package/src/meeting/request.type.ts +2 -0
  310. package/src/meeting/util.ts +422 -421
  311. package/src/meeting-info/meeting-info-v2.ts +134 -13
  312. package/src/meeting-info/utilv2.ts +13 -3
  313. package/src/meetings/collection.ts +20 -0
  314. package/src/meetings/index.ts +385 -83
  315. package/src/meetings/meetings.types.ts +12 -0
  316. package/src/meetings/request.ts +3 -1
  317. package/src/meetings/util.ts +103 -4
  318. package/src/member/index.ts +42 -0
  319. package/src/member/types.ts +24 -0
  320. package/src/member/util.ts +95 -1
  321. package/src/members/collection.ts +8 -0
  322. package/src/members/index.ts +108 -6
  323. package/src/members/request.ts +98 -17
  324. package/src/members/types.ts +28 -0
  325. package/src/members/util.ts +319 -240
  326. package/src/metrics/config.ts +49 -10
  327. package/src/metrics/constants.ts +2 -4
  328. package/src/metrics/index.ts +43 -27
  329. package/src/multistream/mediaRequestManager.ts +337 -63
  330. package/src/multistream/receiveSlot.ts +68 -26
  331. package/src/multistream/receiveSlotManager.ts +61 -38
  332. package/src/multistream/remoteMedia.ts +29 -3
  333. package/src/multistream/remoteMediaGroup.ts +61 -2
  334. package/src/multistream/remoteMediaManager.ts +260 -66
  335. package/src/networkQualityMonitor/index.ts +6 -6
  336. package/src/reachability/index.ts +75 -25
  337. package/src/reachability/request.ts +10 -5
  338. package/src/reactions/constants.ts +4 -0
  339. package/src/reactions/reactions.ts +4 -4
  340. package/src/reactions/reactions.type.ts +28 -3
  341. package/src/reconnection-manager/index.ts +53 -32
  342. package/src/recording-controller/enums.ts +8 -0
  343. package/src/recording-controller/index.ts +315 -0
  344. package/src/recording-controller/util.ts +58 -0
  345. package/src/roap/index.ts +21 -30
  346. package/src/roap/request.ts +51 -52
  347. package/src/roap/turnDiscovery.ts +51 -27
  348. package/src/statsAnalyzer/global.ts +1 -94
  349. package/src/statsAnalyzer/index.ts +380 -390
  350. package/src/statsAnalyzer/mqaUtil.ts +106 -99
  351. package/test/integration/spec/converged-space-meetings.js +233 -0
  352. package/test/integration/spec/journey.js +331 -254
  353. package/test/integration/spec/space-meeting.js +77 -4
  354. package/test/unit/spec/annotation/index.ts +436 -0
  355. package/test/unit/spec/breakouts/breakout.ts +233 -0
  356. package/test/unit/spec/breakouts/collection.ts +15 -0
  357. package/test/unit/spec/breakouts/edit-lock-error.ts +30 -0
  358. package/test/unit/spec/breakouts/events.ts +77 -0
  359. package/test/unit/spec/breakouts/index.ts +1790 -0
  360. package/test/unit/spec/breakouts/request.ts +104 -0
  361. package/test/unit/spec/breakouts/utils.js +72 -0
  362. package/test/unit/spec/controls-options-manager/index.js +287 -0
  363. package/test/unit/spec/controls-options-manager/util.js +518 -0
  364. package/test/unit/spec/fixture/locus.js +1 -0
  365. package/test/unit/spec/interpretation/collection.ts +15 -0
  366. package/test/unit/spec/interpretation/index.ts +329 -0
  367. package/test/unit/spec/interpretation/siLanguage.ts +26 -0
  368. package/test/unit/spec/locus-info/controlsUtils.js +323 -30
  369. package/test/unit/spec/locus-info/index.js +680 -4
  370. package/test/unit/spec/locus-info/mediaSharesUtils.ts +22 -0
  371. package/test/unit/spec/locus-info/selfConstant.js +48 -0
  372. package/test/unit/spec/locus-info/selfUtils.js +275 -0
  373. package/test/unit/spec/media/index.ts +118 -22
  374. package/test/unit/spec/media/properties.ts +9 -9
  375. package/test/unit/spec/meeting/in-meeting-actions.ts +76 -0
  376. package/test/unit/spec/meeting/index.js +2695 -1513
  377. package/test/unit/spec/meeting/locusMediaRequest.ts +436 -0
  378. package/test/unit/spec/meeting/muteState.js +370 -208
  379. package/test/unit/spec/meeting/request.js +354 -42
  380. package/test/unit/spec/meeting/utils.js +270 -156
  381. package/test/unit/spec/meeting-info/meetinginfov2.js +383 -5
  382. package/test/unit/spec/meeting-info/utilv2.js +21 -0
  383. package/test/unit/spec/meetings/collection.js +14 -0
  384. package/test/unit/spec/meetings/index.js +866 -120
  385. package/test/unit/spec/meetings/utils.js +206 -2
  386. package/test/unit/spec/member/index.js +31 -0
  387. package/test/unit/spec/member/util.js +408 -32
  388. package/test/unit/spec/members/index.js +320 -1
  389. package/test/unit/spec/members/request.js +206 -27
  390. package/test/unit/spec/members/utils.js +184 -0
  391. package/test/unit/spec/metrics/index.js +98 -0
  392. package/test/unit/spec/multistream/mediaRequestManager.ts +1012 -109
  393. package/test/unit/spec/multistream/receiveSlot.ts +77 -18
  394. package/test/unit/spec/multistream/receiveSlotManager.ts +69 -39
  395. package/test/unit/spec/multistream/remoteMedia.ts +32 -2
  396. package/test/unit/spec/multistream/remoteMediaGroup.ts +271 -5
  397. package/test/unit/spec/multistream/remoteMediaManager.ts +672 -65
  398. package/test/unit/spec/networkQualityMonitor/index.js +4 -4
  399. package/test/unit/spec/reachability/index.ts +176 -25
  400. package/test/unit/spec/reachability/request.js +66 -0
  401. package/test/unit/spec/reconnection-manager/index.js +46 -13
  402. package/test/unit/spec/recording-controller/index.js +231 -0
  403. package/test/unit/spec/recording-controller/util.js +102 -0
  404. package/test/unit/spec/roap/index.ts +21 -51
  405. package/test/unit/spec/roap/request.ts +187 -0
  406. package/test/unit/spec/roap/turnDiscovery.ts +73 -34
  407. package/test/unit/spec/stats-analyzer/index.js +94 -43
  408. package/test/utils/constants.js +9 -0
  409. package/test/utils/integrationTestUtils.js +46 -0
  410. package/test/utils/testUtils.js +0 -45
  411. package/test/utils/webex-config.js +4 -0
  412. package/test/utils/webex-test-users.js +7 -3
  413. package/tsconfig.json +6 -0
  414. package/dist/media/internal-media-core-wrapper.js +0 -18
  415. package/dist/media/internal-media-core-wrapper.js.map +0 -1
  416. package/dist/meeting/effectsState.js +0 -262
  417. package/dist/meeting/effectsState.js.map +0 -1
  418. package/dist/multistream/multistreamMedia.js +0 -106
  419. package/dist/multistream/multistreamMedia.js.map +0 -1
  420. package/src/index.js +0 -15
  421. package/src/media/internal-media-core-wrapper.ts +0 -9
  422. package/src/meeting/effectsState.ts +0 -211
  423. package/src/multistream/multistreamMedia.ts +0 -93
  424. package/test/unit/spec/meeting/effectsState.js +0 -281
@@ -1,12 +1,29 @@
1
1
  import uuid from 'uuid';
2
- import {cloneDeep, isEqual, pick, isString, defer} from 'lodash';
2
+ import {cloneDeep, isEqual, pick, defer, isEmpty} from 'lodash';
3
3
  // @ts-ignore - Fix this
4
4
  import {StatelessWebexPlugin} from '@webex/webex-core';
5
- import {Media as WebRTCMedia, MediaConnection as MC} from '@webex/internal-media-core';
5
+ import {
6
+ ConnectionState,
7
+ Errors,
8
+ ErrorType,
9
+ Event,
10
+ MediaContent,
11
+ MediaType,
12
+ RemoteTrackType,
13
+ } from '@webex/internal-media-core';
14
+
15
+ import {
16
+ getDevices,
17
+ LocalTrack,
18
+ LocalCameraTrack,
19
+ LocalDisplayTrack,
20
+ LocalMicrophoneTrack,
21
+ LocalTrackEvents,
22
+ TrackMuteEvent,
23
+ } from '@webex/media-helpers';
6
24
 
7
25
  import {
8
26
  MeetingNotActiveError,
9
- createMeetingsError,
10
27
  UserInLobbyError,
11
28
  NoMediaEstablishedYetError,
12
29
  UserNotJoinedError,
@@ -16,20 +33,22 @@ import NetworkQualityMonitor from '../networkQualityMonitor';
16
33
  import LoggerProxy from '../common/logs/logger-proxy';
17
34
  import Trigger from '../common/events/trigger-proxy';
18
35
  import Roap from '../roap/index';
19
- import Media from '../media';
36
+ import Media, {type BundlePolicy} from '../media';
20
37
  import MediaProperties from '../media/properties';
21
38
  import MeetingStateMachine from './state';
22
- import createMuteState from './muteState';
23
- import createEffectsState from './effectsState';
39
+ import {createMuteState} from './muteState';
24
40
  import LocusInfo from '../locus-info';
25
41
  import Metrics from '../metrics';
26
- import {trigger, mediaType, error as MetricsError, eventType} from '../metrics/config';
42
+ import {trigger, error as MetricsError, eventType} from '../metrics/config';
27
43
  import ReconnectionManager from '../reconnection-manager';
28
44
  import MeetingRequest from './request';
29
45
  import Members from '../members/index';
30
46
  import MeetingUtil from './util';
47
+ import RecordingUtil from '../recording-controller/util';
48
+ import ControlsOptionsUtil from '../controls-options-manager/util';
31
49
  import MediaUtil from '../media/util';
32
50
  import Transcription from '../transcription';
51
+ import {Reactions, SkinTones} from '../reactions/reactions';
33
52
  import PasswordError from '../common/errors/password-error';
34
53
  import CaptchaError from '../common/errors/captcha-error';
35
54
  import ReconnectionError from '../common/errors/reconnection';
@@ -40,10 +59,12 @@ import {
40
59
  _JOIN_,
41
60
  AUDIO,
42
61
  CONTENT,
62
+ DISPLAY_HINTS,
43
63
  ENDED,
44
64
  EVENT_TRIGGERS,
45
65
  EVENT_TYPES,
46
66
  EVENTS,
67
+ BREAKOUTS,
47
68
  FLOOR_ACTION,
48
69
  FULL_STATE,
49
70
  LAYOUT_TYPES,
@@ -64,53 +85,85 @@ import {
64
85
  RECORDING_STATE,
65
86
  SHARE_STATUS,
66
87
  SHARE_STOPPED_REASON,
67
- VIDEO_RESOLUTIONS,
68
88
  VIDEO,
69
- BNR_STATUS,
70
89
  HTTP_VERBS,
90
+ SELF_ROLES,
91
+ INTERPRETATION,
71
92
  } from '../constants';
72
93
  import BEHAVIORAL_METRICS from '../metrics/constants';
73
94
  import ParameterError from '../common/errors/parameter';
74
- import MediaError from '../common/errors/media';
75
95
  import {
76
96
  MeetingInfoV2PasswordError,
77
97
  MeetingInfoV2CaptchaError,
98
+ MeetingInfoV2PolicyError,
78
99
  } from '../meeting-info/meeting-info-v2';
79
100
  import BrowserDetection from '../common/browser-detection';
80
- import {ReceiveSlotManager} from '../multistream/receiveSlotManager';
101
+ import {CSI, ReceiveSlotManager} from '../multistream/receiveSlotManager';
81
102
  import {MediaRequestManager} from '../multistream/mediaRequestManager';
82
103
  import {
104
+ Configuration as RemoteMediaManagerConfiguration,
83
105
  RemoteMediaManager,
84
106
  Event as RemoteMediaManagerEvent,
85
107
  } from '../multistream/remoteMediaManager';
86
- import {MultistreamMedia} from '../multistream/multistreamMedia';
87
- import {SkinTones, Reactions} from '../reactions/reactions';
88
- import {Reaction, ReactionType, SkinToneType} from '../reactions/reactions.type';
108
+ import {
109
+ Reaction,
110
+ ReactionServerType,
111
+ SkinToneType,
112
+ ProcessedReaction,
113
+ RelayEvent,
114
+ } from '../reactions/reactions.type';
115
+ import Breakouts from '../breakouts';
116
+ import SimultaneousInterpretation from '../interpretation';
117
+ import Annotation from '../annotation';
89
118
 
90
119
  import InMeetingActions from './in-meeting-actions';
120
+ import {REACTION_RELAY_TYPES} from '../reactions/constants';
121
+ import RecordingController from '../recording-controller';
122
+ import ControlsOptionsManager from '../controls-options-manager';
123
+ import PermissionError from '../common/errors/permission';
124
+ import {LocusMediaRequest} from './locusMediaRequest';
125
+ import {AnnotationInfo} from '../annotation/annotation.types';
91
126
 
92
127
  const {isBrowser} = BrowserDetection();
93
128
 
94
- const logRequest = (request: any, {header = '', success = '', failure = ''}) => {
95
- LoggerProxy.logger.info(header);
129
+ const logRequest = (request: any, {logText = ''}) => {
130
+ LoggerProxy.logger.info(`${logText} - sending request`);
96
131
 
97
132
  return request
98
133
  .then((arg) => {
99
- LoggerProxy.logger.info(success);
134
+ LoggerProxy.logger.info(`${logText} - has been successfully sent`);
100
135
 
101
136
  return arg;
102
137
  })
103
138
  .catch((error) => {
104
- LoggerProxy.logger.error(failure, error);
139
+ LoggerProxy.logger.error(`${logText} - has failed: `, error);
105
140
  throw error;
106
141
  });
107
142
  };
108
143
 
144
+ export type LocalTracks = {
145
+ microphone?: LocalMicrophoneTrack;
146
+ camera?: LocalCameraTrack;
147
+ screenShare?: {
148
+ audio?: LocalTrack; // todo: for now screen share audio is not supported (will be done in SPARK-399690)
149
+ video?: LocalDisplayTrack;
150
+ };
151
+ annotationInfo?: AnnotationInfo;
152
+ };
153
+
154
+ export type AddMediaOptions = {
155
+ localTracks?: LocalTracks;
156
+ audioEnabled?: boolean; // if not specified, default value true is used
157
+ videoEnabled?: boolean; // if not specified, default value true is used
158
+ receiveShare?: boolean; // if not specified, default value true is used
159
+ remoteMediaManagerConfig?: RemoteMediaManagerConfiguration; // applies only to multistream meetings
160
+ bundlePolicy?: BundlePolicy; // applies only to multistream meetings
161
+ };
162
+
109
163
  export const MEDIA_UPDATE_TYPE = {
110
- ALL: 'ALL',
111
- AUDIO: 'AUDIO',
112
- VIDEO: 'VIDEO',
113
- SHARE: 'SHARE',
164
+ TRANSCODED_MEDIA_CONNECTION: 'TRANSCODED_MEDIA_CONNECTION',
165
+ SHARE_FLOOR_REQUEST: 'SHARE_FLOOR_REQUEST',
166
+ UPDATE_MEDIA: 'UPDATE_MEDIA',
114
167
  };
115
168
 
116
169
  /**
@@ -125,16 +178,6 @@ export const MEDIA_UPDATE_TYPE = {
125
178
  * @property {boolean} isSharing
126
179
  */
127
180
 
128
- /**
129
- * AudioVideo
130
- * @typedef {Object} AudioVideo
131
- * @property {Object} audio
132
- * @property {String} audio.deviceId
133
- * @property {Object} video
134
- * @property {String} video.deviceId
135
- * @property {String} video.localVideoQuality // [240p, 360p, 480p, 720p, 1080p]
136
- */
137
-
138
181
  /**
139
182
  * SharePreferences
140
183
  * @typedef {Object} SharePreferences
@@ -149,18 +192,10 @@ export const MEDIA_UPDATE_TYPE = {
149
192
  * @property {String} [pin]
150
193
  * @property {Boolean} [moderator]
151
194
  * @property {String|Object} [meetingQuality]
152
- * @property {String} [meetingQuality.local]
153
195
  * @property {String} [meetingQuality.remote]
154
196
  * @property {Boolean} [rejoin]
155
197
  * @property {Boolean} [enableMultistream]
156
- */
157
-
158
- /**
159
- * SendOptions
160
- * @typedef {Object} SendOptions
161
- * @property {Boolean} sendAudio
162
- * @property {Boolean} sendVideo
163
- * @property {Boolean} sendShare
198
+ * @property {String} [correlationId]
164
199
  */
165
200
 
166
201
  /**
@@ -403,12 +438,14 @@ export const MEDIA_UPDATE_TYPE = {
403
438
  export default class Meeting extends StatelessWebexPlugin {
404
439
  attrs: any;
405
440
  audio: any;
441
+ breakouts: any;
442
+ simultaneousInterpretation: any;
443
+ annotation: any;
406
444
  conversationUrl: string;
407
445
  correlationId: string;
408
446
  destination: string;
409
447
  destinationType: string;
410
448
  deviceUrl: string;
411
- effects: any;
412
449
  hostId: string;
413
450
  id: string;
414
451
  isMultistream: boolean;
@@ -416,7 +453,7 @@ export default class Meeting extends StatelessWebexPlugin {
416
453
  mediaConnections: any[];
417
454
  mediaId?: string;
418
455
  meetingFiniteStateMachine: any;
419
- meetingInfo: object;
456
+ meetingInfo: any;
420
457
  meetingRequest: MeetingRequest;
421
458
  members: Members;
422
459
  options: object;
@@ -428,6 +465,7 @@ export default class Meeting extends StatelessWebexPlugin {
428
465
  resource: string;
429
466
  roap: Roap;
430
467
  roapSeq: number;
468
+ selfUrl?: string; // comes from Locus, initialized by updateMeetingObject()
431
469
  sipUri: string;
432
470
  type: string;
433
471
  userId: string;
@@ -449,32 +487,37 @@ export default class Meeting extends StatelessWebexPlugin {
449
487
  keepAliveTimerId: NodeJS.Timeout;
450
488
  lastVideoLayoutInfo: any;
451
489
  locusInfo: any;
452
- media: MultistreamMedia;
490
+ locusMediaRequest?: LocusMediaRequest;
453
491
  mediaProperties: MediaProperties;
454
492
  mediaRequestManagers: {
455
493
  audio: MediaRequestManager;
456
494
  video: MediaRequestManager;
495
+ screenShareAudio: MediaRequestManager;
496
+ screenShareVideo: MediaRequestManager;
457
497
  };
458
498
 
459
499
  meetingInfoFailureReason: string;
500
+ meetingInfoFailureCode?: number;
460
501
  networkQualityMonitor: NetworkQualityMonitor;
461
502
  networkStatus: string;
462
503
  passwordStatus: string;
463
504
  queuedMediaUpdates: any[];
464
505
  recording: any;
465
506
  remoteMediaManager: RemoteMediaManager | null;
507
+ recordingController: RecordingController;
508
+ controlsOptionsManager: ControlsOptionsManager;
466
509
  requiredCaptcha: any;
467
510
  receiveSlotManager: ReceiveSlotManager;
468
511
  shareStatus: string;
469
512
  statsAnalyzer: StatsAnalyzer;
470
513
  transcription: Transcription;
471
514
  updateMediaConnections: (mediaConnections: any[]) => void;
472
- endCallInitiateJoinReq: any;
515
+ endCallInitJoinReq: any;
473
516
  endJoinReqResp: any;
474
517
  endLocalSDPGenRemoteSDPRecvDelay: any;
475
518
  joinedWith: any;
476
519
  locusId: any;
477
- startCallInitiateJoinReq: any;
520
+ startCallInitJoinReq: any;
478
521
  startJoinReqResp: any;
479
522
  startLocalSDPGenRemoteSDPRecvDelay: any;
480
523
  wirelessShare: any;
@@ -487,8 +530,13 @@ export default class Meeting extends StatelessWebexPlugin {
487
530
  resourceUrl: string;
488
531
  selfId: string;
489
532
  state: any;
490
-
533
+ localAudioTrackMuteStateHandler: (event: TrackMuteEvent) => void;
534
+ localVideoTrackMuteStateHandler: (event: TrackMuteEvent) => void;
535
+ underlyingLocalTrackChangeHandler: () => void;
536
+ roles: any[];
537
+ environment: string;
491
538
  namespace = MEETINGS;
539
+ annotationInfo: AnnotationInfo;
492
540
 
493
541
  /**
494
542
  * @param {Object} attrs
@@ -523,7 +571,7 @@ export default class Meeting extends StatelessWebexPlugin {
523
571
  */
524
572
  this.id = uuid.v4();
525
573
  /**
526
- * Correlation ID used for network tracking of meeting join
574
+ * Correlation ID used for network tracking of meeting
527
575
  * @instance
528
576
  * @type {String}
529
577
  * @readonly
@@ -573,41 +621,132 @@ export default class Meeting extends StatelessWebexPlugin {
573
621
  */
574
622
  // TODO: needs to be defined as a class
575
623
  this.meetingInfo = {};
624
+ /**
625
+ * @instance
626
+ * @type {Breakouts}
627
+ * @public
628
+ * @memberof Meeting
629
+ */
630
+ // @ts-ignore
631
+ this.breakouts = new Breakouts({meetingId: this.id}, {parent: this.webex});
632
+ /**
633
+ * @instance
634
+ * @type {SimultaneousInterpretation}
635
+ * @public
636
+ * @memberof Meeting
637
+ */
638
+ // @ts-ignore
639
+ this.simultaneousInterpretation = new SimultaneousInterpretation({}, {parent: this.webex});
640
+ /**
641
+ * @instance
642
+ * @type {Annotation}
643
+ * @public
644
+ * @memberof Meeting
645
+ */
646
+ // @ts-ignore
647
+ this.annotation = new Annotation({parent: this.webex});
576
648
  /**
577
649
  * helper class for managing receive slots (for multistream media connections)
578
650
  */
579
- this.receiveSlotManager = new ReceiveSlotManager(this);
651
+ this.receiveSlotManager = new ReceiveSlotManager(
652
+ (mediaType: MediaType) => {
653
+ if (!this.mediaProperties?.webrtcMediaConnection) {
654
+ return Promise.reject(new Error('Webrtc media connection is missing'));
655
+ }
656
+
657
+ return this.mediaProperties.webrtcMediaConnection.createReceiveSlot(mediaType);
658
+ },
659
+ (csi: CSI) => (this.members.findMemberByCsi(csi) as any)?.id
660
+ );
580
661
  /**
581
- * Helper class for managing media requests for main video (for multistream media connections)
582
- * All media requests sent out for main video for this meeting have to go through it.
662
+ * Object containing helper classes for managing media requests for audio/video/screenshare (for multistream media connections)
663
+ * All multistream media requests sent out for this meeting have to go through them.
583
664
  */
584
665
  this.mediaRequestManagers = {
585
- audio: new MediaRequestManager((mediaRequests) => {
586
- if (!this.mediaProperties.webrtcMediaConnection) {
587
- LoggerProxy.logger.warn(
588
- 'Meeting:index#mediaRequestManager --> trying to send audio media request before media connection was created'
666
+ audio: new MediaRequestManager(
667
+ (mediaRequests) => {
668
+ if (!this.mediaProperties.webrtcMediaConnection) {
669
+ LoggerProxy.logger.warn(
670
+ 'Meeting:index#mediaRequestManager --> trying to send audio media request before media connection was created'
671
+ );
672
+
673
+ return;
674
+ }
675
+ this.mediaProperties.webrtcMediaConnection.requestMedia(
676
+ MediaType.AudioMain,
677
+ mediaRequests
589
678
  );
679
+ },
680
+ {
681
+ // @ts-ignore - config coming from registerPlugin
682
+ degradationPreferences: this.config.degradationPreferences,
683
+ kind: 'audio',
684
+ trimRequestsToNumOfSources: false,
685
+ }
686
+ ),
687
+ video: new MediaRequestManager(
688
+ (mediaRequests) => {
689
+ if (!this.mediaProperties.webrtcMediaConnection) {
690
+ LoggerProxy.logger.warn(
691
+ 'Meeting:index#mediaRequestManager --> trying to send video media request before media connection was created'
692
+ );
590
693
 
591
- return;
694
+ return;
695
+ }
696
+ this.mediaProperties.webrtcMediaConnection.requestMedia(
697
+ MediaType.VideoMain,
698
+ mediaRequests
699
+ );
700
+ },
701
+ {
702
+ // @ts-ignore - config coming from registerPlugin
703
+ degradationPreferences: this.config.degradationPreferences,
704
+ kind: 'video',
705
+ trimRequestsToNumOfSources: true,
592
706
  }
593
- this.mediaProperties.webrtcMediaConnection.requestMedia(
594
- MC.MediaType.AudioMain,
595
- mediaRequests
596
- );
597
- }),
598
- video: new MediaRequestManager((mediaRequests) => {
599
- if (!this.mediaProperties.webrtcMediaConnection) {
600
- LoggerProxy.logger.warn(
601
- 'Meeting:index#mediaRequestManager --> trying to send video media request before media connection was created'
707
+ ),
708
+ screenShareAudio: new MediaRequestManager(
709
+ (mediaRequests) => {
710
+ if (!this.mediaProperties.webrtcMediaConnection) {
711
+ LoggerProxy.logger.warn(
712
+ 'Meeting:index#mediaRequestManager --> trying to send screenshare audio media request before media connection was created'
713
+ );
714
+
715
+ return;
716
+ }
717
+ this.mediaProperties.webrtcMediaConnection.requestMedia(
718
+ MediaType.AudioSlides,
719
+ mediaRequests
602
720
  );
721
+ },
722
+ {
723
+ // @ts-ignore - config coming from registerPlugin
724
+ degradationPreferences: this.config.degradationPreferences,
725
+ kind: 'audio',
726
+ trimRequestsToNumOfSources: false,
727
+ }
728
+ ),
729
+ screenShareVideo: new MediaRequestManager(
730
+ (mediaRequests) => {
731
+ if (!this.mediaProperties.webrtcMediaConnection) {
732
+ LoggerProxy.logger.warn(
733
+ 'Meeting:index#mediaRequestManager --> trying to send screenshare video media request before media connection was created'
734
+ );
603
735
 
604
- return;
736
+ return;
737
+ }
738
+ this.mediaProperties.webrtcMediaConnection.requestMedia(
739
+ MediaType.VideoSlides,
740
+ mediaRequests
741
+ );
742
+ },
743
+ {
744
+ // @ts-ignore - config coming from registerPlugin
745
+ degradationPreferences: this.config.degradationPreferences,
746
+ kind: 'video',
747
+ trimRequestsToNumOfSources: false,
605
748
  }
606
- this.mediaProperties.webrtcMediaConnection.requestMedia(
607
- MC.MediaType.VideoMain,
608
- mediaRequests
609
- );
610
- }),
749
+ ),
611
750
  };
612
751
  /**
613
752
  * @instance
@@ -620,8 +759,9 @@ export default class Meeting extends StatelessWebexPlugin {
620
759
  locusUrl: attrs.locus && attrs.locus.url,
621
760
  receiveSlotManager: this.receiveSlotManager,
622
761
  mediaRequestManagers: this.mediaRequestManagers,
623
- // @ts-ignore - Fix type
762
+ meeting: this,
624
763
  },
764
+ // @ts-ignore - Fix type
625
765
  {parent: this.webex}
626
766
  );
627
767
  /**
@@ -653,7 +793,7 @@ export default class Meeting extends StatelessWebexPlugin {
653
793
  */
654
794
  this.reconnectionManager = new ReconnectionManager(this);
655
795
  /**
656
- * created later
796
+ * created with media connection
657
797
  * @instance
658
798
  * @type {MuteState}
659
799
  * @private
@@ -661,21 +801,13 @@ export default class Meeting extends StatelessWebexPlugin {
661
801
  */
662
802
  this.audio = null;
663
803
  /**
664
- * created later
804
+ * created with media connection
665
805
  * @instance
666
806
  * @type {MuteState}
667
807
  * @private
668
808
  * @memberof Meeting
669
809
  */
670
810
  this.video = null;
671
- /**
672
- * created later
673
- * @instance
674
- * @type {EffectsState}
675
- * @private
676
- * @memberof Meeting
677
- */
678
- this.effects = null;
679
811
  /**
680
812
  * @instance
681
813
  * @type {MeetingStateMachine}
@@ -770,7 +902,12 @@ export default class Meeting extends StatelessWebexPlugin {
770
902
  * @private
771
903
  * @memberof Meeting
772
904
  */
773
- this.meetingRequest = new MeetingRequest({}, options);
905
+ this.meetingRequest = new MeetingRequest(
906
+ {
907
+ meeting: this,
908
+ },
909
+ options
910
+ );
774
911
  /**
775
912
  * @instance
776
913
  * @type {Array}
@@ -924,6 +1061,7 @@ export default class Meeting extends StatelessWebexPlugin {
924
1061
  */
925
1062
  // @ts-ignore - Fix type
926
1063
  this.locusInfo = new LocusInfo(this.updateMeetingObject.bind(this), this.webex, this.id);
1064
+
927
1065
  // We had to add listeners first before setting up the locus instance
928
1066
  /**
929
1067
  * @instance
@@ -1014,6 +1152,15 @@ export default class Meeting extends StatelessWebexPlugin {
1014
1152
  */
1015
1153
  this.meetingInfoFailureReason = undefined;
1016
1154
 
1155
+ /**
1156
+ * The numeric code, if any, associated with the last failure to obtain the meeting info
1157
+ * @instance
1158
+ * @type {number}
1159
+ * @private
1160
+ * @memberof Meeting
1161
+ */
1162
+ this.meetingInfoFailureCode = undefined;
1163
+
1017
1164
  /**
1018
1165
  * Repeating timer used to send keepAlives when in lobby
1019
1166
  * @instance
@@ -1023,16 +1170,68 @@ export default class Meeting extends StatelessWebexPlugin {
1023
1170
  */
1024
1171
  this.keepAliveTimerId = null;
1025
1172
 
1173
+ /**
1174
+ * The class that helps to control recording functions: start, stop, pause, resume, etc
1175
+ * @instance
1176
+ * @type {RecordingController}
1177
+ * @public
1178
+ * @memberof Meeting
1179
+ */
1180
+ this.recordingController = new RecordingController(this.meetingRequest, {
1181
+ serviceUrl: this.locusInfo?.links?.services?.record?.url,
1182
+ sessionId: this.locusInfo?.fullState?.sessionId,
1183
+ locusUrl: this.locusInfo?.url,
1184
+ displayHints: [],
1185
+ });
1186
+
1187
+ /**
1188
+ * The class that helps to control recording functions: start, stop, pause, resume, etc
1189
+ * @instance
1190
+ * @type {ControlsOptionsManager}
1191
+ * @public
1192
+ * @memberof Meeting
1193
+ */
1194
+ this.controlsOptionsManager = new ControlsOptionsManager(this.meetingRequest, {
1195
+ locusUrl: this.locusInfo?.url,
1196
+ displayHints: [],
1197
+ });
1198
+
1026
1199
  this.setUpLocusInfoListeners();
1027
1200
  this.locusInfo.init(attrs.locus ? attrs.locus : {});
1028
1201
  this.hasJoinedOnce = false;
1029
1202
 
1030
- this.media = new MultistreamMedia(this);
1031
-
1032
1203
  /**
1033
1204
  * helper class for managing remote streams
1034
1205
  */
1035
1206
  this.remoteMediaManager = null;
1207
+
1208
+ this.localAudioTrackMuteStateHandler = (event) => {
1209
+ this.audio.handleLocalTrackMuteStateChange(this, event.trackState.muted);
1210
+ };
1211
+
1212
+ this.localVideoTrackMuteStateHandler = (event) => {
1213
+ this.video.handleLocalTrackMuteStateChange(this, event.trackState.muted);
1214
+ };
1215
+
1216
+ // The handling of underlying track changes should be done inside
1217
+ // @webex/internal-media-core, but for now we have to do it here, because
1218
+ // RoapMediaConnection has to use raw MediaStreamTracks in its API until
1219
+ // the Calling SDK also moves to using webrtc-core tracks
1220
+ this.underlyingLocalTrackChangeHandler = () => {
1221
+ if (!this.isMultistream) {
1222
+ this.updateTranscodedMediaConnection();
1223
+ }
1224
+ };
1225
+ }
1226
+
1227
+ /**
1228
+ * returns meeting is joined
1229
+ * @private
1230
+ * @memberof Meeting
1231
+ * @returns {Boolean}
1232
+ */
1233
+ private isJoined() {
1234
+ return this.joinedWith?.state === 'JOINED';
1036
1235
  }
1037
1236
 
1038
1237
  /**
@@ -1047,9 +1246,11 @@ export default class Meeting extends StatelessWebexPlugin {
1047
1246
  public async fetchMeetingInfo({
1048
1247
  password = null,
1049
1248
  captchaCode = null,
1249
+ extraParams = {},
1050
1250
  }: {
1051
1251
  password?: string;
1052
1252
  captchaCode?: string;
1253
+ extraParams?: Record<string, any>;
1053
1254
  }) {
1054
1255
  // when fetch meeting info is called directly by the client, we want to clear out the random timer for sdk to do it
1055
1256
  if (this.fetchMeetingInfoTimeoutId) {
@@ -1080,11 +1281,16 @@ export default class Meeting extends StatelessWebexPlugin {
1080
1281
  this.destination,
1081
1282
  this.destinationType,
1082
1283
  password,
1083
- captchaInfo
1284
+ captchaInfo,
1285
+ // @ts-ignore - config coming from registerPlugin
1286
+ this.config.installedOrgID,
1287
+ this.locusId,
1288
+ extraParams,
1289
+ {meetingId: this.id}
1084
1290
  );
1085
1291
 
1086
1292
  this.parseMeetingInfo(info, this.destination);
1087
- this.meetingInfo = info ? info.body : null;
1293
+ this.meetingInfo = info ? {...info.body, meetingLookupUrl: info?.url} : null;
1088
1294
  this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NONE;
1089
1295
  this.requiredCaptcha = null;
1090
1296
  if (
@@ -1107,9 +1313,18 @@ export default class Meeting extends StatelessWebexPlugin {
1107
1313
 
1108
1314
  return Promise.resolve();
1109
1315
  } catch (err) {
1110
- if (err instanceof MeetingInfoV2PasswordError) {
1111
- // @ts-ignore
1316
+ if (err instanceof MeetingInfoV2PolicyError) {
1317
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.POLICY;
1318
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1319
+
1320
+ if (err.meetingInfo) {
1321
+ this.meetingInfo = err.meetingInfo;
1322
+ }
1323
+
1324
+ throw new PermissionError();
1325
+ } else if (err instanceof MeetingInfoV2PasswordError) {
1112
1326
  LoggerProxy.logger.info(
1327
+ // @ts-ignore
1113
1328
  `Meeting:index#fetchMeetingInfo --> Info Unable to fetch meeting info for ${this.destination} - password required (code=${err?.body?.code}).`
1114
1329
  );
1115
1330
 
@@ -1119,6 +1334,8 @@ export default class Meeting extends StatelessWebexPlugin {
1119
1334
  this.meetingNumber = err.meetingInfo.meetingNumber;
1120
1335
  }
1121
1336
 
1337
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1338
+
1122
1339
  this.passwordStatus = PASSWORD_STATUS.REQUIRED;
1123
1340
  this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD;
1124
1341
  if (this.requiredCaptcha) {
@@ -1128,8 +1345,8 @@ export default class Meeting extends StatelessWebexPlugin {
1128
1345
 
1129
1346
  throw new PasswordError();
1130
1347
  } else if (err instanceof MeetingInfoV2CaptchaError) {
1131
- // @ts-ignore
1132
1348
  LoggerProxy.logger.info(
1349
+ // @ts-ignore
1133
1350
  `Meeting:index#fetchMeetingInfo --> Info Unable to fetch meeting info for ${this.destination} - captcha required (code=${err?.body?.code}).`
1134
1351
  );
1135
1352
 
@@ -1137,6 +1354,8 @@ export default class Meeting extends StatelessWebexPlugin {
1137
1354
  ? MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA
1138
1355
  : MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD;
1139
1356
 
1357
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1358
+
1140
1359
  if (err.isPasswordRequired) {
1141
1360
  this.passwordStatus = PASSWORD_STATUS.REQUIRED;
1142
1361
  }
@@ -1201,22 +1420,39 @@ export default class Meeting extends StatelessWebexPlugin {
1201
1420
  // we have to pass the wbxappapi hostname as the siteFullName parameter
1202
1421
  const {hostname} = new URL(this.requiredCaptcha.refreshURL);
1203
1422
 
1204
- return this.meetingRequest
1205
- .refreshCaptcha({
1206
- captchaRefreshUrl: `${this.requiredCaptcha.refreshURL}&siteFullName=${hostname}`,
1207
- captchaId: this.requiredCaptcha.captchaId,
1208
- })
1209
- .then((response) => {
1210
- this.requiredCaptcha.captchaId = response.body.captchaID;
1211
- this.requiredCaptcha.verificationImageURL = response.body.verificationImageURL;
1212
- this.requiredCaptcha.verificationAudioURL = response.body.verificationAudioURL;
1213
- })
1214
- .catch((error) => {
1215
- LoggerProxy.logger.error(
1216
- `Meeting:index#refreshCaptcha --> Error Unable to refresh captcha for ${this.destination} - ${error}`
1217
- );
1218
- throw error;
1219
- });
1423
+ return (
1424
+ this.meetingRequest
1425
+ // @ts-ignore
1426
+ .refreshCaptcha({
1427
+ captchaRefreshUrl: `${this.requiredCaptcha.refreshURL}&siteFullName=${hostname}`,
1428
+ captchaId: this.requiredCaptcha.captchaId,
1429
+ })
1430
+ .then((response) => {
1431
+ this.requiredCaptcha.captchaId = response.body.captchaID;
1432
+ this.requiredCaptcha.verificationImageURL = response.body.verificationImageURL;
1433
+ this.requiredCaptcha.verificationAudioURL = response.body.verificationAudioURL;
1434
+ })
1435
+ .catch((error) => {
1436
+ LoggerProxy.logger.error(
1437
+ `Meeting:index#refreshCaptcha --> Error Unable to refresh captcha for ${this.destination} - ${error}`
1438
+ );
1439
+ throw error;
1440
+ })
1441
+ );
1442
+ }
1443
+
1444
+ /**
1445
+ * Posts metrics event for this meeting. Allows the app to send Call Analyzer events.
1446
+ * @param {String} eventName - Call Analyzer event, see eventType in src/metrics/config.ts for possible values
1447
+ * @public
1448
+ * @memberof Meeting
1449
+ * @returns {Promise}
1450
+ */
1451
+ public postMetrics(eventName: string) {
1452
+ Metrics.postEvent({
1453
+ event: eventName,
1454
+ meeting: this,
1455
+ });
1220
1456
  }
1221
1457
 
1222
1458
  /**
@@ -1229,6 +1465,7 @@ export default class Meeting extends StatelessWebexPlugin {
1229
1465
  // meeting update listeners
1230
1466
  this.setUpLocusInfoSelfListener();
1231
1467
  this.setUpLocusInfoMeetingListener();
1468
+ this.setUpLocusServicesListener();
1232
1469
  // members update listeners
1233
1470
  this.setUpLocusFullStateListener();
1234
1471
  this.setUpLocusUrlListener();
@@ -1241,6 +1478,116 @@ export default class Meeting extends StatelessWebexPlugin {
1241
1478
  this.setUpLocusInfoMeetingInfoListener();
1242
1479
  this.setUpLocusInfoAssignHostListener();
1243
1480
  this.setUpLocusInfoMediaInactiveListener();
1481
+ this.setUpBreakoutsListener();
1482
+ this.setUpInterpretationListener();
1483
+ }
1484
+
1485
+ /**
1486
+ * Set up the listeners for breakouts
1487
+ * @returns {undefined}
1488
+ * @private
1489
+ * @memberof Meeting
1490
+ */
1491
+ setUpBreakoutsListener() {
1492
+ this.breakouts.on(BREAKOUTS.EVENTS.BREAKOUTS_CLOSING, () => {
1493
+ Trigger.trigger(
1494
+ this,
1495
+ {
1496
+ file: 'meeting/index',
1497
+ function: 'setUpBreakoutsListener',
1498
+ },
1499
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_CLOSING
1500
+ );
1501
+ });
1502
+
1503
+ this.breakouts.on(BREAKOUTS.EVENTS.MESSAGE, (messageEvent) => {
1504
+ Trigger.trigger(
1505
+ this,
1506
+ {
1507
+ file: 'meeting/index',
1508
+ function: 'setUpBreakoutsListener',
1509
+ },
1510
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_MESSAGE,
1511
+ messageEvent
1512
+ );
1513
+ });
1514
+
1515
+ this.breakouts.on(BREAKOUTS.EVENTS.MEMBERS_UPDATE, () => {
1516
+ Trigger.trigger(
1517
+ this,
1518
+ {
1519
+ file: 'meeting/index',
1520
+ function: 'setUpBreakoutsListener',
1521
+ },
1522
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_UPDATE
1523
+ );
1524
+ });
1525
+
1526
+ this.breakouts.on(BREAKOUTS.EVENTS.ASK_RETURN_TO_MAIN, () => {
1527
+ if (this.isJoined()) {
1528
+ Trigger.trigger(
1529
+ this,
1530
+ {
1531
+ file: 'meeting/index',
1532
+ function: 'setUpBreakoutsListener',
1533
+ },
1534
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_ASK_RETURN_TO_MAIN
1535
+ );
1536
+ }
1537
+ });
1538
+
1539
+ this.breakouts.on(BREAKOUTS.EVENTS.LEAVE_BREAKOUT, () => {
1540
+ Trigger.trigger(
1541
+ this,
1542
+ {
1543
+ file: 'meeting/index',
1544
+ function: 'setUpBreakoutsListener',
1545
+ },
1546
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_LEAVE
1547
+ );
1548
+ });
1549
+
1550
+ this.breakouts.on(BREAKOUTS.EVENTS.ASK_FOR_HELP, (helpEvent) => {
1551
+ Trigger.trigger(
1552
+ this,
1553
+ {
1554
+ file: 'meeting/index',
1555
+ function: 'setUpBreakoutsListener',
1556
+ },
1557
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_ASK_FOR_HELP,
1558
+ helpEvent
1559
+ );
1560
+ });
1561
+
1562
+ this.breakouts.on(BREAKOUTS.EVENTS.PRE_ASSIGNMENTS_UPDATE, () => {
1563
+ Trigger.trigger(
1564
+ this,
1565
+ {
1566
+ file: 'meeting/index',
1567
+ function: 'setUpBreakoutsListener',
1568
+ },
1569
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_PRE_ASSIGNMENTS_UPDATE
1570
+ );
1571
+ });
1572
+ }
1573
+
1574
+ /**
1575
+ * Set up the listeners for interpretation
1576
+ * @returns {undefined}
1577
+ * @private
1578
+ * @memberof Meeting
1579
+ */
1580
+ private setUpInterpretationListener() {
1581
+ this.simultaneousInterpretation.on(INTERPRETATION.EVENTS.SUPPORT_LANGUAGES_UPDATE, () => {
1582
+ Trigger.trigger(
1583
+ this,
1584
+ {
1585
+ file: 'meeting/index',
1586
+ function: 'setUpInterpretationListener',
1587
+ },
1588
+ EVENT_TRIGGERS.MEETING_INTERPRETATION_SUPPORT_LANGUAGES_UPDATE
1589
+ );
1590
+ });
1244
1591
  }
1245
1592
 
1246
1593
  /**
@@ -1356,19 +1703,20 @@ export default class Meeting extends StatelessWebexPlugin {
1356
1703
  * @returns {Object}
1357
1704
  * @memberof Meeting
1358
1705
  */
1359
- getAnalyzerMetricsPrePayload(
1360
- options:
1361
- | {
1362
- event: string;
1363
- trackingId: string;
1364
- locus: object;
1365
- mediaConnections: Array<any>;
1366
- errors: object;
1367
- }
1368
- | any
1369
- ) {
1706
+ getAnalyzerMetricsPrePayload(options: {
1707
+ type?: string;
1708
+ event: string;
1709
+ trackingId: string;
1710
+ locus: object;
1711
+ mediaConnections?: Array<any>;
1712
+ errors?: object;
1713
+ meetingLookupUrl?: string;
1714
+ clientType?: any;
1715
+ subClientType?: any;
1716
+ [key: string]: any;
1717
+ }) {
1370
1718
  if (options) {
1371
- const {event, trackingId, mediaConnections} = options;
1719
+ const {event, trackingId, mediaConnections, meetingLookupUrl} = options;
1372
1720
 
1373
1721
  if (!event) {
1374
1722
  LoggerProxy.logger.error(
@@ -1407,6 +1755,10 @@ export default class Meeting extends StatelessWebexPlugin {
1407
1755
  identifiers.mediaAgentCluster = this.mediaConnections?.[0].mediaAgentCluster;
1408
1756
  }
1409
1757
 
1758
+ if (meetingLookupUrl) {
1759
+ identifiers.meetingLookupUrl = meetingLookupUrl;
1760
+ }
1761
+
1410
1762
  if (options.trackingId) {
1411
1763
  identifiers.trackingId = trackingId;
1412
1764
  }
@@ -1456,12 +1808,12 @@ export default class Meeting extends StatelessWebexPlugin {
1456
1808
  };
1457
1809
  }
1458
1810
 
1459
- const callInitiateJoinReq = this.getCallInitiateJoinReq();
1811
+ const callInitJoinReq = this.getCallInitJoinReq();
1460
1812
 
1461
- if (callInitiateJoinReq) {
1813
+ if (callInitJoinReq) {
1462
1814
  options.joinTimes = {
1463
1815
  ...options.joinTimes,
1464
- callInitiateJoinReq,
1816
+ callInitJoinReq,
1465
1817
  };
1466
1818
  }
1467
1819
 
@@ -1474,15 +1826,31 @@ export default class Meeting extends StatelessWebexPlugin {
1474
1826
  };
1475
1827
  }
1476
1828
 
1477
- const getTotalJmt = this.getTotalJmt();
1829
+ const totalJmt = this.getTotalJmt();
1478
1830
 
1479
- if (getTotalJmt) {
1831
+ if (totalJmt) {
1480
1832
  options.joinTimes = {
1481
1833
  ...options.joinTimes,
1482
- getTotalJmt,
1834
+ totalJmt,
1483
1835
  };
1484
1836
  }
1485
1837
 
1838
+ const curUserType = this.getCurUserType();
1839
+
1840
+ if (curUserType) {
1841
+ options.userType = curUserType;
1842
+ }
1843
+
1844
+ const curLoginType = this.getCurLoginType();
1845
+
1846
+ if (curLoginType) {
1847
+ options.loginType = curLoginType;
1848
+ }
1849
+
1850
+ if (this.environment) {
1851
+ options.environment = this.environment;
1852
+ }
1853
+
1486
1854
  if (options.type === MQA_STATS.CA_TYPE) {
1487
1855
  payload = Metrics.initMediaPayload(options.event, identifiers, options);
1488
1856
  } else {
@@ -1596,11 +1964,7 @@ export default class Meeting extends StatelessWebexPlugin {
1596
1964
  this.pstnUpdate(payload);
1597
1965
 
1598
1966
  // If user moved to a JOINED state and there is a pending floor grant trigger it
1599
- if (this.floorGrantPending && payload.newSelf.state === MEETING_STATE.STATES.JOINED) {
1600
- this.requestScreenShareFloor().then(() => {
1601
- this.floorGrantPending = false;
1602
- });
1603
- }
1967
+ this.requestScreenShareFloorIfPending();
1604
1968
  });
1605
1969
  }
1606
1970
 
@@ -1786,6 +2150,43 @@ export default class Meeting extends StatelessWebexPlugin {
1786
2150
  }
1787
2151
  );
1788
2152
 
2153
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_MEETING_BREAKOUT_UPDATED, ({breakout}) => {
2154
+ this.breakouts.updateBreakout(breakout);
2155
+ Trigger.trigger(
2156
+ this,
2157
+ {
2158
+ file: 'meeting/index',
2159
+ function: 'setupLocusControlsListener',
2160
+ },
2161
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_UPDATE
2162
+ );
2163
+ });
2164
+
2165
+ this.locusInfo.on(
2166
+ LOCUSINFO.EVENTS.CONTROLS_MEETING_INTERPRETATION_UPDATED,
2167
+ ({interpretation}) => {
2168
+ this.simultaneousInterpretation.updateInterpretation(interpretation);
2169
+ Trigger.trigger(
2170
+ this,
2171
+ {
2172
+ file: 'meeting/index',
2173
+ function: 'setupLocusControlsListener',
2174
+ },
2175
+ EVENT_TRIGGERS.MEETING_INTERPRETATION_UPDATE
2176
+ );
2177
+ }
2178
+ );
2179
+
2180
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_JOIN_BREAKOUT_FROM_MAIN, ({mainLocusUrl}) => {
2181
+ this.meetingRequest.getLocusStatusByUrl(mainLocusUrl).catch((error) => {
2182
+ // clear main session cache when attendee join into breakout and forbidden to get locus from main locus url,
2183
+ // which means main session is not active for the attendee
2184
+ if (error?.statusCode === 403) {
2185
+ this.locusInfo.clearMainSessionLocusCache();
2186
+ }
2187
+ });
2188
+ });
2189
+
1789
2190
  this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_ENTRY_EXIT_TONE_UPDATED, ({entryExitTone}) => {
1790
2191
  Trigger.trigger(
1791
2192
  this,
@@ -1797,6 +2198,96 @@ export default class Meeting extends StatelessWebexPlugin {
1797
2198
  {entryExitTone}
1798
2199
  );
1799
2200
  });
2201
+
2202
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_MUTE_ON_ENTRY_CHANGED, ({state}) => {
2203
+ Trigger.trigger(
2204
+ this,
2205
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2206
+ EVENT_TRIGGERS.MEETING_CONTROLS_MUTE_ON_ENTRY_UPDATED,
2207
+ {state}
2208
+ );
2209
+ });
2210
+
2211
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_SHARE_CONTROL_CHANGED, ({state}) => {
2212
+ Trigger.trigger(
2213
+ this,
2214
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2215
+ EVENT_TRIGGERS.MEETING_CONTROLS_SHARE_CONTROL_UPDATED,
2216
+ {state}
2217
+ );
2218
+ });
2219
+
2220
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_DISALLOW_UNMUTE_CHANGED, ({state}) => {
2221
+ Trigger.trigger(
2222
+ this,
2223
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2224
+ EVENT_TRIGGERS.MEETING_CONTROLS_DISALLOW_UNMUTE_UPDATED,
2225
+ {state}
2226
+ );
2227
+ });
2228
+
2229
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_REACTIONS_CHANGED, ({state}) => {
2230
+ Trigger.trigger(
2231
+ this,
2232
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2233
+ EVENT_TRIGGERS.MEETING_CONTROLS_REACTIONS_UPDATED,
2234
+ {state}
2235
+ );
2236
+ });
2237
+
2238
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_VIEW_THE_PARTICIPANTS_LIST_CHANGED, ({state}) => {
2239
+ Trigger.trigger(
2240
+ this,
2241
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2242
+ EVENT_TRIGGERS.MEETING_CONTROLS_VIEW_THE_PARTICIPANTS_LIST_UPDATED,
2243
+ {state}
2244
+ );
2245
+ });
2246
+
2247
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_RAISE_HAND_CHANGED, ({state}) => {
2248
+ Trigger.trigger(
2249
+ this,
2250
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2251
+ EVENT_TRIGGERS.MEETING_CONTROLS_RAISE_HAND_UPDATED,
2252
+ {state}
2253
+ );
2254
+ });
2255
+
2256
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_VIDEO_CHANGED, ({state}) => {
2257
+ Trigger.trigger(
2258
+ this,
2259
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2260
+ EVENT_TRIGGERS.MEETING_CONTROLS_VIDEO_UPDATED,
2261
+ {state}
2262
+ );
2263
+ });
2264
+ }
2265
+
2266
+ /**
2267
+ * Trigger annotation info update event
2268
+ @returns {undefined}
2269
+ @param {object} contentShare
2270
+ @param {object} previousContentShare
2271
+ */
2272
+ private triggerAnnotationInfoEvent(contentShare, previousContentShare) {
2273
+ if (
2274
+ contentShare?.annotation &&
2275
+ !isEqual(contentShare?.annotation, previousContentShare?.annotation)
2276
+ ) {
2277
+ Trigger.trigger(
2278
+ // @ts-ignore
2279
+ this.webex.meetings,
2280
+ {
2281
+ file: 'meeting/index',
2282
+ function: 'triggerAnnotationInfoEvent',
2283
+ },
2284
+ EVENT_TRIGGERS.MEETING_UPDATE_ANNOTATION_INFO,
2285
+ {
2286
+ annotationInfo: contentShare?.annotation,
2287
+ meetingId: this.id,
2288
+ }
2289
+ );
2290
+ }
1800
2291
  }
1801
2292
 
1802
2293
  /**
@@ -1809,11 +2300,13 @@ export default class Meeting extends StatelessWebexPlugin {
1809
2300
  */
1810
2301
  private setUpLocusMediaSharesListener() {
1811
2302
  // Will get triggered on local and remote share
1812
- this.locusInfo.on(EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES, (payload) => {
2303
+ this.locusInfo.on(EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES, async (payload) => {
1813
2304
  const {content: contentShare, whiteboard: whiteboardShare} = payload.current;
1814
2305
  const previousContentShare = payload.previous?.content;
1815
2306
  const previousWhiteboardShare = payload.previous?.whiteboard;
1816
2307
 
2308
+ this.triggerAnnotationInfoEvent(contentShare, previousContentShare);
2309
+
1817
2310
  if (
1818
2311
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
1819
2312
  contentShare.disposition === previousContentShare?.disposition &&
@@ -1841,19 +2334,8 @@ export default class Meeting extends StatelessWebexPlugin {
1841
2334
  this.selfId === contentShare.beneficiaryId &&
1842
2335
  contentShare.disposition === FLOOR_ACTION.GRANTED
1843
2336
  ) {
1844
- if (this.mediaProperties.shareTrack?.readyState === 'ended') {
1845
- this.stopShare({
1846
- skipSignalingCheck: true,
1847
- }).catch((error) => {
1848
- LoggerProxy.logger.log(
1849
- 'Meeting:index#setUpLocusMediaSharesListener --> Error stopping share: ',
1850
- error
1851
- );
1852
- });
1853
- } else {
1854
- // CONTENT - sharing content local
1855
- newShareStatus = SHARE_STATUS.LOCAL_SHARE_ACTIVE;
1856
- }
2337
+ // CONTENT - sharing content local
2338
+ newShareStatus = SHARE_STATUS.LOCAL_SHARE_ACTIVE;
1857
2339
  }
1858
2340
  // If we did not hit the cases above, no one is sharng content, so we check if we are sharing whiteboard
1859
2341
  // There is no concept of local/remote share for whiteboard
@@ -1937,23 +2419,22 @@ export default class Meeting extends StatelessWebexPlugin {
1937
2419
  EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
1938
2420
  {
1939
2421
  memberId: contentShare.beneficiaryId,
2422
+ url: contentShare.url,
2423
+ shareInstanceId: contentShare.shareInstanceId,
1940
2424
  }
1941
2425
  );
1942
2426
  };
1943
2427
 
1944
- // if a remote participant is stealing the presentation from us
1945
- if (
1946
- !this.mediaProperties.mediaDirection?.sendShare ||
1947
- oldShareStatus === SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE
1948
- ) {
2428
+ try {
2429
+ // if a remote participant is stealing the presentation from us
2430
+ if (
2431
+ this.mediaProperties.mediaDirection?.sendShare &&
2432
+ oldShareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE
2433
+ ) {
2434
+ await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo screen share audio (SPARK-399690)
2435
+ }
2436
+ } finally {
1949
2437
  sendStartedSharingRemote();
1950
- } else {
1951
- this.updateShare({
1952
- sendShare: false,
1953
- receiveShare: this.mediaProperties.mediaDirection.receiveShare,
1954
- }).finally(() => {
1955
- sendStartedSharingRemote();
1956
- });
1957
2438
  }
1958
2439
  break;
1959
2440
  }
@@ -2007,6 +2488,8 @@ export default class Meeting extends StatelessWebexPlugin {
2007
2488
  EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
2008
2489
  {
2009
2490
  memberId: contentShare.beneficiaryId,
2491
+ url: contentShare.url,
2492
+ shareInstanceId: contentShare.shareInstanceId,
2010
2493
  }
2011
2494
  );
2012
2495
  this.members.locusMediaSharesUpdate(payload);
@@ -2041,8 +2524,31 @@ export default class Meeting extends StatelessWebexPlugin {
2041
2524
  private setUpLocusUrlListener() {
2042
2525
  this.locusInfo.on(EVENTS.LOCUS_INFO_UPDATE_URL, (payload) => {
2043
2526
  this.members.locusUrlUpdate(payload);
2527
+ this.breakouts.locusUrlUpdate(payload);
2528
+ this.simultaneousInterpretation.locusUrlUpdate(payload);
2529
+ this.annotation.locusUrlUpdate(payload);
2044
2530
  this.locusUrl = payload;
2045
2531
  this.locusId = this.locusUrl?.split('/').pop();
2532
+ this.recordingController.setLocusUrl(this.locusUrl);
2533
+ this.controlsOptionsManager.setLocusUrl(this.locusUrl);
2534
+ });
2535
+ }
2536
+
2537
+ /**
2538
+ * Set up the locus info service link listener
2539
+ * update the locusInfo for recording controller
2540
+ * does not currently re-emit the event as it's internal only
2541
+ * payload is unused
2542
+ * @returns {undefined}
2543
+ * @private
2544
+ * @memberof Meeting
2545
+ */
2546
+ private setUpLocusServicesListener() {
2547
+ this.locusInfo.on(LOCUSINFO.EVENTS.LINKS_SERVICES, (payload) => {
2548
+ this.recordingController.setServiceUrl(payload?.services?.record?.url);
2549
+ this.recordingController.setSessionId(this.locusInfo?.fullState?.sessionId);
2550
+ this.breakouts.breakoutServiceUrlUpdate(payload?.services?.breakout?.url);
2551
+ this.annotation.approvalUrlUpdate(payload?.services?.approval?.url);
2046
2552
  });
2047
2553
  }
2048
2554
 
@@ -2092,10 +2598,22 @@ export default class Meeting extends StatelessWebexPlugin {
2092
2598
  canAdmitParticipant: MeetingUtil.canAdmitParticipant(payload.info.userDisplayHints),
2093
2599
  canLock: MeetingUtil.canUserLock(payload.info.userDisplayHints),
2094
2600
  canUnlock: MeetingUtil.canUserUnlock(payload.info.userDisplayHints),
2095
- canStartRecording: MeetingUtil.canUserRecord(payload.info.userDisplayHints),
2096
- canStopRecording: MeetingUtil.canUserStop(payload.info.userDisplayHints),
2097
- canPauseRecording: MeetingUtil.canUserPause(payload.info.userDisplayHints),
2098
- canResumeRecording: MeetingUtil.canUserResume(payload.info.userDisplayHints),
2601
+ canSetDisallowUnmute: ControlsOptionsUtil.canSetDisallowUnmute(
2602
+ payload.info.userDisplayHints
2603
+ ),
2604
+ canUnsetDisallowUnmute: ControlsOptionsUtil.canUnsetDisallowUnmute(
2605
+ payload.info.userDisplayHints
2606
+ ),
2607
+ canSetMuteOnEntry: ControlsOptionsUtil.canSetMuteOnEntry(payload.info.userDisplayHints),
2608
+ canUnsetMuteOnEntry: ControlsOptionsUtil.canUnsetMuteOnEntry(
2609
+ payload.info.userDisplayHints
2610
+ ),
2611
+ canSetMuted: ControlsOptionsUtil.canSetMuted(payload.info.userDisplayHints),
2612
+ canUnsetMuted: ControlsOptionsUtil.canUnsetMuted(payload.info.userDisplayHints),
2613
+ canStartRecording: RecordingUtil.canUserStart(payload.info.userDisplayHints),
2614
+ canStopRecording: RecordingUtil.canUserStop(payload.info.userDisplayHints),
2615
+ canPauseRecording: RecordingUtil.canUserPause(payload.info.userDisplayHints),
2616
+ canResumeRecording: RecordingUtil.canUserResume(payload.info.userDisplayHints),
2099
2617
  canRaiseHand: MeetingUtil.canUserRaiseHand(payload.info.userDisplayHints),
2100
2618
  canLowerAllHands: MeetingUtil.canUserLowerAllHands(payload.info.userDisplayHints),
2101
2619
  canLowerSomeoneElsesHand: MeetingUtil.canUserLowerSomeoneElsesHand(
@@ -2108,6 +2626,9 @@ export default class Meeting extends StatelessWebexPlugin {
2108
2626
  canStartTranscribing: MeetingUtil.canStartTranscribing(payload.info.userDisplayHints),
2109
2627
  canStopTranscribing: MeetingUtil.canStopTranscribing(payload.info.userDisplayHints),
2110
2628
  isClosedCaptionActive: MeetingUtil.isClosedCaptionActive(payload.info.userDisplayHints),
2629
+ isSaveTranscriptsEnabled: MeetingUtil.isSaveTranscriptsEnabled(
2630
+ payload.info.userDisplayHints
2631
+ ),
2111
2632
  isWebexAssistantActive: MeetingUtil.isWebexAssistantActive(payload.info.userDisplayHints),
2112
2633
  canViewCaptionPanel: MeetingUtil.canViewCaptionPanel(payload.info.userDisplayHints),
2113
2634
  isRealTimeTranslationEnabled: MeetingUtil.isRealTimeTranslationEnabled(
@@ -2117,8 +2638,118 @@ export default class Meeting extends StatelessWebexPlugin {
2117
2638
  payload.info.userDisplayHints
2118
2639
  ),
2119
2640
  waitingForOthersToJoin: MeetingUtil.waitingForOthersToJoin(payload.info.userDisplayHints),
2641
+ canSendReactions: MeetingUtil.canSendReactions(
2642
+ this.inMeetingActions.canSendReactions,
2643
+ payload.info.userDisplayHints
2644
+ ),
2645
+ canManageBreakout: MeetingUtil.canManageBreakout(payload.info.userDisplayHints),
2646
+ canBroadcastMessageToBreakout: MeetingUtil.canBroadcastMessageToBreakout(
2647
+ payload.info.userDisplayHints
2648
+ ),
2649
+ canAdmitLobbyToBreakout: MeetingUtil.canAdmitLobbyToBreakout(
2650
+ payload.info.userDisplayHints
2651
+ ),
2652
+ isBreakoutPreassignmentsEnabled: MeetingUtil.isBreakoutPreassignmentsEnabled(
2653
+ payload.info.userDisplayHints
2654
+ ),
2655
+ canUserAskForHelp: MeetingUtil.canUserAskForHelp(payload.info.userDisplayHints),
2656
+ canUserRenameSelfAndObserved: MeetingUtil.canUserRenameSelfAndObserved(
2657
+ payload.info.userDisplayHints
2658
+ ),
2659
+ canUserRenameOthers: MeetingUtil.canUserRenameOthers(payload.info.userDisplayHints),
2660
+ canMuteAll: ControlsOptionsUtil.hasHints({
2661
+ requiredHints: [DISPLAY_HINTS.MUTE_ALL],
2662
+ displayHints: payload.info.userDisplayHints,
2663
+ }),
2664
+ canUnmuteAll: ControlsOptionsUtil.hasHints({
2665
+ requiredHints: [DISPLAY_HINTS.UNMUTE_ALL],
2666
+ displayHints: payload.info.userDisplayHints,
2667
+ }),
2668
+ canEnableHardMute: ControlsOptionsUtil.hasHints({
2669
+ requiredHints: [DISPLAY_HINTS.ENABLE_HARD_MUTE],
2670
+ displayHints: payload.info.userDisplayHints,
2671
+ }),
2672
+ canDisableHardMute: ControlsOptionsUtil.hasHints({
2673
+ requiredHints: [DISPLAY_HINTS.DISABLE_HARD_MUTE],
2674
+ displayHints: payload.info.userDisplayHints,
2675
+ }),
2676
+ canEnableMuteOnEntry: ControlsOptionsUtil.hasHints({
2677
+ requiredHints: [DISPLAY_HINTS.ENABLE_MUTE_ON_ENTRY],
2678
+ displayHints: payload.info.userDisplayHints,
2679
+ }),
2680
+ canDisableMuteOnEntry: ControlsOptionsUtil.hasHints({
2681
+ requiredHints: [DISPLAY_HINTS.DISABLE_MUTE_ON_ENTRY],
2682
+ displayHints: payload.info.userDisplayHints,
2683
+ }),
2684
+ canEnableReactions: ControlsOptionsUtil.hasHints({
2685
+ requiredHints: [DISPLAY_HINTS.ENABLE_REACTIONS],
2686
+ displayHints: payload.info.userDisplayHints,
2687
+ }),
2688
+ canDisableReactions: ControlsOptionsUtil.hasHints({
2689
+ requiredHints: [DISPLAY_HINTS.DISABLE_REACTIONS],
2690
+ displayHints: payload.info.userDisplayHints,
2691
+ }),
2692
+ canEnableReactionDisplayNames: ControlsOptionsUtil.hasHints({
2693
+ requiredHints: [DISPLAY_HINTS.ENABLE_SHOW_DISPLAY_NAME],
2694
+ displayHints: payload.info.userDisplayHints,
2695
+ }),
2696
+ canDisableReactionDisplayNames: ControlsOptionsUtil.hasHints({
2697
+ requiredHints: [DISPLAY_HINTS.DISABLE_SHOW_DISPLAY_NAME],
2698
+ displayHints: payload.info.userDisplayHints,
2699
+ }),
2700
+ canUpdateShareControl: ControlsOptionsUtil.hasHints({
2701
+ requiredHints: [DISPLAY_HINTS.SHARE_CONTROL],
2702
+ displayHints: payload.info.userDisplayHints,
2703
+ }),
2704
+ canEnableViewTheParticipantsList: ControlsOptionsUtil.hasHints({
2705
+ requiredHints: [DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST],
2706
+ displayHints: payload.info.userDisplayHints,
2707
+ }),
2708
+ canDisableViewTheParticipantsList: ControlsOptionsUtil.hasHints({
2709
+ requiredHints: [DISPLAY_HINTS.DISABLE_VIEW_THE_PARTICIPANT_LIST],
2710
+ displayHints: payload.info.userDisplayHints,
2711
+ }),
2712
+ canEnableRaiseHand: ControlsOptionsUtil.hasHints({
2713
+ requiredHints: [DISPLAY_HINTS.ENABLE_RAISE_HAND],
2714
+ displayHints: payload.info.userDisplayHints,
2715
+ }),
2716
+ canDisableRaiseHand: ControlsOptionsUtil.hasHints({
2717
+ requiredHints: [DISPLAY_HINTS.DISABLE_RAISE_HAND],
2718
+ displayHints: payload.info.userDisplayHints,
2719
+ }),
2720
+ canEnableVideo: ControlsOptionsUtil.hasHints({
2721
+ requiredHints: [DISPLAY_HINTS.ENABLE_VIDEO],
2722
+ displayHints: payload.info.userDisplayHints,
2723
+ }),
2724
+ canDisableVideo: ControlsOptionsUtil.hasHints({
2725
+ requiredHints: [DISPLAY_HINTS.DISABLE_VIDEO],
2726
+ displayHints: payload.info.userDisplayHints,
2727
+ }),
2728
+ canShareFile: ControlsOptionsUtil.hasHints({
2729
+ requiredHints: [DISPLAY_HINTS.SHARE_FILE],
2730
+ displayHints: payload.info.userDisplayHints,
2731
+ }),
2732
+ canShareApplication: ControlsOptionsUtil.hasHints({
2733
+ requiredHints: [DISPLAY_HINTS.SHARE_APPLICATION],
2734
+ displayHints: payload.info.userDisplayHints,
2735
+ }),
2736
+ canShareCamera: ControlsOptionsUtil.hasHints({
2737
+ requiredHints: [DISPLAY_HINTS.SHARE_CAMERA],
2738
+ displayHints: payload.info.userDisplayHints,
2739
+ }),
2740
+ canShareDesktop: ControlsOptionsUtil.hasHints({
2741
+ requiredHints: [DISPLAY_HINTS.SHARE_DESKTOP],
2742
+ displayHints: payload.info.userDisplayHints,
2743
+ }),
2744
+ canShareContent: ControlsOptionsUtil.hasHints({
2745
+ requiredHints: [DISPLAY_HINTS.SHARE_CONTENT],
2746
+ displayHints: payload.info.userDisplayHints,
2747
+ }),
2120
2748
  });
2121
2749
 
2750
+ this.recordingController.setDisplayHints(payload.info.userDisplayHints);
2751
+ this.controlsOptionsManager.setDisplayHints(payload.info.userDisplayHints);
2752
+
2122
2753
  if (changed) {
2123
2754
  Trigger.trigger(
2124
2755
  this,
@@ -2141,7 +2772,9 @@ export default class Meeting extends StatelessWebexPlugin {
2141
2772
  * @param {String} datachannelUrl
2142
2773
  * @returns {void}
2143
2774
  */
2775
+
2144
2776
  handleDataChannelUrlChange(datachannelUrl) {
2777
+ // @ts-ignore - config coming from registerPlugin
2145
2778
  if (datachannelUrl && this.config.enableAutomaticLLM) {
2146
2779
  // Defer this as updateLLMConnection relies upon this.locusInfo.url which is only set
2147
2780
  // after the MEETING_INFO_UPDATED callback finishes
@@ -2196,10 +2829,34 @@ export default class Meeting extends StatelessWebexPlugin {
2196
2829
  );
2197
2830
  }
2198
2831
  });
2832
+
2833
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED, (payload) => {
2834
+ if (payload) {
2835
+ if (this.video) {
2836
+ payload.muted = payload.muted ?? this.video.isRemotelyMuted();
2837
+ payload.unmuteAllowed = payload.unmuteAllowed ?? this.video.isUnmuteAllowed();
2838
+ this.video.handleServerRemoteMuteUpdate(this, payload.muted, payload.unmuteAllowed);
2839
+ }
2840
+ Trigger.trigger(
2841
+ this,
2842
+ {
2843
+ file: 'meeting/index',
2844
+ function: 'setUpLocusInfoSelfListener',
2845
+ },
2846
+ payload.muted
2847
+ ? EVENT_TRIGGERS.MEETING_SELF_VIDEO_MUTED_BY_OTHERS
2848
+ : EVENT_TRIGGERS.MEETING_SELF_VIDEO_UNMUTED_BY_OTHERS,
2849
+ {
2850
+ payload,
2851
+ }
2852
+ );
2853
+ }
2854
+ });
2855
+
2199
2856
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED, (payload) => {
2200
2857
  if (payload) {
2201
2858
  if (this.audio) {
2202
- this.audio.handleServerRemoteMuteUpdate(payload.muted, payload.unmuteAllowed);
2859
+ this.audio.handleServerRemoteMuteUpdate(this, payload.muted, payload.unmuteAllowed);
2203
2860
  }
2204
2861
  // with "mute on entry" server will send us remote mute even if we don't have media configured,
2205
2862
  // so if being muted by others, always send the notification,
@@ -2322,6 +2979,51 @@ export default class Meeting extends StatelessWebexPlugin {
2322
2979
  );
2323
2980
  });
2324
2981
 
2982
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_BREAKOUTS_CHANGED, (payload) => {
2983
+ this.breakouts.updateBreakoutSessions(payload);
2984
+ Trigger.trigger(
2985
+ this,
2986
+ {
2987
+ file: 'meeting/index',
2988
+ function: 'setUpLocusInfoSelfListener',
2989
+ },
2990
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_UPDATE
2991
+ );
2992
+ });
2993
+
2994
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_INTERPRETATION_CHANGED, (payload) => {
2995
+ this.simultaneousInterpretation.updateSelfInterpretation(payload);
2996
+ Trigger.trigger(
2997
+ this,
2998
+ {
2999
+ file: 'meeting/index',
3000
+ function: 'setUpLocusInfoSelfListener',
3001
+ },
3002
+ EVENT_TRIGGERS.MEETING_INTERPRETATION_UPDATE
3003
+ );
3004
+ });
3005
+
3006
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ROLES_CHANGED, (payload) => {
3007
+ const isModeratorOrCohost =
3008
+ payload.newRoles?.includes(SELF_ROLES.MODERATOR) ||
3009
+ payload.newRoles?.includes(SELF_ROLES.COHOST);
3010
+ this.breakouts.updateCanManageBreakouts(isModeratorOrCohost);
3011
+ this.simultaneousInterpretation.updateCanManageInterpreters(
3012
+ payload.newRoles?.includes(SELF_ROLES.MODERATOR)
3013
+ );
3014
+ Trigger.trigger(
3015
+ this,
3016
+ {
3017
+ file: 'meeting/index',
3018
+ function: 'setUpLocusInfoSelfListener',
3019
+ },
3020
+ EVENT_TRIGGERS.MEETING_SELF_ROLES_CHANGED,
3021
+ {
3022
+ payload,
3023
+ }
3024
+ );
3025
+ });
3026
+
2325
3027
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_IS_SHARING_BLOCKED_CHANGE, (payload) => {
2326
3028
  Trigger.trigger(
2327
3029
  this,
@@ -2357,19 +3059,18 @@ export default class Meeting extends StatelessWebexPlugin {
2357
3059
  .catch((error) => {
2358
3060
  // @ts-ignore
2359
3061
  LoggerProxy.logger.error(
2360
- `Meeting:index#setUpLocusInfoMeetingListener --> REMOTE_RESPONSE. Issue with leave for meeting, meeting still in collection: ${this.meeting}, error: ${error}`
3062
+ `Meeting:index#setUpLocusInfoMeetingListener --> REMOTE_RESPONSE. Issue with leave for meeting, meeting still in collection: ${this}, error: ${error}`
2361
3063
  );
2362
3064
  });
2363
3065
  }
2364
3066
  });
2365
- this.locusInfo.on(EVENTS.DESTROY_MEETING, (payload) => {
3067
+ this.locusInfo.on(EVENTS.DESTROY_MEETING, async (payload) => {
2366
3068
  // if self state is NOT left
2367
3069
 
2368
3070
  // TODO: Handle sharing and wireless sharing when meeting end
2369
3071
  if (this.wirelessShare) {
2370
3072
  if (this.mediaProperties.shareTrack) {
2371
- this.mediaProperties.shareTrack.onended = null;
2372
- this.mediaProperties.shareTrack.stop();
3073
+ await this.setLocalShareTrack(undefined);
2373
3074
  }
2374
3075
  }
2375
3076
  // when multiple WEB deviceType join with same user
@@ -2383,18 +3084,18 @@ export default class Meeting extends StatelessWebexPlugin {
2383
3084
  if (payload.shouldLeave) {
2384
3085
  // TODO: We should do cleaning of meeting object if the shouldLeave: false because there might be meeting object which we are not cleaning
2385
3086
 
2386
- this.leave({reason: payload.reason})
2387
- .then(() => {
2388
- LoggerProxy.logger.warn(
2389
- 'Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. The meeting has been left, but has not been destroyed, you should see a later event for leave.'
2390
- );
2391
- })
2392
- .catch((error) => {
2393
- // @ts-ignore
2394
- LoggerProxy.logger.error(
2395
- `Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. Issue with leave for meeting, meeting still in collection: ${this.meeting}, error: ${error}`
2396
- );
2397
- });
3087
+ try {
3088
+ await this.leave({reason: payload.reason});
3089
+
3090
+ LoggerProxy.logger.warn(
3091
+ 'Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. The meeting has been left, but has not been destroyed, you should see a later event for leave.'
3092
+ );
3093
+ } catch (error) {
3094
+ // @ts-ignore
3095
+ LoggerProxy.logger.error(
3096
+ `Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. Issue with leave for meeting, meeting still in collection: ${this}, error: ${error}`
3097
+ );
3098
+ }
2398
3099
  } else {
2399
3100
  LoggerProxy.logger.info(
2400
3101
  'Meeting:index#setUpLocusInfoMeetingListener --> MEETING_REMOVED_REASON',
@@ -2472,14 +3173,32 @@ export default class Meeting extends StatelessWebexPlugin {
2472
3173
  }
2473
3174
 
2474
3175
  /**
2475
- * Admit the guest(s) to the call once they are waiting
3176
+ * Admit the guest(s) to the call once they are waiting.
3177
+ * If the host/cohost is in a breakout session, the locus url
3178
+ * of the session must be provided as the authorizingLocusUrl.
3179
+ * Regardless of host/cohost location, the locus Id (lid) in
3180
+ * the path should be the locus Id of the main, which means the
3181
+ * locus url of the api call must be from the main session.
3182
+ * If these loucs urls are not provided, the function will do the check.
2476
3183
  * @param {Array} memberIds
3184
+ * @param {Object} sessionLocusUrls: {authorizingLocusUrl, mainLocusUrl}
2477
3185
  * @returns {Promise} see #members.admitMembers
2478
3186
  * @public
2479
3187
  * @memberof Meeting
2480
3188
  */
2481
- public admit(memberIds: Array<any>) {
2482
- return this.members.admitMembers(memberIds);
3189
+ public admit(
3190
+ memberIds: Array<any>,
3191
+ sessionLocusUrls?: {authorizingLocusUrl: string; mainLocusUrl: string}
3192
+ ) {
3193
+ let locusUrls = sessionLocusUrls;
3194
+ if (!locusUrls) {
3195
+ const {locusUrl, mainLocusUrl} = this.breakouts;
3196
+ if (locusUrl && mainLocusUrl) {
3197
+ locusUrls = {authorizingLocusUrl: locusUrl, mainLocusUrl};
3198
+ }
3199
+ }
3200
+
3201
+ return this.members.admitMembers(memberIds, locusUrls);
2483
3202
  }
2484
3203
 
2485
3204
  /**
@@ -2527,66 +3246,6 @@ export default class Meeting extends StatelessWebexPlugin {
2527
3246
  return this.members;
2528
3247
  }
2529
3248
 
2530
- /**
2531
- * Truthy when a meeting has an audio connection established
2532
- * @returns {Boolean} true if meeting audio is connected otherwise false
2533
- * @public
2534
- * @memberof Meeting
2535
- */
2536
- public isAudioConnected() {
2537
- return !!this.audio;
2538
- }
2539
-
2540
- /**
2541
- * Convenience function to tell whether a meeting is muted
2542
- * @returns {Boolean} if meeting audio muted or not
2543
- * @public
2544
- * @memberof Meeting
2545
- */
2546
- public isAudioMuted() {
2547
- return this.audio && this.audio.isMuted();
2548
- }
2549
-
2550
- /**
2551
- * Convenience function to tell if the end user last changed the audio state
2552
- * @returns {Boolean} if audio was manipulated by the end user
2553
- * @public
2554
- * @memberof Meeting
2555
- */
2556
- public isAudioSelf() {
2557
- return this.audio && this.audio.isSelf();
2558
- }
2559
-
2560
- /**
2561
- * Truthy when a meeting has a video connection established
2562
- * @returns {Boolean} true if meeting video connected otherwise false
2563
- * @public
2564
- * @memberof Meeting
2565
- */
2566
- public isVideoConnected() {
2567
- return !!this.video;
2568
- }
2569
-
2570
- /**
2571
- * Convenience function to tell whether video is muted
2572
- * @returns {Boolean} if meeting video is muted or not
2573
- * @public
2574
- * @memberof Meeting
2575
- */
2576
- public isVideoMuted() {
2577
- return this.video && this.video.isMuted();
2578
- }
2579
-
2580
- /**
2581
- * Convenience function to tell whether the end user changed the video state
2582
- * @returns {Boolean} if meeting video is muted or not
2583
- * @public
2584
- * @memberof Meeting
2585
- */
2586
- public isVideoSelf() {
2587
- return this.video && this.video.isSelf();
2588
- }
2589
-
2590
3249
  /**
2591
3250
  * Sets the meeting info on the class instance
2592
3251
  * @param {Object} meetingInfo
@@ -2634,6 +3293,7 @@ export default class Meeting extends StatelessWebexPlugin {
2634
3293
  this.locusUrl = locusMeetingObject?.url || webexMeetingInfo?.locusUrl || this.locusUrl;
2635
3294
  // @ts-ignore - config coming from registerPlugin
2636
3295
  this.setSipUri(
3296
+ // @ts-ignore
2637
3297
  this.config.experimental.enableUnifiedMeetings
2638
3298
  ? locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipUrl
2639
3299
  : locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipMeetingUri || this.sipUri
@@ -2650,35 +3310,8 @@ export default class Meeting extends StatelessWebexPlugin {
2650
3310
  webexMeetingInfo?.hostId ||
2651
3311
  this.owner;
2652
3312
  this.permissionToken = webexMeetingInfo?.permissionToken;
2653
- }
2654
- }
2655
-
2656
- /**
2657
- * Sets the first locus info on the class instance
2658
- * @param {Object} locus
2659
- * @param {String} locus.url
2660
- * @param {Array} locus.participants
2661
- * @param {Object} locus.self
2662
- * @returns {undefined}
2663
- * @private
2664
- * @memberof Meeting
2665
- */
2666
- private parseLocus(locus: {url: string; participants: Array<any>; self: object}) {
2667
- if (locus) {
2668
- this.locusUrl = locus.url;
2669
- // TODO: move this to parse participants module
2670
- this.setLocus(locus);
2671
-
2672
- // check if we can extract this info from partner
2673
- // Parsing of locus object must be finished at this state
2674
- if (locus.participants && locus.self) {
2675
- this.partner = MeetingUtil.getLocusPartner(locus.participants, locus.self);
2676
- }
2677
-
2678
- // For webex meeting the sipUrl gets updated in info parser
2679
- if (!this.sipUri && this.partner && this.type === _CALL_) {
2680
- this.setSipUri(this.partner.person.sipUrl || this.partner.person.id);
2681
- }
3313
+ // Need to populate environment when sending CA event
3314
+ this.environment = locusMeetingObject?.info.channel || webexMeetingInfo?.channel;
2682
3315
  }
2683
3316
  }
2684
3317
 
@@ -2708,7 +3341,7 @@ export default class Meeting extends StatelessWebexPlugin {
2708
3341
  * @private
2709
3342
  * @memberof Meeting
2710
3343
  */
2711
- private setLocus(
3344
+ setLocus(
2712
3345
  locus:
2713
3346
  | {
2714
3347
  mediaConnections: Array<any>;
@@ -2743,21 +3376,6 @@ export default class Meeting extends StatelessWebexPlugin {
2743
3376
  Trigger.trigger(this, options, EVENTS.REQUEST_UPLOAD_LOGS, this);
2744
3377
  }
2745
3378
 
2746
- /**
2747
- * Removes remote audio and video stream on the class instance and triggers an event
2748
- * to developers
2749
- * @returns {undefined}
2750
- * @public
2751
- * @memberof Meeting
2752
- * @deprecated after v1.89.3
2753
- */
2754
- public unsetRemoteStream() {
2755
- LoggerProxy.logger.warn(
2756
- 'Meeting:index#unsetRemoteStream --> [DEPRECATION WARNING]: unsetRemoteStream has been deprecated after v1.89.3'
2757
- );
2758
- this.mediaProperties.unsetRemoteMedia();
2759
- }
2760
-
2761
3379
  /**
2762
3380
  * Removes remote audio, video and share tracks from class instance's mediaProperties
2763
3381
  * @returns {undefined}
@@ -2842,257 +3460,124 @@ export default class Meeting extends StatelessWebexPlugin {
2842
3460
  }
2843
3461
 
2844
3462
  /**
2845
- * Emits the 'media:ready' event with a local stream that consists of 1 local audio and 1 local video track
2846
- * @returns {undefined}
2847
- * @private
2848
- * @memberof Meeting
3463
+ * Stores the reference to a new microphone track, sets up the required event listeners
3464
+ * on it, cleans up previous track, etc.
3465
+ *
3466
+ * @param {LocalMicrophoneTrack | null} localTrack local microphone track
3467
+ * @returns {Promise<void>}
2849
3468
  */
2850
- private sendLocalMediaReadyEvent() {
2851
- Trigger.trigger(
2852
- this,
2853
- {
2854
- file: 'meeting/index',
2855
- function: 'setLocalTracks',
2856
- },
2857
- EVENT_TRIGGERS.MEDIA_READY,
2858
- {
2859
- type: EVENT_TYPES.LOCAL,
2860
- stream: MediaUtil.createMediaStream([
2861
- this.mediaProperties.audioTrack,
2862
- this.mediaProperties.videoTrack,
2863
- ]),
2864
- }
2865
- );
2866
- }
3469
+ private async setLocalAudioTrack(localTrack?: LocalMicrophoneTrack) {
3470
+ const oldTrack = this.mediaProperties.audioTrack;
2867
3471
 
2868
- /**
2869
- * Sets the local audio track on the class and emits an event to the developer
2870
- * @param {MediaStreamTrack} audioTrack
2871
- * @param {Boolean} emitEvent if true, a media ready event is emitted to the developer
2872
- * @returns {undefined}
2873
- * @private
2874
- * @memberof Meeting
2875
- */
2876
- private setLocalAudioTrack(audioTrack: MediaStreamTrack, emitEvent = true) {
2877
- if (audioTrack) {
2878
- const settings = audioTrack.getSettings();
3472
+ oldTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3473
+ oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
2879
3474
 
2880
- this.mediaProperties.setMediaSettings('audio', {
2881
- echoCancellation: settings.echoCancellation,
2882
- noiseSuppression: settings.noiseSuppression,
2883
- });
3475
+ // we don't update this.mediaProperties.mediaDirection.sendAudio, because we always keep it as true to avoid extra SDP exchanges
3476
+ this.mediaProperties.setLocalAudioTrack(localTrack);
2884
3477
 
2885
- LoggerProxy.logger.log(
2886
- 'Meeting:index#setLocalAudioTrack --> Audio settings.',
2887
- JSON.stringify(this.mediaProperties.mediaSettings.audio)
2888
- );
2889
- this.mediaProperties.setLocalAudioTrack(audioTrack);
2890
- if (this.audio) this.audio.applyClientStateLocally(this);
2891
- }
3478
+ this.audio.handleLocalTrackChange(this);
2892
3479
 
2893
- if (emitEvent) {
2894
- this.sendLocalMediaReadyEvent();
3480
+ localTrack?.on(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3481
+ localTrack?.on(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3482
+
3483
+ if (!this.isMultistream || !localTrack) {
3484
+ // for multistream WCME automatically un-publishes the old track when we publish a new one
3485
+ await this.unpublishTrack(oldTrack);
2895
3486
  }
3487
+ await this.publishTrack(this.mediaProperties.audioTrack);
2896
3488
  }
2897
3489
 
2898
3490
  /**
2899
- * Sets the local video track on the class and emits an event to the developer
2900
- * @param {MediaStreamTrack} videoTrack
2901
- * @param {Boolean} emitEvent if true, a media ready event is emitted to the developer
2902
- * @returns {undefined}
2903
- * @private
2904
- * @memberof Meeting
3491
+ * Stores the reference to a new camera track, sets up the required event listeners
3492
+ * on it, cleans up previous track, etc.
3493
+ *
3494
+ * @param {LocalCameraTrack | null} localTrack local camera track
3495
+ * @returns {Promise<void>}
2905
3496
  */
2906
- private setLocalVideoTrack(videoTrack: MediaStreamTrack, emitEvent = true) {
2907
- if (videoTrack) {
2908
- const {aspectRatio, frameRate, height, width, deviceId} = videoTrack.getSettings();
2909
-
2910
- const {localQualityLevel} = this.mediaProperties;
3497
+ private async setLocalVideoTrack(localTrack?: LocalCameraTrack) {
3498
+ const oldTrack = this.mediaProperties.videoTrack;
2911
3499
 
2912
- if (Number(localQualityLevel.slice(0, -1)) > height) {
2913
- LoggerProxy.logger
2914
- .warn(`Meeting:index#setLocalVideoTrack --> Local video quality of ${localQualityLevel} not supported,
2915
- downscaling to highest possible resolution of ${height}p`);
3500
+ oldTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3501
+ oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
2916
3502
 
2917
- this.mediaProperties.setLocalQualityLevel(`${height}p`);
2918
- }
3503
+ // we don't update this.mediaProperties.mediaDirection.sendVideo, because we always keep it as true to avoid extra SDP exchanges
3504
+ this.mediaProperties.setLocalVideoTrack(localTrack);
2919
3505
 
2920
- this.mediaProperties.setLocalVideoTrack(videoTrack);
2921
- if (this.video) this.video.applyClientStateLocally(this);
3506
+ this.video.handleLocalTrackChange(this);
2922
3507
 
2923
- this.mediaProperties.setMediaSettings('video', {
2924
- aspectRatio,
2925
- frameRate,
2926
- height,
2927
- width,
2928
- });
2929
- // store and save the selected video input device
2930
- if (deviceId) {
2931
- this.mediaProperties.setVideoDeviceId(deviceId);
2932
- }
2933
- LoggerProxy.logger.log(
2934
- 'Meeting:index#setLocalVideoTrack --> Video settings.',
2935
- JSON.stringify(this.mediaProperties.mediaSettings.video)
2936
- );
2937
- }
3508
+ localTrack?.on(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3509
+ localTrack?.on(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
2938
3510
 
2939
- if (emitEvent) {
2940
- this.sendLocalMediaReadyEvent();
3511
+ if (!this.isMultistream || !localTrack) {
3512
+ // for multistream WCME automatically un-publishes the old track when we publish a new one
3513
+ await this.unpublishTrack(oldTrack);
2941
3514
  }
3515
+ await this.publishTrack(this.mediaProperties.videoTrack);
2942
3516
  }
2943
3517
 
2944
3518
  /**
2945
- * Sets the local media stream on the class and emits an event to the developer
2946
- * @param {Stream} localStream the local media stream
2947
- * @returns {undefined}
2948
- * @public
2949
- * @memberof Meeting
3519
+ * Stores the reference to a new screen share track, sets up the required event listeners
3520
+ * on it, cleans up previous track, etc.
3521
+ * It also sends the floor grant/release request.
3522
+ *
3523
+ * @param {LocalDisplayTrack | undefined} localDisplayTrack local camera track
3524
+ * @returns {Promise<void>}
2950
3525
  */
2951
- public setLocalTracks(localStream: any) {
2952
- if (localStream) {
2953
- const {audioTrack, videoTrack} = MeetingUtil.getTrack(localStream);
3526
+ private async setLocalShareTrack(localDisplayTrack?: LocalDisplayTrack) {
3527
+ const oldTrack = this.mediaProperties.shareTrack;
2954
3528
 
2955
- this.setLocalAudioTrack(audioTrack, false);
2956
- this.setLocalVideoTrack(videoTrack, false);
3529
+ oldTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3530
+ oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
2957
3531
 
2958
- this.sendLocalMediaReadyEvent();
2959
- }
2960
- }
3532
+ this.mediaProperties.setLocalShareTrack(localDisplayTrack);
2961
3533
 
2962
- /**
2963
- * Sets the local media stream on the class and emits an event to the developer
2964
- * @param {MediaStream} localShare the local media stream
2965
- * @returns {undefined}
2966
- * @public
2967
- * @memberof Meeting
2968
- */
2969
- public setLocalShareTrack(localShare: MediaStream) {
2970
- let settings = null;
2971
-
2972
- if (localShare) {
2973
- this.mediaProperties.setLocalShareTrack(MeetingUtil.getTrack(localShare).videoTrack);
2974
- const contentTracks = this.mediaProperties.shareTrack;
2975
-
2976
- if (contentTracks) {
2977
- settings = contentTracks.getSettings();
2978
- this.mediaProperties.setMediaSettings('screen', {
2979
- aspectRatio: settings.aspectRatio,
2980
- frameRate: settings.frameRate,
2981
- height: settings.height,
2982
- width: settings.width,
2983
- displaySurface: settings.displaySurface,
2984
- cursor: settings.cursor,
2985
- });
2986
- LoggerProxy.logger.log(
2987
- 'Meeting:index#setLocalShareTrack --> Screen settings.',
2988
- JSON.stringify(this.mediaProperties.mediaSettings.screen)
2989
- );
2990
- }
3534
+ localDisplayTrack?.on(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3535
+ localDisplayTrack?.on(
3536
+ LocalTrackEvents.UnderlyingTrackChange,
3537
+ this.underlyingLocalTrackChangeHandler
3538
+ );
2991
3539
 
2992
- contentTracks.onended = () => this.handleShareTrackEnded(localShare);
3540
+ this.mediaProperties.mediaDirection.sendShare = !!localDisplayTrack;
2993
3541
 
2994
- Trigger.trigger(
2995
- this,
2996
- {
2997
- file: 'meeting/index',
2998
- function: 'setLocalShareTrack',
2999
- },
3000
- EVENT_TRIGGERS.MEDIA_READY,
3001
- {
3002
- type: EVENT_TYPES.LOCAL_SHARE,
3003
- stream: localShare,
3004
- }
3005
- );
3542
+ if (!this.isMultistream || !localDisplayTrack) {
3543
+ // for multistream WCME automatically un-publishes the old track when we publish a new one
3544
+ await this.unpublishTrack(oldTrack);
3006
3545
  }
3546
+ await this.publishTrack(this.mediaProperties.shareTrack);
3007
3547
  }
3008
3548
 
3009
3549
  /**
3010
- * Closes the local stream from the class and emits an event to the developer
3011
- * @returns {undefined}
3012
- * @event media:stopped
3013
- * @public
3014
- * @memberof Meeting
3550
+ * Removes references to local tracks. This function should be called
3551
+ * on cleanup when we leave the meeting etc.
3552
+ *
3553
+ * @internal
3554
+ * @returns {void}
3015
3555
  */
3016
- public closeLocalStream() {
3017
- const {audioTrack, videoTrack} = this.mediaProperties;
3556
+ public cleanupLocalTracks() {
3557
+ const {audioTrack, videoTrack, shareTrack} = this.mediaProperties;
3018
3558
 
3019
- return Media.stopTracks(audioTrack)
3020
- .then(() => Media.stopTracks(videoTrack))
3021
- .then(() => {
3022
- const audioStopped = audioTrack && audioTrack.readyState === ENDED;
3023
- const videoStopped = videoTrack && videoTrack.readyState === ENDED;
3559
+ audioTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3560
+ audioTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3024
3561
 
3025
- // triggers event for audio and video stop , sometime either audio or video one of them exists
3026
- if (audioStopped || videoStopped) {
3027
- Trigger.trigger(
3028
- this,
3029
- {
3030
- file: 'meeting/index',
3031
- function: 'closeLocalStream',
3032
- },
3033
- EVENT_TRIGGERS.MEDIA_STOPPED,
3034
- {
3035
- type: EVENT_TYPES.LOCAL,
3036
- }
3037
- );
3038
- } else if (audioTrack || videoTrack) {
3039
- LoggerProxy.logger.warn(
3040
- 'Meeting:index#closeLocalStream --> Warning: track might already been ended or unavaliable.'
3041
- );
3042
- }
3043
- });
3044
- }
3562
+ videoTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3563
+ videoTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3045
3564
 
3046
- /**
3047
- * Closes the local stream from the class and emits an event to the developer
3048
- * @returns {undefined}
3049
- * @event media:stopped
3050
- * @public
3051
- * @memberof Meeting
3052
- */
3053
- public closeLocalShare() {
3054
- const track = this.mediaProperties.shareTrack;
3565
+ shareTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3566
+ shareTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3055
3567
 
3056
- return Media.stopTracks(track).then(() => {
3057
- if (track && track.readyState === ENDED) {
3058
- Trigger.trigger(
3059
- this,
3060
- {
3061
- file: 'meeting/index',
3062
- function: 'closeLocalShare',
3063
- },
3064
- EVENT_TRIGGERS.MEDIA_STOPPED,
3065
- {
3066
- type: EVENT_TYPES.LOCAL_SHARE,
3067
- }
3068
- );
3069
- } else if (track) {
3070
- // Track exists but with wrong readyState
3071
- LoggerProxy.logger.warn(
3072
- `Meeting:index#closeLocalShare --> Error: MediaStreamTrack.readyState is ${track.readyState} for localShare`
3073
- );
3074
- }
3075
- });
3076
- }
3568
+ this.mediaProperties.setLocalAudioTrack(undefined);
3569
+ this.mediaProperties.setLocalVideoTrack(undefined);
3570
+ this.mediaProperties.setLocalShareTrack(undefined);
3077
3571
 
3078
- /**
3079
- * Removes the local stream from the class and emits an event to the developer
3080
- * @returns {undefined}
3081
- * @public
3082
- * @memberof Meeting
3083
- */
3084
- public unsetLocalVideoTrack() {
3085
- this.mediaProperties.unsetLocalVideoTrack();
3086
- }
3572
+ this.mediaProperties.mediaDirection.sendAudio = false;
3573
+ this.mediaProperties.mediaDirection.sendVideo = false;
3574
+ this.mediaProperties.mediaDirection.sendShare = false;
3087
3575
 
3088
- /**
3089
- * Removes the local share from the class and emits an event to the developer
3090
- * @returns {undefined}
3091
- * @public
3092
- * @memberof Meeting
3093
- */
3094
- public unsetLocalShareTrack() {
3095
- this.mediaProperties.unsetLocalShareTrack();
3576
+ // WCME doesn't unpublish tracks when multistream connection is closed, so we do it here
3577
+ // (we have to do it for transcoded meetings anyway, so we might as well do for multistream too)
3578
+ audioTrack?.setPublished(false);
3579
+ videoTrack?.setPublished(false);
3580
+ shareTrack?.setPublished(false);
3096
3581
  }
3097
3582
 
3098
3583
  /**
@@ -3143,6 +3628,8 @@ export default class Meeting extends StatelessWebexPlugin {
3143
3628
  * @memberof Meeting
3144
3629
  */
3145
3630
  public closePeerConnections() {
3631
+ this.locusMediaRequest = undefined;
3632
+
3146
3633
  if (this.mediaProperties.webrtcMediaConnection) {
3147
3634
  if (this.remoteMediaManager) {
3148
3635
  this.remoteMediaManager.stop();
@@ -3157,6 +3644,9 @@ export default class Meeting extends StatelessWebexPlugin {
3157
3644
  this.mediaProperties.webrtcMediaConnection.close();
3158
3645
  }
3159
3646
 
3647
+ this.audio = null;
3648
+ this.video = null;
3649
+
3160
3650
  return Promise.resolve();
3161
3651
  }
3162
3652
 
@@ -3187,264 +3677,36 @@ export default class Meeting extends StatelessWebexPlugin {
3187
3677
  this.correlationId = id;
3188
3678
  }
3189
3679
 
3190
- /**
3191
- * Mute the audio for a meeting
3192
- * @returns {Promise} resolves the data from muting audio {mute, self} or rejects if there is no audio set
3193
- * @public
3194
- * @memberof Meeting
3195
- */
3196
- public muteAudio() {
3197
- if (!MeetingUtil.isUserInJoinedState(this.locusInfo)) {
3198
- return Promise.reject(new UserNotJoinedError());
3199
- }
3200
-
3201
- // @ts-ignore
3202
- if (!this.mediaId) {
3203
- // Happens when addMedia and mute are triggered in succession
3204
- return Promise.reject(new NoMediaEstablishedYetError());
3205
- }
3206
-
3207
- if (!this.audio) {
3208
- return Promise.reject(new ParameterError('no audio control associated to the meeting'));
3209
- }
3210
-
3211
- const LOG_HEADER = 'Meeting:index#muteAudio -->';
3212
-
3213
- // First, stop sending the local audio media
3214
- return logRequest(
3215
- this.audio
3216
- .handleClientRequest(this, true)
3217
- .then(() => {
3218
- MeetingUtil.handleAudioLogging(this.mediaProperties.audioTrack);
3219
- Metrics.postEvent({
3220
- event: eventType.MUTED,
3221
- meeting: this,
3222
- data: {trigger: trigger.USER_INTERACTION, mediaType: mediaType.AUDIO},
3223
- });
3224
- })
3225
- .catch((error) => {
3226
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MUTE_AUDIO_FAILURE, {
3227
- correlation_id: this.correlationId,
3228
- locus_id: this.locusUrl.split('/').pop(),
3229
- reason: error.message,
3230
- stack: error.stack,
3231
- });
3232
-
3233
- throw error;
3234
- }),
3235
- {
3236
- header: `${LOG_HEADER} muting audio`,
3237
- success: `${LOG_HEADER} muted audio successfully`,
3238
- failure: `${LOG_HEADER} muting audio failed, `,
3239
- }
3240
- );
3241
- }
3242
-
3243
- /**
3244
- * Unmute meeting audio
3245
- * @returns {Promise} resolves data from muting audio {mute, self} or rejects if there is no audio set
3246
- * @public
3247
- * @memberof Meeting
3248
- */
3249
- public unmuteAudio() {
3250
- if (!MeetingUtil.isUserInJoinedState(this.locusInfo)) {
3251
- return Promise.reject(new UserNotJoinedError());
3252
- }
3253
-
3254
- // @ts-ignore
3255
- if (!this.mediaId) {
3256
- // Happens when addMedia and mute are triggered in succession
3257
- return Promise.reject(new NoMediaEstablishedYetError());
3258
- }
3259
-
3260
- if (!this.audio) {
3261
- return Promise.reject(new ParameterError('no audio control associated to the meeting'));
3262
- }
3263
-
3264
- const LOG_HEADER = 'Meeting:index#unmuteAudio -->';
3265
-
3266
- // First, send the control to unmute the participant on the server
3267
- return logRequest(
3268
- this.audio
3269
- .handleClientRequest(this, false)
3270
- .then(() => {
3271
- MeetingUtil.handleAudioLogging(this.mediaProperties.audioTrack);
3272
- Metrics.postEvent({
3273
- event: eventType.UNMUTED,
3274
- meeting: this,
3275
- data: {trigger: trigger.USER_INTERACTION, mediaType: mediaType.AUDIO},
3276
- });
3277
- })
3278
- .catch((error) => {
3279
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.UNMUTE_AUDIO_FAILURE, {
3280
- correlation_id: this.correlationId,
3281
- locus_id: this.locusUrl.split('/').pop(),
3282
- reason: error.message,
3283
- stack: error.stack,
3284
- });
3285
-
3286
- throw error;
3287
- }),
3288
- {
3289
- header: `${LOG_HEADER} unmuting audio`,
3290
- success: `${LOG_HEADER} unmuted audio successfully`,
3291
- failure: `${LOG_HEADER} unmuting audio failed, `,
3292
- }
3293
- );
3294
- }
3295
-
3296
- /**
3297
- * Mute the video for a meeting
3298
- * @returns {Promise} resolves data from muting video {mute, self} or rejects if there is no video set
3299
- * @public
3300
- * @memberof Meeting
3301
- */
3302
- public muteVideo() {
3303
- if (!MeetingUtil.isUserInJoinedState(this.locusInfo)) {
3304
- return Promise.reject(new UserNotJoinedError());
3305
- }
3306
-
3307
- // @ts-ignore
3308
- if (!this.mediaId) {
3309
- // Happens when addMedia and mute are triggered in succession
3310
- return Promise.reject(new NoMediaEstablishedYetError());
3311
- }
3312
-
3313
- if (!this.video) {
3314
- return Promise.reject(new ParameterError('no video control associated to the meeting'));
3315
- }
3316
-
3317
- const LOG_HEADER = 'Meeting:index#muteVideo -->';
3318
-
3319
- return logRequest(
3320
- this.video
3321
- .handleClientRequest(this, true)
3322
- .then(() => {
3323
- MeetingUtil.handleVideoLogging(this.mediaProperties.videoTrack);
3324
- Metrics.postEvent({
3325
- event: eventType.MUTED,
3326
- meeting: this,
3327
- data: {trigger: trigger.USER_INTERACTION, mediaType: mediaType.VIDEO},
3328
- });
3329
- })
3330
- .catch((error) => {
3331
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MUTE_VIDEO_FAILURE, {
3332
- correlation_id: this.correlationId,
3333
- locus_id: this.locusUrl.split('/').pop(),
3334
- reason: error.message,
3335
- stack: error.stack,
3336
- });
3337
-
3338
- throw error;
3339
- }),
3340
- {
3341
- header: `${LOG_HEADER} muting video`,
3342
- success: `${LOG_HEADER} muted video successfully`,
3343
- failure: `${LOG_HEADER} muting video failed, `,
3344
- }
3345
- );
3346
- }
3347
-
3348
- /**
3349
- * Unmute meeting video
3350
- * @returns {Promise} resolves data from muting video {mute, self} or rejects if there is no video set
3351
- * @public
3352
- * @memberof Meeting
3353
- */
3354
- public unmuteVideo() {
3355
- if (!MeetingUtil.isUserInJoinedState(this.locusInfo)) {
3356
- return Promise.reject(new UserNotJoinedError());
3357
- }
3358
-
3359
- // @ts-ignore
3360
- if (!this.mediaId) {
3361
- // Happens when addMedia and mute are triggered in succession
3362
- return Promise.reject(new NoMediaEstablishedYetError());
3363
- }
3364
-
3365
- if (!this.video) {
3366
- return Promise.reject(new ParameterError('no audio control associated to the meeting'));
3367
- }
3368
-
3369
- const LOG_HEADER = 'Meeting:index#unmuteVideo -->';
3370
-
3371
- return logRequest(
3372
- this.video
3373
- .handleClientRequest(this, false)
3374
- .then(() => {
3375
- MeetingUtil.handleVideoLogging(this.mediaProperties.videoTrack);
3376
- Metrics.postEvent({
3377
- event: eventType.UNMUTED,
3378
- meeting: this,
3379
- data: {trigger: trigger.USER_INTERACTION, mediaType: mediaType.VIDEO},
3380
- });
3381
- })
3382
- .catch((error) => {
3383
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.UNMUTE_VIDEO_FAILURE, {
3384
- correlation_id: this.correlationId,
3385
- locus_id: this.locusUrl.split('/').pop(),
3386
- reason: error.message,
3387
- stack: error.stack,
3388
- });
3389
-
3390
- throw error;
3391
- }),
3392
- {
3393
- header: `${LOG_HEADER} unmuting video`,
3394
- success: `${LOG_HEADER} unmuted video successfully`,
3395
- failure: `${LOG_HEADER} unmuting video failed, `,
3396
- }
3397
- );
3398
- }
3399
-
3400
3680
  /**
3401
3681
  * Shorthand function to join AND set up media
3402
3682
  * @param {Object} options - options to join with media
3403
3683
  * @param {JoinOptions} [options.joinOptions] - see #join()
3404
- * @param {MediaDirection} options.mediaSettings - see #addMedia()
3405
- * @param {AudioVideo} [options.audioVideoOptions] - see #getMediaStreams()
3406
- * @returns {Promise} -- {join: see join(), media: see addMedia(), local: see getMediaStreams()}
3684
+ * @param {MediaDirection} [options.mediaOptions] - see #addMedia()
3685
+ * @returns {Promise} -- {join: see join(), media: see addMedia()}
3407
3686
  * @public
3408
3687
  * @memberof Meeting
3409
3688
  * @example
3410
3689
  * joinWithMedia({
3411
3690
  * joinOptions: {resourceId: 'resourceId' },
3412
- * mediaSettings: {
3413
- * sendAudio: true,
3414
- * sendVideo: true,
3415
- * sendShare: false,
3416
- * receiveVideo:true,
3417
- * receiveAudio: true,
3418
- * receiveShare: true
3419
- * }
3420
- * audioVideoOptions: {
3421
- * audio: 'audioDeviceId',
3422
- * video: 'videoDeviceId'
3423
- * }})
3691
+ * mediaOptions: {
3692
+ * localTracks: { microphone: microphoneTrack, camera: cameraTrack }
3693
+ * }
3694
+ * })
3424
3695
  */
3425
3696
  public joinWithMedia(
3426
3697
  options: {
3427
3698
  joinOptions?: any;
3428
- mediaSettings: any;
3429
- audioVideoOptions?: any;
3430
- } = {} as any
3699
+ mediaOptions?: AddMediaOptions;
3700
+ } = {}
3431
3701
  ) {
3432
- // TODO: add validations for parameters
3433
- const {mediaSettings, joinOptions, audioVideoOptions} = options;
3702
+ const {mediaOptions, joinOptions} = options;
3434
3703
 
3435
3704
  return this.join(joinOptions)
3436
3705
  .then((joinResponse) =>
3437
- this.getMediaStreams(mediaSettings, audioVideoOptions).then(([localStream, localShare]) =>
3438
- this.addMedia({
3439
- mediaSettings,
3440
- localShare,
3441
- localStream,
3442
- }).then((mediaResponse) => ({
3443
- join: joinResponse,
3444
- media: mediaResponse,
3445
- local: [localStream, localShare],
3446
- }))
3447
- )
3706
+ this.addMedia(mediaOptions).then((mediaResponse) => ({
3707
+ join: joinResponse,
3708
+ media: mediaResponse,
3709
+ }))
3448
3710
  )
3449
3711
  .catch((error) => {
3450
3712
  LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
@@ -3582,6 +3844,20 @@ export default class Meeting extends StatelessWebexPlugin {
3582
3844
  return false;
3583
3845
  }
3584
3846
 
3847
+ /**
3848
+ * Check if the meeting supports the Reactions
3849
+ * @returns {boolean}
3850
+ */
3851
+ isReactionsSupported() {
3852
+ if (this.locusInfo?.controls?.reactions.enabled) {
3853
+ return true;
3854
+ }
3855
+
3856
+ LoggerProxy.logger.error('Meeting:index#isReactionsSupported --> Reactions is not supported');
3857
+
3858
+ return false;
3859
+ }
3860
+
3585
3861
  /**
3586
3862
  * Monitor the Low-Latency Mercury (LLM) web socket connection on `onError` and `onClose` states
3587
3863
  * @private
@@ -3633,6 +3909,7 @@ export default class Meeting extends StatelessWebexPlugin {
3633
3909
  // @ts-ignore - fix type
3634
3910
  const {
3635
3911
  body: {webSocketUrl},
3912
+ // @ts-ignore
3636
3913
  } = await this.request({
3637
3914
  method: HTTP_VERBS.POST,
3638
3915
  uri: datachannelUrl,
@@ -3682,6 +3959,44 @@ export default class Meeting extends StatelessWebexPlugin {
3682
3959
  }
3683
3960
  }
3684
3961
 
3962
+ /**
3963
+ * Callback called when a relay event is received from meeting LLM Connection
3964
+ * @param {RelayEvent} e Event object coming from LLM Connection
3965
+ * @private
3966
+ * @returns {void}
3967
+ */
3968
+ private processRelayEvent = (e: RelayEvent): void => {
3969
+ switch (e.data.relayType) {
3970
+ case REACTION_RELAY_TYPES.REACTION:
3971
+ if (
3972
+ // @ts-ignore - config coming from registerPlugin
3973
+ (this.config.receiveReactions || options.receiveReactions) &&
3974
+ this.isReactionsSupported()
3975
+ ) {
3976
+ const {name} = this.members.membersCollection.get(e.data.sender.participantId);
3977
+ const processedReaction: ProcessedReaction = {
3978
+ reaction: e.data.reaction,
3979
+ sender: {
3980
+ id: e.data.sender.participantId,
3981
+ name,
3982
+ },
3983
+ };
3984
+ Trigger.trigger(
3985
+ this,
3986
+ {
3987
+ file: 'meeting/index',
3988
+ function: 'join',
3989
+ },
3990
+ EVENT_TRIGGERS.MEETING_RECEIVE_REACTIONS,
3991
+ processedReaction
3992
+ );
3993
+ }
3994
+ break;
3995
+ default:
3996
+ break;
3997
+ }
3998
+ };
3999
+
3685
4000
  /**
3686
4001
  * stop recieving Transcription by closing
3687
4002
  * the web socket connection properly
@@ -3753,9 +4068,16 @@ export default class Meeting extends StatelessWebexPlugin {
3753
4068
  joinSuccess = resolve;
3754
4069
  });
3755
4070
 
4071
+ if (options.correlationId) {
4072
+ this.setCorrelationId(options.correlationId);
4073
+ LoggerProxy.logger.log(
4074
+ `Meeting:index#join --> Using a new correlation id from app ${this.correlationId}`
4075
+ );
4076
+ }
4077
+
3756
4078
  if (!this.hasJoinedOnce) {
3757
4079
  this.hasJoinedOnce = true;
3758
- } else {
4080
+ } else if (!options.correlationId) {
3759
4081
  LoggerProxy.logger.log(
3760
4082
  `Meeting:index#join --> Generating a new correlation id for meeting ${this.id}`
3761
4083
  );
@@ -3776,10 +4098,25 @@ export default class Meeting extends StatelessWebexPlugin {
3776
4098
  data: {trigger: trigger.USER_INTERACTION, isRoapCallEnabled: true},
3777
4099
  });
3778
4100
 
3779
- LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
4101
+ if (!isEmpty(this.meetingInfo)) {
4102
+ Metrics.postEvent({
4103
+ event: eventType.MEETING_INFO_REQUEST,
4104
+ meeting: this,
4105
+ });
3780
4106
 
3781
- if (this.meetingFiniteStateMachine.state === MEETING_STATE_MACHINE.STATES.ENDED) {
3782
- this.meetingFiniteStateMachine.reset();
4107
+ Metrics.postEvent({
4108
+ event: eventType.MEETING_INFO_RESPONSE,
4109
+ meeting: this,
4110
+ data: {
4111
+ meetingLookupUrl: this.meetingInfo?.meetingLookupUrl,
4112
+ },
4113
+ });
4114
+ }
4115
+
4116
+ LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
4117
+
4118
+ if (this.meetingFiniteStateMachine.state === MEETING_STATE_MACHINE.STATES.ENDED) {
4119
+ this.meetingFiniteStateMachine.reset();
3783
4120
  }
3784
4121
  if (this.meetingFiniteStateMachine.state !== MEETING_STATE_MACHINE.STATES.RINGING) {
3785
4122
  this.meetingFiniteStateMachine.ring(_JOIN_);
@@ -3804,18 +4141,12 @@ export default class Meeting extends StatelessWebexPlugin {
3804
4141
  return Promise.reject(error);
3805
4142
  }
3806
4143
 
3807
- this.mediaProperties.setLocalQualityLevel(options.meetingQuality);
3808
4144
  this.mediaProperties.setRemoteQualityLevel(options.meetingQuality);
3809
4145
  }
3810
4146
 
3811
4147
  if (typeof options.meetingQuality === 'object') {
3812
- if (
3813
- !QUALITY_LEVELS[options.meetingQuality.local] &&
3814
- !QUALITY_LEVELS[options.meetingQuality.remote]
3815
- ) {
3816
- const errorMessage = `Meeting:index#join --> ${
3817
- options.meetingQuality.local || options.meetingQuality.remote
3818
- } not defined`;
4148
+ if (!QUALITY_LEVELS[options.meetingQuality.remote]) {
4149
+ const errorMessage = `Meeting:index#join --> ${options.meetingQuality.remote} not defined`;
3819
4150
 
3820
4151
  LoggerProxy.logger.error(errorMessage);
3821
4152
 
@@ -3827,9 +4158,6 @@ export default class Meeting extends StatelessWebexPlugin {
3827
4158
  return Promise.reject(new Error(errorMessage));
3828
4159
  }
3829
4160
 
3830
- if (options.meetingQuality.local) {
3831
- this.mediaProperties.setLocalQualityLevel(options.meetingQuality.local);
3832
- }
3833
4161
  if (options.meetingQuality.remote) {
3834
4162
  this.mediaProperties.setRemoteQualityLevel(options.meetingQuality.remote);
3835
4163
  }
@@ -3855,6 +4183,7 @@ export default class Meeting extends StatelessWebexPlugin {
3855
4183
  return join;
3856
4184
  })
3857
4185
  .then(async (join) => {
4186
+ // @ts-ignore - config coming from registerPlugin
3858
4187
  if (this.config.enableAutomaticLLM) {
3859
4188
  await this.updateLLMConnection();
3860
4189
  }
@@ -3923,22 +4252,41 @@ export default class Meeting extends StatelessWebexPlugin {
3923
4252
  * @returns {Promise}
3924
4253
  */
3925
4254
  async updateLLMConnection() {
4255
+ // @ts-ignore - Fix type
3926
4256
  const {url, info: {datachannelUrl} = {}} = this.locusInfo;
3927
4257
 
3928
- const isJoined = this.joinedWith && this.joinedWith.state === 'JOINED';
4258
+ const isJoined = this.isJoined();
3929
4259
 
4260
+ // @ts-ignore - Fix type
3930
4261
  if (this.webex.internal.llm.isConnected()) {
4262
+ // @ts-ignore - Fix type
3931
4263
  if (url === this.webex.internal.llm.getLocusUrl() && isJoined) {
3932
4264
  return undefined;
3933
4265
  }
4266
+ // @ts-ignore - Fix type
3934
4267
  await this.webex.internal.llm.disconnectLLM();
4268
+ // @ts-ignore - Fix type
4269
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
3935
4270
  }
3936
4271
 
3937
4272
  if (!isJoined) {
3938
4273
  return undefined;
3939
4274
  }
3940
4275
 
3941
- return this.webex.internal.llm.registerAndConnect(url, datachannelUrl);
4276
+ // @ts-ignore - Fix type
4277
+ return this.webex.internal.llm
4278
+ .registerAndConnect(url, datachannelUrl)
4279
+ .then((registerAndConnectResult) => {
4280
+ // @ts-ignore - Fix type
4281
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
4282
+ // @ts-ignore - Fix type
4283
+ this.webex.internal.llm.on('event:relay.event', this.processRelayEvent);
4284
+ LoggerProxy.logger.info(
4285
+ 'Meeting:index#updateLLMConnection --> enabled to receive relay events!'
4286
+ );
4287
+
4288
+ return Promise.resolve(registerAndConnectResult);
4289
+ });
3942
4290
  }
3943
4291
 
3944
4292
  /**
@@ -3980,28 +4328,28 @@ export default class Meeting extends StatelessWebexPlugin {
3980
4328
 
3981
4329
  if (!this.dialInUrl) this.dialInUrl = `dialin:///${uuid.v4()}`;
3982
4330
 
3983
- return this.meetingRequest
3984
- .dialIn({
3985
- correlationId,
3986
- dialInUrl: this.dialInUrl,
3987
- locusUrl,
3988
- clientUrl: this.deviceUrl,
3989
- })
3990
- .then((res) => {
3991
- this.locusInfo.onFullLocus(res.body.locus);
3992
- })
3993
- .catch((error) => {
3994
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_DIAL_IN_FAILURE, {
3995
- correlation_id: this.correlationId,
3996
- dial_in_url: this.dialInUrl,
3997
- locus_id: locusUrl.split('/').pop(),
3998
- client_url: this.deviceUrl,
3999
- reason: error.error?.message,
4000
- stack: error.stack,
4001
- });
4331
+ return (
4332
+ this.meetingRequest
4333
+ // @ts-ignore
4334
+ .dialIn({
4335
+ correlationId,
4336
+ dialInUrl: this.dialInUrl,
4337
+ locusUrl,
4338
+ clientUrl: this.deviceUrl,
4339
+ })
4340
+ .catch((error) => {
4341
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_DIAL_IN_FAILURE, {
4342
+ correlation_id: this.correlationId,
4343
+ dial_in_url: this.dialInUrl,
4344
+ locus_id: locusUrl.split('/').pop(),
4345
+ client_url: this.deviceUrl,
4346
+ reason: error.error?.message,
4347
+ stack: error.stack,
4348
+ });
4002
4349
 
4003
- return Promise.reject(error);
4004
- });
4350
+ return Promise.reject(error);
4351
+ })
4352
+ );
4005
4353
  }
4006
4354
 
4007
4355
  /**
@@ -4018,29 +4366,29 @@ export default class Meeting extends StatelessWebexPlugin {
4018
4366
 
4019
4367
  if (!this.dialOutUrl) this.dialOutUrl = `dialout:///${uuid.v4()}`;
4020
4368
 
4021
- return this.meetingRequest
4022
- .dialOut({
4023
- correlationId,
4024
- dialOutUrl: this.dialOutUrl,
4025
- phoneNumber,
4026
- locusUrl,
4027
- clientUrl: this.deviceUrl,
4028
- })
4029
- .then((res) => {
4030
- this.locusInfo.onFullLocus(res.body.locus);
4031
- })
4032
- .catch((error) => {
4033
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_DIAL_OUT_FAILURE, {
4034
- correlation_id: this.correlationId,
4035
- dial_out_url: this.dialOutUrl,
4036
- locus_id: locusUrl.split('/').pop(),
4037
- client_url: this.deviceUrl,
4038
- reason: error.error?.message,
4039
- stack: error.stack,
4040
- });
4369
+ return (
4370
+ this.meetingRequest
4371
+ // @ts-ignore
4372
+ .dialOut({
4373
+ correlationId,
4374
+ dialOutUrl: this.dialOutUrl,
4375
+ phoneNumber,
4376
+ locusUrl,
4377
+ clientUrl: this.deviceUrl,
4378
+ })
4379
+ .catch((error) => {
4380
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_DIAL_OUT_FAILURE, {
4381
+ correlation_id: this.correlationId,
4382
+ dial_out_url: this.dialOutUrl,
4383
+ locus_id: locusUrl.split('/').pop(),
4384
+ client_url: this.deviceUrl,
4385
+ reason: error.error?.message,
4386
+ stack: error.stack,
4387
+ });
4041
4388
 
4042
- return Promise.reject(error);
4043
- });
4389
+ return Promise.reject(error);
4390
+ })
4391
+ );
4044
4392
  }
4045
4393
 
4046
4394
  /**
@@ -4116,14 +4464,10 @@ export default class Meeting extends StatelessWebexPlugin {
4116
4464
  },
4117
4465
  };
4118
4466
 
4119
- // clean up the local tracks
4120
- this.mediaProperties.setMediaDirection(mediaSettings.mediaDirection);
4121
-
4122
- // close the existing local tracks
4123
- await this.closeLocalStream();
4124
- await this.closeLocalShare();
4467
+ this.cleanupLocalTracks();
4125
4468
 
4126
- this.mediaProperties.unsetMediaTracks();
4469
+ this.mediaProperties.setMediaDirection(mediaSettings.mediaDirection);
4470
+ this.mediaProperties.unsetRemoteMedia();
4127
4471
 
4128
4472
  // when a move to is intiated by the client , Locus delets the existing media node from the server as soon the DX answers the meeting
4129
4473
  // once the DX answers we establish connection back the media server with only receiveShare enabled
@@ -4206,165 +4550,6 @@ export default class Meeting extends StatelessWebexPlugin {
4206
4550
  });
4207
4551
  }
4208
4552
 
4209
- /**
4210
- * Get local media streams based on options passed
4211
- *
4212
- * NOTE: this method can only be used with transcoded meetings, not with multistream meetings
4213
- *
4214
- * @param {MediaDirection} mediaDirection A configurable options object for joining a meeting
4215
- * @param {AudioVideo} [audioVideo] audio/video object to set audioinput and videoinput devices, see #Media.getUserMedia
4216
- * @param {SharePreferences} [sharePreferences] audio/video object to set audioinput and videoinput devices, see #Media.getUserMedia
4217
- * @returns {Promise} see #Media.getUserMedia
4218
- * @public
4219
- * @todo should be static, or moved so can be called outside of a meeting
4220
- * @memberof Meeting
4221
- */
4222
- getMediaStreams = (
4223
- mediaDirection: any,
4224
- // This return an OBJECT {video: {height, widght}}
4225
- // eslint-disable-next-line default-param-last
4226
- audioVideo: any = VIDEO_RESOLUTIONS[this.mediaProperties.localQualityLevel],
4227
- sharePreferences?: any
4228
- ) => {
4229
- if (
4230
- mediaDirection &&
4231
- (mediaDirection.sendAudio || mediaDirection.sendVideo || mediaDirection.sendShare)
4232
- ) {
4233
- if (
4234
- mediaDirection &&
4235
- mediaDirection.sendAudio &&
4236
- mediaDirection.sendVideo &&
4237
- mediaDirection.sendShare &&
4238
- isBrowser('safari')
4239
- ) {
4240
- LoggerProxy.logger.warn(
4241
- 'Meeting:index#getMediaStreams --> Setting `sendShare` to FALSE, due to complications with Safari'
4242
- );
4243
-
4244
- mediaDirection.sendShare = false;
4245
-
4246
- LoggerProxy.logger.warn(
4247
- 'Meeting:index#getMediaStreams --> Enabling `sendShare` along with `sendAudio` & `sendVideo`, on Safari, causes a failure while setting up a screen share at the same time as the camera+mic stream'
4248
- );
4249
- LoggerProxy.logger.warn(
4250
- 'Meeting:index#getMediaStreams --> Please use `meeting.shareScreen()` to manually start the screen share after successfully joining the meeting'
4251
- );
4252
- }
4253
-
4254
- if (audioVideo && isString(audioVideo)) {
4255
- if (Object.keys(VIDEO_RESOLUTIONS).includes(audioVideo)) {
4256
- this.mediaProperties.setLocalQualityLevel(audioVideo);
4257
- audioVideo = {video: VIDEO_RESOLUTIONS[audioVideo].video};
4258
- } else {
4259
- throw new ParameterError(
4260
- `${audioVideo} not supported. Either pass level from pre-defined resolutions or pass complete audioVideo object`
4261
- );
4262
- }
4263
- }
4264
-
4265
- if (!audioVideo.video) {
4266
- audioVideo = {
4267
- ...audioVideo,
4268
- video: {
4269
- ...audioVideo.video,
4270
- ...VIDEO_RESOLUTIONS[this.mediaProperties.localQualityLevel].video,
4271
- },
4272
- };
4273
- }
4274
- // extract deviceId if exists otherwise default to null.
4275
- const {deviceId: preferredVideoDevice} = (audioVideo && audioVideo.video) || {deviceId: null};
4276
- const lastVideoDeviceId = this.mediaProperties.getVideoDeviceId();
4277
-
4278
- if (preferredVideoDevice) {
4279
- // Store new preferred video input device
4280
- this.mediaProperties.setVideoDeviceId(preferredVideoDevice);
4281
- } else if (lastVideoDeviceId) {
4282
- // no new video preference specified so use last stored value,
4283
- // works with empty object {} or media constraint.
4284
- // eslint-disable-next-line no-param-reassign
4285
- audioVideo = {
4286
- ...audioVideo,
4287
- video: {
4288
- ...audioVideo.video,
4289
- deviceId: lastVideoDeviceId,
4290
- },
4291
- };
4292
- }
4293
-
4294
- return Media.getSupportedDevice({
4295
- sendAudio: mediaDirection.sendAudio,
4296
- sendVideo: mediaDirection.sendVideo,
4297
- })
4298
- .catch((error) =>
4299
- Promise.reject(
4300
- new MediaError(
4301
- 'Given constraints do not match permission set for either camera or microphone',
4302
- error
4303
- )
4304
- )
4305
- )
4306
- .then((devicePermissions) =>
4307
- Media.getUserMedia(
4308
- {
4309
- ...mediaDirection,
4310
- sendAudio: devicePermissions.sendAudio,
4311
- sendVideo: devicePermissions.sendVideo,
4312
- isSharing: this.shareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE,
4313
- },
4314
- audioVideo,
4315
- sharePreferences,
4316
- // @ts-ignore - config coming from registerPlugin
4317
- this.config
4318
- ).catch((error) => {
4319
- // Whenever there is a failure when trying to access a user's device
4320
- // report it as an Behavioral metric
4321
- // This gives visibility into common errors and can help
4322
- // with further troubleshooting
4323
- const metricName = BEHAVIORAL_METRICS.GET_USER_MEDIA_FAILURE;
4324
- const data = {
4325
- correlation_id: this.correlationId,
4326
- locus_id: this.locusUrl?.split('/').pop(),
4327
- reason: error.message,
4328
- stack: error.stack,
4329
- };
4330
- const metadata = {
4331
- type: error.name,
4332
- };
4333
-
4334
- Metrics.sendBehavioralMetric(metricName, data, metadata);
4335
- throw new MediaError('Unable to retrieve media streams', error);
4336
- })
4337
- );
4338
- }
4339
-
4340
- return Promise.reject(
4341
- new MediaError('At least one of the mediaDirection value should be true')
4342
- );
4343
- };
4344
-
4345
- /**
4346
- * Checks if the machine has at least one audio or video device
4347
- * @param {Object} options
4348
- * @param {Boolean} options.sendAudio
4349
- * @param {Boolean} options.sendVideo
4350
- * @returns {Object}
4351
- * @memberof Meetings
4352
- */
4353
- getSupportedDevices = ({
4354
- sendAudio = true,
4355
- sendVideo = true,
4356
- }: {
4357
- sendAudio: boolean;
4358
- sendVideo: boolean;
4359
- }) => Media.getSupportedDevice({sendAudio, sendVideo});
4360
-
4361
- /**
4362
- * Get the devices from the Media module
4363
- * @returns {Promise} resolves to an array of DeviceInfo
4364
- * @memberof Meetings
4365
- */
4366
- getDevices = () => Media.getDevices();
4367
-
4368
4553
  /**
4369
4554
  * Handles ROAP_FAILURE event from the webrtc media connection
4370
4555
  *
@@ -4387,7 +4572,7 @@ export default class Meeting extends StatelessWebexPlugin {
4387
4572
  Metrics.sendBehavioralMetric(metricName, data, metadata);
4388
4573
  };
4389
4574
 
4390
- if (error instanceof MC.Errors.SdpOfferCreationError) {
4575
+ if (error instanceof Errors.SdpOfferCreationError) {
4391
4576
  sendBehavioralMetric(BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE, error, this.id);
4392
4577
 
4393
4578
  Metrics.postEvent({
@@ -4401,8 +4586,8 @@ export default class Meeting extends StatelessWebexPlugin {
4401
4586
  },
4402
4587
  });
4403
4588
  } else if (
4404
- error instanceof MC.Errors.SdpOfferHandlingError ||
4405
- error instanceof MC.Errors.SdpAnswerHandlingError
4589
+ error instanceof Errors.SdpOfferHandlingError ||
4590
+ error instanceof Errors.SdpAnswerHandlingError
4406
4591
  ) {
4407
4592
  sendBehavioralMetric(BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE, error, this.id);
4408
4593
 
@@ -4416,8 +4601,8 @@ export default class Meeting extends StatelessWebexPlugin {
4416
4601
  ],
4417
4602
  },
4418
4603
  });
4419
- } else if (error instanceof MC.Errors.SdpError) {
4420
- // this covers also the case of MC.Errors.IceGatheringError which extends MC.Errors.SdpError
4604
+ } else if (error instanceof Errors.SdpError) {
4605
+ // this covers also the case of Errors.IceGatheringError which extends Errors.SdpError
4421
4606
  sendBehavioralMetric(BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE, error, this.id);
4422
4607
 
4423
4608
  Metrics.postEvent({
@@ -4434,20 +4619,20 @@ export default class Meeting extends StatelessWebexPlugin {
4434
4619
  };
4435
4620
 
4436
4621
  setupMediaConnectionListeners = () => {
4437
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_STARTED, () => {
4622
+ this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_STARTED, () => {
4438
4623
  this.isRoapInProgress = true;
4439
4624
  });
4440
4625
 
4441
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_DONE, () => {
4626
+ this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_DONE, () => {
4442
4627
  this.mediaNegotiatedEvent();
4443
4628
  this.isRoapInProgress = false;
4444
4629
  this.processNextQueuedMediaUpdate();
4445
4630
  });
4446
4631
 
4447
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_FAILURE, this.handleRoapFailure);
4632
+ this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_FAILURE, this.handleRoapFailure);
4448
4633
 
4449
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_MESSAGE_TO_SEND, (event) => {
4450
- const LOG_HEADER = 'Meeting:index#setupMediaConnectionListeners.ROAP_MESSAGE_TO_SEND -->';
4634
+ this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_MESSAGE_TO_SEND, (event) => {
4635
+ const LOG_HEADER = `Meeting:index#setupMediaConnectionListeners.ROAP_MESSAGE_TO_SEND --> correlationId=${this.correlationId}`;
4451
4636
 
4452
4637
  switch (event.roapMessage.messageType) {
4453
4638
  case 'OK':
@@ -4463,9 +4648,7 @@ export default class Meeting extends StatelessWebexPlugin {
4463
4648
  correlationId: this.correlationId,
4464
4649
  }),
4465
4650
  {
4466
- header: `${LOG_HEADER} Send Roap OK`,
4467
- success: `${LOG_HEADER} Successfully send roap OK`,
4468
- failure: `${LOG_HEADER} Error joining the call on send roap OK, `,
4651
+ logText: `${LOG_HEADER} Roap OK`,
4469
4652
  }
4470
4653
  );
4471
4654
  break;
@@ -4485,9 +4668,7 @@ export default class Meeting extends StatelessWebexPlugin {
4485
4668
  reconnect: this.reconnectionManager.isReconnectInProgress(),
4486
4669
  }),
4487
4670
  {
4488
- header: `${LOG_HEADER} Send Roap Offer`,
4489
- success: `${LOG_HEADER} Successfully send roap offer`,
4490
- failure: `${LOG_HEADER} Error joining the call on send roap offer, `,
4671
+ logText: `${LOG_HEADER} Roap Offer`,
4491
4672
  }
4492
4673
  );
4493
4674
  break;
@@ -4506,9 +4687,7 @@ export default class Meeting extends StatelessWebexPlugin {
4506
4687
  correlationId: this.correlationId,
4507
4688
  }),
4508
4689
  {
4509
- header: `${LOG_HEADER} Send Roap Answer.`,
4510
- success: `${LOG_HEADER} Successfully send roap answer`,
4511
- failure: `${LOG_HEADER} Error joining the call on send roap answer, `,
4690
+ logText: `${LOG_HEADER} Roap Answer`,
4512
4691
  }
4513
4692
  ).catch((error) => {
4514
4693
  const metricName = BEHAVIORAL_METRICS.ROAP_ANSWER_FAILURE;
@@ -4528,8 +4707,8 @@ export default class Meeting extends StatelessWebexPlugin {
4528
4707
 
4529
4708
  case 'ERROR':
4530
4709
  if (
4531
- event.roapMessage.errorType === MC.ErrorType.CONFLICT ||
4532
- event.roapMessage.errorType === MC.ErrorType.DOUBLECONFLICT
4710
+ event.roapMessage.errorType === ErrorType.CONFLICT ||
4711
+ event.roapMessage.errorType === ErrorType.DOUBLECONFLICT
4533
4712
  ) {
4534
4713
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_GLARE_CONDITION, {
4535
4714
  correlation_id: this.correlationId,
@@ -4545,9 +4724,7 @@ export default class Meeting extends StatelessWebexPlugin {
4545
4724
  correlationId: this.correlationId,
4546
4725
  }),
4547
4726
  {
4548
- header: `${LOG_HEADER} Send Roap Error.`,
4549
- success: `${LOG_HEADER} Successfully send roap error`,
4550
- failure: `${LOG_HEADER} Failed to send roap error, `,
4727
+ logText: `${LOG_HEADER} Roap Error (${event.roapMessage.errorType})`,
4551
4728
  }
4552
4729
  );
4553
4730
  break;
@@ -4561,7 +4738,7 @@ export default class Meeting extends StatelessWebexPlugin {
4561
4738
  });
4562
4739
 
4563
4740
  // eslint-disable-next-line no-param-reassign
4564
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.REMOTE_TRACK_ADDED, (event) => {
4741
+ this.mediaProperties.webrtcMediaConnection.on(Event.REMOTE_TRACK_ADDED, (event) => {
4565
4742
  LoggerProxy.logger.log(
4566
4743
  `Meeting:index#setupMediaConnectionListeners --> REMOTE_TRACK_ADDED event received for webrtcMediaConnection: ${JSON.stringify(
4567
4744
  event
@@ -4574,15 +4751,15 @@ export default class Meeting extends StatelessWebexPlugin {
4574
4751
  let eventType;
4575
4752
 
4576
4753
  switch (event.type) {
4577
- case MC.RemoteTrackType.AUDIO:
4754
+ case RemoteTrackType.AUDIO:
4578
4755
  eventType = EVENT_TYPES.REMOTE_AUDIO;
4579
4756
  this.mediaProperties.setRemoteAudioTrack(event.track);
4580
4757
  break;
4581
- case MC.RemoteTrackType.VIDEO:
4758
+ case RemoteTrackType.VIDEO:
4582
4759
  eventType = EVENT_TYPES.REMOTE_VIDEO;
4583
4760
  this.mediaProperties.setRemoteVideoTrack(event.track);
4584
4761
  break;
4585
- case MC.RemoteTrackType.SCREENSHARE_VIDEO:
4762
+ case RemoteTrackType.SCREENSHARE_VIDEO:
4586
4763
  if (event.track) {
4587
4764
  eventType = EVENT_TYPES.REMOTE_SHARE;
4588
4765
  this.mediaProperties.setRemoteShare(event.track);
@@ -4595,10 +4772,6 @@ export default class Meeting extends StatelessWebexPlugin {
4595
4772
  }
4596
4773
  }
4597
4774
 
4598
- // start stats here the stats are coming null if you dont receive streams
4599
-
4600
- this.statsAnalyzer.startAnalyzer(this.mediaProperties.webrtcMediaConnection);
4601
-
4602
4775
  if (eventType && mediaTrack) {
4603
4776
  Trigger.trigger(
4604
4777
  this,
@@ -4615,7 +4788,7 @@ export default class Meeting extends StatelessWebexPlugin {
4615
4788
  }
4616
4789
  });
4617
4790
 
4618
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.CONNECTION_STATE_CHANGED, (event) => {
4791
+ this.mediaProperties.webrtcMediaConnection.on(Event.CONNECTION_STATE_CHANGED, (event) => {
4619
4792
  const connectionFailed = () => {
4620
4793
  // we know the media connection failed and browser will not attempt to recover it any more
4621
4794
  // so reset the timer as it's not needed anymore, we want to reconnect immediately
@@ -4645,13 +4818,13 @@ export default class Meeting extends StatelessWebexPlugin {
4645
4818
  };
4646
4819
 
4647
4820
  LoggerProxy.logger.info(
4648
- `Meeting:index#setupMediaConnectionListeners --> connection state changed to ${event.state}`
4821
+ `Meeting:index#setupMediaConnectionListeners --> correlationId=${this.correlationId} connection state changed to ${event.state}`
4649
4822
  );
4650
4823
  switch (event.state) {
4651
- case MC.ConnectionState.Connecting:
4824
+ case ConnectionState.Connecting:
4652
4825
  Metrics.postEvent({event: eventType.ICE_START, meeting: this});
4653
4826
  break;
4654
- case MC.ConnectionState.Connected:
4827
+ case ConnectionState.Connected:
4655
4828
  Metrics.postEvent({event: eventType.ICE_END, meeting: this});
4656
4829
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_SUCCESS, {
4657
4830
  correlation_id: this.correlationId,
@@ -4659,8 +4832,9 @@ export default class Meeting extends StatelessWebexPlugin {
4659
4832
  });
4660
4833
  this.setNetworkStatus(NETWORK_STATUS.CONNECTED);
4661
4834
  this.reconnectionManager.iceReconnected();
4835
+ this.statsAnalyzer.startAnalyzer(this.mediaProperties.webrtcMediaConnection);
4662
4836
  break;
4663
- case MC.ConnectionState.Disconnected:
4837
+ case ConnectionState.Disconnected:
4664
4838
  this.setNetworkStatus(NETWORK_STATUS.DISCONNECTED);
4665
4839
  this.reconnectionManager.waitForIceReconnect().catch(() => {
4666
4840
  LoggerProxy.logger.info(
@@ -4670,7 +4844,7 @@ export default class Meeting extends StatelessWebexPlugin {
4670
4844
  connectionFailed();
4671
4845
  });
4672
4846
  break;
4673
- case MC.ConnectionState.Failed:
4847
+ case ConnectionState.Failed:
4674
4848
  connectionFailed();
4675
4849
  break;
4676
4850
  default:
@@ -4678,7 +4852,7 @@ export default class Meeting extends StatelessWebexPlugin {
4678
4852
  }
4679
4853
  });
4680
4854
 
4681
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.ACTIVE_SPEAKERS_CHANGED, (msg) => {
4855
+ this.mediaProperties.webrtcMediaConnection.on(Event.ACTIVE_SPEAKERS_CHANGED, (msg) => {
4682
4856
  Trigger.trigger(
4683
4857
  this,
4684
4858
  {
@@ -4689,6 +4863,7 @@ export default class Meeting extends StatelessWebexPlugin {
4689
4863
  {
4690
4864
  seqNum: msg.seqNum,
4691
4865
  memberIds: msg.csis
4866
+ // @ts-ignore
4692
4867
  .map((csi) => this.members.findMemberByCsi(csi)?.id)
4693
4868
  .filter((item) => item !== undefined),
4694
4869
  }
@@ -4696,8 +4871,8 @@ export default class Meeting extends StatelessWebexPlugin {
4696
4871
  });
4697
4872
 
4698
4873
  this.mediaProperties.webrtcMediaConnection.on(
4699
- MC.Event.VIDEO_SOURCES_COUNT_CHANGED,
4700
- (numTotalSources, numLiveSources) => {
4874
+ Event.VIDEO_SOURCES_COUNT_CHANGED,
4875
+ (numTotalSources, numLiveSources, mediaContent) => {
4701
4876
  Trigger.trigger(
4702
4877
  this,
4703
4878
  {
@@ -4708,14 +4883,19 @@ export default class Meeting extends StatelessWebexPlugin {
4708
4883
  {
4709
4884
  numTotalSources,
4710
4885
  numLiveSources,
4886
+ mediaContent,
4711
4887
  }
4712
4888
  );
4889
+
4890
+ if (mediaContent === MediaContent.Main) {
4891
+ this.mediaRequestManagers.video.setNumCurrentSources(numTotalSources, numLiveSources);
4892
+ }
4713
4893
  }
4714
4894
  );
4715
4895
 
4716
4896
  this.mediaProperties.webrtcMediaConnection.on(
4717
- MC.Event.AUDIO_SOURCES_COUNT_CHANGED,
4718
- (numTotalSources, numLiveSources) => {
4897
+ Event.AUDIO_SOURCES_COUNT_CHANGED,
4898
+ (numTotalSources, numLiveSources, mediaContent) => {
4719
4899
  Trigger.trigger(
4720
4900
  this,
4721
4901
  {
@@ -4726,6 +4906,7 @@ export default class Meeting extends StatelessWebexPlugin {
4726
4906
  {
4727
4907
  numTotalSources,
4728
4908
  numLiveSources,
4909
+ mediaContent,
4729
4910
  }
4730
4911
  );
4731
4912
  }
@@ -4744,6 +4925,7 @@ export default class Meeting extends StatelessWebexPlugin {
4744
4925
  // Add ip address info if geo hint is present
4745
4926
  // @ts-ignore fix type
4746
4927
  options.data.intervalMetadata.peerReflexiveIP =
4928
+ // @ts-ignore
4747
4929
  this.webex.meetings.geoHintInfo?.clientAddress ||
4748
4930
  options.data.intervalMetadata.peerReflexiveIP ||
4749
4931
  MQA_STATS.DEFAULT_IP;
@@ -4813,7 +4995,15 @@ export default class Meeting extends StatelessWebexPlugin {
4813
4995
  return `MC-${this.id.substring(0, 4)}`;
4814
4996
  }
4815
4997
 
4816
- createMediaConnection(turnServerInfo) {
4998
+ /**
4999
+ * Creates a webrtc media connection and publishes tracks to it
5000
+ *
5001
+ * @param {Object} turnServerInfo TURN server information
5002
+ * @param {BundlePolicy} [bundlePolicy] Bundle policy settings
5003
+ * @returns {RoapMediaConnection | MultistreamRoapMediaConnection}
5004
+ */
5005
+ private async createMediaConnection(turnServerInfo, bundlePolicy?: BundlePolicy) {
5006
+ // create the actual media connection
4817
5007
  const mc = Media.createMediaConnection(this.isMultistream, this.getMediaConnectionDebugId(), {
4818
5008
  mediaProperties: this.mediaProperties,
4819
5009
  remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
@@ -4822,11 +5012,23 @@ export default class Meeting extends StatelessWebexPlugin {
4822
5012
  // @ts-ignore - config coming from registerPlugin
4823
5013
  enableExtmap: this.config.enableExtmap,
4824
5014
  turnServerInfo,
5015
+ bundlePolicy,
4825
5016
  });
4826
5017
 
4827
5018
  this.mediaProperties.setMediaPeerConnection(mc);
4828
5019
  this.setupMediaConnectionListeners();
4829
5020
 
5021
+ // publish the tracks
5022
+ if (this.mediaProperties.audioTrack) {
5023
+ await this.publishTrack(this.mediaProperties.audioTrack);
5024
+ }
5025
+ if (this.mediaProperties.videoTrack) {
5026
+ await this.publishTrack(this.mediaProperties.videoTrack);
5027
+ }
5028
+ if (this.mediaProperties.shareTrack) {
5029
+ await this.publishTrack(this.mediaProperties.shareTrack);
5030
+ }
5031
+
4830
5032
  return mc;
4831
5033
  }
4832
5034
 
@@ -4854,23 +5056,21 @@ export default class Meeting extends StatelessWebexPlugin {
4854
5056
  }
4855
5057
 
4856
5058
  /**
4857
- * Specify joining via audio (option: pstn), video, screenshare
4858
- * @param {Object} options A configurable options object for joining a meeting
4859
- * @param {Object} options.resourceId pass the deviceId
4860
- * @param {MediaDirection} options.mediaSettings pass media options
4861
- * @param {MediaStream} options.localStream
4862
- * @param {MediaStream} options.localShare
4863
- * @param {RemoteMediaManagerConfig} options.remoteMediaManagerConfig only applies if multistream is enabled
5059
+ * Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
5060
+ *
5061
+ * @param {AddMediaOptions} options
4864
5062
  * @returns {Promise}
4865
5063
  * @public
4866
5064
  * @memberof Meeting
4867
5065
  */
4868
- addMedia(options: any = {}) {
5066
+ addMedia(options: AddMediaOptions = {}) {
4869
5067
  const LOG_HEADER = 'Meeting:index#addMedia -->';
4870
5068
 
4871
5069
  let turnDiscoverySkippedReason;
4872
5070
  let turnServerUsed = false;
4873
5071
 
5072
+ LoggerProxy.logger.info(`${LOG_HEADER} called with: ${JSON.stringify(options)}`);
5073
+
4874
5074
  if (this.meetingState !== FULL_STATE.ACTIVE) {
4875
5075
  return Promise.reject(new MeetingNotActiveError());
4876
5076
  }
@@ -4884,9 +5084,14 @@ export default class Meeting extends StatelessWebexPlugin {
4884
5084
  return Promise.reject(new UserInLobbyError());
4885
5085
  }
4886
5086
 
4887
- const {localStream, localShare, mediaSettings, remoteMediaManagerConfig} = options;
4888
-
4889
- LoggerProxy.logger.info(`${LOG_HEADER} Adding Media.`);
5087
+ const {
5088
+ localTracks,
5089
+ audioEnabled = true,
5090
+ videoEnabled = true,
5091
+ receiveShare = true,
5092
+ remoteMediaManagerConfig,
5093
+ bundlePolicy,
5094
+ } = options;
4890
5095
 
4891
5096
  Metrics.postEvent({
4892
5097
  event: eventType.MEDIA_CAPABILITIES,
@@ -4911,17 +5116,61 @@ export default class Meeting extends StatelessWebexPlugin {
4911
5116
  },
4912
5117
  });
4913
5118
 
4914
- return MeetingUtil.validateOptions(options)
5119
+ // when audioEnabled/videoEnabled is true, we set sendAudio/sendVideo to true even before any tracks are published
5120
+ // to avoid doing an extra SDP exchange when they are published for the first time
5121
+ this.mediaProperties.setMediaDirection({
5122
+ sendAudio: audioEnabled,
5123
+ sendVideo: videoEnabled,
5124
+ sendShare: false,
5125
+ receiveAudio: audioEnabled,
5126
+ receiveVideo: videoEnabled,
5127
+ receiveShare,
5128
+ });
5129
+
5130
+ this.locusMediaRequest = new LocusMediaRequest(
5131
+ {
5132
+ correlationId: this.correlationId,
5133
+ device: {
5134
+ url: this.deviceUrl,
5135
+ // @ts-ignore
5136
+ deviceType: this.config.deviceType,
5137
+ },
5138
+ preferTranscoding: !this.isMultistream,
5139
+ },
5140
+ {
5141
+ // @ts-ignore
5142
+ parent: this.webex,
5143
+ }
5144
+ );
5145
+
5146
+ this.audio = createMuteState(AUDIO, this, audioEnabled);
5147
+ this.video = createMuteState(VIDEO, this, videoEnabled);
5148
+
5149
+ this.annotationInfo = localTracks?.annotationInfo;
5150
+
5151
+ const promises = [];
5152
+
5153
+ // setup all the references to local tracks in this.mediaProperties before creating media connection
5154
+ // and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages
5155
+ if (localTracks?.microphone) {
5156
+ promises.push(this.setLocalAudioTrack(localTracks.microphone));
5157
+ }
5158
+ if (localTracks?.camera) {
5159
+ promises.push(this.setLocalVideoTrack(localTracks.camera));
5160
+ }
5161
+ if (localTracks?.screenShare?.video) {
5162
+ promises.push(this.setLocalShareTrack(localTracks.screenShare.video));
5163
+ }
5164
+
5165
+ return Promise.all(promises)
4915
5166
  .then(() => this.roap.doTurnDiscovery(this, false))
4916
- .then((turnDiscoveryObject) => {
5167
+ .then(async (turnDiscoveryObject) => {
4917
5168
  ({turnDiscoverySkippedReason} = turnDiscoveryObject);
4918
5169
  turnServerUsed = !turnDiscoverySkippedReason;
4919
5170
 
4920
5171
  const {turnServerInfo} = turnDiscoveryObject;
4921
5172
 
4922
- this.preMedia(localStream, localShare, mediaSettings);
4923
-
4924
- const mc = this.createMediaConnection(turnServerInfo);
5173
+ const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
4925
5174
 
4926
5175
  if (this.isMultistream) {
4927
5176
  this.remoteMediaManager = new RemoteMediaManager(
@@ -4946,18 +5195,21 @@ export default class Meeting extends StatelessWebexPlugin {
4946
5195
  EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
4947
5196
  );
4948
5197
 
4949
- return this.remoteMediaManager.start().then(() => mc.initiateOffer());
5198
+ await this.remoteMediaManager.start();
4950
5199
  }
4951
5200
 
4952
- return mc.initiateOffer();
5201
+ await mc.initiateOffer();
4953
5202
  })
4954
5203
  .then(() => {
4955
5204
  this.setMercuryListener();
4956
5205
  })
4957
- .then(() =>
4958
- this.getDevices().then((devices) => {
4959
- MeetingUtil.handleDeviceLogging(devices);
4960
- })
5206
+ .then(
5207
+ () =>
5208
+ getDevices()
5209
+ .then((devices) => {
5210
+ MeetingUtil.handleDeviceLogging(devices);
5211
+ })
5212
+ .catch(() => {}) // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
4961
5213
  )
4962
5214
  .then(() => {
4963
5215
  this.handleMediaLogging(this.mediaProperties);
@@ -4967,8 +5219,12 @@ export default class Meeting extends StatelessWebexPlugin {
4967
5219
  if (this.config.stats.enableStatsAnalyzer) {
4968
5220
  // @ts-ignore - config coming from registerPlugin
4969
5221
  this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
4970
- // @ts-ignore - config coming from registerPlugin
4971
- this.statsAnalyzer = new StatsAnalyzer(this.config.stats, this.networkQualityMonitor);
5222
+ this.statsAnalyzer = new StatsAnalyzer(
5223
+ // @ts-ignore - config coming from registerPlugin
5224
+ this.config.stats,
5225
+ (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
5226
+ this.networkQualityMonitor
5227
+ );
4972
5228
  this.setupStatsAnalyzerEventHandlers();
4973
5229
  this.networkQualityMonitor.on(
4974
5230
  EVENT_TRIGGERS.NETWORK_QUALITY,
@@ -4991,7 +5247,7 @@ export default class Meeting extends StatelessWebexPlugin {
4991
5247
 
4992
5248
  // eslint-disable-next-line func-names
4993
5249
  // eslint-disable-next-line prefer-arrow-callback
4994
- if (this.type === _CALL_) {
5250
+ if (this.type === _CALL_ || this.meetingState === FULL_STATE.ACTIVE) {
4995
5251
  resolve();
4996
5252
  }
4997
5253
  const joiningTimer = setInterval(() => {
@@ -5010,21 +5266,15 @@ export default class Meeting extends StatelessWebexPlugin {
5010
5266
  )
5011
5267
  .then(() =>
5012
5268
  this.mediaProperties.waitForMediaConnectionConnected().catch(() => {
5013
- throw createMeetingsError(30202, 'Meeting connection failed');
5269
+ throw new Error(
5270
+ `Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
5271
+ );
5014
5272
  })
5015
5273
  )
5016
5274
  .then(() => {
5017
- LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection CONNECTED`);
5018
- if (mediaSettings && mediaSettings.sendShare && localShare) {
5019
- if (this.state === MEETING_STATE.STATES.JOINED) {
5020
- return this.requestScreenShareFloor();
5021
- }
5022
-
5023
- // When the self state changes to JOINED then request the floor
5024
- this.floorGrantPending = true;
5275
+ if (localTracks?.screenShare?.video) {
5276
+ this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.SHARE_FLOOR_REQUEST);
5025
5277
  }
5026
-
5027
- return {};
5028
5278
  })
5029
5279
  .then(() => this.mediaProperties.getCurrentConnectionType())
5030
5280
  .then((connectionType) => {
@@ -5032,9 +5282,36 @@ export default class Meeting extends StatelessWebexPlugin {
5032
5282
  correlation_id: this.correlationId,
5033
5283
  locus_id: this.locusUrl.split('/').pop(),
5034
5284
  connectionType,
5285
+ isMultistream: this.isMultistream,
5035
5286
  });
5036
5287
  })
5037
5288
  .catch((error) => {
5289
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
5290
+ correlation_id: this.correlationId,
5291
+ locus_id: this.locusUrl.split('/').pop(),
5292
+ reason: error.message,
5293
+ stack: error.stack,
5294
+ code: error.code,
5295
+ turnDiscoverySkippedReason,
5296
+ turnServerUsed,
5297
+ isMultistream: this.isMultistream,
5298
+ signalingState:
5299
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5300
+ ?.signalingState ||
5301
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
5302
+ 'unknown',
5303
+ connectionState:
5304
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5305
+ ?.connectionState ||
5306
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.connectionState ||
5307
+ 'unknown',
5308
+ iceConnectionState:
5309
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5310
+ ?.iceConnectionState ||
5311
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
5312
+ 'unknown',
5313
+ });
5314
+
5038
5315
  // Clean up stats analyzer, peer connection, and turn off listeners
5039
5316
  const stopStatsAnalyzer = this.statsAnalyzer
5040
5317
  ? this.statsAnalyzer.stopAnalyzer()
@@ -5053,16 +5330,6 @@ export default class Meeting extends StatelessWebexPlugin {
5053
5330
  error
5054
5331
  );
5055
5332
 
5056
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
5057
- correlation_id: this.correlationId,
5058
- locus_id: this.locusUrl.split('/').pop(),
5059
- reason: error.message,
5060
- stack: error.stack,
5061
- code: error.code,
5062
- turnDiscoverySkippedReason,
5063
- turnServerUsed,
5064
- });
5065
-
5066
5333
  // Upload logs on error while adding media
5067
5334
  Trigger.trigger(
5068
5335
  this,
@@ -5074,7 +5341,7 @@ export default class Meeting extends StatelessWebexPlugin {
5074
5341
  this
5075
5342
  );
5076
5343
 
5077
- if (error instanceof MC.Errors.SdpError) {
5344
+ if (error instanceof Errors.SdpError) {
5078
5345
  this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
5079
5346
  }
5080
5347
 
@@ -5102,7 +5369,9 @@ export default class Meeting extends StatelessWebexPlugin {
5102
5369
  * @private
5103
5370
  * @memberof Meeting
5104
5371
  */
5105
- private enqueueMediaUpdate(mediaUpdateType: string, options: object) {
5372
+ private enqueueMediaUpdate(mediaUpdateType: string, options: any = {}): Promise<void> {
5373
+ const canUpdateMediaNow = this.canUpdateMedia();
5374
+
5106
5375
  return new Promise((resolve, reject) => {
5107
5376
  const queueItem = {
5108
5377
  pendingPromiseResolve: resolve,
@@ -5115,6 +5384,10 @@ export default class Meeting extends StatelessWebexPlugin {
5115
5384
  `Meeting:index#enqueueMediaUpdate --> enqueuing media update type=${mediaUpdateType}`
5116
5385
  );
5117
5386
  this.queuedMediaUpdates.push(queueItem);
5387
+
5388
+ if (canUpdateMediaNow) {
5389
+ this.processNextQueuedMediaUpdate();
5390
+ }
5118
5391
  });
5119
5392
  }
5120
5393
 
@@ -5154,18 +5427,17 @@ export default class Meeting extends StatelessWebexPlugin {
5154
5427
  LoggerProxy.logger.log(
5155
5428
  `Meeting:index#processNextQueuedMediaUpdate --> performing delayed media update type=${mediaUpdateType}`
5156
5429
  );
5430
+ let mediaUpdate = Promise.resolve();
5431
+
5157
5432
  switch (mediaUpdateType) {
5158
- case MEDIA_UPDATE_TYPE.ALL:
5159
- this.updateMedia(options).then(pendingPromiseResolve, pendingPromiseReject);
5160
- break;
5161
- case MEDIA_UPDATE_TYPE.AUDIO:
5162
- this.updateAudio(options).then(pendingPromiseResolve, pendingPromiseReject);
5433
+ case MEDIA_UPDATE_TYPE.TRANSCODED_MEDIA_CONNECTION:
5434
+ mediaUpdate = this.updateTranscodedMediaConnection();
5163
5435
  break;
5164
- case MEDIA_UPDATE_TYPE.VIDEO:
5165
- this.updateVideo(options).then(pendingPromiseResolve, pendingPromiseReject);
5436
+ case MEDIA_UPDATE_TYPE.SHARE_FLOOR_REQUEST:
5437
+ mediaUpdate = this.requestScreenShareFloor();
5166
5438
  break;
5167
- case MEDIA_UPDATE_TYPE.SHARE:
5168
- this.updateShare(options).then(pendingPromiseResolve, pendingPromiseReject);
5439
+ case MEDIA_UPDATE_TYPE.UPDATE_MEDIA:
5440
+ mediaUpdate = this.updateMedia(options);
5169
5441
  break;
5170
5442
  default:
5171
5443
  LoggerProxy.logger.error(
@@ -5173,329 +5445,84 @@ export default class Meeting extends StatelessWebexPlugin {
5173
5445
  );
5174
5446
  break;
5175
5447
  }
5448
+
5449
+ mediaUpdate
5450
+ .then(pendingPromiseResolve, pendingPromiseReject)
5451
+ .then(() => this.processNextQueuedMediaUpdate());
5176
5452
  }
5177
5453
  };
5178
5454
 
5179
5455
  /**
5180
- * A confluence of updateAudio, updateVideo, and updateShare
5181
- * this function re-establishes all of the media streams with new options
5456
+ * Updates the media connection - it allows to enable/disable all audio/video/share in the meeting.
5457
+ * This does not affect the published tracks, so for example if a microphone track is published and
5458
+ * updateMedia({audioEnabled: false}) is called, the audio will not be sent or received anymore,
5459
+ * but the track's "published" state is not changed and when updateMedia({audioEnabled: true}) is called,
5460
+ * the sending of the audio from the same track will resume.
5461
+ *
5182
5462
  * @param {Object} options
5183
- * @param {MediaStream} options.localStream
5184
- * @param {MediaStream} options.localShare
5185
- * @param {MediaDirection} options.mediaSettings
5463
+ * @param {boolean} options.audioEnabled [optional] enables/disables receiving and sending of main audio in the meeting
5464
+ * @param {boolean} options.videoEnabled [optional] enables/disables receiving and sending of main video in the meeting
5465
+ * @param {boolean} options.shareEnabled [optional] enables/disables receiving and sending of screen share in the meeting
5186
5466
  * @returns {Promise}
5187
5467
  * @public
5188
5468
  * @memberof Meeting
5189
5469
  */
5190
- public updateMedia(
5191
- options: {
5192
- localStream?: MediaStream;
5193
- localShare?: MediaStream;
5194
- mediaSettings?: any;
5195
- } = {} as any
5196
- ) {
5197
- const LOG_HEADER = 'Meeting:index#updateMedia -->';
5470
+ public async updateMedia(options: {
5471
+ audioEnabled?: boolean;
5472
+ videoEnabled?: boolean;
5473
+ receiveShare?: boolean;
5474
+ }) {
5475
+ this.checkMediaConnection();
5198
5476
 
5199
- if (!this.canUpdateMedia()) {
5200
- return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.ALL, options);
5201
- }
5202
- const {localStream, localShare, mediaSettings} = options;
5477
+ const {audioEnabled, videoEnabled, receiveShare} = options;
5203
5478
 
5204
- const previousSendShareStatus = this.mediaProperties.mediaDirection.sendShare;
5479
+ LoggerProxy.logger.log(
5480
+ `Meeting:index#updateMedia --> called with options=${JSON.stringify(options)}`
5481
+ );
5205
5482
 
5206
- if (!this.mediaProperties.webrtcMediaConnection) {
5207
- return Promise.reject(new Error('media connection not established, call addMedia() first'));
5483
+ if (!this.canUpdateMedia()) {
5484
+ return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.UPDATE_MEDIA, options);
5208
5485
  }
5209
5486
 
5210
- return MeetingUtil.validateOptions(options)
5211
- .then(() => this.preMedia(localStream, localShare, mediaSettings))
5212
- .then(() =>
5213
- this.mediaProperties.webrtcMediaConnection
5214
- .updateSendReceiveOptions({
5215
- send: {
5216
- audio: this.mediaProperties.mediaDirection.sendAudio
5217
- ? this.mediaProperties.audioTrack
5218
- : null,
5219
- video: this.mediaProperties.mediaDirection.sendVideo
5220
- ? this.mediaProperties.videoTrack
5221
- : null,
5222
- screenShareVideo: this.mediaProperties.mediaDirection.sendShare
5223
- ? this.mediaProperties.shareTrack
5224
- : null,
5225
- },
5226
- receive: {
5227
- audio: this.mediaProperties.mediaDirection.receiveAudio,
5228
- video: this.mediaProperties.mediaDirection.receiveVideo,
5229
- screenShareVideo: this.mediaProperties.mediaDirection.receiveShare,
5230
- remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
5231
- },
5232
- })
5233
- .then(() => {
5234
- LoggerProxy.logger.info(
5235
- `${LOG_HEADER} webrtcMediaConnection.updateSendReceiveOptions done`
5236
- );
5237
- })
5238
- .catch((error) => {
5239
- LoggerProxy.logger.error(`${LOG_HEADER} Error updatedMedia, `, error);
5240
-
5241
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.UPDATE_MEDIA_FAILURE, {
5242
- correlation_id: this.correlationId,
5243
- locus_id: this.locusUrl.split('/').pop(),
5244
- reason: error.message,
5245
- stack: error.stack,
5246
- });
5247
-
5248
- throw error;
5249
- })
5250
- // todo: the following code used to be called always after sending the roap message with the new SDP
5251
- // now it's called independently from the roap message (so might be before it), check if that's OK
5252
- // if not, ensure it's called after (now it's called after roap message is sent out, but we're not
5253
- // waiting for sendRoapMediaRequest() to be resolved)
5254
- .then(() => this.checkForStopShare(mediaSettings.sendShare, previousSendShareStatus))
5255
- .then((startShare) => {
5256
- // This is a special case if we do an /floor grant followed by /media
5257
- // we actually get a OFFER from the server and a GLAR condition happens
5258
- if (startShare) {
5259
- // We are assuming that the clients are connected when doing an update
5260
- return this.requestScreenShareFloor();
5261
- }
5262
-
5263
- return Promise.resolve();
5264
- })
5265
- );
5266
- }
5487
+ if (this.isMultistream) {
5488
+ if (videoEnabled !== undefined) {
5489
+ throw new Error(
5490
+ 'enabling/disabling video in a meeting is not supported for multistream, it can only be done upfront when calling addMedia()'
5491
+ );
5492
+ }
5267
5493
 
5268
- /**
5269
- * Update the main audio track with new parameters
5270
- *
5271
- * NOTE: this method can only be used with transcoded meetings, for multistream meetings use publishTrack()
5272
- *
5273
- * @param {Object} options
5274
- * @param {boolean} options.sendAudio
5275
- * @param {boolean} options.receiveAudio
5276
- * @param {MediaStream} options.stream Stream that contains the audio track to update
5277
- * @returns {Promise}
5278
- * @public
5279
- * @memberof Meeting
5280
- */
5281
- public async updateAudio(options: {
5282
- sendAudio: boolean;
5283
- receiveAudio: boolean;
5284
- stream: MediaStream;
5285
- }) {
5286
- if (!this.canUpdateMedia()) {
5287
- return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.AUDIO, options);
5494
+ if (receiveShare !== undefined) {
5495
+ throw new Error(
5496
+ 'toggling receiveShare in a multistream meeting is not supported, to control receiving screen share call meeting.remoteMediaManager.setLayout() with appropriate layout'
5497
+ );
5498
+ }
5288
5499
  }
5289
- const {sendAudio, receiveAudio, stream} = options;
5290
- let track = MeetingUtil.getTrack(stream).audioTrack;
5291
5500
 
5292
- if (typeof sendAudio !== 'boolean' || typeof receiveAudio !== 'boolean') {
5293
- return Promise.reject(new ParameterError('Pass sendAudio and receiveAudio parameter'));
5501
+ if (audioEnabled !== undefined) {
5502
+ this.mediaProperties.mediaDirection.sendAudio = audioEnabled;
5503
+ this.mediaProperties.mediaDirection.receiveAudio = audioEnabled;
5504
+ this.audio.enable(this, audioEnabled);
5294
5505
  }
5295
5506
 
5296
- if (!this.mediaProperties.webrtcMediaConnection) {
5297
- return Promise.reject(new Error('media connection not established, call addMedia() first'));
5507
+ if (videoEnabled !== undefined) {
5508
+ this.mediaProperties.mediaDirection.sendVideo = videoEnabled;
5509
+ this.mediaProperties.mediaDirection.receiveVideo = videoEnabled;
5510
+ this.video.enable(this, videoEnabled);
5298
5511
  }
5299
5512
 
5300
- if (this.effects && this.effects.state) {
5301
- const bnrEnabled = this.effects.state.bnr.enabled;
5513
+ if (receiveShare !== undefined) {
5514
+ this.mediaProperties.mediaDirection.receiveShare = receiveShare;
5515
+ }
5302
5516
 
5303
- if (
5304
- sendAudio &&
5305
- !this.isAudioMuted() &&
5306
- (bnrEnabled === BNR_STATUS.ENABLED || bnrEnabled === BNR_STATUS.SHOULD_ENABLE)
5307
- ) {
5308
- LoggerProxy.logger.info('Meeting:index#updateAudio. Calling WebRTC enable bnr method');
5309
- track = await this.internal_enableBNR(track);
5310
- LoggerProxy.logger.info('Meeting:index#updateAudio. WebRTC enable bnr request completed');
5517
+ if (this.isMultistream) {
5518
+ if (audioEnabled !== undefined) {
5519
+ await this.mediaProperties.webrtcMediaConnection.enableMultistreamAudio(audioEnabled);
5311
5520
  }
5521
+ } else {
5522
+ await this.updateTranscodedMediaConnection();
5312
5523
  }
5313
5524
 
5314
- return MeetingUtil.validateOptions({sendAudio, localStream: stream})
5315
- .then(() =>
5316
- this.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions({
5317
- send: {audio: track},
5318
- receive: {
5319
- audio: options.receiveAudio,
5320
- video: this.mediaProperties.mediaDirection.receiveVideo,
5321
- screenShareVideo: this.mediaProperties.mediaDirection.receiveShare,
5322
- remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
5323
- },
5324
- })
5325
- )
5326
- .then(() => {
5327
- this.setLocalAudioTrack(track);
5328
- // todo: maybe this.mediaProperties.mediaDirection could be removed? it's duplicating stuff from webrtcMediaConnection
5329
- this.mediaProperties.mediaDirection.sendAudio = sendAudio;
5330
- this.mediaProperties.mediaDirection.receiveAudio = receiveAudio;
5331
-
5332
- // audio state could be undefined if you have not sent audio before
5333
- this.audio =
5334
- this.audio || createMuteState(AUDIO, this, this.mediaProperties.mediaDirection);
5335
- });
5336
- }
5337
-
5338
- /**
5339
- * Update the main video track with new parameters
5340
- *
5341
- * NOTE: this method can only be used with transcoded meetings, for multistream meetings use publishTrack()
5342
- *
5343
- * @param {Object} options
5344
- * @param {boolean} options.sendVideo
5345
- * @param {boolean} options.receiveVideo
5346
- * @param {MediaStream} options.stream Stream that contains the video track to update
5347
- * @returns {Promise}
5348
- * @public
5349
- * @memberof Meeting
5350
- */
5351
- public updateVideo(options: {sendVideo: boolean; receiveVideo: boolean; stream: MediaStream}) {
5352
- if (!this.canUpdateMedia()) {
5353
- return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.VIDEO, options);
5354
- }
5355
- const {sendVideo, receiveVideo, stream} = options;
5356
- const track = MeetingUtil.getTrack(stream).videoTrack;
5357
-
5358
- if (typeof sendVideo !== 'boolean' || typeof receiveVideo !== 'boolean') {
5359
- return Promise.reject(new ParameterError('Pass sendVideo and receiveVideo parameter'));
5360
- }
5361
-
5362
- if (!this.mediaProperties.webrtcMediaConnection) {
5363
- return Promise.reject(new Error('media connection not established, call addMedia() first'));
5364
- }
5365
-
5366
- return MeetingUtil.validateOptions({sendVideo, localStream: stream})
5367
- .then(() =>
5368
- this.mediaProperties.webrtcMediaConnection.updateSendReceiveOptions({
5369
- send: {video: track},
5370
- receive: {
5371
- audio: this.mediaProperties.mediaDirection.receiveAudio,
5372
- video: options.receiveVideo,
5373
- screenShareVideo: this.mediaProperties.mediaDirection.receiveShare,
5374
- remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
5375
- },
5376
- })
5377
- )
5378
- .then(() => {
5379
- this.setLocalVideoTrack(track);
5380
- this.mediaProperties.mediaDirection.sendVideo = sendVideo;
5381
- this.mediaProperties.mediaDirection.receiveVideo = receiveVideo;
5382
-
5383
- // video state could be undefined if you have not sent video before
5384
- this.video =
5385
- this.video || createMuteState(VIDEO, this, this.mediaProperties.mediaDirection);
5386
- });
5387
- }
5388
-
5389
- /**
5390
- * Internal function when stopping a share stream, cleanup
5391
- * @param {boolean} sendShare
5392
- * @param {boolean} previousShareStatus
5393
- * @returns {Promise}
5394
- * @private
5395
- * @memberof Meeting
5396
- */
5397
- private checkForStopShare(sendShare: boolean, previousShareStatus: boolean) {
5398
- if (sendShare && !previousShareStatus) {
5399
- // When user starts sharing
5400
- return Promise.resolve(true);
5401
- }
5402
-
5403
- if (!sendShare && previousShareStatus) {
5404
- // When user stops sharing
5405
- return this.releaseScreenShareFloor().then(() => Promise.resolve(false));
5406
- }
5407
-
5408
- return Promise.resolve();
5409
- }
5410
-
5411
- /**
5412
- * Update the share streams, can be used to start sharing
5413
- *
5414
- * NOTE: this method can only be used with transcoded meetings, for multistream meetings use publishTrack()
5415
- *
5416
- * @param {Object} options
5417
- * @param {boolean} options.sendShare
5418
- * @param {boolean} options.receiveShare
5419
- * @returns {Promise}
5420
- * @public
5421
- * @memberof Meeting
5422
- */
5423
- public updateShare(options: {
5424
- sendShare?: boolean;
5425
- receiveShare?: boolean;
5426
- stream?: any;
5427
- skipSignalingCheck?: boolean;
5428
- }) {
5429
- if (!options.skipSignalingCheck && !this.canUpdateMedia()) {
5430
- return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.SHARE, options);
5431
- }
5432
- const {sendShare, receiveShare, stream} = options;
5433
- const track = MeetingUtil.getTrack(stream).videoTrack;
5434
-
5435
- if (typeof sendShare !== 'boolean' || typeof receiveShare !== 'boolean') {
5436
- return Promise.reject(new ParameterError('Pass sendShare and receiveShare parameter'));
5437
- }
5438
-
5439
- if (!this.mediaProperties.webrtcMediaConnection) {
5440
- return Promise.reject(new Error('media connection not established, call addMedia() first'));
5441
- }
5442
-
5443
- const previousSendShareStatus = this.mediaProperties.mediaDirection.sendShare;
5444
-
5445
- this.setLocalShareTrack(stream);
5446
-
5447
- return MeetingUtil.validateOptions({sendShare, localShare: stream})
5448
- .then(() => this.checkForStopShare(sendShare, previousSendShareStatus))
5449
- .then((startShare) =>
5450
- this.mediaProperties.webrtcMediaConnection
5451
- .updateSendReceiveOptions({
5452
- send: {screenShareVideo: track},
5453
- receive: {
5454
- audio: this.mediaProperties.mediaDirection.receiveAudio,
5455
- video: this.mediaProperties.mediaDirection.receiveVideo,
5456
- screenShareVideo: options.receiveShare,
5457
- remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
5458
- },
5459
- })
5460
- .then(() => {
5461
- if (startShare) {
5462
- return this.requestScreenShareFloor();
5463
- }
5464
-
5465
- return Promise.resolve();
5466
- })
5467
- )
5468
- .then(() => {
5469
- this.mediaProperties.mediaDirection.sendShare = sendShare;
5470
- this.mediaProperties.mediaDirection.receiveShare = receiveShare;
5471
- })
5472
- .catch((error) => {
5473
- this.unsetLocalShareTrack();
5474
- throw error;
5475
- });
5476
- }
5477
-
5478
- /**
5479
- * Do all the attach media pre set up before executing the actual attach
5480
- * @param {MediaStream} localStream
5481
- * @param {MediaStream} localShare
5482
- * @param {MediaDirection} mediaSettings
5483
- * @returns {undefined}
5484
- * @private
5485
- * @memberof Meeting
5486
- */
5487
- private preMedia(localStream: MediaStream, localShare: MediaStream, mediaSettings: any) {
5488
- // eslint-disable-next-line no-warning-comments
5489
- // TODO wire into default config. There's currently an issue with the stateless plugin or how we register
5490
- // @ts-ignore - config coming from registerPlugin
5491
- this.mediaProperties.setMediaDirection(Object.assign(this.config.mediaSettings, mediaSettings));
5492
- // add a setup a function move the create and setup media in future
5493
- // TODO: delete old audio and video if stale
5494
- this.audio = this.audio || createMuteState(AUDIO, this, this.mediaProperties.mediaDirection);
5495
- this.video = this.video || createMuteState(VIDEO, this, this.mediaProperties.mediaDirection);
5496
- // Validation is already done in addMedia so no need to check if the lenght is greater then 0
5497
- this.setLocalTracks(localStream);
5498
- this.setLocalShareTrack(localShare);
5525
+ return undefined;
5499
5526
  }
5500
5527
 
5501
5528
  /**
@@ -5563,13 +5590,12 @@ export default class Meeting extends StatelessWebexPlugin {
5563
5590
  * @memberof Meeting
5564
5591
  */
5565
5592
  public leave(options: {resourceId?: string; reason?: any} = {} as any) {
5593
+ const leaveReason = options.reason || MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST;
5566
5594
  Metrics.postEvent({
5567
5595
  event: eventType.LEAVE,
5568
5596
  meeting: this,
5569
- data: {trigger: trigger.USER_INTERACTION, canProceed: false},
5597
+ data: {trigger: trigger.USER_INTERACTION, canProceed: false, reason: leaveReason},
5570
5598
  });
5571
- const leaveReason = options.reason || MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST;
5572
-
5573
5599
  LoggerProxy.logger.log('Meeting:index#leave --> Leaving a meeting');
5574
5600
 
5575
5601
  return MeetingUtil.leaveMeeting(this, options)
@@ -5738,55 +5764,68 @@ export default class Meeting extends StatelessWebexPlugin {
5738
5764
  * @memberof Meeting
5739
5765
  */
5740
5766
  private requestScreenShareFloor() {
5741
- const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
5767
+ if (!this.mediaProperties.shareTrack || !this.mediaProperties.mediaDirection.sendShare) {
5768
+ LoggerProxy.logger.log(
5769
+ `Meeting:index#requestScreenShareFloor --> NOT requesting floor, because we don't have the share track anymore (shareTrack=${
5770
+ this.mediaProperties.shareTrack ? 'yes' : 'no'
5771
+ }, sendShare=${this.mediaProperties.mediaDirection.sendShare})`
5772
+ );
5742
5773
 
5743
- if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
5744
- Metrics.postEvent({event: eventType.SHARE_INITIATED, meeting: this});
5774
+ return Promise.resolve({});
5775
+ }
5776
+ if (this.state === MEETING_STATE.STATES.JOINED) {
5777
+ const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
5778
+
5779
+ if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
5780
+ Metrics.postEvent({event: eventType.SHARE_INITIATED, meeting: this});
5781
+
5782
+ return this.meetingRequest
5783
+ .changeMeetingFloor({
5784
+ disposition: FLOOR_ACTION.GRANTED,
5785
+ personUrl: this.locusInfo.self.url,
5786
+ deviceUrl: this.deviceUrl,
5787
+ uri: content.url,
5788
+ resourceUrl: this.resourceUrl,
5789
+ annotationInfo: this.annotationInfo,
5790
+ })
5791
+ .then(() => {
5792
+ this.isSharing = true;
5745
5793
 
5746
- return this.meetingRequest
5747
- .changeMeetingFloor({
5748
- disposition: FLOOR_ACTION.GRANTED,
5749
- personUrl: this.locusInfo.self.url,
5750
- deviceUrl: this.deviceUrl,
5751
- uri: content.url,
5752
- resourceUrl: this.resourceUrl,
5753
- })
5754
- .then(() => {
5755
- this.isSharing = true;
5794
+ return Promise.resolve();
5795
+ })
5796
+ .catch((error) => {
5797
+ LoggerProxy.logger.error('Meeting:index#share --> Error ', error);
5756
5798
 
5757
- return Promise.resolve();
5758
- })
5759
- .catch((error) => {
5760
- LoggerProxy.logger.error('Meeting:index#share --> Error ', error);
5799
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_FAILURE, {
5800
+ correlation_id: this.correlationId,
5801
+ locus_id: this.locusUrl.split('/').pop(),
5802
+ reason: error.message,
5803
+ stack: error.stack,
5804
+ });
5761
5805
 
5762
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_FAILURE, {
5763
- correlation_id: this.correlationId,
5764
- locus_id: this.locusUrl.split('/').pop(),
5765
- reason: error.message,
5766
- stack: error.stack,
5806
+ return Promise.reject(error);
5767
5807
  });
5808
+ }
5768
5809
 
5769
- return Promise.reject(error);
5770
- });
5810
+ return Promise.reject(new ParameterError('Cannot share without content.'));
5771
5811
  }
5812
+ this.floorGrantPending = true;
5772
5813
 
5773
- return Promise.reject(new ParameterError('Cannot share without content.'));
5814
+ return Promise.resolve({});
5774
5815
  }
5775
5816
 
5776
5817
  /**
5777
- * Stops the screen share
5778
- * @returns {Promise} see #updateShare
5779
- * @public
5780
- * @memberof Meeting
5818
+ * Requests screen share floor if such request is pending.
5819
+ * It should be called whenever meeting state changes to JOINED
5820
+ *
5821
+ * @returns {void}
5781
5822
  */
5782
- // Internal only, temporarily allows optional params
5783
- // eslint-disable-next-line valid-jsdoc
5784
- public stopShare(options = {}) {
5785
- return this.updateShare({
5786
- sendShare: false,
5787
- receiveShare: this.mediaProperties.mediaDirection.receiveShare,
5788
- ...options,
5789
- });
5823
+ private requestScreenShareFloorIfPending() {
5824
+ if (this.floorGrantPending && this.state === MEETING_STATE.STATES.JOINED) {
5825
+ this.requestScreenShareFloor().then(() => {
5826
+ this.floorGrantPending = false;
5827
+ });
5828
+ }
5790
5829
  }
5791
5830
 
5792
5831
  /**
@@ -5798,11 +5837,10 @@ export default class Meeting extends StatelessWebexPlugin {
5798
5837
  private releaseScreenShareFloor() {
5799
5838
  const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
5800
5839
 
5801
- if (content && this.mediaProperties.mediaDirection.sendShare) {
5840
+ if (content) {
5802
5841
  Metrics.postEvent({event: eventType.SHARE_STOPPED, meeting: this});
5803
- Media.stopTracks(this.mediaProperties.shareTrack);
5804
5842
 
5805
- if (content.floor.beneficiary.id !== this.selfId) {
5843
+ if (content.floor?.beneficiary.id !== this.selfId) {
5806
5844
  // remote participant started sharing and caused our sharing to stop, we don't want to send any floor action request in that case
5807
5845
  this.isSharing = false;
5808
5846
 
@@ -5834,7 +5872,10 @@ export default class Meeting extends StatelessWebexPlugin {
5834
5872
  });
5835
5873
  }
5836
5874
 
5837
- return Promise.reject(new ParameterError('Cannot stop share without content'));
5875
+ // according to Locus there is no content, so we don't need to release the floor (it's probably already been released)
5876
+ this.isSharing = false;
5877
+
5878
+ return Promise.resolve();
5838
5879
  }
5839
5880
 
5840
5881
  /**
@@ -5844,7 +5885,50 @@ export default class Meeting extends StatelessWebexPlugin {
5844
5885
  * @memberof Meeting
5845
5886
  */
5846
5887
  public startRecording() {
5847
- return MeetingUtil.startRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
5888
+ return this.recordingController.startRecording();
5889
+ }
5890
+
5891
+ /**
5892
+ * set the mute on entry flag for participants if you're the host
5893
+ * @returns {Promise}
5894
+ * @param {boolean} enabled
5895
+ * @public
5896
+ * @memberof Meeting
5897
+ */
5898
+ public setMuteOnEntry(enabled: boolean) {
5899
+ return this.controlsOptionsManager.setMuteOnEntry(enabled);
5900
+ }
5901
+
5902
+ /**
5903
+ * set the disallow unmute flag for participants if you're the host
5904
+ * @returns {Promise}
5905
+ * @param {boolean} enabled
5906
+ * @public
5907
+ * @memberof Meeting
5908
+ */
5909
+ public setDisallowUnmute(enabled: boolean) {
5910
+ return this.controlsOptionsManager.setDisallowUnmute(enabled);
5911
+ }
5912
+
5913
+ /**
5914
+ * set the mute all flag for participants if you're the host
5915
+ * @returns {Promise}
5916
+ * @param {boolean} mutedEnabled
5917
+ * @param {boolean} disallowUnmuteEnabled
5918
+ * @param {boolean} muteOnEntryEnabled
5919
+ * @public
5920
+ * @memberof Meeting
5921
+ */
5922
+ public setMuteAll(
5923
+ mutedEnabled: boolean,
5924
+ disallowUnmuteEnabled: boolean,
5925
+ muteOnEntryEnabled: boolean
5926
+ ) {
5927
+ return this.controlsOptionsManager.setMuteAll(
5928
+ mutedEnabled,
5929
+ disallowUnmuteEnabled,
5930
+ muteOnEntryEnabled
5931
+ );
5848
5932
  }
5849
5933
 
5850
5934
  /**
@@ -5854,7 +5938,7 @@ export default class Meeting extends StatelessWebexPlugin {
5854
5938
  * @memberof Meeting
5855
5939
  */
5856
5940
  public stopRecording() {
5857
- return MeetingUtil.stopRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
5941
+ return this.recordingController.stopRecording();
5858
5942
  }
5859
5943
 
5860
5944
  /**
@@ -5864,7 +5948,7 @@ export default class Meeting extends StatelessWebexPlugin {
5864
5948
  * @memberof Meeting
5865
5949
  */
5866
5950
  public pauseRecording() {
5867
- return MeetingUtil.pauseRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
5951
+ return this.recordingController.pauseRecording();
5868
5952
  }
5869
5953
 
5870
5954
  /**
@@ -5874,7 +5958,7 @@ export default class Meeting extends StatelessWebexPlugin {
5874
5958
  * @memberof Meeting
5875
5959
  */
5876
5960
  public resumeRecording() {
5877
- return MeetingUtil.resumeRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
5961
+ return this.recordingController.resumeRecording();
5878
5962
  }
5879
5963
 
5880
5964
  /**
@@ -6048,11 +6132,6 @@ export default class Meeting extends StatelessWebexPlugin {
6048
6132
  main: layoutInfo.main,
6049
6133
  content: layoutInfo.content,
6050
6134
  })
6051
- .then((response) => {
6052
- if (response && response.body && response.body.locus) {
6053
- this.locusInfo.onFullLocus(response.body.locus);
6054
- }
6055
- })
6056
6135
  .catch((error) => {
6057
6136
  LoggerProxy.logger.error('Meeting:index#changeVideoLayout --> Error ', error);
6058
6137
 
@@ -6060,62 +6139,6 @@ export default class Meeting extends StatelessWebexPlugin {
6060
6139
  });
6061
6140
  }
6062
6141
 
6063
- /**
6064
- * Sets the quality of the local video stream
6065
- * @param {String} level {LOW|MEDIUM|HIGH}
6066
- * @returns {Promise<MediaStream>} localStream
6067
- */
6068
- setLocalVideoQuality(level: string) {
6069
- LoggerProxy.logger.log(`Meeting:index#setLocalVideoQuality --> Setting quality to ${level}`);
6070
-
6071
- if (!VIDEO_RESOLUTIONS[level]) {
6072
- return this.rejectWithErrorLog(`Meeting:index#setLocalVideoQuality --> ${level} not defined`);
6073
- }
6074
-
6075
- if (!this.mediaProperties.mediaDirection.sendVideo) {
6076
- return this.rejectWithErrorLog(
6077
- 'Meeting:index#setLocalVideoQuality --> unable to change video quality, sendVideo is disabled'
6078
- );
6079
- }
6080
-
6081
- // If level is already the same, don't do anything
6082
- if (level === this.mediaProperties.localQualityLevel) {
6083
- LoggerProxy.logger.warn(
6084
- `Meeting:index#setLocalQualityLevel --> Quality already set to ${level}`
6085
- );
6086
-
6087
- return Promise.resolve();
6088
- }
6089
-
6090
- // Set the quality level in properties
6091
- this.mediaProperties.setLocalQualityLevel(level);
6092
-
6093
- const mediaDirection = {
6094
- sendAudio: this.mediaProperties.mediaDirection.sendAudio,
6095
- sendVideo: this.mediaProperties.mediaDirection.sendVideo,
6096
- sendShare: this.mediaProperties.mediaDirection.sendShare,
6097
- };
6098
-
6099
- // When changing local video quality level
6100
- // Need to stop current track first as chrome doesn't support resolution upscaling(for eg. changing 480p to 720p)
6101
- // Without feeding it a new track
6102
- // open bug link: https://bugs.chromium.org/p/chromium/issues/detail?id=943469
6103
- if (isBrowser('chrome') && this.mediaProperties.videoTrack)
6104
- Media.stopTracks(this.mediaProperties.videoTrack);
6105
-
6106
- return this.getMediaStreams(mediaDirection, VIDEO_RESOLUTIONS[level]).then(
6107
- async ([localStream]) => {
6108
- await this.updateVideo({
6109
- sendVideo: true,
6110
- receiveVideo: true,
6111
- stream: localStream,
6112
- });
6113
-
6114
- return localStream;
6115
- }
6116
- );
6117
- }
6118
-
6119
6142
  /**
6120
6143
  * Sets the quality level of the remote incoming media
6121
6144
  * @param {String} level {LOW|MEDIUM|HIGH}
@@ -6151,129 +6174,7 @@ export default class Meeting extends StatelessWebexPlugin {
6151
6174
  // Set the quality level in properties
6152
6175
  this.mediaProperties.setRemoteQualityLevel(level);
6153
6176
 
6154
- return this.updateMedia({mediaSettings: this.mediaProperties.mediaDirection});
6155
- }
6156
-
6157
- /**
6158
- * This is deprecated, please use setLocalVideoQuality for setting local and setRemoteQualityLevel for remote
6159
- * @param {String} level {LOW|MEDIUM|HIGH}
6160
- * @returns {Promise}
6161
- * @deprecated After FHD support
6162
- */
6163
- setMeetingQuality(level: string) {
6164
- LoggerProxy.logger.log(`Meeting:index#setMeetingQuality --> Setting quality to ${level}`);
6165
-
6166
- if (!QUALITY_LEVELS[level]) {
6167
- return this.rejectWithErrorLog(`Meeting:index#setMeetingQuality --> ${level} not defined`);
6168
- }
6169
-
6170
- const previousLevel = {
6171
- local: this.mediaProperties.localQualityLevel,
6172
- remote: this.mediaProperties.remoteQualityLevel,
6173
- };
6174
-
6175
- // If level is already the same, don't do anything
6176
- if (
6177
- level === this.mediaProperties.localQualityLevel &&
6178
- level === this.mediaProperties.remoteQualityLevel
6179
- ) {
6180
- LoggerProxy.logger.warn(
6181
- `Meeting:index#setMeetingQuality --> Quality already set to ${level}`
6182
- );
6183
-
6184
- return Promise.resolve();
6185
- }
6186
-
6187
- // Determine the direction of our current media
6188
- const {receiveAudio, receiveVideo, sendVideo} = this.mediaProperties.mediaDirection;
6189
-
6190
- return (sendVideo ? this.setLocalVideoQuality(level) : Promise.resolve())
6191
- .then(() =>
6192
- receiveAudio || receiveVideo ? this.setRemoteQualityLevel(level) : Promise.resolve()
6193
- )
6194
- .catch((error) => {
6195
- // From troubleshooting it seems that the stream itself doesn't change the max-fs if the peer connection isn't stable
6196
- this.mediaProperties.setLocalQualityLevel(previousLevel.local);
6197
- this.mediaProperties.setRemoteQualityLevel(previousLevel.remote);
6198
-
6199
- LoggerProxy.logger.error(`Meeting:index#setMeetingQuality --> ${error.message}`);
6200
-
6201
- Metrics.sendBehavioralMetric(
6202
- BEHAVIORAL_METRICS.SET_MEETING_QUALITY_FAILURE,
6203
- {
6204
- correlation_id: this.correlationId,
6205
- locus_id: this.locusUrl.split('/').pop(),
6206
- reason: error.message,
6207
- stack: error.stack,
6208
- },
6209
- {
6210
- type: error.name,
6211
- }
6212
- );
6213
-
6214
- return Promise.reject(error);
6215
- });
6216
- }
6217
-
6218
- /**
6219
- *
6220
- * NOTE: this method can only be used with transcoded meetings, for multistream use publishTrack()
6221
- *
6222
- * @param {Object} options parameter
6223
- * @param {Boolean} options.sendAudio send audio from the display share
6224
- * @param {Boolean} options.sendShare send video from the display share
6225
- * @param {Object} options.sharePreferences
6226
- * @param {MediaTrackConstraints} options.sharePreferences.shareConstraints constraints to apply to video
6227
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints}
6228
- * @param {Boolean} options.sharePreferences.highFrameRate if shareConstraints isn't provided, set default values based off of this boolean
6229
- * @returns {Promise}
6230
- */
6231
- shareScreen(
6232
- options: {
6233
- sendAudio: boolean;
6234
- sendShare: boolean;
6235
- sharePreferences: {shareConstraints: MediaTrackConstraints};
6236
- } = {} as any
6237
- ) {
6238
- LoggerProxy.logger.log('Meeting:index#shareScreen --> Getting local share');
6239
-
6240
- const shareConstraints = {
6241
- sendShare: true,
6242
- sendAudio: false,
6243
- ...options,
6244
- };
6245
-
6246
- // @ts-ignore - config coming from registerPlugin
6247
- return Media.getDisplayMedia(shareConstraints, this.config)
6248
- .then((shareStream) =>
6249
- this.updateShare({
6250
- sendShare: true,
6251
- receiveShare: this.mediaProperties.mediaDirection.receiveShare,
6252
- stream: shareStream,
6253
- })
6254
- )
6255
- .catch((error) => {
6256
- // Whenever there is a failure when trying to access a user's display
6257
- // report it as an Behavioral metric
6258
- // This gives visibility into common errors and can help
6259
- // with further troubleshooting
6260
-
6261
- // This metrics will get erros for getDisplayMedia and share errors for now
6262
- // TODO: The getDisplayMedia errors need to be moved inside `media.getDisplayMedia`
6263
- const metricName = BEHAVIORAL_METRICS.GET_DISPLAY_MEDIA_FAILURE;
6264
- const data = {
6265
- correlation_id: this.correlationId,
6266
- locus_id: this.locusUrl.split('/').pop(),
6267
- reason: error.message,
6268
- stack: error.stack,
6269
- };
6270
- const metadata = {
6271
- type: error.name,
6272
- };
6273
-
6274
- Metrics.sendBehavioralMetric(metricName, data, metadata);
6275
- throw new MediaError('Unable to retrieve display media stream', error);
6276
- });
6177
+ return this.updateTranscodedMediaConnection();
6277
6178
  }
6278
6179
 
6279
6180
  /**
@@ -6283,20 +6184,18 @@ export default class Meeting extends StatelessWebexPlugin {
6283
6184
  * @param {MediaStream} localShare
6284
6185
  * @returns {undefined}
6285
6186
  */
6286
- private handleShareTrackEnded(localShare: MediaStream) {
6187
+ private handleShareTrackEnded = async () => {
6287
6188
  if (this.wirelessShare) {
6288
6189
  this.leave({reason: MEETING_REMOVED_REASON.USER_ENDED_SHARE_STREAMS});
6289
6190
  } else {
6290
- // Skip checking for a stable peerConnection
6291
- // to allow immediately stopping screenshare
6292
- this.stopShare({
6293
- skipSignalingCheck: true,
6294
- }).catch((error) => {
6191
+ try {
6192
+ await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo: screen share audio (SPARK-399690)
6193
+ } catch (error) {
6295
6194
  LoggerProxy.logger.log(
6296
6195
  'Meeting:index#handleShareTrackEnded --> Error stopping share: ',
6297
6196
  error
6298
6197
  );
6299
- });
6198
+ }
6300
6199
  }
6301
6200
 
6302
6201
  Trigger.trigger(
@@ -6307,11 +6206,10 @@ export default class Meeting extends StatelessWebexPlugin {
6307
6206
  },
6308
6207
  EVENT_TRIGGERS.MEETING_STOPPED_SHARING_LOCAL,
6309
6208
  {
6310
- type: EVENT_TYPES.LOCAL_SHARE,
6311
- stream: localShare,
6209
+ reason: SHARE_STOPPED_REASON.TRACK_ENDED,
6312
6210
  }
6313
6211
  );
6314
- }
6212
+ };
6315
6213
 
6316
6214
  /**
6317
6215
  * Emits the 'network:quality' event
@@ -6341,14 +6239,16 @@ export default class Meeting extends StatelessWebexPlugin {
6341
6239
 
6342
6240
  /**
6343
6241
  * Handle logging the media
6344
- * @param {Object} audioTrack The audio track
6345
- * @param {Object} videoTrack The video track
6242
+ * @param {Object} mediaProperties
6346
6243
  * @private
6347
6244
  * @returns {undefined}
6348
6245
  */
6349
- private handleMediaLogging({audioTrack, videoTrack}: any) {
6350
- MeetingUtil.handleVideoLogging(videoTrack);
6351
- MeetingUtil.handleAudioLogging(audioTrack);
6246
+ private handleMediaLogging(mediaProperties: {
6247
+ audioTrack?: LocalMicrophoneTrack;
6248
+ videoTrack?: LocalCameraTrack;
6249
+ }) {
6250
+ MeetingUtil.handleVideoLogging(mediaProperties.videoTrack);
6251
+ MeetingUtil.handleAudioLogging(mediaProperties.audioTrack);
6352
6252
  }
6353
6253
 
6354
6254
  /**
@@ -6437,7 +6337,7 @@ export default class Meeting extends StatelessWebexPlugin {
6437
6337
  const end = this.endLocalSDPGenRemoteSDPRecvDelay;
6438
6338
 
6439
6339
  if (start && end) {
6440
- const calculatedDelay = end - start;
6340
+ const calculatedDelay = Math.round(end - start);
6441
6341
 
6442
6342
  return calculatedDelay > METRICS_JOIN_TIMES_MAX_DURATION ? undefined : calculatedDelay;
6443
6343
  }
@@ -6449,26 +6349,26 @@ export default class Meeting extends StatelessWebexPlugin {
6449
6349
  *
6450
6350
  * @returns {undefined}
6451
6351
  */
6452
- setStartCallInitiateJoinReq() {
6453
- this.startCallInitiateJoinReq = performance.now();
6454
- this.endCallInitiateJoinReq = undefined;
6352
+ setStartCallInitJoinReq() {
6353
+ this.startCallInitJoinReq = performance.now();
6354
+ this.endCallInitJoinReq = undefined;
6455
6355
  }
6456
6356
 
6457
6357
  /**
6458
6358
  *
6459
6359
  * @returns {undefined}
6460
6360
  */
6461
- setEndCallInitiateJoinReq() {
6462
- this.endCallInitiateJoinReq = performance.now();
6361
+ setEndCallInitJoinReq() {
6362
+ this.endCallInitJoinReq = performance.now();
6463
6363
  }
6464
6364
 
6465
6365
  /**
6466
6366
  *
6467
6367
  * @returns {string} duration between call initiate and sending join request to locus
6468
6368
  */
6469
- getCallInitiateJoinReq() {
6470
- const start = this.startCallInitiateJoinReq;
6471
- const end = this.endCallInitiateJoinReq;
6369
+ getCallInitJoinReq() {
6370
+ const start = this.startCallInitJoinReq;
6371
+ const end = this.endCallInitJoinReq;
6472
6372
 
6473
6373
  if (start && end) {
6474
6374
  const calculatedDelay = end - start;
@@ -6505,7 +6405,7 @@ export default class Meeting extends StatelessWebexPlugin {
6505
6405
  const end = this.endJoinReqResp;
6506
6406
 
6507
6407
  if (start && end) {
6508
- const calculatedDelay = end - start;
6408
+ const calculatedDelay = Math.round(end - start);
6509
6409
 
6510
6410
  return calculatedDelay > METRICS_JOIN_TIMES_MAX_DURATION ? undefined : calculatedDelay;
6511
6411
  }
@@ -6518,10 +6418,45 @@ export default class Meeting extends StatelessWebexPlugin {
6518
6418
  * @returns {string} duration between call initiate and successful locus join (even if it is in lobby)
6519
6419
  */
6520
6420
  getTotalJmt() {
6521
- const start = this.startCallInitiateJoinReq;
6421
+ const start = this.startCallInitJoinReq;
6522
6422
  const end = this.endJoinReqResp;
6523
6423
 
6524
- return start && end ? end - start : undefined;
6424
+ return start && end ? Math.round(end - start) : undefined;
6425
+ }
6426
+
6427
+ /**
6428
+ *
6429
+ * @returns {string} one of 'attendee','host','cohost', returns the user type of the current user
6430
+ */
6431
+ getCurUserType() {
6432
+ const {roles} = this;
6433
+ if (roles) {
6434
+ if (roles.includes(SELF_ROLES.MODERATOR)) {
6435
+ return 'host';
6436
+ }
6437
+ if (roles.includes(SELF_ROLES.COHOST)) {
6438
+ return 'cohost';
6439
+ }
6440
+ if (roles.includes(SELF_ROLES.ATTENDEE)) {
6441
+ return 'attendee';
6442
+ }
6443
+ }
6444
+
6445
+ return null;
6446
+ }
6447
+
6448
+ /**
6449
+ *
6450
+ * @returns {string} one of 'login-ci','unverified-guest', returns the login type of the current user
6451
+ */
6452
+ getCurLoginType() {
6453
+ // @ts-ignore
6454
+ if (this.webex.canAuthorize) {
6455
+ // @ts-ignore
6456
+ return this.webex.credentials.isUnverifiedGuest ? 'unverified-guest' : 'login-ci';
6457
+ }
6458
+
6459
+ return null;
6525
6460
  }
6526
6461
 
6527
6462
  /**
@@ -6611,121 +6546,6 @@ export default class Meeting extends StatelessWebexPlugin {
6611
6546
  }
6612
6547
  };
6613
6548
 
6614
- /**
6615
- * Internal API to return status of BNR
6616
- * @returns {Boolean}
6617
- * @public
6618
- * @memberof Meeting
6619
- */
6620
- public isBnrEnabled() {
6621
- return this.effects && this.effects.isBnrEnabled();
6622
- }
6623
-
6624
- /**
6625
- * Internal API to obtain BNR enabled MediaStream
6626
- * @returns {Promise<MediaStreamTrack>}
6627
- * @private
6628
- * @param {MedaiStreamTrack} audioTrack from updateAudio
6629
- * @memberof Meeting
6630
- */
6631
- private async internal_enableBNR(audioTrack: any) {
6632
- try {
6633
- LoggerProxy.logger.info('Meeting:index#internal_enableBNR. Internal enable BNR called');
6634
- const bnrAudioTrack = await WebRTCMedia.Effects.BNR.enableBNR(audioTrack);
6635
-
6636
- LoggerProxy.logger.info(
6637
- 'Meeting:index#internal_enableBNR. BNR enabled track obtained from WebRTC & returned as stream'
6638
- );
6639
-
6640
- return bnrAudioTrack;
6641
- } catch (error) {
6642
- LoggerProxy.logger.error('Meeting:index#internal_enableBNR.', error);
6643
- throw error;
6644
- }
6645
- }
6646
-
6647
- /**
6648
- * Enable the audio track with BNR for a meeting
6649
- * @returns {Promise} resolves the data from enable bnr or rejects if there is no audio or audio is muted
6650
- * @public
6651
- * @memberof Meeting
6652
- */
6653
- public enableBNR() {
6654
- if (
6655
- typeof this.mediaProperties === 'undefined' ||
6656
- typeof this.mediaProperties.audioTrack === 'undefined'
6657
- ) {
6658
- return Promise.reject(new Error("Meeting doesn't have an audioTrack attached"));
6659
- }
6660
-
6661
- if (this.isAudioMuted()) {
6662
- return Promise.reject(new Error('Cannot enable BNR while meeting is muted'));
6663
- }
6664
-
6665
- this.effects = this.effects || createEffectsState('BNR');
6666
-
6667
- const LOG_HEADER = 'Meeting:index#enableBNR -->';
6668
-
6669
- return logRequest(
6670
- this.effects
6671
- .handleClientRequest(true, this)
6672
- .then((res) => {
6673
- LoggerProxy.logger.info('Meeting:index#enableBNR. Enable bnr completed');
6674
-
6675
- return res;
6676
- })
6677
- .catch((error) => {
6678
- throw error;
6679
- }),
6680
- {
6681
- header: `${LOG_HEADER} enable bnr`,
6682
- success: `${LOG_HEADER} enable bnr success`,
6683
- failure: `${LOG_HEADER} enable bnr failure, `,
6684
- }
6685
- );
6686
- }
6687
-
6688
- /**
6689
- * Disable the BNR for an audio track
6690
- * @returns {Promise} resolves the data from disable bnr or rejects if there is no audio set
6691
- * @public
6692
- * @memberof Meeting
6693
- */
6694
- public disableBNR() {
6695
- if (
6696
- typeof this.mediaProperties === 'undefined' ||
6697
- typeof this.mediaProperties.audioTrack === 'undefined'
6698
- ) {
6699
- return Promise.reject(new Error("Meeting doesn't have an audioTrack attached"));
6700
- }
6701
-
6702
- if (!this.isBnrEnabled()) {
6703
- return Promise.reject(new Error('Can not disable as BNR is not enabled'));
6704
- }
6705
-
6706
- this.effects = this.effects || createEffectsState('BNR');
6707
-
6708
- const LOG_HEADER = 'Meeting:index#disableBNR -->';
6709
-
6710
- return logRequest(
6711
- this.effects
6712
- .handleClientRequest(false, this)
6713
- .then((res) => {
6714
- LoggerProxy.logger.info('Meeting:index#disableBNR. Disable bnr completed');
6715
-
6716
- return res;
6717
- })
6718
- .catch((error) => {
6719
- throw error;
6720
- }),
6721
- {
6722
- header: `${LOG_HEADER} disable bnr`,
6723
- success: `${LOG_HEADER} disable bnr success`,
6724
- failure: `${LOG_HEADER} disable bnr failure, `,
6725
- }
6726
- );
6727
- }
6728
-
6729
6549
  /**
6730
6550
  * starts keepAlives being sent
6731
6551
  * @returns {void}
@@ -6791,13 +6611,13 @@ export default class Meeting extends StatelessWebexPlugin {
6791
6611
  /**
6792
6612
  * Send a reaction inside the meeting.
6793
6613
  *
6794
- * @param {ReactionType} reactionType - type of reaction to be sent. Example: "thumbs_up"
6614
+ * @param {ReactionServerType} reactionType - type of reaction to be sent. Example: "thumbs_up"
6795
6615
  * @param {SkinToneType} skinToneType - skin tone for the reaction. Example: "medium_dark"
6796
6616
  * @returns {Promise}
6797
6617
  * @public
6798
6618
  * @memberof Meeting
6799
6619
  */
6800
- public sendReaction(reactionType: ReactionType, skinToneType?: SkinToneType) {
6620
+ public sendReaction(reactionType: ReactionServerType, skinToneType?: SkinToneType) {
6801
6621
  const reactionChannelUrl = this.locusInfo?.controls?.reactions?.reactionChannelUrl as string;
6802
6622
  const participantId = this.members.selfId;
6803
6623
 
@@ -6840,4 +6660,222 @@ export default class Meeting extends StatelessWebexPlugin {
6840
6660
  requestingParticipantId: this.members.selfId,
6841
6661
  });
6842
6662
  }
6663
+
6664
+ /**
6665
+ * Throws if we don't have a media connection created
6666
+ *
6667
+ * @returns {void}
6668
+ */
6669
+ private checkMediaConnection() {
6670
+ if (this.mediaProperties?.webrtcMediaConnection) {
6671
+ return;
6672
+ }
6673
+ throw new NoMediaEstablishedYetError();
6674
+ }
6675
+
6676
+ /**
6677
+ * Method to enable or disable the 'Music mode' effect on audio track
6678
+ *
6679
+ * @param {boolean} shouldEnableMusicMode
6680
+ * @returns {Promise}
6681
+ */
6682
+ async enableMusicMode(shouldEnableMusicMode: boolean) {
6683
+ this.checkMediaConnection();
6684
+
6685
+ if (!this.isMultistream) {
6686
+ throw new Error('enableMusicMode() only supported with multistream');
6687
+ }
6688
+
6689
+ if (shouldEnableMusicMode) {
6690
+ await this.mediaProperties.webrtcMediaConnection.setCodecParameters(MediaType.AudioMain, {
6691
+ maxaveragebitrate: '64000',
6692
+ maxplaybackrate: '48000',
6693
+ });
6694
+ } else {
6695
+ await this.mediaProperties.webrtcMediaConnection.deleteCodecParameters(MediaType.AudioMain, [
6696
+ 'maxaveragebitrate',
6697
+ 'maxplaybackrate',
6698
+ ]);
6699
+ }
6700
+ }
6701
+
6702
+ /** Updates the tracks being sent on the transcoded media connection
6703
+ *
6704
+ * @returns {Promise<void>}
6705
+ */
6706
+ private updateTranscodedMediaConnection(): Promise<void> {
6707
+ const LOG_HEADER = 'Meeting:index#updateTranscodedMediaConnection -->';
6708
+
6709
+ LoggerProxy.logger.info(`${LOG_HEADER} starting`);
6710
+
6711
+ if (!this.canUpdateMedia()) {
6712
+ return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.TRANSCODED_MEDIA_CONNECTION);
6713
+ }
6714
+
6715
+ return this.mediaProperties.webrtcMediaConnection
6716
+ .update({
6717
+ localTracks: {
6718
+ audio: this.mediaProperties.audioTrack?.underlyingTrack || null,
6719
+ video: this.mediaProperties.videoTrack?.underlyingTrack || null,
6720
+ screenShareVideo: this.mediaProperties.shareTrack?.underlyingTrack || null,
6721
+ },
6722
+ direction: {
6723
+ audio: Media.getDirection(
6724
+ true,
6725
+ this.mediaProperties.mediaDirection.receiveAudio,
6726
+ this.mediaProperties.mediaDirection.sendAudio
6727
+ ),
6728
+ video: Media.getDirection(
6729
+ true,
6730
+ this.mediaProperties.mediaDirection.receiveVideo,
6731
+ this.mediaProperties.mediaDirection.sendVideo
6732
+ ),
6733
+ screenShareVideo: Media.getDirection(
6734
+ false,
6735
+ this.mediaProperties.mediaDirection.receiveShare,
6736
+ this.mediaProperties.mediaDirection.sendShare
6737
+ ),
6738
+ },
6739
+ remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
6740
+ })
6741
+ .then(() => {
6742
+ LoggerProxy.logger.info(`${LOG_HEADER} done`);
6743
+ })
6744
+ .catch((error) => {
6745
+ LoggerProxy.logger.error(`${LOG_HEADER} Error: `, error);
6746
+
6747
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.UPDATE_MEDIA_FAILURE, {
6748
+ correlation_id: this.correlationId,
6749
+ locus_id: this.locusUrl.split('/').pop(),
6750
+ reason: error.message,
6751
+ stack: error.stack,
6752
+ });
6753
+
6754
+ throw error;
6755
+ });
6756
+ }
6757
+
6758
+ /**
6759
+ * Publishes a track.
6760
+ *
6761
+ * @param {LocalTrack} track to publish
6762
+ * @returns {Promise}
6763
+ */
6764
+ private async publishTrack(track?: LocalTrack) {
6765
+ if (!track) {
6766
+ return;
6767
+ }
6768
+
6769
+ if (this.mediaProperties.webrtcMediaConnection) {
6770
+ if (this.isMultistream) {
6771
+ await this.mediaProperties.webrtcMediaConnection.publishTrack(track);
6772
+ } else {
6773
+ track.setPublished(true); // for multistream, this call is done by WCME
6774
+ }
6775
+ }
6776
+ }
6777
+
6778
+ /**
6779
+ * Un-publishes a track.
6780
+ *
6781
+ * @param {LocalTrack} track to unpublish
6782
+ * @returns {Promise}
6783
+ */
6784
+ private async unpublishTrack(track?: LocalTrack) {
6785
+ if (!track) {
6786
+ return;
6787
+ }
6788
+
6789
+ if (this.isMultistream && this.mediaProperties.webrtcMediaConnection) {
6790
+ await this.mediaProperties.webrtcMediaConnection.unpublishTrack(track);
6791
+ } else {
6792
+ track.setPublished(false); // for multistream, this call is done by WCME
6793
+ }
6794
+ }
6795
+
6796
+ /**
6797
+ * Publishes specified local tracks in the meeting
6798
+ *
6799
+ * @param {Object} tracks
6800
+ * @returns {Promise}
6801
+ */
6802
+ async publishTracks(tracks: LocalTracks): Promise<void> {
6803
+ this.checkMediaConnection();
6804
+
6805
+ this.annotationInfo = tracks.annotationInfo;
6806
+
6807
+ if (
6808
+ !tracks.microphone &&
6809
+ !tracks.camera &&
6810
+ !tracks.screenShare?.audio &&
6811
+ !tracks.screenShare?.video
6812
+ ) {
6813
+ // nothing to do
6814
+ return;
6815
+ }
6816
+
6817
+ let floorRequestNeeded = false;
6818
+
6819
+ if (tracks.screenShare?.video) {
6820
+ await this.setLocalShareTrack(tracks.screenShare?.video);
6821
+
6822
+ floorRequestNeeded = true;
6823
+ }
6824
+
6825
+ if (tracks.microphone) {
6826
+ await this.setLocalAudioTrack(tracks.microphone);
6827
+ }
6828
+
6829
+ if (tracks.camera) {
6830
+ await this.setLocalVideoTrack(tracks.camera);
6831
+ }
6832
+
6833
+ if (!this.isMultistream) {
6834
+ await this.updateTranscodedMediaConnection();
6835
+ }
6836
+
6837
+ if (floorRequestNeeded) {
6838
+ // we're sending the http request to Locus to request the screen share floor
6839
+ // only after the SDP update, because that's how it's always been done for transcoded meetings
6840
+ // and also if sharing from the start, we need confluence to have been created
6841
+ await this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.SHARE_FLOOR_REQUEST);
6842
+ }
6843
+ }
6844
+
6845
+ /**
6846
+ * Un-publishes specified local tracks in the meeting
6847
+ *
6848
+ * @param {Array<MediaStreamTrack>} tracks
6849
+ * @returns {Promise}
6850
+ */
6851
+ async unpublishTracks(tracks: LocalTrack[]): Promise<void> {
6852
+ this.checkMediaConnection();
6853
+
6854
+ const promises = [];
6855
+
6856
+ for (const track of tracks.filter((t) => !!t)) {
6857
+ if (track === this.mediaProperties.shareTrack) {
6858
+ try {
6859
+ this.releaseScreenShareFloor(); // we ignore the returned promise here on purpose
6860
+ } catch (e) {
6861
+ // nothing to do here, error is logged already inside releaseScreenShareFloor()
6862
+ }
6863
+ promises.push(this.setLocalShareTrack(undefined));
6864
+ }
6865
+
6866
+ if (track === this.mediaProperties.audioTrack) {
6867
+ promises.push(this.setLocalAudioTrack(undefined));
6868
+ }
6869
+
6870
+ if (track === this.mediaProperties.videoTrack) {
6871
+ promises.push(this.setLocalVideoTrack(undefined));
6872
+ }
6873
+ }
6874
+
6875
+ if (!this.isMultistream) {
6876
+ promises.push(this.updateTranscodedMediaConnection());
6877
+ }
6878
+
6879
+ await Promise.all(promises);
6880
+ }
6843
6881
  }