@webex/plugin-meetings 3.0.0-stream-classes.4 → 3.0.0

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 (469) hide show
  1. package/.eslintrc.js +6 -0
  2. package/README.md +12 -0
  3. package/babel.config.js +3 -0
  4. package/dist/annotation/constants.js +12 -20
  5. package/dist/annotation/constants.js.map +1 -1
  6. package/dist/annotation/index.js +25 -10
  7. package/dist/annotation/index.js.map +1 -1
  8. package/dist/breakouts/breakout.js +2 -3
  9. package/dist/breakouts/breakout.js.map +1 -1
  10. package/dist/breakouts/collection.js +1 -2
  11. package/dist/breakouts/collection.js.map +1 -1
  12. package/dist/breakouts/edit-lock-error.js +1 -2
  13. package/dist/breakouts/edit-lock-error.js.map +1 -1
  14. package/dist/breakouts/events.js +1 -2
  15. package/dist/breakouts/events.js.map +1 -1
  16. package/dist/breakouts/index.js +13 -14
  17. package/dist/breakouts/index.js.map +1 -1
  18. package/dist/breakouts/request.js +1 -2
  19. package/dist/breakouts/request.js.map +1 -1
  20. package/dist/breakouts/utils.js +3 -6
  21. package/dist/breakouts/utils.js.map +1 -1
  22. package/dist/common/browser-detection.js +2 -3
  23. package/dist/common/browser-detection.js.map +1 -1
  24. package/dist/common/collection.js +3 -4
  25. package/dist/common/collection.js.map +1 -1
  26. package/dist/common/config.js +1 -2
  27. package/dist/common/config.js.map +1 -1
  28. package/dist/common/errors/captcha-error.js +1 -2
  29. package/dist/common/errors/captcha-error.js.map +1 -1
  30. package/dist/common/errors/intent-to-join.js +1 -2
  31. package/dist/common/errors/intent-to-join.js.map +1 -1
  32. package/dist/common/errors/join-meeting.js +1 -2
  33. package/dist/common/errors/join-meeting.js.map +1 -1
  34. package/dist/common/errors/media.js +1 -2
  35. package/dist/common/errors/media.js.map +1 -1
  36. package/dist/common/errors/no-meeting-info.d.ts +14 -0
  37. package/dist/common/errors/no-meeting-info.js +50 -0
  38. package/dist/common/errors/no-meeting-info.js.map +1 -0
  39. package/dist/common/errors/parameter.js +3 -4
  40. package/dist/common/errors/parameter.js.map +1 -1
  41. package/dist/common/errors/password-error.js +1 -2
  42. package/dist/common/errors/password-error.js.map +1 -1
  43. package/dist/common/errors/permission.js +1 -2
  44. package/dist/common/errors/permission.js.map +1 -1
  45. package/dist/common/errors/reclaim-host-role-errors.d.ts +60 -0
  46. package/dist/common/errors/reclaim-host-role-errors.js +154 -0
  47. package/dist/common/errors/reclaim-host-role-errors.js.map +1 -0
  48. package/dist/common/errors/reconnection-in-progress.js +1 -2
  49. package/dist/common/errors/reconnection-in-progress.js.map +1 -1
  50. package/dist/common/errors/reconnection.js +1 -2
  51. package/dist/common/errors/reconnection.js.map +1 -1
  52. package/dist/common/errors/stats.js +1 -2
  53. package/dist/common/errors/stats.js.map +1 -1
  54. package/dist/{types/common → common}/errors/webex-errors.d.ts +13 -1
  55. package/dist/common/errors/webex-errors.js +35 -16
  56. package/dist/common/errors/webex-errors.js.map +1 -1
  57. package/dist/common/errors/webex-meetings-error.js +1 -2
  58. package/dist/common/errors/webex-meetings-error.js.map +1 -1
  59. package/dist/common/events/events-scope.js +1 -2
  60. package/dist/common/events/events-scope.js.map +1 -1
  61. package/dist/common/events/events.js +1 -2
  62. package/dist/common/events/events.js.map +1 -1
  63. package/dist/common/events/trigger-proxy.js +1 -2
  64. package/dist/common/events/trigger-proxy.js.map +1 -1
  65. package/dist/common/events/util.js +1 -2
  66. package/dist/common/events/util.js.map +1 -1
  67. package/dist/common/logs/logger-config.js +1 -2
  68. package/dist/common/logs/logger-config.js.map +1 -1
  69. package/dist/common/logs/logger-proxy.js +1 -2
  70. package/dist/common/logs/logger-proxy.js.map +1 -1
  71. package/dist/{types/common → common}/logs/request.d.ts +3 -1
  72. package/dist/common/logs/request.js +8 -5
  73. package/dist/common/logs/request.js.map +1 -1
  74. package/dist/common/queue.js +2 -4
  75. package/dist/common/queue.js.map +1 -1
  76. package/dist/{types/config.d.ts → config.d.ts} +1 -1
  77. package/dist/config.js +3 -3
  78. package/dist/config.js.map +1 -1
  79. package/dist/{types/constants.d.ts → constants.d.ts} +72 -15
  80. package/dist/constants.js +254 -371
  81. package/dist/constants.js.map +1 -1
  82. package/dist/controls-options-manager/constants.js +3 -6
  83. package/dist/controls-options-manager/constants.js.map +1 -1
  84. package/dist/controls-options-manager/enums.js +7 -10
  85. package/dist/controls-options-manager/enums.js.map +1 -1
  86. package/dist/controls-options-manager/index.js +27 -32
  87. package/dist/controls-options-manager/index.js.map +1 -1
  88. package/dist/controls-options-manager/util.js +1 -2
  89. package/dist/controls-options-manager/util.js.map +1 -1
  90. package/dist/index.js +8 -5
  91. package/dist/index.js.map +1 -1
  92. package/dist/interceptors/index.d.ts +2 -0
  93. package/dist/interceptors/index.js +15 -0
  94. package/dist/interceptors/index.js.map +1 -0
  95. package/dist/interceptors/locusRetry.d.ts +27 -0
  96. package/dist/interceptors/locusRetry.js +94 -0
  97. package/dist/interceptors/locusRetry.js.map +1 -0
  98. package/dist/interpretation/collection.js +1 -2
  99. package/dist/interpretation/collection.js.map +1 -1
  100. package/dist/interpretation/index.js +2 -3
  101. package/dist/interpretation/index.js.map +1 -1
  102. package/dist/interpretation/siLanguage.js +2 -3
  103. package/dist/interpretation/siLanguage.js.map +1 -1
  104. package/dist/locus-info/controlsUtils.js +12 -13
  105. package/dist/locus-info/controlsUtils.js.map +1 -1
  106. package/dist/locus-info/embeddedAppsUtils.js +3 -4
  107. package/dist/locus-info/embeddedAppsUtils.js.map +1 -1
  108. package/dist/locus-info/fullState.js +1 -2
  109. package/dist/locus-info/fullState.js.map +1 -1
  110. package/dist/locus-info/hostUtils.js +1 -2
  111. package/dist/locus-info/hostUtils.js.map +1 -1
  112. package/dist/{types/locus-info → locus-info}/index.d.ts +1 -1
  113. package/dist/locus-info/index.js +63 -38
  114. package/dist/locus-info/index.js.map +1 -1
  115. package/dist/locus-info/infoUtils.js +3 -4
  116. package/dist/locus-info/infoUtils.js.map +1 -1
  117. package/dist/locus-info/mediaSharesUtils.js +16 -3
  118. package/dist/locus-info/mediaSharesUtils.js.map +1 -1
  119. package/dist/{types/locus-info → locus-info}/parser.d.ts +3 -2
  120. package/dist/locus-info/parser.js +48 -31
  121. package/dist/locus-info/parser.js.map +1 -1
  122. package/dist/locus-info/selfUtils.js +7 -6
  123. package/dist/locus-info/selfUtils.js.map +1 -1
  124. package/dist/media/index.js +15 -10
  125. package/dist/media/index.js.map +1 -1
  126. package/dist/media/properties.js +16 -7
  127. package/dist/media/properties.js.map +1 -1
  128. package/dist/media/util.js +1 -2
  129. package/dist/media/util.js.map +1 -1
  130. package/dist/mediaQualityMetrics/config.d.ts +241 -0
  131. package/dist/mediaQualityMetrics/config.js +135 -339
  132. package/dist/mediaQualityMetrics/config.js.map +1 -1
  133. package/dist/{types/meeting → meeting}/in-meeting-actions.d.ts +4 -0
  134. package/dist/meeting/in-meeting-actions.js +18 -2
  135. package/dist/meeting/in-meeting-actions.js.map +1 -1
  136. package/dist/{types/meeting → meeting}/index.d.ts +331 -44
  137. package/dist/meeting/index.js +2639 -1367
  138. package/dist/meeting/index.js.map +1 -1
  139. package/dist/{types/meeting → meeting}/locusMediaRequest.d.ts +1 -2
  140. package/dist/meeting/locusMediaRequest.js +4 -5
  141. package/dist/meeting/locusMediaRequest.js.map +1 -1
  142. package/dist/meeting/muteState.js +2 -4
  143. package/dist/meeting/muteState.js.map +1 -1
  144. package/dist/{types/meeting → meeting}/request.d.ts +4 -1
  145. package/dist/meeting/request.js +47 -32
  146. package/dist/meeting/request.js.map +1 -1
  147. package/dist/meeting/state.js +1 -2
  148. package/dist/meeting/state.js.map +1 -1
  149. package/dist/{types/meeting → meeting}/util.d.ts +26 -1
  150. package/dist/meeting/util.js +83 -10
  151. package/dist/meeting/util.js.map +1 -1
  152. package/dist/meeting/voicea-meeting.d.ts +16 -0
  153. package/dist/meeting/voicea-meeting.js +169 -0
  154. package/dist/meeting/voicea-meeting.js.map +1 -0
  155. package/dist/meeting-info/collection.js +3 -4
  156. package/dist/meeting-info/collection.js.map +1 -1
  157. package/dist/{types/meeting-info → meeting-info}/index.d.ts +7 -0
  158. package/dist/meeting-info/index.js +53 -27
  159. package/dist/meeting-info/index.js.map +1 -1
  160. package/dist/{types/meeting-info → meeting-info}/meeting-info-v2.d.ts +1 -0
  161. package/dist/meeting-info/meeting-info-v2.js +52 -33
  162. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  163. package/dist/meeting-info/request.js +1 -2
  164. package/dist/meeting-info/request.js.map +1 -1
  165. package/dist/meeting-info/util.js +8 -8
  166. package/dist/meeting-info/util.js.map +1 -1
  167. package/dist/meeting-info/utilv2.js +12 -9
  168. package/dist/meeting-info/utilv2.js.map +1 -1
  169. package/dist/{types/meetings → meetings}/collection.d.ts +9 -0
  170. package/dist/meetings/collection.js +21 -5
  171. package/dist/meetings/collection.js.map +1 -1
  172. package/dist/{types/meetings → meetings}/index.d.ts +45 -16
  173. package/dist/meetings/index.js +166 -74
  174. package/dist/meetings/index.js.map +1 -1
  175. package/dist/meetings/request.js +2 -3
  176. package/dist/meetings/request.js.map +1 -1
  177. package/dist/meetings/util.js +3 -10
  178. package/dist/meetings/util.js.map +1 -1
  179. package/dist/{types/member → member}/index.d.ts +1 -0
  180. package/dist/member/index.js +10 -3
  181. package/dist/member/index.js.map +1 -1
  182. package/dist/member/member.types.d.ts +11 -0
  183. package/dist/member/member.types.js +17 -0
  184. package/dist/member/member.types.js.map +1 -0
  185. package/dist/member/types.js +6 -8
  186. package/dist/member/types.js.map +1 -1
  187. package/dist/member/util.js +12 -2
  188. package/dist/member/util.js.map +1 -1
  189. package/dist/members/collection.js +1 -2
  190. package/dist/members/collection.js.map +1 -1
  191. package/dist/members/index.js +25 -8
  192. package/dist/members/index.js.map +1 -1
  193. package/dist/members/request.js +2 -3
  194. package/dist/members/request.js.map +1 -1
  195. package/dist/{types/members → members}/types.d.ts +1 -0
  196. package/dist/members/types.js +3 -4
  197. package/dist/members/types.js.map +1 -1
  198. package/dist/{types/members → members}/util.d.ts +6 -1
  199. package/dist/members/util.js +18 -8
  200. package/dist/members/util.js.map +1 -1
  201. package/dist/{types/metrics → metrics}/constants.d.ts +15 -0
  202. package/dist/metrics/constants.js +16 -3
  203. package/dist/metrics/constants.js.map +1 -1
  204. package/dist/metrics/index.js +3 -2
  205. package/dist/metrics/index.js.map +1 -1
  206. package/dist/multistream/mediaRequestManager.js +9 -11
  207. package/dist/multistream/mediaRequestManager.js.map +1 -1
  208. package/dist/multistream/receiveSlot.js +3 -5
  209. package/dist/multistream/receiveSlot.js.map +1 -1
  210. package/dist/multistream/receiveSlotManager.js +7 -9
  211. package/dist/multistream/receiveSlotManager.js.map +1 -1
  212. package/dist/multistream/remoteMedia.js +3 -5
  213. package/dist/multistream/remoteMedia.js.map +1 -1
  214. package/dist/multistream/remoteMediaGroup.js +7 -6
  215. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  216. package/dist/{types/multistream → multistream}/remoteMediaManager.d.ts +9 -1
  217. package/dist/multistream/remoteMediaManager.js +74 -36
  218. package/dist/multistream/remoteMediaManager.js.map +1 -1
  219. package/dist/multistream/sendSlotManager.js +9 -6
  220. package/dist/multistream/sendSlotManager.js.map +1 -1
  221. package/dist/networkQualityMonitor/index.js +1 -2
  222. package/dist/networkQualityMonitor/index.js.map +1 -1
  223. package/dist/personal-meeting-room/index.js +2 -3
  224. package/dist/personal-meeting-room/index.js.map +1 -1
  225. package/dist/personal-meeting-room/request.js +2 -3
  226. package/dist/personal-meeting-room/request.js.map +1 -1
  227. package/dist/personal-meeting-room/util.js +1 -2
  228. package/dist/personal-meeting-room/util.js.map +1 -1
  229. package/dist/reachability/clusterReachability.d.ts +109 -0
  230. package/dist/reachability/clusterReachability.js +357 -0
  231. package/dist/reachability/clusterReachability.js.map +1 -0
  232. package/dist/reachability/index.d.ts +105 -0
  233. package/dist/reachability/index.js +279 -436
  234. package/dist/reachability/index.js.map +1 -1
  235. package/dist/{types/reachability → reachability}/request.d.ts +1 -1
  236. package/dist/reachability/request.js +14 -11
  237. package/dist/reachability/request.js.map +1 -1
  238. package/dist/reachability/util.d.ts +8 -0
  239. package/dist/reachability/util.js +29 -0
  240. package/dist/reachability/util.js.map +1 -0
  241. package/dist/reactions/constants.js +1 -2
  242. package/dist/reactions/constants.js.map +1 -1
  243. package/dist/reactions/reactions.js +2 -4
  244. package/dist/reactions/reactions.js.map +1 -1
  245. package/dist/reactions/reactions.type.js +6 -8
  246. package/dist/reactions/reactions.type.js.map +1 -1
  247. package/dist/{types/reconnection-manager → reconnection-manager}/index.d.ts +10 -0
  248. package/dist/reconnection-manager/index.js +129 -106
  249. package/dist/reconnection-manager/index.js.map +1 -1
  250. package/dist/recording-controller/enums.js +4 -5
  251. package/dist/recording-controller/enums.js.map +1 -1
  252. package/dist/recording-controller/index.js +43 -51
  253. package/dist/recording-controller/index.js.map +1 -1
  254. package/dist/recording-controller/util.js +1 -2
  255. package/dist/recording-controller/util.js.map +1 -1
  256. package/dist/{types/roap → roap}/index.d.ts +2 -1
  257. package/dist/roap/index.js +59 -28
  258. package/dist/roap/index.js.map +1 -1
  259. package/dist/{types/roap → roap}/request.d.ts +2 -1
  260. package/dist/roap/request.js +14 -22
  261. package/dist/roap/request.js.map +1 -1
  262. package/dist/{types/roap → roap}/turnDiscovery.d.ts +21 -4
  263. package/dist/roap/turnDiscovery.js +182 -89
  264. package/dist/roap/turnDiscovery.js.map +1 -1
  265. package/dist/rtcMetrics/constants.js +1 -2
  266. package/dist/rtcMetrics/constants.js.map +1 -1
  267. package/dist/{types/rtcMetrics → rtcMetrics}/index.d.ts +15 -1
  268. package/dist/rtcMetrics/index.js +72 -12
  269. package/dist/rtcMetrics/index.js.map +1 -1
  270. package/dist/statsAnalyzer/global.js +1 -2
  271. package/dist/statsAnalyzer/global.js.map +1 -1
  272. package/dist/{types/statsAnalyzer → statsAnalyzer}/index.d.ts +28 -11
  273. package/dist/statsAnalyzer/index.js +371 -318
  274. package/dist/statsAnalyzer/index.js.map +1 -1
  275. package/dist/statsAnalyzer/mqaUtil.d.ts +48 -0
  276. package/dist/statsAnalyzer/mqaUtil.js +295 -162
  277. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  278. package/dist/transcription/index.js +1 -2
  279. package/dist/transcription/index.js.map +1 -1
  280. package/dist/webinar/collection.d.ts +16 -0
  281. package/dist/webinar/collection.js +43 -0
  282. package/dist/webinar/collection.js.map +1 -0
  283. package/dist/webinar/index.d.ts +5 -0
  284. package/dist/webinar/index.js +68 -0
  285. package/dist/webinar/index.js.map +1 -0
  286. package/jest.config.js +3 -0
  287. package/package.json +44 -24
  288. package/process +1 -0
  289. package/src/common/errors/no-meeting-info.ts +24 -0
  290. package/src/common/errors/reclaim-host-role-errors.ts +134 -0
  291. package/src/common/errors/webex-errors.ts +19 -2
  292. package/src/common/logs/request.ts +5 -1
  293. package/src/config.ts +3 -5
  294. package/src/constants.ts +78 -8
  295. package/src/index.ts +4 -0
  296. package/src/interceptors/index.ts +3 -0
  297. package/src/interceptors/locusRetry.ts +67 -0
  298. package/src/locus-info/index.ts +52 -16
  299. package/src/locus-info/mediaSharesUtils.ts +16 -0
  300. package/src/locus-info/parser.ts +47 -21
  301. package/src/media/index.ts +8 -6
  302. package/src/media/properties.ts +17 -2
  303. package/src/mediaQualityMetrics/config.ts +103 -238
  304. package/src/meeting/in-meeting-actions.ts +8 -0
  305. package/src/meeting/index.ts +1692 -627
  306. package/src/meeting/request.ts +19 -1
  307. package/src/meeting/util.ts +102 -1
  308. package/src/meeting/voicea-meeting.ts +122 -0
  309. package/src/meeting-info/index.ts +47 -20
  310. package/src/meeting-info/meeting-info-v2.ts +32 -16
  311. package/src/meeting-info/util.ts +12 -9
  312. package/src/meeting-info/utilv2.ts +25 -15
  313. package/src/meetings/collection.ts +13 -0
  314. package/src/meetings/index.ts +112 -31
  315. package/src/meetings/util.ts +2 -8
  316. package/src/member/index.ts +9 -1
  317. package/src/member/member.types.ts +13 -0
  318. package/src/member/util.ts +14 -0
  319. package/src/members/index.ts +29 -2
  320. package/src/members/types.ts +1 -0
  321. package/src/members/util.ts +15 -1
  322. package/src/metrics/constants.ts +14 -0
  323. package/src/multistream/remoteMediaManager.ts +41 -4
  324. package/src/reachability/clusterReachability.ts +320 -0
  325. package/src/reachability/index.ts +221 -382
  326. package/src/reachability/request.ts +1 -1
  327. package/src/reachability/util.ts +24 -0
  328. package/src/reconnection-manager/index.ts +87 -83
  329. package/src/roap/index.ts +60 -24
  330. package/src/roap/request.ts +4 -17
  331. package/src/roap/turnDiscovery.ts +112 -39
  332. package/src/rtcMetrics/index.ts +71 -5
  333. package/src/statsAnalyzer/index.ts +430 -427
  334. package/src/statsAnalyzer/mqaUtil.ts +317 -168
  335. package/src/webinar/collection.ts +31 -0
  336. package/src/webinar/index.ts +62 -0
  337. package/test/integration/spec/converged-space-meetings.js +7 -7
  338. package/test/integration/spec/journey.js +88 -106
  339. package/test/integration/spec/space-meeting.js +10 -10
  340. package/test/unit/spec/breakouts/breakout.ts +2 -1
  341. package/test/unit/spec/breakouts/index.ts +7 -4
  342. package/test/unit/spec/interceptors/locusRetry.ts +131 -0
  343. package/test/unit/spec/locus-info/index.js +206 -13
  344. package/test/unit/spec/locus-info/lib/SeqCmp.json +16 -0
  345. package/test/unit/spec/locus-info/mediaSharesUtils.ts +10 -0
  346. package/test/unit/spec/locus-info/parser.js +54 -13
  347. package/test/unit/spec/locus-info/selfUtils.js +1 -1
  348. package/test/unit/spec/media/index.ts +25 -4
  349. package/test/unit/spec/media/properties.ts +2 -2
  350. package/test/unit/spec/meeting/in-meeting-actions.ts +4 -0
  351. package/test/unit/spec/meeting/index.js +4354 -1285
  352. package/test/unit/spec/meeting/request.js +63 -12
  353. package/test/unit/spec/meeting/utils.js +145 -10
  354. package/test/unit/spec/meeting/voicea-meeting.ts +266 -0
  355. package/test/unit/spec/meeting-info/index.js +180 -61
  356. package/test/unit/spec/meeting-info/meetinginfov2.js +216 -68
  357. package/test/unit/spec/meetings/collection.js +12 -0
  358. package/test/unit/spec/meetings/index.js +676 -195
  359. package/test/unit/spec/meetings/utils.js +35 -12
  360. package/test/unit/spec/member/index.js +8 -7
  361. package/test/unit/spec/member/util.js +32 -0
  362. package/test/unit/spec/members/index.js +130 -17
  363. package/test/unit/spec/members/utils.js +26 -0
  364. package/test/unit/spec/metrics/index.js +1 -2
  365. package/test/unit/spec/multistream/mediaRequestManager.ts +1 -0
  366. package/test/unit/spec/multistream/remoteMediaManager.ts +10 -2
  367. package/test/unit/spec/reachability/clusterReachability.ts +279 -0
  368. package/test/unit/spec/reachability/index.ts +505 -135
  369. package/test/unit/spec/reachability/util.ts +40 -0
  370. package/test/unit/spec/reconnection-manager/index.js +74 -17
  371. package/test/unit/spec/recording-controller/index.js +0 -1
  372. package/test/unit/spec/roap/index.ts +181 -61
  373. package/test/unit/spec/roap/request.ts +27 -3
  374. package/test/unit/spec/roap/turnDiscovery.ts +363 -102
  375. package/test/unit/spec/rtcMetrics/index.ts +57 -3
  376. package/test/unit/spec/stats-analyzer/index.js +1225 -12
  377. package/test/unit/spec/webinar/collection.ts +13 -0
  378. package/test/unit/spec/webinar/index.ts +60 -0
  379. package/test/utils/integrationTestUtils.js +4 -4
  380. package/test/utils/webex-test-users.js +12 -4
  381. package/dist/types/mediaQualityMetrics/config.d.ts +0 -365
  382. package/dist/types/reachability/index.d.ts +0 -158
  383. package/dist/types/statsAnalyzer/mqaUtil.d.ts +0 -24
  384. /package/dist/{types/annotation → annotation}/annotation.types.d.ts +0 -0
  385. /package/dist/{types/annotation → annotation}/constants.d.ts +0 -0
  386. /package/dist/{types/annotation → annotation}/index.d.ts +0 -0
  387. /package/dist/{types/breakouts → breakouts}/breakout.d.ts +0 -0
  388. /package/dist/{types/breakouts → breakouts}/collection.d.ts +0 -0
  389. /package/dist/{types/breakouts → breakouts}/edit-lock-error.d.ts +0 -0
  390. /package/dist/{types/breakouts → breakouts}/events.d.ts +0 -0
  391. /package/dist/{types/breakouts → breakouts}/index.d.ts +0 -0
  392. /package/dist/{types/breakouts → breakouts}/request.d.ts +0 -0
  393. /package/dist/{types/breakouts → breakouts}/utils.d.ts +0 -0
  394. /package/dist/{types/common → common}/browser-detection.d.ts +0 -0
  395. /package/dist/{types/common → common}/collection.d.ts +0 -0
  396. /package/dist/{types/common → common}/config.d.ts +0 -0
  397. /package/dist/{types/common → common}/errors/captcha-error.d.ts +0 -0
  398. /package/dist/{types/common → common}/errors/intent-to-join.d.ts +0 -0
  399. /package/dist/{types/common → common}/errors/join-meeting.d.ts +0 -0
  400. /package/dist/{types/common → common}/errors/media.d.ts +0 -0
  401. /package/dist/{types/common → common}/errors/parameter.d.ts +0 -0
  402. /package/dist/{types/common → common}/errors/password-error.d.ts +0 -0
  403. /package/dist/{types/common → common}/errors/permission.d.ts +0 -0
  404. /package/dist/{types/common → common}/errors/reconnection-in-progress.d.ts +0 -0
  405. /package/dist/{types/common → common}/errors/reconnection.d.ts +0 -0
  406. /package/dist/{types/common → common}/errors/stats.d.ts +0 -0
  407. /package/dist/{types/common → common}/errors/webex-meetings-error.d.ts +0 -0
  408. /package/dist/{types/common → common}/events/events-scope.d.ts +0 -0
  409. /package/dist/{types/common → common}/events/events.d.ts +0 -0
  410. /package/dist/{types/common → common}/events/trigger-proxy.d.ts +0 -0
  411. /package/dist/{types/common → common}/events/util.d.ts +0 -0
  412. /package/dist/{types/common → common}/logs/logger-config.d.ts +0 -0
  413. /package/dist/{types/common → common}/logs/logger-proxy.d.ts +0 -0
  414. /package/dist/{types/common → common}/queue.d.ts +0 -0
  415. /package/dist/{types/controls-options-manager → controls-options-manager}/constants.d.ts +0 -0
  416. /package/dist/{types/controls-options-manager → controls-options-manager}/enums.d.ts +0 -0
  417. /package/dist/{types/controls-options-manager → controls-options-manager}/index.d.ts +0 -0
  418. /package/dist/{types/controls-options-manager → controls-options-manager}/types.d.ts +0 -0
  419. /package/dist/{types/controls-options-manager → controls-options-manager}/util.d.ts +0 -0
  420. /package/dist/{types/index.d.ts → index.d.ts} +0 -0
  421. /package/dist/{types/interpretation → interpretation}/collection.d.ts +0 -0
  422. /package/dist/{types/interpretation → interpretation}/index.d.ts +0 -0
  423. /package/dist/{types/interpretation → interpretation}/siLanguage.d.ts +0 -0
  424. /package/dist/{types/locus-info → locus-info}/controlsUtils.d.ts +0 -0
  425. /package/dist/{types/locus-info → locus-info}/embeddedAppsUtils.d.ts +0 -0
  426. /package/dist/{types/locus-info → locus-info}/fullState.d.ts +0 -0
  427. /package/dist/{types/locus-info → locus-info}/hostUtils.d.ts +0 -0
  428. /package/dist/{types/locus-info → locus-info}/infoUtils.d.ts +0 -0
  429. /package/dist/{types/locus-info → locus-info}/mediaSharesUtils.d.ts +0 -0
  430. /package/dist/{types/locus-info → locus-info}/selfUtils.d.ts +0 -0
  431. /package/dist/{types/media → media}/index.d.ts +0 -0
  432. /package/dist/{types/media → media}/properties.d.ts +0 -0
  433. /package/dist/{types/media → media}/util.d.ts +0 -0
  434. /package/dist/{types/meeting → meeting}/muteState.d.ts +0 -0
  435. /package/dist/{types/meeting → meeting}/request.type.d.ts +0 -0
  436. /package/dist/{types/meeting → meeting}/state.d.ts +0 -0
  437. /package/dist/{types/meeting-info → meeting-info}/collection.d.ts +0 -0
  438. /package/dist/{types/meeting-info → meeting-info}/request.d.ts +0 -0
  439. /package/dist/{types/meeting-info → meeting-info}/util.d.ts +0 -0
  440. /package/dist/{types/meeting-info → meeting-info}/utilv2.d.ts +0 -0
  441. /package/dist/{types/meetings → meetings}/meetings.types.d.ts +0 -0
  442. /package/dist/{types/meetings → meetings}/request.d.ts +0 -0
  443. /package/dist/{types/meetings → meetings}/util.d.ts +0 -0
  444. /package/dist/{types/member → member}/types.d.ts +0 -0
  445. /package/dist/{types/member → member}/util.d.ts +0 -0
  446. /package/dist/{types/members → members}/collection.d.ts +0 -0
  447. /package/dist/{types/members → members}/index.d.ts +0 -0
  448. /package/dist/{types/members → members}/request.d.ts +0 -0
  449. /package/dist/{types/metrics → metrics}/index.d.ts +0 -0
  450. /package/dist/{types/multistream → multistream}/mediaRequestManager.d.ts +0 -0
  451. /package/dist/{types/multistream → multistream}/receiveSlot.d.ts +0 -0
  452. /package/dist/{types/multistream → multistream}/receiveSlotManager.d.ts +0 -0
  453. /package/dist/{types/multistream → multistream}/remoteMedia.d.ts +0 -0
  454. /package/dist/{types/multistream → multistream}/remoteMediaGroup.d.ts +0 -0
  455. /package/dist/{types/multistream → multistream}/sendSlotManager.d.ts +0 -0
  456. /package/dist/{types/networkQualityMonitor → networkQualityMonitor}/index.d.ts +0 -0
  457. /package/dist/{types/personal-meeting-room → personal-meeting-room}/index.d.ts +0 -0
  458. /package/dist/{types/personal-meeting-room → personal-meeting-room}/request.d.ts +0 -0
  459. /package/dist/{types/personal-meeting-room → personal-meeting-room}/util.d.ts +0 -0
  460. /package/dist/{types/reactions → reactions}/constants.d.ts +0 -0
  461. /package/dist/{types/reactions → reactions}/reactions.d.ts +0 -0
  462. /package/dist/{types/reactions → reactions}/reactions.type.d.ts +0 -0
  463. /package/dist/{types/recording-controller → recording-controller}/enums.d.ts +0 -0
  464. /package/dist/{types/recording-controller → recording-controller}/index.d.ts +0 -0
  465. /package/dist/{types/recording-controller → recording-controller}/util.d.ts +0 -0
  466. /package/dist/{types/rtcMetrics → rtcMetrics}/constants.d.ts +0 -0
  467. /package/dist/{types/statsAnalyzer → statsAnalyzer}/global.d.ts +0 -0
  468. /package/dist/{types/transcription → transcription}/index.d.ts +0 -0
  469. /package/test/unit/spec/locus-info/{selfConstant.js → lib/selfConstant.js} +0 -0
@@ -1,12 +1,14 @@
1
1
  import uuid from 'uuid';
2
2
  import {cloneDeep, isEqual, isEmpty} from 'lodash';
3
- import jwt from 'jsonwebtoken';
3
+ import jwtDecode from 'jwt-decode';
4
4
  // @ts-ignore - Fix this
5
5
  import {StatelessWebexPlugin} from '@webex/webex-core';
6
+ // @ts-ignore - Types not available for @webex/common
7
+ import {Defer} from '@webex/common';
6
8
  import {
7
9
  ClientEvent,
8
10
  ClientEventLeaveReason,
9
- CALL_DIAGNOSTIC_CONFIG,
11
+ CallDiagnosticUtils,
10
12
  } from '@webex/internal-plugin-metrics';
11
13
  import {
12
14
  ConnectionState,
@@ -16,6 +18,7 @@ import {
16
18
  MediaContent,
17
19
  MediaType,
18
20
  RemoteTrackType,
21
+ RoapMessage,
19
22
  } from '@webex/internal-media-core';
20
23
 
21
24
  import {
@@ -30,12 +33,20 @@ import {
30
33
  RemoteStream,
31
34
  } from '@webex/media-helpers';
32
35
 
36
+ import {
37
+ EVENT_TRIGGERS as VOICEAEVENTS,
38
+ TURN_ON_CAPTION_STATUS,
39
+ } from '@webex/internal-plugin-voicea';
40
+ import {processNewCaptions} from './voicea-meeting';
41
+
33
42
  import {
34
43
  MeetingNotActiveError,
35
44
  UserInLobbyError,
36
45
  NoMediaEstablishedYetError,
37
46
  UserNotJoinedError,
47
+ AddMediaFailed,
38
48
  } from '../common/errors/webex-errors';
49
+
39
50
  import {StatsAnalyzer, EVENTS as StatsAnalyzerEvents} from '../statsAnalyzer';
40
51
  import NetworkQualityMonitor from '../networkQualityMonitor';
41
52
  import LoggerProxy from '../common/logs/logger-proxy';
@@ -51,19 +62,20 @@ import ReconnectionManager from '../reconnection-manager';
51
62
  import MeetingRequest from './request';
52
63
  import Members from '../members/index';
53
64
  import MeetingUtil from './util';
65
+ import MeetingsUtil from '../meetings/util';
54
66
  import RecordingUtil from '../recording-controller/util';
55
67
  import ControlsOptionsUtil from '../controls-options-manager/util';
56
68
  import MediaUtil from '../media/util';
57
- import Transcription from '../transcription';
58
69
  import {Reactions, SkinTones} from '../reactions/reactions';
59
70
  import PasswordError from '../common/errors/password-error';
60
71
  import CaptchaError from '../common/errors/captcha-error';
61
72
  import ReconnectionError from '../common/errors/reconnection';
62
73
  import ReconnectInProgress from '../common/errors/reconnection-in-progress';
63
74
  import {
64
- _CALL_,
75
+ _CONVERSATION_URL_,
65
76
  _INCOMING_,
66
77
  _JOIN_,
78
+ _MEETING_LINK_,
67
79
  AUDIO,
68
80
  CONTENT,
69
81
  DISPLAY_HINTS,
@@ -91,10 +103,14 @@ import {
91
103
  SHARE_STATUS,
92
104
  SHARE_STOPPED_REASON,
93
105
  VIDEO,
94
- HTTP_VERBS,
95
106
  SELF_ROLES,
96
107
  INTERPRETATION,
97
108
  SELF_POLICY,
109
+ MEETING_PERMISSION_TOKEN_REFRESH_THRESHOLD_IN_SEC,
110
+ MEETING_PERMISSION_TOKEN_REFRESH_REASON,
111
+ ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
112
+ RECONNECTION,
113
+ LANGUAGE_ENGLISH,
98
114
  } from '../constants';
99
115
  import BEHAVIORAL_METRICS from '../metrics/constants';
100
116
  import ParameterError from '../common/errors/parameter';
@@ -122,6 +138,7 @@ import {
122
138
  import Breakouts from '../breakouts';
123
139
  import SimultaneousInterpretation from '../interpretation';
124
140
  import Annotation from '../annotation';
141
+ import Webinar from '../webinar';
125
142
 
126
143
  import InMeetingActions from './in-meeting-actions';
127
144
  import {REACTION_RELAY_TYPES} from '../reactions/constants';
@@ -147,6 +164,36 @@ const logRequest = (request: any, {logText = ''}) => {
147
164
  });
148
165
  };
149
166
 
167
+ export type CaptionData = {
168
+ id: string;
169
+ isFinal: boolean;
170
+ translations: Array<string>;
171
+ text: string;
172
+ currentCaptionLanguage: string;
173
+ timestamp: string;
174
+ speaker: string;
175
+ };
176
+
177
+ export type Transcription = {
178
+ languageOptions: {
179
+ captionLanguages?: string; // list of supported caption languages from backend
180
+ maxLanguages?: number;
181
+ spokenLanguages?: Array<string>; // list of supported spoken languages from backend
182
+ currentCaptionLanguage?: string; // current caption language - default is english
183
+ requestedCaptionLanguage?: string; // requested caption language
184
+ currentSpokenLanguage?: string; // current spoken language - default is english
185
+ };
186
+ status: string;
187
+ isListening: boolean;
188
+ commandText: string;
189
+ captions: Array<CaptionData>;
190
+ showCaptionBox: boolean;
191
+ transcribingRequestStatus: string;
192
+ isCaptioning: boolean;
193
+ speakerProxy: Map<string, any>;
194
+ interimCaptions: Map<string, CaptionData>;
195
+ };
196
+
150
197
  export type LocalStreams = {
151
198
  microphone?: LocalMicrophoneStream;
152
199
  camera?: LocalCameraStream;
@@ -167,6 +214,12 @@ export type AddMediaOptions = {
167
214
  allowMediaInLobby?: boolean; // allows adding media when in the lobby
168
215
  };
169
216
 
217
+ export type CallStateForMetrics = {
218
+ correlationId?: string;
219
+ joinTrigger?: string;
220
+ loginType?: string;
221
+ };
222
+
170
223
  export const MEDIA_UPDATE_TYPE = {
171
224
  TRANSCODED_MEDIA_CONNECTION: 'TRANSCODED_MEDIA_CONNECTION',
172
225
  SHARE_FLOOR_REQUEST: 'SHARE_FLOOR_REQUEST',
@@ -179,6 +232,13 @@ export enum ScreenShareFloorStatus {
179
232
  RELEASED = 'floor_released',
180
233
  }
181
234
 
235
+ type FetchMeetingInfoParams = {
236
+ password?: string;
237
+ captchaCode?: string;
238
+ extraParams?: Record<string, any>;
239
+ sendCAevents?: boolean;
240
+ };
241
+
182
242
  /**
183
243
  * MediaDirection
184
244
  * @typedef {Object} MediaDirection
@@ -457,8 +517,9 @@ export default class Meeting extends StatelessWebexPlugin {
457
517
  breakouts: any;
458
518
  simultaneousInterpretation: any;
459
519
  annotation: any;
520
+ webinar: any;
460
521
  conversationUrl: string;
461
- correlationId: string;
522
+ callStateForMetrics: CallStateForMetrics;
462
523
  destination: string;
463
524
  destinationType: string;
464
525
  deviceUrl: string;
@@ -514,8 +575,9 @@ export default class Meeting extends StatelessWebexPlugin {
514
575
 
515
576
  meetingInfoFailureReason: string;
516
577
  meetingInfoFailureCode?: number;
578
+ meetingInfoExtraParams?: Record<string, any>;
517
579
  networkQualityMonitor: NetworkQualityMonitor;
518
- networkStatus: string;
580
+ networkStatus?: NETWORK_STATUS;
519
581
  passwordStatus: string;
520
582
  queuedMediaUpdates: any[];
521
583
  recording: any;
@@ -525,6 +587,7 @@ export default class Meeting extends StatelessWebexPlugin {
525
587
  requiredCaptcha: any;
526
588
  receiveSlotManager: ReceiveSlotManager;
527
589
  selfUserPolicies: any;
590
+ enforceVBGImagesURL: string;
528
591
  shareStatus: string;
529
592
  screenShareFloorState: ScreenShareFloorStatus;
530
593
  statsAnalyzer: StatsAnalyzer;
@@ -544,7 +607,9 @@ export default class Meeting extends StatelessWebexPlugin {
544
607
  meetingJoinUrl: any;
545
608
  meetingNumber: any;
546
609
  meetingState: any;
547
- permissionToken: any;
610
+ permissionToken: string;
611
+ permissionTokenPayload: any;
612
+ permissionTokenReceivedLocalTime: number;
548
613
  resourceId: any;
549
614
  resourceUrl: string;
550
615
  selfId: string;
@@ -556,7 +621,55 @@ export default class Meeting extends StatelessWebexPlugin {
556
621
  environment: string;
557
622
  namespace = MEETINGS;
558
623
  allowMediaInLobby: boolean;
624
+ localShareInstanceId: string;
625
+ remoteShareInstanceId: string;
626
+ turnDiscoverySkippedReason: string;
627
+ turnServerUsed: boolean;
628
+ areVoiceaEventsSetup = false;
629
+ voiceaListenerCallbacks: object = {
630
+ [VOICEAEVENTS.VOICEA_ANNOUNCEMENT]: (payload: Transcription['languageOptions']) => {
631
+ this.transcription.languageOptions = payload;
632
+ Trigger.trigger(
633
+ this,
634
+ {
635
+ file: 'meeting/index',
636
+ function: 'setUpVoiceaListeners',
637
+ },
638
+ EVENT_TRIGGERS.MEETING_STARTED_RECEIVING_TRANSCRIPTION,
639
+ payload
640
+ );
641
+ },
642
+ [VOICEAEVENTS.CAPTIONS_TURNED_ON]: () => {
643
+ this.transcription.status = TURN_ON_CAPTION_STATUS.ENABLED;
644
+ },
645
+ [VOICEAEVENTS.EVA_COMMAND]: (payload) => {
646
+ const {data} = payload;
647
+
648
+ this.transcription.isListening = !!data.isListening;
649
+ this.transcription.commandText = data.text ?? '';
650
+ },
651
+ [VOICEAEVENTS.NEW_CAPTION]: (data) => {
652
+ processNewCaptions({data, meeting: this});
653
+ Trigger.trigger(
654
+ this,
655
+ {
656
+ file: 'meeting/index',
657
+ function: 'setUpVoiceaListeners',
658
+ },
659
+ EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED,
660
+ {
661
+ captions: this.transcription.captions,
662
+ interimCaptions: this.transcription.interimCaptions,
663
+ }
664
+ );
665
+ },
666
+ };
667
+
668
+ private retriedWithTurnServer: boolean;
559
669
  private sendSlotManager: SendSlotManager = new SendSlotManager(LoggerProxy);
670
+ private deferSDPAnswer?: Defer; // used for waiting for a response
671
+ private sdpResponseTimer?: ReturnType<typeof setTimeout>;
672
+ private hasMediaConnectionConnectedAtLeastOnce: boolean;
560
673
 
561
674
  /**
562
675
  * @param {Object} attrs
@@ -591,20 +704,22 @@ export default class Meeting extends StatelessWebexPlugin {
591
704
  */
592
705
  this.id = uuid.v4();
593
706
  /**
594
- * Correlation ID used for network tracking of meeting
707
+ * Call state used for metrics
595
708
  * @instance
596
- * @type {String}
709
+ * @type {CallStateForMetrics}
597
710
  * @readonly
598
711
  * @public
599
712
  * @memberof Meeting
600
713
  */
601
- if (attrs.correlationId) {
714
+ this.callStateForMetrics = attrs.callStateForMetrics || {};
715
+ const correlationId = attrs.correlationId || attrs.callStateForMetrics?.correlationId;
716
+ if (correlationId) {
602
717
  LoggerProxy.logger.log(
603
- `Meetings:index#constructor --> Initializing the meeting object with correlation id from app ${this.correlationId}`
718
+ `Meetings:index#constructor --> Initializing the meeting object with correlation id from app ${correlationId}`
604
719
  );
605
- this.correlationId = attrs.correlationId;
720
+ this.callStateForMetrics.correlationId = correlationId;
606
721
  } else {
607
- this.correlationId = this.id;
722
+ this.callStateForMetrics.correlationId = this.id;
608
723
  }
609
724
  /**
610
725
  * @instance
@@ -672,6 +787,14 @@ export default class Meeting extends StatelessWebexPlugin {
672
787
  */
673
788
  // @ts-ignore
674
789
  this.annotation = new Annotation({parent: this.webex});
790
+ /**
791
+ * @instance
792
+ * @type {Webinar}
793
+ * @public
794
+ * @memberof Meeting
795
+ */
796
+ // @ts-ignore
797
+ this.webinar = new Webinar({}, {parent: this.webex});
675
798
  /**
676
799
  * helper class for managing receive slots (for multistream media connections)
677
800
  */
@@ -1068,13 +1191,14 @@ export default class Meeting extends StatelessWebexPlugin {
1068
1191
  */
1069
1192
  this.networkQualityMonitor = null;
1070
1193
  /**
1194
+ * Indicates network status of the webrtc media connection
1071
1195
  * @instance
1072
1196
  * @type {String}
1073
1197
  * @readonly
1074
1198
  * @public
1075
1199
  * @memberof Meeting
1076
1200
  */
1077
- this.networkStatus = null;
1201
+ this.networkStatus = undefined;
1078
1202
  /**
1079
1203
  * Passing only info as we send basic info for meeting added event
1080
1204
  * @instance
@@ -1139,7 +1263,17 @@ export default class Meeting extends StatelessWebexPlugin {
1139
1263
  * @private
1140
1264
  * @memberof Meeting
1141
1265
  */
1142
- this.transcription = undefined;
1266
+ this.transcription = {
1267
+ captions: [],
1268
+ isListening: false,
1269
+ commandText: '',
1270
+ languageOptions: {},
1271
+ showCaptionBox: false,
1272
+ transcribingRequestStatus: 'INACTIVE',
1273
+ isCaptioning: false,
1274
+ interimCaptions: {} as Map<string, CaptionData>,
1275
+ speakerProxy: {} as Map<string, any>,
1276
+ } as Transcription;
1143
1277
 
1144
1278
  /**
1145
1279
  * Password status. If it's PASSWORD_STATUS.REQUIRED then verifyPassword() needs to be called
@@ -1193,6 +1327,24 @@ export default class Meeting extends StatelessWebexPlugin {
1193
1327
  */
1194
1328
  this.keepAliveTimerId = null;
1195
1329
 
1330
+ /**
1331
+ * id for tracking Local Share instances in Call Analyzer
1332
+ * @instance
1333
+ * @type {String}
1334
+ * @private
1335
+ * @memberof Meeting
1336
+ */
1337
+ this.localShareInstanceId = null;
1338
+
1339
+ /**
1340
+ * id for tracking Remote Share instances in Call Analyzer
1341
+ * @instance
1342
+ * @type {String}
1343
+ * @private
1344
+ * @memberof Meeting
1345
+ */
1346
+ this.remoteShareInstanceId = null;
1347
+
1196
1348
  /**
1197
1349
  * The class that helps to control recording functions: start, stop, pause, resume, etc
1198
1350
  * @instance
@@ -1245,6 +1397,60 @@ export default class Meeting extends StatelessWebexPlugin {
1245
1397
  this.updateTranscodedMediaConnection();
1246
1398
  }
1247
1399
  };
1400
+
1401
+ /**
1402
+ * Promise that exists if SDP offer has been generated, and resolves once sdp answer is received.
1403
+ * @instance
1404
+ * @type {Defer}
1405
+ * @private
1406
+ * @memberof Meeting
1407
+ */
1408
+ this.deferSDPAnswer = undefined;
1409
+
1410
+ /**
1411
+ * Timer for waiting for sdp answer.
1412
+ * @instance
1413
+ * @type {ReturnType<typeof setTimeout>}
1414
+ * @private
1415
+ * @memberof Meeting
1416
+ */
1417
+ this.sdpResponseTimer = undefined;
1418
+
1419
+ /**
1420
+ * Reason why TURN discovery is skipped.
1421
+ * @instance
1422
+ * @type {string}
1423
+ * @public
1424
+ * @memberof Meeting
1425
+ */
1426
+ this.turnDiscoverySkippedReason = undefined;
1427
+
1428
+ /**
1429
+ * Whether TURN discovery is used or not.
1430
+ * @instance
1431
+ * @type {boolean}
1432
+ * @public
1433
+ * @memberof Meeting
1434
+ */
1435
+ this.turnServerUsed = false;
1436
+
1437
+ /**
1438
+ * Whether retry was done using TURN Discovery.
1439
+ * @instance
1440
+ * @type {boolean}
1441
+ * @private
1442
+ * @memberof Meeting
1443
+ */
1444
+ this.retriedWithTurnServer = false;
1445
+
1446
+ /**
1447
+ * Whether or not the media connection has ever successfully connected.
1448
+ * @instance
1449
+ * @type {boolean}
1450
+ * @private
1451
+ * @memberof Meeting
1452
+ */
1453
+ this.hasMediaConnectionConnectedAtLeastOnce = false;
1248
1454
  }
1249
1455
 
1250
1456
  /**
@@ -1278,23 +1484,87 @@ export default class Meeting extends StatelessWebexPlugin {
1278
1484
  }
1279
1485
 
1280
1486
  /**
1281
- * Fetches meeting information.
1282
- * @param {Object} options
1283
- * @param {String} [options.password] optional
1284
- * @param {String} [options.captchaCode] optional
1285
- * @public
1286
- * @memberof Meeting
1287
- * @returns {Promise}
1487
+ * Getter - Returns callStateForMetrics.correlationId
1488
+ * @returns {string}
1288
1489
  */
1289
- public async fetchMeetingInfo({
1290
- password = null,
1291
- captchaCode = null,
1292
- extraParams = {},
1293
- }: {
1294
- password?: string;
1295
- captchaCode?: string;
1296
- extraParams?: Record<string, any>;
1297
- }) {
1490
+ get correlationId() {
1491
+ return this.callStateForMetrics.correlationId;
1492
+ }
1493
+
1494
+ /**
1495
+ * Setter - sets callStateForMetrics.correlationId
1496
+ * @param {string} correlationId
1497
+ */
1498
+ set correlationId(correlationId: string) {
1499
+ this.callStateForMetrics.correlationId = correlationId;
1500
+ }
1501
+
1502
+ /**
1503
+ * Set meeting info and trigger `MEETING_INFO_AVAILABLE` event
1504
+ * @param {any} info
1505
+ * @param {string} [meetingLookupUrl] Lookup url, defined when the meeting info fetched
1506
+ * @returns {void}
1507
+ */
1508
+ private setMeetingInfo(info, meetingLookupUrl) {
1509
+ this.meetingInfo = info ? {...info, meetingLookupUrl} : null;
1510
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NONE;
1511
+
1512
+ this.requiredCaptcha = null;
1513
+ if (
1514
+ this.passwordStatus === PASSWORD_STATUS.REQUIRED ||
1515
+ this.passwordStatus === PASSWORD_STATUS.VERIFIED
1516
+ ) {
1517
+ this.passwordStatus = PASSWORD_STATUS.VERIFIED;
1518
+ } else {
1519
+ this.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
1520
+ }
1521
+
1522
+ Trigger.trigger(
1523
+ this,
1524
+ {
1525
+ file: 'meetings',
1526
+ function: 'fetchMeetingInfo',
1527
+ },
1528
+ EVENT_TRIGGERS.MEETING_INFO_AVAILABLE
1529
+ );
1530
+
1531
+ this.updateMeetingActions();
1532
+ }
1533
+
1534
+ /**
1535
+ * Add pre-fetched meeting info
1536
+ *
1537
+ * The passed meeting info should be be complete, e.g.: fetched after password or captcha provided
1538
+ *
1539
+ * @param {Object} meetingInfo - Complete meeting info
1540
+ * @param {FetchMeetingInfoParams} fetchParams - Fetch parameters for validation
1541
+ * @param {String|undefined} meetingLookupUrl - Lookup url, defined when the meeting info fetched
1542
+ * @returns {Promise<void>}
1543
+ */
1544
+ public async injectMeetingInfo(
1545
+ meetingInfo: any,
1546
+ fetchParams: FetchMeetingInfoParams,
1547
+ meetingLookupUrl: string | undefined
1548
+ ): Promise<void> {
1549
+ await this.prepForFetchMeetingInfo(fetchParams, 'injectMeetingInfo');
1550
+
1551
+ this.parseMeetingInfo(meetingInfo, this.destination);
1552
+ this.setMeetingInfo(meetingInfo, meetingLookupUrl);
1553
+ }
1554
+
1555
+ /**
1556
+ * Validate fetch parameters and clear the fetchMeetingInfoTimeout timeout
1557
+ *
1558
+ * @param {FetchMeetingInfoParams} fetchParams - fetch parameters for validation
1559
+ * @param {String} caller - Name of the caller for logging
1560
+ *
1561
+ * @returns {Promise<void>}
1562
+ * @private
1563
+ */
1564
+ private prepForFetchMeetingInfo(
1565
+ {password = null, captchaCode = null, extraParams = {}}: FetchMeetingInfoParams,
1566
+ caller: string
1567
+ ): Promise<void> {
1298
1568
  // when fetch meeting info is called directly by the client, we want to clear out the random timer for sdk to do it
1299
1569
  if (this.fetchMeetingInfoTimeoutId) {
1300
1570
  clearTimeout(this.fetchMeetingInfoTimeoutId);
@@ -1302,7 +1572,7 @@ export default class Meeting extends StatelessWebexPlugin {
1302
1572
  }
1303
1573
  if (captchaCode && !this.requiredCaptcha) {
1304
1574
  return Promise.reject(
1305
- new Error('fetchMeetingInfo() called with captchaCode when captcha was not required')
1575
+ new Error(`${caller}() called with captchaCode when captcha was not required`)
1306
1576
  );
1307
1577
  }
1308
1578
  if (
@@ -1311,50 +1581,47 @@ export default class Meeting extends StatelessWebexPlugin {
1311
1581
  this.passwordStatus !== PASSWORD_STATUS.UNKNOWN
1312
1582
  ) {
1313
1583
  return Promise.reject(
1314
- new Error('fetchMeetingInfo() called with password when password was not required')
1584
+ new Error(`${caller}() called with password when password was not required`)
1315
1585
  );
1316
1586
  }
1317
1587
 
1588
+ this.meetingInfoExtraParams = cloneDeep(extraParams);
1589
+
1590
+ return Promise.resolve();
1591
+ }
1592
+
1593
+ /**
1594
+ * Internal method for fetching meeting info
1595
+ *
1596
+ * @returns {Promise}
1597
+ */
1598
+ private async fetchMeetingInfoInternal({
1599
+ destination,
1600
+ destinationType,
1601
+ password = null,
1602
+ captchaCode = null,
1603
+ extraParams = {},
1604
+ sendCAevents = false,
1605
+ }): Promise<void> {
1318
1606
  try {
1319
1607
  const captchaInfo = captchaCode
1320
1608
  ? {code: captchaCode, id: this.requiredCaptcha.captchaId}
1321
1609
  : null;
1322
1610
 
1323
1611
  const info = await this.attrs.meetingInfoProvider.fetchMeetingInfo(
1324
- this.destination,
1325
- this.destinationType,
1612
+ destination,
1613
+ destinationType,
1326
1614
  password,
1327
1615
  captchaInfo,
1328
1616
  // @ts-ignore - config coming from registerPlugin
1329
1617
  this.config.installedOrgID,
1330
1618
  this.locusId,
1331
1619
  extraParams,
1332
- {meetingId: this.id}
1333
- );
1334
-
1335
- this.parseMeetingInfo(info, this.destination);
1336
- this.meetingInfo = info ? {...info.body, meetingLookupUrl: info?.url} : null;
1337
- this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NONE;
1338
- this.requiredCaptcha = null;
1339
- if (
1340
- this.passwordStatus === PASSWORD_STATUS.REQUIRED ||
1341
- this.passwordStatus === PASSWORD_STATUS.VERIFIED
1342
- ) {
1343
- this.passwordStatus = PASSWORD_STATUS.VERIFIED;
1344
- } else {
1345
- this.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
1346
- }
1347
-
1348
- Trigger.trigger(
1349
- this,
1350
- {
1351
- file: 'meetings',
1352
- function: 'fetchMeetingInfo',
1353
- },
1354
- EVENT_TRIGGERS.MEETING_INFO_AVAILABLE
1620
+ {meetingId: this.id, sendCAevents}
1355
1621
  );
1356
1622
 
1357
- this.updateMeetingActions();
1623
+ this.parseMeetingInfo(info?.body, this.destination, info?.errors);
1624
+ this.setMeetingInfo(info?.body, info?.url);
1358
1625
 
1359
1626
  return Promise.resolve();
1360
1627
  } catch (err) {
@@ -1416,19 +1683,113 @@ export default class Meeting extends StatelessWebexPlugin {
1416
1683
  }
1417
1684
  }
1418
1685
 
1686
+ /**
1687
+ * Refreshes the meeting info permission token (it's required for joining meetings)
1688
+ *
1689
+ * @param {string} [reason] used for metrics and logging purposes (optional)
1690
+ * @returns {Promise}
1691
+ */
1692
+ public async refreshPermissionToken(reason?: string): Promise<void> {
1693
+ if (!this.meetingInfo?.permissionToken) {
1694
+ LoggerProxy.logger.info(
1695
+ `Meeting:index#refreshPermissionToken --> cannot refresh the permission token, because we don't have it (reason=${reason})`
1696
+ );
1697
+
1698
+ return;
1699
+ }
1700
+
1701
+ const isStartingSpaceInstantV2Meeting =
1702
+ this.destinationType === _CONVERSATION_URL_ &&
1703
+ // @ts-ignore - config coming from registerPlugin
1704
+ this.config.experimental.enableAdhocMeetings &&
1705
+ // @ts-ignore
1706
+ this.webex.meetings.preferredWebexSite;
1707
+
1708
+ const destination = isStartingSpaceInstantV2Meeting
1709
+ ? this.meetingInfo.meetingJoinUrl
1710
+ : this.destination;
1711
+ const destinationType = isStartingSpaceInstantV2Meeting ? _MEETING_LINK_ : this.destinationType;
1712
+
1713
+ const permissionTokenExpiryInfo = this.getPermissionTokenExpiryInfo();
1714
+
1715
+ const timeLeft = permissionTokenExpiryInfo?.timeLeft;
1716
+ const expiryTime = permissionTokenExpiryInfo?.expiryTime;
1717
+ const currentTime = permissionTokenExpiryInfo?.currentTime;
1718
+
1719
+ LoggerProxy.logger.info(
1720
+ `Meeting:index#refreshPermissionToken --> refreshing permission token, destinationType=${destinationType}, timeLeft=${timeLeft}, permissionTokenExpiry=${expiryTime}, currentTimestamp=${currentTime},reason=${reason}`
1721
+ );
1722
+
1723
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.PERMISSION_TOKEN_REFRESH, {
1724
+ correlationId: this.correlationId,
1725
+ timeLeft,
1726
+ expiryTime,
1727
+ currentTime,
1728
+ reason,
1729
+ destinationType,
1730
+ });
1731
+
1732
+ try {
1733
+ await this.fetchMeetingInfoInternal({
1734
+ destination,
1735
+ destinationType,
1736
+ extraParams: {
1737
+ ...this.meetingInfoExtraParams,
1738
+ permissionToken: this.meetingInfo.permissionToken,
1739
+ },
1740
+ sendCAevents: true, // because if we're refreshing the permissionToken, it means that user is intending to join that meeting, so we want CA events
1741
+ });
1742
+ } catch (error) {
1743
+ LoggerProxy.logger.info(
1744
+ 'Meeting:index#refreshPermissionToken --> failed to refresh the permission token:',
1745
+ error
1746
+ );
1747
+
1748
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.PERMISSION_TOKEN_REFRESH_ERROR, {
1749
+ correlationId: this.correlationId,
1750
+ reason: error.message,
1751
+ stack: error.stack,
1752
+ });
1753
+
1754
+ throw error;
1755
+ }
1756
+ }
1757
+
1758
+ /**
1759
+ * Fetches meeting information.
1760
+ * @param {Object} options
1761
+ * @param {String} [options.password] optional
1762
+ * @param {String} [options.captchaCode] optional
1763
+ * @param {Boolean} [options.sendCAevents] optional - Whether to submit Call Analyzer events or not. Default: false.
1764
+ * @public
1765
+ * @memberof Meeting
1766
+ * @returns {Promise}
1767
+ */
1768
+ public async fetchMeetingInfo(options: FetchMeetingInfoParams) {
1769
+ await this.prepForFetchMeetingInfo(options, 'fetchMeetingInfo');
1770
+
1771
+ return this.fetchMeetingInfoInternal({
1772
+ destination: this.destination,
1773
+ destinationType: this.destinationType,
1774
+ ...options,
1775
+ });
1776
+ }
1777
+
1419
1778
  /**
1420
1779
  * Checks if the supplied password/host key is correct. It returns a promise with information whether the
1421
1780
  * password and captcha code were correct or not.
1422
1781
  * @param {String} password - this can be either a password or a host key, can be undefined if only captcha was required
1423
1782
  * @param {String} captchaCode - can be undefined if captcha was not required by the server
1783
+ * @param {Boolean} sendCAevents - whether Call Analyzer events should be sent when fetching meeting information
1424
1784
  * @public
1425
1785
  * @memberof Meeting
1426
1786
  * @returns {Promise<{isPasswordValid: boolean, requiredCaptcha: boolean, failureReason: MEETING_INFO_FAILURE_REASON}>}
1427
1787
  */
1428
- public verifyPassword(password: string, captchaCode: string) {
1788
+ public verifyPassword(password: string, captchaCode: string, sendCAevents = false) {
1429
1789
  return this.fetchMeetingInfo({
1430
1790
  password,
1431
1791
  captchaCode,
1792
+ sendCAevents,
1432
1793
  })
1433
1794
  .then(() => {
1434
1795
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.VERIFY_PASSWORD_SUCCESS);
@@ -1628,6 +1989,7 @@ export default class Meeting extends StatelessWebexPlugin {
1628
1989
  * @memberof Meeting
1629
1990
  */
1630
1991
  private setUpInterpretationListener() {
1992
+ // TODO: check if its getting used or not
1631
1993
  this.simultaneousInterpretation.on(INTERPRETATION.EVENTS.SUPPORT_LANGUAGES_UPDATE, () => {
1632
1994
  Trigger.trigger(
1633
1995
  this,
@@ -1638,7 +2000,7 @@ export default class Meeting extends StatelessWebexPlugin {
1638
2000
  EVENT_TRIGGERS.MEETING_INTERPRETATION_SUPPORT_LANGUAGES_UPDATE
1639
2001
  );
1640
2002
  });
1641
-
2003
+ // TODO: check if its getting used or not
1642
2004
  this.simultaneousInterpretation.on(
1643
2005
  INTERPRETATION.EVENTS.HANDOFF_REQUESTS_ARRIVED,
1644
2006
  (payload) => {
@@ -1655,6 +2017,43 @@ export default class Meeting extends StatelessWebexPlugin {
1655
2017
  );
1656
2018
  }
1657
2019
 
2020
+ /**
2021
+ * Set up the listeners for captions
2022
+ * @returns {undefined}
2023
+ * @private
2024
+ * @memberof Meeting
2025
+ */
2026
+ private setUpVoiceaListeners() {
2027
+ // @ts-ignore
2028
+ this.webex.internal.voicea.listenToEvents();
2029
+
2030
+ // @ts-ignore
2031
+ this.webex.internal.voicea.on(
2032
+ VOICEAEVENTS.VOICEA_ANNOUNCEMENT,
2033
+ this.voiceaListenerCallbacks[VOICEAEVENTS.VOICEA_ANNOUNCEMENT]
2034
+ );
2035
+
2036
+ // @ts-ignore
2037
+ this.webex.internal.voicea.on(
2038
+ VOICEAEVENTS.CAPTIONS_TURNED_ON,
2039
+ this.voiceaListenerCallbacks[VOICEAEVENTS.CAPTIONS_TURNED_ON]
2040
+ );
2041
+
2042
+ // @ts-ignore
2043
+ this.webex.internal.voicea.on(
2044
+ VOICEAEVENTS.EVA_COMMAND,
2045
+ this.voiceaListenerCallbacks[VOICEAEVENTS.EVA_COMMAND]
2046
+ );
2047
+
2048
+ // @ts-ignore
2049
+ this.webex.internal.voicea.on(
2050
+ VOICEAEVENTS.NEW_CAPTION,
2051
+ this.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]
2052
+ );
2053
+
2054
+ this.areVoiceaEventsSetup = true;
2055
+ }
2056
+
1658
2057
  /**
1659
2058
  * Set up the locus info listener for meetings disconnected due to inactivity
1660
2059
  * @returns {undefined}
@@ -1759,12 +2158,12 @@ export default class Meeting extends StatelessWebexPlugin {
1759
2158
 
1760
2159
  /**
1761
2160
  * sets the network status on meeting object
1762
- * @param {String} networkStatus
2161
+ * @param {NETWORK_STATUS} networkStatus
1763
2162
  * @private
1764
2163
  * @returns {undefined}
1765
2164
  * @memberof Meeting
1766
2165
  */
1767
- private setNetworkStatus(networkStatus: string) {
2166
+ private setNetworkStatus(networkStatus?: NETWORK_STATUS) {
1768
2167
  if (networkStatus === NETWORK_STATUS.DISCONNECTED) {
1769
2168
  Trigger.trigger(
1770
2169
  this,
@@ -1944,7 +2343,6 @@ export default class Meeting extends StatelessWebexPlugin {
1944
2343
  modifiedBy,
1945
2344
  lastModified,
1946
2345
  };
1947
-
1948
2346
  Trigger.trigger(
1949
2347
  this,
1950
2348
  {
@@ -1975,19 +2373,22 @@ export default class Meeting extends StatelessWebexPlugin {
1975
2373
  this.locusInfo.on(
1976
2374
  LOCUSINFO.EVENTS.CONTROLS_MEETING_TRANSCRIBE_UPDATED,
1977
2375
  ({caption, transcribing}) => {
1978
- // @ts-ignore - config coming from registerPlugin
1979
- if (transcribing && this.transcription && this.config.receiveTranscription) {
1980
- this.receiveTranscription();
1981
- } else if (!transcribing && this.transcription) {
1982
- Trigger.trigger(
1983
- this,
1984
- {
1985
- file: 'meeting/index',
1986
- function: 'setupLocusControlsListener',
1987
- },
1988
- EVENT_TRIGGERS.MEETING_STOPPED_RECEIVING_TRANSCRIPTION,
1989
- {caption, transcribing}
1990
- );
2376
+ // user need to be joined to start the llm and receive transcription
2377
+ if (this.isJoined()) {
2378
+ // @ts-ignore - config coming from registerPlugin
2379
+ if (transcribing && !this.transcription) {
2380
+ this.startTranscription();
2381
+ } else if (!transcribing && this.transcription) {
2382
+ Trigger.trigger(
2383
+ this,
2384
+ {
2385
+ file: 'meeting/index',
2386
+ function: 'setupLocusControlsListener',
2387
+ },
2388
+ EVENT_TRIGGERS.MEETING_STOPPED_RECEIVING_TRANSCRIPTION,
2389
+ {caption, transcribing}
2390
+ );
2391
+ }
1991
2392
  }
1992
2393
  }
1993
2394
  );
@@ -2019,16 +2420,6 @@ export default class Meeting extends StatelessWebexPlugin {
2019
2420
  }
2020
2421
  );
2021
2422
 
2022
- this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_JOIN_BREAKOUT_FROM_MAIN, ({mainLocusUrl}) => {
2023
- this.meetingRequest.getLocusStatusByUrl(mainLocusUrl).catch((error) => {
2024
- // clear main session cache when attendee join into breakout and forbidden to get locus from main locus url,
2025
- // which means main session is not active for the attendee
2026
- if (error?.statusCode === 403) {
2027
- this.locusInfo.clearMainSessionLocusCache();
2028
- }
2029
- });
2030
- });
2031
-
2032
2423
  this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_ENTRY_EXIT_TONE_UPDATED, ({entryExitTone}) => {
2033
2424
  Trigger.trigger(
2034
2425
  this,
@@ -2152,6 +2543,7 @@ export default class Meeting extends StatelessWebexPlugin {
2152
2543
  if (
2153
2544
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
2154
2545
  contentShare.disposition === previousContentShare?.disposition &&
2546
+ contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
2155
2547
  whiteboardShare.beneficiaryId === previousWhiteboardShare?.beneficiaryId &&
2156
2548
  whiteboardShare.disposition === previousWhiteboardShare?.disposition &&
2157
2549
  whiteboardShare.resourceUrl === previousWhiteboardShare?.resourceUrl
@@ -2174,11 +2566,21 @@ export default class Meeting extends StatelessWebexPlugin {
2174
2566
  // LOCAL - check if we started sharing content
2175
2567
  else if (
2176
2568
  this.selfId === contentShare.beneficiaryId &&
2177
- contentShare.disposition === FLOOR_ACTION.GRANTED
2569
+ contentShare.disposition === FLOOR_ACTION.GRANTED &&
2570
+ contentShare.deviceUrlSharing === this.deviceUrl
2178
2571
  ) {
2179
2572
  // CONTENT - sharing content local
2180
2573
  newShareStatus = SHARE_STATUS.LOCAL_SHARE_ACTIVE;
2181
2574
  }
2575
+ // SAME USER REMOTE - check if same user started sharing content from another client
2576
+ else if (
2577
+ this.selfId === contentShare.beneficiaryId &&
2578
+ contentShare.disposition === FLOOR_ACTION.GRANTED &&
2579
+ contentShare.deviceUrlSharing !== this.deviceUrl
2580
+ ) {
2581
+ // CONTENT - same user sharing content remote
2582
+ newShareStatus = SHARE_STATUS.REMOTE_SHARE_ACTIVE;
2583
+ }
2182
2584
  // If we did not hit the cases above, no one is sharng content, so we check if we are sharing whiteboard
2183
2585
  // There is no concept of local/remote share for whiteboard
2184
2586
  // It does not matter who requested to share the whiteboard, everyone gets the same view
@@ -2252,6 +2654,8 @@ export default class Meeting extends StatelessWebexPlugin {
2252
2654
  switch (newShareStatus) {
2253
2655
  case SHARE_STATUS.REMOTE_SHARE_ACTIVE: {
2254
2656
  const sendStartedSharingRemote = () => {
2657
+ this.remoteShareInstanceId = contentShare.shareInstanceId;
2658
+
2255
2659
  Trigger.trigger(
2256
2660
  this,
2257
2661
  {
@@ -2262,7 +2666,7 @@ export default class Meeting extends StatelessWebexPlugin {
2262
2666
  {
2263
2667
  memberId: contentShare.beneficiaryId,
2264
2668
  url: contentShare.url,
2265
- shareInstanceId: contentShare.shareInstanceId,
2669
+ shareInstanceId: this.remoteShareInstanceId,
2266
2670
  annotationInfo: contentShare.annotation,
2267
2671
  }
2268
2672
  );
@@ -2299,6 +2703,7 @@ export default class Meeting extends StatelessWebexPlugin {
2299
2703
  name: 'client.share.floor-granted.local',
2300
2704
  payload: {
2301
2705
  mediaType: 'share',
2706
+ shareInstanceId: this.localShareInstanceId,
2302
2707
  },
2303
2708
  options: {meetingId: this.id},
2304
2709
  });
@@ -2341,6 +2746,8 @@ export default class Meeting extends StatelessWebexPlugin {
2341
2746
  } else if (newShareStatus === SHARE_STATUS.REMOTE_SHARE_ACTIVE) {
2342
2747
  // if we got here, then some remote participant has stolen
2343
2748
  // the presentation from another remote participant
2749
+ this.remoteShareInstanceId = contentShare.shareInstanceId;
2750
+
2344
2751
  Trigger.trigger(
2345
2752
  this,
2346
2753
  {
@@ -2351,7 +2758,7 @@ export default class Meeting extends StatelessWebexPlugin {
2351
2758
  {
2352
2759
  memberId: contentShare.beneficiaryId,
2353
2760
  url: contentShare.url,
2354
- shareInstanceId: contentShare.shareInstanceId,
2761
+ shareInstanceId: this.remoteShareInstanceId,
2355
2762
  annotationInfo: contentShare.annotation,
2356
2763
  }
2357
2764
  );
@@ -2403,6 +2810,7 @@ export default class Meeting extends StatelessWebexPlugin {
2403
2810
  this.locusId = this.locusUrl?.split('/').pop();
2404
2811
  this.recordingController.setLocusUrl(this.locusUrl);
2405
2812
  this.controlsOptionsManager.setLocusUrl(this.locusUrl);
2813
+ this.webinar.locusUrlUpdate(payload);
2406
2814
 
2407
2815
  Trigger.trigger(
2408
2816
  this,
@@ -2432,6 +2840,10 @@ export default class Meeting extends StatelessWebexPlugin {
2432
2840
  this.breakouts.breakoutServiceUrlUpdate(payload?.services?.breakout?.url);
2433
2841
  this.annotation.approvalUrlUpdate(payload?.services?.approval?.url);
2434
2842
  this.simultaneousInterpretation.approvalUrlUpdate(payload?.services?.approval?.url);
2843
+ this.webinar.webcastUrlUpdate(payload?.services?.webcast?.url);
2844
+ this.webinar.webinarAttendeesSearchingUrlUpdate(
2845
+ payload?.services?.webinarAttendeesSearching?.url
2846
+ );
2435
2847
  });
2436
2848
  }
2437
2849
 
@@ -2472,12 +2884,24 @@ export default class Meeting extends StatelessWebexPlugin {
2472
2884
  );
2473
2885
  }
2474
2886
  });
2475
- this.locusInfo.on(LOCUSINFO.EVENTS.MEETING_INFO_UPDATED, () => {
2887
+ this.locusInfo.on(LOCUSINFO.EVENTS.MEETING_INFO_UPDATED, ({isInitializing}) => {
2476
2888
  this.updateMeetingActions();
2477
2889
  this.recordingController.setDisplayHints(this.userDisplayHints);
2478
2890
  this.recordingController.setUserPolicy(this.selfUserPolicies);
2479
2891
  this.controlsOptionsManager.setDisplayHints(this.userDisplayHints);
2480
2892
  this.handleDataChannelUrlChange(this.datachannelUrl);
2893
+
2894
+ if (!isInitializing) {
2895
+ // send updated trigger only if locus is not initializing the meeting
2896
+ Trigger.trigger(
2897
+ this,
2898
+ {
2899
+ file: 'meetings',
2900
+ function: 'setUpLocusInfoMeetingInfoListener',
2901
+ },
2902
+ EVENT_TRIGGERS.MEETING_INFO_UPDATED
2903
+ );
2904
+ }
2481
2905
  });
2482
2906
  }
2483
2907
 
@@ -2623,7 +3047,7 @@ export default class Meeting extends StatelessWebexPlugin {
2623
3047
  });
2624
3048
  }
2625
3049
  });
2626
- this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, (payload) => {
3050
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, async (payload) => {
2627
3051
  this.stopKeepAlive();
2628
3052
 
2629
3053
  if (payload) {
@@ -2722,6 +3146,7 @@ export default class Meeting extends StatelessWebexPlugin {
2722
3146
  this.simultaneousInterpretation.updateCanManageInterpreters(
2723
3147
  payload.newRoles?.includes(SELF_ROLES.MODERATOR)
2724
3148
  );
3149
+ this.webinar.updateCanManageWebcast(payload.newRoles?.includes(SELF_ROLES.MODERATOR));
2725
3150
  Trigger.trigger(
2726
3151
  this,
2727
3152
  {
@@ -2963,30 +3388,40 @@ export default class Meeting extends StatelessWebexPlugin {
2963
3388
  /**
2964
3389
  * Sets the meeting info on the class instance
2965
3390
  * @param {Object} meetingInfo
2966
- * @param {Object} meetingInfo.body
2967
- * @param {String} meetingInfo.body.conversationUrl
2968
- * @param {String} meetingInfo.body.locusUrl
2969
- * @param {String} meetingInfo.body.sipUri
2970
- * @param {Object} meetingInfo.body.owner
3391
+ * @param {String} meetingInfo.conversationUrl
3392
+ * @param {String} meetingInfo.locusUrl
3393
+ * @param {String} meetingInfo.sipUri
3394
+ * @param {String} [meetingInfo.sipUrl]
3395
+ * @param {String} [meetingInfo.sipMeetingUri]
3396
+ * @param {String} [meetingInfo.meetingNumber]
3397
+ * @param {String} [meetingInfo.meetingJoinUrl]
3398
+ * @param {String} [meetingInfo.hostId]
3399
+ * @param {String} [meetingInfo.permissionToken]
3400
+ * @param {String} [meetingInfo.channel]
3401
+ * @param {Object} meetingInfo.owner
2971
3402
  * @param {Object | String} destination locus object with meeting data or destination string (sip url, meeting link, etc)
3403
+ * @param {Object | String} errors Meeting info request error
2972
3404
  * @returns {undefined}
2973
3405
  * @private
2974
3406
  * @memberof Meeting
2975
3407
  */
2976
3408
  parseMeetingInfo(
2977
- meetingInfo:
2978
- | {
2979
- body: {
2980
- conversationUrl: string;
2981
- locusUrl: string;
2982
- sipUri: string;
2983
- owner: object;
2984
- };
2985
- }
2986
- | any,
2987
- destination: object | string | null = null
3409
+ meetingInfo: {
3410
+ conversationUrl: string;
3411
+ locusUrl: string;
3412
+ sipUri: string;
3413
+ owner: object;
3414
+ sipUrl?: string;
3415
+ sipMeetingUri?: string;
3416
+ meetingNumber?: string;
3417
+ meetingJoinUrl?: string;
3418
+ hostId?: string;
3419
+ permissionToken?: string;
3420
+ channel?: string;
3421
+ },
3422
+ destination: object | string | null = null,
3423
+ errors: any = undefined
2988
3424
  ) {
2989
- const webexMeetingInfo = meetingInfo?.body;
2990
3425
  // We try to use as much info from Locus meeting object, stored in destination
2991
3426
 
2992
3427
  let locusMeetingObject;
@@ -2996,39 +3431,31 @@ export default class Meeting extends StatelessWebexPlugin {
2996
3431
  }
2997
3432
 
2998
3433
  // MeetingInfo will be undefined for 1:1 calls
2999
- if (
3000
- locusMeetingObject ||
3001
- (webexMeetingInfo && !(meetingInfo?.errors && meetingInfo?.errors.length > 0))
3002
- ) {
3434
+ if (locusMeetingObject || (meetingInfo && !(errors?.length > 0))) {
3003
3435
  this.conversationUrl =
3004
- locusMeetingObject?.conversationUrl ||
3005
- webexMeetingInfo?.conversationUrl ||
3006
- this.conversationUrl;
3007
- this.locusUrl = locusMeetingObject?.url || webexMeetingInfo?.locusUrl || this.locusUrl;
3436
+ locusMeetingObject?.conversationUrl || meetingInfo?.conversationUrl || this.conversationUrl;
3437
+ this.locusUrl = locusMeetingObject?.url || meetingInfo?.locusUrl || this.locusUrl;
3008
3438
  // @ts-ignore - config coming from registerPlugin
3009
3439
  this.setSipUri(
3010
3440
  // @ts-ignore
3011
3441
  this.config.experimental.enableUnifiedMeetings
3012
- ? locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipUrl
3013
- : locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipMeetingUri || this.sipUri
3442
+ ? locusMeetingObject?.info.sipUri || meetingInfo?.sipUrl
3443
+ : locusMeetingObject?.info.sipUri || meetingInfo?.sipMeetingUri || this.sipUri
3014
3444
  );
3015
3445
  // @ts-ignore - config coming from registerPlugin
3016
3446
  if (this.config.experimental.enableUnifiedMeetings) {
3017
- this.meetingNumber =
3018
- locusMeetingObject?.info.webExMeetingId || webexMeetingInfo?.meetingNumber;
3019
- this.meetingJoinUrl = webexMeetingInfo?.meetingJoinUrl;
3447
+ this.meetingNumber = locusMeetingObject?.info.webExMeetingId || meetingInfo?.meetingNumber;
3448
+ this.meetingJoinUrl = meetingInfo?.meetingJoinUrl;
3020
3449
  }
3021
3450
  this.owner =
3022
- locusMeetingObject?.info.owner ||
3023
- webexMeetingInfo?.owner ||
3024
- webexMeetingInfo?.hostId ||
3025
- this.owner;
3026
- this.permissionToken = webexMeetingInfo?.permissionToken;
3027
- this.setSelfUserPolicies(this.permissionToken);
3451
+ locusMeetingObject?.info.owner || meetingInfo?.owner || meetingInfo?.hostId || this.owner;
3452
+ this.permissionToken = meetingInfo?.permissionToken;
3453
+ this.setPermissionTokenPayload(meetingInfo?.permissionToken);
3454
+ this.setSelfUserPolicies();
3028
3455
  // Need to populate environment when sending CA event
3029
- this.environment = locusMeetingObject?.info.channel || webexMeetingInfo?.channel;
3456
+ this.environment = locusMeetingObject?.info.channel || meetingInfo?.channel;
3030
3457
  }
3031
- MeetingUtil.parseInterpretationInfo(this, webexMeetingInfo);
3458
+ MeetingUtil.parseInterpretationInfo(this, meetingInfo);
3032
3459
  }
3033
3460
 
3034
3461
  /**
@@ -3080,6 +3507,11 @@ export default class Meeting extends StatelessWebexPlugin {
3080
3507
  }) &&
3081
3508
  this.meetingInfo?.video?.supportHDV) ||
3082
3509
  !this.arePolicyRestrictionsSupported(),
3510
+ enforceVirtualBackground:
3511
+ ControlsOptionsUtil.hasPolicies({
3512
+ requiredPolicies: [SELF_POLICY.ENFORCE_VIRTUAL_BACKGROUND],
3513
+ policies: this.selfUserPolicies,
3514
+ }) && this.arePolicyRestrictionsSupported(),
3083
3515
  supportHQV:
3084
3516
  (ControlsOptionsUtil.hasPolicies({
3085
3517
  requiredPolicies: [SELF_POLICY.SUPPORT_HQV],
@@ -3233,6 +3665,10 @@ export default class Meeting extends StatelessWebexPlugin {
3233
3665
  requiredPolicies: [SELF_POLICY.SUPPORT_FILE_TRANSFER],
3234
3666
  policies: this.selfUserPolicies,
3235
3667
  }),
3668
+ canChat: ControlsOptionsUtil.hasPolicies({
3669
+ requiredPolicies: [SELF_POLICY.SUPPORT_CHAT],
3670
+ policies: this.selfUserPolicies,
3671
+ }),
3236
3672
  canShareApplication:
3237
3673
  (ControlsOptionsUtil.hasHints({
3238
3674
  requiredHints: [DISPLAY_HINTS.SHARE_APPLICATION],
@@ -3288,11 +3724,22 @@ export default class Meeting extends StatelessWebexPlugin {
3288
3724
 
3289
3725
  /**
3290
3726
  * Sets the self user policies based on the contents of the permission token
3727
+ * @returns {void}
3728
+ */
3729
+ setSelfUserPolicies() {
3730
+ this.selfUserPolicies = this.permissionTokenPayload?.permission?.userPolicies;
3731
+ this.enforceVBGImagesURL = this.permissionTokenPayload?.permission?.enforceVBGImagesURL;
3732
+ }
3733
+
3734
+ /**
3735
+ * Sets the permission token payload on the class instance
3736
+ *
3291
3737
  * @param {String} permissionToken
3292
3738
  * @returns {void}
3293
3739
  */
3294
- setSelfUserPolicies(permissionToken: string) {
3295
- this.selfUserPolicies = jwt.decode(permissionToken)?.permission?.userPolicies;
3740
+ public setPermissionTokenPayload(permissionToken: string) {
3741
+ this.permissionTokenPayload = jwtDecode(permissionToken);
3742
+ this.permissionTokenReceivedLocalTime = new Date().getTime();
3296
3743
  }
3297
3744
 
3298
3745
  /**
@@ -3386,8 +3833,7 @@ export default class Meeting extends StatelessWebexPlugin {
3386
3833
  * @memberof Meeting
3387
3834
  */
3388
3835
  closeRemoteStreams() {
3389
- const {remoteAudioStream, remoteVideoStream, remoteShareStream, shareAudioStream} =
3390
- this.mediaProperties;
3836
+ const {remoteAudioStream, remoteVideoStream, remoteShareStream} = this.mediaProperties;
3391
3837
 
3392
3838
  /**
3393
3839
  * Triggers an event to the developer
@@ -3428,7 +3874,6 @@ export default class Meeting extends StatelessWebexPlugin {
3428
3874
  stopStream(remoteAudioStream, EVENT_TYPES.REMOTE_AUDIO),
3429
3875
  stopStream(remoteVideoStream, EVENT_TYPES.REMOTE_VIDEO),
3430
3876
  stopStream(remoteShareStream, EVENT_TYPES.REMOTE_SHARE),
3431
- stopStream(shareAudioStream, EVENT_TYPES.REMOTE_SHARE_AUDIO),
3432
3877
  ]);
3433
3878
  }
3434
3879
 
@@ -3499,11 +3944,16 @@ export default class Meeting extends StatelessWebexPlugin {
3499
3944
  private async setLocalShareVideoStream(localDisplayStream?: LocalDisplayStream) {
3500
3945
  const oldStream = this.mediaProperties.shareVideoStream;
3501
3946
 
3947
+ oldStream?.off(StreamEventNames.MuteStateChange, this.handleShareVideoStreamMuteStateChange);
3502
3948
  oldStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3503
3949
  oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3504
3950
 
3505
3951
  this.mediaProperties.setLocalShareVideoStream(localDisplayStream);
3506
3952
 
3953
+ localDisplayStream?.on(
3954
+ StreamEventNames.MuteStateChange,
3955
+ this.handleShareVideoStreamMuteStateChange
3956
+ );
3507
3957
  localDisplayStream?.on(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3508
3958
  localDisplayStream?.on(
3509
3959
  LocalStreamEventNames.OutputTrackChange,
@@ -3559,7 +4009,7 @@ export default class Meeting extends StatelessWebexPlugin {
3559
4009
  functionName: string;
3560
4010
  isPublished: boolean;
3561
4011
  mediaType: MediaType;
3562
- stream: MediaStream;
4012
+ stream: LocalStream;
3563
4013
  }) {
3564
4014
  const {functionName, isPublished, mediaType, stream} = options;
3565
4015
  Trigger.trigger(
@@ -3593,12 +4043,16 @@ export default class Meeting extends StatelessWebexPlugin {
3593
4043
  videoStream?.off(StreamEventNames.MuteStateChange, this.localVideoStreamMuteStateHandler);
3594
4044
  videoStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3595
4045
 
3596
- shareAudioStream?.off(StreamEventNames.MuteStateChange, this.handleShareAudioStreamEnded);
4046
+ shareAudioStream?.off(StreamEventNames.Ended, this.handleShareAudioStreamEnded);
3597
4047
  shareAudioStream?.off(
3598
4048
  LocalStreamEventNames.OutputTrackChange,
3599
4049
  this.localOutputTrackChangeHandler
3600
4050
  );
3601
- shareVideoStream?.off(StreamEventNames.MuteStateChange, this.handleShareVideoStreamEnded);
4051
+ shareVideoStream?.off(
4052
+ StreamEventNames.MuteStateChange,
4053
+ this.handleShareVideoStreamMuteStateChange
4054
+ );
4055
+ shareVideoStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3602
4056
  shareVideoStream?.off(
3603
4057
  LocalStreamEventNames.OutputTrackChange,
3604
4058
  this.localOutputTrackChangeHandler
@@ -3712,6 +4166,7 @@ export default class Meeting extends StatelessWebexPlugin {
3712
4166
  this.receiveSlotManager.reset();
3713
4167
  this.mediaProperties.webrtcMediaConnection.close();
3714
4168
  this.sendSlotManager.reset();
4169
+ this.setNetworkStatus(undefined);
3715
4170
  }
3716
4171
 
3717
4172
  this.audio = null;
@@ -3733,18 +4188,31 @@ export default class Meeting extends StatelessWebexPlugin {
3733
4188
  if (this.config.reconnection.detection) {
3734
4189
  // @ts-ignore
3735
4190
  this.webex.internal.mercury.off(ONLINE);
4191
+ // @ts-ignore
4192
+ this.webex.internal.mercury.off(OFFLINE);
3736
4193
  }
3737
4194
  }
3738
4195
 
3739
4196
  /**
3740
- * Convenience method to set the correlation id for the Meeting
3741
- * @param {String} id correlation id to set on the class
4197
+ * Convenience method to set the correlation id for the callStateForMetrics
4198
+ * @param {String} id correlation id to set on the callStateForMetrics
3742
4199
  * @returns {undefined}
3743
- * @private
4200
+ * @public
4201
+ * @memberof Meeting
4202
+ */
4203
+ public setCorrelationId(id: string) {
4204
+ this.callStateForMetrics.correlationId = id;
4205
+ }
4206
+
4207
+ /**
4208
+ * Update the callStateForMetrics
4209
+ * @param {CallStateForMetrics} callStateForMetrics updated values for callStateForMetrics
4210
+ * @returns {undefined}
4211
+ * @public
3744
4212
  * @memberof Meeting
3745
4213
  */
3746
- private setCorrelationId(id: string) {
3747
- this.correlationId = id;
4214
+ public updateCallStateForMetrics(callStateForMetrics: CallStateForMetrics) {
4215
+ this.callStateForMetrics = {...this.callStateForMetrics, ...callStateForMetrics};
3748
4216
  }
3749
4217
 
3750
4218
  /**
@@ -3981,6 +4449,14 @@ export default class Meeting extends StatelessWebexPlugin {
3981
4449
  ) {
3982
4450
  const {mediaOptions, joinOptions} = options;
3983
4451
 
4452
+ if (!mediaOptions?.allowMediaInLobby) {
4453
+ return Promise.reject(
4454
+ new ParameterError('joinWithMedia() can only be used with allowMediaInLobby set to true')
4455
+ );
4456
+ }
4457
+
4458
+ LoggerProxy.logger.info('Meeting:index#joinWithMedia called');
4459
+
3984
4460
  return this.join(joinOptions)
3985
4461
  .then((joinResponse) =>
3986
4462
  this.addMedia(mediaOptions).then((mediaResponse) => ({
@@ -4062,6 +4538,8 @@ export default class Meeting extends StatelessWebexPlugin {
4062
4538
 
4063
4539
  return this.reconnectionManager
4064
4540
  .reconnect(options)
4541
+ .then(() => this.waitForRemoteSDPAnswer())
4542
+ .then(() => this.waitForMediaConnectionConnected())
4065
4543
  .then(() => {
4066
4544
  Trigger.trigger(
4067
4545
  this,
@@ -4072,6 +4550,18 @@ export default class Meeting extends StatelessWebexPlugin {
4072
4550
  EVENT_TRIGGERS.MEETING_RECONNECTION_SUCCESS
4073
4551
  );
4074
4552
  LoggerProxy.logger.log('Meeting:index#reconnect --> Meeting reconnect success');
4553
+
4554
+ // @ts-ignore
4555
+ this.webex.internal.newMetrics.submitClientEvent({
4556
+ name: 'client.media.recovered',
4557
+ payload: {
4558
+ recoveredBy: 'new',
4559
+ },
4560
+ options: {
4561
+ meetingId: this.id,
4562
+ },
4563
+ });
4564
+ this.reconnectionManager.setStatus(RECONNECTION.STATE.COMPLETE);
4075
4565
  })
4076
4566
  .catch((error) => {
4077
4567
  Trigger.trigger(
@@ -4118,7 +4608,7 @@ export default class Meeting extends StatelessWebexPlugin {
4118
4608
  }
4119
4609
 
4120
4610
  LoggerProxy.logger.error(
4121
- 'Meeting:index#isTranscriptionSupported --> Webex Assistant is not supported'
4611
+ 'Meeting:index#isTranscriptionSupported --> Webex Assistant is not enabled/supported'
4122
4612
  );
4123
4613
 
4124
4614
  return false;
@@ -4139,109 +4629,139 @@ export default class Meeting extends StatelessWebexPlugin {
4139
4629
  }
4140
4630
 
4141
4631
  /**
4142
- * Monitor the Low-Latency Mercury (LLM) web socket connection on `onError` and `onClose` states
4143
- * @private
4144
- * @returns {void}
4632
+ * sets Caption language for the meeting
4633
+ * @param {string} language
4634
+ * @returns {Promise}
4145
4635
  */
4146
- private monitorTranscriptionSocketConnection() {
4147
- this.transcription.onCloseSocket((event) => {
4148
- LoggerProxy.logger.info(
4149
- `Meeting:index#onCloseSocket -->
4150
- unable to continue receiving transcription;
4151
- low-latency mercury web socket connection is closed now.
4152
- ${event}`
4153
- );
4636
+ public setCaptionLanguage(language: string) {
4637
+ return new Promise((resolve, reject) => {
4638
+ if (!this.isTranscriptionSupported()) {
4639
+ LoggerProxy.logger.error(
4640
+ 'Meeting:index#setCaptionLanguage --> Webex Assistant is not enabled/supported'
4641
+ );
4154
4642
 
4155
- this.triggerStopReceivingTranscriptionEvent();
4156
- });
4643
+ reject(new Error('Webex Assistant is not enabled/supported'));
4644
+ }
4157
4645
 
4158
- this.transcription.onErrorSocket((event) => {
4159
- LoggerProxy.logger.error(
4160
- `Meeting:index#onErrorSocket -->
4161
- unable to continue receiving transcription;
4162
- low-latency mercury web socket connection error had occured.
4163
- ${event}`
4164
- );
4646
+ try {
4647
+ const voiceaListenerCaptionUpdate = (payload) => {
4648
+ // @ts-ignore
4649
+ this.webex.internal.voicea.off(
4650
+ VOICEAEVENTS.CAPTION_LANGUAGE_UPDATE,
4651
+ voiceaListenerCaptionUpdate
4652
+ );
4653
+ const {statusCode} = payload;
4165
4654
 
4166
- this.triggerStopReceivingTranscriptionEvent();
4655
+ if (statusCode === 200) {
4656
+ this.transcription.languageOptions = {
4657
+ ...this.transcription.languageOptions,
4658
+ currentCaptionLanguage: language,
4659
+ };
4660
+ resolve(language);
4661
+ } else {
4662
+ reject(payload);
4663
+ }
4664
+ };
4665
+ // @ts-ignore
4666
+ this.webex.internal.voicea.on(
4667
+ VOICEAEVENTS.CAPTION_LANGUAGE_UPDATE,
4668
+ voiceaListenerCaptionUpdate
4669
+ );
4670
+ // @ts-ignore
4671
+ this.webex.internal.voicea.requestLanguage(language);
4672
+ } catch (error) {
4673
+ LoggerProxy.logger.error(`Meeting:index#setCaptionLanguage --> ${error}`);
4167
4674
 
4168
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.RECEIVE_TRANSCRIPTION_FAILURE, {
4169
- correlation_id: this.correlationId,
4170
- reason: 'unexpected error: transcription LLM web socket connection error had occured.',
4171
- event,
4172
- });
4675
+ reject(error);
4676
+ }
4173
4677
  });
4174
4678
  }
4175
4679
 
4176
4680
  /**
4177
- * Request for a WebSocket Url, open and monitor the WebSocket connection
4178
- * @private
4179
- * @returns {Promise<void>} a promise to open the WebSocket connection
4681
+ * sets Spoken language for the meeting
4682
+ * @param {string} language
4683
+ * @returns {Promise}
4180
4684
  */
4181
- private async receiveTranscription() {
4182
- LoggerProxy.logger.info(
4183
- `Meeting:index#receiveTranscription -->
4184
- Attempting to generate a web socket url.`
4185
- );
4685
+ public setSpokenLanguage(language: string) {
4686
+ return new Promise((resolve, reject) => {
4687
+ if (!this.isTranscriptionSupported()) {
4688
+ LoggerProxy.logger.error(
4689
+ 'Meeting:index#setCaptionLanguage --> Webex Assistant is not enabled/supported'
4690
+ );
4691
+
4692
+ reject(new Error('Webex Assistant is not enabled/supported'));
4693
+ }
4694
+
4695
+ try {
4696
+ const voiceaListenerLanguageUpdate = (payload) => {
4697
+ // @ts-ignore
4698
+ this.webex.internal.voicea.off(
4699
+ VOICEAEVENTS.SPOKEN_LANGUAGE_UPDATE,
4700
+ voiceaListenerLanguageUpdate
4701
+ );
4702
+ const {languageCode} = payload;
4703
+
4704
+ if (languageCode) {
4705
+ this.transcription.languageOptions = {
4706
+ ...this.transcription.languageOptions,
4707
+ currentSpokenLanguage: languageCode,
4708
+ };
4709
+ resolve(languageCode);
4710
+ } else {
4711
+ reject(payload);
4712
+ }
4713
+ };
4186
4714
 
4187
- try {
4188
- const {datachannelUrl} = this.locusInfo.info;
4189
- // @ts-ignore - fix type
4190
- const {
4191
- body: {webSocketUrl},
4192
4715
  // @ts-ignore
4193
- } = await this.request({
4194
- method: HTTP_VERBS.POST,
4195
- uri: datachannelUrl,
4196
- body: {deviceUrl: this.deviceUrl},
4197
- });
4716
+ this.webex.internal.voicea.on(
4717
+ VOICEAEVENTS.SPOKEN_LANGUAGE_UPDATE,
4718
+ voiceaListenerLanguageUpdate
4719
+ );
4198
4720
 
4199
- LoggerProxy.logger.info(
4200
- `Meeting:index#receiveTranscription -->
4201
- Generated web socket url succesfully.`
4202
- );
4721
+ // @ts-ignore
4722
+ this.webex.internal.voicea.setSpokenLanguage(language);
4723
+ } catch (error) {
4724
+ LoggerProxy.logger.error(`Meeting:index#setSpokenLanguage --> ${error}`);
4203
4725
 
4204
- this.transcription = new Transcription(
4205
- webSocketUrl,
4206
- // @ts-ignore - fix type
4207
- this.webex.sessionId,
4208
- this.members
4209
- );
4726
+ reject(error);
4727
+ }
4728
+ });
4729
+ }
4210
4730
 
4731
+ /**
4732
+ * This method will enable the transcription for the current meeting if the meeting has enabled/supports Webex Assistant
4733
+ * @param {Object} options object with spokenlanguage setting
4734
+ * @public
4735
+ * @returns {Promise<void>} a promise to open the WebSocket connection
4736
+ */
4737
+ public async startTranscription(options?: {spokenLanguage?: string}) {
4738
+ if (this.isJoined()) {
4211
4739
  LoggerProxy.logger.info(
4212
- `Meeting:index#receiveTranscription -->
4213
- opened LLM web socket connection successfully.`
4740
+ 'Meeting:index#startTranscription --> Attempting to enable transcription!'
4214
4741
  );
4215
4742
 
4216
- if (!this.inMeetingActions.isClosedCaptionActive) {
4217
- LoggerProxy.logger.error(
4218
- `Meeting:index#receiveTranscription --> Transcription cannot be started until a licensed user enables it`
4219
- );
4220
- }
4221
-
4222
- // retrieve and pass the payload
4223
- this.transcription.subscribe((payload) => {
4224
- Trigger.trigger(
4225
- this,
4226
- {
4227
- file: 'meeting/index',
4228
- function: 'join',
4229
- },
4230
- EVENT_TRIGGERS.MEETING_STARTED_RECEIVING_TRANSCRIPTION,
4231
- payload
4232
- );
4233
- });
4743
+ try {
4744
+ if (!this.areVoiceaEventsSetup) {
4745
+ this.setUpVoiceaListeners();
4746
+ }
4234
4747
 
4235
- this.monitorTranscriptionSocketConnection();
4236
- // @ts-ignore - fix type
4237
- this.transcription.connect(this.webex.credentials.supertoken.access_token);
4238
- } catch (error) {
4239
- LoggerProxy.logger.error(`Meeting:index#receiveTranscription --> ${error}`);
4240
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.RECEIVE_TRANSCRIPTION_FAILURE, {
4241
- correlation_id: this.correlationId,
4242
- reason: error.message,
4243
- stack: error.stack,
4244
- });
4748
+ if (this.getCurUserType() === 'host') {
4749
+ // @ts-ignore
4750
+ await this.webex.internal.voicea.toggleTranscribing(true, options?.spokenLanguage);
4751
+ }
4752
+ } catch (error) {
4753
+ LoggerProxy.logger.error(`Meeting:index#startTranscription --> ${error}`);
4754
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.RECEIVE_TRANSCRIPTION_FAILURE, {
4755
+ correlation_id: this.correlationId,
4756
+ reason: error.message,
4757
+ stack: error.stack,
4758
+ });
4759
+ }
4760
+ } else {
4761
+ LoggerProxy.logger.error(
4762
+ `Meeting:index#startTranscription --> meeting joined : ${this.isJoined()}`
4763
+ );
4764
+ throw new Error('Meeting is not joined');
4245
4765
  }
4246
4766
  }
4247
4767
 
@@ -4284,13 +4804,37 @@ export default class Meeting extends StatelessWebexPlugin {
4284
4804
  };
4285
4805
 
4286
4806
  /**
4287
- * stop recieving Transcription by closing
4288
- * the web socket connection properly
4807
+ * This method stops receiving transcription for the current meeting
4289
4808
  * @returns {void}
4290
4809
  */
4291
- stopReceivingTranscription() {
4810
+ stopTranscription() {
4292
4811
  if (this.transcription) {
4293
- this.transcription.closeSocket();
4812
+ // @ts-ignore
4813
+ this.webex.internal.voicea.off(
4814
+ VOICEAEVENTS.VOICEA_ANNOUNCEMENT,
4815
+ this.voiceaListenerCallbacks[VOICEAEVENTS.VOICEA_ANNOUNCEMENT]
4816
+ );
4817
+
4818
+ // @ts-ignore
4819
+ this.webex.internal.voicea.off(
4820
+ VOICEAEVENTS.CAPTIONS_TURNED_ON,
4821
+ this.voiceaListenerCallbacks[VOICEAEVENTS.CAPTIONS_TURNED_ON]
4822
+ );
4823
+
4824
+ // @ts-ignore
4825
+ this.webex.internal.voicea.off(
4826
+ VOICEAEVENTS.EVA_COMMAND,
4827
+ this.voiceaListenerCallbacks[VOICEAEVENTS.EVA_COMMAND]
4828
+ );
4829
+
4830
+ // @ts-ignore
4831
+ this.webex.internal.voicea.off(
4832
+ VOICEAEVENTS.NEW_CAPTION,
4833
+ this.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]
4834
+ );
4835
+
4836
+ this.areVoiceaEventsSetup = false;
4837
+ this.triggerStopReceivingTranscriptionEvent();
4294
4838
  }
4295
4839
  }
4296
4840
 
@@ -4303,12 +4847,12 @@ export default class Meeting extends StatelessWebexPlugin {
4303
4847
  private triggerStopReceivingTranscriptionEvent() {
4304
4848
  LoggerProxy.logger.info(`
4305
4849
  Meeting:index#stopReceivingTranscription -->
4306
- closed transcription LLM web socket connection successfully.`);
4850
+ closed voicea event listeners successfully.`);
4307
4851
 
4308
4852
  Trigger.trigger(
4309
4853
  this,
4310
4854
  {
4311
- file: 'meeting',
4855
+ file: 'meeting/index',
4312
4856
  function: 'triggerStopReceivingTranscriptionEvent',
4313
4857
  },
4314
4858
  EVENT_TRIGGERS.MEETING_STOPPED_RECEIVING_TRANSCRIPTION
@@ -4327,7 +4871,7 @@ export default class Meeting extends StatelessWebexPlugin {
4327
4871
  * if joining as host on second loop, pass pin and pass moderator if joining as guest on second loop
4328
4872
  * Scenario D: Joining any other way (sip, pstn, conversationUrl, link just need to specify resourceId)
4329
4873
  */
4330
- public join(options: any = {}) {
4874
+ public async join(options: any = {}) {
4331
4875
  // @ts-ignore - fix type
4332
4876
  if (!this.webex.meetings.registered) {
4333
4877
  const errorMessage = 'Meeting:index#join --> Device not registered';
@@ -4381,27 +4925,14 @@ export default class Meeting extends StatelessWebexPlugin {
4381
4925
  // @ts-ignore
4382
4926
  this.webex.internal.newMetrics.submitClientEvent({
4383
4927
  name: 'client.call.initiated',
4384
- payload: {trigger: 'user-interaction', isRoapCallEnabled: true},
4928
+ payload: {
4929
+ trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
4930
+ isRoapCallEnabled: true,
4931
+ pstnAudioType: options?.pstnAudioType,
4932
+ },
4385
4933
  options: {meetingId: this.id},
4386
4934
  });
4387
4935
 
4388
- if (!isEmpty(this.meetingInfo)) {
4389
- // @ts-ignore
4390
- this.webex.internal.newMetrics.submitClientEvent({
4391
- name: 'client.meetinginfo.request',
4392
- options: {meetingId: this.id},
4393
- });
4394
-
4395
- // @ts-ignore
4396
- this.webex.internal.newMetrics.submitClientEvent({
4397
- name: 'client.meetinginfo.response',
4398
- payload: {
4399
- identifiers: {meetingLookupUrl: this.meetingInfo?.meetingLookupUrl},
4400
- },
4401
- options: {meetingId: this.id},
4402
- });
4403
- }
4404
-
4405
4936
  LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
4406
4937
 
4407
4938
  if (this.meetingFiniteStateMachine.state === MEETING_STATE_MACHINE.STATES.ENDED) {
@@ -4455,44 +4986,55 @@ export default class Meeting extends StatelessWebexPlugin {
4455
4986
 
4456
4987
  this.isMultistream = !!options.enableMultistream;
4457
4988
 
4989
+ try {
4990
+ // refresh the permission token if its about to expire in 10sec
4991
+ await this.checkAndRefreshPermissionToken(
4992
+ MEETING_PERMISSION_TOKEN_REFRESH_THRESHOLD_IN_SEC,
4993
+ MEETING_PERMISSION_TOKEN_REFRESH_REASON
4994
+ );
4995
+ } catch (error) {
4996
+ LoggerProxy.logger.error('Meeting:index#join --> Failed to refresh permission token:', error);
4997
+
4998
+ if (
4999
+ error instanceof CaptchaError ||
5000
+ error instanceof PasswordError ||
5001
+ error instanceof PermissionError
5002
+ ) {
5003
+ this.meetingFiniteStateMachine.fail(error);
5004
+
5005
+ // Upload logs on refreshpermissionToken refresh Failure
5006
+ Trigger.trigger(
5007
+ this,
5008
+ {
5009
+ file: 'meeting/index',
5010
+ function: 'join',
5011
+ },
5012
+ EVENTS.REQUEST_UPLOAD_LOGS,
5013
+ this
5014
+ );
5015
+
5016
+ joinFailed(error);
5017
+
5018
+ this.deferJoin = undefined;
5019
+
5020
+ // if refresh permission token requires captcha, password or permission, we are throwing the errors
5021
+ // and bubble it up to client
5022
+ return Promise.reject(error);
5023
+ }
5024
+ }
5025
+
4458
5026
  return MeetingUtil.joinMeetingOptions(this, options)
4459
5027
  .then((join) => {
4460
5028
  this.meetingFiniteStateMachine.join();
4461
5029
  LoggerProxy.logger.log('Meeting:index#join --> Success');
4462
5030
 
4463
- return join;
4464
- })
4465
- .then((join) => {
4466
- joinSuccess(join);
4467
- this.deferJoin = undefined;
4468
5031
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.JOIN_SUCCESS, {
4469
5032
  correlation_id: this.correlationId,
4470
5033
  });
4471
5034
 
4472
- return join;
4473
- })
4474
- .then(async (join) => {
4475
- // @ts-ignore - config coming from registerPlugin
4476
- if (this.config.enableAutomaticLLM) {
4477
- await this.updateLLMConnection();
4478
- }
5035
+ joinSuccess(join);
4479
5036
 
4480
- return join;
4481
- })
4482
- .then(async (join) => {
4483
- if (isBrowser) {
4484
- // @ts-ignore - config coming from registerPlugin
4485
- if (this.config.receiveTranscription || options.receiveTranscription) {
4486
- if (this.isTranscriptionSupported()) {
4487
- await this.receiveTranscription();
4488
- LoggerProxy.logger.info('Meeting:index#join --> enabled to recieve transcription!');
4489
- }
4490
- }
4491
- } else {
4492
- LoggerProxy.logger.error(
4493
- 'Meeting:index#join --> Receving transcription is not supported on this platform'
4494
- );
4495
- }
5037
+ this.deferJoin = undefined;
4496
5038
 
4497
5039
  return join;
4498
5040
  })
@@ -4528,9 +5070,44 @@ export default class Meeting extends StatelessWebexPlugin {
4528
5070
  );
4529
5071
 
4530
5072
  joinFailed(error);
5073
+
4531
5074
  this.deferJoin = undefined;
4532
5075
 
4533
5076
  return Promise.reject(error);
5077
+ })
5078
+ .then((join) => {
5079
+ // @ts-ignore - config coming from registerPlugin
5080
+ if (this.config.enableAutomaticLLM) {
5081
+ this.updateLLMConnection()
5082
+ .catch((error) => {
5083
+ LoggerProxy.logger.error(
5084
+ 'Meeting:index#join --> Transcription Socket Connection Failed',
5085
+ error
5086
+ );
5087
+
5088
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.LLM_CONNECTION_AFTER_JOIN_FAILURE, {
5089
+ correlation_id: this.correlationId,
5090
+ reason: error?.message,
5091
+ stack: error.stack,
5092
+ });
5093
+ })
5094
+ .then(() => {
5095
+ LoggerProxy.logger.info(
5096
+ 'Meeting:index#join --> Transcription Socket Connection Success'
5097
+ );
5098
+ Trigger.trigger(
5099
+ this,
5100
+ {
5101
+ file: 'meeting/index',
5102
+ function: 'join',
5103
+ },
5104
+ EVENT_TRIGGERS.MEETING_TRANSCRIPTION_CONNECTED,
5105
+ undefined
5106
+ );
5107
+ });
5108
+ }
5109
+
5110
+ return join;
4534
5111
  });
4535
5112
  }
4536
5113
 
@@ -4910,10 +5487,77 @@ export default class Meeting extends StatelessWebexPlugin {
4910
5487
  }
4911
5488
  };
4912
5489
 
4913
- setupMediaConnectionListeners = () => {
4914
- this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_STARTED, () => {
4915
- this.isRoapInProgress = true;
4916
- });
5490
+ /**
5491
+ * Handles an incoming Roap message
5492
+ * @internal
5493
+ * @param {RoapMessage} roapMessage roap message
5494
+ * @returns {undefined}
5495
+ */
5496
+ public roapMessageReceived = (roapMessage: RoapMessage) => {
5497
+ const mediaServer = MeetingsUtil.getMediaServer(roapMessage.sdp);
5498
+
5499
+ this.mediaProperties.webrtcMediaConnection.roapMessageReceived(roapMessage);
5500
+
5501
+ if (mediaServer) {
5502
+ this.mediaProperties.webrtcMediaConnection.mediaServer = mediaServer;
5503
+ }
5504
+ };
5505
+
5506
+ /**
5507
+ * This function makes sure we send the right metrics when local and remote SDPs are processed/generated
5508
+ *
5509
+ * @returns {undefined}
5510
+ */
5511
+ setupSdpListeners = () => {
5512
+ this.mediaProperties.webrtcMediaConnection.on(Event.REMOTE_SDP_ANSWER_PROCESSED, () => {
5513
+ // @ts-ignore
5514
+ const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
5515
+
5516
+ // @ts-ignore
5517
+ this.webex.internal.newMetrics.submitClientEvent({
5518
+ name: 'client.media-engine.remote-sdp-received',
5519
+ options: {meetingId: this.id},
5520
+ });
5521
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ROAP_OFFER_TO_ANSWER_LATENCY, {
5522
+ correlation_id: this.correlationId,
5523
+ latency: cdl.getLocalSDPGenRemoteSDPRecv(),
5524
+ meetingId: this.id,
5525
+ });
5526
+
5527
+ if (this.deferSDPAnswer) {
5528
+ this.deferSDPAnswer.resolve();
5529
+ clearTimeout(this.sdpResponseTimer);
5530
+ this.sdpResponseTimer = undefined;
5531
+ }
5532
+ });
5533
+
5534
+ this.mediaProperties.webrtcMediaConnection.on(Event.LOCAL_SDP_OFFER_GENERATED, () => {
5535
+ // @ts-ignore
5536
+ this.webex.internal.newMetrics.submitClientEvent({
5537
+ name: 'client.media-engine.local-sdp-generated',
5538
+ options: {meetingId: this.id},
5539
+ });
5540
+
5541
+ // Instantiate Defer so that the SDP offer/answer exchange timeout can start, see waitForRemoteSDPAnswer()
5542
+ this.deferSDPAnswer = new Defer();
5543
+ });
5544
+
5545
+ this.mediaProperties.webrtcMediaConnection.on(Event.LOCAL_SDP_ANSWER_GENERATED, () => {
5546
+ // we are sending "remote-sdp-received" only after we've generated the answer - this indicates that we've fully processed that incoming offer
5547
+ // @ts-ignore
5548
+ this.webex.internal.newMetrics.submitClientEvent({
5549
+ name: 'client.media-engine.remote-sdp-received',
5550
+ options: {meetingId: this.id},
5551
+ });
5552
+ });
5553
+ };
5554
+
5555
+ setupMediaConnectionListeners = () => {
5556
+ this.setupSdpListeners();
5557
+
5558
+ this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_STARTED, () => {
5559
+ this.isRoapInProgress = true;
5560
+ });
4917
5561
 
4918
5562
  this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_DONE, () => {
4919
5563
  this.mediaNegotiatedEvent();
@@ -4928,12 +5572,6 @@ export default class Meeting extends StatelessWebexPlugin {
4928
5572
 
4929
5573
  switch (event.roapMessage.messageType) {
4930
5574
  case 'OK':
4931
- // @ts-ignore
4932
- this.webex.internal.newMetrics.submitClientEvent({
4933
- name: 'client.media-engine.remote-sdp-received',
4934
- options: {meetingId: this.id},
4935
- });
4936
-
4937
5575
  logRequest(
4938
5576
  this.roap.sendRoapOK({
4939
5577
  seq: event.roapMessage.seq,
@@ -4947,33 +5585,32 @@ export default class Meeting extends StatelessWebexPlugin {
4947
5585
  break;
4948
5586
 
4949
5587
  case 'OFFER':
4950
- // @ts-ignore
4951
- this.webex.internal.newMetrics.submitClientEvent({
4952
- name: 'client.media-engine.local-sdp-generated',
4953
- options: {meetingId: this.id},
4954
- });
4955
-
4956
5588
  logRequest(
4957
- this.roap.sendRoapMediaRequest({
4958
- sdp: event.roapMessage.sdp,
4959
- seq: event.roapMessage.seq,
4960
- tieBreaker: event.roapMessage.tieBreaker,
4961
- meeting: this, // or can pass meeting ID
4962
- reconnect: this.reconnectionManager.isReconnectInProgress(),
4963
- }),
5589
+ this.roap
5590
+ .sendRoapMediaRequest({
5591
+ sdp: event.roapMessage.sdp,
5592
+ seq: event.roapMessage.seq,
5593
+ tieBreaker: event.roapMessage.tieBreaker,
5594
+ meeting: this, // or can pass meeting ID
5595
+ })
5596
+ .then(({roapAnswer}) => {
5597
+ if (roapAnswer) {
5598
+ LoggerProxy.logger.log(`${LOG_HEADER} received Roap ANSWER in http response`);
5599
+
5600
+ this.roapMessageReceived(roapAnswer);
5601
+ }
5602
+ }),
4964
5603
  {
4965
5604
  logText: `${LOG_HEADER} Roap Offer`,
4966
5605
  }
4967
- );
5606
+ ).catch(() => {
5607
+ this.deferSDPAnswer.reject();
5608
+ clearTimeout(this.sdpResponseTimer);
5609
+ this.sdpResponseTimer = undefined;
5610
+ });
4968
5611
  break;
4969
5612
 
4970
5613
  case 'ANSWER':
4971
- // @ts-ignore
4972
- this.webex.internal.newMetrics.submitClientEvent({
4973
- name: 'client.media-engine.remote-sdp-received',
4974
- options: {meetingId: this.id},
4975
- });
4976
-
4977
5614
  logRequest(
4978
5615
  this.roap.sendRoapAnswer({
4979
5616
  sdp: event.roapMessage.sdp,
@@ -5086,68 +5723,71 @@ export default class Meeting extends StatelessWebexPlugin {
5086
5723
 
5087
5724
  this.mediaProperties.webrtcMediaConnection.on(Event.CONNECTION_STATE_CHANGED, (event) => {
5088
5725
  const connectionFailed = () => {
5089
- // we know the media connection failed and browser will not attempt to recover it any more
5090
- // so reset the timer as it's not needed anymore, we want to reconnect immediately
5091
- this.reconnectionManager.resetReconnectionTimer();
5092
-
5093
- this.reconnect({networkDisconnect: true});
5094
- // @ts-ignore
5095
- this.webex.internal.newMetrics.submitClientEvent({
5096
- name: 'client.ice.end',
5097
- payload: {
5098
- canProceed: false,
5099
- icePhase: 'IN_MEETING',
5100
- errors: [
5101
- // @ts-ignore
5102
- this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
5103
- CALL_DIAGNOSTIC_CONFIG.ICE_FAILURE_CLIENT_CODE
5104
- ),
5105
- ],
5106
- },
5107
- options: {
5108
- meetingId: this.id,
5109
- },
5110
- });
5111
-
5112
- this.uploadLogs({
5113
- file: 'peer-connection-manager/index',
5114
- function: 'connectionFailed',
5115
- });
5116
-
5117
5726
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_FAILURE, {
5118
5727
  correlation_id: this.correlationId,
5119
5728
  locus_id: this.locusId,
5729
+ networkStatus: this.networkStatus,
5730
+ hasMediaConnectionConnectedAtLeastOnce: this.hasMediaConnectionConnectedAtLeastOnce,
5120
5731
  });
5732
+
5733
+ if (this.hasMediaConnectionConnectedAtLeastOnce) {
5734
+ // we know the media connection failed and browser will not attempt to recover it any more
5735
+ // so reset the timer as it's not needed anymore, we want to reconnect immediately
5736
+ this.reconnectionManager.resetReconnectionTimer();
5737
+
5738
+ this.reconnect({networkDisconnect: true});
5739
+
5740
+ this.uploadLogs({
5741
+ file: 'peer-connection-manager/index',
5742
+ function: 'connectionFailed',
5743
+ });
5744
+ }
5121
5745
  };
5122
5746
 
5123
5747
  LoggerProxy.logger.info(
5124
5748
  `Meeting:index#setupMediaConnectionListeners --> correlationId=${this.correlationId} connection state changed to ${event.state}`
5125
5749
  );
5750
+
5751
+ // @ts-ignore
5752
+ const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
5753
+
5126
5754
  switch (event.state) {
5127
5755
  case ConnectionState.Connecting:
5128
- // @ts-ignore
5129
- this.webex.internal.newMetrics.submitClientEvent({
5130
- name: 'client.ice.start',
5131
- options: {
5132
- meetingId: this.id,
5133
- },
5134
- });
5756
+ if (!this.hasMediaConnectionConnectedAtLeastOnce) {
5757
+ // Only send CA event for join flow if we haven't successfully connected media yet
5758
+ // @ts-ignore
5759
+ this.webex.internal.newMetrics.submitClientEvent({
5760
+ name: 'client.ice.start',
5761
+ options: {
5762
+ meetingId: this.id,
5763
+ },
5764
+ });
5765
+ }
5135
5766
  break;
5136
5767
  case ConnectionState.Connected:
5137
- // @ts-ignore
5138
- this.webex.internal.newMetrics.submitClientEvent({
5139
- name: 'client.ice.end',
5140
- options: {
5141
- meetingId: this.id,
5142
- },
5143
- });
5768
+ if (!this.hasMediaConnectionConnectedAtLeastOnce) {
5769
+ // Only send CA event for join flow if we haven't successfully connected media yet
5770
+ // @ts-ignore
5771
+ this.webex.internal.newMetrics.submitClientEvent({
5772
+ name: 'client.ice.end',
5773
+ payload: {
5774
+ canProceed: true,
5775
+ icePhase: 'JOIN_MEETING_FINAL',
5776
+ },
5777
+ options: {
5778
+ meetingId: this.id,
5779
+ },
5780
+ });
5781
+ }
5144
5782
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_SUCCESS, {
5145
5783
  correlation_id: this.correlationId,
5146
5784
  locus_id: this.locusId,
5785
+ latency: cdl.getICESetupTime(),
5147
5786
  });
5148
5787
  this.setNetworkStatus(NETWORK_STATUS.CONNECTED);
5149
5788
  this.reconnectionManager.iceReconnected();
5150
5789
  this.statsAnalyzer.startAnalyzer(this.mediaProperties.webrtcMediaConnection);
5790
+ this.hasMediaConnectionConnectedAtLeastOnce = true;
5151
5791
  break;
5152
5792
  case ConnectionState.Disconnected:
5153
5793
  this.setNetworkStatus(NETWORK_STATUS.DISCONNECTED);
@@ -5268,7 +5908,10 @@ export default class Meeting extends StatelessWebexPlugin {
5268
5908
  // @ts-ignore
5269
5909
  this.webex.internal.newMetrics.submitClientEvent({
5270
5910
  name: 'client.media.tx.start',
5271
- payload: {mediaType: data.type},
5911
+ payload: {
5912
+ mediaType: data.type,
5913
+ shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
5914
+ },
5272
5915
  options: {
5273
5916
  meetingId: this.id,
5274
5917
  },
@@ -5278,7 +5921,10 @@ export default class Meeting extends StatelessWebexPlugin {
5278
5921
  // @ts-ignore
5279
5922
  this.webex.internal.newMetrics.submitClientEvent({
5280
5923
  name: 'client.media.tx.stop',
5281
- payload: {mediaType: data.type},
5924
+ payload: {
5925
+ mediaType: data.type,
5926
+ shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
5927
+ },
5282
5928
  options: {
5283
5929
  meetingId: this.id,
5284
5930
  },
@@ -5297,7 +5943,10 @@ export default class Meeting extends StatelessWebexPlugin {
5297
5943
  // @ts-ignore
5298
5944
  this.webex.internal.newMetrics.submitClientEvent({
5299
5945
  name: 'client.media.rx.start',
5300
- payload: {mediaType: data.type},
5946
+ payload: {
5947
+ mediaType: data.type,
5948
+ shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
5949
+ },
5301
5950
  options: {
5302
5951
  meetingId: this.id,
5303
5952
  },
@@ -5307,7 +5956,10 @@ export default class Meeting extends StatelessWebexPlugin {
5307
5956
  // @ts-ignore
5308
5957
  this.webex.internal.newMetrics.submitClientEvent({
5309
5958
  name: 'client.media.rx.stop',
5310
- payload: {mediaType: data.type},
5959
+ payload: {
5960
+ mediaType: data.type,
5961
+ shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
5962
+ },
5311
5963
  options: {
5312
5964
  meetingId: this.id,
5313
5965
  },
@@ -5360,8 +6012,8 @@ export default class Meeting extends StatelessWebexPlugin {
5360
6012
  this.mediaProperties.mediaDirection.receiveShare,
5361
6013
  ];
5362
6014
 
5363
- this.sendSlotManager.createSlot(mc, MediaType.VideoMain, audioEnabled);
5364
- this.sendSlotManager.createSlot(mc, MediaType.AudioMain, videoEnabled);
6015
+ this.sendSlotManager.createSlot(mc, MediaType.VideoMain, videoEnabled);
6016
+ this.sendSlotManager.createSlot(mc, MediaType.AudioMain, audioEnabled);
5365
6017
  this.sendSlotManager.createSlot(mc, MediaType.VideoSlides, shareEnabled);
5366
6018
  this.sendSlotManager.createSlot(mc, MediaType.AudioSlides, shareEnabled);
5367
6019
  }
@@ -5384,50 +6036,464 @@ export default class Meeting extends StatelessWebexPlugin {
5384
6036
  }
5385
6037
 
5386
6038
  /**
5387
- * Listens for an event emitted by eventEmitter and emits it from the meeting object
6039
+ * Listens for an event emitted by eventEmitter and emits it from the meeting object
6040
+ *
6041
+ * @private
6042
+ * @param {*} eventEmitter object from which to forward the event
6043
+ * @param {*} eventTypeToForward which event type to listen on and to forward
6044
+ * @param {string} meetingEventType event type to be used in the event emitted from the meeting object
6045
+ * @returns {void}
6046
+ */
6047
+ forwardEvent(eventEmitter, eventTypeToForward, meetingEventType) {
6048
+ eventEmitter.on(eventTypeToForward, (data) =>
6049
+ Trigger.trigger(
6050
+ this,
6051
+ {
6052
+ file: 'meetings',
6053
+ function: 'addMedia',
6054
+ },
6055
+ meetingEventType,
6056
+ data
6057
+ )
6058
+ );
6059
+ }
6060
+
6061
+ /**
6062
+ * Sets up all the references to local streams in this.mediaProperties before creating media connection
6063
+ * and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages.
6064
+ *
6065
+ * @private
6066
+ * @param {LocalStreams} localStreams
6067
+ * @returns {Promise<void>}
6068
+ */
6069
+ private async setUpLocalStreamReferences(localStreams: LocalStreams) {
6070
+ const setUpStreamPromises = [];
6071
+
6072
+ if (localStreams?.microphone) {
6073
+ setUpStreamPromises.push(this.setLocalAudioStream(localStreams.microphone));
6074
+ }
6075
+ if (localStreams?.camera) {
6076
+ setUpStreamPromises.push(this.setLocalVideoStream(localStreams.camera));
6077
+ }
6078
+ if (localStreams?.screenShare?.video) {
6079
+ setUpStreamPromises.push(this.setLocalShareVideoStream(localStreams.screenShare.video));
6080
+ }
6081
+ if (localStreams?.screenShare?.audio) {
6082
+ setUpStreamPromises.push(this.setLocalShareAudioStream(localStreams.screenShare.audio));
6083
+ }
6084
+
6085
+ try {
6086
+ await Promise.all(setUpStreamPromises);
6087
+ } catch (error) {
6088
+ LoggerProxy.logger.error(
6089
+ `Meeting:index#addMedia():setUpLocalStreamReferences --> Error , `,
6090
+ error
6091
+ );
6092
+
6093
+ throw error;
6094
+ }
6095
+ }
6096
+
6097
+ /**
6098
+ * Calls mediaProperties.waitForMediaConnectionConnected() and sends CA client.ice.end metric on failure
6099
+ *
6100
+ * @private
6101
+ * @returns {Promise<void>}
6102
+ */
6103
+ private async waitForMediaConnectionConnected(): Promise<void> {
6104
+ try {
6105
+ await this.mediaProperties.waitForMediaConnectionConnected();
6106
+ } catch (error) {
6107
+ if (!this.hasMediaConnectionConnectedAtLeastOnce) {
6108
+ // Only send CA event for join flow if we haven't successfully connected media yet
6109
+ // @ts-ignore
6110
+ this.webex.internal.newMetrics.submitClientEvent({
6111
+ name: 'client.ice.end',
6112
+ payload: {
6113
+ canProceed: !this.turnServerUsed, // If we haven't done turn tls retry yet we will proceed with join attempt
6114
+ icePhase: this.turnServerUsed ? 'JOIN_MEETING_FINAL' : 'JOIN_MEETING_RETRY',
6115
+ errors: [
6116
+ // @ts-ignore
6117
+ this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
6118
+ {
6119
+ clientErrorCode: CallDiagnosticUtils.generateClientErrorCodeForIceFailure({
6120
+ signalingState:
6121
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
6122
+ ?.signalingState ||
6123
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
6124
+ ?.signalingState ||
6125
+ 'unknown',
6126
+ iceConnectionState:
6127
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
6128
+ ?.iceConnectionState ||
6129
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
6130
+ ?.iceConnectionState ||
6131
+ 'unknown',
6132
+ turnServerUsed: this.turnServerUsed,
6133
+ }),
6134
+ }
6135
+ ),
6136
+ ],
6137
+ },
6138
+ options: {
6139
+ meetingId: this.id,
6140
+ },
6141
+ });
6142
+ }
6143
+ throw new Error(
6144
+ `Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
6145
+ );
6146
+ }
6147
+ }
6148
+
6149
+ /**
6150
+ * Enables statsAnalyser if config allows it
6151
+ *
6152
+ * @private
6153
+ * @returns {void}
6154
+ */
6155
+ private createStatsAnalyzer() {
6156
+ // @ts-ignore - config coming from registerPlugin
6157
+ if (this.config.stats.enableStatsAnalyzer) {
6158
+ // @ts-ignore - config coming from registerPlugin
6159
+ this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
6160
+ this.statsAnalyzer = new StatsAnalyzer(
6161
+ // @ts-ignore - config coming from registerPlugin
6162
+ this.config.stats,
6163
+ (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
6164
+ this.networkQualityMonitor
6165
+ );
6166
+ this.setupStatsAnalyzerEventHandlers();
6167
+ this.networkQualityMonitor.on(
6168
+ EVENT_TRIGGERS.NETWORK_QUALITY,
6169
+ this.sendNetworkQualityEvent.bind(this)
6170
+ );
6171
+ }
6172
+ }
6173
+
6174
+ /**
6175
+ * Handles device logging
6176
+ *
6177
+ * @private
6178
+ * @static
6179
+ * @returns {Promise<void>}
6180
+ */
6181
+ private static async handleDeviceLogging(): Promise<void> {
6182
+ try {
6183
+ const devices = await getDevices();
6184
+
6185
+ MeetingUtil.handleDeviceLogging(devices);
6186
+ } catch {
6187
+ // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
6188
+ }
6189
+ }
6190
+
6191
+ /**
6192
+ * Returns a promise. This promise is created once the local sdp offer has been successfully created and is resolved
6193
+ * once the remote sdp answer has been received.
6194
+ *
6195
+ * @private
6196
+ * @returns {Promise<void>}
6197
+ */
6198
+ private async waitForRemoteSDPAnswer(): Promise<void> {
6199
+ const LOG_HEADER = 'Meeting:index#addMedia():waitForRemoteSDPAnswer -->';
6200
+
6201
+ if (!this.deferSDPAnswer) {
6202
+ LoggerProxy.logger.warn(`${LOG_HEADER} offer not created yet`);
6203
+
6204
+ return Promise.reject(
6205
+ new Error('waitForRemoteSDPAnswer() called before local sdp offer created')
6206
+ );
6207
+ }
6208
+
6209
+ const {deferSDPAnswer} = this;
6210
+
6211
+ this.sdpResponseTimer = setTimeout(() => {
6212
+ LoggerProxy.logger.warn(
6213
+ `${LOG_HEADER} timeout! no REMOTE SDP ANSWER received within ${
6214
+ ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT / 1000
6215
+ } seconds`
6216
+ );
6217
+ deferSDPAnswer.reject(new Error('Timed out waiting for REMOTE SDP ANSWER'));
6218
+ }, ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT);
6219
+
6220
+ LoggerProxy.logger.info(`${LOG_HEADER} waiting for REMOTE SDP ANSWER...`);
6221
+
6222
+ return deferSDPAnswer.promise;
6223
+ }
6224
+
6225
+ /**
6226
+ * Calls establishMediaConnection with isForced = true to force turn discovery to happen
6227
+ *
6228
+ * @private
6229
+ * @param {RemoteMediaManagerConfiguration} [remoteMediaManagerConfig]
6230
+ * @param {BundlePolicy} [bundlePolicy]
6231
+ * @returns {Promise<void>}
6232
+ */
6233
+ private async retryEstablishMediaConnectionWithForcedTurnDiscovery(
6234
+ remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
6235
+ bundlePolicy?: BundlePolicy
6236
+ ): Promise<void> {
6237
+ const LOG_HEADER =
6238
+ 'Meeting:index#addMedia():retryEstablishMediaConnectionWithForcedTurnDiscovery -->';
6239
+
6240
+ try {
6241
+ await this.establishMediaConnection(remoteMediaManagerConfig, bundlePolicy, true);
6242
+ } catch (err) {
6243
+ LoggerProxy.logger.error(
6244
+ `${LOG_HEADER} retry with TURN-TLS failed, media connection unable to connect, `,
6245
+ err
6246
+ );
6247
+
6248
+ throw err;
6249
+ }
6250
+ }
6251
+
6252
+ /**
6253
+ * Does relevant clean up before retrying to establish media connection
6254
+ * and performs the retry with forced turn discovery
6255
+ *
6256
+ * @private
6257
+ * @param {RemoteMediaManagerConfiguration} [remoteMediaManagerConfig]
6258
+ * @param {BundlePolicy} [bundlePolicy]
6259
+ * @returns {Promise<void>}
6260
+ */
6261
+ private async retryWithForcedTurnDiscovery(
6262
+ remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
6263
+ bundlePolicy?: BundlePolicy
6264
+ ): Promise<void> {
6265
+ this.retriedWithTurnServer = true;
6266
+ const LOG_HEADER = 'Meeting:index#addMedia():retryWithForcedTurnDiscovery -->';
6267
+
6268
+ await this.cleanUpBeforeRetryWithTurnServer();
6269
+
6270
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_RETRY, {
6271
+ correlation_id: this.correlationId,
6272
+ state: this.state,
6273
+ meetingState: this.meetingState,
6274
+ reason: 'forcingTurnTls',
6275
+ });
6276
+
6277
+ if (this.state === MEETING_STATE.STATES.LEFT) {
6278
+ LoggerProxy.logger.info(
6279
+ `${LOG_HEADER} meeting state was LEFT after first attempt to establish media connection. Attempting to rejoin. `
6280
+ );
6281
+ await this.join({rejoin: true});
6282
+ }
6283
+
6284
+ await this.retryEstablishMediaConnectionWithForcedTurnDiscovery(
6285
+ remoteMediaManagerConfig,
6286
+ bundlePolicy
6287
+ );
6288
+ }
6289
+
6290
+ /**
6291
+ * If waitForMediaConnectionConnected() fails when we haven't done turn discovery then we
6292
+ * attempt to establish a media connection again, but this time using turn discovery. If we
6293
+ * used turn discovery on the first pass we do not attempt connection again.
6294
+ *
6295
+ * @private
6296
+ * @param {Error} error
6297
+ * @param {RemoteMediaManagerConfiguration} [remoteMediaManagerConfig]
6298
+ * @param {BundlePolicy} [bundlePolicy]
6299
+ * @returns {Promise<void>}
6300
+ */
6301
+ private async handleWaitForMediaConnectionConnectedError(
6302
+ error: Error,
6303
+ remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
6304
+ bundlePolicy?: BundlePolicy
6305
+ ): Promise<void> {
6306
+ const LOG_HEADER = 'Meeting:index#addMedia():handleWaitForMediaConnectionConnectedError -->';
6307
+
6308
+ // @ts-ignore - config coming from registerPlugin
6309
+ if (!this.turnServerUsed) {
6310
+ LoggerProxy.logger.info(
6311
+ `${LOG_HEADER} error waiting for media to connect on UDP, TCP, retrying using TURN-TLS, `,
6312
+ error
6313
+ );
6314
+
6315
+ await this.retryWithForcedTurnDiscovery(remoteMediaManagerConfig, bundlePolicy);
6316
+ } else {
6317
+ LoggerProxy.logger.error(
6318
+ `${LOG_HEADER} error waiting for media to connect using UDP, TCP and TURN-TLS`,
6319
+ error
6320
+ );
6321
+
6322
+ throw new AddMediaFailed();
6323
+ }
6324
+ }
6325
+
6326
+ /**
6327
+ * Does TURN discovery, SDP offer/answer exhange, establishes ICE connection and DTLS handshake.
6328
+ *
6329
+ * @private
6330
+ * @param {RemoteMediaManagerConfiguration} [remoteMediaManagerConfig]
6331
+ * @param {BundlePolicy} [bundlePolicy]
6332
+ * @param {boolean} [isForced] - let isForced be true to do turn discovery regardless of reachability results
6333
+ * @returns {Promise<void>}
6334
+ */
6335
+ private async establishMediaConnection(
6336
+ remoteMediaManagerConfig?: RemoteMediaManagerConfiguration,
6337
+ bundlePolicy?: BundlePolicy,
6338
+ isForced?: boolean
6339
+ ): Promise<void> {
6340
+ const LOG_HEADER = 'Meeting:index#addMedia():establishMediaConnection -->';
6341
+ // @ts-ignore
6342
+ const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
6343
+ const isRetry = this.retriedWithTurnServer;
6344
+
6345
+ try {
6346
+ // @ts-ignore
6347
+ this.webex.internal.newMetrics.submitInternalEvent({
6348
+ name: 'internal.client.add-media.turn-discovery.start',
6349
+ });
6350
+
6351
+ const turnDiscoveryObject = await this.roap.doTurnDiscovery(this, isRetry, isForced);
6352
+
6353
+ this.turnDiscoverySkippedReason = turnDiscoveryObject?.turnDiscoverySkippedReason;
6354
+ this.turnServerUsed = !this.turnDiscoverySkippedReason;
6355
+
6356
+ // @ts-ignore
6357
+ this.webex.internal.newMetrics.submitInternalEvent({
6358
+ name: 'internal.client.add-media.turn-discovery.end',
6359
+ });
6360
+
6361
+ const {turnServerInfo} = turnDiscoveryObject;
6362
+
6363
+ if (this.turnServerUsed && turnServerInfo) {
6364
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.TURN_DISCOVERY_LATENCY, {
6365
+ correlation_id: this.correlationId,
6366
+ latency: cdl.getTurnDiscoveryTime(),
6367
+ turnServerUsed: this.turnServerUsed,
6368
+ retriedWithTurnServer: this.retriedWithTurnServer,
6369
+ });
6370
+ }
6371
+
6372
+ const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
6373
+
6374
+ LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
6375
+
6376
+ if (this.isMultistream) {
6377
+ this.remoteMediaManager = new RemoteMediaManager(
6378
+ this.receiveSlotManager,
6379
+ this.mediaRequestManagers,
6380
+ remoteMediaManagerConfig
6381
+ );
6382
+
6383
+ this.forwardEvent(
6384
+ this.remoteMediaManager,
6385
+ RemoteMediaManagerEvent.AudioCreated,
6386
+ EVENT_TRIGGERS.REMOTE_MEDIA_AUDIO_CREATED
6387
+ );
6388
+ this.forwardEvent(
6389
+ this.remoteMediaManager,
6390
+ RemoteMediaManagerEvent.ScreenShareAudioCreated,
6391
+ EVENT_TRIGGERS.REMOTE_MEDIA_SCREEN_SHARE_AUDIO_CREATED
6392
+ );
6393
+ this.forwardEvent(
6394
+ this.remoteMediaManager,
6395
+ RemoteMediaManagerEvent.VideoLayoutChanged,
6396
+ EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
6397
+ );
6398
+
6399
+ await this.remoteMediaManager.start();
6400
+ }
6401
+
6402
+ await mc.initiateOffer();
6403
+
6404
+ await this.waitForRemoteSDPAnswer();
6405
+
6406
+ this.handleMediaLogging(this.mediaProperties);
6407
+ } catch (error) {
6408
+ LoggerProxy.logger.error(`${LOG_HEADER} error establishing media connection, `, error);
6409
+
6410
+ throw error;
6411
+ }
6412
+
6413
+ try {
6414
+ await this.waitForMediaConnectionConnected();
6415
+ } catch (error) {
6416
+ await this.handleWaitForMediaConnectionConnectedError(
6417
+ error,
6418
+ remoteMediaManagerConfig,
6419
+ bundlePolicy
6420
+ );
6421
+ }
6422
+ }
6423
+
6424
+ /**
6425
+ * Cleans up stats analyzer, peer connection, and turns off listeners
6426
+ *
6427
+ * @private
6428
+ * @returns {Promise<void>}
6429
+ */
6430
+ private async cleanUpOnAddMediaFailure(): Promise<void> {
6431
+ if (this.statsAnalyzer) {
6432
+ await this.statsAnalyzer.stopAnalyzer();
6433
+ }
6434
+
6435
+ this.statsAnalyzer = null;
6436
+
6437
+ // when media fails, we want to upload a webrtc dump to see whats going on
6438
+ // this function is async, but returns once the stats have been gathered
6439
+ await this.forceSendStatsReport({callFrom: 'addMedia'});
6440
+
6441
+ if (this.mediaProperties.webrtcMediaConnection) {
6442
+ this.closePeerConnections();
6443
+ this.unsetPeerConnections();
6444
+ }
6445
+ }
6446
+
6447
+ /**
6448
+ * Sends stats report, closes peer connection and cleans up any media connection
6449
+ * related things before trying to establish media connection again with turn server
5388
6450
  *
5389
6451
  * @private
5390
- * @param {*} eventEmitter object from which to forward the event
5391
- * @param {*} eventTypeToForward which event type to listen on and to forward
5392
- * @param {string} meetingEventType event type to be used in the event emitted from the meeting object
5393
- * @returns {void}
6452
+ * @returns {Promise<void>}
5394
6453
  */
5395
- forwardEvent(eventEmitter, eventTypeToForward, meetingEventType) {
5396
- eventEmitter.on(eventTypeToForward, (data) =>
5397
- Trigger.trigger(
5398
- this,
5399
- {
5400
- file: 'meetings',
5401
- function: 'addMedia',
5402
- },
5403
- meetingEventType,
5404
- data
5405
- )
5406
- );
6454
+ private async cleanUpBeforeRetryWithTurnServer(): Promise<void> {
6455
+ // when media fails, we want to upload a webrtc dump to see whats going on
6456
+ // this function is async, but returns once the stats have been gathered
6457
+ await this.forceSendStatsReport({callFrom: 'cleanUpBeforeRetryWithTurnServer'});
6458
+
6459
+ if (this.mediaProperties.webrtcMediaConnection) {
6460
+ if (this.remoteMediaManager) {
6461
+ this.remoteMediaManager.stop();
6462
+ this.remoteMediaManager = null;
6463
+ }
6464
+
6465
+ Object.values(this.mediaRequestManagers).forEach((mediaRequestManager) =>
6466
+ mediaRequestManager.reset()
6467
+ );
6468
+
6469
+ this.receiveSlotManager.reset();
6470
+ this.mediaProperties.webrtcMediaConnection.close();
6471
+ this.sendSlotManager.reset();
6472
+
6473
+ this.mediaProperties.unsetPeerConnection();
6474
+ }
5407
6475
  }
5408
6476
 
5409
6477
  /**
5410
6478
  * Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
5411
6479
  *
5412
6480
  * @param {AddMediaOptions} options
5413
- * @returns {Promise}
6481
+ * @returns {Promise<void>}
5414
6482
  * @public
5415
6483
  * @memberof Meeting
5416
6484
  */
5417
- addMedia(options: AddMediaOptions = {}) {
6485
+ async addMedia(options: AddMediaOptions = {}): Promise<void> {
6486
+ this.retriedWithTurnServer = false;
6487
+ this.hasMediaConnectionConnectedAtLeastOnce = false;
5418
6488
  const LOG_HEADER = 'Meeting:index#addMedia -->';
5419
-
5420
- let turnDiscoverySkippedReason;
5421
- let turnServerUsed = false;
5422
-
5423
6489
  LoggerProxy.logger.info(`${LOG_HEADER} called with: ${JSON.stringify(options)}`);
5424
6490
 
5425
- if (this.meetingState !== FULL_STATE.ACTIVE) {
5426
- return Promise.reject(new MeetingNotActiveError());
6491
+ if (options.allowMediaInLobby !== true && this.meetingState !== FULL_STATE.ACTIVE) {
6492
+ throw new MeetingNotActiveError();
5427
6493
  }
5428
6494
 
5429
6495
  if (MeetingUtil.isUserInLeftState(this.locusInfo)) {
5430
- return Promise.reject(new UserNotJoinedError());
6496
+ throw new UserNotJoinedError();
5431
6497
  }
5432
6498
 
5433
6499
  const {
@@ -5446,7 +6512,7 @@ export default class Meeting extends StatelessWebexPlugin {
5446
6512
  // If the user is unjoined or guest waiting in lobby dont allow the user to addMedia
5447
6513
  // @ts-ignore - isUserUnadmitted coming from SelfUtil
5448
6514
  if (this.isUserUnadmitted && !this.wirelessShare && !allowMediaInLobby) {
5449
- return Promise.reject(new UserInLobbyError());
6515
+ throw new UserInLobbyError();
5450
6516
  }
5451
6517
 
5452
6518
  // @ts-ignore
@@ -5506,235 +6572,100 @@ export default class Meeting extends StatelessWebexPlugin {
5506
6572
 
5507
6573
  this.audio = createMuteState(AUDIO, this, audioEnabled);
5508
6574
  this.video = createMuteState(VIDEO, this, videoEnabled);
5509
- const promises = [];
5510
-
5511
- // setup all the references to local streams in this.mediaProperties before creating media connection
5512
- // and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages
5513
- if (localStreams?.microphone) {
5514
- promises.push(this.setLocalAudioStream(localStreams.microphone));
5515
- }
5516
- if (localStreams?.camera) {
5517
- promises.push(this.setLocalVideoStream(localStreams.camera));
5518
- }
5519
- if (localStreams?.screenShare?.video) {
5520
- promises.push(this.setLocalShareVideoStream(localStreams.screenShare.video));
5521
- }
5522
- if (localStreams?.screenShare?.audio) {
5523
- promises.push(this.setLocalShareAudioStream(localStreams.screenShare.audio));
5524
- }
5525
-
5526
- return Promise.all(promises)
5527
- .then(() => this.roap.doTurnDiscovery(this, false))
5528
- .then(async (turnDiscoveryObject) => {
5529
- ({turnDiscoverySkippedReason} = turnDiscoveryObject);
5530
- turnServerUsed = !turnDiscoverySkippedReason;
5531
-
5532
- const {turnServerInfo} = turnDiscoveryObject;
5533
-
5534
- const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
5535
6575
 
5536
- if (this.isMultistream) {
5537
- this.remoteMediaManager = new RemoteMediaManager(
5538
- this.receiveSlotManager,
5539
- this.mediaRequestManagers,
5540
- remoteMediaManagerConfig
5541
- );
6576
+ try {
6577
+ await this.setUpLocalStreamReferences(localStreams);
5542
6578
 
5543
- this.forwardEvent(
5544
- this.remoteMediaManager,
5545
- RemoteMediaManagerEvent.AudioCreated,
5546
- EVENT_TRIGGERS.REMOTE_MEDIA_AUDIO_CREATED
5547
- );
5548
- this.forwardEvent(
5549
- this.remoteMediaManager,
5550
- RemoteMediaManagerEvent.ScreenShareAudioCreated,
5551
- EVENT_TRIGGERS.REMOTE_MEDIA_SCREEN_SHARE_AUDIO_CREATED
5552
- );
5553
- this.forwardEvent(
5554
- this.remoteMediaManager,
5555
- RemoteMediaManagerEvent.VideoLayoutChanged,
5556
- EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
5557
- );
6579
+ this.setMercuryListener();
5558
6580
 
5559
- await this.remoteMediaManager.start();
5560
- }
6581
+ this.createStatsAnalyzer();
5561
6582
 
5562
- await mc.initiateOffer();
5563
- })
5564
- .then(() => {
5565
- this.setMercuryListener();
5566
- })
5567
- .then(
5568
- () =>
5569
- getDevices()
5570
- .then((devices) => {
5571
- MeetingUtil.handleDeviceLogging(devices);
5572
- })
5573
- .catch(() => {}) // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
5574
- )
5575
- .then(() => {
5576
- this.handleMediaLogging(this.mediaProperties);
5577
- LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
6583
+ await this.establishMediaConnection(remoteMediaManagerConfig, bundlePolicy, false);
5578
6584
 
5579
- // @ts-ignore - config coming from registerPlugin
5580
- if (this.config.stats.enableStatsAnalyzer) {
5581
- // @ts-ignore - config coming from registerPlugin
5582
- this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
5583
- this.statsAnalyzer = new StatsAnalyzer(
5584
- // @ts-ignore - config coming from registerPlugin
5585
- this.config.stats,
5586
- (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
5587
- this.networkQualityMonitor
5588
- );
5589
- this.setupStatsAnalyzerEventHandlers();
5590
- this.networkQualityMonitor.on(
5591
- EVENT_TRIGGERS.NETWORK_QUALITY,
5592
- this.sendNetworkQualityEvent.bind(this)
5593
- );
5594
- }
5595
- })
5596
- .catch((error) => {
5597
- LoggerProxy.logger.error(
5598
- `${LOG_HEADER} Error adding media , setting up peerconnection, `,
5599
- error
5600
- );
6585
+ await Meeting.handleDeviceLogging();
5601
6586
 
5602
- throw error;
5603
- })
5604
- .then(
5605
- () =>
5606
- new Promise<void>((resolve, reject) => {
5607
- let timerCount = 0;
5608
-
5609
- // eslint-disable-next-line func-names
5610
- // eslint-disable-next-line prefer-arrow-callback
5611
- if (this.type === _CALL_ || this.meetingState === FULL_STATE.ACTIVE) {
5612
- resolve();
5613
- }
5614
- const joiningTimer = setInterval(() => {
5615
- timerCount += 1;
5616
- if (this.meetingState === FULL_STATE.ACTIVE) {
5617
- clearInterval(joiningTimer);
5618
- resolve();
5619
- }
6587
+ if (this.mediaProperties.hasLocalShareStream()) {
6588
+ await this.enqueueScreenShareFloorRequest();
6589
+ }
5620
6590
 
5621
- if (timerCount === 4) {
5622
- clearInterval(joiningTimer);
5623
- reject(new Error('Meeting is still not active '));
5624
- }
5625
- }, 1000);
5626
- })
5627
- )
5628
- .then(() =>
5629
- this.mediaProperties.waitForMediaConnectionConnected().catch(() => {
5630
- // @ts-ignore
5631
- this.webex.internal.newMetrics.submitClientEvent({
5632
- name: 'client.ice.end',
5633
- payload: {
5634
- canProceed: false,
5635
- icePhase: 'JOIN_MEETING_FINAL',
5636
- errors: [
5637
- // @ts-ignore
5638
- this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
5639
- CALL_DIAGNOSTIC_CONFIG.ICE_FAILURE_CLIENT_CODE
5640
- ),
5641
- ],
5642
- },
5643
- options: {
5644
- meetingId: this.id,
5645
- },
5646
- });
5647
- throw new Error(
5648
- `Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
5649
- );
5650
- })
5651
- )
5652
- .then(() => {
5653
- if (this.mediaProperties.hasLocalShareStream()) {
5654
- return this.enqueueScreenShareFloorRequest();
5655
- }
6591
+ const connectionType = await this.mediaProperties.getCurrentConnectionType();
6592
+ // @ts-ignore
6593
+ const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics();
5656
6594
 
5657
- return Promise.resolve();
5658
- })
5659
- .then(() => this.mediaProperties.getCurrentConnectionType())
5660
- .then((connectionType) => {
5661
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
5662
- correlation_id: this.correlationId,
5663
- locus_id: this.locusUrl.split('/').pop(),
5664
- connectionType,
5665
- isMultistream: this.isMultistream,
5666
- });
5667
- // @ts-ignore
5668
- this.webex.internal.newMetrics.submitClientEvent({
5669
- name: 'client.media-engine.ready',
5670
- options: {
5671
- meetingId: this.id,
5672
- },
5673
- });
5674
- })
5675
- .catch((error) => {
5676
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
5677
- correlation_id: this.correlationId,
5678
- locus_id: this.locusUrl.split('/').pop(),
5679
- reason: error.message,
5680
- stack: error.stack,
5681
- code: error.code,
5682
- turnDiscoverySkippedReason,
5683
- turnServerUsed,
5684
- isMultistream: this.isMultistream,
5685
- signalingState:
5686
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5687
- ?.signalingState ||
5688
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
5689
- 'unknown',
5690
- connectionState:
5691
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5692
- ?.connectionState ||
5693
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.connectionState ||
5694
- 'unknown',
5695
- iceConnectionState:
5696
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5697
- ?.iceConnectionState ||
5698
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
5699
- 'unknown',
5700
- });
6595
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
6596
+ correlation_id: this.correlationId,
6597
+ locus_id: this.locusUrl.split('/').pop(),
6598
+ connectionType,
6599
+ isMultistream: this.isMultistream,
6600
+ retriedWithTurnServer: this.retriedWithTurnServer,
6601
+ ...reachabilityStats,
6602
+ });
6603
+ // @ts-ignore
6604
+ this.webex.internal.newMetrics.submitClientEvent({
6605
+ name: 'client.media-engine.ready',
6606
+ options: {
6607
+ meetingId: this.id,
6608
+ },
6609
+ });
6610
+ LoggerProxy.logger.info(
6611
+ `${LOG_HEADER} successfully established media connection, type=${connectionType}`
6612
+ );
5701
6613
 
5702
- // Clean up stats analyzer, peer connection, and turn off listeners
5703
- const stopStatsAnalyzer = this.statsAnalyzer
5704
- ? this.statsAnalyzer.stopAnalyzer()
5705
- : Promise.resolve();
6614
+ // We can log ReceiveSlot SSRCs only after the SDP exchange, so doing it here:
6615
+ this.remoteMediaManager?.logAllReceiveSlots();
6616
+ } catch (error) {
6617
+ LoggerProxy.logger.error(`${LOG_HEADER} failed to establish media connection: `, error);
5706
6618
 
5707
- return stopStatsAnalyzer.then(() => {
5708
- this.statsAnalyzer = null;
6619
+ // @ts-ignore
6620
+ const reachabilityMetrics = await this.webex.meetings.reachability.getReachabilityMetrics();
5709
6621
 
5710
- if (this.mediaProperties.webrtcMediaConnection) {
5711
- this.closePeerConnections();
5712
- this.unsetPeerConnections();
5713
- }
6622
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
6623
+ correlation_id: this.correlationId,
6624
+ locus_id: this.locusUrl.split('/').pop(),
6625
+ reason: error.message,
6626
+ stack: error.stack,
6627
+ code: error.code,
6628
+ turnDiscoverySkippedReason: this.turnDiscoverySkippedReason,
6629
+ turnServerUsed: this.turnServerUsed,
6630
+ retriedWithTurnServer: this.retriedWithTurnServer,
6631
+ isMultistream: this.isMultistream,
6632
+ signalingState:
6633
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
6634
+ ?.signalingState ||
6635
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
6636
+ 'unknown',
6637
+ connectionState:
6638
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
6639
+ ?.connectionState ||
6640
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.connectionState ||
6641
+ 'unknown',
6642
+ iceConnectionState:
6643
+ this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
6644
+ ?.iceConnectionState ||
6645
+ this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
6646
+ 'unknown',
6647
+ ...reachabilityMetrics,
6648
+ });
5714
6649
 
5715
- LoggerProxy.logger.error(
5716
- `${LOG_HEADER} Error adding media failed to initiate PC and send request, `,
5717
- error
5718
- );
6650
+ await this.cleanUpOnAddMediaFailure();
5719
6651
 
5720
- // Upload logs on error while adding media
5721
- Trigger.trigger(
5722
- this,
5723
- {
5724
- file: 'meeting/index',
5725
- function: 'addMedia',
5726
- },
5727
- EVENTS.REQUEST_UPLOAD_LOGS,
5728
- this
5729
- );
6652
+ // Upload logs on error while adding media
6653
+ Trigger.trigger(
6654
+ this,
6655
+ {
6656
+ file: 'meeting/index',
6657
+ function: 'addMedia',
6658
+ },
6659
+ EVENTS.REQUEST_UPLOAD_LOGS,
6660
+ this
6661
+ );
5730
6662
 
5731
- if (error instanceof Errors.SdpError) {
5732
- this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
5733
- }
6663
+ if (error instanceof Errors.SdpError) {
6664
+ this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
6665
+ }
5734
6666
 
5735
- throw error;
5736
- });
5737
- });
6667
+ throw error;
6668
+ }
5738
6669
  }
5739
6670
 
5740
6671
  /**
@@ -5748,6 +6679,24 @@ export default class Meeting extends StatelessWebexPlugin {
5748
6679
  return !this.isRoapInProgress;
5749
6680
  }
5750
6681
 
6682
+ /**
6683
+ * media failed, so collect a stats report from webrtc using the wcme connection to grab the rtc stats report
6684
+ * send a webrtc telemetry dump to the configured server using the internal media core check metrics configured callback
6685
+ * @param {String} callFrom - the function calling this function, optional.
6686
+ * @returns {Promise<void>}
6687
+ */
6688
+ private forceSendStatsReport = async ({callFrom}: {callFrom?: string}) => {
6689
+ const LOG_HEADER = `Meeting:index#forceSendStatsReport --> called from ${callFrom} : `;
6690
+ try {
6691
+ await this.mediaProperties?.webrtcMediaConnection?.forceRtcMetricsSend();
6692
+ LoggerProxy.logger.info(
6693
+ `${LOG_HEADER} successfully uploaded available webrtc telemetry statistics`
6694
+ );
6695
+ } catch (e) {
6696
+ LoggerProxy.logger.error(`${LOG_HEADER} failed to upload webrtc telemetry statistics: `, e);
6697
+ }
6698
+ };
6699
+
5751
6700
  /**
5752
6701
  * Enqueues a media update operation.
5753
6702
  * @param {String} mediaUpdateType one of MEDIA_UPDATE_TYPE values
@@ -5872,12 +6821,15 @@ export default class Meeting extends StatelessWebexPlugin {
5872
6821
  return this.enqueueMediaUpdate(MEDIA_UPDATE_TYPE.UPDATE_MEDIA, options);
5873
6822
  }
5874
6823
 
5875
- if (
5876
- this.isMultistream &&
5877
- (shareAudioEnabled !== undefined || shareVideoEnabled !== undefined)
5878
- ) {
6824
+ if (this.isMultistream) {
6825
+ if (shareAudioEnabled !== undefined || shareVideoEnabled !== undefined) {
6826
+ throw new Error(
6827
+ 'toggling shareAudioEnabled or shareVideoEnabled in a multistream meeting is not supported, to control receiving screen share call meeting.remoteMediaManager.setLayout() with appropriate layout'
6828
+ );
6829
+ }
6830
+ } else if (shareAudioEnabled !== undefined) {
5879
6831
  throw new Error(
5880
- 'toggling shareAudioEnabled or shareVideoEnabled in a multistream meeting is not supported, to control receiving screen share call meeting.remoteMediaManager.setLayout() with appropriate layout'
6832
+ 'toggling shareAudioEnabled in a transcoded meeting is not supported as of now'
5881
6833
  );
5882
6834
  }
5883
6835
 
@@ -6210,17 +7162,13 @@ export default class Meeting extends StatelessWebexPlugin {
6210
7162
  .catch((error) => {
6211
7163
  LoggerProxy.logger.error('Meeting:index#stopWhiteboardShare --> Error ', error);
6212
7164
 
6213
- Metrics.sendBehavioralMetric(
6214
- // @ts-ignore - check if STOP_WHITEBOARD_SHARE_FAILURE exists
6215
- BEHAVIORAL_METRICS.STOP_WHITEBOARD_SHARE_FAILURE,
6216
- {
6217
- correlation_id: this.correlationId,
6218
- locus_id: this.locusUrl.split('/').pop(),
6219
- reason: error.message,
6220
- stack: error.stack,
6221
- board: {channelUrl},
6222
- }
6223
- );
7165
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_STOP_WHITEBOARD_SHARE_FAILURE, {
7166
+ correlation_id: this.correlationId,
7167
+ locus_id: this.locusUrl.split('/').pop(),
7168
+ reason: error.message,
7169
+ stack: error.stack,
7170
+ board: {channelUrl},
7171
+ });
6224
7172
 
6225
7173
  return Promise.reject(error);
6226
7174
  })
@@ -6257,11 +7205,14 @@ export default class Meeting extends StatelessWebexPlugin {
6257
7205
  if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
6258
7206
  // @ts-ignore
6259
7207
  this.webex.internal.newMetrics.submitClientEvent({
6260
- name: 'client.share.initiated',
7208
+ name: 'client.share.floor-grant.request',
6261
7209
  payload: {
6262
7210
  mediaType: 'share',
7211
+ shareInstanceId: this.localShareInstanceId,
7212
+ },
7213
+ options: {
7214
+ meetingId: this.id,
6263
7215
  },
6264
- options: {meetingId: this.id},
6265
7216
  });
6266
7217
 
6267
7218
  return this.meetingRequest
@@ -6271,10 +7222,16 @@ export default class Meeting extends StatelessWebexPlugin {
6271
7222
  deviceUrl: this.deviceUrl,
6272
7223
  uri: content.url,
6273
7224
  resourceUrl: this.resourceUrl,
7225
+ shareInstanceId: this.localShareInstanceId,
6274
7226
  })
6275
7227
  .then(() => {
6276
7228
  this.screenShareFloorState = ScreenShareFloorStatus.GRANTED;
6277
7229
 
7230
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_SUCCESS, {
7231
+ correlation_id: this.correlationId,
7232
+ locus_id: this.locusUrl.split('/').pop(),
7233
+ });
7234
+
6278
7235
  return Promise.resolve();
6279
7236
  })
6280
7237
  .catch((error) => {
@@ -6287,6 +7244,19 @@ export default class Meeting extends StatelessWebexPlugin {
6287
7244
  stack: error.stack,
6288
7245
  });
6289
7246
 
7247
+ // @ts-ignore
7248
+ this.webex.internal.newMetrics.submitClientEvent({
7249
+ name: 'client.share.floor-granted.local',
7250
+ payload: {
7251
+ mediaType: 'share',
7252
+ errors: MeetingUtil.getChangeMeetingFloorErrorPayload(error.message),
7253
+ shareInstanceId: this.localShareInstanceId,
7254
+ },
7255
+ options: {
7256
+ meetingId: this.id,
7257
+ },
7258
+ });
7259
+
6290
7260
  this.screenShareFloorState = ScreenShareFloorStatus.RELEASED;
6291
7261
 
6292
7262
  return Promise.reject(error);
@@ -6338,6 +7308,7 @@ export default class Meeting extends StatelessWebexPlugin {
6338
7308
  name: 'client.share.stopped',
6339
7309
  payload: {
6340
7310
  mediaType: 'share',
7311
+ shareInstanceId: this.localShareInstanceId,
6341
7312
  },
6342
7313
  options: {meetingId: this.id},
6343
7314
  });
@@ -6354,6 +7325,7 @@ export default class Meeting extends StatelessWebexPlugin {
6354
7325
  deviceUrl: this.deviceUrl,
6355
7326
  uri: content.url,
6356
7327
  resourceUrl: this.resourceUrl,
7328
+ shareInstanceId: this.localShareInstanceId,
6357
7329
  })
6358
7330
  .catch((error) => {
6359
7331
  LoggerProxy.logger.error('Meeting:index#releaseScreenShareFloor --> Error ', error);
@@ -6557,8 +7529,8 @@ export default class Meeting extends StatelessWebexPlugin {
6557
7529
 
6558
7530
  if (layoutType) {
6559
7531
  if (!LAYOUT_TYPES.includes(layoutType)) {
6560
- this.rejectWithErrorLog(
6561
- 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType recieved.'
7532
+ return this.rejectWithErrorLog(
7533
+ 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType received.'
6562
7534
  );
6563
7535
  }
6564
7536
 
@@ -6696,6 +7668,23 @@ export default class Meeting extends StatelessWebexPlugin {
6696
7668
  }
6697
7669
  };
6698
7670
 
7671
+ /**
7672
+ * Functionality for when a share video is muted or unmuted.
7673
+ * @private
7674
+ * @memberof Meeting
7675
+ * @param {boolean} muted
7676
+ * @returns {undefined}
7677
+ */
7678
+ private handleShareVideoStreamMuteStateChange = (muted: boolean) => {
7679
+ LoggerProxy.logger.log(
7680
+ `Meeting:index#handleShareVideoStreamMuteStateChange --> Share video stream mute state changed to muted ${muted}`
7681
+ );
7682
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE, {
7683
+ correlationId: this.correlationId,
7684
+ muted,
7685
+ });
7686
+ };
7687
+
6699
7688
  /**
6700
7689
  * Functionality for when a share video is ended.
6701
7690
  * @private
@@ -6796,6 +7785,9 @@ export default class Meeting extends StatelessWebexPlugin {
6796
7785
  if (roles.includes(SELF_ROLES.COHOST)) {
6797
7786
  return 'cohost';
6798
7787
  }
7788
+ if (roles.includes(SELF_ROLES.PRESENTER)) {
7789
+ return 'presenter';
7790
+ }
6799
7791
  if (roles.includes(SELF_ROLES.ATTENDEE)) {
6800
7792
  return 'attendee';
6801
7793
  }
@@ -6886,8 +7878,7 @@ export default class Meeting extends StatelessWebexPlugin {
6886
7878
  this.queuedMediaUpdates = [];
6887
7879
 
6888
7880
  if (this.transcription) {
6889
- this.transcription.closeSocket();
6890
- this.triggerStopReceivingTranscriptionEvent();
7881
+ this.stopTranscription();
6891
7882
  this.transcription = undefined;
6892
7883
  }
6893
7884
  };
@@ -7062,10 +8053,12 @@ export default class Meeting extends StatelessWebexPlugin {
7062
8053
  .update({
7063
8054
  // TODO: RoapMediaConnection is not ready to use stream classes yet, so we pass the raw MediaStreamTrack for now
7064
8055
  localTracks: {
7065
- audio: this.mediaProperties.audioStream?.outputTrack || null,
7066
- video: this.mediaProperties.videoStream?.outputTrack || null,
7067
- screenShareVideo: this.mediaProperties.shareVideoStream?.outputTrack || null,
7068
- screenShareAudio: this.mediaProperties.shareAudioStream?.outputTrack || null,
8056
+ audio: this.mediaProperties.audioStream?.outputStream?.getTracks()[0] || null,
8057
+ video: this.mediaProperties.videoStream?.outputStream?.getTracks()[0] || null,
8058
+ screenShareVideo:
8059
+ this.mediaProperties.shareVideoStream?.outputStream?.getTracks()[0] || null,
8060
+ screenShareAudio:
8061
+ this.mediaProperties.shareAudioStream?.outputStream?.getTracks()[0] || null,
7069
8062
  },
7070
8063
  direction: {
7071
8064
  audio: Media.getDirection(
@@ -7199,6 +8192,23 @@ export default class Meeting extends StatelessWebexPlugin {
7199
8192
  }
7200
8193
 
7201
8194
  if (floorRequestNeeded) {
8195
+ this.localShareInstanceId = uuid.v4();
8196
+
8197
+ // @ts-ignore
8198
+ this.webex.internal.newMetrics.submitClientEvent({
8199
+ name: 'client.share.initiated',
8200
+ payload: {
8201
+ mediaType: 'share',
8202
+ shareInstanceId: this.localShareInstanceId,
8203
+ },
8204
+ options: {meetingId: this.id},
8205
+ });
8206
+
8207
+ this.statsAnalyzer.updateMediaStatus({
8208
+ expected: {
8209
+ sendShare: true,
8210
+ },
8211
+ });
7202
8212
  // we're sending the http request to Locus to request the screen share floor
7203
8213
  // only after the SDP update, because that's how it's always been done for transcoded meetings
7204
8214
  // and also if sharing from the start, we need confluence to have been created
@@ -7247,9 +8257,64 @@ export default class Meeting extends StatelessWebexPlugin {
7247
8257
  if (!this.mediaProperties.hasLocalShareStream()) {
7248
8258
  try {
7249
8259
  this.releaseScreenShareFloor(); // we ignore the returned promise here on purpose
8260
+
8261
+ this.statsAnalyzer.updateMediaStatus({
8262
+ expected: {
8263
+ sendShare: false,
8264
+ },
8265
+ });
7250
8266
  } catch (e) {
7251
8267
  // nothing to do here, error is logged already inside releaseScreenShareFloor()
7252
8268
  }
7253
8269
  }
7254
8270
  }
8271
+
8272
+ /**
8273
+ * Gets permission token expiry information including timeLeft, expiryTime, currentTime
8274
+ * (from the time the function has been fired)
8275
+ *
8276
+ * @returns {object} permissionTokenExpiryInfo
8277
+ * @returns {number} permissionTokenExpiryInfo.timeLeft The time left for token to expire
8278
+ * @returns {number} permissionTokenExpiryInfo.expiryTime The expiry time of permission token from the server
8279
+ * @returns {number} permissionTokenExpiryInfo.currentTime The current time of the local machine
8280
+ */
8281
+ public getPermissionTokenExpiryInfo() {
8282
+ if (!this.permissionTokenPayload) {
8283
+ return undefined;
8284
+ }
8285
+
8286
+ const permissionTokenExpiryFromServer = Number(this.permissionTokenPayload.exp);
8287
+ const permissionTokenIssuedTimeFromServer = Number(this.permissionTokenPayload.iat);
8288
+
8289
+ const shiftInTime = this.permissionTokenReceivedLocalTime - permissionTokenIssuedTimeFromServer;
8290
+
8291
+ // using new Date instead of Date.now() to allow for accurate unit testing
8292
+ // https://github.com/sinonjs/fake-timers/issues/321
8293
+ const currentTime = new Date().getTime();
8294
+
8295
+ // adjusted time is calculated in case your machine time is wrong
8296
+ const adjustedCurrentTime = currentTime - shiftInTime;
8297
+
8298
+ const timeLeft = (permissionTokenExpiryFromServer - adjustedCurrentTime) / 1000;
8299
+
8300
+ return {timeLeft, expiryTime: permissionTokenExpiryFromServer, currentTime};
8301
+ }
8302
+
8303
+ /**
8304
+ * Check if there is enough time left till the permission token expires
8305
+ * If not - refresh the permission token
8306
+ *
8307
+ * @param {number} threshold - time in seconds
8308
+ * @param {string} reason - reason for refreshing the permission token
8309
+ * @returns {Promise<void>}
8310
+ */
8311
+ public checkAndRefreshPermissionToken(threshold: number, reason: string): Promise<void> {
8312
+ const timeLeft = this.getPermissionTokenExpiryInfo()?.timeLeft;
8313
+
8314
+ if (timeLeft !== undefined && timeLeft <= threshold) {
8315
+ return this.refreshPermissionToken(reason);
8316
+ }
8317
+
8318
+ return Promise.resolve();
8319
+ }
7255
8320
  }