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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (480) hide show
  1. package/README.md +45 -1
  2. package/dist/annotation/annotation.types.js +7 -0
  3. package/dist/annotation/annotation.types.js.map +1 -0
  4. package/dist/annotation/constants.js +49 -0
  5. package/dist/annotation/constants.js.map +1 -0
  6. package/dist/annotation/index.js +359 -0
  7. package/dist/annotation/index.js.map +1 -0
  8. package/dist/breakouts/breakout.js +193 -0
  9. package/dist/breakouts/breakout.js.map +1 -0
  10. package/dist/breakouts/collection.js +23 -0
  11. package/dist/breakouts/collection.js.map +1 -0
  12. package/dist/breakouts/edit-lock-error.js +52 -0
  13. package/dist/breakouts/edit-lock-error.js.map +1 -0
  14. package/dist/breakouts/events.js +43 -0
  15. package/dist/breakouts/events.js.map +1 -0
  16. package/dist/breakouts/index.js +1046 -0
  17. package/dist/breakouts/index.js.map +1 -0
  18. package/dist/breakouts/request.js +78 -0
  19. package/dist/breakouts/request.js.map +1 -0
  20. package/dist/breakouts/utils.js +67 -0
  21. package/dist/breakouts/utils.js.map +1 -0
  22. package/dist/common/browser-detection.js +1 -21
  23. package/dist/common/browser-detection.js.map +1 -1
  24. package/dist/common/collection.js +5 -20
  25. package/dist/common/collection.js.map +1 -1
  26. package/dist/common/config.js +0 -7
  27. package/dist/common/config.js.map +1 -1
  28. package/dist/common/errors/captcha-error.js +0 -21
  29. package/dist/common/errors/captcha-error.js.map +1 -1
  30. package/dist/common/errors/intent-to-join.js +0 -21
  31. package/dist/common/errors/intent-to-join.js.map +1 -1
  32. package/dist/common/errors/join-meeting.js +0 -21
  33. package/dist/common/errors/join-meeting.js.map +1 -1
  34. package/dist/common/errors/media.js +0 -21
  35. package/dist/common/errors/media.js.map +1 -1
  36. package/dist/common/errors/parameter.js +0 -28
  37. package/dist/common/errors/parameter.js.map +1 -1
  38. package/dist/common/errors/password-error.js +0 -21
  39. package/dist/common/errors/password-error.js.map +1 -1
  40. package/dist/common/errors/permission.js +0 -21
  41. package/dist/common/errors/permission.js.map +1 -1
  42. package/dist/common/errors/reconnection-in-progress.js +0 -17
  43. package/dist/common/errors/reconnection-in-progress.js.map +1 -1
  44. package/dist/common/errors/reconnection.js +0 -21
  45. package/dist/common/errors/reconnection.js.map +1 -1
  46. package/dist/common/errors/stats.js +0 -21
  47. package/dist/common/errors/stats.js.map +1 -1
  48. package/dist/common/errors/webex-errors.js +9 -43
  49. package/dist/common/errors/webex-errors.js.map +1 -1
  50. package/dist/common/errors/webex-meetings-error.js +1 -24
  51. package/dist/common/errors/webex-meetings-error.js.map +1 -1
  52. package/dist/common/events/events-scope.js +0 -22
  53. package/dist/common/events/events-scope.js.map +1 -1
  54. package/dist/common/events/events.js +0 -23
  55. package/dist/common/events/events.js.map +1 -1
  56. package/dist/common/events/trigger-proxy.js +0 -12
  57. package/dist/common/events/trigger-proxy.js.map +1 -1
  58. package/dist/common/events/util.js +0 -15
  59. package/dist/common/events/util.js.map +1 -1
  60. package/dist/common/logs/logger-config.js +0 -4
  61. package/dist/common/logs/logger-config.js.map +1 -1
  62. package/dist/common/logs/logger-proxy.js +1 -8
  63. package/dist/common/logs/logger-proxy.js.map +1 -1
  64. package/dist/common/logs/request.js +35 -61
  65. package/dist/common/logs/request.js.map +1 -1
  66. package/dist/common/queue.js +4 -14
  67. package/dist/common/queue.js.map +1 -1
  68. package/dist/config.js +7 -13
  69. package/dist/config.js.map +1 -1
  70. package/dist/constants.js +208 -64
  71. package/dist/constants.js.map +1 -1
  72. package/dist/controls-options-manager/constants.js +14 -0
  73. package/dist/controls-options-manager/constants.js.map +1 -0
  74. package/dist/controls-options-manager/enums.js +27 -0
  75. package/dist/controls-options-manager/enums.js.map +1 -0
  76. package/dist/controls-options-manager/index.js +297 -0
  77. package/dist/controls-options-manager/index.js.map +1 -0
  78. package/dist/controls-options-manager/types.js +7 -0
  79. package/dist/controls-options-manager/types.js.map +1 -0
  80. package/dist/controls-options-manager/util.js +300 -0
  81. package/dist/controls-options-manager/util.js.map +1 -0
  82. package/dist/index.js +78 -17
  83. package/dist/index.js.map +1 -1
  84. package/dist/locus-info/controlsUtils.js +100 -29
  85. package/dist/locus-info/controlsUtils.js.map +1 -1
  86. package/dist/locus-info/embeddedAppsUtils.js +3 -26
  87. package/dist/locus-info/embeddedAppsUtils.js.map +1 -1
  88. package/dist/locus-info/fullState.js +0 -15
  89. package/dist/locus-info/fullState.js.map +1 -1
  90. package/dist/locus-info/hostUtils.js +4 -12
  91. package/dist/locus-info/hostUtils.js.map +1 -1
  92. package/dist/locus-info/index.js +387 -208
  93. package/dist/locus-info/index.js.map +1 -1
  94. package/dist/locus-info/infoUtils.js +0 -38
  95. package/dist/locus-info/infoUtils.js.map +1 -1
  96. package/dist/locus-info/mediaSharesUtils.js +54 -38
  97. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  98. package/dist/locus-info/parser.js +90 -126
  99. package/dist/locus-info/parser.js.map +1 -1
  100. package/dist/locus-info/selfUtils.js +93 -92
  101. package/dist/locus-info/selfUtils.js.map +1 -1
  102. package/dist/media/index.js +70 -219
  103. package/dist/media/index.js.map +1 -1
  104. package/dist/media/properties.js +74 -198
  105. package/dist/media/properties.js.map +1 -1
  106. package/dist/media/util.js +1 -8
  107. package/dist/media/util.js.map +1 -1
  108. package/dist/mediaQualityMetrics/config.js +505 -495
  109. package/dist/mediaQualityMetrics/config.js.map +1 -1
  110. package/dist/meeting/in-meeting-actions.js +79 -14
  111. package/dist/meeting/in-meeting-actions.js.map +1 -1
  112. package/dist/meeting/index.js +2685 -3324
  113. package/dist/meeting/index.js.map +1 -1
  114. package/dist/meeting/locusMediaRequest.js +291 -0
  115. package/dist/meeting/locusMediaRequest.js.map +1 -0
  116. package/dist/meeting/muteState.js +243 -185
  117. package/dist/meeting/muteState.js.map +1 -1
  118. package/dist/meeting/request.js +296 -342
  119. package/dist/meeting/request.js.map +1 -1
  120. package/dist/meeting/request.type.js +0 -1
  121. package/dist/meeting/request.type.js.map +1 -1
  122. package/dist/meeting/state.js +16 -26
  123. package/dist/meeting/state.js.map +1 -1
  124. package/dist/meeting/util.js +446 -585
  125. package/dist/meeting/util.js.map +1 -1
  126. package/dist/meeting-info/collection.js +3 -25
  127. package/dist/meeting-info/collection.js.map +1 -1
  128. package/dist/meeting-info/index.js +8 -31
  129. package/dist/meeting-info/index.js.map +1 -1
  130. package/dist/meeting-info/meeting-info-v2.js +261 -242
  131. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  132. package/dist/meeting-info/request.js +1 -16
  133. package/dist/meeting-info/request.js.map +1 -1
  134. package/dist/meeting-info/util.js +98 -183
  135. package/dist/meeting-info/util.js.map +1 -1
  136. package/dist/meeting-info/utilv2.js +156 -232
  137. package/dist/meeting-info/utilv2.js.map +1 -1
  138. package/dist/meetings/collection.js +24 -20
  139. package/dist/meetings/collection.js.map +1 -1
  140. package/dist/meetings/index.js +526 -372
  141. package/dist/meetings/index.js.map +1 -1
  142. package/dist/meetings/meetings.types.js +7 -0
  143. package/dist/meetings/meetings.types.js.map +1 -0
  144. package/dist/meetings/request.js +21 -40
  145. package/dist/meetings/request.js.map +1 -1
  146. package/dist/meetings/util.js +172 -141
  147. package/dist/meetings/util.js.map +1 -1
  148. package/dist/member/index.js +58 -57
  149. package/dist/member/index.js.map +1 -1
  150. package/dist/member/types.js +15 -0
  151. package/dist/member/types.js.map +1 -0
  152. package/dist/member/util.js +101 -69
  153. package/dist/member/util.js.map +1 -1
  154. package/dist/members/collection.js +12 -12
  155. package/dist/members/collection.js.map +1 -1
  156. package/dist/members/index.js +123 -162
  157. package/dist/members/index.js.map +1 -1
  158. package/dist/members/request.js +120 -85
  159. package/dist/members/request.js.map +1 -1
  160. package/dist/members/types.js +15 -0
  161. package/dist/members/types.js.map +1 -0
  162. package/dist/members/util.js +314 -260
  163. package/dist/members/util.js.map +1 -1
  164. package/dist/metrics/config.js +50 -16
  165. package/dist/metrics/config.js.map +1 -1
  166. package/dist/metrics/constants.js +4 -7
  167. package/dist/metrics/constants.js.map +1 -1
  168. package/dist/metrics/index.js +75 -147
  169. package/dist/metrics/index.js.map +1 -1
  170. package/dist/multistream/mediaRequestManager.js +170 -50
  171. package/dist/multistream/mediaRequestManager.js.map +1 -1
  172. package/dist/multistream/receiveSlot.js +58 -65
  173. package/dist/multistream/receiveSlot.js.map +1 -1
  174. package/dist/multistream/receiveSlotManager.js +73 -94
  175. package/dist/multistream/receiveSlotManager.js.map +1 -1
  176. package/dist/multistream/remoteMedia.js +55 -74
  177. package/dist/multistream/remoteMedia.js.map +1 -1
  178. package/dist/multistream/remoteMediaGroup.js +66 -43
  179. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  180. package/dist/multistream/remoteMediaManager.js +502 -442
  181. package/dist/multistream/remoteMediaManager.js.map +1 -1
  182. package/dist/networkQualityMonitor/index.js +24 -51
  183. package/dist/networkQualityMonitor/index.js.map +1 -1
  184. package/dist/personal-meeting-room/index.js +3 -38
  185. package/dist/personal-meeting-room/index.js.map +1 -1
  186. package/dist/personal-meeting-room/request.js +2 -33
  187. package/dist/personal-meeting-room/request.js.map +1 -1
  188. package/dist/personal-meeting-room/util.js +0 -13
  189. package/dist/personal-meeting-room/util.js.map +1 -1
  190. package/dist/reachability/index.js +190 -199
  191. package/dist/reachability/index.js.map +1 -1
  192. package/dist/reachability/request.js +14 -23
  193. package/dist/reachability/request.js.map +1 -1
  194. package/dist/reactions/constants.js +13 -0
  195. package/dist/reactions/constants.js.map +1 -0
  196. package/dist/reactions/reactions.js +2 -4
  197. package/dist/reactions/reactions.js.map +1 -1
  198. package/dist/reactions/reactions.type.js +18 -24
  199. package/dist/reactions/reactions.type.js.map +1 -1
  200. package/dist/reconnection-manager/index.js +356 -476
  201. package/dist/reconnection-manager/index.js.map +1 -1
  202. package/dist/recording-controller/enums.js +17 -0
  203. package/dist/recording-controller/enums.js.map +1 -0
  204. package/dist/recording-controller/index.js +343 -0
  205. package/dist/recording-controller/index.js.map +1 -0
  206. package/dist/recording-controller/util.js +63 -0
  207. package/dist/recording-controller/util.js.map +1 -0
  208. package/dist/roap/index.js +32 -75
  209. package/dist/roap/index.js.map +1 -1
  210. package/dist/roap/request.js +129 -136
  211. package/dist/roap/request.js.map +1 -1
  212. package/dist/roap/turnDiscovery.js +143 -103
  213. package/dist/roap/turnDiscovery.js.map +1 -1
  214. package/dist/statsAnalyzer/global.js +1 -95
  215. package/dist/statsAnalyzer/global.js.map +1 -1
  216. package/dist/statsAnalyzer/index.js +369 -462
  217. package/dist/statsAnalyzer/index.js.map +1 -1
  218. package/dist/statsAnalyzer/mqaUtil.js +144 -94
  219. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  220. package/dist/transcription/index.js +9 -44
  221. package/dist/transcription/index.js.map +1 -1
  222. package/dist/types/annotation/annotation.types.d.ts +43 -0
  223. package/dist/types/annotation/constants.d.ts +31 -0
  224. package/dist/types/annotation/index.d.ts +124 -0
  225. package/dist/types/breakouts/breakout.d.ts +8 -0
  226. package/dist/types/breakouts/collection.d.ts +5 -0
  227. package/dist/types/breakouts/edit-lock-error.d.ts +15 -0
  228. package/dist/types/breakouts/events.d.ts +2 -0
  229. package/dist/types/breakouts/index.d.ts +5 -0
  230. package/dist/types/breakouts/request.d.ts +22 -0
  231. package/dist/types/breakouts/utils.d.ts +15 -0
  232. package/dist/types/common/browser-detection.d.ts +9 -0
  233. package/dist/types/common/collection.d.ts +48 -0
  234. package/dist/types/common/config.d.ts +2 -0
  235. package/dist/types/common/errors/captcha-error.d.ts +15 -0
  236. package/dist/types/common/errors/intent-to-join.d.ts +16 -0
  237. package/dist/types/common/errors/join-meeting.d.ts +17 -0
  238. package/dist/types/common/errors/media.d.ts +15 -0
  239. package/dist/types/common/errors/parameter.d.ts +15 -0
  240. package/dist/types/common/errors/password-error.d.ts +15 -0
  241. package/dist/types/common/errors/permission.d.ts +14 -0
  242. package/dist/types/common/errors/reconnection-in-progress.d.ts +9 -0
  243. package/dist/types/common/errors/reconnection.d.ts +15 -0
  244. package/dist/types/common/errors/stats.d.ts +15 -0
  245. package/dist/types/common/errors/webex-errors.d.ts +69 -0
  246. package/dist/types/common/errors/webex-meetings-error.d.ts +20 -0
  247. package/dist/types/common/events/events-scope.d.ts +17 -0
  248. package/dist/types/common/events/events.d.ts +12 -0
  249. package/dist/types/common/events/trigger-proxy.d.ts +2 -0
  250. package/dist/types/common/events/util.d.ts +2 -0
  251. package/dist/types/common/logs/logger-config.d.ts +2 -0
  252. package/dist/types/common/logs/logger-proxy.d.ts +2 -0
  253. package/dist/types/common/logs/request.d.ts +34 -0
  254. package/dist/types/common/queue.d.ts +32 -0
  255. package/dist/types/config.d.ts +72 -0
  256. package/dist/types/constants.d.ts +978 -0
  257. package/dist/types/controls-options-manager/constants.d.ts +4 -0
  258. package/dist/types/controls-options-manager/enums.d.ts +15 -0
  259. package/dist/types/controls-options-manager/index.d.ts +136 -0
  260. package/dist/types/controls-options-manager/types.d.ts +43 -0
  261. package/dist/types/controls-options-manager/util.d.ts +1 -0
  262. package/dist/types/index.d.ts +7 -0
  263. package/dist/types/locus-info/controlsUtils.d.ts +2 -0
  264. package/dist/types/locus-info/embeddedAppsUtils.d.ts +2 -0
  265. package/dist/types/locus-info/fullState.d.ts +2 -0
  266. package/dist/types/locus-info/hostUtils.d.ts +2 -0
  267. package/dist/types/locus-info/index.d.ts +315 -0
  268. package/dist/types/locus-info/infoUtils.d.ts +2 -0
  269. package/dist/types/locus-info/mediaSharesUtils.d.ts +2 -0
  270. package/dist/types/locus-info/parser.d.ts +212 -0
  271. package/dist/types/locus-info/selfUtils.d.ts +2 -0
  272. package/dist/types/media/index.d.ts +34 -0
  273. package/dist/types/media/properties.d.ts +86 -0
  274. package/dist/types/media/util.d.ts +2 -0
  275. package/dist/types/mediaQualityMetrics/config.d.ts +365 -0
  276. package/dist/types/meeting/in-meeting-actions.d.ts +149 -0
  277. package/dist/types/meeting/index.d.ts +1509 -0
  278. package/dist/types/meeting/locusMediaRequest.d.ts +70 -0
  279. package/dist/types/meeting/muteState.d.ts +184 -0
  280. package/dist/types/meeting/request.d.ts +270 -0
  281. package/dist/types/meeting/request.type.d.ts +11 -0
  282. package/dist/types/meeting/state.d.ts +9 -0
  283. package/dist/types/meeting/util.d.ts +75 -0
  284. package/dist/types/meeting-info/collection.d.ts +20 -0
  285. package/dist/types/meeting-info/index.d.ts +57 -0
  286. package/dist/types/meeting-info/meeting-info-v2.d.ts +122 -0
  287. package/dist/types/meeting-info/request.d.ts +22 -0
  288. package/dist/types/meeting-info/util.d.ts +2 -0
  289. package/dist/types/meeting-info/utilv2.d.ts +2 -0
  290. package/dist/types/meetings/collection.d.ts +31 -0
  291. package/dist/types/meetings/index.d.ts +364 -0
  292. package/dist/types/meetings/meetings.types.d.ts +4 -0
  293. package/dist/types/meetings/request.d.ts +27 -0
  294. package/dist/types/meetings/util.d.ts +18 -0
  295. package/dist/types/member/index.d.ts +157 -0
  296. package/dist/types/member/types.d.ts +21 -0
  297. package/dist/types/member/util.d.ts +2 -0
  298. package/dist/types/members/collection.d.ts +29 -0
  299. package/dist/types/members/index.d.ts +353 -0
  300. package/dist/types/members/request.d.ts +114 -0
  301. package/dist/types/members/types.d.ts +24 -0
  302. package/dist/types/members/util.d.ts +210 -0
  303. package/dist/types/metrics/config.d.ts +195 -0
  304. package/dist/types/metrics/constants.d.ts +55 -0
  305. package/dist/types/metrics/index.d.ts +169 -0
  306. package/dist/types/multistream/mediaRequestManager.d.ts +104 -0
  307. package/dist/types/multistream/receiveSlot.d.ts +68 -0
  308. package/dist/types/multistream/receiveSlotManager.d.ts +56 -0
  309. package/dist/types/multistream/remoteMedia.d.ts +72 -0
  310. package/dist/types/multistream/remoteMediaGroup.d.ts +47 -0
  311. package/dist/types/multistream/remoteMediaManager.d.ts +277 -0
  312. package/dist/types/networkQualityMonitor/index.d.ts +70 -0
  313. package/dist/types/personal-meeting-room/index.d.ts +47 -0
  314. package/dist/types/personal-meeting-room/request.d.ts +14 -0
  315. package/dist/types/personal-meeting-room/util.d.ts +2 -0
  316. package/dist/types/reachability/index.d.ts +152 -0
  317. package/dist/types/reachability/request.d.ts +37 -0
  318. package/dist/types/reactions/constants.d.ts +3 -0
  319. package/dist/types/reactions/reactions.d.ts +4 -0
  320. package/dist/types/reactions/reactions.type.d.ts +52 -0
  321. package/dist/types/reconnection-manager/index.d.ts +126 -0
  322. package/dist/types/recording-controller/enums.d.ts +7 -0
  323. package/dist/types/recording-controller/index.d.ts +193 -0
  324. package/dist/types/recording-controller/util.d.ts +13 -0
  325. package/dist/types/roap/index.d.ts +77 -0
  326. package/dist/types/roap/request.d.ts +36 -0
  327. package/dist/types/roap/turnDiscovery.d.ts +91 -0
  328. package/dist/types/statsAnalyzer/global.d.ts +36 -0
  329. package/dist/types/statsAnalyzer/index.d.ts +200 -0
  330. package/dist/types/statsAnalyzer/mqaUtil.d.ts +24 -0
  331. package/dist/types/transcription/index.d.ts +64 -0
  332. package/package.json +28 -21
  333. package/src/annotation/annotation.types.ts +52 -0
  334. package/src/annotation/constants.ts +36 -0
  335. package/src/annotation/index.ts +343 -0
  336. package/src/breakouts/README.md +220 -0
  337. package/src/breakouts/breakout.ts +163 -0
  338. package/src/breakouts/collection.ts +19 -0
  339. package/src/breakouts/edit-lock-error.ts +25 -0
  340. package/src/breakouts/events.ts +37 -0
  341. package/src/breakouts/index.ts +921 -0
  342. package/src/breakouts/request.ts +55 -0
  343. package/src/breakouts/utils.ts +57 -0
  344. package/src/common/errors/webex-errors.ts +6 -2
  345. package/src/common/logs/logger-proxy.ts +1 -1
  346. package/src/config.ts +5 -7
  347. package/src/constants.ts +155 -20
  348. package/src/controls-options-manager/constants.ts +5 -0
  349. package/src/controls-options-manager/enums.ts +18 -0
  350. package/src/controls-options-manager/index.ts +278 -0
  351. package/src/controls-options-manager/types.ts +59 -0
  352. package/src/controls-options-manager/util.ts +286 -0
  353. package/src/index.ts +34 -0
  354. package/src/locus-info/controlsUtils.ts +108 -0
  355. package/src/locus-info/index.ts +310 -21
  356. package/src/locus-info/mediaSharesUtils.ts +48 -0
  357. package/src/locus-info/parser.ts +2 -1
  358. package/src/locus-info/selfUtils.ts +71 -1
  359. package/src/media/index.ts +70 -142
  360. package/src/media/properties.ts +41 -104
  361. package/src/mediaQualityMetrics/config.ts +379 -377
  362. package/src/meeting/in-meeting-actions.ts +156 -0
  363. package/src/meeting/index.ts +1730 -1768
  364. package/src/meeting/locusMediaRequest.ts +309 -0
  365. package/src/meeting/muteState.ts +228 -132
  366. package/src/meeting/request.ts +100 -91
  367. package/src/meeting/request.type.ts +2 -0
  368. package/src/meeting/util.ts +421 -421
  369. package/src/meeting-info/meeting-info-v2.ts +134 -13
  370. package/src/meeting-info/utilv2.ts +13 -3
  371. package/src/meetings/collection.ts +20 -0
  372. package/src/meetings/index.ts +375 -83
  373. package/src/meetings/meetings.types.ts +9 -0
  374. package/src/meetings/request.ts +3 -1
  375. package/src/meetings/util.ts +103 -4
  376. package/src/member/index.ts +40 -0
  377. package/src/member/types.ts +24 -0
  378. package/src/member/util.ts +81 -1
  379. package/src/members/collection.ts +8 -0
  380. package/src/members/index.ts +108 -6
  381. package/src/members/request.ts +98 -17
  382. package/src/members/types.ts +28 -0
  383. package/src/members/util.ts +319 -240
  384. package/src/metrics/config.ts +49 -10
  385. package/src/metrics/constants.ts +2 -4
  386. package/src/metrics/index.ts +43 -27
  387. package/src/multistream/mediaRequestManager.ts +210 -45
  388. package/src/multistream/receiveSlot.ts +68 -26
  389. package/src/multistream/receiveSlotManager.ts +61 -38
  390. package/src/multistream/remoteMedia.ts +29 -3
  391. package/src/multistream/remoteMediaGroup.ts +61 -2
  392. package/src/multistream/remoteMediaManager.ts +260 -66
  393. package/src/networkQualityMonitor/index.ts +6 -6
  394. package/src/reachability/index.ts +75 -25
  395. package/src/reachability/request.ts +10 -5
  396. package/src/reactions/constants.ts +4 -0
  397. package/src/reactions/reactions.ts +4 -4
  398. package/src/reactions/reactions.type.ts +28 -3
  399. package/src/reconnection-manager/index.ts +53 -32
  400. package/src/recording-controller/enums.ts +8 -0
  401. package/src/recording-controller/index.ts +315 -0
  402. package/src/recording-controller/util.ts +58 -0
  403. package/src/roap/index.ts +21 -30
  404. package/src/roap/request.ts +51 -52
  405. package/src/roap/turnDiscovery.ts +51 -27
  406. package/src/statsAnalyzer/global.ts +1 -94
  407. package/src/statsAnalyzer/index.ts +380 -390
  408. package/src/statsAnalyzer/mqaUtil.ts +106 -99
  409. package/test/integration/spec/converged-space-meetings.js +233 -0
  410. package/test/integration/spec/journey.js +332 -255
  411. package/test/integration/spec/space-meeting.js +78 -5
  412. package/test/integration/spec/transcription.js +1 -1
  413. package/test/unit/spec/annotation/index.ts +436 -0
  414. package/test/unit/spec/breakouts/breakout.ts +203 -0
  415. package/test/unit/spec/breakouts/collection.ts +15 -0
  416. package/test/unit/spec/breakouts/edit-lock-error.ts +30 -0
  417. package/test/unit/spec/breakouts/events.ts +77 -0
  418. package/test/unit/spec/breakouts/index.ts +1790 -0
  419. package/test/unit/spec/breakouts/request.ts +104 -0
  420. package/test/unit/spec/breakouts/utils.js +72 -0
  421. package/test/unit/spec/controls-options-manager/index.js +287 -0
  422. package/test/unit/spec/controls-options-manager/util.js +518 -0
  423. package/test/unit/spec/fixture/locus.js +1 -0
  424. package/test/unit/spec/locus-info/controlsUtils.js +303 -30
  425. package/test/unit/spec/locus-info/index.js +615 -4
  426. package/test/unit/spec/locus-info/mediaSharesUtils.ts +22 -0
  427. package/test/unit/spec/locus-info/selfConstant.js +38 -0
  428. package/test/unit/spec/locus-info/selfUtils.js +200 -0
  429. package/test/unit/spec/media/index.ts +118 -22
  430. package/test/unit/spec/media/properties.ts +9 -9
  431. package/test/unit/spec/meeting/in-meeting-actions.ts +76 -0
  432. package/test/unit/spec/meeting/index.js +2394 -1381
  433. package/test/unit/spec/meeting/locusMediaRequest.ts +436 -0
  434. package/test/unit/spec/meeting/muteState.js +370 -208
  435. package/test/unit/spec/meeting/request.js +354 -42
  436. package/test/unit/spec/meeting/utils.js +268 -156
  437. package/test/unit/spec/meeting-info/meetinginfov2.js +383 -5
  438. package/test/unit/spec/meeting-info/utilv2.js +21 -0
  439. package/test/unit/spec/meetings/collection.js +14 -0
  440. package/test/unit/spec/meetings/index.js +842 -128
  441. package/test/unit/spec/meetings/utils.js +206 -2
  442. package/test/unit/spec/member/index.js +24 -0
  443. package/test/unit/spec/member/util.js +384 -32
  444. package/test/unit/spec/members/index.js +320 -1
  445. package/test/unit/spec/members/request.js +206 -27
  446. package/test/unit/spec/members/utils.js +184 -0
  447. package/test/unit/spec/metrics/index.js +98 -0
  448. package/test/unit/spec/multistream/mediaRequestManager.ts +676 -105
  449. package/test/unit/spec/multistream/receiveSlot.ts +77 -18
  450. package/test/unit/spec/multistream/receiveSlotManager.ts +69 -39
  451. package/test/unit/spec/multistream/remoteMedia.ts +32 -2
  452. package/test/unit/spec/multistream/remoteMediaGroup.ts +271 -5
  453. package/test/unit/spec/multistream/remoteMediaManager.ts +672 -65
  454. package/test/unit/spec/networkQualityMonitor/index.js +4 -4
  455. package/test/unit/spec/reachability/index.ts +176 -25
  456. package/test/unit/spec/reachability/request.js +66 -0
  457. package/test/unit/spec/reconnection-manager/index.js +46 -13
  458. package/test/unit/spec/recording-controller/index.js +231 -0
  459. package/test/unit/spec/recording-controller/util.js +102 -0
  460. package/test/unit/spec/roap/index.ts +21 -51
  461. package/test/unit/spec/roap/request.ts +187 -0
  462. package/test/unit/spec/roap/turnDiscovery.ts +73 -34
  463. package/test/unit/spec/stats-analyzer/index.js +94 -43
  464. package/test/utils/constants.js +9 -0
  465. package/test/utils/integrationTestUtils.js +46 -0
  466. package/test/utils/testUtils.js +0 -45
  467. package/test/utils/webex-config.js +4 -0
  468. package/test/utils/webex-test-users.js +7 -3
  469. package/tsconfig.json +6 -0
  470. package/dist/media/internal-media-core-wrapper.js +0 -22
  471. package/dist/media/internal-media-core-wrapper.js.map +0 -1
  472. package/dist/meeting/effectsState.js +0 -334
  473. package/dist/meeting/effectsState.js.map +0 -1
  474. package/dist/multistream/multistreamMedia.js +0 -117
  475. package/dist/multistream/multistreamMedia.js.map +0 -1
  476. package/src/index.js +0 -15
  477. package/src/media/internal-media-core-wrapper.ts +0 -9
  478. package/src/meeting/effectsState.ts +0 -211
  479. package/src/multistream/multistreamMedia.ts +0 -93
  480. package/test/unit/spec/meeting/effectsState.js +0 -281
@@ -1,12 +1,28 @@
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
+ MediaType,
11
+ RemoteTrackType,
12
+ } from '@webex/internal-media-core';
13
+
14
+ import {
15
+ getDevices,
16
+ LocalTrack,
17
+ LocalCameraTrack,
18
+ LocalDisplayTrack,
19
+ LocalMicrophoneTrack,
20
+ LocalTrackEvents,
21
+ TrackMuteEvent,
22
+ } from '@webex/media-helpers';
6
23
 
7
24
  import {
8
25
  MeetingNotActiveError,
9
- createMeetingsError,
10
26
  UserInLobbyError,
11
27
  NoMediaEstablishedYetError,
12
28
  UserNotJoinedError,
@@ -16,20 +32,22 @@ import NetworkQualityMonitor from '../networkQualityMonitor';
16
32
  import LoggerProxy from '../common/logs/logger-proxy';
17
33
  import Trigger from '../common/events/trigger-proxy';
18
34
  import Roap from '../roap/index';
19
- import Media from '../media';
35
+ import Media, {type BundlePolicy} from '../media';
20
36
  import MediaProperties from '../media/properties';
21
37
  import MeetingStateMachine from './state';
22
- import createMuteState from './muteState';
23
- import createEffectsState from './effectsState';
38
+ import {createMuteState} from './muteState';
24
39
  import LocusInfo from '../locus-info';
25
40
  import Metrics from '../metrics';
26
- import {trigger, mediaType, error as MetricsError, eventType} from '../metrics/config';
41
+ import {trigger, error as MetricsError, eventType} from '../metrics/config';
27
42
  import ReconnectionManager from '../reconnection-manager';
28
43
  import MeetingRequest from './request';
29
44
  import Members from '../members/index';
30
45
  import MeetingUtil from './util';
46
+ import RecordingUtil from '../recording-controller/util';
47
+ import ControlsOptionsUtil from '../controls-options-manager/util';
31
48
  import MediaUtil from '../media/util';
32
49
  import Transcription from '../transcription';
50
+ import {Reactions, SkinTones} from '../reactions/reactions';
33
51
  import PasswordError from '../common/errors/password-error';
34
52
  import CaptchaError from '../common/errors/captcha-error';
35
53
  import ReconnectionError from '../common/errors/reconnection';
@@ -40,10 +58,12 @@ import {
40
58
  _JOIN_,
41
59
  AUDIO,
42
60
  CONTENT,
61
+ DISPLAY_HINTS,
43
62
  ENDED,
44
63
  EVENT_TRIGGERS,
45
64
  EVENT_TYPES,
46
65
  EVENTS,
66
+ BREAKOUTS,
47
67
  FLOOR_ACTION,
48
68
  FULL_STATE,
49
69
  LAYOUT_TYPES,
@@ -64,53 +84,83 @@ import {
64
84
  RECORDING_STATE,
65
85
  SHARE_STATUS,
66
86
  SHARE_STOPPED_REASON,
67
- VIDEO_RESOLUTIONS,
68
87
  VIDEO,
69
- BNR_STATUS,
70
88
  HTTP_VERBS,
89
+ SELF_ROLES,
71
90
  } from '../constants';
72
91
  import BEHAVIORAL_METRICS from '../metrics/constants';
73
92
  import ParameterError from '../common/errors/parameter';
74
- import MediaError from '../common/errors/media';
75
93
  import {
76
94
  MeetingInfoV2PasswordError,
77
95
  MeetingInfoV2CaptchaError,
96
+ MeetingInfoV2PolicyError,
78
97
  } from '../meeting-info/meeting-info-v2';
79
98
  import BrowserDetection from '../common/browser-detection';
80
- import {ReceiveSlotManager} from '../multistream/receiveSlotManager';
99
+ import {CSI, ReceiveSlotManager} from '../multistream/receiveSlotManager';
81
100
  import {MediaRequestManager} from '../multistream/mediaRequestManager';
82
101
  import {
102
+ Configuration as RemoteMediaManagerConfiguration,
83
103
  RemoteMediaManager,
84
104
  Event as RemoteMediaManagerEvent,
85
105
  } 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';
106
+ import {
107
+ Reaction,
108
+ ReactionServerType,
109
+ SkinToneType,
110
+ ProcessedReaction,
111
+ RelayEvent,
112
+ } from '../reactions/reactions.type';
113
+ import Breakouts from '../breakouts';
114
+ import Annotation from '../annotation';
89
115
 
90
116
  import InMeetingActions from './in-meeting-actions';
117
+ import {REACTION_RELAY_TYPES} from '../reactions/constants';
118
+ import RecordingController from '../recording-controller';
119
+ import ControlsOptionsManager from '../controls-options-manager';
120
+ import PermissionError from '../common/errors/permission';
121
+ import {LocusMediaRequest} from './locusMediaRequest';
122
+ import {AnnotationInfo} from '../annotation/annotation.types';
91
123
 
92
124
  const {isBrowser} = BrowserDetection();
93
125
 
94
- const logRequest = (request: any, {header = '', success = '', failure = ''}) => {
95
- LoggerProxy.logger.info(header);
126
+ const logRequest = (request: any, {logText = ''}) => {
127
+ LoggerProxy.logger.info(`${logText} - sending request`);
96
128
 
97
129
  return request
98
130
  .then((arg) => {
99
- LoggerProxy.logger.info(success);
131
+ LoggerProxy.logger.info(`${logText} - has been successfully sent`);
100
132
 
101
133
  return arg;
102
134
  })
103
135
  .catch((error) => {
104
- LoggerProxy.logger.error(failure, error);
136
+ LoggerProxy.logger.error(`${logText} - has failed: `, error);
105
137
  throw error;
106
138
  });
107
139
  };
108
140
 
141
+ export type LocalTracks = {
142
+ microphone?: LocalMicrophoneTrack;
143
+ camera?: LocalCameraTrack;
144
+ screenShare?: {
145
+ audio?: LocalTrack; // todo: for now screen share audio is not supported (will be done in SPARK-399690)
146
+ video?: LocalDisplayTrack;
147
+ };
148
+ annotationInfo?: AnnotationInfo;
149
+ };
150
+
151
+ export type AddMediaOptions = {
152
+ localTracks?: LocalTracks;
153
+ audioEnabled?: boolean; // if not specified, default value true is used
154
+ videoEnabled?: boolean; // if not specified, default value true is used
155
+ receiveShare?: boolean; // if not specified, default value true is used
156
+ remoteMediaManagerConfig?: RemoteMediaManagerConfiguration; // applies only to multistream meetings
157
+ bundlePolicy?: BundlePolicy; // applies only to multistream meetings
158
+ };
159
+
109
160
  export const MEDIA_UPDATE_TYPE = {
110
- ALL: 'ALL',
111
- AUDIO: 'AUDIO',
112
- VIDEO: 'VIDEO',
113
- SHARE: 'SHARE',
161
+ TRANSCODED_MEDIA_CONNECTION: 'TRANSCODED_MEDIA_CONNECTION',
162
+ LAMBDA: 'LAMBDA',
163
+ UPDATE_MEDIA: 'UPDATE_MEDIA',
114
164
  };
115
165
 
116
166
  /**
@@ -125,16 +175,6 @@ export const MEDIA_UPDATE_TYPE = {
125
175
  * @property {boolean} isSharing
126
176
  */
127
177
 
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
178
  /**
139
179
  * SharePreferences
140
180
  * @typedef {Object} SharePreferences
@@ -149,18 +189,10 @@ export const MEDIA_UPDATE_TYPE = {
149
189
  * @property {String} [pin]
150
190
  * @property {Boolean} [moderator]
151
191
  * @property {String|Object} [meetingQuality]
152
- * @property {String} [meetingQuality.local]
153
192
  * @property {String} [meetingQuality.remote]
154
193
  * @property {Boolean} [rejoin]
155
194
  * @property {Boolean} [enableMultistream]
156
- */
157
-
158
- /**
159
- * SendOptions
160
- * @typedef {Object} SendOptions
161
- * @property {Boolean} sendAudio
162
- * @property {Boolean} sendVideo
163
- * @property {Boolean} sendShare
195
+ * @property {String} [correlationId]
164
196
  */
165
197
 
166
198
  /**
@@ -403,12 +435,13 @@ export const MEDIA_UPDATE_TYPE = {
403
435
  export default class Meeting extends StatelessWebexPlugin {
404
436
  attrs: any;
405
437
  audio: any;
438
+ breakouts: any;
439
+ annotation: any;
406
440
  conversationUrl: string;
407
441
  correlationId: string;
408
442
  destination: string;
409
443
  destinationType: string;
410
444
  deviceUrl: string;
411
- effects: any;
412
445
  hostId: string;
413
446
  id: string;
414
447
  isMultistream: boolean;
@@ -416,7 +449,7 @@ export default class Meeting extends StatelessWebexPlugin {
416
449
  mediaConnections: any[];
417
450
  mediaId?: string;
418
451
  meetingFiniteStateMachine: any;
419
- meetingInfo: object;
452
+ meetingInfo: any;
420
453
  meetingRequest: MeetingRequest;
421
454
  members: Members;
422
455
  options: object;
@@ -428,6 +461,7 @@ export default class Meeting extends StatelessWebexPlugin {
428
461
  resource: string;
429
462
  roap: Roap;
430
463
  roapSeq: number;
464
+ selfUrl?: string; // comes from Locus, initialized by updateMeetingObject()
431
465
  sipUri: string;
432
466
  type: string;
433
467
  userId: string;
@@ -449,32 +483,37 @@ export default class Meeting extends StatelessWebexPlugin {
449
483
  keepAliveTimerId: NodeJS.Timeout;
450
484
  lastVideoLayoutInfo: any;
451
485
  locusInfo: any;
452
- media: MultistreamMedia;
486
+ locusMediaRequest?: LocusMediaRequest;
453
487
  mediaProperties: MediaProperties;
454
488
  mediaRequestManagers: {
455
489
  audio: MediaRequestManager;
456
490
  video: MediaRequestManager;
491
+ screenShareAudio: MediaRequestManager;
492
+ screenShareVideo: MediaRequestManager;
457
493
  };
458
494
 
459
495
  meetingInfoFailureReason: string;
496
+ meetingInfoFailureCode?: number;
460
497
  networkQualityMonitor: NetworkQualityMonitor;
461
498
  networkStatus: string;
462
499
  passwordStatus: string;
463
500
  queuedMediaUpdates: any[];
464
501
  recording: any;
465
502
  remoteMediaManager: RemoteMediaManager | null;
503
+ recordingController: RecordingController;
504
+ controlsOptionsManager: ControlsOptionsManager;
466
505
  requiredCaptcha: any;
467
506
  receiveSlotManager: ReceiveSlotManager;
468
507
  shareStatus: string;
469
508
  statsAnalyzer: StatsAnalyzer;
470
509
  transcription: Transcription;
471
510
  updateMediaConnections: (mediaConnections: any[]) => void;
472
- endCallInitiateJoinReq: any;
511
+ endCallInitJoinReq: any;
473
512
  endJoinReqResp: any;
474
513
  endLocalSDPGenRemoteSDPRecvDelay: any;
475
514
  joinedWith: any;
476
515
  locusId: any;
477
- startCallInitiateJoinReq: any;
516
+ startCallInitJoinReq: any;
478
517
  startJoinReqResp: any;
479
518
  startLocalSDPGenRemoteSDPRecvDelay: any;
480
519
  wirelessShare: any;
@@ -487,8 +526,13 @@ export default class Meeting extends StatelessWebexPlugin {
487
526
  resourceUrl: string;
488
527
  selfId: string;
489
528
  state: any;
490
-
529
+ localAudioTrackMuteStateHandler: (event: TrackMuteEvent) => void;
530
+ localVideoTrackMuteStateHandler: (event: TrackMuteEvent) => void;
531
+ underlyingLocalTrackChangeHandler: () => void;
532
+ roles: any[];
533
+ environment: string;
491
534
  namespace = MEETINGS;
535
+ annotationInfo: AnnotationInfo;
492
536
 
493
537
  /**
494
538
  * @param {Object} attrs
@@ -523,7 +567,7 @@ export default class Meeting extends StatelessWebexPlugin {
523
567
  */
524
568
  this.id = uuid.v4();
525
569
  /**
526
- * Correlation ID used for network tracking of meeting join
570
+ * Correlation ID used for network tracking of meeting
527
571
  * @instance
528
572
  * @type {String}
529
573
  * @readonly
@@ -573,41 +617,120 @@ export default class Meeting extends StatelessWebexPlugin {
573
617
  */
574
618
  // TODO: needs to be defined as a class
575
619
  this.meetingInfo = {};
620
+ /**
621
+ * @instance
622
+ * @type {Breakouts}
623
+ * @public
624
+ * @memberof Meeting
625
+ */
626
+ // @ts-ignore
627
+ this.breakouts = new Breakouts({meetingId: this.id}, {parent: this.webex});
628
+ /**
629
+ * @instance
630
+ * @type {Annotation}
631
+ * @public
632
+ * @memberof Meeting
633
+ */
634
+ // @ts-ignore
635
+ this.annotation = new Annotation({parent: this.webex});
576
636
  /**
577
637
  * helper class for managing receive slots (for multistream media connections)
578
638
  */
579
- this.receiveSlotManager = new ReceiveSlotManager(this);
639
+ this.receiveSlotManager = new ReceiveSlotManager(
640
+ (mediaType: MediaType) => {
641
+ if (!this.mediaProperties?.webrtcMediaConnection) {
642
+ return Promise.reject(new Error('Webrtc media connection is missing'));
643
+ }
644
+
645
+ return this.mediaProperties.webrtcMediaConnection.createReceiveSlot(mediaType);
646
+ },
647
+ (csi: CSI) => (this.members.findMemberByCsi(csi) as any)?.id
648
+ );
580
649
  /**
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.
650
+ * Object containing helper classes for managing media requests for audio/video/screenshare (for multistream media connections)
651
+ * All multistream media requests sent out for this meeting have to go through them.
583
652
  */
584
653
  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'
654
+ audio: new MediaRequestManager(
655
+ (mediaRequests) => {
656
+ if (!this.mediaProperties.webrtcMediaConnection) {
657
+ LoggerProxy.logger.warn(
658
+ 'Meeting:index#mediaRequestManager --> trying to send audio media request before media connection was created'
659
+ );
660
+
661
+ return;
662
+ }
663
+ this.mediaProperties.webrtcMediaConnection.requestMedia(
664
+ MediaType.AudioMain,
665
+ mediaRequests
589
666
  );
667
+ },
668
+ {
669
+ // @ts-ignore - config coming from registerPlugin
670
+ degradationPreferences: this.config.degradationPreferences,
671
+ kind: 'audio',
672
+ }
673
+ ),
674
+ video: new MediaRequestManager(
675
+ (mediaRequests) => {
676
+ if (!this.mediaProperties.webrtcMediaConnection) {
677
+ LoggerProxy.logger.warn(
678
+ 'Meeting:index#mediaRequestManager --> trying to send video media request before media connection was created'
679
+ );
590
680
 
591
- return;
681
+ return;
682
+ }
683
+ this.mediaProperties.webrtcMediaConnection.requestMedia(
684
+ MediaType.VideoMain,
685
+ mediaRequests
686
+ );
687
+ },
688
+ {
689
+ // @ts-ignore - config coming from registerPlugin
690
+ degradationPreferences: this.config.degradationPreferences,
691
+ kind: 'video',
592
692
  }
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'
693
+ ),
694
+ screenShareAudio: new MediaRequestManager(
695
+ (mediaRequests) => {
696
+ if (!this.mediaProperties.webrtcMediaConnection) {
697
+ LoggerProxy.logger.warn(
698
+ 'Meeting:index#mediaRequestManager --> trying to send screenshare audio media request before media connection was created'
699
+ );
700
+
701
+ return;
702
+ }
703
+ this.mediaProperties.webrtcMediaConnection.requestMedia(
704
+ MediaType.AudioSlides,
705
+ mediaRequests
602
706
  );
707
+ },
708
+ {
709
+ // @ts-ignore - config coming from registerPlugin
710
+ degradationPreferences: this.config.degradationPreferences,
711
+ kind: 'audio',
712
+ }
713
+ ),
714
+ screenShareVideo: new MediaRequestManager(
715
+ (mediaRequests) => {
716
+ if (!this.mediaProperties.webrtcMediaConnection) {
717
+ LoggerProxy.logger.warn(
718
+ 'Meeting:index#mediaRequestManager --> trying to send screenshare video media request before media connection was created'
719
+ );
603
720
 
604
- return;
721
+ return;
722
+ }
723
+ this.mediaProperties.webrtcMediaConnection.requestMedia(
724
+ MediaType.VideoSlides,
725
+ mediaRequests
726
+ );
727
+ },
728
+ {
729
+ // @ts-ignore - config coming from registerPlugin
730
+ degradationPreferences: this.config.degradationPreferences,
731
+ kind: 'video',
605
732
  }
606
- this.mediaProperties.webrtcMediaConnection.requestMedia(
607
- MC.MediaType.VideoMain,
608
- mediaRequests
609
- );
610
- }),
733
+ ),
611
734
  };
612
735
  /**
613
736
  * @instance
@@ -620,8 +743,9 @@ export default class Meeting extends StatelessWebexPlugin {
620
743
  locusUrl: attrs.locus && attrs.locus.url,
621
744
  receiveSlotManager: this.receiveSlotManager,
622
745
  mediaRequestManagers: this.mediaRequestManagers,
623
- // @ts-ignore - Fix type
746
+ meeting: this,
624
747
  },
748
+ // @ts-ignore - Fix type
625
749
  {parent: this.webex}
626
750
  );
627
751
  /**
@@ -653,7 +777,7 @@ export default class Meeting extends StatelessWebexPlugin {
653
777
  */
654
778
  this.reconnectionManager = new ReconnectionManager(this);
655
779
  /**
656
- * created later
780
+ * created with media connection
657
781
  * @instance
658
782
  * @type {MuteState}
659
783
  * @private
@@ -661,21 +785,13 @@ export default class Meeting extends StatelessWebexPlugin {
661
785
  */
662
786
  this.audio = null;
663
787
  /**
664
- * created later
788
+ * created with media connection
665
789
  * @instance
666
790
  * @type {MuteState}
667
791
  * @private
668
792
  * @memberof Meeting
669
793
  */
670
794
  this.video = null;
671
- /**
672
- * created later
673
- * @instance
674
- * @type {EffectsState}
675
- * @private
676
- * @memberof Meeting
677
- */
678
- this.effects = null;
679
795
  /**
680
796
  * @instance
681
797
  * @type {MeetingStateMachine}
@@ -770,7 +886,12 @@ export default class Meeting extends StatelessWebexPlugin {
770
886
  * @private
771
887
  * @memberof Meeting
772
888
  */
773
- this.meetingRequest = new MeetingRequest({}, options);
889
+ this.meetingRequest = new MeetingRequest(
890
+ {
891
+ meeting: this,
892
+ },
893
+ options
894
+ );
774
895
  /**
775
896
  * @instance
776
897
  * @type {Array}
@@ -924,6 +1045,7 @@ export default class Meeting extends StatelessWebexPlugin {
924
1045
  */
925
1046
  // @ts-ignore - Fix type
926
1047
  this.locusInfo = new LocusInfo(this.updateMeetingObject.bind(this), this.webex, this.id);
1048
+
927
1049
  // We had to add listeners first before setting up the locus instance
928
1050
  /**
929
1051
  * @instance
@@ -1014,6 +1136,15 @@ export default class Meeting extends StatelessWebexPlugin {
1014
1136
  */
1015
1137
  this.meetingInfoFailureReason = undefined;
1016
1138
 
1139
+ /**
1140
+ * The numeric code, if any, associated with the last failure to obtain the meeting info
1141
+ * @instance
1142
+ * @type {number}
1143
+ * @private
1144
+ * @memberof Meeting
1145
+ */
1146
+ this.meetingInfoFailureCode = undefined;
1147
+
1017
1148
  /**
1018
1149
  * Repeating timer used to send keepAlives when in lobby
1019
1150
  * @instance
@@ -1023,16 +1154,58 @@ export default class Meeting extends StatelessWebexPlugin {
1023
1154
  */
1024
1155
  this.keepAliveTimerId = null;
1025
1156
 
1157
+ /**
1158
+ * The class that helps to control recording functions: start, stop, pause, resume, etc
1159
+ * @instance
1160
+ * @type {RecordingController}
1161
+ * @public
1162
+ * @memberof Meeting
1163
+ */
1164
+ this.recordingController = new RecordingController(this.meetingRequest, {
1165
+ serviceUrl: this.locusInfo?.links?.services?.record?.url,
1166
+ sessionId: this.locusInfo?.fullState?.sessionId,
1167
+ locusUrl: this.locusInfo?.url,
1168
+ displayHints: [],
1169
+ });
1170
+
1171
+ /**
1172
+ * The class that helps to control recording functions: start, stop, pause, resume, etc
1173
+ * @instance
1174
+ * @type {ControlsOptionsManager}
1175
+ * @public
1176
+ * @memberof Meeting
1177
+ */
1178
+ this.controlsOptionsManager = new ControlsOptionsManager(this.meetingRequest, {
1179
+ locusUrl: this.locusInfo?.url,
1180
+ displayHints: [],
1181
+ });
1182
+
1026
1183
  this.setUpLocusInfoListeners();
1027
1184
  this.locusInfo.init(attrs.locus ? attrs.locus : {});
1028
1185
  this.hasJoinedOnce = false;
1029
1186
 
1030
- this.media = new MultistreamMedia(this);
1031
-
1032
1187
  /**
1033
1188
  * helper class for managing remote streams
1034
1189
  */
1035
1190
  this.remoteMediaManager = null;
1191
+
1192
+ this.localAudioTrackMuteStateHandler = (event) => {
1193
+ this.audio.handleLocalTrackMuteStateChange(this, event.trackState.muted);
1194
+ };
1195
+
1196
+ this.localVideoTrackMuteStateHandler = (event) => {
1197
+ this.video.handleLocalTrackMuteStateChange(this, event.trackState.muted);
1198
+ };
1199
+
1200
+ // The handling of underlying track changes should be done inside
1201
+ // @webex/internal-media-core, but for now we have to do it here, because
1202
+ // RoapMediaConnection has to use raw MediaStreamTracks in its API until
1203
+ // the Calling SDK also moves to using webrtc-core tracks
1204
+ this.underlyingLocalTrackChangeHandler = () => {
1205
+ if (!this.isMultistream) {
1206
+ this.updateTranscodedMediaConnection();
1207
+ }
1208
+ };
1036
1209
  }
1037
1210
 
1038
1211
  /**
@@ -1047,9 +1220,11 @@ export default class Meeting extends StatelessWebexPlugin {
1047
1220
  public async fetchMeetingInfo({
1048
1221
  password = null,
1049
1222
  captchaCode = null,
1223
+ extraParams = {},
1050
1224
  }: {
1051
1225
  password?: string;
1052
1226
  captchaCode?: string;
1227
+ extraParams?: Record<string, any>;
1053
1228
  }) {
1054
1229
  // when fetch meeting info is called directly by the client, we want to clear out the random timer for sdk to do it
1055
1230
  if (this.fetchMeetingInfoTimeoutId) {
@@ -1080,11 +1255,16 @@ export default class Meeting extends StatelessWebexPlugin {
1080
1255
  this.destination,
1081
1256
  this.destinationType,
1082
1257
  password,
1083
- captchaInfo
1258
+ captchaInfo,
1259
+ // @ts-ignore - config coming from registerPlugin
1260
+ this.config.installedOrgID,
1261
+ this.locusId,
1262
+ extraParams,
1263
+ {meetingId: this.id}
1084
1264
  );
1085
1265
 
1086
1266
  this.parseMeetingInfo(info, this.destination);
1087
- this.meetingInfo = info ? info.body : null;
1267
+ this.meetingInfo = info ? {...info.body, meetingLookupUrl: info?.url} : null;
1088
1268
  this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NONE;
1089
1269
  this.requiredCaptcha = null;
1090
1270
  if (
@@ -1107,9 +1287,18 @@ export default class Meeting extends StatelessWebexPlugin {
1107
1287
 
1108
1288
  return Promise.resolve();
1109
1289
  } catch (err) {
1110
- if (err instanceof MeetingInfoV2PasswordError) {
1111
- // @ts-ignore
1290
+ if (err instanceof MeetingInfoV2PolicyError) {
1291
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.POLICY;
1292
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1293
+
1294
+ if (err.meetingInfo) {
1295
+ this.meetingInfo = err.meetingInfo;
1296
+ }
1297
+
1298
+ throw new PermissionError();
1299
+ } else if (err instanceof MeetingInfoV2PasswordError) {
1112
1300
  LoggerProxy.logger.info(
1301
+ // @ts-ignore
1113
1302
  `Meeting:index#fetchMeetingInfo --> Info Unable to fetch meeting info for ${this.destination} - password required (code=${err?.body?.code}).`
1114
1303
  );
1115
1304
 
@@ -1119,6 +1308,8 @@ export default class Meeting extends StatelessWebexPlugin {
1119
1308
  this.meetingNumber = err.meetingInfo.meetingNumber;
1120
1309
  }
1121
1310
 
1311
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1312
+
1122
1313
  this.passwordStatus = PASSWORD_STATUS.REQUIRED;
1123
1314
  this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD;
1124
1315
  if (this.requiredCaptcha) {
@@ -1128,8 +1319,8 @@ export default class Meeting extends StatelessWebexPlugin {
1128
1319
 
1129
1320
  throw new PasswordError();
1130
1321
  } else if (err instanceof MeetingInfoV2CaptchaError) {
1131
- // @ts-ignore
1132
1322
  LoggerProxy.logger.info(
1323
+ // @ts-ignore
1133
1324
  `Meeting:index#fetchMeetingInfo --> Info Unable to fetch meeting info for ${this.destination} - captcha required (code=${err?.body?.code}).`
1134
1325
  );
1135
1326
 
@@ -1137,6 +1328,8 @@ export default class Meeting extends StatelessWebexPlugin {
1137
1328
  ? MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA
1138
1329
  : MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD;
1139
1330
 
1331
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1332
+
1140
1333
  if (err.isPasswordRequired) {
1141
1334
  this.passwordStatus = PASSWORD_STATUS.REQUIRED;
1142
1335
  }
@@ -1201,22 +1394,39 @@ export default class Meeting extends StatelessWebexPlugin {
1201
1394
  // we have to pass the wbxappapi hostname as the siteFullName parameter
1202
1395
  const {hostname} = new URL(this.requiredCaptcha.refreshURL);
1203
1396
 
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
- });
1397
+ return (
1398
+ this.meetingRequest
1399
+ // @ts-ignore
1400
+ .refreshCaptcha({
1401
+ captchaRefreshUrl: `${this.requiredCaptcha.refreshURL}&siteFullName=${hostname}`,
1402
+ captchaId: this.requiredCaptcha.captchaId,
1403
+ })
1404
+ .then((response) => {
1405
+ this.requiredCaptcha.captchaId = response.body.captchaID;
1406
+ this.requiredCaptcha.verificationImageURL = response.body.verificationImageURL;
1407
+ this.requiredCaptcha.verificationAudioURL = response.body.verificationAudioURL;
1408
+ })
1409
+ .catch((error) => {
1410
+ LoggerProxy.logger.error(
1411
+ `Meeting:index#refreshCaptcha --> Error Unable to refresh captcha for ${this.destination} - ${error}`
1412
+ );
1413
+ throw error;
1414
+ })
1415
+ );
1416
+ }
1417
+
1418
+ /**
1419
+ * Posts metrics event for this meeting. Allows the app to send Call Analyzer events.
1420
+ * @param {String} eventName - Call Analyzer event, see eventType in src/metrics/config.ts for possible values
1421
+ * @public
1422
+ * @memberof Meeting
1423
+ * @returns {Promise}
1424
+ */
1425
+ public postMetrics(eventName: string) {
1426
+ Metrics.postEvent({
1427
+ event: eventName,
1428
+ meeting: this,
1429
+ });
1220
1430
  }
1221
1431
 
1222
1432
  /**
@@ -1229,6 +1439,7 @@ export default class Meeting extends StatelessWebexPlugin {
1229
1439
  // meeting update listeners
1230
1440
  this.setUpLocusInfoSelfListener();
1231
1441
  this.setUpLocusInfoMeetingListener();
1442
+ this.setUpLocusServicesListener();
1232
1443
  // members update listeners
1233
1444
  this.setUpLocusFullStateListener();
1234
1445
  this.setUpLocusUrlListener();
@@ -1241,6 +1452,94 @@ export default class Meeting extends StatelessWebexPlugin {
1241
1452
  this.setUpLocusInfoMeetingInfoListener();
1242
1453
  this.setUpLocusInfoAssignHostListener();
1243
1454
  this.setUpLocusInfoMediaInactiveListener();
1455
+ this.setUpBreakoutsListener();
1456
+ }
1457
+
1458
+ /**
1459
+ * Set up the listeners for breakouts
1460
+ * @returns {undefined}
1461
+ * @private
1462
+ * @memberof Meeting
1463
+ */
1464
+ setUpBreakoutsListener() {
1465
+ this.breakouts.on(BREAKOUTS.EVENTS.BREAKOUTS_CLOSING, () => {
1466
+ Trigger.trigger(
1467
+ this,
1468
+ {
1469
+ file: 'meeting/index',
1470
+ function: 'setUpBreakoutsListener',
1471
+ },
1472
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_CLOSING
1473
+ );
1474
+ });
1475
+
1476
+ this.breakouts.on(BREAKOUTS.EVENTS.MESSAGE, (messageEvent) => {
1477
+ Trigger.trigger(
1478
+ this,
1479
+ {
1480
+ file: 'meeting/index',
1481
+ function: 'setUpBreakoutsListener',
1482
+ },
1483
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_MESSAGE,
1484
+ messageEvent
1485
+ );
1486
+ });
1487
+
1488
+ this.breakouts.on(BREAKOUTS.EVENTS.MEMBERS_UPDATE, () => {
1489
+ Trigger.trigger(
1490
+ this,
1491
+ {
1492
+ file: 'meeting/index',
1493
+ function: 'setUpBreakoutsListener',
1494
+ },
1495
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_UPDATE
1496
+ );
1497
+ });
1498
+
1499
+ this.breakouts.on(BREAKOUTS.EVENTS.ASK_RETURN_TO_MAIN, () => {
1500
+ Trigger.trigger(
1501
+ this,
1502
+ {
1503
+ file: 'meeting/index',
1504
+ function: 'setUpBreakoutsListener',
1505
+ },
1506
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_ASK_RETURN_TO_MAIN
1507
+ );
1508
+ });
1509
+
1510
+ this.breakouts.on(BREAKOUTS.EVENTS.LEAVE_BREAKOUT, () => {
1511
+ Trigger.trigger(
1512
+ this,
1513
+ {
1514
+ file: 'meeting/index',
1515
+ function: 'setUpBreakoutsListener',
1516
+ },
1517
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_LEAVE
1518
+ );
1519
+ });
1520
+
1521
+ this.breakouts.on(BREAKOUTS.EVENTS.ASK_FOR_HELP, (helpEvent) => {
1522
+ Trigger.trigger(
1523
+ this,
1524
+ {
1525
+ file: 'meeting/index',
1526
+ function: 'setUpBreakoutsListener',
1527
+ },
1528
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_ASK_FOR_HELP,
1529
+ helpEvent
1530
+ );
1531
+ });
1532
+
1533
+ this.breakouts.on(BREAKOUTS.EVENTS.PRE_ASSIGNMENTS_UPDATE, () => {
1534
+ Trigger.trigger(
1535
+ this,
1536
+ {
1537
+ file: 'meeting/index',
1538
+ function: 'setUpBreakoutsListener',
1539
+ },
1540
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_PRE_ASSIGNMENTS_UPDATE
1541
+ );
1542
+ });
1244
1543
  }
1245
1544
 
1246
1545
  /**
@@ -1356,19 +1655,20 @@ export default class Meeting extends StatelessWebexPlugin {
1356
1655
  * @returns {Object}
1357
1656
  * @memberof Meeting
1358
1657
  */
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
- ) {
1658
+ getAnalyzerMetricsPrePayload(options: {
1659
+ type?: string;
1660
+ event: string;
1661
+ trackingId: string;
1662
+ locus: object;
1663
+ mediaConnections?: Array<any>;
1664
+ errors?: object;
1665
+ meetingLookupUrl?: string;
1666
+ clientType?: any;
1667
+ subClientType?: any;
1668
+ [key: string]: any;
1669
+ }) {
1370
1670
  if (options) {
1371
- const {event, trackingId, mediaConnections} = options;
1671
+ const {event, trackingId, mediaConnections, meetingLookupUrl} = options;
1372
1672
 
1373
1673
  if (!event) {
1374
1674
  LoggerProxy.logger.error(
@@ -1407,6 +1707,10 @@ export default class Meeting extends StatelessWebexPlugin {
1407
1707
  identifiers.mediaAgentCluster = this.mediaConnections?.[0].mediaAgentCluster;
1408
1708
  }
1409
1709
 
1710
+ if (meetingLookupUrl) {
1711
+ identifiers.meetingLookupUrl = meetingLookupUrl;
1712
+ }
1713
+
1410
1714
  if (options.trackingId) {
1411
1715
  identifiers.trackingId = trackingId;
1412
1716
  }
@@ -1456,12 +1760,12 @@ export default class Meeting extends StatelessWebexPlugin {
1456
1760
  };
1457
1761
  }
1458
1762
 
1459
- const callInitiateJoinReq = this.getCallInitiateJoinReq();
1763
+ const callInitJoinReq = this.getCallInitJoinReq();
1460
1764
 
1461
- if (callInitiateJoinReq) {
1765
+ if (callInitJoinReq) {
1462
1766
  options.joinTimes = {
1463
1767
  ...options.joinTimes,
1464
- callInitiateJoinReq,
1768
+ callInitJoinReq,
1465
1769
  };
1466
1770
  }
1467
1771
 
@@ -1474,15 +1778,31 @@ export default class Meeting extends StatelessWebexPlugin {
1474
1778
  };
1475
1779
  }
1476
1780
 
1477
- const getTotalJmt = this.getTotalJmt();
1781
+ const totalJmt = this.getTotalJmt();
1478
1782
 
1479
- if (getTotalJmt) {
1783
+ if (totalJmt) {
1480
1784
  options.joinTimes = {
1481
1785
  ...options.joinTimes,
1482
- getTotalJmt,
1786
+ totalJmt,
1483
1787
  };
1484
1788
  }
1485
1789
 
1790
+ const curUserType = this.getCurUserType();
1791
+
1792
+ if (curUserType) {
1793
+ options.userType = curUserType;
1794
+ }
1795
+
1796
+ const curLoginType = this.getCurLoginType();
1797
+
1798
+ if (curLoginType) {
1799
+ options.loginType = curLoginType;
1800
+ }
1801
+
1802
+ if (this.environment) {
1803
+ options.environment = this.environment;
1804
+ }
1805
+
1486
1806
  if (options.type === MQA_STATS.CA_TYPE) {
1487
1807
  payload = Metrics.initMediaPayload(options.event, identifiers, options);
1488
1808
  } else {
@@ -1596,11 +1916,7 @@ export default class Meeting extends StatelessWebexPlugin {
1596
1916
  this.pstnUpdate(payload);
1597
1917
 
1598
1918
  // 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
- }
1919
+ this.requestScreenShareFloorIfPending();
1604
1920
  });
1605
1921
  }
1606
1922
 
@@ -1786,6 +2102,28 @@ export default class Meeting extends StatelessWebexPlugin {
1786
2102
  }
1787
2103
  );
1788
2104
 
2105
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_MEETING_BREAKOUT_UPDATED, ({breakout}) => {
2106
+ this.breakouts.updateBreakout(breakout);
2107
+ Trigger.trigger(
2108
+ this,
2109
+ {
2110
+ file: 'meeting/index',
2111
+ function: 'setupLocusControlsListener',
2112
+ },
2113
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_UPDATE
2114
+ );
2115
+ });
2116
+
2117
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_JOIN_BREAKOUT_FROM_MAIN, ({mainLocusUrl}) => {
2118
+ this.meetingRequest.getLocusStatusByUrl(mainLocusUrl).catch((error) => {
2119
+ // clear main session cache when attendee join into breakout and forbidden to get locus from main locus url,
2120
+ // which means main session is not active for the attendee
2121
+ if (error?.statusCode === 403) {
2122
+ this.locusInfo.clearMainSessionLocusCache();
2123
+ }
2124
+ });
2125
+ });
2126
+
1789
2127
  this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_ENTRY_EXIT_TONE_UPDATED, ({entryExitTone}) => {
1790
2128
  Trigger.trigger(
1791
2129
  this,
@@ -1797,23 +2135,111 @@ export default class Meeting extends StatelessWebexPlugin {
1797
2135
  {entryExitTone}
1798
2136
  );
1799
2137
  });
1800
- }
1801
2138
 
1802
- /**
1803
- * Set up the locus info media shares listener
1804
- * update content and whiteboard sharing id value for members, and updates the member
1805
- * notifies consumer with members:content:update {activeContentSharingId, endedContentSharingId}
1806
- * @returns {undefined}
1807
- * @private
1808
- * @memberof Meeting
1809
- */
1810
- private setUpLocusMediaSharesListener() {
1811
- // Will get triggered on local and remote share
1812
- this.locusInfo.on(EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES, (payload) => {
1813
- const {content: contentShare, whiteboard: whiteboardShare} = payload.current;
1814
- const previousContentShare = payload.previous?.content;
2139
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_MUTE_ON_ENTRY_CHANGED, ({state}) => {
2140
+ Trigger.trigger(
2141
+ this,
2142
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2143
+ EVENT_TRIGGERS.MEETING_CONTROLS_MUTE_ON_ENTRY_UPDATED,
2144
+ {state}
2145
+ );
2146
+ });
2147
+
2148
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_SHARE_CONTROL_CHANGED, ({state}) => {
2149
+ Trigger.trigger(
2150
+ this,
2151
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2152
+ EVENT_TRIGGERS.MEETING_CONTROLS_SHARE_CONTROL_UPDATED,
2153
+ {state}
2154
+ );
2155
+ });
2156
+
2157
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_DISALLOW_UNMUTE_CHANGED, ({state}) => {
2158
+ Trigger.trigger(
2159
+ this,
2160
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2161
+ EVENT_TRIGGERS.MEETING_CONTROLS_DISALLOW_UNMUTE_UPDATED,
2162
+ {state}
2163
+ );
2164
+ });
2165
+
2166
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_REACTIONS_CHANGED, ({state}) => {
2167
+ Trigger.trigger(
2168
+ this,
2169
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2170
+ EVENT_TRIGGERS.MEETING_CONTROLS_REACTIONS_UPDATED,
2171
+ {state}
2172
+ );
2173
+ });
2174
+
2175
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_VIEW_THE_PARTICIPANTS_LIST_CHANGED, ({state}) => {
2176
+ Trigger.trigger(
2177
+ this,
2178
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2179
+ EVENT_TRIGGERS.MEETING_CONTROLS_VIEW_THE_PARTICIPANTS_LIST_UPDATED,
2180
+ {state}
2181
+ );
2182
+ });
2183
+
2184
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_RAISE_HAND_CHANGED, ({state}) => {
2185
+ Trigger.trigger(
2186
+ this,
2187
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2188
+ EVENT_TRIGGERS.MEETING_CONTROLS_RAISE_HAND_UPDATED,
2189
+ {state}
2190
+ );
2191
+ });
2192
+
2193
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_VIDEO_CHANGED, ({state}) => {
2194
+ Trigger.trigger(
2195
+ this,
2196
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2197
+ EVENT_TRIGGERS.MEETING_CONTROLS_VIDEO_UPDATED,
2198
+ {state}
2199
+ );
2200
+ });
2201
+ }
2202
+
2203
+ /**
2204
+ * Trigger annotation info update event
2205
+ @returns {undefined}
2206
+ @param {object} contentShare
2207
+ @param {object} previousContentShare
2208
+ */
2209
+ private triggerAnnotationInfoEvent(contentShare, previousContentShare) {
2210
+ if (
2211
+ contentShare?.annotation &&
2212
+ !isEqual(contentShare?.annotation, previousContentShare?.annotation)
2213
+ ) {
2214
+ Trigger.trigger(
2215
+ this,
2216
+ {
2217
+ file: 'meeting/index',
2218
+ function: 'triggerAnnotationInfoEvent',
2219
+ },
2220
+ EVENT_TRIGGERS.MEETING_UPDATE_ANNOTATION_INFO,
2221
+ contentShare.annotation
2222
+ );
2223
+ }
2224
+ }
2225
+
2226
+ /**
2227
+ * Set up the locus info media shares listener
2228
+ * update content and whiteboard sharing id value for members, and updates the member
2229
+ * notifies consumer with members:content:update {activeContentSharingId, endedContentSharingId}
2230
+ * @returns {undefined}
2231
+ * @private
2232
+ * @memberof Meeting
2233
+ */
2234
+ private setUpLocusMediaSharesListener() {
2235
+ // Will get triggered on local and remote share
2236
+ this.locusInfo.on(EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES, async (payload) => {
2237
+ const {content: contentShare, whiteboard: whiteboardShare} = payload.current;
2238
+ const previousContentShare = payload.previous?.content;
1815
2239
  const previousWhiteboardShare = payload.previous?.whiteboard;
1816
2240
 
2241
+ this.triggerAnnotationInfoEvent(contentShare, previousContentShare);
2242
+
1817
2243
  if (
1818
2244
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
1819
2245
  contentShare.disposition === previousContentShare?.disposition &&
@@ -1841,19 +2267,8 @@ export default class Meeting extends StatelessWebexPlugin {
1841
2267
  this.selfId === contentShare.beneficiaryId &&
1842
2268
  contentShare.disposition === FLOOR_ACTION.GRANTED
1843
2269
  ) {
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
- }
2270
+ // CONTENT - sharing content local
2271
+ newShareStatus = SHARE_STATUS.LOCAL_SHARE_ACTIVE;
1857
2272
  }
1858
2273
  // If we did not hit the cases above, no one is sharng content, so we check if we are sharing whiteboard
1859
2274
  // There is no concept of local/remote share for whiteboard
@@ -1937,23 +2352,22 @@ export default class Meeting extends StatelessWebexPlugin {
1937
2352
  EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
1938
2353
  {
1939
2354
  memberId: contentShare.beneficiaryId,
2355
+ url: contentShare.url,
2356
+ shareInstanceId: contentShare.shareInstanceId,
1940
2357
  }
1941
2358
  );
1942
2359
  };
1943
2360
 
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
- ) {
2361
+ try {
2362
+ // if a remote participant is stealing the presentation from us
2363
+ if (
2364
+ this.mediaProperties.mediaDirection?.sendShare &&
2365
+ oldShareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE
2366
+ ) {
2367
+ await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo screen share audio (SPARK-399690)
2368
+ }
2369
+ } finally {
1949
2370
  sendStartedSharingRemote();
1950
- } else {
1951
- this.updateShare({
1952
- sendShare: false,
1953
- receiveShare: this.mediaProperties.mediaDirection.receiveShare,
1954
- }).finally(() => {
1955
- sendStartedSharingRemote();
1956
- });
1957
2371
  }
1958
2372
  break;
1959
2373
  }
@@ -2007,6 +2421,8 @@ export default class Meeting extends StatelessWebexPlugin {
2007
2421
  EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
2008
2422
  {
2009
2423
  memberId: contentShare.beneficiaryId,
2424
+ url: contentShare.url,
2425
+ shareInstanceId: contentShare.shareInstanceId,
2010
2426
  }
2011
2427
  );
2012
2428
  this.members.locusMediaSharesUpdate(payload);
@@ -2041,8 +2457,30 @@ export default class Meeting extends StatelessWebexPlugin {
2041
2457
  private setUpLocusUrlListener() {
2042
2458
  this.locusInfo.on(EVENTS.LOCUS_INFO_UPDATE_URL, (payload) => {
2043
2459
  this.members.locusUrlUpdate(payload);
2460
+ this.breakouts.locusUrlUpdate(payload);
2461
+ this.annotation.locusUrlUpdate(payload);
2044
2462
  this.locusUrl = payload;
2045
2463
  this.locusId = this.locusUrl?.split('/').pop();
2464
+ this.recordingController.setLocusUrl(this.locusUrl);
2465
+ this.controlsOptionsManager.setLocusUrl(this.locusUrl);
2466
+ });
2467
+ }
2468
+
2469
+ /**
2470
+ * Set up the locus info service link listener
2471
+ * update the locusInfo for recording controller
2472
+ * does not currently re-emit the event as it's internal only
2473
+ * payload is unused
2474
+ * @returns {undefined}
2475
+ * @private
2476
+ * @memberof Meeting
2477
+ */
2478
+ private setUpLocusServicesListener() {
2479
+ this.locusInfo.on(LOCUSINFO.EVENTS.LINKS_SERVICES, (payload) => {
2480
+ this.recordingController.setServiceUrl(payload?.services?.record?.url);
2481
+ this.recordingController.setSessionId(this.locusInfo?.fullState?.sessionId);
2482
+ this.breakouts.breakoutServiceUrlUpdate(payload?.services?.breakout?.url);
2483
+ this.annotation.approvalUrlUpdate(payload?.services?.approval?.url);
2046
2484
  });
2047
2485
  }
2048
2486
 
@@ -2092,10 +2530,22 @@ export default class Meeting extends StatelessWebexPlugin {
2092
2530
  canAdmitParticipant: MeetingUtil.canAdmitParticipant(payload.info.userDisplayHints),
2093
2531
  canLock: MeetingUtil.canUserLock(payload.info.userDisplayHints),
2094
2532
  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),
2533
+ canSetDisallowUnmute: ControlsOptionsUtil.canSetDisallowUnmute(
2534
+ payload.info.userDisplayHints
2535
+ ),
2536
+ canUnsetDisallowUnmute: ControlsOptionsUtil.canUnsetDisallowUnmute(
2537
+ payload.info.userDisplayHints
2538
+ ),
2539
+ canSetMuteOnEntry: ControlsOptionsUtil.canSetMuteOnEntry(payload.info.userDisplayHints),
2540
+ canUnsetMuteOnEntry: ControlsOptionsUtil.canUnsetMuteOnEntry(
2541
+ payload.info.userDisplayHints
2542
+ ),
2543
+ canSetMuted: ControlsOptionsUtil.canSetMuted(payload.info.userDisplayHints),
2544
+ canUnsetMuted: ControlsOptionsUtil.canUnsetMuted(payload.info.userDisplayHints),
2545
+ canStartRecording: RecordingUtil.canUserStart(payload.info.userDisplayHints),
2546
+ canStopRecording: RecordingUtil.canUserStop(payload.info.userDisplayHints),
2547
+ canPauseRecording: RecordingUtil.canUserPause(payload.info.userDisplayHints),
2548
+ canResumeRecording: RecordingUtil.canUserResume(payload.info.userDisplayHints),
2099
2549
  canRaiseHand: MeetingUtil.canUserRaiseHand(payload.info.userDisplayHints),
2100
2550
  canLowerAllHands: MeetingUtil.canUserLowerAllHands(payload.info.userDisplayHints),
2101
2551
  canLowerSomeoneElsesHand: MeetingUtil.canUserLowerSomeoneElsesHand(
@@ -2108,6 +2558,9 @@ export default class Meeting extends StatelessWebexPlugin {
2108
2558
  canStartTranscribing: MeetingUtil.canStartTranscribing(payload.info.userDisplayHints),
2109
2559
  canStopTranscribing: MeetingUtil.canStopTranscribing(payload.info.userDisplayHints),
2110
2560
  isClosedCaptionActive: MeetingUtil.isClosedCaptionActive(payload.info.userDisplayHints),
2561
+ isSaveTranscriptsEnabled: MeetingUtil.isSaveTranscriptsEnabled(
2562
+ payload.info.userDisplayHints
2563
+ ),
2111
2564
  isWebexAssistantActive: MeetingUtil.isWebexAssistantActive(payload.info.userDisplayHints),
2112
2565
  canViewCaptionPanel: MeetingUtil.canViewCaptionPanel(payload.info.userDisplayHints),
2113
2566
  isRealTimeTranslationEnabled: MeetingUtil.isRealTimeTranslationEnabled(
@@ -2117,8 +2570,118 @@ export default class Meeting extends StatelessWebexPlugin {
2117
2570
  payload.info.userDisplayHints
2118
2571
  ),
2119
2572
  waitingForOthersToJoin: MeetingUtil.waitingForOthersToJoin(payload.info.userDisplayHints),
2573
+ canSendReactions: MeetingUtil.canSendReactions(
2574
+ this.inMeetingActions.canSendReactions,
2575
+ payload.info.userDisplayHints
2576
+ ),
2577
+ canManageBreakout: MeetingUtil.canManageBreakout(payload.info.userDisplayHints),
2578
+ canBroadcastMessageToBreakout: MeetingUtil.canBroadcastMessageToBreakout(
2579
+ payload.info.userDisplayHints
2580
+ ),
2581
+ canAdmitLobbyToBreakout: MeetingUtil.canAdmitLobbyToBreakout(
2582
+ payload.info.userDisplayHints
2583
+ ),
2584
+ isBreakoutPreassignmentsEnabled: MeetingUtil.isBreakoutPreassignmentsEnabled(
2585
+ payload.info.userDisplayHints
2586
+ ),
2587
+ canUserAskForHelp: MeetingUtil.canUserAskForHelp(payload.info.userDisplayHints),
2588
+ canUserRenameSelfAndObserved: MeetingUtil.canUserRenameSelfAndObserved(
2589
+ payload.info.userDisplayHints
2590
+ ),
2591
+ canUserRenameOthers: MeetingUtil.canUserRenameOthers(payload.info.userDisplayHints),
2592
+ canMuteAll: ControlsOptionsUtil.hasHints({
2593
+ requiredHints: [DISPLAY_HINTS.MUTE_ALL],
2594
+ displayHints: payload.info.userDisplayHints,
2595
+ }),
2596
+ canUnmuteAll: ControlsOptionsUtil.hasHints({
2597
+ requiredHints: [DISPLAY_HINTS.UNMUTE_ALL],
2598
+ displayHints: payload.info.userDisplayHints,
2599
+ }),
2600
+ canEnableHardMute: ControlsOptionsUtil.hasHints({
2601
+ requiredHints: [DISPLAY_HINTS.ENABLE_HARD_MUTE],
2602
+ displayHints: payload.info.userDisplayHints,
2603
+ }),
2604
+ canDisableHardMute: ControlsOptionsUtil.hasHints({
2605
+ requiredHints: [DISPLAY_HINTS.DISABLE_HARD_MUTE],
2606
+ displayHints: payload.info.userDisplayHints,
2607
+ }),
2608
+ canEnableMuteOnEntry: ControlsOptionsUtil.hasHints({
2609
+ requiredHints: [DISPLAY_HINTS.ENABLE_MUTE_ON_ENTRY],
2610
+ displayHints: payload.info.userDisplayHints,
2611
+ }),
2612
+ canDisableMuteOnEntry: ControlsOptionsUtil.hasHints({
2613
+ requiredHints: [DISPLAY_HINTS.DISABLE_MUTE_ON_ENTRY],
2614
+ displayHints: payload.info.userDisplayHints,
2615
+ }),
2616
+ canEnableReactions: ControlsOptionsUtil.hasHints({
2617
+ requiredHints: [DISPLAY_HINTS.ENABLE_REACTIONS],
2618
+ displayHints: payload.info.userDisplayHints,
2619
+ }),
2620
+ canDisableReactions: ControlsOptionsUtil.hasHints({
2621
+ requiredHints: [DISPLAY_HINTS.DISABLE_REACTIONS],
2622
+ displayHints: payload.info.userDisplayHints,
2623
+ }),
2624
+ canEnableReactionDisplayNames: ControlsOptionsUtil.hasHints({
2625
+ requiredHints: [DISPLAY_HINTS.ENABLE_SHOW_DISPLAY_NAME],
2626
+ displayHints: payload.info.userDisplayHints,
2627
+ }),
2628
+ canDisableReactionDisplayNames: ControlsOptionsUtil.hasHints({
2629
+ requiredHints: [DISPLAY_HINTS.DISABLE_SHOW_DISPLAY_NAME],
2630
+ displayHints: payload.info.userDisplayHints,
2631
+ }),
2632
+ canUpdateShareControl: ControlsOptionsUtil.hasHints({
2633
+ requiredHints: [DISPLAY_HINTS.SHARE_CONTROL],
2634
+ displayHints: payload.info.userDisplayHints,
2635
+ }),
2636
+ canEnableViewTheParticipantsList: ControlsOptionsUtil.hasHints({
2637
+ requiredHints: [DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST],
2638
+ displayHints: payload.info.userDisplayHints,
2639
+ }),
2640
+ canDisableViewTheParticipantsList: ControlsOptionsUtil.hasHints({
2641
+ requiredHints: [DISPLAY_HINTS.DISABLE_VIEW_THE_PARTICIPANT_LIST],
2642
+ displayHints: payload.info.userDisplayHints,
2643
+ }),
2644
+ canEnableRaiseHand: ControlsOptionsUtil.hasHints({
2645
+ requiredHints: [DISPLAY_HINTS.ENABLE_RAISE_HAND],
2646
+ displayHints: payload.info.userDisplayHints,
2647
+ }),
2648
+ canDisableRaiseHand: ControlsOptionsUtil.hasHints({
2649
+ requiredHints: [DISPLAY_HINTS.DISABLE_RAISE_HAND],
2650
+ displayHints: payload.info.userDisplayHints,
2651
+ }),
2652
+ canEnableVideo: ControlsOptionsUtil.hasHints({
2653
+ requiredHints: [DISPLAY_HINTS.ENABLE_VIDEO],
2654
+ displayHints: payload.info.userDisplayHints,
2655
+ }),
2656
+ canDisableVideo: ControlsOptionsUtil.hasHints({
2657
+ requiredHints: [DISPLAY_HINTS.DISABLE_VIDEO],
2658
+ displayHints: payload.info.userDisplayHints,
2659
+ }),
2660
+ canShareFile: ControlsOptionsUtil.hasHints({
2661
+ requiredHints: [DISPLAY_HINTS.SHARE_FILE],
2662
+ displayHints: payload.info.userDisplayHints,
2663
+ }),
2664
+ canShareApplication: ControlsOptionsUtil.hasHints({
2665
+ requiredHints: [DISPLAY_HINTS.SHARE_APPLICATION],
2666
+ displayHints: payload.info.userDisplayHints,
2667
+ }),
2668
+ canShareCamera: ControlsOptionsUtil.hasHints({
2669
+ requiredHints: [DISPLAY_HINTS.SHARE_CAMERA],
2670
+ displayHints: payload.info.userDisplayHints,
2671
+ }),
2672
+ canShareDesktop: ControlsOptionsUtil.hasHints({
2673
+ requiredHints: [DISPLAY_HINTS.SHARE_DESKTOP],
2674
+ displayHints: payload.info.userDisplayHints,
2675
+ }),
2676
+ canShareContent: ControlsOptionsUtil.hasHints({
2677
+ requiredHints: [DISPLAY_HINTS.SHARE_CONTENT],
2678
+ displayHints: payload.info.userDisplayHints,
2679
+ }),
2120
2680
  });
2121
2681
 
2682
+ this.recordingController.setDisplayHints(payload.info.userDisplayHints);
2683
+ this.controlsOptionsManager.setDisplayHints(payload.info.userDisplayHints);
2684
+
2122
2685
  if (changed) {
2123
2686
  Trigger.trigger(
2124
2687
  this,
@@ -2141,7 +2704,9 @@ export default class Meeting extends StatelessWebexPlugin {
2141
2704
  * @param {String} datachannelUrl
2142
2705
  * @returns {void}
2143
2706
  */
2707
+
2144
2708
  handleDataChannelUrlChange(datachannelUrl) {
2709
+ // @ts-ignore - config coming from registerPlugin
2145
2710
  if (datachannelUrl && this.config.enableAutomaticLLM) {
2146
2711
  // Defer this as updateLLMConnection relies upon this.locusInfo.url which is only set
2147
2712
  // after the MEETING_INFO_UPDATED callback finishes
@@ -2196,10 +2761,34 @@ export default class Meeting extends StatelessWebexPlugin {
2196
2761
  );
2197
2762
  }
2198
2763
  });
2764
+
2765
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED, (payload) => {
2766
+ if (payload) {
2767
+ if (this.video) {
2768
+ payload.muted = payload.muted ?? this.video.isRemotelyMuted();
2769
+ payload.unmuteAllowed = payload.unmuteAllowed ?? this.video.isUnmuteAllowed();
2770
+ this.video.handleServerRemoteMuteUpdate(this, payload.muted, payload.unmuteAllowed);
2771
+ }
2772
+ Trigger.trigger(
2773
+ this,
2774
+ {
2775
+ file: 'meeting/index',
2776
+ function: 'setUpLocusInfoSelfListener',
2777
+ },
2778
+ payload.muted
2779
+ ? EVENT_TRIGGERS.MEETING_SELF_VIDEO_MUTED_BY_OTHERS
2780
+ : EVENT_TRIGGERS.MEETING_SELF_VIDEO_UNMUTED_BY_OTHERS,
2781
+ {
2782
+ payload,
2783
+ }
2784
+ );
2785
+ }
2786
+ });
2787
+
2199
2788
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED, (payload) => {
2200
2789
  if (payload) {
2201
2790
  if (this.audio) {
2202
- this.audio.handleServerRemoteMuteUpdate(payload.muted, payload.unmuteAllowed);
2791
+ this.audio.handleServerRemoteMuteUpdate(this, payload.muted, payload.unmuteAllowed);
2203
2792
  }
2204
2793
  // with "mute on entry" server will send us remote mute even if we don't have media configured,
2205
2794
  // so if being muted by others, always send the notification,
@@ -2322,6 +2911,37 @@ export default class Meeting extends StatelessWebexPlugin {
2322
2911
  );
2323
2912
  });
2324
2913
 
2914
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_BREAKOUTS_CHANGED, (payload) => {
2915
+ this.breakouts.updateBreakoutSessions(payload);
2916
+ Trigger.trigger(
2917
+ this,
2918
+ {
2919
+ file: 'meeting/index',
2920
+ function: 'setUpLocusInfoSelfListener',
2921
+ },
2922
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_UPDATE
2923
+ );
2924
+ });
2925
+
2926
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ROLES_CHANGED, (payload) => {
2927
+ const isModeratorOrCohost =
2928
+ payload.newRoles?.includes(SELF_ROLES.MODERATOR) ||
2929
+ payload.newRoles?.includes(SELF_ROLES.COHOST);
2930
+ this.breakouts.updateCanManageBreakouts(isModeratorOrCohost);
2931
+
2932
+ Trigger.trigger(
2933
+ this,
2934
+ {
2935
+ file: 'meeting/index',
2936
+ function: 'setUpLocusInfoSelfListener',
2937
+ },
2938
+ EVENT_TRIGGERS.MEETING_SELF_ROLES_CHANGED,
2939
+ {
2940
+ payload,
2941
+ }
2942
+ );
2943
+ });
2944
+
2325
2945
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_IS_SHARING_BLOCKED_CHANGE, (payload) => {
2326
2946
  Trigger.trigger(
2327
2947
  this,
@@ -2357,19 +2977,18 @@ export default class Meeting extends StatelessWebexPlugin {
2357
2977
  .catch((error) => {
2358
2978
  // @ts-ignore
2359
2979
  LoggerProxy.logger.error(
2360
- `Meeting:index#setUpLocusInfoMeetingListener --> REMOTE_RESPONSE. Issue with leave for meeting, meeting still in collection: ${this.meeting}, error: ${error}`
2980
+ `Meeting:index#setUpLocusInfoMeetingListener --> REMOTE_RESPONSE. Issue with leave for meeting, meeting still in collection: ${this}, error: ${error}`
2361
2981
  );
2362
2982
  });
2363
2983
  }
2364
2984
  });
2365
- this.locusInfo.on(EVENTS.DESTROY_MEETING, (payload) => {
2985
+ this.locusInfo.on(EVENTS.DESTROY_MEETING, async (payload) => {
2366
2986
  // if self state is NOT left
2367
2987
 
2368
2988
  // TODO: Handle sharing and wireless sharing when meeting end
2369
2989
  if (this.wirelessShare) {
2370
2990
  if (this.mediaProperties.shareTrack) {
2371
- this.mediaProperties.shareTrack.onended = null;
2372
- this.mediaProperties.shareTrack.stop();
2991
+ await this.setLocalShareTrack(undefined);
2373
2992
  }
2374
2993
  }
2375
2994
  // when multiple WEB deviceType join with same user
@@ -2383,18 +3002,18 @@ export default class Meeting extends StatelessWebexPlugin {
2383
3002
  if (payload.shouldLeave) {
2384
3003
  // TODO: We should do cleaning of meeting object if the shouldLeave: false because there might be meeting object which we are not cleaning
2385
3004
 
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
- });
3005
+ try {
3006
+ await this.leave({reason: payload.reason});
3007
+
3008
+ LoggerProxy.logger.warn(
3009
+ 'Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. The meeting has been left, but has not been destroyed, you should see a later event for leave.'
3010
+ );
3011
+ } catch (error) {
3012
+ // @ts-ignore
3013
+ LoggerProxy.logger.error(
3014
+ `Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. Issue with leave for meeting, meeting still in collection: ${this}, error: ${error}`
3015
+ );
3016
+ }
2398
3017
  } else {
2399
3018
  LoggerProxy.logger.info(
2400
3019
  'Meeting:index#setUpLocusInfoMeetingListener --> MEETING_REMOVED_REASON',
@@ -2472,14 +3091,32 @@ export default class Meeting extends StatelessWebexPlugin {
2472
3091
  }
2473
3092
 
2474
3093
  /**
2475
- * Admit the guest(s) to the call once they are waiting
3094
+ * Admit the guest(s) to the call once they are waiting.
3095
+ * If the host/cohost is in a breakout session, the locus url
3096
+ * of the session must be provided as the authorizingLocusUrl.
3097
+ * Regardless of host/cohost location, the locus Id (lid) in
3098
+ * the path should be the locus Id of the main, which means the
3099
+ * locus url of the api call must be from the main session.
3100
+ * If these loucs urls are not provided, the function will do the check.
2476
3101
  * @param {Array} memberIds
3102
+ * @param {Object} sessionLocusUrls: {authorizingLocusUrl, mainLocusUrl}
2477
3103
  * @returns {Promise} see #members.admitMembers
2478
3104
  * @public
2479
3105
  * @memberof Meeting
2480
3106
  */
2481
- public admit(memberIds: Array<any>) {
2482
- return this.members.admitMembers(memberIds);
3107
+ public admit(
3108
+ memberIds: Array<any>,
3109
+ sessionLocusUrls?: {authorizingLocusUrl: string; mainLocusUrl: string}
3110
+ ) {
3111
+ let locusUrls = sessionLocusUrls;
3112
+ if (!locusUrls) {
3113
+ const {locusUrl, mainLocusUrl} = this.breakouts;
3114
+ if (locusUrl && mainLocusUrl) {
3115
+ locusUrls = {authorizingLocusUrl: locusUrl, mainLocusUrl};
3116
+ }
3117
+ }
3118
+
3119
+ return this.members.admitMembers(memberIds, locusUrls);
2483
3120
  }
2484
3121
 
2485
3122
  /**
@@ -2527,66 +3164,6 @@ export default class Meeting extends StatelessWebexPlugin {
2527
3164
  return this.members;
2528
3165
  }
2529
3166
 
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
3167
  /**
2591
3168
  * Sets the meeting info on the class instance
2592
3169
  * @param {Object} meetingInfo
@@ -2634,6 +3211,7 @@ export default class Meeting extends StatelessWebexPlugin {
2634
3211
  this.locusUrl = locusMeetingObject?.url || webexMeetingInfo?.locusUrl || this.locusUrl;
2635
3212
  // @ts-ignore - config coming from registerPlugin
2636
3213
  this.setSipUri(
3214
+ // @ts-ignore
2637
3215
  this.config.experimental.enableUnifiedMeetings
2638
3216
  ? locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipUrl
2639
3217
  : locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipMeetingUri || this.sipUri
@@ -2650,35 +3228,8 @@ export default class Meeting extends StatelessWebexPlugin {
2650
3228
  webexMeetingInfo?.hostId ||
2651
3229
  this.owner;
2652
3230
  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
- }
3231
+ // Need to populate environment when sending CA event
3232
+ this.environment = locusMeetingObject?.info.channel || webexMeetingInfo?.channel;
2682
3233
  }
2683
3234
  }
2684
3235
 
@@ -2708,7 +3259,7 @@ export default class Meeting extends StatelessWebexPlugin {
2708
3259
  * @private
2709
3260
  * @memberof Meeting
2710
3261
  */
2711
- private setLocus(
3262
+ setLocus(
2712
3263
  locus:
2713
3264
  | {
2714
3265
  mediaConnections: Array<any>;
@@ -2743,21 +3294,6 @@ export default class Meeting extends StatelessWebexPlugin {
2743
3294
  Trigger.trigger(this, options, EVENTS.REQUEST_UPLOAD_LOGS, this);
2744
3295
  }
2745
3296
 
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
3297
  /**
2762
3298
  * Removes remote audio, video and share tracks from class instance's mediaProperties
2763
3299
  * @returns {undefined}
@@ -2842,257 +3378,124 @@ export default class Meeting extends StatelessWebexPlugin {
2842
3378
  }
2843
3379
 
2844
3380
  /**
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
3381
+ * Stores the reference to a new microphone track, sets up the required event listeners
3382
+ * on it, cleans up previous track, etc.
3383
+ *
3384
+ * @param {LocalMicrophoneTrack | null} localTrack local microphone track
3385
+ * @returns {Promise<void>}
2849
3386
  */
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
- }
3387
+ private async setLocalAudioTrack(localTrack?: LocalMicrophoneTrack) {
3388
+ const oldTrack = this.mediaProperties.audioTrack;
2867
3389
 
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();
3390
+ oldTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3391
+ oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
2879
3392
 
2880
- this.mediaProperties.setMediaSettings('audio', {
2881
- echoCancellation: settings.echoCancellation,
2882
- noiseSuppression: settings.noiseSuppression,
2883
- });
3393
+ // we don't update this.mediaProperties.mediaDirection.sendAudio, because we always keep it as true to avoid extra SDP exchanges
3394
+ this.mediaProperties.setLocalAudioTrack(localTrack);
2884
3395
 
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
- }
3396
+ this.audio.handleLocalTrackChange(this);
3397
+
3398
+ localTrack?.on(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3399
+ localTrack?.on(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
2892
3400
 
2893
- if (emitEvent) {
2894
- this.sendLocalMediaReadyEvent();
3401
+ if (!this.isMultistream || !localTrack) {
3402
+ // for multistream WCME automatically un-publishes the old track when we publish a new one
3403
+ await this.unpublishTrack(oldTrack);
2895
3404
  }
3405
+ await this.publishTrack(this.mediaProperties.audioTrack);
2896
3406
  }
2897
3407
 
2898
3408
  /**
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
3409
+ * Stores the reference to a new camera track, sets up the required event listeners
3410
+ * on it, cleans up previous track, etc.
3411
+ *
3412
+ * @param {LocalCameraTrack | null} localTrack local camera track
3413
+ * @returns {Promise<void>}
2905
3414
  */
2906
- private setLocalVideoTrack(videoTrack: MediaStreamTrack, emitEvent = true) {
2907
- if (videoTrack) {
2908
- const {aspectRatio, frameRate, height, width, deviceId} = videoTrack.getSettings();
3415
+ private async setLocalVideoTrack(localTrack?: LocalCameraTrack) {
3416
+ const oldTrack = this.mediaProperties.videoTrack;
2909
3417
 
2910
- const {localQualityLevel} = this.mediaProperties;
3418
+ oldTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3419
+ oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
2911
3420
 
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`);
2916
-
2917
- this.mediaProperties.setLocalQualityLevel(`${height}p`);
2918
- }
3421
+ // we don't update this.mediaProperties.mediaDirection.sendVideo, because we always keep it as true to avoid extra SDP exchanges
3422
+ this.mediaProperties.setLocalVideoTrack(localTrack);
2919
3423
 
2920
- this.mediaProperties.setLocalVideoTrack(videoTrack);
2921
- if (this.video) this.video.applyClientStateLocally(this);
3424
+ this.video.handleLocalTrackChange(this);
2922
3425
 
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
- }
3426
+ localTrack?.on(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3427
+ localTrack?.on(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
2938
3428
 
2939
- if (emitEvent) {
2940
- this.sendLocalMediaReadyEvent();
3429
+ if (!this.isMultistream || !localTrack) {
3430
+ // for multistream WCME automatically un-publishes the old track when we publish a new one
3431
+ await this.unpublishTrack(oldTrack);
2941
3432
  }
3433
+ await this.publishTrack(this.mediaProperties.videoTrack);
2942
3434
  }
2943
3435
 
2944
3436
  /**
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
3437
+ * Stores the reference to a new screen share track, sets up the required event listeners
3438
+ * on it, cleans up previous track, etc.
3439
+ * It also sends the floor grant/release request.
3440
+ *
3441
+ * @param {LocalDisplayTrack | undefined} localDisplayTrack local camera track
3442
+ * @returns {Promise<void>}
2950
3443
  */
2951
- public setLocalTracks(localStream: any) {
2952
- if (localStream) {
2953
- const {audioTrack, videoTrack} = MeetingUtil.getTrack(localStream);
3444
+ private async setLocalShareTrack(localDisplayTrack?: LocalDisplayTrack) {
3445
+ const oldTrack = this.mediaProperties.shareTrack;
2954
3446
 
2955
- this.setLocalAudioTrack(audioTrack, false);
2956
- this.setLocalVideoTrack(videoTrack, false);
3447
+ oldTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3448
+ oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
2957
3449
 
2958
- this.sendLocalMediaReadyEvent();
2959
- }
2960
- }
3450
+ this.mediaProperties.setLocalShareTrack(localDisplayTrack);
2961
3451
 
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
- }
3452
+ localDisplayTrack?.on(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3453
+ localDisplayTrack?.on(
3454
+ LocalTrackEvents.UnderlyingTrackChange,
3455
+ this.underlyingLocalTrackChangeHandler
3456
+ );
2991
3457
 
2992
- contentTracks.onended = () => this.handleShareTrackEnded(localShare);
3458
+ this.mediaProperties.mediaDirection.sendShare = !!localDisplayTrack;
2993
3459
 
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
- );
3460
+ if (!this.isMultistream || !localDisplayTrack) {
3461
+ // for multistream WCME automatically un-publishes the old track when we publish a new one
3462
+ await this.unpublishTrack(oldTrack);
3006
3463
  }
3464
+ await this.publishTrack(this.mediaProperties.shareTrack);
3007
3465
  }
3008
3466
 
3009
3467
  /**
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
3468
+ * Removes references to local tracks. This function should be called
3469
+ * on cleanup when we leave the meeting etc.
3470
+ *
3471
+ * @internal
3472
+ * @returns {void}
3015
3473
  */
3016
- public closeLocalStream() {
3017
- const {audioTrack, videoTrack} = this.mediaProperties;
3474
+ public cleanupLocalTracks() {
3475
+ const {audioTrack, videoTrack, shareTrack} = this.mediaProperties;
3018
3476
 
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;
3477
+ audioTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3478
+ audioTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3024
3479
 
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
- }
3480
+ videoTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3481
+ videoTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3045
3482
 
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;
3483
+ shareTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3484
+ shareTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3055
3485
 
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
- }
3486
+ this.mediaProperties.setLocalAudioTrack(undefined);
3487
+ this.mediaProperties.setLocalVideoTrack(undefined);
3488
+ this.mediaProperties.setLocalShareTrack(undefined);
3077
3489
 
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
- }
3490
+ this.mediaProperties.mediaDirection.sendAudio = false;
3491
+ this.mediaProperties.mediaDirection.sendVideo = false;
3492
+ this.mediaProperties.mediaDirection.sendShare = false;
3087
3493
 
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();
3494
+ // WCME doesn't unpublish tracks when multistream connection is closed, so we do it here
3495
+ // (we have to do it for transcoded meetings anyway, so we might as well do for multistream too)
3496
+ audioTrack?.setPublished(false);
3497
+ videoTrack?.setPublished(false);
3498
+ shareTrack?.setPublished(false);
3096
3499
  }
3097
3500
 
3098
3501
  /**
@@ -3143,6 +3546,8 @@ export default class Meeting extends StatelessWebexPlugin {
3143
3546
  * @memberof Meeting
3144
3547
  */
3145
3548
  public closePeerConnections() {
3549
+ this.locusMediaRequest = undefined;
3550
+
3146
3551
  if (this.mediaProperties.webrtcMediaConnection) {
3147
3552
  if (this.remoteMediaManager) {
3148
3553
  this.remoteMediaManager.stop();
@@ -3157,6 +3562,9 @@ export default class Meeting extends StatelessWebexPlugin {
3157
3562
  this.mediaProperties.webrtcMediaConnection.close();
3158
3563
  }
3159
3564
 
3565
+ this.audio = null;
3566
+ this.video = null;
3567
+
3160
3568
  return Promise.resolve();
3161
3569
  }
3162
3570
 
@@ -3187,264 +3595,36 @@ export default class Meeting extends StatelessWebexPlugin {
3187
3595
  this.correlationId = id;
3188
3596
  }
3189
3597
 
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
3598
  /**
3401
3599
  * Shorthand function to join AND set up media
3402
3600
  * @param {Object} options - options to join with media
3403
3601
  * @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()}
3602
+ * @param {MediaDirection} [options.mediaOptions] - see #addMedia()
3603
+ * @returns {Promise} -- {join: see join(), media: see addMedia()}
3407
3604
  * @public
3408
3605
  * @memberof Meeting
3409
3606
  * @example
3410
3607
  * joinWithMedia({
3411
3608
  * 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
- * }})
3609
+ * mediaOptions: {
3610
+ * localTracks: { microphone: microphoneTrack, camera: cameraTrack }
3611
+ * }
3612
+ * })
3424
3613
  */
3425
3614
  public joinWithMedia(
3426
3615
  options: {
3427
3616
  joinOptions?: any;
3428
- mediaSettings: any;
3429
- audioVideoOptions?: any;
3430
- } = {} as any
3617
+ mediaOptions?: AddMediaOptions;
3618
+ } = {}
3431
3619
  ) {
3432
- // TODO: add validations for parameters
3433
- const {mediaSettings, joinOptions, audioVideoOptions} = options;
3620
+ const {mediaOptions, joinOptions} = options;
3434
3621
 
3435
3622
  return this.join(joinOptions)
3436
3623
  .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
- )
3624
+ this.addMedia(mediaOptions).then((mediaResponse) => ({
3625
+ join: joinResponse,
3626
+ media: mediaResponse,
3627
+ }))
3448
3628
  )
3449
3629
  .catch((error) => {
3450
3630
  LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
@@ -3582,6 +3762,20 @@ export default class Meeting extends StatelessWebexPlugin {
3582
3762
  return false;
3583
3763
  }
3584
3764
 
3765
+ /**
3766
+ * Check if the meeting supports the Reactions
3767
+ * @returns {boolean}
3768
+ */
3769
+ isReactionsSupported() {
3770
+ if (this.locusInfo?.controls?.reactions.enabled) {
3771
+ return true;
3772
+ }
3773
+
3774
+ LoggerProxy.logger.error('Meeting:index#isReactionsSupported --> Reactions is not supported');
3775
+
3776
+ return false;
3777
+ }
3778
+
3585
3779
  /**
3586
3780
  * Monitor the Low-Latency Mercury (LLM) web socket connection on `onError` and `onClose` states
3587
3781
  * @private
@@ -3633,6 +3827,7 @@ export default class Meeting extends StatelessWebexPlugin {
3633
3827
  // @ts-ignore - fix type
3634
3828
  const {
3635
3829
  body: {webSocketUrl},
3830
+ // @ts-ignore
3636
3831
  } = await this.request({
3637
3832
  method: HTTP_VERBS.POST,
3638
3833
  uri: datachannelUrl,
@@ -3682,6 +3877,44 @@ export default class Meeting extends StatelessWebexPlugin {
3682
3877
  }
3683
3878
  }
3684
3879
 
3880
+ /**
3881
+ * Callback called when a relay event is received from meeting LLM Connection
3882
+ * @param {RelayEvent} e Event object coming from LLM Connection
3883
+ * @private
3884
+ * @returns {void}
3885
+ */
3886
+ private processRelayEvent = (e: RelayEvent): void => {
3887
+ switch (e.data.relayType) {
3888
+ case REACTION_RELAY_TYPES.REACTION:
3889
+ if (
3890
+ // @ts-ignore - config coming from registerPlugin
3891
+ (this.config.receiveReactions || options.receiveReactions) &&
3892
+ this.isReactionsSupported()
3893
+ ) {
3894
+ const {name} = this.members.membersCollection.get(e.data.sender.participantId);
3895
+ const processedReaction: ProcessedReaction = {
3896
+ reaction: e.data.reaction,
3897
+ sender: {
3898
+ id: e.data.sender.participantId,
3899
+ name,
3900
+ },
3901
+ };
3902
+ Trigger.trigger(
3903
+ this,
3904
+ {
3905
+ file: 'meeting/index',
3906
+ function: 'join',
3907
+ },
3908
+ EVENT_TRIGGERS.MEETING_RECEIVE_REACTIONS,
3909
+ processedReaction
3910
+ );
3911
+ }
3912
+ break;
3913
+ default:
3914
+ break;
3915
+ }
3916
+ };
3917
+
3685
3918
  /**
3686
3919
  * stop recieving Transcription by closing
3687
3920
  * the web socket connection properly
@@ -3753,9 +3986,16 @@ export default class Meeting extends StatelessWebexPlugin {
3753
3986
  joinSuccess = resolve;
3754
3987
  });
3755
3988
 
3989
+ if (options.correlationId) {
3990
+ this.setCorrelationId(options.correlationId);
3991
+ LoggerProxy.logger.log(
3992
+ `Meeting:index#join --> Using a new correlation id from app ${this.correlationId}`
3993
+ );
3994
+ }
3995
+
3756
3996
  if (!this.hasJoinedOnce) {
3757
3997
  this.hasJoinedOnce = true;
3758
- } else {
3998
+ } else if (!options.correlationId) {
3759
3999
  LoggerProxy.logger.log(
3760
4000
  `Meeting:index#join --> Generating a new correlation id for meeting ${this.id}`
3761
4001
  );
@@ -3776,6 +4016,21 @@ export default class Meeting extends StatelessWebexPlugin {
3776
4016
  data: {trigger: trigger.USER_INTERACTION, isRoapCallEnabled: true},
3777
4017
  });
3778
4018
 
4019
+ if (!isEmpty(this.meetingInfo)) {
4020
+ Metrics.postEvent({
4021
+ event: eventType.MEETING_INFO_REQUEST,
4022
+ meeting: this,
4023
+ });
4024
+
4025
+ Metrics.postEvent({
4026
+ event: eventType.MEETING_INFO_RESPONSE,
4027
+ meeting: this,
4028
+ data: {
4029
+ meetingLookupUrl: this.meetingInfo?.meetingLookupUrl,
4030
+ },
4031
+ });
4032
+ }
4033
+
3779
4034
  LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
3780
4035
 
3781
4036
  if (this.meetingFiniteStateMachine.state === MEETING_STATE_MACHINE.STATES.ENDED) {
@@ -3804,18 +4059,12 @@ export default class Meeting extends StatelessWebexPlugin {
3804
4059
  return Promise.reject(error);
3805
4060
  }
3806
4061
 
3807
- this.mediaProperties.setLocalQualityLevel(options.meetingQuality);
3808
4062
  this.mediaProperties.setRemoteQualityLevel(options.meetingQuality);
3809
4063
  }
3810
4064
 
3811
4065
  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`;
4066
+ if (!QUALITY_LEVELS[options.meetingQuality.remote]) {
4067
+ const errorMessage = `Meeting:index#join --> ${options.meetingQuality.remote} not defined`;
3819
4068
 
3820
4069
  LoggerProxy.logger.error(errorMessage);
3821
4070
 
@@ -3827,9 +4076,6 @@ export default class Meeting extends StatelessWebexPlugin {
3827
4076
  return Promise.reject(new Error(errorMessage));
3828
4077
  }
3829
4078
 
3830
- if (options.meetingQuality.local) {
3831
- this.mediaProperties.setLocalQualityLevel(options.meetingQuality.local);
3832
- }
3833
4079
  if (options.meetingQuality.remote) {
3834
4080
  this.mediaProperties.setRemoteQualityLevel(options.meetingQuality.remote);
3835
4081
  }
@@ -3855,6 +4101,7 @@ export default class Meeting extends StatelessWebexPlugin {
3855
4101
  return join;
3856
4102
  })
3857
4103
  .then(async (join) => {
4104
+ // @ts-ignore - config coming from registerPlugin
3858
4105
  if (this.config.enableAutomaticLLM) {
3859
4106
  await this.updateLLMConnection();
3860
4107
  }
@@ -3923,22 +4170,41 @@ export default class Meeting extends StatelessWebexPlugin {
3923
4170
  * @returns {Promise}
3924
4171
  */
3925
4172
  async updateLLMConnection() {
4173
+ // @ts-ignore - Fix type
3926
4174
  const {url, info: {datachannelUrl} = {}} = this.locusInfo;
3927
4175
 
3928
4176
  const isJoined = this.joinedWith && this.joinedWith.state === 'JOINED';
3929
4177
 
4178
+ // @ts-ignore - Fix type
3930
4179
  if (this.webex.internal.llm.isConnected()) {
4180
+ // @ts-ignore - Fix type
3931
4181
  if (url === this.webex.internal.llm.getLocusUrl() && isJoined) {
3932
4182
  return undefined;
3933
4183
  }
4184
+ // @ts-ignore - Fix type
3934
4185
  await this.webex.internal.llm.disconnectLLM();
4186
+ // @ts-ignore - Fix type
4187
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
3935
4188
  }
3936
4189
 
3937
4190
  if (!isJoined) {
3938
4191
  return undefined;
3939
4192
  }
3940
4193
 
3941
- return this.webex.internal.llm.registerAndConnect(url, datachannelUrl);
4194
+ // @ts-ignore - Fix type
4195
+ return this.webex.internal.llm
4196
+ .registerAndConnect(url, datachannelUrl)
4197
+ .then((registerAndConnectResult) => {
4198
+ // @ts-ignore - Fix type
4199
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
4200
+ // @ts-ignore - Fix type
4201
+ this.webex.internal.llm.on('event:relay.event', this.processRelayEvent);
4202
+ LoggerProxy.logger.info(
4203
+ 'Meeting:index#updateLLMConnection --> enabled to receive relay events!'
4204
+ );
4205
+
4206
+ return Promise.resolve(registerAndConnectResult);
4207
+ });
3942
4208
  }
3943
4209
 
3944
4210
  /**
@@ -3980,28 +4246,28 @@ export default class Meeting extends StatelessWebexPlugin {
3980
4246
 
3981
4247
  if (!this.dialInUrl) this.dialInUrl = `dialin:///${uuid.v4()}`;
3982
4248
 
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
- });
4249
+ return (
4250
+ this.meetingRequest
4251
+ // @ts-ignore
4252
+ .dialIn({
4253
+ correlationId,
4254
+ dialInUrl: this.dialInUrl,
4255
+ locusUrl,
4256
+ clientUrl: this.deviceUrl,
4257
+ })
4258
+ .catch((error) => {
4259
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_DIAL_IN_FAILURE, {
4260
+ correlation_id: this.correlationId,
4261
+ dial_in_url: this.dialInUrl,
4262
+ locus_id: locusUrl.split('/').pop(),
4263
+ client_url: this.deviceUrl,
4264
+ reason: error.error?.message,
4265
+ stack: error.stack,
4266
+ });
4002
4267
 
4003
- return Promise.reject(error);
4004
- });
4268
+ return Promise.reject(error);
4269
+ })
4270
+ );
4005
4271
  }
4006
4272
 
4007
4273
  /**
@@ -4018,29 +4284,29 @@ export default class Meeting extends StatelessWebexPlugin {
4018
4284
 
4019
4285
  if (!this.dialOutUrl) this.dialOutUrl = `dialout:///${uuid.v4()}`;
4020
4286
 
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
- });
4287
+ return (
4288
+ this.meetingRequest
4289
+ // @ts-ignore
4290
+ .dialOut({
4291
+ correlationId,
4292
+ dialOutUrl: this.dialOutUrl,
4293
+ phoneNumber,
4294
+ locusUrl,
4295
+ clientUrl: this.deviceUrl,
4296
+ })
4297
+ .catch((error) => {
4298
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_DIAL_OUT_FAILURE, {
4299
+ correlation_id: this.correlationId,
4300
+ dial_out_url: this.dialOutUrl,
4301
+ locus_id: locusUrl.split('/').pop(),
4302
+ client_url: this.deviceUrl,
4303
+ reason: error.error?.message,
4304
+ stack: error.stack,
4305
+ });
4041
4306
 
4042
- return Promise.reject(error);
4043
- });
4307
+ return Promise.reject(error);
4308
+ })
4309
+ );
4044
4310
  }
4045
4311
 
4046
4312
  /**
@@ -4116,14 +4382,10 @@ export default class Meeting extends StatelessWebexPlugin {
4116
4382
  },
4117
4383
  };
4118
4384
 
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();
4385
+ this.cleanupLocalTracks();
4125
4386
 
4126
- this.mediaProperties.unsetMediaTracks();
4387
+ this.mediaProperties.setMediaDirection(mediaSettings.mediaDirection);
4388
+ this.mediaProperties.unsetRemoteMedia();
4127
4389
 
4128
4390
  // 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
4391
  // once the DX answers we establish connection back the media server with only receiveShare enabled
@@ -4206,165 +4468,6 @@ export default class Meeting extends StatelessWebexPlugin {
4206
4468
  });
4207
4469
  }
4208
4470
 
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
4471
  /**
4369
4472
  * Handles ROAP_FAILURE event from the webrtc media connection
4370
4473
  *
@@ -4387,7 +4490,7 @@ export default class Meeting extends StatelessWebexPlugin {
4387
4490
  Metrics.sendBehavioralMetric(metricName, data, metadata);
4388
4491
  };
4389
4492
 
4390
- if (error instanceof MC.Errors.SdpOfferCreationError) {
4493
+ if (error instanceof Errors.SdpOfferCreationError) {
4391
4494
  sendBehavioralMetric(BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE, error, this.id);
4392
4495
 
4393
4496
  Metrics.postEvent({
@@ -4401,8 +4504,8 @@ export default class Meeting extends StatelessWebexPlugin {
4401
4504
  },
4402
4505
  });
4403
4506
  } else if (
4404
- error instanceof MC.Errors.SdpOfferHandlingError ||
4405
- error instanceof MC.Errors.SdpAnswerHandlingError
4507
+ error instanceof Errors.SdpOfferHandlingError ||
4508
+ error instanceof Errors.SdpAnswerHandlingError
4406
4509
  ) {
4407
4510
  sendBehavioralMetric(BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE, error, this.id);
4408
4511
 
@@ -4416,8 +4519,8 @@ export default class Meeting extends StatelessWebexPlugin {
4416
4519
  ],
4417
4520
  },
4418
4521
  });
4419
- } else if (error instanceof MC.Errors.SdpError) {
4420
- // this covers also the case of MC.Errors.IceGatheringError which extends MC.Errors.SdpError
4522
+ } else if (error instanceof Errors.SdpError) {
4523
+ // this covers also the case of Errors.IceGatheringError which extends Errors.SdpError
4421
4524
  sendBehavioralMetric(BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE, error, this.id);
4422
4525
 
4423
4526
  Metrics.postEvent({
@@ -4434,20 +4537,20 @@ export default class Meeting extends StatelessWebexPlugin {
4434
4537
  };
4435
4538
 
4436
4539
  setupMediaConnectionListeners = () => {
4437
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_STARTED, () => {
4540
+ this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_STARTED, () => {
4438
4541
  this.isRoapInProgress = true;
4439
4542
  });
4440
4543
 
4441
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_DONE, () => {
4544
+ this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_DONE, () => {
4442
4545
  this.mediaNegotiatedEvent();
4443
4546
  this.isRoapInProgress = false;
4444
4547
  this.processNextQueuedMediaUpdate();
4445
4548
  });
4446
4549
 
4447
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_FAILURE, this.handleRoapFailure);
4550
+ this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_FAILURE, this.handleRoapFailure);
4448
4551
 
4449
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_MESSAGE_TO_SEND, (event) => {
4450
- const LOG_HEADER = 'Meeting:index#setupMediaConnectionListeners.ROAP_MESSAGE_TO_SEND -->';
4552
+ this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_MESSAGE_TO_SEND, (event) => {
4553
+ const LOG_HEADER = `Meeting:index#setupMediaConnectionListeners.ROAP_MESSAGE_TO_SEND --> correlationId=${this.correlationId}`;
4451
4554
 
4452
4555
  switch (event.roapMessage.messageType) {
4453
4556
  case 'OK':
@@ -4463,9 +4566,7 @@ export default class Meeting extends StatelessWebexPlugin {
4463
4566
  correlationId: this.correlationId,
4464
4567
  }),
4465
4568
  {
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, `,
4569
+ logText: `${LOG_HEADER} Roap OK`,
4469
4570
  }
4470
4571
  );
4471
4572
  break;
@@ -4485,9 +4586,7 @@ export default class Meeting extends StatelessWebexPlugin {
4485
4586
  reconnect: this.reconnectionManager.isReconnectInProgress(),
4486
4587
  }),
4487
4588
  {
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, `,
4589
+ logText: `${LOG_HEADER} Roap Offer`,
4491
4590
  }
4492
4591
  );
4493
4592
  break;
@@ -4506,9 +4605,7 @@ export default class Meeting extends StatelessWebexPlugin {
4506
4605
  correlationId: this.correlationId,
4507
4606
  }),
4508
4607
  {
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, `,
4608
+ logText: `${LOG_HEADER} Roap Answer`,
4512
4609
  }
4513
4610
  ).catch((error) => {
4514
4611
  const metricName = BEHAVIORAL_METRICS.ROAP_ANSWER_FAILURE;
@@ -4528,8 +4625,8 @@ export default class Meeting extends StatelessWebexPlugin {
4528
4625
 
4529
4626
  case 'ERROR':
4530
4627
  if (
4531
- event.roapMessage.errorType === MC.ErrorType.CONFLICT ||
4532
- event.roapMessage.errorType === MC.ErrorType.DOUBLECONFLICT
4628
+ event.roapMessage.errorType === ErrorType.CONFLICT ||
4629
+ event.roapMessage.errorType === ErrorType.DOUBLECONFLICT
4533
4630
  ) {
4534
4631
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_GLARE_CONDITION, {
4535
4632
  correlation_id: this.correlationId,
@@ -4545,9 +4642,7 @@ export default class Meeting extends StatelessWebexPlugin {
4545
4642
  correlationId: this.correlationId,
4546
4643
  }),
4547
4644
  {
4548
- header: `${LOG_HEADER} Send Roap Error.`,
4549
- success: `${LOG_HEADER} Successfully send roap error`,
4550
- failure: `${LOG_HEADER} Failed to send roap error, `,
4645
+ logText: `${LOG_HEADER} Roap Error (${event.roapMessage.errorType})`,
4551
4646
  }
4552
4647
  );
4553
4648
  break;
@@ -4561,7 +4656,7 @@ export default class Meeting extends StatelessWebexPlugin {
4561
4656
  });
4562
4657
 
4563
4658
  // eslint-disable-next-line no-param-reassign
4564
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.REMOTE_TRACK_ADDED, (event) => {
4659
+ this.mediaProperties.webrtcMediaConnection.on(Event.REMOTE_TRACK_ADDED, (event) => {
4565
4660
  LoggerProxy.logger.log(
4566
4661
  `Meeting:index#setupMediaConnectionListeners --> REMOTE_TRACK_ADDED event received for webrtcMediaConnection: ${JSON.stringify(
4567
4662
  event
@@ -4574,15 +4669,15 @@ export default class Meeting extends StatelessWebexPlugin {
4574
4669
  let eventType;
4575
4670
 
4576
4671
  switch (event.type) {
4577
- case MC.RemoteTrackType.AUDIO:
4672
+ case RemoteTrackType.AUDIO:
4578
4673
  eventType = EVENT_TYPES.REMOTE_AUDIO;
4579
4674
  this.mediaProperties.setRemoteAudioTrack(event.track);
4580
4675
  break;
4581
- case MC.RemoteTrackType.VIDEO:
4676
+ case RemoteTrackType.VIDEO:
4582
4677
  eventType = EVENT_TYPES.REMOTE_VIDEO;
4583
4678
  this.mediaProperties.setRemoteVideoTrack(event.track);
4584
4679
  break;
4585
- case MC.RemoteTrackType.SCREENSHARE_VIDEO:
4680
+ case RemoteTrackType.SCREENSHARE_VIDEO:
4586
4681
  if (event.track) {
4587
4682
  eventType = EVENT_TYPES.REMOTE_SHARE;
4588
4683
  this.mediaProperties.setRemoteShare(event.track);
@@ -4595,10 +4690,6 @@ export default class Meeting extends StatelessWebexPlugin {
4595
4690
  }
4596
4691
  }
4597
4692
 
4598
- // start stats here the stats are coming null if you dont receive streams
4599
-
4600
- this.statsAnalyzer.startAnalyzer(this.mediaProperties.webrtcMediaConnection);
4601
-
4602
4693
  if (eventType && mediaTrack) {
4603
4694
  Trigger.trigger(
4604
4695
  this,
@@ -4615,7 +4706,7 @@ export default class Meeting extends StatelessWebexPlugin {
4615
4706
  }
4616
4707
  });
4617
4708
 
4618
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.CONNECTION_STATE_CHANGED, (event) => {
4709
+ this.mediaProperties.webrtcMediaConnection.on(Event.CONNECTION_STATE_CHANGED, (event) => {
4619
4710
  const connectionFailed = () => {
4620
4711
  // we know the media connection failed and browser will not attempt to recover it any more
4621
4712
  // so reset the timer as it's not needed anymore, we want to reconnect immediately
@@ -4645,13 +4736,13 @@ export default class Meeting extends StatelessWebexPlugin {
4645
4736
  };
4646
4737
 
4647
4738
  LoggerProxy.logger.info(
4648
- `Meeting:index#setupMediaConnectionListeners --> connection state changed to ${event.state}`
4739
+ `Meeting:index#setupMediaConnectionListeners --> correlationId=${this.correlationId} connection state changed to ${event.state}`
4649
4740
  );
4650
4741
  switch (event.state) {
4651
- case MC.ConnectionState.Connecting:
4742
+ case ConnectionState.Connecting:
4652
4743
  Metrics.postEvent({event: eventType.ICE_START, meeting: this});
4653
4744
  break;
4654
- case MC.ConnectionState.Connected:
4745
+ case ConnectionState.Connected:
4655
4746
  Metrics.postEvent({event: eventType.ICE_END, meeting: this});
4656
4747
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_SUCCESS, {
4657
4748
  correlation_id: this.correlationId,
@@ -4659,8 +4750,9 @@ export default class Meeting extends StatelessWebexPlugin {
4659
4750
  });
4660
4751
  this.setNetworkStatus(NETWORK_STATUS.CONNECTED);
4661
4752
  this.reconnectionManager.iceReconnected();
4753
+ this.statsAnalyzer.startAnalyzer(this.mediaProperties.webrtcMediaConnection);
4662
4754
  break;
4663
- case MC.ConnectionState.Disconnected:
4755
+ case ConnectionState.Disconnected:
4664
4756
  this.setNetworkStatus(NETWORK_STATUS.DISCONNECTED);
4665
4757
  this.reconnectionManager.waitForIceReconnect().catch(() => {
4666
4758
  LoggerProxy.logger.info(
@@ -4670,7 +4762,7 @@ export default class Meeting extends StatelessWebexPlugin {
4670
4762
  connectionFailed();
4671
4763
  });
4672
4764
  break;
4673
- case MC.ConnectionState.Failed:
4765
+ case ConnectionState.Failed:
4674
4766
  connectionFailed();
4675
4767
  break;
4676
4768
  default:
@@ -4678,7 +4770,7 @@ export default class Meeting extends StatelessWebexPlugin {
4678
4770
  }
4679
4771
  });
4680
4772
 
4681
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.ACTIVE_SPEAKERS_CHANGED, (msg) => {
4773
+ this.mediaProperties.webrtcMediaConnection.on(Event.ACTIVE_SPEAKERS_CHANGED, (msg) => {
4682
4774
  Trigger.trigger(
4683
4775
  this,
4684
4776
  {
@@ -4689,6 +4781,7 @@ export default class Meeting extends StatelessWebexPlugin {
4689
4781
  {
4690
4782
  seqNum: msg.seqNum,
4691
4783
  memberIds: msg.csis
4784
+ // @ts-ignore
4692
4785
  .map((csi) => this.members.findMemberByCsi(csi)?.id)
4693
4786
  .filter((item) => item !== undefined),
4694
4787
  }
@@ -4696,8 +4789,8 @@ export default class Meeting extends StatelessWebexPlugin {
4696
4789
  });
4697
4790
 
4698
4791
  this.mediaProperties.webrtcMediaConnection.on(
4699
- MC.Event.VIDEO_SOURCES_COUNT_CHANGED,
4700
- (numTotalSources, numLiveSources) => {
4792
+ Event.VIDEO_SOURCES_COUNT_CHANGED,
4793
+ (numTotalSources, numLiveSources, mediaContent) => {
4701
4794
  Trigger.trigger(
4702
4795
  this,
4703
4796
  {
@@ -4708,14 +4801,15 @@ export default class Meeting extends StatelessWebexPlugin {
4708
4801
  {
4709
4802
  numTotalSources,
4710
4803
  numLiveSources,
4804
+ mediaContent,
4711
4805
  }
4712
4806
  );
4713
4807
  }
4714
4808
  );
4715
4809
 
4716
4810
  this.mediaProperties.webrtcMediaConnection.on(
4717
- MC.Event.AUDIO_SOURCES_COUNT_CHANGED,
4718
- (numTotalSources, numLiveSources) => {
4811
+ Event.AUDIO_SOURCES_COUNT_CHANGED,
4812
+ (numTotalSources, numLiveSources, mediaContent) => {
4719
4813
  Trigger.trigger(
4720
4814
  this,
4721
4815
  {
@@ -4726,6 +4820,7 @@ export default class Meeting extends StatelessWebexPlugin {
4726
4820
  {
4727
4821
  numTotalSources,
4728
4822
  numLiveSources,
4823
+ mediaContent,
4729
4824
  }
4730
4825
  );
4731
4826
  }
@@ -4744,6 +4839,7 @@ export default class Meeting extends StatelessWebexPlugin {
4744
4839
  // Add ip address info if geo hint is present
4745
4840
  // @ts-ignore fix type
4746
4841
  options.data.intervalMetadata.peerReflexiveIP =
4842
+ // @ts-ignore
4747
4843
  this.webex.meetings.geoHintInfo?.clientAddress ||
4748
4844
  options.data.intervalMetadata.peerReflexiveIP ||
4749
4845
  MQA_STATS.DEFAULT_IP;
@@ -4813,7 +4909,15 @@ export default class Meeting extends StatelessWebexPlugin {
4813
4909
  return `MC-${this.id.substring(0, 4)}`;
4814
4910
  }
4815
4911
 
4816
- createMediaConnection(turnServerInfo) {
4912
+ /**
4913
+ * Creates a webrtc media connection and publishes tracks to it
4914
+ *
4915
+ * @param {Object} turnServerInfo TURN server information
4916
+ * @param {BundlePolicy} [bundlePolicy] Bundle policy settings
4917
+ * @returns {RoapMediaConnection | MultistreamRoapMediaConnection}
4918
+ */
4919
+ private async createMediaConnection(turnServerInfo, bundlePolicy?: BundlePolicy) {
4920
+ // create the actual media connection
4817
4921
  const mc = Media.createMediaConnection(this.isMultistream, this.getMediaConnectionDebugId(), {
4818
4922
  mediaProperties: this.mediaProperties,
4819
4923
  remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
@@ -4822,11 +4926,23 @@ export default class Meeting extends StatelessWebexPlugin {
4822
4926
  // @ts-ignore - config coming from registerPlugin
4823
4927
  enableExtmap: this.config.enableExtmap,
4824
4928
  turnServerInfo,
4929
+ bundlePolicy,
4825
4930
  });
4826
4931
 
4827
4932
  this.mediaProperties.setMediaPeerConnection(mc);
4828
4933
  this.setupMediaConnectionListeners();
4829
4934
 
4935
+ // publish the tracks
4936
+ if (this.mediaProperties.audioTrack) {
4937
+ await this.publishTrack(this.mediaProperties.audioTrack);
4938
+ }
4939
+ if (this.mediaProperties.videoTrack) {
4940
+ await this.publishTrack(this.mediaProperties.videoTrack);
4941
+ }
4942
+ if (this.mediaProperties.shareTrack) {
4943
+ await this.publishTrack(this.mediaProperties.shareTrack);
4944
+ }
4945
+
4830
4946
  return mc;
4831
4947
  }
4832
4948
 
@@ -4854,23 +4970,21 @@ export default class Meeting extends StatelessWebexPlugin {
4854
4970
  }
4855
4971
 
4856
4972
  /**
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
4973
+ * Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
4974
+ *
4975
+ * @param {AddMediaOptions} options
4864
4976
  * @returns {Promise}
4865
4977
  * @public
4866
4978
  * @memberof Meeting
4867
4979
  */
4868
- addMedia(options: any = {}) {
4980
+ addMedia(options: AddMediaOptions = {}) {
4869
4981
  const LOG_HEADER = 'Meeting:index#addMedia -->';
4870
4982
 
4871
4983
  let turnDiscoverySkippedReason;
4872
4984
  let turnServerUsed = false;
4873
4985
 
4986
+ LoggerProxy.logger.info(`${LOG_HEADER} called with: ${JSON.stringify(options)}`);
4987
+
4874
4988
  if (this.meetingState !== FULL_STATE.ACTIVE) {
4875
4989
  return Promise.reject(new MeetingNotActiveError());
4876
4990
  }
@@ -4884,9 +4998,14 @@ export default class Meeting extends StatelessWebexPlugin {
4884
4998
  return Promise.reject(new UserInLobbyError());
4885
4999
  }
4886
5000
 
4887
- const {localStream, localShare, mediaSettings, remoteMediaManagerConfig} = options;
4888
-
4889
- LoggerProxy.logger.info(`${LOG_HEADER} Adding Media.`);
5001
+ const {
5002
+ localTracks,
5003
+ audioEnabled = true,
5004
+ videoEnabled = true,
5005
+ receiveShare = true,
5006
+ remoteMediaManagerConfig,
5007
+ bundlePolicy,
5008
+ } = options;
4890
5009
 
4891
5010
  Metrics.postEvent({
4892
5011
  event: eventType.MEDIA_CAPABILITIES,
@@ -4911,17 +5030,61 @@ export default class Meeting extends StatelessWebexPlugin {
4911
5030
  },
4912
5031
  });
4913
5032
 
4914
- return MeetingUtil.validateOptions(options)
5033
+ // when audioEnabled/videoEnabled is true, we set sendAudio/sendVideo to true even before any tracks are published
5034
+ // to avoid doing an extra SDP exchange when they are published for the first time
5035
+ this.mediaProperties.setMediaDirection({
5036
+ sendAudio: audioEnabled,
5037
+ sendVideo: videoEnabled,
5038
+ sendShare: false,
5039
+ receiveAudio: audioEnabled,
5040
+ receiveVideo: videoEnabled,
5041
+ receiveShare,
5042
+ });
5043
+
5044
+ this.locusMediaRequest = new LocusMediaRequest(
5045
+ {
5046
+ correlationId: this.correlationId,
5047
+ device: {
5048
+ url: this.deviceUrl,
5049
+ // @ts-ignore
5050
+ deviceType: this.config.deviceType,
5051
+ },
5052
+ preferTranscoding: !this.isMultistream,
5053
+ },
5054
+ {
5055
+ // @ts-ignore
5056
+ parent: this.webex,
5057
+ }
5058
+ );
5059
+
5060
+ this.audio = createMuteState(AUDIO, this, audioEnabled);
5061
+ this.video = createMuteState(VIDEO, this, videoEnabled);
5062
+
5063
+ this.annotationInfo = localTracks?.annotationInfo;
5064
+
5065
+ const promises = [];
5066
+
5067
+ // setup all the references to local tracks in this.mediaProperties before creating media connection
5068
+ // and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages
5069
+ if (localTracks?.microphone) {
5070
+ promises.push(this.setLocalAudioTrack(localTracks.microphone));
5071
+ }
5072
+ if (localTracks?.camera) {
5073
+ promises.push(this.setLocalVideoTrack(localTracks.camera));
5074
+ }
5075
+ if (localTracks?.screenShare?.video) {
5076
+ promises.push(this.setLocalShareTrack(localTracks.screenShare.video));
5077
+ }
5078
+
5079
+ return Promise.all(promises)
4915
5080
  .then(() => this.roap.doTurnDiscovery(this, false))
4916
- .then((turnDiscoveryObject) => {
5081
+ .then(async (turnDiscoveryObject) => {
4917
5082
  ({turnDiscoverySkippedReason} = turnDiscoveryObject);
4918
5083
  turnServerUsed = !turnDiscoverySkippedReason;
4919
5084
 
4920
5085
  const {turnServerInfo} = turnDiscoveryObject;
4921
5086
 
4922
- this.preMedia(localStream, localShare, mediaSettings);
4923
-
4924
- const mc = this.createMediaConnection(turnServerInfo);
5087
+ const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
4925
5088
 
4926
5089
  if (this.isMultistream) {
4927
5090
  this.remoteMediaManager = new RemoteMediaManager(
@@ -4946,16 +5109,16 @@ export default class Meeting extends StatelessWebexPlugin {
4946
5109
  EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
4947
5110
  );
4948
5111
 
4949
- return this.remoteMediaManager.start().then(() => mc.initiateOffer());
5112
+ await this.remoteMediaManager.start();
4950
5113
  }
4951
5114
 
4952
- return mc.initiateOffer();
5115
+ await mc.initiateOffer();
4953
5116
  })
4954
5117
  .then(() => {
4955
5118
  this.setMercuryListener();
4956
5119
  })
4957
5120
  .then(() =>
4958
- this.getDevices().then((devices) => {
5121
+ getDevices().then((devices) => {
4959
5122
  MeetingUtil.handleDeviceLogging(devices);
4960
5123
  })
4961
5124
  )
@@ -4967,8 +5130,12 @@ export default class Meeting extends StatelessWebexPlugin {
4967
5130
  if (this.config.stats.enableStatsAnalyzer) {
4968
5131
  // @ts-ignore - config coming from registerPlugin
4969
5132
  this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
4970
- // @ts-ignore - config coming from registerPlugin
4971
- this.statsAnalyzer = new StatsAnalyzer(this.config.stats, this.networkQualityMonitor);
5133
+ this.statsAnalyzer = new StatsAnalyzer(
5134
+ // @ts-ignore - config coming from registerPlugin
5135
+ this.config.stats,
5136
+ (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
5137
+ this.networkQualityMonitor
5138
+ );
4972
5139
  this.setupStatsAnalyzerEventHandlers();
4973
5140
  this.networkQualityMonitor.on(
4974
5141
  EVENT_TRIGGERS.NETWORK_QUALITY,
@@ -4991,7 +5158,7 @@ export default class Meeting extends StatelessWebexPlugin {
4991
5158
 
4992
5159
  // eslint-disable-next-line func-names
4993
5160
  // eslint-disable-next-line prefer-arrow-callback
4994
- if (this.type === _CALL_) {
5161
+ if (this.type === _CALL_ || this.meetingState === FULL_STATE.ACTIVE) {
4995
5162
  resolve();
4996
5163
  }
4997
5164
  const joiningTimer = setInterval(() => {
@@ -5010,21 +5177,19 @@ export default class Meeting extends StatelessWebexPlugin {
5010
5177
  )
5011
5178
  .then(() =>
5012
5179
  this.mediaProperties.waitForMediaConnectionConnected().catch(() => {
5013
- throw createMeetingsError(30202, 'Meeting connection failed');
5180
+ throw new Error(
5181
+ `Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
5182
+ );
5014
5183
  })
5015
5184
  )
5016
5185
  .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;
5186
+ if (localTracks?.screenShare?.video) {
5187
+ this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.LAMBDA, {
5188
+ lambda: async () => {
5189
+ return this.requestScreenShareFloor();
5190
+ },
5191
+ });
5025
5192
  }
5026
-
5027
- return {};
5028
5193
  })
5029
5194
  .then(() => this.mediaProperties.getCurrentConnectionType())
5030
5195
  .then((connectionType) => {
@@ -5032,9 +5197,36 @@ export default class Meeting extends StatelessWebexPlugin {
5032
5197
  correlation_id: this.correlationId,
5033
5198
  locus_id: this.locusUrl.split('/').pop(),
5034
5199
  connectionType,
5200
+ isMultistream: this.isMultistream,
5035
5201
  });
5036
5202
  })
5037
5203
  .catch((error) => {
5204
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
5205
+ correlation_id: this.correlationId,
5206
+ locus_id: this.locusUrl.split('/').pop(),
5207
+ reason: error.message,
5208
+ stack: error.stack,
5209
+ code: error.code,
5210
+ turnDiscoverySkippedReason,
5211
+ turnServerUsed,
5212
+ isMultistream: this.isMultistream,
5213
+ signalingState:
5214
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5215
+ ?.signalingState ||
5216
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
5217
+ 'unknown',
5218
+ connectionState:
5219
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5220
+ ?.connectionState ||
5221
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.connectionState ||
5222
+ 'unknown',
5223
+ iceConnectionState:
5224
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5225
+ ?.iceConnectionState ||
5226
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
5227
+ 'unknown',
5228
+ });
5229
+
5038
5230
  // Clean up stats analyzer, peer connection, and turn off listeners
5039
5231
  const stopStatsAnalyzer = this.statsAnalyzer
5040
5232
  ? this.statsAnalyzer.stopAnalyzer()
@@ -5053,16 +5245,6 @@ export default class Meeting extends StatelessWebexPlugin {
5053
5245
  error
5054
5246
  );
5055
5247
 
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
5248
  // Upload logs on error while adding media
5067
5249
  Trigger.trigger(
5068
5250
  this,
@@ -5074,7 +5256,7 @@ export default class Meeting extends StatelessWebexPlugin {
5074
5256
  this
5075
5257
  );
5076
5258
 
5077
- if (error instanceof MC.Errors.SdpError) {
5259
+ if (error instanceof Errors.SdpError) {
5078
5260
  this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
5079
5261
  }
5080
5262
 
@@ -5102,7 +5284,14 @@ export default class Meeting extends StatelessWebexPlugin {
5102
5284
  * @private
5103
5285
  * @memberof Meeting
5104
5286
  */
5105
- private enqueueMediaUpdate(mediaUpdateType: string, options: object) {
5287
+ private enqueueMediaUpdate(mediaUpdateType: string, options: any): Promise<void> {
5288
+ if (mediaUpdateType === MEDIA_UPDATE_TYPE.LAMBDA && typeof options?.lambda !== 'function') {
5289
+ return Promise.reject(
5290
+ new Error('lambda must be specified when enqueuing MEDIA_UPDATE_TYPE.LAMBDA')
5291
+ );
5292
+ }
5293
+ const canUpdateMediaNow = this.canUpdateMedia();
5294
+
5106
5295
  return new Promise((resolve, reject) => {
5107
5296
  const queueItem = {
5108
5297
  pendingPromiseResolve: resolve,
@@ -5115,6 +5304,10 @@ export default class Meeting extends StatelessWebexPlugin {
5115
5304
  `Meeting:index#enqueueMediaUpdate --> enqueuing media update type=${mediaUpdateType}`
5116
5305
  );
5117
5306
  this.queuedMediaUpdates.push(queueItem);
5307
+
5308
+ if (canUpdateMediaNow) {
5309
+ this.processNextQueuedMediaUpdate();
5310
+ }
5118
5311
  });
5119
5312
  }
5120
5313
 
@@ -5154,18 +5347,17 @@ export default class Meeting extends StatelessWebexPlugin {
5154
5347
  LoggerProxy.logger.log(
5155
5348
  `Meeting:index#processNextQueuedMediaUpdate --> performing delayed media update type=${mediaUpdateType}`
5156
5349
  );
5350
+ let mediaUpdate = Promise.resolve();
5351
+
5157
5352
  switch (mediaUpdateType) {
5158
- case MEDIA_UPDATE_TYPE.ALL:
5159
- this.updateMedia(options).then(pendingPromiseResolve, pendingPromiseReject);
5353
+ case MEDIA_UPDATE_TYPE.TRANSCODED_MEDIA_CONNECTION:
5354
+ mediaUpdate = this.updateTranscodedMediaConnection();
5160
5355
  break;
5161
- case MEDIA_UPDATE_TYPE.AUDIO:
5162
- this.updateAudio(options).then(pendingPromiseResolve, pendingPromiseReject);
5356
+ case MEDIA_UPDATE_TYPE.LAMBDA:
5357
+ mediaUpdate = options.lambda();
5163
5358
  break;
5164
- case MEDIA_UPDATE_TYPE.VIDEO:
5165
- this.updateVideo(options).then(pendingPromiseResolve, pendingPromiseReject);
5166
- break;
5167
- case MEDIA_UPDATE_TYPE.SHARE:
5168
- this.updateShare(options).then(pendingPromiseResolve, pendingPromiseReject);
5359
+ case MEDIA_UPDATE_TYPE.UPDATE_MEDIA:
5360
+ mediaUpdate = this.updateMedia(options);
5169
5361
  break;
5170
5362
  default:
5171
5363
  LoggerProxy.logger.error(
@@ -5173,358 +5365,113 @@ export default class Meeting extends StatelessWebexPlugin {
5173
5365
  );
5174
5366
  break;
5175
5367
  }
5368
+
5369
+ mediaUpdate
5370
+ .then(pendingPromiseResolve, pendingPromiseReject)
5371
+ .then(() => this.processNextQueuedMediaUpdate());
5176
5372
  }
5177
5373
  };
5178
5374
 
5179
5375
  /**
5180
- * A confluence of updateAudio, updateVideo, and updateShare
5181
- * this function re-establishes all of the media streams with new options
5376
+ * Updates the media connection - it allows to enable/disable all audio/video/share in the meeting.
5377
+ * This does not affect the published tracks, so for example if a microphone track is published and
5378
+ * updateMedia({audioEnabled: false}) is called, the audio will not be sent or received anymore,
5379
+ * but the track's "published" state is not changed and when updateMedia({audioEnabled: true}) is called,
5380
+ * the sending of the audio from the same track will resume.
5381
+ *
5182
5382
  * @param {Object} options
5183
- * @param {MediaStream} options.localStream
5184
- * @param {MediaStream} options.localShare
5185
- * @param {MediaDirection} options.mediaSettings
5383
+ * @param {boolean} options.audioEnabled [optional] enables/disables receiving and sending of main audio in the meeting
5384
+ * @param {boolean} options.videoEnabled [optional] enables/disables receiving and sending of main video in the meeting
5385
+ * @param {boolean} options.shareEnabled [optional] enables/disables receiving and sending of screen share in the meeting
5186
5386
  * @returns {Promise}
5187
5387
  * @public
5188
5388
  * @memberof Meeting
5189
5389
  */
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 -->';
5390
+ public async updateMedia(options: {
5391
+ audioEnabled?: boolean;
5392
+ videoEnabled?: boolean;
5393
+ receiveShare?: boolean;
5394
+ }) {
5395
+ this.checkMediaConnection();
5198
5396
 
5199
- if (!this.canUpdateMedia()) {
5200
- return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.ALL, options);
5201
- }
5202
- const {localStream, localShare, mediaSettings} = options;
5397
+ const {audioEnabled, videoEnabled, receiveShare} = options;
5203
5398
 
5204
- const previousSendShareStatus = this.mediaProperties.mediaDirection.sendShare;
5399
+ LoggerProxy.logger.log(
5400
+ `Meeting:index#updateMedia --> called with options=${JSON.stringify(options)}`
5401
+ );
5205
5402
 
5206
- if (!this.mediaProperties.webrtcMediaConnection) {
5207
- return Promise.reject(new Error('media connection not established, call addMedia() first'));
5403
+ if (!this.canUpdateMedia()) {
5404
+ return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.UPDATE_MEDIA, options);
5208
5405
  }
5209
5406
 
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
- }
5407
+ if (this.isMultistream) {
5408
+ if (videoEnabled !== undefined) {
5409
+ throw new Error(
5410
+ 'enabling/disabling video in a meeting is not supported for multistream, it can only be done upfront when calling addMedia()'
5411
+ );
5412
+ }
5267
5413
 
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);
5414
+ if (receiveShare !== undefined) {
5415
+ throw new Error(
5416
+ 'toggling receiveShare in a multistream meeting is not supported, to control receiving screen share call meeting.remoteMediaManager.setLayout() with appropriate layout'
5417
+ );
5418
+ }
5288
5419
  }
5289
- const {sendAudio, receiveAudio, stream} = options;
5290
- let track = MeetingUtil.getTrack(stream).audioTrack;
5291
5420
 
5292
- if (typeof sendAudio !== 'boolean' || typeof receiveAudio !== 'boolean') {
5293
- return Promise.reject(new ParameterError('Pass sendAudio and receiveAudio parameter'));
5421
+ if (audioEnabled !== undefined) {
5422
+ this.mediaProperties.mediaDirection.sendAudio = audioEnabled;
5423
+ this.mediaProperties.mediaDirection.receiveAudio = audioEnabled;
5424
+ this.audio.enable(this, audioEnabled);
5294
5425
  }
5295
5426
 
5296
- if (!this.mediaProperties.webrtcMediaConnection) {
5297
- return Promise.reject(new Error('media connection not established, call addMedia() first'));
5427
+ if (videoEnabled !== undefined) {
5428
+ this.mediaProperties.mediaDirection.sendVideo = videoEnabled;
5429
+ this.mediaProperties.mediaDirection.receiveVideo = videoEnabled;
5430
+ this.video.enable(this, videoEnabled);
5298
5431
  }
5299
5432
 
5300
- if (this.effects && this.effects.state) {
5301
- const bnrEnabled = this.effects.state.bnr.enabled;
5433
+ if (receiveShare !== undefined) {
5434
+ this.mediaProperties.mediaDirection.receiveShare = receiveShare;
5435
+ }
5302
5436
 
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');
5437
+ if (this.isMultistream) {
5438
+ if (audioEnabled !== undefined) {
5439
+ await this.mediaProperties.webrtcMediaConnection.enableMultistreamAudio(audioEnabled);
5311
5440
  }
5441
+ } else {
5442
+ await this.updateTranscodedMediaConnection();
5312
5443
  }
5313
5444
 
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
- });
5445
+ return undefined;
5336
5446
  }
5337
5447
 
5338
5448
  /**
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}
5449
+ * Acknowledge the meeting, outgoing or incoming
5450
+ * @param {String} type
5451
+ * @returns {Promise} resolve {message, ringing, response}
5348
5452
  * @public
5349
5453
  * @memberof Meeting
5350
5454
  */
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'));
5455
+ public acknowledge(type: string) {
5456
+ if (!type) {
5457
+ return Promise.reject(new ParameterError('Type must be set to acknowledge the meeting.'));
5360
5458
  }
5459
+ if (type === _INCOMING_) {
5460
+ return this.meetingRequest
5461
+ .acknowledgeMeeting({
5462
+ locusUrl: this.locusUrl,
5463
+ deviceUrl: this.deviceUrl,
5464
+ correlationId: this.correlationId,
5465
+ })
5466
+ .then((response) => Promise.resolve(response))
5467
+ .then((response) => {
5468
+ this.meetingFiniteStateMachine.ring(type);
5469
+ Metrics.postEvent({event: eventType.ALERT_DISPLAYED, meeting: this});
5361
5470
 
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);
5499
- }
5500
-
5501
- /**
5502
- * Acknowledge the meeting, outgoing or incoming
5503
- * @param {String} type
5504
- * @returns {Promise} resolve {message, ringing, response}
5505
- * @public
5506
- * @memberof Meeting
5507
- */
5508
- public acknowledge(type: string) {
5509
- if (!type) {
5510
- return Promise.reject(new ParameterError('Type must be set to acknowledge the meeting.'));
5511
- }
5512
- if (type === _INCOMING_) {
5513
- return this.meetingRequest
5514
- .acknowledgeMeeting({
5515
- locusUrl: this.locusUrl,
5516
- deviceUrl: this.deviceUrl,
5517
- correlationId: this.correlationId,
5518
- })
5519
- .then((response) => Promise.resolve(response))
5520
- .then((response) => {
5521
- this.meetingFiniteStateMachine.ring(type);
5522
- Metrics.postEvent({event: eventType.ALERT_DISPLAYED, meeting: this});
5523
-
5524
- return Promise.resolve({
5525
- response,
5526
- });
5527
- });
5471
+ return Promise.resolve({
5472
+ response,
5473
+ });
5474
+ });
5528
5475
  }
5529
5476
 
5530
5477
  // TODO: outside of 1:1 incoming, and all outgoing calls
@@ -5563,13 +5510,12 @@ export default class Meeting extends StatelessWebexPlugin {
5563
5510
  * @memberof Meeting
5564
5511
  */
5565
5512
  public leave(options: {resourceId?: string; reason?: any} = {} as any) {
5513
+ const leaveReason = options.reason || MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST;
5566
5514
  Metrics.postEvent({
5567
5515
  event: eventType.LEAVE,
5568
5516
  meeting: this,
5569
- data: {trigger: trigger.USER_INTERACTION, canProceed: false},
5517
+ data: {trigger: trigger.USER_INTERACTION, canProceed: false, reason: leaveReason},
5570
5518
  });
5571
- const leaveReason = options.reason || MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST;
5572
-
5573
5519
  LoggerProxy.logger.log('Meeting:index#leave --> Leaving a meeting');
5574
5520
 
5575
5521
  return MeetingUtil.leaveMeeting(this, options)
@@ -5738,55 +5684,68 @@ export default class Meeting extends StatelessWebexPlugin {
5738
5684
  * @memberof Meeting
5739
5685
  */
5740
5686
  private requestScreenShareFloor() {
5741
- const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
5687
+ if (!this.mediaProperties.shareTrack || !this.mediaProperties.mediaDirection.sendShare) {
5688
+ LoggerProxy.logger.log(
5689
+ `Meeting:index#requestScreenShareFloor --> NOT requesting floor, because we don't have the share track anymore (shareTrack=${
5690
+ this.mediaProperties.shareTrack ? 'yes' : 'no'
5691
+ }, sendShare=${this.mediaProperties.mediaDirection.sendShare})`
5692
+ );
5742
5693
 
5743
- if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
5744
- Metrics.postEvent({event: eventType.SHARE_INITIATED, meeting: this});
5694
+ return Promise.resolve({});
5695
+ }
5696
+ if (this.state === MEETING_STATE.STATES.JOINED) {
5697
+ const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
5698
+
5699
+ if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
5700
+ Metrics.postEvent({event: eventType.SHARE_INITIATED, meeting: this});
5701
+
5702
+ return this.meetingRequest
5703
+ .changeMeetingFloor({
5704
+ disposition: FLOOR_ACTION.GRANTED,
5705
+ personUrl: this.locusInfo.self.url,
5706
+ deviceUrl: this.deviceUrl,
5707
+ uri: content.url,
5708
+ resourceUrl: this.resourceUrl,
5709
+ annotationInfo: this.annotationInfo,
5710
+ })
5711
+ .then(() => {
5712
+ this.isSharing = true;
5745
5713
 
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;
5714
+ return Promise.resolve();
5715
+ })
5716
+ .catch((error) => {
5717
+ LoggerProxy.logger.error('Meeting:index#share --> Error ', error);
5756
5718
 
5757
- return Promise.resolve();
5758
- })
5759
- .catch((error) => {
5760
- LoggerProxy.logger.error('Meeting:index#share --> Error ', error);
5719
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_FAILURE, {
5720
+ correlation_id: this.correlationId,
5721
+ locus_id: this.locusUrl.split('/').pop(),
5722
+ reason: error.message,
5723
+ stack: error.stack,
5724
+ });
5761
5725
 
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,
5726
+ return Promise.reject(error);
5767
5727
  });
5728
+ }
5768
5729
 
5769
- return Promise.reject(error);
5770
- });
5730
+ return Promise.reject(new ParameterError('Cannot share without content.'));
5771
5731
  }
5732
+ this.floorGrantPending = true;
5772
5733
 
5773
- return Promise.reject(new ParameterError('Cannot share without content.'));
5734
+ return Promise.resolve({});
5774
5735
  }
5775
5736
 
5776
5737
  /**
5777
- * Stops the screen share
5778
- * @returns {Promise} see #updateShare
5779
- * @public
5780
- * @memberof Meeting
5738
+ * Requests screen share floor if such request is pending.
5739
+ * It should be called whenever meeting state changes to JOINED
5740
+ *
5741
+ * @returns {void}
5781
5742
  */
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
- });
5743
+ private requestScreenShareFloorIfPending() {
5744
+ if (this.floorGrantPending && this.state === MEETING_STATE.STATES.JOINED) {
5745
+ this.requestScreenShareFloor().then(() => {
5746
+ this.floorGrantPending = false;
5747
+ });
5748
+ }
5790
5749
  }
5791
5750
 
5792
5751
  /**
@@ -5798,11 +5757,10 @@ export default class Meeting extends StatelessWebexPlugin {
5798
5757
  private releaseScreenShareFloor() {
5799
5758
  const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
5800
5759
 
5801
- if (content && this.mediaProperties.mediaDirection.sendShare) {
5760
+ if (content) {
5802
5761
  Metrics.postEvent({event: eventType.SHARE_STOPPED, meeting: this});
5803
- Media.stopTracks(this.mediaProperties.shareTrack);
5804
5762
 
5805
- if (content.floor.beneficiary.id !== this.selfId) {
5763
+ if (content.floor?.beneficiary.id !== this.selfId) {
5806
5764
  // remote participant started sharing and caused our sharing to stop, we don't want to send any floor action request in that case
5807
5765
  this.isSharing = false;
5808
5766
 
@@ -5834,7 +5792,10 @@ export default class Meeting extends StatelessWebexPlugin {
5834
5792
  });
5835
5793
  }
5836
5794
 
5837
- return Promise.reject(new ParameterError('Cannot stop share without content'));
5795
+ // according to Locus there is no content, so we don't need to release the floor (it's probably already been released)
5796
+ this.isSharing = false;
5797
+
5798
+ return Promise.resolve();
5838
5799
  }
5839
5800
 
5840
5801
  /**
@@ -5844,7 +5805,50 @@ export default class Meeting extends StatelessWebexPlugin {
5844
5805
  * @memberof Meeting
5845
5806
  */
5846
5807
  public startRecording() {
5847
- return MeetingUtil.startRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
5808
+ return this.recordingController.startRecording();
5809
+ }
5810
+
5811
+ /**
5812
+ * set the mute on entry flag for participants if you're the host
5813
+ * @returns {Promise}
5814
+ * @param {boolean} enabled
5815
+ * @public
5816
+ * @memberof Meeting
5817
+ */
5818
+ public setMuteOnEntry(enabled: boolean) {
5819
+ return this.controlsOptionsManager.setMuteOnEntry(enabled);
5820
+ }
5821
+
5822
+ /**
5823
+ * set the disallow unmute flag for participants if you're the host
5824
+ * @returns {Promise}
5825
+ * @param {boolean} enabled
5826
+ * @public
5827
+ * @memberof Meeting
5828
+ */
5829
+ public setDisallowUnmute(enabled: boolean) {
5830
+ return this.controlsOptionsManager.setDisallowUnmute(enabled);
5831
+ }
5832
+
5833
+ /**
5834
+ * set the mute all flag for participants if you're the host
5835
+ * @returns {Promise}
5836
+ * @param {boolean} mutedEnabled
5837
+ * @param {boolean} disallowUnmuteEnabled
5838
+ * @param {boolean} muteOnEntryEnabled
5839
+ * @public
5840
+ * @memberof Meeting
5841
+ */
5842
+ public setMuteAll(
5843
+ mutedEnabled: boolean,
5844
+ disallowUnmuteEnabled: boolean,
5845
+ muteOnEntryEnabled: boolean
5846
+ ) {
5847
+ return this.controlsOptionsManager.setMuteAll(
5848
+ mutedEnabled,
5849
+ disallowUnmuteEnabled,
5850
+ muteOnEntryEnabled
5851
+ );
5848
5852
  }
5849
5853
 
5850
5854
  /**
@@ -5854,7 +5858,7 @@ export default class Meeting extends StatelessWebexPlugin {
5854
5858
  * @memberof Meeting
5855
5859
  */
5856
5860
  public stopRecording() {
5857
- return MeetingUtil.stopRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
5861
+ return this.recordingController.stopRecording();
5858
5862
  }
5859
5863
 
5860
5864
  /**
@@ -5864,7 +5868,7 @@ export default class Meeting extends StatelessWebexPlugin {
5864
5868
  * @memberof Meeting
5865
5869
  */
5866
5870
  public pauseRecording() {
5867
- return MeetingUtil.pauseRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
5871
+ return this.recordingController.pauseRecording();
5868
5872
  }
5869
5873
 
5870
5874
  /**
@@ -5874,7 +5878,7 @@ export default class Meeting extends StatelessWebexPlugin {
5874
5878
  * @memberof Meeting
5875
5879
  */
5876
5880
  public resumeRecording() {
5877
- return MeetingUtil.resumeRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
5881
+ return this.recordingController.resumeRecording();
5878
5882
  }
5879
5883
 
5880
5884
  /**
@@ -6048,11 +6052,6 @@ export default class Meeting extends StatelessWebexPlugin {
6048
6052
  main: layoutInfo.main,
6049
6053
  content: layoutInfo.content,
6050
6054
  })
6051
- .then((response) => {
6052
- if (response && response.body && response.body.locus) {
6053
- this.locusInfo.onFullLocus(response.body.locus);
6054
- }
6055
- })
6056
6055
  .catch((error) => {
6057
6056
  LoggerProxy.logger.error('Meeting:index#changeVideoLayout --> Error ', error);
6058
6057
 
@@ -6060,62 +6059,6 @@ export default class Meeting extends StatelessWebexPlugin {
6060
6059
  });
6061
6060
  }
6062
6061
 
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
6062
  /**
6120
6063
  * Sets the quality level of the remote incoming media
6121
6064
  * @param {String} level {LOW|MEDIUM|HIGH}
@@ -6151,129 +6094,7 @@ export default class Meeting extends StatelessWebexPlugin {
6151
6094
  // Set the quality level in properties
6152
6095
  this.mediaProperties.setRemoteQualityLevel(level);
6153
6096
 
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
- });
6097
+ return this.updateTranscodedMediaConnection();
6277
6098
  }
6278
6099
 
6279
6100
  /**
@@ -6283,20 +6104,18 @@ export default class Meeting extends StatelessWebexPlugin {
6283
6104
  * @param {MediaStream} localShare
6284
6105
  * @returns {undefined}
6285
6106
  */
6286
- private handleShareTrackEnded(localShare: MediaStream) {
6107
+ private handleShareTrackEnded = async () => {
6287
6108
  if (this.wirelessShare) {
6288
6109
  this.leave({reason: MEETING_REMOVED_REASON.USER_ENDED_SHARE_STREAMS});
6289
6110
  } else {
6290
- // Skip checking for a stable peerConnection
6291
- // to allow immediately stopping screenshare
6292
- this.stopShare({
6293
- skipSignalingCheck: true,
6294
- }).catch((error) => {
6111
+ try {
6112
+ await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo: screen share audio (SPARK-399690)
6113
+ } catch (error) {
6295
6114
  LoggerProxy.logger.log(
6296
6115
  'Meeting:index#handleShareTrackEnded --> Error stopping share: ',
6297
6116
  error
6298
6117
  );
6299
- });
6118
+ }
6300
6119
  }
6301
6120
 
6302
6121
  Trigger.trigger(
@@ -6307,11 +6126,10 @@ export default class Meeting extends StatelessWebexPlugin {
6307
6126
  },
6308
6127
  EVENT_TRIGGERS.MEETING_STOPPED_SHARING_LOCAL,
6309
6128
  {
6310
- type: EVENT_TYPES.LOCAL_SHARE,
6311
- stream: localShare,
6129
+ reason: SHARE_STOPPED_REASON.TRACK_ENDED,
6312
6130
  }
6313
6131
  );
6314
- }
6132
+ };
6315
6133
 
6316
6134
  /**
6317
6135
  * Emits the 'network:quality' event
@@ -6341,14 +6159,16 @@ export default class Meeting extends StatelessWebexPlugin {
6341
6159
 
6342
6160
  /**
6343
6161
  * Handle logging the media
6344
- * @param {Object} audioTrack The audio track
6345
- * @param {Object} videoTrack The video track
6162
+ * @param {Object} mediaProperties
6346
6163
  * @private
6347
6164
  * @returns {undefined}
6348
6165
  */
6349
- private handleMediaLogging({audioTrack, videoTrack}: any) {
6350
- MeetingUtil.handleVideoLogging(videoTrack);
6351
- MeetingUtil.handleAudioLogging(audioTrack);
6166
+ private handleMediaLogging(mediaProperties: {
6167
+ audioTrack?: LocalMicrophoneTrack;
6168
+ videoTrack?: LocalCameraTrack;
6169
+ }) {
6170
+ MeetingUtil.handleVideoLogging(mediaProperties.videoTrack);
6171
+ MeetingUtil.handleAudioLogging(mediaProperties.audioTrack);
6352
6172
  }
6353
6173
 
6354
6174
  /**
@@ -6437,7 +6257,7 @@ export default class Meeting extends StatelessWebexPlugin {
6437
6257
  const end = this.endLocalSDPGenRemoteSDPRecvDelay;
6438
6258
 
6439
6259
  if (start && end) {
6440
- const calculatedDelay = end - start;
6260
+ const calculatedDelay = Math.round(end - start);
6441
6261
 
6442
6262
  return calculatedDelay > METRICS_JOIN_TIMES_MAX_DURATION ? undefined : calculatedDelay;
6443
6263
  }
@@ -6449,26 +6269,26 @@ export default class Meeting extends StatelessWebexPlugin {
6449
6269
  *
6450
6270
  * @returns {undefined}
6451
6271
  */
6452
- setStartCallInitiateJoinReq() {
6453
- this.startCallInitiateJoinReq = performance.now();
6454
- this.endCallInitiateJoinReq = undefined;
6272
+ setStartCallInitJoinReq() {
6273
+ this.startCallInitJoinReq = performance.now();
6274
+ this.endCallInitJoinReq = undefined;
6455
6275
  }
6456
6276
 
6457
6277
  /**
6458
6278
  *
6459
6279
  * @returns {undefined}
6460
6280
  */
6461
- setEndCallInitiateJoinReq() {
6462
- this.endCallInitiateJoinReq = performance.now();
6281
+ setEndCallInitJoinReq() {
6282
+ this.endCallInitJoinReq = performance.now();
6463
6283
  }
6464
6284
 
6465
6285
  /**
6466
6286
  *
6467
6287
  * @returns {string} duration between call initiate and sending join request to locus
6468
6288
  */
6469
- getCallInitiateJoinReq() {
6470
- const start = this.startCallInitiateJoinReq;
6471
- const end = this.endCallInitiateJoinReq;
6289
+ getCallInitJoinReq() {
6290
+ const start = this.startCallInitJoinReq;
6291
+ const end = this.endCallInitJoinReq;
6472
6292
 
6473
6293
  if (start && end) {
6474
6294
  const calculatedDelay = end - start;
@@ -6505,7 +6325,7 @@ export default class Meeting extends StatelessWebexPlugin {
6505
6325
  const end = this.endJoinReqResp;
6506
6326
 
6507
6327
  if (start && end) {
6508
- const calculatedDelay = end - start;
6328
+ const calculatedDelay = Math.round(end - start);
6509
6329
 
6510
6330
  return calculatedDelay > METRICS_JOIN_TIMES_MAX_DURATION ? undefined : calculatedDelay;
6511
6331
  }
@@ -6518,10 +6338,45 @@ export default class Meeting extends StatelessWebexPlugin {
6518
6338
  * @returns {string} duration between call initiate and successful locus join (even if it is in lobby)
6519
6339
  */
6520
6340
  getTotalJmt() {
6521
- const start = this.startCallInitiateJoinReq;
6341
+ const start = this.startCallInitJoinReq;
6522
6342
  const end = this.endJoinReqResp;
6523
6343
 
6524
- return start && end ? end - start : undefined;
6344
+ return start && end ? Math.round(end - start) : undefined;
6345
+ }
6346
+
6347
+ /**
6348
+ *
6349
+ * @returns {string} one of 'attendee','host','cohost', returns the user type of the current user
6350
+ */
6351
+ getCurUserType() {
6352
+ const {roles} = this;
6353
+ if (roles) {
6354
+ if (roles.includes(SELF_ROLES.MODERATOR)) {
6355
+ return 'host';
6356
+ }
6357
+ if (roles.includes(SELF_ROLES.COHOST)) {
6358
+ return 'cohost';
6359
+ }
6360
+ if (roles.includes(SELF_ROLES.ATTENDEE)) {
6361
+ return 'attendee';
6362
+ }
6363
+ }
6364
+
6365
+ return null;
6366
+ }
6367
+
6368
+ /**
6369
+ *
6370
+ * @returns {string} one of 'login-ci','unverified-guest', returns the login type of the current user
6371
+ */
6372
+ getCurLoginType() {
6373
+ // @ts-ignore
6374
+ if (this.webex.canAuthorize) {
6375
+ // @ts-ignore
6376
+ return this.webex.credentials.isUnverifiedGuest ? 'unverified-guest' : 'login-ci';
6377
+ }
6378
+
6379
+ return null;
6525
6380
  }
6526
6381
 
6527
6382
  /**
@@ -6611,121 +6466,6 @@ export default class Meeting extends StatelessWebexPlugin {
6611
6466
  }
6612
6467
  };
6613
6468
 
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
6469
  /**
6730
6470
  * starts keepAlives being sent
6731
6471
  * @returns {void}
@@ -6791,13 +6531,13 @@ export default class Meeting extends StatelessWebexPlugin {
6791
6531
  /**
6792
6532
  * Send a reaction inside the meeting.
6793
6533
  *
6794
- * @param {ReactionType} reactionType - type of reaction to be sent. Example: "thumbs_up"
6534
+ * @param {ReactionServerType} reactionType - type of reaction to be sent. Example: "thumbs_up"
6795
6535
  * @param {SkinToneType} skinToneType - skin tone for the reaction. Example: "medium_dark"
6796
6536
  * @returns {Promise}
6797
6537
  * @public
6798
6538
  * @memberof Meeting
6799
6539
  */
6800
- public sendReaction(reactionType: ReactionType, skinToneType?: SkinToneType) {
6540
+ public sendReaction(reactionType: ReactionServerType, skinToneType?: SkinToneType) {
6801
6541
  const reactionChannelUrl = this.locusInfo?.controls?.reactions?.reactionChannelUrl as string;
6802
6542
  const participantId = this.members.selfId;
6803
6543
 
@@ -6840,4 +6580,226 @@ export default class Meeting extends StatelessWebexPlugin {
6840
6580
  requestingParticipantId: this.members.selfId,
6841
6581
  });
6842
6582
  }
6583
+
6584
+ /**
6585
+ * Throws if we don't have a media connection created
6586
+ *
6587
+ * @returns {void}
6588
+ */
6589
+ private checkMediaConnection() {
6590
+ if (this.mediaProperties?.webrtcMediaConnection) {
6591
+ return;
6592
+ }
6593
+ throw new NoMediaEstablishedYetError();
6594
+ }
6595
+
6596
+ /**
6597
+ * Method to enable or disable the 'Music mode' effect on audio track
6598
+ *
6599
+ * @param {boolean} shouldEnableMusicMode
6600
+ * @returns {Promise}
6601
+ */
6602
+ async enableMusicMode(shouldEnableMusicMode: boolean) {
6603
+ this.checkMediaConnection();
6604
+
6605
+ if (!this.isMultistream) {
6606
+ throw new Error('enableMusicMode() only supported with multistream');
6607
+ }
6608
+
6609
+ if (shouldEnableMusicMode) {
6610
+ await this.mediaProperties.webrtcMediaConnection.setCodecParameters(MediaType.AudioMain, {
6611
+ maxaveragebitrate: '64000',
6612
+ maxplaybackrate: '48000',
6613
+ });
6614
+ } else {
6615
+ await this.mediaProperties.webrtcMediaConnection.deleteCodecParameters(MediaType.AudioMain, [
6616
+ 'maxaveragebitrate',
6617
+ 'maxplaybackrate',
6618
+ ]);
6619
+ }
6620
+ }
6621
+
6622
+ /** Updates the tracks being sent on the transcoded media connection
6623
+ *
6624
+ * @returns {Promise<void>}
6625
+ */
6626
+ private updateTranscodedMediaConnection(): Promise<void> {
6627
+ const LOG_HEADER = 'Meeting:index#updateTranscodedMediaConnection -->';
6628
+
6629
+ LoggerProxy.logger.info(`${LOG_HEADER} starting`);
6630
+
6631
+ if (!this.canUpdateMedia()) {
6632
+ return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.TRANSCODED_MEDIA_CONNECTION, {});
6633
+ }
6634
+
6635
+ return this.mediaProperties.webrtcMediaConnection
6636
+ .update({
6637
+ localTracks: {
6638
+ audio: this.mediaProperties.audioTrack?.underlyingTrack || null,
6639
+ video: this.mediaProperties.videoTrack?.underlyingTrack || null,
6640
+ screenShareVideo: this.mediaProperties.shareTrack?.underlyingTrack || null,
6641
+ },
6642
+ direction: {
6643
+ audio: Media.getDirection(
6644
+ true,
6645
+ this.mediaProperties.mediaDirection.receiveAudio,
6646
+ this.mediaProperties.mediaDirection.sendAudio
6647
+ ),
6648
+ video: Media.getDirection(
6649
+ true,
6650
+ this.mediaProperties.mediaDirection.receiveVideo,
6651
+ this.mediaProperties.mediaDirection.sendVideo
6652
+ ),
6653
+ screenShareVideo: Media.getDirection(
6654
+ false,
6655
+ this.mediaProperties.mediaDirection.receiveShare,
6656
+ this.mediaProperties.mediaDirection.sendShare
6657
+ ),
6658
+ },
6659
+ remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
6660
+ })
6661
+ .then(() => {
6662
+ LoggerProxy.logger.info(`${LOG_HEADER} done`);
6663
+ })
6664
+ .catch((error) => {
6665
+ LoggerProxy.logger.error(`${LOG_HEADER} Error: `, error);
6666
+
6667
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.UPDATE_MEDIA_FAILURE, {
6668
+ correlation_id: this.correlationId,
6669
+ locus_id: this.locusUrl.split('/').pop(),
6670
+ reason: error.message,
6671
+ stack: error.stack,
6672
+ });
6673
+
6674
+ throw error;
6675
+ });
6676
+ }
6677
+
6678
+ /**
6679
+ * Publishes a track.
6680
+ *
6681
+ * @param {LocalTrack} track to publish
6682
+ * @returns {Promise}
6683
+ */
6684
+ private async publishTrack(track?: LocalTrack) {
6685
+ if (!track) {
6686
+ return;
6687
+ }
6688
+
6689
+ if (this.mediaProperties.webrtcMediaConnection) {
6690
+ if (this.isMultistream) {
6691
+ await this.mediaProperties.webrtcMediaConnection.publishTrack(track);
6692
+ } else {
6693
+ track.setPublished(true); // for multistream, this call is done by WCME
6694
+ }
6695
+ }
6696
+ }
6697
+
6698
+ /**
6699
+ * Un-publishes a track.
6700
+ *
6701
+ * @param {LocalTrack} track to unpublish
6702
+ * @returns {Promise}
6703
+ */
6704
+ private async unpublishTrack(track?: LocalTrack) {
6705
+ if (!track) {
6706
+ return;
6707
+ }
6708
+
6709
+ if (this.isMultistream && this.mediaProperties.webrtcMediaConnection) {
6710
+ await this.mediaProperties.webrtcMediaConnection.unpublishTrack(track);
6711
+ } else {
6712
+ track.setPublished(false); // for multistream, this call is done by WCME
6713
+ }
6714
+ }
6715
+
6716
+ /**
6717
+ * Publishes specified local tracks in the meeting
6718
+ *
6719
+ * @param {Object} tracks
6720
+ * @returns {Promise}
6721
+ */
6722
+ async publishTracks(tracks: LocalTracks): Promise<void> {
6723
+ this.checkMediaConnection();
6724
+
6725
+ this.annotationInfo = tracks.annotationInfo;
6726
+
6727
+ if (
6728
+ !tracks.microphone &&
6729
+ !tracks.camera &&
6730
+ !tracks.screenShare?.audio &&
6731
+ !tracks.screenShare?.video
6732
+ ) {
6733
+ // nothing to do
6734
+ return;
6735
+ }
6736
+
6737
+ let floorRequestNeeded = false;
6738
+
6739
+ if (tracks.screenShare?.video) {
6740
+ await this.setLocalShareTrack(tracks.screenShare?.video);
6741
+
6742
+ floorRequestNeeded = true;
6743
+ }
6744
+
6745
+ if (tracks.microphone) {
6746
+ await this.setLocalAudioTrack(tracks.microphone);
6747
+ }
6748
+
6749
+ if (tracks.camera) {
6750
+ await this.setLocalVideoTrack(tracks.camera);
6751
+ }
6752
+
6753
+ if (!this.isMultistream) {
6754
+ await this.updateTranscodedMediaConnection();
6755
+ }
6756
+
6757
+ if (floorRequestNeeded) {
6758
+ // we're sending the http request to Locus to request the screen share floor
6759
+ // only after the SDP update, because that's how it's always been done for transcoded meetings
6760
+ // and also if sharing from the start, we need confluence to have been created
6761
+ await this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.LAMBDA, {
6762
+ lambda: async () => {
6763
+ return this.requestScreenShareFloor();
6764
+ },
6765
+ });
6766
+ }
6767
+ }
6768
+
6769
+ /**
6770
+ * Un-publishes specified local tracks in the meeting
6771
+ *
6772
+ * @param {Array<MediaStreamTrack>} tracks
6773
+ * @returns {Promise}
6774
+ */
6775
+ async unpublishTracks(tracks: LocalTrack[]): Promise<void> {
6776
+ this.checkMediaConnection();
6777
+
6778
+ const promises = [];
6779
+
6780
+ for (const track of tracks.filter((t) => !!t)) {
6781
+ if (track === this.mediaProperties.shareTrack) {
6782
+ try {
6783
+ this.releaseScreenShareFloor(); // we ignore the returned promise here on purpose
6784
+ } catch (e) {
6785
+ // nothing to do here, error is logged already inside releaseScreenShareFloor()
6786
+ }
6787
+ promises.push(this.setLocalShareTrack(undefined));
6788
+ }
6789
+
6790
+ if (track === this.mediaProperties.audioTrack) {
6791
+ promises.push(this.setLocalAudioTrack(undefined));
6792
+ }
6793
+
6794
+ if (track === this.mediaProperties.videoTrack) {
6795
+ promises.push(this.setLocalVideoTrack(undefined));
6796
+ }
6797
+ }
6798
+
6799
+ if (!this.isMultistream) {
6800
+ promises.push(this.updateTranscodedMediaConnection());
6801
+ }
6802
+
6803
+ await Promise.all(promises);
6804
+ }
6843
6805
  }