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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. package/README.md +45 -1
  2. package/dist/annotation/annotation.types.js +7 -0
  3. package/dist/annotation/annotation.types.js.map +1 -0
  4. package/dist/annotation/constants.js +49 -0
  5. package/dist/annotation/constants.js.map +1 -0
  6. package/dist/annotation/index.js +359 -0
  7. package/dist/annotation/index.js.map +1 -0
  8. package/dist/breakouts/breakout.js +212 -0
  9. package/dist/breakouts/breakout.js.map +1 -0
  10. package/dist/breakouts/collection.js +23 -0
  11. package/dist/breakouts/collection.js.map +1 -0
  12. package/dist/breakouts/edit-lock-error.js +52 -0
  13. package/dist/breakouts/edit-lock-error.js.map +1 -0
  14. package/dist/breakouts/events.js +43 -0
  15. package/dist/breakouts/events.js.map +1 -0
  16. package/dist/breakouts/index.js +1046 -0
  17. package/dist/breakouts/index.js.map +1 -0
  18. package/dist/breakouts/request.js +78 -0
  19. package/dist/breakouts/request.js.map +1 -0
  20. package/dist/breakouts/utils.js +67 -0
  21. package/dist/breakouts/utils.js.map +1 -0
  22. package/dist/common/errors/webex-errors.js +3 -2
  23. package/dist/common/errors/webex-errors.js.map +1 -1
  24. package/dist/common/logs/logger-proxy.js +1 -1
  25. package/dist/common/logs/logger-proxy.js.map +1 -1
  26. package/dist/config.js +6 -8
  27. package/dist/config.js.map +1 -1
  28. package/dist/constants.js +165 -26
  29. package/dist/constants.js.map +1 -1
  30. package/dist/controls-options-manager/constants.js +14 -0
  31. package/dist/controls-options-manager/constants.js.map +1 -0
  32. package/dist/controls-options-manager/enums.js +27 -0
  33. package/dist/controls-options-manager/enums.js.map +1 -0
  34. package/dist/controls-options-manager/index.js +297 -0
  35. package/dist/controls-options-manager/index.js.map +1 -0
  36. package/dist/controls-options-manager/types.js +7 -0
  37. package/dist/controls-options-manager/types.js.map +1 -0
  38. package/dist/controls-options-manager/util.js +300 -0
  39. package/dist/controls-options-manager/util.js.map +1 -0
  40. package/dist/index.js +77 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/locus-info/controlsUtils.js +91 -2
  43. package/dist/locus-info/controlsUtils.js.map +1 -1
  44. package/dist/locus-info/index.js +298 -24
  45. package/dist/locus-info/index.js.map +1 -1
  46. package/dist/locus-info/mediaSharesUtils.js +43 -1
  47. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  48. package/dist/locus-info/parser.js +2 -1
  49. package/dist/locus-info/parser.js.map +1 -1
  50. package/dist/locus-info/selfUtils.js +88 -14
  51. package/dist/locus-info/selfUtils.js.map +1 -1
  52. package/dist/media/index.js +39 -134
  53. package/dist/media/index.js.map +1 -1
  54. package/dist/media/properties.js +19 -97
  55. package/dist/media/properties.js.map +1 -1
  56. package/dist/mediaQualityMetrics/config.js +505 -493
  57. package/dist/mediaQualityMetrics/config.js.map +1 -1
  58. package/dist/meeting/in-meeting-actions.js +79 -1
  59. package/dist/meeting/in-meeting-actions.js.map +1 -1
  60. package/dist/meeting/index.js +2275 -2152
  61. package/dist/meeting/index.js.map +1 -1
  62. package/dist/meeting/locusMediaRequest.js +291 -0
  63. package/dist/meeting/locusMediaRequest.js.map +1 -0
  64. package/dist/meeting/muteState.js +229 -124
  65. package/dist/meeting/muteState.js.map +1 -1
  66. package/dist/meeting/request.js +191 -167
  67. package/dist/meeting/request.js.map +1 -1
  68. package/dist/meeting/request.type.js.map +1 -1
  69. package/dist/meeting/util.js +443 -443
  70. package/dist/meeting/util.js.map +1 -1
  71. package/dist/meeting-info/meeting-info-v2.js +157 -49
  72. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  73. package/dist/meeting-info/utilv2.js +20 -5
  74. package/dist/meeting-info/utilv2.js.map +1 -1
  75. package/dist/meetings/collection.js +22 -0
  76. package/dist/meetings/collection.js.map +1 -1
  77. package/dist/meetings/index.js +365 -73
  78. package/dist/meetings/index.js.map +1 -1
  79. package/dist/meetings/meetings.types.js +7 -0
  80. package/dist/meetings/meetings.types.js.map +1 -0
  81. package/dist/meetings/request.js +16 -12
  82. package/dist/meetings/request.js.map +1 -1
  83. package/dist/meetings/util.js +88 -1
  84. package/dist/meetings/util.js.map +1 -1
  85. package/dist/member/index.js +41 -0
  86. package/dist/member/index.js.map +1 -1
  87. package/dist/member/types.js +15 -0
  88. package/dist/member/types.js.map +1 -0
  89. package/dist/member/util.js +86 -3
  90. package/dist/member/util.js.map +1 -1
  91. package/dist/members/collection.js +10 -0
  92. package/dist/members/collection.js.map +1 -1
  93. package/dist/members/index.js +94 -11
  94. package/dist/members/index.js.map +1 -1
  95. package/dist/members/request.js +109 -39
  96. package/dist/members/request.js.map +1 -1
  97. package/dist/members/types.js +15 -0
  98. package/dist/members/types.js.map +1 -0
  99. package/dist/members/util.js +316 -233
  100. package/dist/members/util.js.map +1 -1
  101. package/dist/metrics/config.js +50 -14
  102. package/dist/metrics/config.js.map +1 -1
  103. package/dist/metrics/constants.js +3 -5
  104. package/dist/metrics/constants.js.map +1 -1
  105. package/dist/metrics/index.js +48 -29
  106. package/dist/metrics/index.js.map +1 -1
  107. package/dist/multistream/mediaRequestManager.js +265 -36
  108. package/dist/multistream/mediaRequestManager.js.map +1 -1
  109. package/dist/multistream/receiveSlot.js +52 -19
  110. package/dist/multistream/receiveSlot.js.map +1 -1
  111. package/dist/multistream/receiveSlotManager.js +53 -33
  112. package/dist/multistream/receiveSlotManager.js.map +1 -1
  113. package/dist/multistream/remoteMedia.js +44 -18
  114. package/dist/multistream/remoteMedia.js.map +1 -1
  115. package/dist/multistream/remoteMediaGroup.js +60 -3
  116. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  117. package/dist/multistream/remoteMediaManager.js +322 -103
  118. package/dist/multistream/remoteMediaManager.js.map +1 -1
  119. package/dist/networkQualityMonitor/index.js +4 -2
  120. package/dist/networkQualityMonitor/index.js.map +1 -1
  121. package/dist/reachability/index.js +117 -60
  122. package/dist/reachability/index.js.map +1 -1
  123. package/dist/reachability/request.js +12 -5
  124. package/dist/reachability/request.js.map +1 -1
  125. package/dist/reactions/constants.js +13 -0
  126. package/dist/reactions/constants.js.map +1 -0
  127. package/dist/reactions/reactions.js +2 -2
  128. package/dist/reactions/reactions.js.map +1 -1
  129. package/dist/reactions/reactions.type.js +18 -18
  130. package/dist/reactions/reactions.type.js.map +1 -1
  131. package/dist/reconnection-manager/index.js +190 -145
  132. package/dist/reconnection-manager/index.js.map +1 -1
  133. package/dist/recording-controller/enums.js +17 -0
  134. package/dist/recording-controller/enums.js.map +1 -0
  135. package/dist/recording-controller/index.js +343 -0
  136. package/dist/recording-controller/index.js.map +1 -0
  137. package/dist/recording-controller/util.js +63 -0
  138. package/dist/recording-controller/util.js.map +1 -0
  139. package/dist/roap/index.js +21 -29
  140. package/dist/roap/index.js.map +1 -1
  141. package/dist/roap/request.js +127 -92
  142. package/dist/roap/request.js.map +1 -1
  143. package/dist/roap/turnDiscovery.js +135 -53
  144. package/dist/roap/turnDiscovery.js.map +1 -1
  145. package/dist/statsAnalyzer/global.js +1 -93
  146. package/dist/statsAnalyzer/global.js.map +1 -1
  147. package/dist/statsAnalyzer/index.js +329 -314
  148. package/dist/statsAnalyzer/index.js.map +1 -1
  149. package/dist/statsAnalyzer/mqaUtil.js +103 -54
  150. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  151. package/dist/types/annotation/annotation.types.d.ts +43 -0
  152. package/dist/types/annotation/constants.d.ts +31 -0
  153. package/dist/types/annotation/index.d.ts +124 -0
  154. package/dist/types/breakouts/breakout.d.ts +8 -0
  155. package/dist/types/breakouts/collection.d.ts +5 -0
  156. package/dist/types/breakouts/edit-lock-error.d.ts +15 -0
  157. package/dist/types/breakouts/events.d.ts +2 -0
  158. package/dist/types/breakouts/index.d.ts +5 -0
  159. package/dist/types/breakouts/request.d.ts +22 -0
  160. package/dist/types/breakouts/utils.d.ts +15 -0
  161. package/dist/types/common/browser-detection.d.ts +9 -0
  162. package/dist/types/common/collection.d.ts +48 -0
  163. package/dist/types/common/config.d.ts +2 -0
  164. package/dist/types/common/errors/captcha-error.d.ts +15 -0
  165. package/dist/types/common/errors/intent-to-join.d.ts +16 -0
  166. package/dist/types/common/errors/join-meeting.d.ts +17 -0
  167. package/dist/types/common/errors/media.d.ts +15 -0
  168. package/dist/types/common/errors/parameter.d.ts +15 -0
  169. package/dist/types/common/errors/password-error.d.ts +15 -0
  170. package/dist/types/common/errors/permission.d.ts +14 -0
  171. package/dist/types/common/errors/reconnection-in-progress.d.ts +9 -0
  172. package/dist/types/common/errors/reconnection.d.ts +15 -0
  173. package/dist/types/common/errors/stats.d.ts +15 -0
  174. package/dist/types/common/errors/webex-errors.d.ts +69 -0
  175. package/dist/types/common/errors/webex-meetings-error.d.ts +20 -0
  176. package/dist/types/common/events/events-scope.d.ts +17 -0
  177. package/dist/types/common/events/events.d.ts +12 -0
  178. package/dist/types/common/events/trigger-proxy.d.ts +2 -0
  179. package/dist/types/common/events/util.d.ts +2 -0
  180. package/dist/types/common/logs/logger-config.d.ts +2 -0
  181. package/dist/types/common/logs/logger-proxy.d.ts +2 -0
  182. package/dist/types/common/logs/request.d.ts +34 -0
  183. package/dist/types/common/queue.d.ts +32 -0
  184. package/dist/types/config.d.ts +72 -0
  185. package/dist/types/constants.d.ts +978 -0
  186. package/dist/types/controls-options-manager/constants.d.ts +4 -0
  187. package/dist/types/controls-options-manager/enums.d.ts +15 -0
  188. package/dist/types/controls-options-manager/index.d.ts +136 -0
  189. package/dist/types/controls-options-manager/types.d.ts +43 -0
  190. package/dist/types/controls-options-manager/util.d.ts +1 -0
  191. package/dist/types/index.d.ts +7 -0
  192. package/dist/types/locus-info/controlsUtils.d.ts +2 -0
  193. package/dist/types/locus-info/embeddedAppsUtils.d.ts +2 -0
  194. package/dist/types/locus-info/fullState.d.ts +2 -0
  195. package/dist/types/locus-info/hostUtils.d.ts +2 -0
  196. package/dist/types/locus-info/index.d.ts +315 -0
  197. package/dist/types/locus-info/infoUtils.d.ts +2 -0
  198. package/dist/types/locus-info/mediaSharesUtils.d.ts +2 -0
  199. package/dist/types/locus-info/parser.d.ts +212 -0
  200. package/dist/types/locus-info/selfUtils.d.ts +2 -0
  201. package/dist/types/media/index.d.ts +34 -0
  202. package/dist/types/media/properties.d.ts +86 -0
  203. package/dist/types/media/util.d.ts +2 -0
  204. package/dist/types/mediaQualityMetrics/config.d.ts +365 -0
  205. package/dist/types/meeting/in-meeting-actions.d.ts +149 -0
  206. package/dist/types/meeting/index.d.ts +1516 -0
  207. package/dist/types/meeting/locusMediaRequest.d.ts +70 -0
  208. package/dist/types/meeting/muteState.d.ts +184 -0
  209. package/dist/types/meeting/request.d.ts +270 -0
  210. package/dist/types/meeting/request.type.d.ts +11 -0
  211. package/dist/types/meeting/state.d.ts +9 -0
  212. package/dist/types/meeting/util.d.ts +75 -0
  213. package/dist/types/meeting-info/collection.d.ts +20 -0
  214. package/dist/types/meeting-info/index.d.ts +57 -0
  215. package/dist/types/meeting-info/meeting-info-v2.d.ts +122 -0
  216. package/dist/types/meeting-info/request.d.ts +22 -0
  217. package/dist/types/meeting-info/util.d.ts +2 -0
  218. package/dist/types/meeting-info/utilv2.d.ts +2 -0
  219. package/dist/types/meetings/collection.d.ts +31 -0
  220. package/dist/types/meetings/index.d.ts +364 -0
  221. package/dist/types/meetings/meetings.types.d.ts +4 -0
  222. package/dist/types/meetings/request.d.ts +27 -0
  223. package/dist/types/meetings/util.d.ts +18 -0
  224. package/dist/types/member/index.d.ts +157 -0
  225. package/dist/types/member/types.d.ts +21 -0
  226. package/dist/types/member/util.d.ts +2 -0
  227. package/dist/types/members/collection.d.ts +29 -0
  228. package/dist/types/members/index.d.ts +353 -0
  229. package/dist/types/members/request.d.ts +114 -0
  230. package/dist/types/members/types.d.ts +24 -0
  231. package/dist/types/members/util.d.ts +210 -0
  232. package/dist/types/metrics/config.d.ts +195 -0
  233. package/dist/types/metrics/constants.d.ts +55 -0
  234. package/dist/types/metrics/index.d.ts +169 -0
  235. package/dist/types/multistream/mediaRequestManager.d.ts +118 -0
  236. package/dist/types/multistream/receiveSlot.d.ts +68 -0
  237. package/dist/types/multistream/receiveSlotManager.d.ts +56 -0
  238. package/dist/types/multistream/remoteMedia.d.ts +72 -0
  239. package/dist/types/multistream/remoteMediaGroup.d.ts +47 -0
  240. package/dist/types/multistream/remoteMediaManager.d.ts +277 -0
  241. package/dist/types/networkQualityMonitor/index.d.ts +70 -0
  242. package/dist/types/personal-meeting-room/index.d.ts +47 -0
  243. package/dist/types/personal-meeting-room/request.d.ts +14 -0
  244. package/dist/types/personal-meeting-room/util.d.ts +2 -0
  245. package/dist/types/reachability/index.d.ts +152 -0
  246. package/dist/types/reachability/request.d.ts +37 -0
  247. package/dist/types/reactions/constants.d.ts +3 -0
  248. package/dist/types/reactions/reactions.d.ts +4 -0
  249. package/dist/types/reactions/reactions.type.d.ts +52 -0
  250. package/dist/types/reconnection-manager/index.d.ts +126 -0
  251. package/dist/types/recording-controller/enums.d.ts +7 -0
  252. package/dist/types/recording-controller/index.d.ts +193 -0
  253. package/dist/types/recording-controller/util.d.ts +13 -0
  254. package/dist/types/roap/index.d.ts +77 -0
  255. package/dist/types/roap/request.d.ts +36 -0
  256. package/dist/types/roap/turnDiscovery.d.ts +91 -0
  257. package/dist/types/statsAnalyzer/global.d.ts +36 -0
  258. package/dist/types/statsAnalyzer/index.d.ts +200 -0
  259. package/dist/types/statsAnalyzer/mqaUtil.d.ts +24 -0
  260. package/dist/types/transcription/index.d.ts +64 -0
  261. package/package.json +28 -21
  262. package/src/annotation/annotation.types.ts +52 -0
  263. package/src/annotation/constants.ts +36 -0
  264. package/src/annotation/index.ts +343 -0
  265. package/src/breakouts/README.md +220 -0
  266. package/src/breakouts/breakout.ts +180 -0
  267. package/src/breakouts/collection.ts +19 -0
  268. package/src/breakouts/edit-lock-error.ts +25 -0
  269. package/src/breakouts/events.ts +37 -0
  270. package/src/breakouts/index.ts +921 -0
  271. package/src/breakouts/request.ts +55 -0
  272. package/src/breakouts/utils.ts +57 -0
  273. package/src/common/errors/webex-errors.ts +6 -2
  274. package/src/common/logs/logger-proxy.ts +1 -1
  275. package/src/config.ts +5 -7
  276. package/src/constants.ts +155 -20
  277. package/src/controls-options-manager/constants.ts +5 -0
  278. package/src/controls-options-manager/enums.ts +18 -0
  279. package/src/controls-options-manager/index.ts +278 -0
  280. package/src/controls-options-manager/types.ts +59 -0
  281. package/src/controls-options-manager/util.ts +286 -0
  282. package/src/index.ts +34 -0
  283. package/src/locus-info/controlsUtils.ts +108 -0
  284. package/src/locus-info/index.ts +310 -21
  285. package/src/locus-info/mediaSharesUtils.ts +48 -0
  286. package/src/locus-info/parser.ts +2 -1
  287. package/src/locus-info/selfUtils.ts +80 -2
  288. package/src/media/index.ts +70 -142
  289. package/src/media/properties.ts +41 -104
  290. package/src/mediaQualityMetrics/config.ts +379 -377
  291. package/src/meeting/in-meeting-actions.ts +156 -0
  292. package/src/meeting/index.ts +1744 -1767
  293. package/src/meeting/locusMediaRequest.ts +309 -0
  294. package/src/meeting/muteState.ts +228 -132
  295. package/src/meeting/request.ts +100 -91
  296. package/src/meeting/request.type.ts +2 -0
  297. package/src/meeting/util.ts +421 -421
  298. package/src/meeting-info/meeting-info-v2.ts +134 -13
  299. package/src/meeting-info/utilv2.ts +13 -3
  300. package/src/meetings/collection.ts +20 -0
  301. package/src/meetings/index.ts +385 -83
  302. package/src/meetings/meetings.types.ts +12 -0
  303. package/src/meetings/request.ts +3 -1
  304. package/src/meetings/util.ts +103 -4
  305. package/src/member/index.ts +40 -0
  306. package/src/member/types.ts +24 -0
  307. package/src/member/util.ts +81 -1
  308. package/src/members/collection.ts +8 -0
  309. package/src/members/index.ts +108 -6
  310. package/src/members/request.ts +98 -17
  311. package/src/members/types.ts +28 -0
  312. package/src/members/util.ts +319 -240
  313. package/src/metrics/config.ts +49 -10
  314. package/src/metrics/constants.ts +2 -4
  315. package/src/metrics/index.ts +43 -27
  316. package/src/multistream/mediaRequestManager.ts +337 -63
  317. package/src/multistream/receiveSlot.ts +68 -26
  318. package/src/multistream/receiveSlotManager.ts +61 -38
  319. package/src/multistream/remoteMedia.ts +29 -3
  320. package/src/multistream/remoteMediaGroup.ts +61 -2
  321. package/src/multistream/remoteMediaManager.ts +260 -66
  322. package/src/networkQualityMonitor/index.ts +6 -6
  323. package/src/reachability/index.ts +75 -25
  324. package/src/reachability/request.ts +10 -5
  325. package/src/reactions/constants.ts +4 -0
  326. package/src/reactions/reactions.ts +4 -4
  327. package/src/reactions/reactions.type.ts +28 -3
  328. package/src/reconnection-manager/index.ts +53 -32
  329. package/src/recording-controller/enums.ts +8 -0
  330. package/src/recording-controller/index.ts +315 -0
  331. package/src/recording-controller/util.ts +58 -0
  332. package/src/roap/index.ts +21 -30
  333. package/src/roap/request.ts +51 -52
  334. package/src/roap/turnDiscovery.ts +51 -27
  335. package/src/statsAnalyzer/global.ts +1 -94
  336. package/src/statsAnalyzer/index.ts +380 -390
  337. package/src/statsAnalyzer/mqaUtil.ts +106 -99
  338. package/test/integration/spec/converged-space-meetings.js +233 -0
  339. package/test/integration/spec/journey.js +331 -254
  340. package/test/integration/spec/space-meeting.js +77 -4
  341. package/test/unit/spec/annotation/index.ts +436 -0
  342. package/test/unit/spec/breakouts/breakout.ts +233 -0
  343. package/test/unit/spec/breakouts/collection.ts +15 -0
  344. package/test/unit/spec/breakouts/edit-lock-error.ts +30 -0
  345. package/test/unit/spec/breakouts/events.ts +77 -0
  346. package/test/unit/spec/breakouts/index.ts +1790 -0
  347. package/test/unit/spec/breakouts/request.ts +104 -0
  348. package/test/unit/spec/breakouts/utils.js +72 -0
  349. package/test/unit/spec/controls-options-manager/index.js +287 -0
  350. package/test/unit/spec/controls-options-manager/util.js +518 -0
  351. package/test/unit/spec/fixture/locus.js +1 -0
  352. package/test/unit/spec/locus-info/controlsUtils.js +303 -30
  353. package/test/unit/spec/locus-info/index.js +616 -4
  354. package/test/unit/spec/locus-info/mediaSharesUtils.ts +22 -0
  355. package/test/unit/spec/locus-info/selfConstant.js +38 -0
  356. package/test/unit/spec/locus-info/selfUtils.js +249 -0
  357. package/test/unit/spec/media/index.ts +118 -22
  358. package/test/unit/spec/media/properties.ts +9 -9
  359. package/test/unit/spec/meeting/in-meeting-actions.ts +76 -0
  360. package/test/unit/spec/meeting/index.js +2496 -1375
  361. package/test/unit/spec/meeting/locusMediaRequest.ts +436 -0
  362. package/test/unit/spec/meeting/muteState.js +370 -208
  363. package/test/unit/spec/meeting/request.js +354 -42
  364. package/test/unit/spec/meeting/utils.js +268 -156
  365. package/test/unit/spec/meeting-info/meetinginfov2.js +383 -5
  366. package/test/unit/spec/meeting-info/utilv2.js +21 -0
  367. package/test/unit/spec/meetings/collection.js +14 -0
  368. package/test/unit/spec/meetings/index.js +866 -120
  369. package/test/unit/spec/meetings/utils.js +206 -2
  370. package/test/unit/spec/member/index.js +24 -0
  371. package/test/unit/spec/member/util.js +384 -32
  372. package/test/unit/spec/members/index.js +320 -1
  373. package/test/unit/spec/members/request.js +206 -27
  374. package/test/unit/spec/members/utils.js +184 -0
  375. package/test/unit/spec/metrics/index.js +98 -0
  376. package/test/unit/spec/multistream/mediaRequestManager.ts +1012 -109
  377. package/test/unit/spec/multistream/receiveSlot.ts +77 -18
  378. package/test/unit/spec/multistream/receiveSlotManager.ts +69 -39
  379. package/test/unit/spec/multistream/remoteMedia.ts +32 -2
  380. package/test/unit/spec/multistream/remoteMediaGroup.ts +271 -5
  381. package/test/unit/spec/multistream/remoteMediaManager.ts +672 -65
  382. package/test/unit/spec/networkQualityMonitor/index.js +4 -4
  383. package/test/unit/spec/reachability/index.ts +176 -25
  384. package/test/unit/spec/reachability/request.js +66 -0
  385. package/test/unit/spec/reconnection-manager/index.js +46 -13
  386. package/test/unit/spec/recording-controller/index.js +231 -0
  387. package/test/unit/spec/recording-controller/util.js +102 -0
  388. package/test/unit/spec/roap/index.ts +21 -51
  389. package/test/unit/spec/roap/request.ts +187 -0
  390. package/test/unit/spec/roap/turnDiscovery.ts +73 -34
  391. package/test/unit/spec/stats-analyzer/index.js +94 -43
  392. package/test/utils/constants.js +9 -0
  393. package/test/utils/integrationTestUtils.js +46 -0
  394. package/test/utils/testUtils.js +0 -45
  395. package/test/utils/webex-config.js +4 -0
  396. package/test/utils/webex-test-users.js +7 -3
  397. package/tsconfig.json +6 -0
  398. package/dist/media/internal-media-core-wrapper.js +0 -18
  399. package/dist/media/internal-media-core-wrapper.js.map +0 -1
  400. package/dist/meeting/effectsState.js +0 -262
  401. package/dist/meeting/effectsState.js.map +0 -1
  402. package/dist/multistream/multistreamMedia.js +0 -106
  403. package/dist/multistream/multistreamMedia.js.map +0 -1
  404. package/src/index.js +0 -15
  405. package/src/media/internal-media-core-wrapper.ts +0 -9
  406. package/src/meeting/effectsState.ts +0 -211
  407. package/src/multistream/multistreamMedia.ts +0 -93
  408. package/test/unit/spec/meeting/effectsState.js +0 -281
@@ -1,12 +1,29 @@
1
1
  import uuid from 'uuid';
2
- import {cloneDeep, isEqual, pick, isString, defer} from 'lodash';
2
+ import {cloneDeep, isEqual, pick, defer, isEmpty} from 'lodash';
3
3
  // @ts-ignore - Fix this
4
4
  import {StatelessWebexPlugin} from '@webex/webex-core';
5
- import {Media as WebRTCMedia, MediaConnection as MC} from '@webex/internal-media-core';
5
+ import {
6
+ ConnectionState,
7
+ Errors,
8
+ ErrorType,
9
+ Event,
10
+ MediaContent,
11
+ MediaType,
12
+ RemoteTrackType,
13
+ } from '@webex/internal-media-core';
14
+
15
+ import {
16
+ getDevices,
17
+ LocalTrack,
18
+ LocalCameraTrack,
19
+ LocalDisplayTrack,
20
+ LocalMicrophoneTrack,
21
+ LocalTrackEvents,
22
+ TrackMuteEvent,
23
+ } from '@webex/media-helpers';
6
24
 
7
25
  import {
8
26
  MeetingNotActiveError,
9
- createMeetingsError,
10
27
  UserInLobbyError,
11
28
  NoMediaEstablishedYetError,
12
29
  UserNotJoinedError,
@@ -16,20 +33,22 @@ import NetworkQualityMonitor from '../networkQualityMonitor';
16
33
  import LoggerProxy from '../common/logs/logger-proxy';
17
34
  import Trigger from '../common/events/trigger-proxy';
18
35
  import Roap from '../roap/index';
19
- import Media from '../media';
36
+ import Media, {type BundlePolicy} from '../media';
20
37
  import MediaProperties from '../media/properties';
21
38
  import MeetingStateMachine from './state';
22
- import createMuteState from './muteState';
23
- import createEffectsState from './effectsState';
39
+ import {createMuteState} from './muteState';
24
40
  import LocusInfo from '../locus-info';
25
41
  import Metrics from '../metrics';
26
- import {trigger, mediaType, error as MetricsError, eventType} from '../metrics/config';
42
+ import {trigger, error as MetricsError, eventType} from '../metrics/config';
27
43
  import ReconnectionManager from '../reconnection-manager';
28
44
  import MeetingRequest from './request';
29
45
  import Members from '../members/index';
30
46
  import MeetingUtil from './util';
47
+ import RecordingUtil from '../recording-controller/util';
48
+ import ControlsOptionsUtil from '../controls-options-manager/util';
31
49
  import MediaUtil from '../media/util';
32
50
  import Transcription from '../transcription';
51
+ import {Reactions, SkinTones} from '../reactions/reactions';
33
52
  import PasswordError from '../common/errors/password-error';
34
53
  import CaptchaError from '../common/errors/captcha-error';
35
54
  import ReconnectionError from '../common/errors/reconnection';
@@ -40,10 +59,12 @@ import {
40
59
  _JOIN_,
41
60
  AUDIO,
42
61
  CONTENT,
62
+ DISPLAY_HINTS,
43
63
  ENDED,
44
64
  EVENT_TRIGGERS,
45
65
  EVENT_TYPES,
46
66
  EVENTS,
67
+ BREAKOUTS,
47
68
  FLOOR_ACTION,
48
69
  FULL_STATE,
49
70
  LAYOUT_TYPES,
@@ -64,53 +85,83 @@ import {
64
85
  RECORDING_STATE,
65
86
  SHARE_STATUS,
66
87
  SHARE_STOPPED_REASON,
67
- VIDEO_RESOLUTIONS,
68
88
  VIDEO,
69
- BNR_STATUS,
70
89
  HTTP_VERBS,
90
+ SELF_ROLES,
71
91
  } from '../constants';
72
92
  import BEHAVIORAL_METRICS from '../metrics/constants';
73
93
  import ParameterError from '../common/errors/parameter';
74
- import MediaError from '../common/errors/media';
75
94
  import {
76
95
  MeetingInfoV2PasswordError,
77
96
  MeetingInfoV2CaptchaError,
97
+ MeetingInfoV2PolicyError,
78
98
  } from '../meeting-info/meeting-info-v2';
79
99
  import BrowserDetection from '../common/browser-detection';
80
- import {ReceiveSlotManager} from '../multistream/receiveSlotManager';
100
+ import {CSI, ReceiveSlotManager} from '../multistream/receiveSlotManager';
81
101
  import {MediaRequestManager} from '../multistream/mediaRequestManager';
82
102
  import {
103
+ Configuration as RemoteMediaManagerConfiguration,
83
104
  RemoteMediaManager,
84
105
  Event as RemoteMediaManagerEvent,
85
106
  } 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';
107
+ import {
108
+ Reaction,
109
+ ReactionServerType,
110
+ SkinToneType,
111
+ ProcessedReaction,
112
+ RelayEvent,
113
+ } from '../reactions/reactions.type';
114
+ import Breakouts from '../breakouts';
115
+ import Annotation from '../annotation';
89
116
 
90
117
  import InMeetingActions from './in-meeting-actions';
118
+ import {REACTION_RELAY_TYPES} from '../reactions/constants';
119
+ import RecordingController from '../recording-controller';
120
+ import ControlsOptionsManager from '../controls-options-manager';
121
+ import PermissionError from '../common/errors/permission';
122
+ import {LocusMediaRequest} from './locusMediaRequest';
123
+ import {AnnotationInfo} from '../annotation/annotation.types';
91
124
 
92
125
  const {isBrowser} = BrowserDetection();
93
126
 
94
- const logRequest = (request: any, {header = '', success = '', failure = ''}) => {
95
- LoggerProxy.logger.info(header);
127
+ const logRequest = (request: any, {logText = ''}) => {
128
+ LoggerProxy.logger.info(`${logText} - sending request`);
96
129
 
97
130
  return request
98
131
  .then((arg) => {
99
- LoggerProxy.logger.info(success);
132
+ LoggerProxy.logger.info(`${logText} - has been successfully sent`);
100
133
 
101
134
  return arg;
102
135
  })
103
136
  .catch((error) => {
104
- LoggerProxy.logger.error(failure, error);
137
+ LoggerProxy.logger.error(`${logText} - has failed: `, error);
105
138
  throw error;
106
139
  });
107
140
  };
108
141
 
142
+ export type LocalTracks = {
143
+ microphone?: LocalMicrophoneTrack;
144
+ camera?: LocalCameraTrack;
145
+ screenShare?: {
146
+ audio?: LocalTrack; // todo: for now screen share audio is not supported (will be done in SPARK-399690)
147
+ video?: LocalDisplayTrack;
148
+ };
149
+ annotationInfo?: AnnotationInfo;
150
+ };
151
+
152
+ export type AddMediaOptions = {
153
+ localTracks?: LocalTracks;
154
+ audioEnabled?: boolean; // if not specified, default value true is used
155
+ videoEnabled?: boolean; // if not specified, default value true is used
156
+ receiveShare?: boolean; // if not specified, default value true is used
157
+ remoteMediaManagerConfig?: RemoteMediaManagerConfiguration; // applies only to multistream meetings
158
+ bundlePolicy?: BundlePolicy; // applies only to multistream meetings
159
+ };
160
+
109
161
  export const MEDIA_UPDATE_TYPE = {
110
- ALL: 'ALL',
111
- AUDIO: 'AUDIO',
112
- VIDEO: 'VIDEO',
113
- SHARE: 'SHARE',
162
+ TRANSCODED_MEDIA_CONNECTION: 'TRANSCODED_MEDIA_CONNECTION',
163
+ SHARE_FLOOR_REQUEST: 'SHARE_FLOOR_REQUEST',
164
+ UPDATE_MEDIA: 'UPDATE_MEDIA',
114
165
  };
115
166
 
116
167
  /**
@@ -125,16 +176,6 @@ export const MEDIA_UPDATE_TYPE = {
125
176
  * @property {boolean} isSharing
126
177
  */
127
178
 
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
179
  /**
139
180
  * SharePreferences
140
181
  * @typedef {Object} SharePreferences
@@ -149,18 +190,10 @@ export const MEDIA_UPDATE_TYPE = {
149
190
  * @property {String} [pin]
150
191
  * @property {Boolean} [moderator]
151
192
  * @property {String|Object} [meetingQuality]
152
- * @property {String} [meetingQuality.local]
153
193
  * @property {String} [meetingQuality.remote]
154
194
  * @property {Boolean} [rejoin]
155
195
  * @property {Boolean} [enableMultistream]
156
- */
157
-
158
- /**
159
- * SendOptions
160
- * @typedef {Object} SendOptions
161
- * @property {Boolean} sendAudio
162
- * @property {Boolean} sendVideo
163
- * @property {Boolean} sendShare
196
+ * @property {String} [correlationId]
164
197
  */
165
198
 
166
199
  /**
@@ -403,12 +436,13 @@ export const MEDIA_UPDATE_TYPE = {
403
436
  export default class Meeting extends StatelessWebexPlugin {
404
437
  attrs: any;
405
438
  audio: any;
439
+ breakouts: any;
440
+ annotation: any;
406
441
  conversationUrl: string;
407
442
  correlationId: string;
408
443
  destination: string;
409
444
  destinationType: string;
410
445
  deviceUrl: string;
411
- effects: any;
412
446
  hostId: string;
413
447
  id: string;
414
448
  isMultistream: boolean;
@@ -416,7 +450,7 @@ export default class Meeting extends StatelessWebexPlugin {
416
450
  mediaConnections: any[];
417
451
  mediaId?: string;
418
452
  meetingFiniteStateMachine: any;
419
- meetingInfo: object;
453
+ meetingInfo: any;
420
454
  meetingRequest: MeetingRequest;
421
455
  members: Members;
422
456
  options: object;
@@ -428,6 +462,7 @@ export default class Meeting extends StatelessWebexPlugin {
428
462
  resource: string;
429
463
  roap: Roap;
430
464
  roapSeq: number;
465
+ selfUrl?: string; // comes from Locus, initialized by updateMeetingObject()
431
466
  sipUri: string;
432
467
  type: string;
433
468
  userId: string;
@@ -449,32 +484,37 @@ export default class Meeting extends StatelessWebexPlugin {
449
484
  keepAliveTimerId: NodeJS.Timeout;
450
485
  lastVideoLayoutInfo: any;
451
486
  locusInfo: any;
452
- media: MultistreamMedia;
487
+ locusMediaRequest?: LocusMediaRequest;
453
488
  mediaProperties: MediaProperties;
454
489
  mediaRequestManagers: {
455
490
  audio: MediaRequestManager;
456
491
  video: MediaRequestManager;
492
+ screenShareAudio: MediaRequestManager;
493
+ screenShareVideo: MediaRequestManager;
457
494
  };
458
495
 
459
496
  meetingInfoFailureReason: string;
497
+ meetingInfoFailureCode?: number;
460
498
  networkQualityMonitor: NetworkQualityMonitor;
461
499
  networkStatus: string;
462
500
  passwordStatus: string;
463
501
  queuedMediaUpdates: any[];
464
502
  recording: any;
465
503
  remoteMediaManager: RemoteMediaManager | null;
504
+ recordingController: RecordingController;
505
+ controlsOptionsManager: ControlsOptionsManager;
466
506
  requiredCaptcha: any;
467
507
  receiveSlotManager: ReceiveSlotManager;
468
508
  shareStatus: string;
469
509
  statsAnalyzer: StatsAnalyzer;
470
510
  transcription: Transcription;
471
511
  updateMediaConnections: (mediaConnections: any[]) => void;
472
- endCallInitiateJoinReq: any;
512
+ endCallInitJoinReq: any;
473
513
  endJoinReqResp: any;
474
514
  endLocalSDPGenRemoteSDPRecvDelay: any;
475
515
  joinedWith: any;
476
516
  locusId: any;
477
- startCallInitiateJoinReq: any;
517
+ startCallInitJoinReq: any;
478
518
  startJoinReqResp: any;
479
519
  startLocalSDPGenRemoteSDPRecvDelay: any;
480
520
  wirelessShare: any;
@@ -487,8 +527,13 @@ export default class Meeting extends StatelessWebexPlugin {
487
527
  resourceUrl: string;
488
528
  selfId: string;
489
529
  state: any;
490
-
530
+ localAudioTrackMuteStateHandler: (event: TrackMuteEvent) => void;
531
+ localVideoTrackMuteStateHandler: (event: TrackMuteEvent) => void;
532
+ underlyingLocalTrackChangeHandler: () => void;
533
+ roles: any[];
534
+ environment: string;
491
535
  namespace = MEETINGS;
536
+ annotationInfo: AnnotationInfo;
492
537
 
493
538
  /**
494
539
  * @param {Object} attrs
@@ -523,7 +568,7 @@ export default class Meeting extends StatelessWebexPlugin {
523
568
  */
524
569
  this.id = uuid.v4();
525
570
  /**
526
- * Correlation ID used for network tracking of meeting join
571
+ * Correlation ID used for network tracking of meeting
527
572
  * @instance
528
573
  * @type {String}
529
574
  * @readonly
@@ -573,41 +618,124 @@ export default class Meeting extends StatelessWebexPlugin {
573
618
  */
574
619
  // TODO: needs to be defined as a class
575
620
  this.meetingInfo = {};
621
+ /**
622
+ * @instance
623
+ * @type {Breakouts}
624
+ * @public
625
+ * @memberof Meeting
626
+ */
627
+ // @ts-ignore
628
+ this.breakouts = new Breakouts({meetingId: this.id}, {parent: this.webex});
629
+ /**
630
+ * @instance
631
+ * @type {Annotation}
632
+ * @public
633
+ * @memberof Meeting
634
+ */
635
+ // @ts-ignore
636
+ this.annotation = new Annotation({parent: this.webex});
576
637
  /**
577
638
  * helper class for managing receive slots (for multistream media connections)
578
639
  */
579
- this.receiveSlotManager = new ReceiveSlotManager(this);
640
+ this.receiveSlotManager = new ReceiveSlotManager(
641
+ (mediaType: MediaType) => {
642
+ if (!this.mediaProperties?.webrtcMediaConnection) {
643
+ return Promise.reject(new Error('Webrtc media connection is missing'));
644
+ }
645
+
646
+ return this.mediaProperties.webrtcMediaConnection.createReceiveSlot(mediaType);
647
+ },
648
+ (csi: CSI) => (this.members.findMemberByCsi(csi) as any)?.id
649
+ );
580
650
  /**
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.
651
+ * Object containing helper classes for managing media requests for audio/video/screenshare (for multistream media connections)
652
+ * All multistream media requests sent out for this meeting have to go through them.
583
653
  */
584
654
  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'
655
+ audio: new MediaRequestManager(
656
+ (mediaRequests) => {
657
+ if (!this.mediaProperties.webrtcMediaConnection) {
658
+ LoggerProxy.logger.warn(
659
+ 'Meeting:index#mediaRequestManager --> trying to send audio media request before media connection was created'
660
+ );
661
+
662
+ return;
663
+ }
664
+ this.mediaProperties.webrtcMediaConnection.requestMedia(
665
+ MediaType.AudioMain,
666
+ mediaRequests
589
667
  );
668
+ },
669
+ {
670
+ // @ts-ignore - config coming from registerPlugin
671
+ degradationPreferences: this.config.degradationPreferences,
672
+ kind: 'audio',
673
+ trimRequestsToNumOfSources: false,
674
+ }
675
+ ),
676
+ video: new MediaRequestManager(
677
+ (mediaRequests) => {
678
+ if (!this.mediaProperties.webrtcMediaConnection) {
679
+ LoggerProxy.logger.warn(
680
+ 'Meeting:index#mediaRequestManager --> trying to send video media request before media connection was created'
681
+ );
590
682
 
591
- return;
683
+ return;
684
+ }
685
+ this.mediaProperties.webrtcMediaConnection.requestMedia(
686
+ MediaType.VideoMain,
687
+ mediaRequests
688
+ );
689
+ },
690
+ {
691
+ // @ts-ignore - config coming from registerPlugin
692
+ degradationPreferences: this.config.degradationPreferences,
693
+ kind: 'video',
694
+ trimRequestsToNumOfSources: true,
592
695
  }
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'
696
+ ),
697
+ screenShareAudio: new MediaRequestManager(
698
+ (mediaRequests) => {
699
+ if (!this.mediaProperties.webrtcMediaConnection) {
700
+ LoggerProxy.logger.warn(
701
+ 'Meeting:index#mediaRequestManager --> trying to send screenshare audio media request before media connection was created'
702
+ );
703
+
704
+ return;
705
+ }
706
+ this.mediaProperties.webrtcMediaConnection.requestMedia(
707
+ MediaType.AudioSlides,
708
+ mediaRequests
602
709
  );
710
+ },
711
+ {
712
+ // @ts-ignore - config coming from registerPlugin
713
+ degradationPreferences: this.config.degradationPreferences,
714
+ kind: 'audio',
715
+ trimRequestsToNumOfSources: false,
716
+ }
717
+ ),
718
+ screenShareVideo: new MediaRequestManager(
719
+ (mediaRequests) => {
720
+ if (!this.mediaProperties.webrtcMediaConnection) {
721
+ LoggerProxy.logger.warn(
722
+ 'Meeting:index#mediaRequestManager --> trying to send screenshare video media request before media connection was created'
723
+ );
603
724
 
604
- return;
725
+ return;
726
+ }
727
+ this.mediaProperties.webrtcMediaConnection.requestMedia(
728
+ MediaType.VideoSlides,
729
+ mediaRequests
730
+ );
731
+ },
732
+ {
733
+ // @ts-ignore - config coming from registerPlugin
734
+ degradationPreferences: this.config.degradationPreferences,
735
+ kind: 'video',
736
+ trimRequestsToNumOfSources: false,
605
737
  }
606
- this.mediaProperties.webrtcMediaConnection.requestMedia(
607
- MC.MediaType.VideoMain,
608
- mediaRequests
609
- );
610
- }),
738
+ ),
611
739
  };
612
740
  /**
613
741
  * @instance
@@ -620,8 +748,9 @@ export default class Meeting extends StatelessWebexPlugin {
620
748
  locusUrl: attrs.locus && attrs.locus.url,
621
749
  receiveSlotManager: this.receiveSlotManager,
622
750
  mediaRequestManagers: this.mediaRequestManagers,
623
- // @ts-ignore - Fix type
751
+ meeting: this,
624
752
  },
753
+ // @ts-ignore - Fix type
625
754
  {parent: this.webex}
626
755
  );
627
756
  /**
@@ -653,7 +782,7 @@ export default class Meeting extends StatelessWebexPlugin {
653
782
  */
654
783
  this.reconnectionManager = new ReconnectionManager(this);
655
784
  /**
656
- * created later
785
+ * created with media connection
657
786
  * @instance
658
787
  * @type {MuteState}
659
788
  * @private
@@ -661,21 +790,13 @@ export default class Meeting extends StatelessWebexPlugin {
661
790
  */
662
791
  this.audio = null;
663
792
  /**
664
- * created later
793
+ * created with media connection
665
794
  * @instance
666
795
  * @type {MuteState}
667
796
  * @private
668
797
  * @memberof Meeting
669
798
  */
670
799
  this.video = null;
671
- /**
672
- * created later
673
- * @instance
674
- * @type {EffectsState}
675
- * @private
676
- * @memberof Meeting
677
- */
678
- this.effects = null;
679
800
  /**
680
801
  * @instance
681
802
  * @type {MeetingStateMachine}
@@ -770,7 +891,12 @@ export default class Meeting extends StatelessWebexPlugin {
770
891
  * @private
771
892
  * @memberof Meeting
772
893
  */
773
- this.meetingRequest = new MeetingRequest({}, options);
894
+ this.meetingRequest = new MeetingRequest(
895
+ {
896
+ meeting: this,
897
+ },
898
+ options
899
+ );
774
900
  /**
775
901
  * @instance
776
902
  * @type {Array}
@@ -924,6 +1050,7 @@ export default class Meeting extends StatelessWebexPlugin {
924
1050
  */
925
1051
  // @ts-ignore - Fix type
926
1052
  this.locusInfo = new LocusInfo(this.updateMeetingObject.bind(this), this.webex, this.id);
1053
+
927
1054
  // We had to add listeners first before setting up the locus instance
928
1055
  /**
929
1056
  * @instance
@@ -1014,6 +1141,15 @@ export default class Meeting extends StatelessWebexPlugin {
1014
1141
  */
1015
1142
  this.meetingInfoFailureReason = undefined;
1016
1143
 
1144
+ /**
1145
+ * The numeric code, if any, associated with the last failure to obtain the meeting info
1146
+ * @instance
1147
+ * @type {number}
1148
+ * @private
1149
+ * @memberof Meeting
1150
+ */
1151
+ this.meetingInfoFailureCode = undefined;
1152
+
1017
1153
  /**
1018
1154
  * Repeating timer used to send keepAlives when in lobby
1019
1155
  * @instance
@@ -1023,16 +1159,68 @@ export default class Meeting extends StatelessWebexPlugin {
1023
1159
  */
1024
1160
  this.keepAliveTimerId = null;
1025
1161
 
1162
+ /**
1163
+ * The class that helps to control recording functions: start, stop, pause, resume, etc
1164
+ * @instance
1165
+ * @type {RecordingController}
1166
+ * @public
1167
+ * @memberof Meeting
1168
+ */
1169
+ this.recordingController = new RecordingController(this.meetingRequest, {
1170
+ serviceUrl: this.locusInfo?.links?.services?.record?.url,
1171
+ sessionId: this.locusInfo?.fullState?.sessionId,
1172
+ locusUrl: this.locusInfo?.url,
1173
+ displayHints: [],
1174
+ });
1175
+
1176
+ /**
1177
+ * The class that helps to control recording functions: start, stop, pause, resume, etc
1178
+ * @instance
1179
+ * @type {ControlsOptionsManager}
1180
+ * @public
1181
+ * @memberof Meeting
1182
+ */
1183
+ this.controlsOptionsManager = new ControlsOptionsManager(this.meetingRequest, {
1184
+ locusUrl: this.locusInfo?.url,
1185
+ displayHints: [],
1186
+ });
1187
+
1026
1188
  this.setUpLocusInfoListeners();
1027
1189
  this.locusInfo.init(attrs.locus ? attrs.locus : {});
1028
1190
  this.hasJoinedOnce = false;
1029
1191
 
1030
- this.media = new MultistreamMedia(this);
1031
-
1032
1192
  /**
1033
1193
  * helper class for managing remote streams
1034
1194
  */
1035
1195
  this.remoteMediaManager = null;
1196
+
1197
+ this.localAudioTrackMuteStateHandler = (event) => {
1198
+ this.audio.handleLocalTrackMuteStateChange(this, event.trackState.muted);
1199
+ };
1200
+
1201
+ this.localVideoTrackMuteStateHandler = (event) => {
1202
+ this.video.handleLocalTrackMuteStateChange(this, event.trackState.muted);
1203
+ };
1204
+
1205
+ // The handling of underlying track changes should be done inside
1206
+ // @webex/internal-media-core, but for now we have to do it here, because
1207
+ // RoapMediaConnection has to use raw MediaStreamTracks in its API until
1208
+ // the Calling SDK also moves to using webrtc-core tracks
1209
+ this.underlyingLocalTrackChangeHandler = () => {
1210
+ if (!this.isMultistream) {
1211
+ this.updateTranscodedMediaConnection();
1212
+ }
1213
+ };
1214
+ }
1215
+
1216
+ /**
1217
+ * returns meeting is joined
1218
+ * @private
1219
+ * @memberof Meeting
1220
+ * @returns {Boolean}
1221
+ */
1222
+ private isJoined() {
1223
+ return this.joinedWith?.state === 'JOINED';
1036
1224
  }
1037
1225
 
1038
1226
  /**
@@ -1047,9 +1235,11 @@ export default class Meeting extends StatelessWebexPlugin {
1047
1235
  public async fetchMeetingInfo({
1048
1236
  password = null,
1049
1237
  captchaCode = null,
1238
+ extraParams = {},
1050
1239
  }: {
1051
1240
  password?: string;
1052
1241
  captchaCode?: string;
1242
+ extraParams?: Record<string, any>;
1053
1243
  }) {
1054
1244
  // when fetch meeting info is called directly by the client, we want to clear out the random timer for sdk to do it
1055
1245
  if (this.fetchMeetingInfoTimeoutId) {
@@ -1080,11 +1270,16 @@ export default class Meeting extends StatelessWebexPlugin {
1080
1270
  this.destination,
1081
1271
  this.destinationType,
1082
1272
  password,
1083
- captchaInfo
1273
+ captchaInfo,
1274
+ // @ts-ignore - config coming from registerPlugin
1275
+ this.config.installedOrgID,
1276
+ this.locusId,
1277
+ extraParams,
1278
+ {meetingId: this.id}
1084
1279
  );
1085
1280
 
1086
1281
  this.parseMeetingInfo(info, this.destination);
1087
- this.meetingInfo = info ? info.body : null;
1282
+ this.meetingInfo = info ? {...info.body, meetingLookupUrl: info?.url} : null;
1088
1283
  this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NONE;
1089
1284
  this.requiredCaptcha = null;
1090
1285
  if (
@@ -1107,9 +1302,18 @@ export default class Meeting extends StatelessWebexPlugin {
1107
1302
 
1108
1303
  return Promise.resolve();
1109
1304
  } catch (err) {
1110
- if (err instanceof MeetingInfoV2PasswordError) {
1111
- // @ts-ignore
1305
+ if (err instanceof MeetingInfoV2PolicyError) {
1306
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.POLICY;
1307
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1308
+
1309
+ if (err.meetingInfo) {
1310
+ this.meetingInfo = err.meetingInfo;
1311
+ }
1312
+
1313
+ throw new PermissionError();
1314
+ } else if (err instanceof MeetingInfoV2PasswordError) {
1112
1315
  LoggerProxy.logger.info(
1316
+ // @ts-ignore
1113
1317
  `Meeting:index#fetchMeetingInfo --> Info Unable to fetch meeting info for ${this.destination} - password required (code=${err?.body?.code}).`
1114
1318
  );
1115
1319
 
@@ -1119,6 +1323,8 @@ export default class Meeting extends StatelessWebexPlugin {
1119
1323
  this.meetingNumber = err.meetingInfo.meetingNumber;
1120
1324
  }
1121
1325
 
1326
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1327
+
1122
1328
  this.passwordStatus = PASSWORD_STATUS.REQUIRED;
1123
1329
  this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD;
1124
1330
  if (this.requiredCaptcha) {
@@ -1128,8 +1334,8 @@ export default class Meeting extends StatelessWebexPlugin {
1128
1334
 
1129
1335
  throw new PasswordError();
1130
1336
  } else if (err instanceof MeetingInfoV2CaptchaError) {
1131
- // @ts-ignore
1132
1337
  LoggerProxy.logger.info(
1338
+ // @ts-ignore
1133
1339
  `Meeting:index#fetchMeetingInfo --> Info Unable to fetch meeting info for ${this.destination} - captcha required (code=${err?.body?.code}).`
1134
1340
  );
1135
1341
 
@@ -1137,6 +1343,8 @@ export default class Meeting extends StatelessWebexPlugin {
1137
1343
  ? MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA
1138
1344
  : MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD;
1139
1345
 
1346
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1347
+
1140
1348
  if (err.isPasswordRequired) {
1141
1349
  this.passwordStatus = PASSWORD_STATUS.REQUIRED;
1142
1350
  }
@@ -1201,22 +1409,39 @@ export default class Meeting extends StatelessWebexPlugin {
1201
1409
  // we have to pass the wbxappapi hostname as the siteFullName parameter
1202
1410
  const {hostname} = new URL(this.requiredCaptcha.refreshURL);
1203
1411
 
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
- });
1412
+ return (
1413
+ this.meetingRequest
1414
+ // @ts-ignore
1415
+ .refreshCaptcha({
1416
+ captchaRefreshUrl: `${this.requiredCaptcha.refreshURL}&siteFullName=${hostname}`,
1417
+ captchaId: this.requiredCaptcha.captchaId,
1418
+ })
1419
+ .then((response) => {
1420
+ this.requiredCaptcha.captchaId = response.body.captchaID;
1421
+ this.requiredCaptcha.verificationImageURL = response.body.verificationImageURL;
1422
+ this.requiredCaptcha.verificationAudioURL = response.body.verificationAudioURL;
1423
+ })
1424
+ .catch((error) => {
1425
+ LoggerProxy.logger.error(
1426
+ `Meeting:index#refreshCaptcha --> Error Unable to refresh captcha for ${this.destination} - ${error}`
1427
+ );
1428
+ throw error;
1429
+ })
1430
+ );
1431
+ }
1432
+
1433
+ /**
1434
+ * Posts metrics event for this meeting. Allows the app to send Call Analyzer events.
1435
+ * @param {String} eventName - Call Analyzer event, see eventType in src/metrics/config.ts for possible values
1436
+ * @public
1437
+ * @memberof Meeting
1438
+ * @returns {Promise}
1439
+ */
1440
+ public postMetrics(eventName: string) {
1441
+ Metrics.postEvent({
1442
+ event: eventName,
1443
+ meeting: this,
1444
+ });
1220
1445
  }
1221
1446
 
1222
1447
  /**
@@ -1229,6 +1454,7 @@ export default class Meeting extends StatelessWebexPlugin {
1229
1454
  // meeting update listeners
1230
1455
  this.setUpLocusInfoSelfListener();
1231
1456
  this.setUpLocusInfoMeetingListener();
1457
+ this.setUpLocusServicesListener();
1232
1458
  // members update listeners
1233
1459
  this.setUpLocusFullStateListener();
1234
1460
  this.setUpLocusUrlListener();
@@ -1241,6 +1467,96 @@ export default class Meeting extends StatelessWebexPlugin {
1241
1467
  this.setUpLocusInfoMeetingInfoListener();
1242
1468
  this.setUpLocusInfoAssignHostListener();
1243
1469
  this.setUpLocusInfoMediaInactiveListener();
1470
+ this.setUpBreakoutsListener();
1471
+ }
1472
+
1473
+ /**
1474
+ * Set up the listeners for breakouts
1475
+ * @returns {undefined}
1476
+ * @private
1477
+ * @memberof Meeting
1478
+ */
1479
+ setUpBreakoutsListener() {
1480
+ this.breakouts.on(BREAKOUTS.EVENTS.BREAKOUTS_CLOSING, () => {
1481
+ Trigger.trigger(
1482
+ this,
1483
+ {
1484
+ file: 'meeting/index',
1485
+ function: 'setUpBreakoutsListener',
1486
+ },
1487
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_CLOSING
1488
+ );
1489
+ });
1490
+
1491
+ this.breakouts.on(BREAKOUTS.EVENTS.MESSAGE, (messageEvent) => {
1492
+ Trigger.trigger(
1493
+ this,
1494
+ {
1495
+ file: 'meeting/index',
1496
+ function: 'setUpBreakoutsListener',
1497
+ },
1498
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_MESSAGE,
1499
+ messageEvent
1500
+ );
1501
+ });
1502
+
1503
+ this.breakouts.on(BREAKOUTS.EVENTS.MEMBERS_UPDATE, () => {
1504
+ Trigger.trigger(
1505
+ this,
1506
+ {
1507
+ file: 'meeting/index',
1508
+ function: 'setUpBreakoutsListener',
1509
+ },
1510
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_UPDATE
1511
+ );
1512
+ });
1513
+
1514
+ this.breakouts.on(BREAKOUTS.EVENTS.ASK_RETURN_TO_MAIN, () => {
1515
+ if (this.isJoined()) {
1516
+ Trigger.trigger(
1517
+ this,
1518
+ {
1519
+ file: 'meeting/index',
1520
+ function: 'setUpBreakoutsListener',
1521
+ },
1522
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_ASK_RETURN_TO_MAIN
1523
+ );
1524
+ }
1525
+ });
1526
+
1527
+ this.breakouts.on(BREAKOUTS.EVENTS.LEAVE_BREAKOUT, () => {
1528
+ Trigger.trigger(
1529
+ this,
1530
+ {
1531
+ file: 'meeting/index',
1532
+ function: 'setUpBreakoutsListener',
1533
+ },
1534
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_LEAVE
1535
+ );
1536
+ });
1537
+
1538
+ this.breakouts.on(BREAKOUTS.EVENTS.ASK_FOR_HELP, (helpEvent) => {
1539
+ Trigger.trigger(
1540
+ this,
1541
+ {
1542
+ file: 'meeting/index',
1543
+ function: 'setUpBreakoutsListener',
1544
+ },
1545
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_ASK_FOR_HELP,
1546
+ helpEvent
1547
+ );
1548
+ });
1549
+
1550
+ this.breakouts.on(BREAKOUTS.EVENTS.PRE_ASSIGNMENTS_UPDATE, () => {
1551
+ Trigger.trigger(
1552
+ this,
1553
+ {
1554
+ file: 'meeting/index',
1555
+ function: 'setUpBreakoutsListener',
1556
+ },
1557
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_PRE_ASSIGNMENTS_UPDATE
1558
+ );
1559
+ });
1244
1560
  }
1245
1561
 
1246
1562
  /**
@@ -1356,19 +1672,20 @@ export default class Meeting extends StatelessWebexPlugin {
1356
1672
  * @returns {Object}
1357
1673
  * @memberof Meeting
1358
1674
  */
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
- ) {
1675
+ getAnalyzerMetricsPrePayload(options: {
1676
+ type?: string;
1677
+ event: string;
1678
+ trackingId: string;
1679
+ locus: object;
1680
+ mediaConnections?: Array<any>;
1681
+ errors?: object;
1682
+ meetingLookupUrl?: string;
1683
+ clientType?: any;
1684
+ subClientType?: any;
1685
+ [key: string]: any;
1686
+ }) {
1370
1687
  if (options) {
1371
- const {event, trackingId, mediaConnections} = options;
1688
+ const {event, trackingId, mediaConnections, meetingLookupUrl} = options;
1372
1689
 
1373
1690
  if (!event) {
1374
1691
  LoggerProxy.logger.error(
@@ -1407,6 +1724,10 @@ export default class Meeting extends StatelessWebexPlugin {
1407
1724
  identifiers.mediaAgentCluster = this.mediaConnections?.[0].mediaAgentCluster;
1408
1725
  }
1409
1726
 
1727
+ if (meetingLookupUrl) {
1728
+ identifiers.meetingLookupUrl = meetingLookupUrl;
1729
+ }
1730
+
1410
1731
  if (options.trackingId) {
1411
1732
  identifiers.trackingId = trackingId;
1412
1733
  }
@@ -1456,12 +1777,12 @@ export default class Meeting extends StatelessWebexPlugin {
1456
1777
  };
1457
1778
  }
1458
1779
 
1459
- const callInitiateJoinReq = this.getCallInitiateJoinReq();
1780
+ const callInitJoinReq = this.getCallInitJoinReq();
1460
1781
 
1461
- if (callInitiateJoinReq) {
1782
+ if (callInitJoinReq) {
1462
1783
  options.joinTimes = {
1463
1784
  ...options.joinTimes,
1464
- callInitiateJoinReq,
1785
+ callInitJoinReq,
1465
1786
  };
1466
1787
  }
1467
1788
 
@@ -1474,15 +1795,31 @@ export default class Meeting extends StatelessWebexPlugin {
1474
1795
  };
1475
1796
  }
1476
1797
 
1477
- const getTotalJmt = this.getTotalJmt();
1798
+ const totalJmt = this.getTotalJmt();
1478
1799
 
1479
- if (getTotalJmt) {
1800
+ if (totalJmt) {
1480
1801
  options.joinTimes = {
1481
1802
  ...options.joinTimes,
1482
- getTotalJmt,
1803
+ totalJmt,
1483
1804
  };
1484
1805
  }
1485
1806
 
1807
+ const curUserType = this.getCurUserType();
1808
+
1809
+ if (curUserType) {
1810
+ options.userType = curUserType;
1811
+ }
1812
+
1813
+ const curLoginType = this.getCurLoginType();
1814
+
1815
+ if (curLoginType) {
1816
+ options.loginType = curLoginType;
1817
+ }
1818
+
1819
+ if (this.environment) {
1820
+ options.environment = this.environment;
1821
+ }
1822
+
1486
1823
  if (options.type === MQA_STATS.CA_TYPE) {
1487
1824
  payload = Metrics.initMediaPayload(options.event, identifiers, options);
1488
1825
  } else {
@@ -1596,11 +1933,7 @@ export default class Meeting extends StatelessWebexPlugin {
1596
1933
  this.pstnUpdate(payload);
1597
1934
 
1598
1935
  // 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
- }
1936
+ this.requestScreenShareFloorIfPending();
1604
1937
  });
1605
1938
  }
1606
1939
 
@@ -1786,6 +2119,28 @@ export default class Meeting extends StatelessWebexPlugin {
1786
2119
  }
1787
2120
  );
1788
2121
 
2122
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_MEETING_BREAKOUT_UPDATED, ({breakout}) => {
2123
+ this.breakouts.updateBreakout(breakout);
2124
+ Trigger.trigger(
2125
+ this,
2126
+ {
2127
+ file: 'meeting/index',
2128
+ function: 'setupLocusControlsListener',
2129
+ },
2130
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_UPDATE
2131
+ );
2132
+ });
2133
+
2134
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_JOIN_BREAKOUT_FROM_MAIN, ({mainLocusUrl}) => {
2135
+ this.meetingRequest.getLocusStatusByUrl(mainLocusUrl).catch((error) => {
2136
+ // clear main session cache when attendee join into breakout and forbidden to get locus from main locus url,
2137
+ // which means main session is not active for the attendee
2138
+ if (error?.statusCode === 403) {
2139
+ this.locusInfo.clearMainSessionLocusCache();
2140
+ }
2141
+ });
2142
+ });
2143
+
1789
2144
  this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_ENTRY_EXIT_TONE_UPDATED, ({entryExitTone}) => {
1790
2145
  Trigger.trigger(
1791
2146
  this,
@@ -1797,23 +2152,115 @@ export default class Meeting extends StatelessWebexPlugin {
1797
2152
  {entryExitTone}
1798
2153
  );
1799
2154
  });
1800
- }
1801
2155
 
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}
2156
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_MUTE_ON_ENTRY_CHANGED, ({state}) => {
2157
+ Trigger.trigger(
2158
+ this,
2159
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2160
+ EVENT_TRIGGERS.MEETING_CONTROLS_MUTE_ON_ENTRY_UPDATED,
2161
+ {state}
2162
+ );
2163
+ });
2164
+
2165
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_SHARE_CONTROL_CHANGED, ({state}) => {
2166
+ Trigger.trigger(
2167
+ this,
2168
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2169
+ EVENT_TRIGGERS.MEETING_CONTROLS_SHARE_CONTROL_UPDATED,
2170
+ {state}
2171
+ );
2172
+ });
2173
+
2174
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_DISALLOW_UNMUTE_CHANGED, ({state}) => {
2175
+ Trigger.trigger(
2176
+ this,
2177
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2178
+ EVENT_TRIGGERS.MEETING_CONTROLS_DISALLOW_UNMUTE_UPDATED,
2179
+ {state}
2180
+ );
2181
+ });
2182
+
2183
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_REACTIONS_CHANGED, ({state}) => {
2184
+ Trigger.trigger(
2185
+ this,
2186
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2187
+ EVENT_TRIGGERS.MEETING_CONTROLS_REACTIONS_UPDATED,
2188
+ {state}
2189
+ );
2190
+ });
2191
+
2192
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_VIEW_THE_PARTICIPANTS_LIST_CHANGED, ({state}) => {
2193
+ Trigger.trigger(
2194
+ this,
2195
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2196
+ EVENT_TRIGGERS.MEETING_CONTROLS_VIEW_THE_PARTICIPANTS_LIST_UPDATED,
2197
+ {state}
2198
+ );
2199
+ });
2200
+
2201
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_RAISE_HAND_CHANGED, ({state}) => {
2202
+ Trigger.trigger(
2203
+ this,
2204
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2205
+ EVENT_TRIGGERS.MEETING_CONTROLS_RAISE_HAND_UPDATED,
2206
+ {state}
2207
+ );
2208
+ });
2209
+
2210
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_VIDEO_CHANGED, ({state}) => {
2211
+ Trigger.trigger(
2212
+ this,
2213
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2214
+ EVENT_TRIGGERS.MEETING_CONTROLS_VIDEO_UPDATED,
2215
+ {state}
2216
+ );
2217
+ });
2218
+ }
2219
+
2220
+ /**
2221
+ * Trigger annotation info update event
2222
+ @returns {undefined}
2223
+ @param {object} contentShare
2224
+ @param {object} previousContentShare
2225
+ */
2226
+ private triggerAnnotationInfoEvent(contentShare, previousContentShare) {
2227
+ if (
2228
+ contentShare?.annotation &&
2229
+ !isEqual(contentShare?.annotation, previousContentShare?.annotation)
2230
+ ) {
2231
+ Trigger.trigger(
2232
+ // @ts-ignore
2233
+ this.webex.meetings,
2234
+ {
2235
+ file: 'meeting/index',
2236
+ function: 'triggerAnnotationInfoEvent',
2237
+ },
2238
+ EVENT_TRIGGERS.MEETING_UPDATE_ANNOTATION_INFO,
2239
+ {
2240
+ annotationInfo: contentShare?.annotation,
2241
+ meetingId: this.id,
2242
+ }
2243
+ );
2244
+ }
2245
+ }
2246
+
2247
+ /**
2248
+ * Set up the locus info media shares listener
2249
+ * update content and whiteboard sharing id value for members, and updates the member
2250
+ * notifies consumer with members:content:update {activeContentSharingId, endedContentSharingId}
1806
2251
  * @returns {undefined}
1807
2252
  * @private
1808
2253
  * @memberof Meeting
1809
2254
  */
1810
2255
  private setUpLocusMediaSharesListener() {
1811
2256
  // Will get triggered on local and remote share
1812
- this.locusInfo.on(EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES, (payload) => {
2257
+ this.locusInfo.on(EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES, async (payload) => {
1813
2258
  const {content: contentShare, whiteboard: whiteboardShare} = payload.current;
1814
2259
  const previousContentShare = payload.previous?.content;
1815
2260
  const previousWhiteboardShare = payload.previous?.whiteboard;
1816
2261
 
2262
+ this.triggerAnnotationInfoEvent(contentShare, previousContentShare);
2263
+
1817
2264
  if (
1818
2265
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
1819
2266
  contentShare.disposition === previousContentShare?.disposition &&
@@ -1841,19 +2288,8 @@ export default class Meeting extends StatelessWebexPlugin {
1841
2288
  this.selfId === contentShare.beneficiaryId &&
1842
2289
  contentShare.disposition === FLOOR_ACTION.GRANTED
1843
2290
  ) {
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
- }
2291
+ // CONTENT - sharing content local
2292
+ newShareStatus = SHARE_STATUS.LOCAL_SHARE_ACTIVE;
1857
2293
  }
1858
2294
  // If we did not hit the cases above, no one is sharng content, so we check if we are sharing whiteboard
1859
2295
  // There is no concept of local/remote share for whiteboard
@@ -1937,23 +2373,22 @@ export default class Meeting extends StatelessWebexPlugin {
1937
2373
  EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
1938
2374
  {
1939
2375
  memberId: contentShare.beneficiaryId,
2376
+ url: contentShare.url,
2377
+ shareInstanceId: contentShare.shareInstanceId,
1940
2378
  }
1941
2379
  );
1942
2380
  };
1943
2381
 
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
- ) {
2382
+ try {
2383
+ // if a remote participant is stealing the presentation from us
2384
+ if (
2385
+ this.mediaProperties.mediaDirection?.sendShare &&
2386
+ oldShareStatus === SHARE_STATUS.LOCAL_SHARE_ACTIVE
2387
+ ) {
2388
+ await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo screen share audio (SPARK-399690)
2389
+ }
2390
+ } finally {
1949
2391
  sendStartedSharingRemote();
1950
- } else {
1951
- this.updateShare({
1952
- sendShare: false,
1953
- receiveShare: this.mediaProperties.mediaDirection.receiveShare,
1954
- }).finally(() => {
1955
- sendStartedSharingRemote();
1956
- });
1957
2392
  }
1958
2393
  break;
1959
2394
  }
@@ -2007,6 +2442,8 @@ export default class Meeting extends StatelessWebexPlugin {
2007
2442
  EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
2008
2443
  {
2009
2444
  memberId: contentShare.beneficiaryId,
2445
+ url: contentShare.url,
2446
+ shareInstanceId: contentShare.shareInstanceId,
2010
2447
  }
2011
2448
  );
2012
2449
  this.members.locusMediaSharesUpdate(payload);
@@ -2041,8 +2478,30 @@ export default class Meeting extends StatelessWebexPlugin {
2041
2478
  private setUpLocusUrlListener() {
2042
2479
  this.locusInfo.on(EVENTS.LOCUS_INFO_UPDATE_URL, (payload) => {
2043
2480
  this.members.locusUrlUpdate(payload);
2481
+ this.breakouts.locusUrlUpdate(payload);
2482
+ this.annotation.locusUrlUpdate(payload);
2044
2483
  this.locusUrl = payload;
2045
2484
  this.locusId = this.locusUrl?.split('/').pop();
2485
+ this.recordingController.setLocusUrl(this.locusUrl);
2486
+ this.controlsOptionsManager.setLocusUrl(this.locusUrl);
2487
+ });
2488
+ }
2489
+
2490
+ /**
2491
+ * Set up the locus info service link listener
2492
+ * update the locusInfo for recording controller
2493
+ * does not currently re-emit the event as it's internal only
2494
+ * payload is unused
2495
+ * @returns {undefined}
2496
+ * @private
2497
+ * @memberof Meeting
2498
+ */
2499
+ private setUpLocusServicesListener() {
2500
+ this.locusInfo.on(LOCUSINFO.EVENTS.LINKS_SERVICES, (payload) => {
2501
+ this.recordingController.setServiceUrl(payload?.services?.record?.url);
2502
+ this.recordingController.setSessionId(this.locusInfo?.fullState?.sessionId);
2503
+ this.breakouts.breakoutServiceUrlUpdate(payload?.services?.breakout?.url);
2504
+ this.annotation.approvalUrlUpdate(payload?.services?.approval?.url);
2046
2505
  });
2047
2506
  }
2048
2507
 
@@ -2092,10 +2551,22 @@ export default class Meeting extends StatelessWebexPlugin {
2092
2551
  canAdmitParticipant: MeetingUtil.canAdmitParticipant(payload.info.userDisplayHints),
2093
2552
  canLock: MeetingUtil.canUserLock(payload.info.userDisplayHints),
2094
2553
  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),
2554
+ canSetDisallowUnmute: ControlsOptionsUtil.canSetDisallowUnmute(
2555
+ payload.info.userDisplayHints
2556
+ ),
2557
+ canUnsetDisallowUnmute: ControlsOptionsUtil.canUnsetDisallowUnmute(
2558
+ payload.info.userDisplayHints
2559
+ ),
2560
+ canSetMuteOnEntry: ControlsOptionsUtil.canSetMuteOnEntry(payload.info.userDisplayHints),
2561
+ canUnsetMuteOnEntry: ControlsOptionsUtil.canUnsetMuteOnEntry(
2562
+ payload.info.userDisplayHints
2563
+ ),
2564
+ canSetMuted: ControlsOptionsUtil.canSetMuted(payload.info.userDisplayHints),
2565
+ canUnsetMuted: ControlsOptionsUtil.canUnsetMuted(payload.info.userDisplayHints),
2566
+ canStartRecording: RecordingUtil.canUserStart(payload.info.userDisplayHints),
2567
+ canStopRecording: RecordingUtil.canUserStop(payload.info.userDisplayHints),
2568
+ canPauseRecording: RecordingUtil.canUserPause(payload.info.userDisplayHints),
2569
+ canResumeRecording: RecordingUtil.canUserResume(payload.info.userDisplayHints),
2099
2570
  canRaiseHand: MeetingUtil.canUserRaiseHand(payload.info.userDisplayHints),
2100
2571
  canLowerAllHands: MeetingUtil.canUserLowerAllHands(payload.info.userDisplayHints),
2101
2572
  canLowerSomeoneElsesHand: MeetingUtil.canUserLowerSomeoneElsesHand(
@@ -2108,6 +2579,9 @@ export default class Meeting extends StatelessWebexPlugin {
2108
2579
  canStartTranscribing: MeetingUtil.canStartTranscribing(payload.info.userDisplayHints),
2109
2580
  canStopTranscribing: MeetingUtil.canStopTranscribing(payload.info.userDisplayHints),
2110
2581
  isClosedCaptionActive: MeetingUtil.isClosedCaptionActive(payload.info.userDisplayHints),
2582
+ isSaveTranscriptsEnabled: MeetingUtil.isSaveTranscriptsEnabled(
2583
+ payload.info.userDisplayHints
2584
+ ),
2111
2585
  isWebexAssistantActive: MeetingUtil.isWebexAssistantActive(payload.info.userDisplayHints),
2112
2586
  canViewCaptionPanel: MeetingUtil.canViewCaptionPanel(payload.info.userDisplayHints),
2113
2587
  isRealTimeTranslationEnabled: MeetingUtil.isRealTimeTranslationEnabled(
@@ -2117,8 +2591,118 @@ export default class Meeting extends StatelessWebexPlugin {
2117
2591
  payload.info.userDisplayHints
2118
2592
  ),
2119
2593
  waitingForOthersToJoin: MeetingUtil.waitingForOthersToJoin(payload.info.userDisplayHints),
2594
+ canSendReactions: MeetingUtil.canSendReactions(
2595
+ this.inMeetingActions.canSendReactions,
2596
+ payload.info.userDisplayHints
2597
+ ),
2598
+ canManageBreakout: MeetingUtil.canManageBreakout(payload.info.userDisplayHints),
2599
+ canBroadcastMessageToBreakout: MeetingUtil.canBroadcastMessageToBreakout(
2600
+ payload.info.userDisplayHints
2601
+ ),
2602
+ canAdmitLobbyToBreakout: MeetingUtil.canAdmitLobbyToBreakout(
2603
+ payload.info.userDisplayHints
2604
+ ),
2605
+ isBreakoutPreassignmentsEnabled: MeetingUtil.isBreakoutPreassignmentsEnabled(
2606
+ payload.info.userDisplayHints
2607
+ ),
2608
+ canUserAskForHelp: MeetingUtil.canUserAskForHelp(payload.info.userDisplayHints),
2609
+ canUserRenameSelfAndObserved: MeetingUtil.canUserRenameSelfAndObserved(
2610
+ payload.info.userDisplayHints
2611
+ ),
2612
+ canUserRenameOthers: MeetingUtil.canUserRenameOthers(payload.info.userDisplayHints),
2613
+ canMuteAll: ControlsOptionsUtil.hasHints({
2614
+ requiredHints: [DISPLAY_HINTS.MUTE_ALL],
2615
+ displayHints: payload.info.userDisplayHints,
2616
+ }),
2617
+ canUnmuteAll: ControlsOptionsUtil.hasHints({
2618
+ requiredHints: [DISPLAY_HINTS.UNMUTE_ALL],
2619
+ displayHints: payload.info.userDisplayHints,
2620
+ }),
2621
+ canEnableHardMute: ControlsOptionsUtil.hasHints({
2622
+ requiredHints: [DISPLAY_HINTS.ENABLE_HARD_MUTE],
2623
+ displayHints: payload.info.userDisplayHints,
2624
+ }),
2625
+ canDisableHardMute: ControlsOptionsUtil.hasHints({
2626
+ requiredHints: [DISPLAY_HINTS.DISABLE_HARD_MUTE],
2627
+ displayHints: payload.info.userDisplayHints,
2628
+ }),
2629
+ canEnableMuteOnEntry: ControlsOptionsUtil.hasHints({
2630
+ requiredHints: [DISPLAY_HINTS.ENABLE_MUTE_ON_ENTRY],
2631
+ displayHints: payload.info.userDisplayHints,
2632
+ }),
2633
+ canDisableMuteOnEntry: ControlsOptionsUtil.hasHints({
2634
+ requiredHints: [DISPLAY_HINTS.DISABLE_MUTE_ON_ENTRY],
2635
+ displayHints: payload.info.userDisplayHints,
2636
+ }),
2637
+ canEnableReactions: ControlsOptionsUtil.hasHints({
2638
+ requiredHints: [DISPLAY_HINTS.ENABLE_REACTIONS],
2639
+ displayHints: payload.info.userDisplayHints,
2640
+ }),
2641
+ canDisableReactions: ControlsOptionsUtil.hasHints({
2642
+ requiredHints: [DISPLAY_HINTS.DISABLE_REACTIONS],
2643
+ displayHints: payload.info.userDisplayHints,
2644
+ }),
2645
+ canEnableReactionDisplayNames: ControlsOptionsUtil.hasHints({
2646
+ requiredHints: [DISPLAY_HINTS.ENABLE_SHOW_DISPLAY_NAME],
2647
+ displayHints: payload.info.userDisplayHints,
2648
+ }),
2649
+ canDisableReactionDisplayNames: ControlsOptionsUtil.hasHints({
2650
+ requiredHints: [DISPLAY_HINTS.DISABLE_SHOW_DISPLAY_NAME],
2651
+ displayHints: payload.info.userDisplayHints,
2652
+ }),
2653
+ canUpdateShareControl: ControlsOptionsUtil.hasHints({
2654
+ requiredHints: [DISPLAY_HINTS.SHARE_CONTROL],
2655
+ displayHints: payload.info.userDisplayHints,
2656
+ }),
2657
+ canEnableViewTheParticipantsList: ControlsOptionsUtil.hasHints({
2658
+ requiredHints: [DISPLAY_HINTS.ENABLE_VIEW_THE_PARTICIPANT_LIST],
2659
+ displayHints: payload.info.userDisplayHints,
2660
+ }),
2661
+ canDisableViewTheParticipantsList: ControlsOptionsUtil.hasHints({
2662
+ requiredHints: [DISPLAY_HINTS.DISABLE_VIEW_THE_PARTICIPANT_LIST],
2663
+ displayHints: payload.info.userDisplayHints,
2664
+ }),
2665
+ canEnableRaiseHand: ControlsOptionsUtil.hasHints({
2666
+ requiredHints: [DISPLAY_HINTS.ENABLE_RAISE_HAND],
2667
+ displayHints: payload.info.userDisplayHints,
2668
+ }),
2669
+ canDisableRaiseHand: ControlsOptionsUtil.hasHints({
2670
+ requiredHints: [DISPLAY_HINTS.DISABLE_RAISE_HAND],
2671
+ displayHints: payload.info.userDisplayHints,
2672
+ }),
2673
+ canEnableVideo: ControlsOptionsUtil.hasHints({
2674
+ requiredHints: [DISPLAY_HINTS.ENABLE_VIDEO],
2675
+ displayHints: payload.info.userDisplayHints,
2676
+ }),
2677
+ canDisableVideo: ControlsOptionsUtil.hasHints({
2678
+ requiredHints: [DISPLAY_HINTS.DISABLE_VIDEO],
2679
+ displayHints: payload.info.userDisplayHints,
2680
+ }),
2681
+ canShareFile: ControlsOptionsUtil.hasHints({
2682
+ requiredHints: [DISPLAY_HINTS.SHARE_FILE],
2683
+ displayHints: payload.info.userDisplayHints,
2684
+ }),
2685
+ canShareApplication: ControlsOptionsUtil.hasHints({
2686
+ requiredHints: [DISPLAY_HINTS.SHARE_APPLICATION],
2687
+ displayHints: payload.info.userDisplayHints,
2688
+ }),
2689
+ canShareCamera: ControlsOptionsUtil.hasHints({
2690
+ requiredHints: [DISPLAY_HINTS.SHARE_CAMERA],
2691
+ displayHints: payload.info.userDisplayHints,
2692
+ }),
2693
+ canShareDesktop: ControlsOptionsUtil.hasHints({
2694
+ requiredHints: [DISPLAY_HINTS.SHARE_DESKTOP],
2695
+ displayHints: payload.info.userDisplayHints,
2696
+ }),
2697
+ canShareContent: ControlsOptionsUtil.hasHints({
2698
+ requiredHints: [DISPLAY_HINTS.SHARE_CONTENT],
2699
+ displayHints: payload.info.userDisplayHints,
2700
+ }),
2120
2701
  });
2121
2702
 
2703
+ this.recordingController.setDisplayHints(payload.info.userDisplayHints);
2704
+ this.controlsOptionsManager.setDisplayHints(payload.info.userDisplayHints);
2705
+
2122
2706
  if (changed) {
2123
2707
  Trigger.trigger(
2124
2708
  this,
@@ -2141,7 +2725,9 @@ export default class Meeting extends StatelessWebexPlugin {
2141
2725
  * @param {String} datachannelUrl
2142
2726
  * @returns {void}
2143
2727
  */
2728
+
2144
2729
  handleDataChannelUrlChange(datachannelUrl) {
2730
+ // @ts-ignore - config coming from registerPlugin
2145
2731
  if (datachannelUrl && this.config.enableAutomaticLLM) {
2146
2732
  // Defer this as updateLLMConnection relies upon this.locusInfo.url which is only set
2147
2733
  // after the MEETING_INFO_UPDATED callback finishes
@@ -2196,10 +2782,34 @@ export default class Meeting extends StatelessWebexPlugin {
2196
2782
  );
2197
2783
  }
2198
2784
  });
2785
+
2786
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_REMOTE_VIDEO_MUTE_STATUS_UPDATED, (payload) => {
2787
+ if (payload) {
2788
+ if (this.video) {
2789
+ payload.muted = payload.muted ?? this.video.isRemotelyMuted();
2790
+ payload.unmuteAllowed = payload.unmuteAllowed ?? this.video.isUnmuteAllowed();
2791
+ this.video.handleServerRemoteMuteUpdate(this, payload.muted, payload.unmuteAllowed);
2792
+ }
2793
+ Trigger.trigger(
2794
+ this,
2795
+ {
2796
+ file: 'meeting/index',
2797
+ function: 'setUpLocusInfoSelfListener',
2798
+ },
2799
+ payload.muted
2800
+ ? EVENT_TRIGGERS.MEETING_SELF_VIDEO_MUTED_BY_OTHERS
2801
+ : EVENT_TRIGGERS.MEETING_SELF_VIDEO_UNMUTED_BY_OTHERS,
2802
+ {
2803
+ payload,
2804
+ }
2805
+ );
2806
+ }
2807
+ });
2808
+
2199
2809
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED, (payload) => {
2200
2810
  if (payload) {
2201
2811
  if (this.audio) {
2202
- this.audio.handleServerRemoteMuteUpdate(payload.muted, payload.unmuteAllowed);
2812
+ this.audio.handleServerRemoteMuteUpdate(this, payload.muted, payload.unmuteAllowed);
2203
2813
  }
2204
2814
  // with "mute on entry" server will send us remote mute even if we don't have media configured,
2205
2815
  // so if being muted by others, always send the notification,
@@ -2322,6 +2932,37 @@ export default class Meeting extends StatelessWebexPlugin {
2322
2932
  );
2323
2933
  });
2324
2934
 
2935
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_BREAKOUTS_CHANGED, (payload) => {
2936
+ this.breakouts.updateBreakoutSessions(payload);
2937
+ Trigger.trigger(
2938
+ this,
2939
+ {
2940
+ file: 'meeting/index',
2941
+ function: 'setUpLocusInfoSelfListener',
2942
+ },
2943
+ EVENT_TRIGGERS.MEETING_BREAKOUTS_UPDATE
2944
+ );
2945
+ });
2946
+
2947
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ROLES_CHANGED, (payload) => {
2948
+ const isModeratorOrCohost =
2949
+ payload.newRoles?.includes(SELF_ROLES.MODERATOR) ||
2950
+ payload.newRoles?.includes(SELF_ROLES.COHOST);
2951
+ this.breakouts.updateCanManageBreakouts(isModeratorOrCohost);
2952
+
2953
+ Trigger.trigger(
2954
+ this,
2955
+ {
2956
+ file: 'meeting/index',
2957
+ function: 'setUpLocusInfoSelfListener',
2958
+ },
2959
+ EVENT_TRIGGERS.MEETING_SELF_ROLES_CHANGED,
2960
+ {
2961
+ payload,
2962
+ }
2963
+ );
2964
+ });
2965
+
2325
2966
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_IS_SHARING_BLOCKED_CHANGE, (payload) => {
2326
2967
  Trigger.trigger(
2327
2968
  this,
@@ -2357,19 +2998,18 @@ export default class Meeting extends StatelessWebexPlugin {
2357
2998
  .catch((error) => {
2358
2999
  // @ts-ignore
2359
3000
  LoggerProxy.logger.error(
2360
- `Meeting:index#setUpLocusInfoMeetingListener --> REMOTE_RESPONSE. Issue with leave for meeting, meeting still in collection: ${this.meeting}, error: ${error}`
3001
+ `Meeting:index#setUpLocusInfoMeetingListener --> REMOTE_RESPONSE. Issue with leave for meeting, meeting still in collection: ${this}, error: ${error}`
2361
3002
  );
2362
3003
  });
2363
3004
  }
2364
3005
  });
2365
- this.locusInfo.on(EVENTS.DESTROY_MEETING, (payload) => {
3006
+ this.locusInfo.on(EVENTS.DESTROY_MEETING, async (payload) => {
2366
3007
  // if self state is NOT left
2367
3008
 
2368
3009
  // TODO: Handle sharing and wireless sharing when meeting end
2369
3010
  if (this.wirelessShare) {
2370
3011
  if (this.mediaProperties.shareTrack) {
2371
- this.mediaProperties.shareTrack.onended = null;
2372
- this.mediaProperties.shareTrack.stop();
3012
+ await this.setLocalShareTrack(undefined);
2373
3013
  }
2374
3014
  }
2375
3015
  // when multiple WEB deviceType join with same user
@@ -2383,18 +3023,18 @@ export default class Meeting extends StatelessWebexPlugin {
2383
3023
  if (payload.shouldLeave) {
2384
3024
  // TODO: We should do cleaning of meeting object if the shouldLeave: false because there might be meeting object which we are not cleaning
2385
3025
 
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
- });
3026
+ try {
3027
+ await this.leave({reason: payload.reason});
3028
+
3029
+ LoggerProxy.logger.warn(
3030
+ 'Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. The meeting has been left, but has not been destroyed, you should see a later event for leave.'
3031
+ );
3032
+ } catch (error) {
3033
+ // @ts-ignore
3034
+ LoggerProxy.logger.error(
3035
+ `Meeting:index#setUpLocusInfoMeetingListener --> DESTROY_MEETING. Issue with leave for meeting, meeting still in collection: ${this}, error: ${error}`
3036
+ );
3037
+ }
2398
3038
  } else {
2399
3039
  LoggerProxy.logger.info(
2400
3040
  'Meeting:index#setUpLocusInfoMeetingListener --> MEETING_REMOVED_REASON',
@@ -2472,14 +3112,32 @@ export default class Meeting extends StatelessWebexPlugin {
2472
3112
  }
2473
3113
 
2474
3114
  /**
2475
- * Admit the guest(s) to the call once they are waiting
3115
+ * Admit the guest(s) to the call once they are waiting.
3116
+ * If the host/cohost is in a breakout session, the locus url
3117
+ * of the session must be provided as the authorizingLocusUrl.
3118
+ * Regardless of host/cohost location, the locus Id (lid) in
3119
+ * the path should be the locus Id of the main, which means the
3120
+ * locus url of the api call must be from the main session.
3121
+ * If these loucs urls are not provided, the function will do the check.
2476
3122
  * @param {Array} memberIds
3123
+ * @param {Object} sessionLocusUrls: {authorizingLocusUrl, mainLocusUrl}
2477
3124
  * @returns {Promise} see #members.admitMembers
2478
3125
  * @public
2479
3126
  * @memberof Meeting
2480
3127
  */
2481
- public admit(memberIds: Array<any>) {
2482
- return this.members.admitMembers(memberIds);
3128
+ public admit(
3129
+ memberIds: Array<any>,
3130
+ sessionLocusUrls?: {authorizingLocusUrl: string; mainLocusUrl: string}
3131
+ ) {
3132
+ let locusUrls = sessionLocusUrls;
3133
+ if (!locusUrls) {
3134
+ const {locusUrl, mainLocusUrl} = this.breakouts;
3135
+ if (locusUrl && mainLocusUrl) {
3136
+ locusUrls = {authorizingLocusUrl: locusUrl, mainLocusUrl};
3137
+ }
3138
+ }
3139
+
3140
+ return this.members.admitMembers(memberIds, locusUrls);
2483
3141
  }
2484
3142
 
2485
3143
  /**
@@ -2527,66 +3185,6 @@ export default class Meeting extends StatelessWebexPlugin {
2527
3185
  return this.members;
2528
3186
  }
2529
3187
 
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
3188
  /**
2591
3189
  * Sets the meeting info on the class instance
2592
3190
  * @param {Object} meetingInfo
@@ -2634,6 +3232,7 @@ export default class Meeting extends StatelessWebexPlugin {
2634
3232
  this.locusUrl = locusMeetingObject?.url || webexMeetingInfo?.locusUrl || this.locusUrl;
2635
3233
  // @ts-ignore - config coming from registerPlugin
2636
3234
  this.setSipUri(
3235
+ // @ts-ignore
2637
3236
  this.config.experimental.enableUnifiedMeetings
2638
3237
  ? locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipUrl
2639
3238
  : locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipMeetingUri || this.sipUri
@@ -2650,35 +3249,8 @@ export default class Meeting extends StatelessWebexPlugin {
2650
3249
  webexMeetingInfo?.hostId ||
2651
3250
  this.owner;
2652
3251
  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
- }
3252
+ // Need to populate environment when sending CA event
3253
+ this.environment = locusMeetingObject?.info.channel || webexMeetingInfo?.channel;
2682
3254
  }
2683
3255
  }
2684
3256
 
@@ -2708,7 +3280,7 @@ export default class Meeting extends StatelessWebexPlugin {
2708
3280
  * @private
2709
3281
  * @memberof Meeting
2710
3282
  */
2711
- private setLocus(
3283
+ setLocus(
2712
3284
  locus:
2713
3285
  | {
2714
3286
  mediaConnections: Array<any>;
@@ -2743,21 +3315,6 @@ export default class Meeting extends StatelessWebexPlugin {
2743
3315
  Trigger.trigger(this, options, EVENTS.REQUEST_UPLOAD_LOGS, this);
2744
3316
  }
2745
3317
 
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
3318
  /**
2762
3319
  * Removes remote audio, video and share tracks from class instance's mediaProperties
2763
3320
  * @returns {undefined}
@@ -2842,257 +3399,124 @@ export default class Meeting extends StatelessWebexPlugin {
2842
3399
  }
2843
3400
 
2844
3401
  /**
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
3402
+ * Stores the reference to a new microphone track, sets up the required event listeners
3403
+ * on it, cleans up previous track, etc.
3404
+ *
3405
+ * @param {LocalMicrophoneTrack | null} localTrack local microphone track
3406
+ * @returns {Promise<void>}
2849
3407
  */
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
- }
3408
+ private async setLocalAudioTrack(localTrack?: LocalMicrophoneTrack) {
3409
+ const oldTrack = this.mediaProperties.audioTrack;
2867
3410
 
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();
3411
+ oldTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3412
+ oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
2879
3413
 
2880
- this.mediaProperties.setMediaSettings('audio', {
2881
- echoCancellation: settings.echoCancellation,
2882
- noiseSuppression: settings.noiseSuppression,
2883
- });
3414
+ // we don't update this.mediaProperties.mediaDirection.sendAudio, because we always keep it as true to avoid extra SDP exchanges
3415
+ this.mediaProperties.setLocalAudioTrack(localTrack);
2884
3416
 
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
- }
3417
+ this.audio.handleLocalTrackChange(this);
2892
3418
 
2893
- if (emitEvent) {
2894
- this.sendLocalMediaReadyEvent();
3419
+ localTrack?.on(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3420
+ localTrack?.on(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3421
+
3422
+ if (!this.isMultistream || !localTrack) {
3423
+ // for multistream WCME automatically un-publishes the old track when we publish a new one
3424
+ await this.unpublishTrack(oldTrack);
2895
3425
  }
3426
+ await this.publishTrack(this.mediaProperties.audioTrack);
2896
3427
  }
2897
3428
 
2898
3429
  /**
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
3430
+ * Stores the reference to a new camera track, sets up the required event listeners
3431
+ * on it, cleans up previous track, etc.
3432
+ *
3433
+ * @param {LocalCameraTrack | null} localTrack local camera track
3434
+ * @returns {Promise<void>}
2905
3435
  */
2906
- private setLocalVideoTrack(videoTrack: MediaStreamTrack, emitEvent = true) {
2907
- if (videoTrack) {
2908
- const {aspectRatio, frameRate, height, width, deviceId} = videoTrack.getSettings();
2909
-
2910
- const {localQualityLevel} = this.mediaProperties;
3436
+ private async setLocalVideoTrack(localTrack?: LocalCameraTrack) {
3437
+ const oldTrack = this.mediaProperties.videoTrack;
2911
3438
 
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`);
3439
+ oldTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3440
+ oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
2916
3441
 
2917
- this.mediaProperties.setLocalQualityLevel(`${height}p`);
2918
- }
3442
+ // we don't update this.mediaProperties.mediaDirection.sendVideo, because we always keep it as true to avoid extra SDP exchanges
3443
+ this.mediaProperties.setLocalVideoTrack(localTrack);
2919
3444
 
2920
- this.mediaProperties.setLocalVideoTrack(videoTrack);
2921
- if (this.video) this.video.applyClientStateLocally(this);
3445
+ this.video.handleLocalTrackChange(this);
2922
3446
 
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
- }
3447
+ localTrack?.on(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3448
+ localTrack?.on(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
2938
3449
 
2939
- if (emitEvent) {
2940
- this.sendLocalMediaReadyEvent();
3450
+ if (!this.isMultistream || !localTrack) {
3451
+ // for multistream WCME automatically un-publishes the old track when we publish a new one
3452
+ await this.unpublishTrack(oldTrack);
2941
3453
  }
3454
+ await this.publishTrack(this.mediaProperties.videoTrack);
2942
3455
  }
2943
3456
 
2944
3457
  /**
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
3458
+ * Stores the reference to a new screen share track, sets up the required event listeners
3459
+ * on it, cleans up previous track, etc.
3460
+ * It also sends the floor grant/release request.
3461
+ *
3462
+ * @param {LocalDisplayTrack | undefined} localDisplayTrack local camera track
3463
+ * @returns {Promise<void>}
2950
3464
  */
2951
- public setLocalTracks(localStream: any) {
2952
- if (localStream) {
2953
- const {audioTrack, videoTrack} = MeetingUtil.getTrack(localStream);
3465
+ private async setLocalShareTrack(localDisplayTrack?: LocalDisplayTrack) {
3466
+ const oldTrack = this.mediaProperties.shareTrack;
2954
3467
 
2955
- this.setLocalAudioTrack(audioTrack, false);
2956
- this.setLocalVideoTrack(videoTrack, false);
3468
+ oldTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3469
+ oldTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
2957
3470
 
2958
- this.sendLocalMediaReadyEvent();
3471
+ this.mediaProperties.setLocalShareTrack(localDisplayTrack);
3472
+
3473
+ localDisplayTrack?.on(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3474
+ localDisplayTrack?.on(
3475
+ LocalTrackEvents.UnderlyingTrackChange,
3476
+ this.underlyingLocalTrackChangeHandler
3477
+ );
3478
+
3479
+ this.mediaProperties.mediaDirection.sendShare = !!localDisplayTrack;
3480
+
3481
+ if (!this.isMultistream || !localDisplayTrack) {
3482
+ // for multistream WCME automatically un-publishes the old track when we publish a new one
3483
+ await this.unpublishTrack(oldTrack);
2959
3484
  }
3485
+ await this.publishTrack(this.mediaProperties.shareTrack);
2960
3486
  }
2961
3487
 
2962
3488
  /**
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
- }
2991
-
2992
- contentTracks.onended = () => this.handleShareTrackEnded(localShare);
2993
-
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
- );
3006
- }
3007
- }
3008
-
3009
- /**
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
3489
+ * Removes references to local tracks. This function should be called
3490
+ * on cleanup when we leave the meeting etc.
3491
+ *
3492
+ * @internal
3493
+ * @returns {void}
3015
3494
  */
3016
- public closeLocalStream() {
3017
- const {audioTrack, videoTrack} = this.mediaProperties;
3495
+ public cleanupLocalTracks() {
3496
+ const {audioTrack, videoTrack, shareTrack} = this.mediaProperties;
3018
3497
 
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;
3498
+ audioTrack?.off(LocalTrackEvents.Muted, this.localAudioTrackMuteStateHandler);
3499
+ audioTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3024
3500
 
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
- }
3501
+ videoTrack?.off(LocalTrackEvents.Muted, this.localVideoTrackMuteStateHandler);
3502
+ videoTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3045
3503
 
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;
3504
+ shareTrack?.off(LocalTrackEvents.Ended, this.handleShareTrackEnded);
3505
+ shareTrack?.off(LocalTrackEvents.UnderlyingTrackChange, this.underlyingLocalTrackChangeHandler);
3055
3506
 
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
- }
3507
+ this.mediaProperties.setLocalAudioTrack(undefined);
3508
+ this.mediaProperties.setLocalVideoTrack(undefined);
3509
+ this.mediaProperties.setLocalShareTrack(undefined);
3077
3510
 
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
- }
3511
+ this.mediaProperties.mediaDirection.sendAudio = false;
3512
+ this.mediaProperties.mediaDirection.sendVideo = false;
3513
+ this.mediaProperties.mediaDirection.sendShare = false;
3087
3514
 
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();
3515
+ // WCME doesn't unpublish tracks when multistream connection is closed, so we do it here
3516
+ // (we have to do it for transcoded meetings anyway, so we might as well do for multistream too)
3517
+ audioTrack?.setPublished(false);
3518
+ videoTrack?.setPublished(false);
3519
+ shareTrack?.setPublished(false);
3096
3520
  }
3097
3521
 
3098
3522
  /**
@@ -3143,6 +3567,8 @@ export default class Meeting extends StatelessWebexPlugin {
3143
3567
  * @memberof Meeting
3144
3568
  */
3145
3569
  public closePeerConnections() {
3570
+ this.locusMediaRequest = undefined;
3571
+
3146
3572
  if (this.mediaProperties.webrtcMediaConnection) {
3147
3573
  if (this.remoteMediaManager) {
3148
3574
  this.remoteMediaManager.stop();
@@ -3157,6 +3583,9 @@ export default class Meeting extends StatelessWebexPlugin {
3157
3583
  this.mediaProperties.webrtcMediaConnection.close();
3158
3584
  }
3159
3585
 
3586
+ this.audio = null;
3587
+ this.video = null;
3588
+
3160
3589
  return Promise.resolve();
3161
3590
  }
3162
3591
 
@@ -3187,264 +3616,36 @@ export default class Meeting extends StatelessWebexPlugin {
3187
3616
  this.correlationId = id;
3188
3617
  }
3189
3618
 
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
3619
  /**
3401
3620
  * Shorthand function to join AND set up media
3402
3621
  * @param {Object} options - options to join with media
3403
3622
  * @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()}
3623
+ * @param {MediaDirection} [options.mediaOptions] - see #addMedia()
3624
+ * @returns {Promise} -- {join: see join(), media: see addMedia()}
3407
3625
  * @public
3408
3626
  * @memberof Meeting
3409
3627
  * @example
3410
3628
  * joinWithMedia({
3411
3629
  * 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
- * }})
3630
+ * mediaOptions: {
3631
+ * localTracks: { microphone: microphoneTrack, camera: cameraTrack }
3632
+ * }
3633
+ * })
3424
3634
  */
3425
3635
  public joinWithMedia(
3426
3636
  options: {
3427
3637
  joinOptions?: any;
3428
- mediaSettings: any;
3429
- audioVideoOptions?: any;
3430
- } = {} as any
3638
+ mediaOptions?: AddMediaOptions;
3639
+ } = {}
3431
3640
  ) {
3432
- // TODO: add validations for parameters
3433
- const {mediaSettings, joinOptions, audioVideoOptions} = options;
3641
+ const {mediaOptions, joinOptions} = options;
3434
3642
 
3435
3643
  return this.join(joinOptions)
3436
3644
  .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
- )
3645
+ this.addMedia(mediaOptions).then((mediaResponse) => ({
3646
+ join: joinResponse,
3647
+ media: mediaResponse,
3648
+ }))
3448
3649
  )
3449
3650
  .catch((error) => {
3450
3651
  LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
@@ -3582,6 +3783,20 @@ export default class Meeting extends StatelessWebexPlugin {
3582
3783
  return false;
3583
3784
  }
3584
3785
 
3786
+ /**
3787
+ * Check if the meeting supports the Reactions
3788
+ * @returns {boolean}
3789
+ */
3790
+ isReactionsSupported() {
3791
+ if (this.locusInfo?.controls?.reactions.enabled) {
3792
+ return true;
3793
+ }
3794
+
3795
+ LoggerProxy.logger.error('Meeting:index#isReactionsSupported --> Reactions is not supported');
3796
+
3797
+ return false;
3798
+ }
3799
+
3585
3800
  /**
3586
3801
  * Monitor the Low-Latency Mercury (LLM) web socket connection on `onError` and `onClose` states
3587
3802
  * @private
@@ -3633,6 +3848,7 @@ export default class Meeting extends StatelessWebexPlugin {
3633
3848
  // @ts-ignore - fix type
3634
3849
  const {
3635
3850
  body: {webSocketUrl},
3851
+ // @ts-ignore
3636
3852
  } = await this.request({
3637
3853
  method: HTTP_VERBS.POST,
3638
3854
  uri: datachannelUrl,
@@ -3682,6 +3898,44 @@ export default class Meeting extends StatelessWebexPlugin {
3682
3898
  }
3683
3899
  }
3684
3900
 
3901
+ /**
3902
+ * Callback called when a relay event is received from meeting LLM Connection
3903
+ * @param {RelayEvent} e Event object coming from LLM Connection
3904
+ * @private
3905
+ * @returns {void}
3906
+ */
3907
+ private processRelayEvent = (e: RelayEvent): void => {
3908
+ switch (e.data.relayType) {
3909
+ case REACTION_RELAY_TYPES.REACTION:
3910
+ if (
3911
+ // @ts-ignore - config coming from registerPlugin
3912
+ (this.config.receiveReactions || options.receiveReactions) &&
3913
+ this.isReactionsSupported()
3914
+ ) {
3915
+ const {name} = this.members.membersCollection.get(e.data.sender.participantId);
3916
+ const processedReaction: ProcessedReaction = {
3917
+ reaction: e.data.reaction,
3918
+ sender: {
3919
+ id: e.data.sender.participantId,
3920
+ name,
3921
+ },
3922
+ };
3923
+ Trigger.trigger(
3924
+ this,
3925
+ {
3926
+ file: 'meeting/index',
3927
+ function: 'join',
3928
+ },
3929
+ EVENT_TRIGGERS.MEETING_RECEIVE_REACTIONS,
3930
+ processedReaction
3931
+ );
3932
+ }
3933
+ break;
3934
+ default:
3935
+ break;
3936
+ }
3937
+ };
3938
+
3685
3939
  /**
3686
3940
  * stop recieving Transcription by closing
3687
3941
  * the web socket connection properly
@@ -3753,9 +4007,16 @@ export default class Meeting extends StatelessWebexPlugin {
3753
4007
  joinSuccess = resolve;
3754
4008
  });
3755
4009
 
4010
+ if (options.correlationId) {
4011
+ this.setCorrelationId(options.correlationId);
4012
+ LoggerProxy.logger.log(
4013
+ `Meeting:index#join --> Using a new correlation id from app ${this.correlationId}`
4014
+ );
4015
+ }
4016
+
3756
4017
  if (!this.hasJoinedOnce) {
3757
4018
  this.hasJoinedOnce = true;
3758
- } else {
4019
+ } else if (!options.correlationId) {
3759
4020
  LoggerProxy.logger.log(
3760
4021
  `Meeting:index#join --> Generating a new correlation id for meeting ${this.id}`
3761
4022
  );
@@ -3776,6 +4037,21 @@ export default class Meeting extends StatelessWebexPlugin {
3776
4037
  data: {trigger: trigger.USER_INTERACTION, isRoapCallEnabled: true},
3777
4038
  });
3778
4039
 
4040
+ if (!isEmpty(this.meetingInfo)) {
4041
+ Metrics.postEvent({
4042
+ event: eventType.MEETING_INFO_REQUEST,
4043
+ meeting: this,
4044
+ });
4045
+
4046
+ Metrics.postEvent({
4047
+ event: eventType.MEETING_INFO_RESPONSE,
4048
+ meeting: this,
4049
+ data: {
4050
+ meetingLookupUrl: this.meetingInfo?.meetingLookupUrl,
4051
+ },
4052
+ });
4053
+ }
4054
+
3779
4055
  LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
3780
4056
 
3781
4057
  if (this.meetingFiniteStateMachine.state === MEETING_STATE_MACHINE.STATES.ENDED) {
@@ -3804,18 +4080,12 @@ export default class Meeting extends StatelessWebexPlugin {
3804
4080
  return Promise.reject(error);
3805
4081
  }
3806
4082
 
3807
- this.mediaProperties.setLocalQualityLevel(options.meetingQuality);
3808
4083
  this.mediaProperties.setRemoteQualityLevel(options.meetingQuality);
3809
4084
  }
3810
4085
 
3811
4086
  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`;
4087
+ if (!QUALITY_LEVELS[options.meetingQuality.remote]) {
4088
+ const errorMessage = `Meeting:index#join --> ${options.meetingQuality.remote} not defined`;
3819
4089
 
3820
4090
  LoggerProxy.logger.error(errorMessage);
3821
4091
 
@@ -3827,9 +4097,6 @@ export default class Meeting extends StatelessWebexPlugin {
3827
4097
  return Promise.reject(new Error(errorMessage));
3828
4098
  }
3829
4099
 
3830
- if (options.meetingQuality.local) {
3831
- this.mediaProperties.setLocalQualityLevel(options.meetingQuality.local);
3832
- }
3833
4100
  if (options.meetingQuality.remote) {
3834
4101
  this.mediaProperties.setRemoteQualityLevel(options.meetingQuality.remote);
3835
4102
  }
@@ -3855,6 +4122,7 @@ export default class Meeting extends StatelessWebexPlugin {
3855
4122
  return join;
3856
4123
  })
3857
4124
  .then(async (join) => {
4125
+ // @ts-ignore - config coming from registerPlugin
3858
4126
  if (this.config.enableAutomaticLLM) {
3859
4127
  await this.updateLLMConnection();
3860
4128
  }
@@ -3923,22 +4191,41 @@ export default class Meeting extends StatelessWebexPlugin {
3923
4191
  * @returns {Promise}
3924
4192
  */
3925
4193
  async updateLLMConnection() {
4194
+ // @ts-ignore - Fix type
3926
4195
  const {url, info: {datachannelUrl} = {}} = this.locusInfo;
3927
4196
 
3928
- const isJoined = this.joinedWith && this.joinedWith.state === 'JOINED';
4197
+ const isJoined = this.isJoined();
3929
4198
 
4199
+ // @ts-ignore - Fix type
3930
4200
  if (this.webex.internal.llm.isConnected()) {
4201
+ // @ts-ignore - Fix type
3931
4202
  if (url === this.webex.internal.llm.getLocusUrl() && isJoined) {
3932
4203
  return undefined;
3933
4204
  }
4205
+ // @ts-ignore - Fix type
3934
4206
  await this.webex.internal.llm.disconnectLLM();
4207
+ // @ts-ignore - Fix type
4208
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
3935
4209
  }
3936
4210
 
3937
4211
  if (!isJoined) {
3938
4212
  return undefined;
3939
4213
  }
3940
4214
 
3941
- return this.webex.internal.llm.registerAndConnect(url, datachannelUrl);
4215
+ // @ts-ignore - Fix type
4216
+ return this.webex.internal.llm
4217
+ .registerAndConnect(url, datachannelUrl)
4218
+ .then((registerAndConnectResult) => {
4219
+ // @ts-ignore - Fix type
4220
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
4221
+ // @ts-ignore - Fix type
4222
+ this.webex.internal.llm.on('event:relay.event', this.processRelayEvent);
4223
+ LoggerProxy.logger.info(
4224
+ 'Meeting:index#updateLLMConnection --> enabled to receive relay events!'
4225
+ );
4226
+
4227
+ return Promise.resolve(registerAndConnectResult);
4228
+ });
3942
4229
  }
3943
4230
 
3944
4231
  /**
@@ -3980,28 +4267,28 @@ export default class Meeting extends StatelessWebexPlugin {
3980
4267
 
3981
4268
  if (!this.dialInUrl) this.dialInUrl = `dialin:///${uuid.v4()}`;
3982
4269
 
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
- });
4270
+ return (
4271
+ this.meetingRequest
4272
+ // @ts-ignore
4273
+ .dialIn({
4274
+ correlationId,
4275
+ dialInUrl: this.dialInUrl,
4276
+ locusUrl,
4277
+ clientUrl: this.deviceUrl,
4278
+ })
4279
+ .catch((error) => {
4280
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_DIAL_IN_FAILURE, {
4281
+ correlation_id: this.correlationId,
4282
+ dial_in_url: this.dialInUrl,
4283
+ locus_id: locusUrl.split('/').pop(),
4284
+ client_url: this.deviceUrl,
4285
+ reason: error.error?.message,
4286
+ stack: error.stack,
4287
+ });
4002
4288
 
4003
- return Promise.reject(error);
4004
- });
4289
+ return Promise.reject(error);
4290
+ })
4291
+ );
4005
4292
  }
4006
4293
 
4007
4294
  /**
@@ -4018,29 +4305,29 @@ export default class Meeting extends StatelessWebexPlugin {
4018
4305
 
4019
4306
  if (!this.dialOutUrl) this.dialOutUrl = `dialout:///${uuid.v4()}`;
4020
4307
 
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
- });
4308
+ return (
4309
+ this.meetingRequest
4310
+ // @ts-ignore
4311
+ .dialOut({
4312
+ correlationId,
4313
+ dialOutUrl: this.dialOutUrl,
4314
+ phoneNumber,
4315
+ locusUrl,
4316
+ clientUrl: this.deviceUrl,
4317
+ })
4318
+ .catch((error) => {
4319
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_DIAL_OUT_FAILURE, {
4320
+ correlation_id: this.correlationId,
4321
+ dial_out_url: this.dialOutUrl,
4322
+ locus_id: locusUrl.split('/').pop(),
4323
+ client_url: this.deviceUrl,
4324
+ reason: error.error?.message,
4325
+ stack: error.stack,
4326
+ });
4041
4327
 
4042
- return Promise.reject(error);
4043
- });
4328
+ return Promise.reject(error);
4329
+ })
4330
+ );
4044
4331
  }
4045
4332
 
4046
4333
  /**
@@ -4116,14 +4403,10 @@ export default class Meeting extends StatelessWebexPlugin {
4116
4403
  },
4117
4404
  };
4118
4405
 
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();
4406
+ this.cleanupLocalTracks();
4125
4407
 
4126
- this.mediaProperties.unsetMediaTracks();
4408
+ this.mediaProperties.setMediaDirection(mediaSettings.mediaDirection);
4409
+ this.mediaProperties.unsetRemoteMedia();
4127
4410
 
4128
4411
  // 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
4412
  // once the DX answers we establish connection back the media server with only receiveShare enabled
@@ -4206,165 +4489,6 @@ export default class Meeting extends StatelessWebexPlugin {
4206
4489
  });
4207
4490
  }
4208
4491
 
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
4492
  /**
4369
4493
  * Handles ROAP_FAILURE event from the webrtc media connection
4370
4494
  *
@@ -4387,7 +4511,7 @@ export default class Meeting extends StatelessWebexPlugin {
4387
4511
  Metrics.sendBehavioralMetric(metricName, data, metadata);
4388
4512
  };
4389
4513
 
4390
- if (error instanceof MC.Errors.SdpOfferCreationError) {
4514
+ if (error instanceof Errors.SdpOfferCreationError) {
4391
4515
  sendBehavioralMetric(BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE, error, this.id);
4392
4516
 
4393
4517
  Metrics.postEvent({
@@ -4401,8 +4525,8 @@ export default class Meeting extends StatelessWebexPlugin {
4401
4525
  },
4402
4526
  });
4403
4527
  } else if (
4404
- error instanceof MC.Errors.SdpOfferHandlingError ||
4405
- error instanceof MC.Errors.SdpAnswerHandlingError
4528
+ error instanceof Errors.SdpOfferHandlingError ||
4529
+ error instanceof Errors.SdpAnswerHandlingError
4406
4530
  ) {
4407
4531
  sendBehavioralMetric(BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE, error, this.id);
4408
4532
 
@@ -4416,8 +4540,8 @@ export default class Meeting extends StatelessWebexPlugin {
4416
4540
  ],
4417
4541
  },
4418
4542
  });
4419
- } else if (error instanceof MC.Errors.SdpError) {
4420
- // this covers also the case of MC.Errors.IceGatheringError which extends MC.Errors.SdpError
4543
+ } else if (error instanceof Errors.SdpError) {
4544
+ // this covers also the case of Errors.IceGatheringError which extends Errors.SdpError
4421
4545
  sendBehavioralMetric(BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE, error, this.id);
4422
4546
 
4423
4547
  Metrics.postEvent({
@@ -4434,20 +4558,20 @@ export default class Meeting extends StatelessWebexPlugin {
4434
4558
  };
4435
4559
 
4436
4560
  setupMediaConnectionListeners = () => {
4437
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_STARTED, () => {
4561
+ this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_STARTED, () => {
4438
4562
  this.isRoapInProgress = true;
4439
4563
  });
4440
4564
 
4441
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_DONE, () => {
4565
+ this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_DONE, () => {
4442
4566
  this.mediaNegotiatedEvent();
4443
4567
  this.isRoapInProgress = false;
4444
4568
  this.processNextQueuedMediaUpdate();
4445
4569
  });
4446
4570
 
4447
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_FAILURE, this.handleRoapFailure);
4571
+ this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_FAILURE, this.handleRoapFailure);
4448
4572
 
4449
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.ROAP_MESSAGE_TO_SEND, (event) => {
4450
- const LOG_HEADER = 'Meeting:index#setupMediaConnectionListeners.ROAP_MESSAGE_TO_SEND -->';
4573
+ this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_MESSAGE_TO_SEND, (event) => {
4574
+ const LOG_HEADER = `Meeting:index#setupMediaConnectionListeners.ROAP_MESSAGE_TO_SEND --> correlationId=${this.correlationId}`;
4451
4575
 
4452
4576
  switch (event.roapMessage.messageType) {
4453
4577
  case 'OK':
@@ -4463,9 +4587,7 @@ export default class Meeting extends StatelessWebexPlugin {
4463
4587
  correlationId: this.correlationId,
4464
4588
  }),
4465
4589
  {
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, `,
4590
+ logText: `${LOG_HEADER} Roap OK`,
4469
4591
  }
4470
4592
  );
4471
4593
  break;
@@ -4485,9 +4607,7 @@ export default class Meeting extends StatelessWebexPlugin {
4485
4607
  reconnect: this.reconnectionManager.isReconnectInProgress(),
4486
4608
  }),
4487
4609
  {
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, `,
4610
+ logText: `${LOG_HEADER} Roap Offer`,
4491
4611
  }
4492
4612
  );
4493
4613
  break;
@@ -4506,9 +4626,7 @@ export default class Meeting extends StatelessWebexPlugin {
4506
4626
  correlationId: this.correlationId,
4507
4627
  }),
4508
4628
  {
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, `,
4629
+ logText: `${LOG_HEADER} Roap Answer`,
4512
4630
  }
4513
4631
  ).catch((error) => {
4514
4632
  const metricName = BEHAVIORAL_METRICS.ROAP_ANSWER_FAILURE;
@@ -4528,8 +4646,8 @@ export default class Meeting extends StatelessWebexPlugin {
4528
4646
 
4529
4647
  case 'ERROR':
4530
4648
  if (
4531
- event.roapMessage.errorType === MC.ErrorType.CONFLICT ||
4532
- event.roapMessage.errorType === MC.ErrorType.DOUBLECONFLICT
4649
+ event.roapMessage.errorType === ErrorType.CONFLICT ||
4650
+ event.roapMessage.errorType === ErrorType.DOUBLECONFLICT
4533
4651
  ) {
4534
4652
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_GLARE_CONDITION, {
4535
4653
  correlation_id: this.correlationId,
@@ -4545,9 +4663,7 @@ export default class Meeting extends StatelessWebexPlugin {
4545
4663
  correlationId: this.correlationId,
4546
4664
  }),
4547
4665
  {
4548
- header: `${LOG_HEADER} Send Roap Error.`,
4549
- success: `${LOG_HEADER} Successfully send roap error`,
4550
- failure: `${LOG_HEADER} Failed to send roap error, `,
4666
+ logText: `${LOG_HEADER} Roap Error (${event.roapMessage.errorType})`,
4551
4667
  }
4552
4668
  );
4553
4669
  break;
@@ -4561,7 +4677,7 @@ export default class Meeting extends StatelessWebexPlugin {
4561
4677
  });
4562
4678
 
4563
4679
  // eslint-disable-next-line no-param-reassign
4564
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.REMOTE_TRACK_ADDED, (event) => {
4680
+ this.mediaProperties.webrtcMediaConnection.on(Event.REMOTE_TRACK_ADDED, (event) => {
4565
4681
  LoggerProxy.logger.log(
4566
4682
  `Meeting:index#setupMediaConnectionListeners --> REMOTE_TRACK_ADDED event received for webrtcMediaConnection: ${JSON.stringify(
4567
4683
  event
@@ -4574,15 +4690,15 @@ export default class Meeting extends StatelessWebexPlugin {
4574
4690
  let eventType;
4575
4691
 
4576
4692
  switch (event.type) {
4577
- case MC.RemoteTrackType.AUDIO:
4693
+ case RemoteTrackType.AUDIO:
4578
4694
  eventType = EVENT_TYPES.REMOTE_AUDIO;
4579
4695
  this.mediaProperties.setRemoteAudioTrack(event.track);
4580
4696
  break;
4581
- case MC.RemoteTrackType.VIDEO:
4697
+ case RemoteTrackType.VIDEO:
4582
4698
  eventType = EVENT_TYPES.REMOTE_VIDEO;
4583
4699
  this.mediaProperties.setRemoteVideoTrack(event.track);
4584
4700
  break;
4585
- case MC.RemoteTrackType.SCREENSHARE_VIDEO:
4701
+ case RemoteTrackType.SCREENSHARE_VIDEO:
4586
4702
  if (event.track) {
4587
4703
  eventType = EVENT_TYPES.REMOTE_SHARE;
4588
4704
  this.mediaProperties.setRemoteShare(event.track);
@@ -4595,10 +4711,6 @@ export default class Meeting extends StatelessWebexPlugin {
4595
4711
  }
4596
4712
  }
4597
4713
 
4598
- // start stats here the stats are coming null if you dont receive streams
4599
-
4600
- this.statsAnalyzer.startAnalyzer(this.mediaProperties.webrtcMediaConnection);
4601
-
4602
4714
  if (eventType && mediaTrack) {
4603
4715
  Trigger.trigger(
4604
4716
  this,
@@ -4615,7 +4727,7 @@ export default class Meeting extends StatelessWebexPlugin {
4615
4727
  }
4616
4728
  });
4617
4729
 
4618
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.CONNECTION_STATE_CHANGED, (event) => {
4730
+ this.mediaProperties.webrtcMediaConnection.on(Event.CONNECTION_STATE_CHANGED, (event) => {
4619
4731
  const connectionFailed = () => {
4620
4732
  // we know the media connection failed and browser will not attempt to recover it any more
4621
4733
  // so reset the timer as it's not needed anymore, we want to reconnect immediately
@@ -4645,13 +4757,13 @@ export default class Meeting extends StatelessWebexPlugin {
4645
4757
  };
4646
4758
 
4647
4759
  LoggerProxy.logger.info(
4648
- `Meeting:index#setupMediaConnectionListeners --> connection state changed to ${event.state}`
4760
+ `Meeting:index#setupMediaConnectionListeners --> correlationId=${this.correlationId} connection state changed to ${event.state}`
4649
4761
  );
4650
4762
  switch (event.state) {
4651
- case MC.ConnectionState.Connecting:
4763
+ case ConnectionState.Connecting:
4652
4764
  Metrics.postEvent({event: eventType.ICE_START, meeting: this});
4653
4765
  break;
4654
- case MC.ConnectionState.Connected:
4766
+ case ConnectionState.Connected:
4655
4767
  Metrics.postEvent({event: eventType.ICE_END, meeting: this});
4656
4768
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_SUCCESS, {
4657
4769
  correlation_id: this.correlationId,
@@ -4659,8 +4771,9 @@ export default class Meeting extends StatelessWebexPlugin {
4659
4771
  });
4660
4772
  this.setNetworkStatus(NETWORK_STATUS.CONNECTED);
4661
4773
  this.reconnectionManager.iceReconnected();
4774
+ this.statsAnalyzer.startAnalyzer(this.mediaProperties.webrtcMediaConnection);
4662
4775
  break;
4663
- case MC.ConnectionState.Disconnected:
4776
+ case ConnectionState.Disconnected:
4664
4777
  this.setNetworkStatus(NETWORK_STATUS.DISCONNECTED);
4665
4778
  this.reconnectionManager.waitForIceReconnect().catch(() => {
4666
4779
  LoggerProxy.logger.info(
@@ -4670,7 +4783,7 @@ export default class Meeting extends StatelessWebexPlugin {
4670
4783
  connectionFailed();
4671
4784
  });
4672
4785
  break;
4673
- case MC.ConnectionState.Failed:
4786
+ case ConnectionState.Failed:
4674
4787
  connectionFailed();
4675
4788
  break;
4676
4789
  default:
@@ -4678,7 +4791,7 @@ export default class Meeting extends StatelessWebexPlugin {
4678
4791
  }
4679
4792
  });
4680
4793
 
4681
- this.mediaProperties.webrtcMediaConnection.on(MC.Event.ACTIVE_SPEAKERS_CHANGED, (msg) => {
4794
+ this.mediaProperties.webrtcMediaConnection.on(Event.ACTIVE_SPEAKERS_CHANGED, (msg) => {
4682
4795
  Trigger.trigger(
4683
4796
  this,
4684
4797
  {
@@ -4689,6 +4802,7 @@ export default class Meeting extends StatelessWebexPlugin {
4689
4802
  {
4690
4803
  seqNum: msg.seqNum,
4691
4804
  memberIds: msg.csis
4805
+ // @ts-ignore
4692
4806
  .map((csi) => this.members.findMemberByCsi(csi)?.id)
4693
4807
  .filter((item) => item !== undefined),
4694
4808
  }
@@ -4696,8 +4810,8 @@ export default class Meeting extends StatelessWebexPlugin {
4696
4810
  });
4697
4811
 
4698
4812
  this.mediaProperties.webrtcMediaConnection.on(
4699
- MC.Event.VIDEO_SOURCES_COUNT_CHANGED,
4700
- (numTotalSources, numLiveSources) => {
4813
+ Event.VIDEO_SOURCES_COUNT_CHANGED,
4814
+ (numTotalSources, numLiveSources, mediaContent) => {
4701
4815
  Trigger.trigger(
4702
4816
  this,
4703
4817
  {
@@ -4708,14 +4822,19 @@ export default class Meeting extends StatelessWebexPlugin {
4708
4822
  {
4709
4823
  numTotalSources,
4710
4824
  numLiveSources,
4825
+ mediaContent,
4711
4826
  }
4712
4827
  );
4828
+
4829
+ if (mediaContent === MediaContent.Main) {
4830
+ this.mediaRequestManagers.video.setNumCurrentSources(numTotalSources, numLiveSources);
4831
+ }
4713
4832
  }
4714
4833
  );
4715
4834
 
4716
4835
  this.mediaProperties.webrtcMediaConnection.on(
4717
- MC.Event.AUDIO_SOURCES_COUNT_CHANGED,
4718
- (numTotalSources, numLiveSources) => {
4836
+ Event.AUDIO_SOURCES_COUNT_CHANGED,
4837
+ (numTotalSources, numLiveSources, mediaContent) => {
4719
4838
  Trigger.trigger(
4720
4839
  this,
4721
4840
  {
@@ -4726,6 +4845,7 @@ export default class Meeting extends StatelessWebexPlugin {
4726
4845
  {
4727
4846
  numTotalSources,
4728
4847
  numLiveSources,
4848
+ mediaContent,
4729
4849
  }
4730
4850
  );
4731
4851
  }
@@ -4744,6 +4864,7 @@ export default class Meeting extends StatelessWebexPlugin {
4744
4864
  // Add ip address info if geo hint is present
4745
4865
  // @ts-ignore fix type
4746
4866
  options.data.intervalMetadata.peerReflexiveIP =
4867
+ // @ts-ignore
4747
4868
  this.webex.meetings.geoHintInfo?.clientAddress ||
4748
4869
  options.data.intervalMetadata.peerReflexiveIP ||
4749
4870
  MQA_STATS.DEFAULT_IP;
@@ -4813,7 +4934,15 @@ export default class Meeting extends StatelessWebexPlugin {
4813
4934
  return `MC-${this.id.substring(0, 4)}`;
4814
4935
  }
4815
4936
 
4816
- createMediaConnection(turnServerInfo) {
4937
+ /**
4938
+ * Creates a webrtc media connection and publishes tracks to it
4939
+ *
4940
+ * @param {Object} turnServerInfo TURN server information
4941
+ * @param {BundlePolicy} [bundlePolicy] Bundle policy settings
4942
+ * @returns {RoapMediaConnection | MultistreamRoapMediaConnection}
4943
+ */
4944
+ private async createMediaConnection(turnServerInfo, bundlePolicy?: BundlePolicy) {
4945
+ // create the actual media connection
4817
4946
  const mc = Media.createMediaConnection(this.isMultistream, this.getMediaConnectionDebugId(), {
4818
4947
  mediaProperties: this.mediaProperties,
4819
4948
  remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
@@ -4822,11 +4951,23 @@ export default class Meeting extends StatelessWebexPlugin {
4822
4951
  // @ts-ignore - config coming from registerPlugin
4823
4952
  enableExtmap: this.config.enableExtmap,
4824
4953
  turnServerInfo,
4954
+ bundlePolicy,
4825
4955
  });
4826
4956
 
4827
4957
  this.mediaProperties.setMediaPeerConnection(mc);
4828
4958
  this.setupMediaConnectionListeners();
4829
4959
 
4960
+ // publish the tracks
4961
+ if (this.mediaProperties.audioTrack) {
4962
+ await this.publishTrack(this.mediaProperties.audioTrack);
4963
+ }
4964
+ if (this.mediaProperties.videoTrack) {
4965
+ await this.publishTrack(this.mediaProperties.videoTrack);
4966
+ }
4967
+ if (this.mediaProperties.shareTrack) {
4968
+ await this.publishTrack(this.mediaProperties.shareTrack);
4969
+ }
4970
+
4830
4971
  return mc;
4831
4972
  }
4832
4973
 
@@ -4854,23 +4995,21 @@ export default class Meeting extends StatelessWebexPlugin {
4854
4995
  }
4855
4996
 
4856
4997
  /**
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
4998
+ * Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
4999
+ *
5000
+ * @param {AddMediaOptions} options
4864
5001
  * @returns {Promise}
4865
5002
  * @public
4866
5003
  * @memberof Meeting
4867
5004
  */
4868
- addMedia(options: any = {}) {
5005
+ addMedia(options: AddMediaOptions = {}) {
4869
5006
  const LOG_HEADER = 'Meeting:index#addMedia -->';
4870
5007
 
4871
5008
  let turnDiscoverySkippedReason;
4872
5009
  let turnServerUsed = false;
4873
5010
 
5011
+ LoggerProxy.logger.info(`${LOG_HEADER} called with: ${JSON.stringify(options)}`);
5012
+
4874
5013
  if (this.meetingState !== FULL_STATE.ACTIVE) {
4875
5014
  return Promise.reject(new MeetingNotActiveError());
4876
5015
  }
@@ -4884,9 +5023,14 @@ export default class Meeting extends StatelessWebexPlugin {
4884
5023
  return Promise.reject(new UserInLobbyError());
4885
5024
  }
4886
5025
 
4887
- const {localStream, localShare, mediaSettings, remoteMediaManagerConfig} = options;
4888
-
4889
- LoggerProxy.logger.info(`${LOG_HEADER} Adding Media.`);
5026
+ const {
5027
+ localTracks,
5028
+ audioEnabled = true,
5029
+ videoEnabled = true,
5030
+ receiveShare = true,
5031
+ remoteMediaManagerConfig,
5032
+ bundlePolicy,
5033
+ } = options;
4890
5034
 
4891
5035
  Metrics.postEvent({
4892
5036
  event: eventType.MEDIA_CAPABILITIES,
@@ -4911,17 +5055,61 @@ export default class Meeting extends StatelessWebexPlugin {
4911
5055
  },
4912
5056
  });
4913
5057
 
4914
- return MeetingUtil.validateOptions(options)
5058
+ // when audioEnabled/videoEnabled is true, we set sendAudio/sendVideo to true even before any tracks are published
5059
+ // to avoid doing an extra SDP exchange when they are published for the first time
5060
+ this.mediaProperties.setMediaDirection({
5061
+ sendAudio: audioEnabled,
5062
+ sendVideo: videoEnabled,
5063
+ sendShare: false,
5064
+ receiveAudio: audioEnabled,
5065
+ receiveVideo: videoEnabled,
5066
+ receiveShare,
5067
+ });
5068
+
5069
+ this.locusMediaRequest = new LocusMediaRequest(
5070
+ {
5071
+ correlationId: this.correlationId,
5072
+ device: {
5073
+ url: this.deviceUrl,
5074
+ // @ts-ignore
5075
+ deviceType: this.config.deviceType,
5076
+ },
5077
+ preferTranscoding: !this.isMultistream,
5078
+ },
5079
+ {
5080
+ // @ts-ignore
5081
+ parent: this.webex,
5082
+ }
5083
+ );
5084
+
5085
+ this.audio = createMuteState(AUDIO, this, audioEnabled);
5086
+ this.video = createMuteState(VIDEO, this, videoEnabled);
5087
+
5088
+ this.annotationInfo = localTracks?.annotationInfo;
5089
+
5090
+ const promises = [];
5091
+
5092
+ // setup all the references to local tracks in this.mediaProperties before creating media connection
5093
+ // and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages
5094
+ if (localTracks?.microphone) {
5095
+ promises.push(this.setLocalAudioTrack(localTracks.microphone));
5096
+ }
5097
+ if (localTracks?.camera) {
5098
+ promises.push(this.setLocalVideoTrack(localTracks.camera));
5099
+ }
5100
+ if (localTracks?.screenShare?.video) {
5101
+ promises.push(this.setLocalShareTrack(localTracks.screenShare.video));
5102
+ }
5103
+
5104
+ return Promise.all(promises)
4915
5105
  .then(() => this.roap.doTurnDiscovery(this, false))
4916
- .then((turnDiscoveryObject) => {
5106
+ .then(async (turnDiscoveryObject) => {
4917
5107
  ({turnDiscoverySkippedReason} = turnDiscoveryObject);
4918
5108
  turnServerUsed = !turnDiscoverySkippedReason;
4919
5109
 
4920
5110
  const {turnServerInfo} = turnDiscoveryObject;
4921
5111
 
4922
- this.preMedia(localStream, localShare, mediaSettings);
4923
-
4924
- const mc = this.createMediaConnection(turnServerInfo);
5112
+ const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
4925
5113
 
4926
5114
  if (this.isMultistream) {
4927
5115
  this.remoteMediaManager = new RemoteMediaManager(
@@ -4946,18 +5134,21 @@ export default class Meeting extends StatelessWebexPlugin {
4946
5134
  EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
4947
5135
  );
4948
5136
 
4949
- return this.remoteMediaManager.start().then(() => mc.initiateOffer());
5137
+ await this.remoteMediaManager.start();
4950
5138
  }
4951
5139
 
4952
- return mc.initiateOffer();
5140
+ await mc.initiateOffer();
4953
5141
  })
4954
5142
  .then(() => {
4955
5143
  this.setMercuryListener();
4956
5144
  })
4957
- .then(() =>
4958
- this.getDevices().then((devices) => {
4959
- MeetingUtil.handleDeviceLogging(devices);
4960
- })
5145
+ .then(
5146
+ () =>
5147
+ getDevices()
5148
+ .then((devices) => {
5149
+ MeetingUtil.handleDeviceLogging(devices);
5150
+ })
5151
+ .catch(() => {}) // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
4961
5152
  )
4962
5153
  .then(() => {
4963
5154
  this.handleMediaLogging(this.mediaProperties);
@@ -4967,8 +5158,12 @@ export default class Meeting extends StatelessWebexPlugin {
4967
5158
  if (this.config.stats.enableStatsAnalyzer) {
4968
5159
  // @ts-ignore - config coming from registerPlugin
4969
5160
  this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
4970
- // @ts-ignore - config coming from registerPlugin
4971
- this.statsAnalyzer = new StatsAnalyzer(this.config.stats, this.networkQualityMonitor);
5161
+ this.statsAnalyzer = new StatsAnalyzer(
5162
+ // @ts-ignore - config coming from registerPlugin
5163
+ this.config.stats,
5164
+ (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
5165
+ this.networkQualityMonitor
5166
+ );
4972
5167
  this.setupStatsAnalyzerEventHandlers();
4973
5168
  this.networkQualityMonitor.on(
4974
5169
  EVENT_TRIGGERS.NETWORK_QUALITY,
@@ -4991,7 +5186,7 @@ export default class Meeting extends StatelessWebexPlugin {
4991
5186
 
4992
5187
  // eslint-disable-next-line func-names
4993
5188
  // eslint-disable-next-line prefer-arrow-callback
4994
- if (this.type === _CALL_) {
5189
+ if (this.type === _CALL_ || this.meetingState === FULL_STATE.ACTIVE) {
4995
5190
  resolve();
4996
5191
  }
4997
5192
  const joiningTimer = setInterval(() => {
@@ -5010,21 +5205,15 @@ export default class Meeting extends StatelessWebexPlugin {
5010
5205
  )
5011
5206
  .then(() =>
5012
5207
  this.mediaProperties.waitForMediaConnectionConnected().catch(() => {
5013
- throw createMeetingsError(30202, 'Meeting connection failed');
5208
+ throw new Error(
5209
+ `Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
5210
+ );
5014
5211
  })
5015
5212
  )
5016
5213
  .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;
5214
+ if (localTracks?.screenShare?.video) {
5215
+ this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.SHARE_FLOOR_REQUEST);
5025
5216
  }
5026
-
5027
- return {};
5028
5217
  })
5029
5218
  .then(() => this.mediaProperties.getCurrentConnectionType())
5030
5219
  .then((connectionType) => {
@@ -5032,9 +5221,36 @@ export default class Meeting extends StatelessWebexPlugin {
5032
5221
  correlation_id: this.correlationId,
5033
5222
  locus_id: this.locusUrl.split('/').pop(),
5034
5223
  connectionType,
5224
+ isMultistream: this.isMultistream,
5035
5225
  });
5036
5226
  })
5037
5227
  .catch((error) => {
5228
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
5229
+ correlation_id: this.correlationId,
5230
+ locus_id: this.locusUrl.split('/').pop(),
5231
+ reason: error.message,
5232
+ stack: error.stack,
5233
+ code: error.code,
5234
+ turnDiscoverySkippedReason,
5235
+ turnServerUsed,
5236
+ isMultistream: this.isMultistream,
5237
+ signalingState:
5238
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5239
+ ?.signalingState ||
5240
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
5241
+ 'unknown',
5242
+ connectionState:
5243
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5244
+ ?.connectionState ||
5245
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.connectionState ||
5246
+ 'unknown',
5247
+ iceConnectionState:
5248
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5249
+ ?.iceConnectionState ||
5250
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
5251
+ 'unknown',
5252
+ });
5253
+
5038
5254
  // Clean up stats analyzer, peer connection, and turn off listeners
5039
5255
  const stopStatsAnalyzer = this.statsAnalyzer
5040
5256
  ? this.statsAnalyzer.stopAnalyzer()
@@ -5053,16 +5269,6 @@ export default class Meeting extends StatelessWebexPlugin {
5053
5269
  error
5054
5270
  );
5055
5271
 
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
5272
  // Upload logs on error while adding media
5067
5273
  Trigger.trigger(
5068
5274
  this,
@@ -5074,7 +5280,7 @@ export default class Meeting extends StatelessWebexPlugin {
5074
5280
  this
5075
5281
  );
5076
5282
 
5077
- if (error instanceof MC.Errors.SdpError) {
5283
+ if (error instanceof Errors.SdpError) {
5078
5284
  this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
5079
5285
  }
5080
5286
 
@@ -5102,7 +5308,9 @@ export default class Meeting extends StatelessWebexPlugin {
5102
5308
  * @private
5103
5309
  * @memberof Meeting
5104
5310
  */
5105
- private enqueueMediaUpdate(mediaUpdateType: string, options: object) {
5311
+ private enqueueMediaUpdate(mediaUpdateType: string, options: any = {}): Promise<void> {
5312
+ const canUpdateMediaNow = this.canUpdateMedia();
5313
+
5106
5314
  return new Promise((resolve, reject) => {
5107
5315
  const queueItem = {
5108
5316
  pendingPromiseResolve: resolve,
@@ -5115,6 +5323,10 @@ export default class Meeting extends StatelessWebexPlugin {
5115
5323
  `Meeting:index#enqueueMediaUpdate --> enqueuing media update type=${mediaUpdateType}`
5116
5324
  );
5117
5325
  this.queuedMediaUpdates.push(queueItem);
5326
+
5327
+ if (canUpdateMediaNow) {
5328
+ this.processNextQueuedMediaUpdate();
5329
+ }
5118
5330
  });
5119
5331
  }
5120
5332
 
@@ -5154,18 +5366,17 @@ export default class Meeting extends StatelessWebexPlugin {
5154
5366
  LoggerProxy.logger.log(
5155
5367
  `Meeting:index#processNextQueuedMediaUpdate --> performing delayed media update type=${mediaUpdateType}`
5156
5368
  );
5369
+ let mediaUpdate = Promise.resolve();
5370
+
5157
5371
  switch (mediaUpdateType) {
5158
- case MEDIA_UPDATE_TYPE.ALL:
5159
- this.updateMedia(options).then(pendingPromiseResolve, pendingPromiseReject);
5160
- break;
5161
- case MEDIA_UPDATE_TYPE.AUDIO:
5162
- this.updateAudio(options).then(pendingPromiseResolve, pendingPromiseReject);
5372
+ case MEDIA_UPDATE_TYPE.TRANSCODED_MEDIA_CONNECTION:
5373
+ mediaUpdate = this.updateTranscodedMediaConnection();
5163
5374
  break;
5164
- case MEDIA_UPDATE_TYPE.VIDEO:
5165
- this.updateVideo(options).then(pendingPromiseResolve, pendingPromiseReject);
5375
+ case MEDIA_UPDATE_TYPE.SHARE_FLOOR_REQUEST:
5376
+ mediaUpdate = this.requestScreenShareFloor();
5166
5377
  break;
5167
- case MEDIA_UPDATE_TYPE.SHARE:
5168
- this.updateShare(options).then(pendingPromiseResolve, pendingPromiseReject);
5378
+ case MEDIA_UPDATE_TYPE.UPDATE_MEDIA:
5379
+ mediaUpdate = this.updateMedia(options);
5169
5380
  break;
5170
5381
  default:
5171
5382
  LoggerProxy.logger.error(
@@ -5173,358 +5384,113 @@ export default class Meeting extends StatelessWebexPlugin {
5173
5384
  );
5174
5385
  break;
5175
5386
  }
5387
+
5388
+ mediaUpdate
5389
+ .then(pendingPromiseResolve, pendingPromiseReject)
5390
+ .then(() => this.processNextQueuedMediaUpdate());
5176
5391
  }
5177
5392
  };
5178
5393
 
5179
5394
  /**
5180
- * A confluence of updateAudio, updateVideo, and updateShare
5181
- * this function re-establishes all of the media streams with new options
5395
+ * Updates the media connection - it allows to enable/disable all audio/video/share in the meeting.
5396
+ * This does not affect the published tracks, so for example if a microphone track is published and
5397
+ * updateMedia({audioEnabled: false}) is called, the audio will not be sent or received anymore,
5398
+ * but the track's "published" state is not changed and when updateMedia({audioEnabled: true}) is called,
5399
+ * the sending of the audio from the same track will resume.
5400
+ *
5182
5401
  * @param {Object} options
5183
- * @param {MediaStream} options.localStream
5184
- * @param {MediaStream} options.localShare
5185
- * @param {MediaDirection} options.mediaSettings
5402
+ * @param {boolean} options.audioEnabled [optional] enables/disables receiving and sending of main audio in the meeting
5403
+ * @param {boolean} options.videoEnabled [optional] enables/disables receiving and sending of main video in the meeting
5404
+ * @param {boolean} options.shareEnabled [optional] enables/disables receiving and sending of screen share in the meeting
5186
5405
  * @returns {Promise}
5187
5406
  * @public
5188
5407
  * @memberof Meeting
5189
5408
  */
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 -->';
5409
+ public async updateMedia(options: {
5410
+ audioEnabled?: boolean;
5411
+ videoEnabled?: boolean;
5412
+ receiveShare?: boolean;
5413
+ }) {
5414
+ this.checkMediaConnection();
5198
5415
 
5199
- if (!this.canUpdateMedia()) {
5200
- return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.ALL, options);
5201
- }
5202
- const {localStream, localShare, mediaSettings} = options;
5416
+ const {audioEnabled, videoEnabled, receiveShare} = options;
5203
5417
 
5204
- const previousSendShareStatus = this.mediaProperties.mediaDirection.sendShare;
5418
+ LoggerProxy.logger.log(
5419
+ `Meeting:index#updateMedia --> called with options=${JSON.stringify(options)}`
5420
+ );
5205
5421
 
5206
- if (!this.mediaProperties.webrtcMediaConnection) {
5207
- return Promise.reject(new Error('media connection not established, call addMedia() first'));
5422
+ if (!this.canUpdateMedia()) {
5423
+ return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.UPDATE_MEDIA, options);
5208
5424
  }
5209
5425
 
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
- }
5426
+ if (this.isMultistream) {
5427
+ if (videoEnabled !== undefined) {
5428
+ throw new Error(
5429
+ 'enabling/disabling video in a meeting is not supported for multistream, it can only be done upfront when calling addMedia()'
5430
+ );
5431
+ }
5267
5432
 
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);
5433
+ if (receiveShare !== undefined) {
5434
+ throw new Error(
5435
+ 'toggling receiveShare in a multistream meeting is not supported, to control receiving screen share call meeting.remoteMediaManager.setLayout() with appropriate layout'
5436
+ );
5437
+ }
5288
5438
  }
5289
- const {sendAudio, receiveAudio, stream} = options;
5290
- let track = MeetingUtil.getTrack(stream).audioTrack;
5291
5439
 
5292
- if (typeof sendAudio !== 'boolean' || typeof receiveAudio !== 'boolean') {
5293
- return Promise.reject(new ParameterError('Pass sendAudio and receiveAudio parameter'));
5440
+ if (audioEnabled !== undefined) {
5441
+ this.mediaProperties.mediaDirection.sendAudio = audioEnabled;
5442
+ this.mediaProperties.mediaDirection.receiveAudio = audioEnabled;
5443
+ this.audio.enable(this, audioEnabled);
5294
5444
  }
5295
5445
 
5296
- if (!this.mediaProperties.webrtcMediaConnection) {
5297
- return Promise.reject(new Error('media connection not established, call addMedia() first'));
5446
+ if (videoEnabled !== undefined) {
5447
+ this.mediaProperties.mediaDirection.sendVideo = videoEnabled;
5448
+ this.mediaProperties.mediaDirection.receiveVideo = videoEnabled;
5449
+ this.video.enable(this, videoEnabled);
5298
5450
  }
5299
5451
 
5300
- if (this.effects && this.effects.state) {
5301
- const bnrEnabled = this.effects.state.bnr.enabled;
5452
+ if (receiveShare !== undefined) {
5453
+ this.mediaProperties.mediaDirection.receiveShare = receiveShare;
5454
+ }
5302
5455
 
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');
5456
+ if (this.isMultistream) {
5457
+ if (audioEnabled !== undefined) {
5458
+ await this.mediaProperties.webrtcMediaConnection.enableMultistreamAudio(audioEnabled);
5311
5459
  }
5460
+ } else {
5461
+ await this.updateTranscodedMediaConnection();
5312
5462
  }
5313
5463
 
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
- });
5464
+ return undefined;
5336
5465
  }
5337
5466
 
5338
5467
  /**
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}
5468
+ * Acknowledge the meeting, outgoing or incoming
5469
+ * @param {String} type
5470
+ * @returns {Promise} resolve {message, ringing, response}
5348
5471
  * @public
5349
5472
  * @memberof Meeting
5350
5473
  */
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'));
5474
+ public acknowledge(type: string) {
5475
+ if (!type) {
5476
+ return Promise.reject(new ParameterError('Type must be set to acknowledge the meeting.'));
5360
5477
  }
5478
+ if (type === _INCOMING_) {
5479
+ return this.meetingRequest
5480
+ .acknowledgeMeeting({
5481
+ locusUrl: this.locusUrl,
5482
+ deviceUrl: this.deviceUrl,
5483
+ correlationId: this.correlationId,
5484
+ })
5485
+ .then((response) => Promise.resolve(response))
5486
+ .then((response) => {
5487
+ this.meetingFiniteStateMachine.ring(type);
5488
+ Metrics.postEvent({event: eventType.ALERT_DISPLAYED, meeting: this});
5361
5489
 
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
- });
5490
+ return Promise.resolve({
5491
+ response,
5492
+ });
5493
+ });
5528
5494
  }
5529
5495
 
5530
5496
  // TODO: outside of 1:1 incoming, and all outgoing calls
@@ -5563,13 +5529,12 @@ export default class Meeting extends StatelessWebexPlugin {
5563
5529
  * @memberof Meeting
5564
5530
  */
5565
5531
  public leave(options: {resourceId?: string; reason?: any} = {} as any) {
5532
+ const leaveReason = options.reason || MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST;
5566
5533
  Metrics.postEvent({
5567
5534
  event: eventType.LEAVE,
5568
5535
  meeting: this,
5569
- data: {trigger: trigger.USER_INTERACTION, canProceed: false},
5536
+ data: {trigger: trigger.USER_INTERACTION, canProceed: false, reason: leaveReason},
5570
5537
  });
5571
- const leaveReason = options.reason || MEETING_REMOVED_REASON.CLIENT_LEAVE_REQUEST;
5572
-
5573
5538
  LoggerProxy.logger.log('Meeting:index#leave --> Leaving a meeting');
5574
5539
 
5575
5540
  return MeetingUtil.leaveMeeting(this, options)
@@ -5738,55 +5703,68 @@ export default class Meeting extends StatelessWebexPlugin {
5738
5703
  * @memberof Meeting
5739
5704
  */
5740
5705
  private requestScreenShareFloor() {
5741
- const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
5706
+ if (!this.mediaProperties.shareTrack || !this.mediaProperties.mediaDirection.sendShare) {
5707
+ LoggerProxy.logger.log(
5708
+ `Meeting:index#requestScreenShareFloor --> NOT requesting floor, because we don't have the share track anymore (shareTrack=${
5709
+ this.mediaProperties.shareTrack ? 'yes' : 'no'
5710
+ }, sendShare=${this.mediaProperties.mediaDirection.sendShare})`
5711
+ );
5742
5712
 
5743
- if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
5744
- Metrics.postEvent({event: eventType.SHARE_INITIATED, meeting: this});
5713
+ return Promise.resolve({});
5714
+ }
5715
+ if (this.state === MEETING_STATE.STATES.JOINED) {
5716
+ const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
5717
+
5718
+ if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
5719
+ Metrics.postEvent({event: eventType.SHARE_INITIATED, meeting: this});
5720
+
5721
+ return this.meetingRequest
5722
+ .changeMeetingFloor({
5723
+ disposition: FLOOR_ACTION.GRANTED,
5724
+ personUrl: this.locusInfo.self.url,
5725
+ deviceUrl: this.deviceUrl,
5726
+ uri: content.url,
5727
+ resourceUrl: this.resourceUrl,
5728
+ annotationInfo: this.annotationInfo,
5729
+ })
5730
+ .then(() => {
5731
+ this.isSharing = true;
5745
5732
 
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;
5733
+ return Promise.resolve();
5734
+ })
5735
+ .catch((error) => {
5736
+ LoggerProxy.logger.error('Meeting:index#share --> Error ', error);
5756
5737
 
5757
- return Promise.resolve();
5758
- })
5759
- .catch((error) => {
5760
- LoggerProxy.logger.error('Meeting:index#share --> Error ', error);
5738
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_FAILURE, {
5739
+ correlation_id: this.correlationId,
5740
+ locus_id: this.locusUrl.split('/').pop(),
5741
+ reason: error.message,
5742
+ stack: error.stack,
5743
+ });
5761
5744
 
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,
5745
+ return Promise.reject(error);
5767
5746
  });
5747
+ }
5768
5748
 
5769
- return Promise.reject(error);
5770
- });
5749
+ return Promise.reject(new ParameterError('Cannot share without content.'));
5771
5750
  }
5751
+ this.floorGrantPending = true;
5772
5752
 
5773
- return Promise.reject(new ParameterError('Cannot share without content.'));
5753
+ return Promise.resolve({});
5774
5754
  }
5775
5755
 
5776
5756
  /**
5777
- * Stops the screen share
5778
- * @returns {Promise} see #updateShare
5779
- * @public
5780
- * @memberof Meeting
5757
+ * Requests screen share floor if such request is pending.
5758
+ * It should be called whenever meeting state changes to JOINED
5759
+ *
5760
+ * @returns {void}
5781
5761
  */
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
- });
5762
+ private requestScreenShareFloorIfPending() {
5763
+ if (this.floorGrantPending && this.state === MEETING_STATE.STATES.JOINED) {
5764
+ this.requestScreenShareFloor().then(() => {
5765
+ this.floorGrantPending = false;
5766
+ });
5767
+ }
5790
5768
  }
5791
5769
 
5792
5770
  /**
@@ -5798,11 +5776,10 @@ export default class Meeting extends StatelessWebexPlugin {
5798
5776
  private releaseScreenShareFloor() {
5799
5777
  const content = this.locusInfo.mediaShares.find((element) => element.name === CONTENT);
5800
5778
 
5801
- if (content && this.mediaProperties.mediaDirection.sendShare) {
5779
+ if (content) {
5802
5780
  Metrics.postEvent({event: eventType.SHARE_STOPPED, meeting: this});
5803
- Media.stopTracks(this.mediaProperties.shareTrack);
5804
5781
 
5805
- if (content.floor.beneficiary.id !== this.selfId) {
5782
+ if (content.floor?.beneficiary.id !== this.selfId) {
5806
5783
  // remote participant started sharing and caused our sharing to stop, we don't want to send any floor action request in that case
5807
5784
  this.isSharing = false;
5808
5785
 
@@ -5834,7 +5811,10 @@ export default class Meeting extends StatelessWebexPlugin {
5834
5811
  });
5835
5812
  }
5836
5813
 
5837
- return Promise.reject(new ParameterError('Cannot stop share without content'));
5814
+ // according to Locus there is no content, so we don't need to release the floor (it's probably already been released)
5815
+ this.isSharing = false;
5816
+
5817
+ return Promise.resolve();
5838
5818
  }
5839
5819
 
5840
5820
  /**
@@ -5844,7 +5824,50 @@ export default class Meeting extends StatelessWebexPlugin {
5844
5824
  * @memberof Meeting
5845
5825
  */
5846
5826
  public startRecording() {
5847
- return MeetingUtil.startRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
5827
+ return this.recordingController.startRecording();
5828
+ }
5829
+
5830
+ /**
5831
+ * set the mute on entry flag for participants if you're the host
5832
+ * @returns {Promise}
5833
+ * @param {boolean} enabled
5834
+ * @public
5835
+ * @memberof Meeting
5836
+ */
5837
+ public setMuteOnEntry(enabled: boolean) {
5838
+ return this.controlsOptionsManager.setMuteOnEntry(enabled);
5839
+ }
5840
+
5841
+ /**
5842
+ * set the disallow unmute flag for participants if you're the host
5843
+ * @returns {Promise}
5844
+ * @param {boolean} enabled
5845
+ * @public
5846
+ * @memberof Meeting
5847
+ */
5848
+ public setDisallowUnmute(enabled: boolean) {
5849
+ return this.controlsOptionsManager.setDisallowUnmute(enabled);
5850
+ }
5851
+
5852
+ /**
5853
+ * set the mute all flag for participants if you're the host
5854
+ * @returns {Promise}
5855
+ * @param {boolean} mutedEnabled
5856
+ * @param {boolean} disallowUnmuteEnabled
5857
+ * @param {boolean} muteOnEntryEnabled
5858
+ * @public
5859
+ * @memberof Meeting
5860
+ */
5861
+ public setMuteAll(
5862
+ mutedEnabled: boolean,
5863
+ disallowUnmuteEnabled: boolean,
5864
+ muteOnEntryEnabled: boolean
5865
+ ) {
5866
+ return this.controlsOptionsManager.setMuteAll(
5867
+ mutedEnabled,
5868
+ disallowUnmuteEnabled,
5869
+ muteOnEntryEnabled
5870
+ );
5848
5871
  }
5849
5872
 
5850
5873
  /**
@@ -5854,7 +5877,7 @@ export default class Meeting extends StatelessWebexPlugin {
5854
5877
  * @memberof Meeting
5855
5878
  */
5856
5879
  public stopRecording() {
5857
- return MeetingUtil.stopRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
5880
+ return this.recordingController.stopRecording();
5858
5881
  }
5859
5882
 
5860
5883
  /**
@@ -5864,7 +5887,7 @@ export default class Meeting extends StatelessWebexPlugin {
5864
5887
  * @memberof Meeting
5865
5888
  */
5866
5889
  public pauseRecording() {
5867
- return MeetingUtil.pauseRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
5890
+ return this.recordingController.pauseRecording();
5868
5891
  }
5869
5892
 
5870
5893
  /**
@@ -5874,7 +5897,7 @@ export default class Meeting extends StatelessWebexPlugin {
5874
5897
  * @memberof Meeting
5875
5898
  */
5876
5899
  public resumeRecording() {
5877
- return MeetingUtil.resumeRecording(this.meetingRequest, this.locusUrl, this.locusInfo);
5900
+ return this.recordingController.resumeRecording();
5878
5901
  }
5879
5902
 
5880
5903
  /**
@@ -6048,11 +6071,6 @@ export default class Meeting extends StatelessWebexPlugin {
6048
6071
  main: layoutInfo.main,
6049
6072
  content: layoutInfo.content,
6050
6073
  })
6051
- .then((response) => {
6052
- if (response && response.body && response.body.locus) {
6053
- this.locusInfo.onFullLocus(response.body.locus);
6054
- }
6055
- })
6056
6074
  .catch((error) => {
6057
6075
  LoggerProxy.logger.error('Meeting:index#changeVideoLayout --> Error ', error);
6058
6076
 
@@ -6060,62 +6078,6 @@ export default class Meeting extends StatelessWebexPlugin {
6060
6078
  });
6061
6079
  }
6062
6080
 
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
6081
  /**
6120
6082
  * Sets the quality level of the remote incoming media
6121
6083
  * @param {String} level {LOW|MEDIUM|HIGH}
@@ -6151,129 +6113,7 @@ export default class Meeting extends StatelessWebexPlugin {
6151
6113
  // Set the quality level in properties
6152
6114
  this.mediaProperties.setRemoteQualityLevel(level);
6153
6115
 
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
- });
6116
+ return this.updateTranscodedMediaConnection();
6277
6117
  }
6278
6118
 
6279
6119
  /**
@@ -6283,20 +6123,18 @@ export default class Meeting extends StatelessWebexPlugin {
6283
6123
  * @param {MediaStream} localShare
6284
6124
  * @returns {undefined}
6285
6125
  */
6286
- private handleShareTrackEnded(localShare: MediaStream) {
6126
+ private handleShareTrackEnded = async () => {
6287
6127
  if (this.wirelessShare) {
6288
6128
  this.leave({reason: MEETING_REMOVED_REASON.USER_ENDED_SHARE_STREAMS});
6289
6129
  } else {
6290
- // Skip checking for a stable peerConnection
6291
- // to allow immediately stopping screenshare
6292
- this.stopShare({
6293
- skipSignalingCheck: true,
6294
- }).catch((error) => {
6130
+ try {
6131
+ await this.unpublishTracks([this.mediaProperties.shareTrack]); // todo: screen share audio (SPARK-399690)
6132
+ } catch (error) {
6295
6133
  LoggerProxy.logger.log(
6296
6134
  'Meeting:index#handleShareTrackEnded --> Error stopping share: ',
6297
6135
  error
6298
6136
  );
6299
- });
6137
+ }
6300
6138
  }
6301
6139
 
6302
6140
  Trigger.trigger(
@@ -6307,11 +6145,10 @@ export default class Meeting extends StatelessWebexPlugin {
6307
6145
  },
6308
6146
  EVENT_TRIGGERS.MEETING_STOPPED_SHARING_LOCAL,
6309
6147
  {
6310
- type: EVENT_TYPES.LOCAL_SHARE,
6311
- stream: localShare,
6148
+ reason: SHARE_STOPPED_REASON.TRACK_ENDED,
6312
6149
  }
6313
6150
  );
6314
- }
6151
+ };
6315
6152
 
6316
6153
  /**
6317
6154
  * Emits the 'network:quality' event
@@ -6341,14 +6178,16 @@ export default class Meeting extends StatelessWebexPlugin {
6341
6178
 
6342
6179
  /**
6343
6180
  * Handle logging the media
6344
- * @param {Object} audioTrack The audio track
6345
- * @param {Object} videoTrack The video track
6181
+ * @param {Object} mediaProperties
6346
6182
  * @private
6347
6183
  * @returns {undefined}
6348
6184
  */
6349
- private handleMediaLogging({audioTrack, videoTrack}: any) {
6350
- MeetingUtil.handleVideoLogging(videoTrack);
6351
- MeetingUtil.handleAudioLogging(audioTrack);
6185
+ private handleMediaLogging(mediaProperties: {
6186
+ audioTrack?: LocalMicrophoneTrack;
6187
+ videoTrack?: LocalCameraTrack;
6188
+ }) {
6189
+ MeetingUtil.handleVideoLogging(mediaProperties.videoTrack);
6190
+ MeetingUtil.handleAudioLogging(mediaProperties.audioTrack);
6352
6191
  }
6353
6192
 
6354
6193
  /**
@@ -6437,7 +6276,7 @@ export default class Meeting extends StatelessWebexPlugin {
6437
6276
  const end = this.endLocalSDPGenRemoteSDPRecvDelay;
6438
6277
 
6439
6278
  if (start && end) {
6440
- const calculatedDelay = end - start;
6279
+ const calculatedDelay = Math.round(end - start);
6441
6280
 
6442
6281
  return calculatedDelay > METRICS_JOIN_TIMES_MAX_DURATION ? undefined : calculatedDelay;
6443
6282
  }
@@ -6449,26 +6288,26 @@ export default class Meeting extends StatelessWebexPlugin {
6449
6288
  *
6450
6289
  * @returns {undefined}
6451
6290
  */
6452
- setStartCallInitiateJoinReq() {
6453
- this.startCallInitiateJoinReq = performance.now();
6454
- this.endCallInitiateJoinReq = undefined;
6291
+ setStartCallInitJoinReq() {
6292
+ this.startCallInitJoinReq = performance.now();
6293
+ this.endCallInitJoinReq = undefined;
6455
6294
  }
6456
6295
 
6457
6296
  /**
6458
6297
  *
6459
6298
  * @returns {undefined}
6460
6299
  */
6461
- setEndCallInitiateJoinReq() {
6462
- this.endCallInitiateJoinReq = performance.now();
6300
+ setEndCallInitJoinReq() {
6301
+ this.endCallInitJoinReq = performance.now();
6463
6302
  }
6464
6303
 
6465
6304
  /**
6466
6305
  *
6467
6306
  * @returns {string} duration between call initiate and sending join request to locus
6468
6307
  */
6469
- getCallInitiateJoinReq() {
6470
- const start = this.startCallInitiateJoinReq;
6471
- const end = this.endCallInitiateJoinReq;
6308
+ getCallInitJoinReq() {
6309
+ const start = this.startCallInitJoinReq;
6310
+ const end = this.endCallInitJoinReq;
6472
6311
 
6473
6312
  if (start && end) {
6474
6313
  const calculatedDelay = end - start;
@@ -6505,7 +6344,7 @@ export default class Meeting extends StatelessWebexPlugin {
6505
6344
  const end = this.endJoinReqResp;
6506
6345
 
6507
6346
  if (start && end) {
6508
- const calculatedDelay = end - start;
6347
+ const calculatedDelay = Math.round(end - start);
6509
6348
 
6510
6349
  return calculatedDelay > METRICS_JOIN_TIMES_MAX_DURATION ? undefined : calculatedDelay;
6511
6350
  }
@@ -6518,10 +6357,45 @@ export default class Meeting extends StatelessWebexPlugin {
6518
6357
  * @returns {string} duration between call initiate and successful locus join (even if it is in lobby)
6519
6358
  */
6520
6359
  getTotalJmt() {
6521
- const start = this.startCallInitiateJoinReq;
6360
+ const start = this.startCallInitJoinReq;
6522
6361
  const end = this.endJoinReqResp;
6523
6362
 
6524
- return start && end ? end - start : undefined;
6363
+ return start && end ? Math.round(end - start) : undefined;
6364
+ }
6365
+
6366
+ /**
6367
+ *
6368
+ * @returns {string} one of 'attendee','host','cohost', returns the user type of the current user
6369
+ */
6370
+ getCurUserType() {
6371
+ const {roles} = this;
6372
+ if (roles) {
6373
+ if (roles.includes(SELF_ROLES.MODERATOR)) {
6374
+ return 'host';
6375
+ }
6376
+ if (roles.includes(SELF_ROLES.COHOST)) {
6377
+ return 'cohost';
6378
+ }
6379
+ if (roles.includes(SELF_ROLES.ATTENDEE)) {
6380
+ return 'attendee';
6381
+ }
6382
+ }
6383
+
6384
+ return null;
6385
+ }
6386
+
6387
+ /**
6388
+ *
6389
+ * @returns {string} one of 'login-ci','unverified-guest', returns the login type of the current user
6390
+ */
6391
+ getCurLoginType() {
6392
+ // @ts-ignore
6393
+ if (this.webex.canAuthorize) {
6394
+ // @ts-ignore
6395
+ return this.webex.credentials.isUnverifiedGuest ? 'unverified-guest' : 'login-ci';
6396
+ }
6397
+
6398
+ return null;
6525
6399
  }
6526
6400
 
6527
6401
  /**
@@ -6611,121 +6485,6 @@ export default class Meeting extends StatelessWebexPlugin {
6611
6485
  }
6612
6486
  };
6613
6487
 
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
6488
  /**
6730
6489
  * starts keepAlives being sent
6731
6490
  * @returns {void}
@@ -6791,13 +6550,13 @@ export default class Meeting extends StatelessWebexPlugin {
6791
6550
  /**
6792
6551
  * Send a reaction inside the meeting.
6793
6552
  *
6794
- * @param {ReactionType} reactionType - type of reaction to be sent. Example: "thumbs_up"
6553
+ * @param {ReactionServerType} reactionType - type of reaction to be sent. Example: "thumbs_up"
6795
6554
  * @param {SkinToneType} skinToneType - skin tone for the reaction. Example: "medium_dark"
6796
6555
  * @returns {Promise}
6797
6556
  * @public
6798
6557
  * @memberof Meeting
6799
6558
  */
6800
- public sendReaction(reactionType: ReactionType, skinToneType?: SkinToneType) {
6559
+ public sendReaction(reactionType: ReactionServerType, skinToneType?: SkinToneType) {
6801
6560
  const reactionChannelUrl = this.locusInfo?.controls?.reactions?.reactionChannelUrl as string;
6802
6561
  const participantId = this.members.selfId;
6803
6562
 
@@ -6840,4 +6599,222 @@ export default class Meeting extends StatelessWebexPlugin {
6840
6599
  requestingParticipantId: this.members.selfId,
6841
6600
  });
6842
6601
  }
6602
+
6603
+ /**
6604
+ * Throws if we don't have a media connection created
6605
+ *
6606
+ * @returns {void}
6607
+ */
6608
+ private checkMediaConnection() {
6609
+ if (this.mediaProperties?.webrtcMediaConnection) {
6610
+ return;
6611
+ }
6612
+ throw new NoMediaEstablishedYetError();
6613
+ }
6614
+
6615
+ /**
6616
+ * Method to enable or disable the 'Music mode' effect on audio track
6617
+ *
6618
+ * @param {boolean} shouldEnableMusicMode
6619
+ * @returns {Promise}
6620
+ */
6621
+ async enableMusicMode(shouldEnableMusicMode: boolean) {
6622
+ this.checkMediaConnection();
6623
+
6624
+ if (!this.isMultistream) {
6625
+ throw new Error('enableMusicMode() only supported with multistream');
6626
+ }
6627
+
6628
+ if (shouldEnableMusicMode) {
6629
+ await this.mediaProperties.webrtcMediaConnection.setCodecParameters(MediaType.AudioMain, {
6630
+ maxaveragebitrate: '64000',
6631
+ maxplaybackrate: '48000',
6632
+ });
6633
+ } else {
6634
+ await this.mediaProperties.webrtcMediaConnection.deleteCodecParameters(MediaType.AudioMain, [
6635
+ 'maxaveragebitrate',
6636
+ 'maxplaybackrate',
6637
+ ]);
6638
+ }
6639
+ }
6640
+
6641
+ /** Updates the tracks being sent on the transcoded media connection
6642
+ *
6643
+ * @returns {Promise<void>}
6644
+ */
6645
+ private updateTranscodedMediaConnection(): Promise<void> {
6646
+ const LOG_HEADER = 'Meeting:index#updateTranscodedMediaConnection -->';
6647
+
6648
+ LoggerProxy.logger.info(`${LOG_HEADER} starting`);
6649
+
6650
+ if (!this.canUpdateMedia()) {
6651
+ return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.TRANSCODED_MEDIA_CONNECTION);
6652
+ }
6653
+
6654
+ return this.mediaProperties.webrtcMediaConnection
6655
+ .update({
6656
+ localTracks: {
6657
+ audio: this.mediaProperties.audioTrack?.underlyingTrack || null,
6658
+ video: this.mediaProperties.videoTrack?.underlyingTrack || null,
6659
+ screenShareVideo: this.mediaProperties.shareTrack?.underlyingTrack || null,
6660
+ },
6661
+ direction: {
6662
+ audio: Media.getDirection(
6663
+ true,
6664
+ this.mediaProperties.mediaDirection.receiveAudio,
6665
+ this.mediaProperties.mediaDirection.sendAudio
6666
+ ),
6667
+ video: Media.getDirection(
6668
+ true,
6669
+ this.mediaProperties.mediaDirection.receiveVideo,
6670
+ this.mediaProperties.mediaDirection.sendVideo
6671
+ ),
6672
+ screenShareVideo: Media.getDirection(
6673
+ false,
6674
+ this.mediaProperties.mediaDirection.receiveShare,
6675
+ this.mediaProperties.mediaDirection.sendShare
6676
+ ),
6677
+ },
6678
+ remoteQualityLevel: this.mediaProperties.remoteQualityLevel,
6679
+ })
6680
+ .then(() => {
6681
+ LoggerProxy.logger.info(`${LOG_HEADER} done`);
6682
+ })
6683
+ .catch((error) => {
6684
+ LoggerProxy.logger.error(`${LOG_HEADER} Error: `, error);
6685
+
6686
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.UPDATE_MEDIA_FAILURE, {
6687
+ correlation_id: this.correlationId,
6688
+ locus_id: this.locusUrl.split('/').pop(),
6689
+ reason: error.message,
6690
+ stack: error.stack,
6691
+ });
6692
+
6693
+ throw error;
6694
+ });
6695
+ }
6696
+
6697
+ /**
6698
+ * Publishes a track.
6699
+ *
6700
+ * @param {LocalTrack} track to publish
6701
+ * @returns {Promise}
6702
+ */
6703
+ private async publishTrack(track?: LocalTrack) {
6704
+ if (!track) {
6705
+ return;
6706
+ }
6707
+
6708
+ if (this.mediaProperties.webrtcMediaConnection) {
6709
+ if (this.isMultistream) {
6710
+ await this.mediaProperties.webrtcMediaConnection.publishTrack(track);
6711
+ } else {
6712
+ track.setPublished(true); // for multistream, this call is done by WCME
6713
+ }
6714
+ }
6715
+ }
6716
+
6717
+ /**
6718
+ * Un-publishes a track.
6719
+ *
6720
+ * @param {LocalTrack} track to unpublish
6721
+ * @returns {Promise}
6722
+ */
6723
+ private async unpublishTrack(track?: LocalTrack) {
6724
+ if (!track) {
6725
+ return;
6726
+ }
6727
+
6728
+ if (this.isMultistream && this.mediaProperties.webrtcMediaConnection) {
6729
+ await this.mediaProperties.webrtcMediaConnection.unpublishTrack(track);
6730
+ } else {
6731
+ track.setPublished(false); // for multistream, this call is done by WCME
6732
+ }
6733
+ }
6734
+
6735
+ /**
6736
+ * Publishes specified local tracks in the meeting
6737
+ *
6738
+ * @param {Object} tracks
6739
+ * @returns {Promise}
6740
+ */
6741
+ async publishTracks(tracks: LocalTracks): Promise<void> {
6742
+ this.checkMediaConnection();
6743
+
6744
+ this.annotationInfo = tracks.annotationInfo;
6745
+
6746
+ if (
6747
+ !tracks.microphone &&
6748
+ !tracks.camera &&
6749
+ !tracks.screenShare?.audio &&
6750
+ !tracks.screenShare?.video
6751
+ ) {
6752
+ // nothing to do
6753
+ return;
6754
+ }
6755
+
6756
+ let floorRequestNeeded = false;
6757
+
6758
+ if (tracks.screenShare?.video) {
6759
+ await this.setLocalShareTrack(tracks.screenShare?.video);
6760
+
6761
+ floorRequestNeeded = true;
6762
+ }
6763
+
6764
+ if (tracks.microphone) {
6765
+ await this.setLocalAudioTrack(tracks.microphone);
6766
+ }
6767
+
6768
+ if (tracks.camera) {
6769
+ await this.setLocalVideoTrack(tracks.camera);
6770
+ }
6771
+
6772
+ if (!this.isMultistream) {
6773
+ await this.updateTranscodedMediaConnection();
6774
+ }
6775
+
6776
+ if (floorRequestNeeded) {
6777
+ // we're sending the http request to Locus to request the screen share floor
6778
+ // only after the SDP update, because that's how it's always been done for transcoded meetings
6779
+ // and also if sharing from the start, we need confluence to have been created
6780
+ await this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.SHARE_FLOOR_REQUEST);
6781
+ }
6782
+ }
6783
+
6784
+ /**
6785
+ * Un-publishes specified local tracks in the meeting
6786
+ *
6787
+ * @param {Array<MediaStreamTrack>} tracks
6788
+ * @returns {Promise}
6789
+ */
6790
+ async unpublishTracks(tracks: LocalTrack[]): Promise<void> {
6791
+ this.checkMediaConnection();
6792
+
6793
+ const promises = [];
6794
+
6795
+ for (const track of tracks.filter((t) => !!t)) {
6796
+ if (track === this.mediaProperties.shareTrack) {
6797
+ try {
6798
+ this.releaseScreenShareFloor(); // we ignore the returned promise here on purpose
6799
+ } catch (e) {
6800
+ // nothing to do here, error is logged already inside releaseScreenShareFloor()
6801
+ }
6802
+ promises.push(this.setLocalShareTrack(undefined));
6803
+ }
6804
+
6805
+ if (track === this.mediaProperties.audioTrack) {
6806
+ promises.push(this.setLocalAudioTrack(undefined));
6807
+ }
6808
+
6809
+ if (track === this.mediaProperties.videoTrack) {
6810
+ promises.push(this.setLocalVideoTrack(undefined));
6811
+ }
6812
+ }
6813
+
6814
+ if (!this.isMultistream) {
6815
+ promises.push(this.updateTranscodedMediaConnection());
6816
+ }
6817
+
6818
+ await Promise.all(promises);
6819
+ }
6843
6820
  }