@webex/plugin-meetings 3.0.0-stream-classes.5 → 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 (465) 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} +71 -15
  80. package/dist/constants.js +252 -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 +38 -37
  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 +43 -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 +318 -45
  137. package/dist/meeting/index.js +2620 -1405
  138. package/dist/meeting/index.js.map +1 -1
  139. package/dist/meeting/locusMediaRequest.js +4 -5
  140. package/dist/meeting/locusMediaRequest.js.map +1 -1
  141. package/dist/meeting/muteState.js +2 -4
  142. package/dist/meeting/muteState.js.map +1 -1
  143. package/dist/{types/meeting → meeting}/request.d.ts +2 -0
  144. package/dist/meeting/request.js +46 -31
  145. package/dist/meeting/request.js.map +1 -1
  146. package/dist/meeting/state.js +1 -2
  147. package/dist/meeting/state.js.map +1 -1
  148. package/dist/{types/meeting → meeting}/util.d.ts +17 -0
  149. package/dist/meeting/util.js +83 -10
  150. package/dist/meeting/util.js.map +1 -1
  151. package/dist/meeting/voicea-meeting.d.ts +16 -0
  152. package/dist/meeting/voicea-meeting.js +169 -0
  153. package/dist/meeting/voicea-meeting.js.map +1 -0
  154. package/dist/meeting-info/collection.js +3 -4
  155. package/dist/meeting-info/collection.js.map +1 -1
  156. package/dist/{types/meeting-info → meeting-info}/index.d.ts +7 -0
  157. package/dist/meeting-info/index.js +53 -27
  158. package/dist/meeting-info/index.js.map +1 -1
  159. package/dist/{types/meeting-info → meeting-info}/meeting-info-v2.d.ts +1 -0
  160. package/dist/meeting-info/meeting-info-v2.js +52 -33
  161. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  162. package/dist/meeting-info/request.js +1 -2
  163. package/dist/meeting-info/request.js.map +1 -1
  164. package/dist/meeting-info/util.js +8 -8
  165. package/dist/meeting-info/util.js.map +1 -1
  166. package/dist/meeting-info/utilv2.js +12 -9
  167. package/dist/meeting-info/utilv2.js.map +1 -1
  168. package/dist/{types/meetings → meetings}/collection.d.ts +9 -0
  169. package/dist/meetings/collection.js +21 -5
  170. package/dist/meetings/collection.js.map +1 -1
  171. package/dist/{types/meetings → meetings}/index.d.ts +45 -16
  172. package/dist/meetings/index.js +166 -74
  173. package/dist/meetings/index.js.map +1 -1
  174. package/dist/meetings/request.js +2 -3
  175. package/dist/meetings/request.js.map +1 -1
  176. package/dist/meetings/util.js +3 -10
  177. package/dist/meetings/util.js.map +1 -1
  178. package/dist/{types/member → member}/index.d.ts +1 -0
  179. package/dist/member/index.js +10 -3
  180. package/dist/member/index.js.map +1 -1
  181. package/dist/member/member.types.d.ts +11 -0
  182. package/dist/member/member.types.js +17 -0
  183. package/dist/member/member.types.js.map +1 -0
  184. package/dist/member/types.js +6 -8
  185. package/dist/member/types.js.map +1 -1
  186. package/dist/member/util.js +12 -2
  187. package/dist/member/util.js.map +1 -1
  188. package/dist/members/collection.js +1 -2
  189. package/dist/members/collection.js.map +1 -1
  190. package/dist/members/index.js +25 -8
  191. package/dist/members/index.js.map +1 -1
  192. package/dist/members/request.js +2 -3
  193. package/dist/members/request.js.map +1 -1
  194. package/dist/{types/members → members}/types.d.ts +1 -0
  195. package/dist/members/types.js +3 -4
  196. package/dist/members/types.js.map +1 -1
  197. package/dist/{types/members → members}/util.d.ts +6 -1
  198. package/dist/members/util.js +18 -8
  199. package/dist/members/util.js.map +1 -1
  200. package/dist/{types/metrics → metrics}/constants.d.ts +12 -0
  201. package/dist/metrics/constants.js +14 -3
  202. package/dist/metrics/constants.js.map +1 -1
  203. package/dist/metrics/index.js +3 -2
  204. package/dist/metrics/index.js.map +1 -1
  205. package/dist/multistream/mediaRequestManager.js +9 -11
  206. package/dist/multistream/mediaRequestManager.js.map +1 -1
  207. package/dist/multistream/receiveSlot.js +3 -5
  208. package/dist/multistream/receiveSlot.js.map +1 -1
  209. package/dist/multistream/receiveSlotManager.js +7 -9
  210. package/dist/multistream/receiveSlotManager.js.map +1 -1
  211. package/dist/multistream/remoteMedia.js +3 -5
  212. package/dist/multistream/remoteMedia.js.map +1 -1
  213. package/dist/multistream/remoteMediaGroup.js +7 -6
  214. package/dist/multistream/remoteMediaGroup.js.map +1 -1
  215. package/dist/multistream/remoteMediaManager.js +28 -27
  216. package/dist/multistream/remoteMediaManager.js.map +1 -1
  217. package/dist/multistream/sendSlotManager.js +9 -6
  218. package/dist/multistream/sendSlotManager.js.map +1 -1
  219. package/dist/networkQualityMonitor/index.js +1 -2
  220. package/dist/networkQualityMonitor/index.js.map +1 -1
  221. package/dist/personal-meeting-room/index.js +2 -3
  222. package/dist/personal-meeting-room/index.js.map +1 -1
  223. package/dist/personal-meeting-room/request.js +2 -3
  224. package/dist/personal-meeting-room/request.js.map +1 -1
  225. package/dist/personal-meeting-room/util.js +1 -2
  226. package/dist/personal-meeting-room/util.js.map +1 -1
  227. package/dist/reachability/clusterReachability.d.ts +109 -0
  228. package/dist/reachability/clusterReachability.js +357 -0
  229. package/dist/reachability/clusterReachability.js.map +1 -0
  230. package/dist/reachability/index.d.ts +105 -0
  231. package/dist/reachability/index.js +279 -436
  232. package/dist/reachability/index.js.map +1 -1
  233. package/dist/reachability/request.js +14 -11
  234. package/dist/reachability/request.js.map +1 -1
  235. package/dist/reachability/util.d.ts +8 -0
  236. package/dist/reachability/util.js +29 -0
  237. package/dist/reachability/util.js.map +1 -0
  238. package/dist/reactions/constants.js +1 -2
  239. package/dist/reactions/constants.js.map +1 -1
  240. package/dist/reactions/reactions.js +2 -4
  241. package/dist/reactions/reactions.js.map +1 -1
  242. package/dist/reactions/reactions.type.js +6 -8
  243. package/dist/reactions/reactions.type.js.map +1 -1
  244. package/dist/{types/reconnection-manager → reconnection-manager}/index.d.ts +10 -0
  245. package/dist/reconnection-manager/index.js +129 -106
  246. package/dist/reconnection-manager/index.js.map +1 -1
  247. package/dist/recording-controller/enums.js +4 -5
  248. package/dist/recording-controller/enums.js.map +1 -1
  249. package/dist/recording-controller/index.js +43 -51
  250. package/dist/recording-controller/index.js.map +1 -1
  251. package/dist/recording-controller/util.js +1 -2
  252. package/dist/recording-controller/util.js.map +1 -1
  253. package/dist/{types/roap → roap}/index.d.ts +2 -1
  254. package/dist/roap/index.js +59 -28
  255. package/dist/roap/index.js.map +1 -1
  256. package/dist/roap/request.js +14 -22
  257. package/dist/roap/request.js.map +1 -1
  258. package/dist/{types/roap → roap}/turnDiscovery.d.ts +21 -4
  259. package/dist/roap/turnDiscovery.js +182 -89
  260. package/dist/roap/turnDiscovery.js.map +1 -1
  261. package/dist/rtcMetrics/constants.js +1 -2
  262. package/dist/rtcMetrics/constants.js.map +1 -1
  263. package/dist/{types/rtcMetrics → rtcMetrics}/index.d.ts +15 -1
  264. package/dist/rtcMetrics/index.js +72 -12
  265. package/dist/rtcMetrics/index.js.map +1 -1
  266. package/dist/statsAnalyzer/global.js +1 -2
  267. package/dist/statsAnalyzer/global.js.map +1 -1
  268. package/dist/{types/statsAnalyzer → statsAnalyzer}/index.d.ts +28 -11
  269. package/dist/statsAnalyzer/index.js +371 -318
  270. package/dist/statsAnalyzer/index.js.map +1 -1
  271. package/dist/statsAnalyzer/mqaUtil.d.ts +48 -0
  272. package/dist/statsAnalyzer/mqaUtil.js +295 -162
  273. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  274. package/dist/transcription/index.js +1 -2
  275. package/dist/transcription/index.js.map +1 -1
  276. package/dist/webinar/collection.d.ts +16 -0
  277. package/dist/webinar/collection.js +43 -0
  278. package/dist/webinar/collection.js.map +1 -0
  279. package/dist/webinar/index.d.ts +5 -0
  280. package/dist/webinar/index.js +68 -0
  281. package/dist/webinar/index.js.map +1 -0
  282. package/jest.config.js +3 -0
  283. package/package.json +44 -24
  284. package/process +1 -0
  285. package/src/common/errors/no-meeting-info.ts +24 -0
  286. package/src/common/errors/reclaim-host-role-errors.ts +134 -0
  287. package/src/common/errors/webex-errors.ts +19 -2
  288. package/src/common/logs/request.ts +5 -1
  289. package/src/config.ts +3 -5
  290. package/src/constants.ts +77 -8
  291. package/src/index.ts +4 -0
  292. package/src/interceptors/index.ts +3 -0
  293. package/src/interceptors/locusRetry.ts +67 -0
  294. package/src/locus-info/index.ts +19 -14
  295. package/src/locus-info/mediaSharesUtils.ts +16 -0
  296. package/src/locus-info/parser.ts +40 -21
  297. package/src/media/index.ts +8 -6
  298. package/src/media/properties.ts +17 -2
  299. package/src/mediaQualityMetrics/config.ts +103 -238
  300. package/src/meeting/in-meeting-actions.ts +8 -0
  301. package/src/meeting/index.ts +1664 -642
  302. package/src/meeting/request.ts +18 -0
  303. package/src/meeting/util.ts +102 -1
  304. package/src/meeting/voicea-meeting.ts +122 -0
  305. package/src/meeting-info/index.ts +47 -20
  306. package/src/meeting-info/meeting-info-v2.ts +32 -16
  307. package/src/meeting-info/util.ts +12 -9
  308. package/src/meeting-info/utilv2.ts +25 -15
  309. package/src/meetings/collection.ts +13 -0
  310. package/src/meetings/index.ts +112 -31
  311. package/src/meetings/util.ts +2 -8
  312. package/src/member/index.ts +9 -1
  313. package/src/member/member.types.ts +13 -0
  314. package/src/member/util.ts +14 -0
  315. package/src/members/index.ts +29 -2
  316. package/src/members/types.ts +1 -0
  317. package/src/members/util.ts +15 -1
  318. package/src/metrics/constants.ts +12 -0
  319. package/src/reachability/clusterReachability.ts +320 -0
  320. package/src/reachability/index.ts +221 -382
  321. package/src/reachability/request.ts +1 -1
  322. package/src/reachability/util.ts +24 -0
  323. package/src/reconnection-manager/index.ts +87 -83
  324. package/src/roap/index.ts +60 -24
  325. package/src/roap/request.ts +4 -17
  326. package/src/roap/turnDiscovery.ts +112 -39
  327. package/src/rtcMetrics/index.ts +71 -5
  328. package/src/statsAnalyzer/index.ts +430 -427
  329. package/src/statsAnalyzer/mqaUtil.ts +317 -168
  330. package/src/webinar/collection.ts +31 -0
  331. package/src/webinar/index.ts +62 -0
  332. package/test/integration/spec/journey.js +12 -12
  333. package/test/integration/spec/space-meeting.js +1 -1
  334. package/test/unit/spec/breakouts/breakout.ts +2 -1
  335. package/test/unit/spec/breakouts/index.ts +7 -4
  336. package/test/unit/spec/interceptors/locusRetry.ts +131 -0
  337. package/test/unit/spec/locus-info/index.js +88 -12
  338. package/test/unit/spec/locus-info/lib/SeqCmp.json +16 -0
  339. package/test/unit/spec/locus-info/mediaSharesUtils.ts +10 -0
  340. package/test/unit/spec/locus-info/parser.js +54 -13
  341. package/test/unit/spec/locus-info/selfUtils.js +1 -1
  342. package/test/unit/spec/media/index.ts +25 -4
  343. package/test/unit/spec/media/properties.ts +2 -2
  344. package/test/unit/spec/meeting/in-meeting-actions.ts +4 -0
  345. package/test/unit/spec/meeting/index.js +4388 -1382
  346. package/test/unit/spec/meeting/request.js +63 -12
  347. package/test/unit/spec/meeting/utils.js +145 -10
  348. package/test/unit/spec/meeting/voicea-meeting.ts +266 -0
  349. package/test/unit/spec/meeting-info/index.js +180 -61
  350. package/test/unit/spec/meeting-info/meetinginfov2.js +216 -68
  351. package/test/unit/spec/meetings/collection.js +12 -0
  352. package/test/unit/spec/meetings/index.js +674 -193
  353. package/test/unit/spec/meetings/utils.js +35 -12
  354. package/test/unit/spec/member/index.js +8 -7
  355. package/test/unit/spec/member/util.js +32 -0
  356. package/test/unit/spec/members/index.js +130 -17
  357. package/test/unit/spec/members/utils.js +26 -0
  358. package/test/unit/spec/metrics/index.js +1 -2
  359. package/test/unit/spec/multistream/mediaRequestManager.ts +1 -0
  360. package/test/unit/spec/reachability/clusterReachability.ts +279 -0
  361. package/test/unit/spec/reachability/index.ts +505 -135
  362. package/test/unit/spec/reachability/util.ts +40 -0
  363. package/test/unit/spec/reconnection-manager/index.js +74 -17
  364. package/test/unit/spec/recording-controller/index.js +0 -1
  365. package/test/unit/spec/roap/index.ts +181 -61
  366. package/test/unit/spec/roap/request.ts +27 -3
  367. package/test/unit/spec/roap/turnDiscovery.ts +363 -102
  368. package/test/unit/spec/rtcMetrics/index.ts +57 -3
  369. package/test/unit/spec/stats-analyzer/index.js +1225 -12
  370. package/test/unit/spec/webinar/collection.ts +13 -0
  371. package/test/unit/spec/webinar/index.ts +60 -0
  372. package/test/utils/webex-test-users.js +12 -4
  373. package/dist/types/mediaQualityMetrics/config.d.ts +0 -365
  374. package/dist/types/reachability/index.d.ts +0 -152
  375. package/dist/types/statsAnalyzer/mqaUtil.d.ts +0 -24
  376. /package/dist/{types/annotation → annotation}/annotation.types.d.ts +0 -0
  377. /package/dist/{types/annotation → annotation}/constants.d.ts +0 -0
  378. /package/dist/{types/annotation → annotation}/index.d.ts +0 -0
  379. /package/dist/{types/breakouts → breakouts}/breakout.d.ts +0 -0
  380. /package/dist/{types/breakouts → breakouts}/collection.d.ts +0 -0
  381. /package/dist/{types/breakouts → breakouts}/edit-lock-error.d.ts +0 -0
  382. /package/dist/{types/breakouts → breakouts}/events.d.ts +0 -0
  383. /package/dist/{types/breakouts → breakouts}/index.d.ts +0 -0
  384. /package/dist/{types/breakouts → breakouts}/request.d.ts +0 -0
  385. /package/dist/{types/breakouts → breakouts}/utils.d.ts +0 -0
  386. /package/dist/{types/common → common}/browser-detection.d.ts +0 -0
  387. /package/dist/{types/common → common}/collection.d.ts +0 -0
  388. /package/dist/{types/common → common}/config.d.ts +0 -0
  389. /package/dist/{types/common → common}/errors/captcha-error.d.ts +0 -0
  390. /package/dist/{types/common → common}/errors/intent-to-join.d.ts +0 -0
  391. /package/dist/{types/common → common}/errors/join-meeting.d.ts +0 -0
  392. /package/dist/{types/common → common}/errors/media.d.ts +0 -0
  393. /package/dist/{types/common → common}/errors/parameter.d.ts +0 -0
  394. /package/dist/{types/common → common}/errors/password-error.d.ts +0 -0
  395. /package/dist/{types/common → common}/errors/permission.d.ts +0 -0
  396. /package/dist/{types/common → common}/errors/reconnection-in-progress.d.ts +0 -0
  397. /package/dist/{types/common → common}/errors/reconnection.d.ts +0 -0
  398. /package/dist/{types/common → common}/errors/stats.d.ts +0 -0
  399. /package/dist/{types/common → common}/errors/webex-meetings-error.d.ts +0 -0
  400. /package/dist/{types/common → common}/events/events-scope.d.ts +0 -0
  401. /package/dist/{types/common → common}/events/events.d.ts +0 -0
  402. /package/dist/{types/common → common}/events/trigger-proxy.d.ts +0 -0
  403. /package/dist/{types/common → common}/events/util.d.ts +0 -0
  404. /package/dist/{types/common → common}/logs/logger-config.d.ts +0 -0
  405. /package/dist/{types/common → common}/logs/logger-proxy.d.ts +0 -0
  406. /package/dist/{types/common → common}/queue.d.ts +0 -0
  407. /package/dist/{types/controls-options-manager → controls-options-manager}/constants.d.ts +0 -0
  408. /package/dist/{types/controls-options-manager → controls-options-manager}/enums.d.ts +0 -0
  409. /package/dist/{types/controls-options-manager → controls-options-manager}/index.d.ts +0 -0
  410. /package/dist/{types/controls-options-manager → controls-options-manager}/types.d.ts +0 -0
  411. /package/dist/{types/controls-options-manager → controls-options-manager}/util.d.ts +0 -0
  412. /package/dist/{types/index.d.ts → index.d.ts} +0 -0
  413. /package/dist/{types/interpretation → interpretation}/collection.d.ts +0 -0
  414. /package/dist/{types/interpretation → interpretation}/index.d.ts +0 -0
  415. /package/dist/{types/interpretation → interpretation}/siLanguage.d.ts +0 -0
  416. /package/dist/{types/locus-info → locus-info}/controlsUtils.d.ts +0 -0
  417. /package/dist/{types/locus-info → locus-info}/embeddedAppsUtils.d.ts +0 -0
  418. /package/dist/{types/locus-info → locus-info}/fullState.d.ts +0 -0
  419. /package/dist/{types/locus-info → locus-info}/hostUtils.d.ts +0 -0
  420. /package/dist/{types/locus-info → locus-info}/infoUtils.d.ts +0 -0
  421. /package/dist/{types/locus-info → locus-info}/mediaSharesUtils.d.ts +0 -0
  422. /package/dist/{types/locus-info → locus-info}/selfUtils.d.ts +0 -0
  423. /package/dist/{types/media → media}/index.d.ts +0 -0
  424. /package/dist/{types/media → media}/properties.d.ts +0 -0
  425. /package/dist/{types/media → media}/util.d.ts +0 -0
  426. /package/dist/{types/meeting → meeting}/locusMediaRequest.d.ts +0 -0
  427. /package/dist/{types/meeting → meeting}/muteState.d.ts +0 -0
  428. /package/dist/{types/meeting → meeting}/request.type.d.ts +0 -0
  429. /package/dist/{types/meeting → meeting}/state.d.ts +0 -0
  430. /package/dist/{types/meeting-info → meeting-info}/collection.d.ts +0 -0
  431. /package/dist/{types/meeting-info → meeting-info}/request.d.ts +0 -0
  432. /package/dist/{types/meeting-info → meeting-info}/util.d.ts +0 -0
  433. /package/dist/{types/meeting-info → meeting-info}/utilv2.d.ts +0 -0
  434. /package/dist/{types/meetings → meetings}/meetings.types.d.ts +0 -0
  435. /package/dist/{types/meetings → meetings}/request.d.ts +0 -0
  436. /package/dist/{types/meetings → meetings}/util.d.ts +0 -0
  437. /package/dist/{types/member → member}/types.d.ts +0 -0
  438. /package/dist/{types/member → member}/util.d.ts +0 -0
  439. /package/dist/{types/members → members}/collection.d.ts +0 -0
  440. /package/dist/{types/members → members}/index.d.ts +0 -0
  441. /package/dist/{types/members → members}/request.d.ts +0 -0
  442. /package/dist/{types/metrics → metrics}/index.d.ts +0 -0
  443. /package/dist/{types/multistream → multistream}/mediaRequestManager.d.ts +0 -0
  444. /package/dist/{types/multistream → multistream}/receiveSlot.d.ts +0 -0
  445. /package/dist/{types/multistream → multistream}/receiveSlotManager.d.ts +0 -0
  446. /package/dist/{types/multistream → multistream}/remoteMedia.d.ts +0 -0
  447. /package/dist/{types/multistream → multistream}/remoteMediaGroup.d.ts +0 -0
  448. /package/dist/{types/multistream → multistream}/remoteMediaManager.d.ts +0 -0
  449. /package/dist/{types/multistream → multistream}/sendSlotManager.d.ts +0 -0
  450. /package/dist/{types/networkQualityMonitor → networkQualityMonitor}/index.d.ts +0 -0
  451. /package/dist/{types/personal-meeting-room → personal-meeting-room}/index.d.ts +0 -0
  452. /package/dist/{types/personal-meeting-room → personal-meeting-room}/request.d.ts +0 -0
  453. /package/dist/{types/personal-meeting-room → personal-meeting-room}/util.d.ts +0 -0
  454. /package/dist/{types/reachability → reachability}/request.d.ts +0 -0
  455. /package/dist/{types/reactions → reactions}/constants.d.ts +0 -0
  456. /package/dist/{types/reactions → reactions}/reactions.d.ts +0 -0
  457. /package/dist/{types/reactions → reactions}/reactions.type.d.ts +0 -0
  458. /package/dist/{types/recording-controller → recording-controller}/enums.d.ts +0 -0
  459. /package/dist/{types/recording-controller → recording-controller}/index.d.ts +0 -0
  460. /package/dist/{types/recording-controller → recording-controller}/util.d.ts +0 -0
  461. /package/dist/{types/roap → roap}/request.d.ts +0 -0
  462. /package/dist/{types/rtcMetrics → rtcMetrics}/constants.d.ts +0 -0
  463. /package/dist/{types/statsAnalyzer → statsAnalyzer}/global.d.ts +0 -0
  464. /package/dist/{types/transcription → transcription}/index.d.ts +0 -0
  465. /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;
@@ -546,6 +609,7 @@ export default class Meeting extends StatelessWebexPlugin {
546
609
  meetingState: any;
547
610
  permissionToken: string;
548
611
  permissionTokenPayload: any;
612
+ permissionTokenReceivedLocalTime: number;
549
613
  resourceId: any;
550
614
  resourceUrl: string;
551
615
  selfId: string;
@@ -557,7 +621,55 @@ export default class Meeting extends StatelessWebexPlugin {
557
621
  environment: string;
558
622
  namespace = MEETINGS;
559
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;
560
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;
561
673
 
562
674
  /**
563
675
  * @param {Object} attrs
@@ -592,20 +704,22 @@ export default class Meeting extends StatelessWebexPlugin {
592
704
  */
593
705
  this.id = uuid.v4();
594
706
  /**
595
- * Correlation ID used for network tracking of meeting
707
+ * Call state used for metrics
596
708
  * @instance
597
- * @type {String}
709
+ * @type {CallStateForMetrics}
598
710
  * @readonly
599
711
  * @public
600
712
  * @memberof Meeting
601
713
  */
602
- if (attrs.correlationId) {
714
+ this.callStateForMetrics = attrs.callStateForMetrics || {};
715
+ const correlationId = attrs.correlationId || attrs.callStateForMetrics?.correlationId;
716
+ if (correlationId) {
603
717
  LoggerProxy.logger.log(
604
- `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}`
605
719
  );
606
- this.correlationId = attrs.correlationId;
720
+ this.callStateForMetrics.correlationId = correlationId;
607
721
  } else {
608
- this.correlationId = this.id;
722
+ this.callStateForMetrics.correlationId = this.id;
609
723
  }
610
724
  /**
611
725
  * @instance
@@ -673,6 +787,14 @@ export default class Meeting extends StatelessWebexPlugin {
673
787
  */
674
788
  // @ts-ignore
675
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});
676
798
  /**
677
799
  * helper class for managing receive slots (for multistream media connections)
678
800
  */
@@ -1069,13 +1191,14 @@ export default class Meeting extends StatelessWebexPlugin {
1069
1191
  */
1070
1192
  this.networkQualityMonitor = null;
1071
1193
  /**
1194
+ * Indicates network status of the webrtc media connection
1072
1195
  * @instance
1073
1196
  * @type {String}
1074
1197
  * @readonly
1075
1198
  * @public
1076
1199
  * @memberof Meeting
1077
1200
  */
1078
- this.networkStatus = null;
1201
+ this.networkStatus = undefined;
1079
1202
  /**
1080
1203
  * Passing only info as we send basic info for meeting added event
1081
1204
  * @instance
@@ -1140,7 +1263,17 @@ export default class Meeting extends StatelessWebexPlugin {
1140
1263
  * @private
1141
1264
  * @memberof Meeting
1142
1265
  */
1143
- 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;
1144
1277
 
1145
1278
  /**
1146
1279
  * Password status. If it's PASSWORD_STATUS.REQUIRED then verifyPassword() needs to be called
@@ -1194,6 +1327,24 @@ export default class Meeting extends StatelessWebexPlugin {
1194
1327
  */
1195
1328
  this.keepAliveTimerId = null;
1196
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
+
1197
1348
  /**
1198
1349
  * The class that helps to control recording functions: start, stop, pause, resume, etc
1199
1350
  * @instance
@@ -1246,6 +1397,60 @@ export default class Meeting extends StatelessWebexPlugin {
1246
1397
  this.updateTranscodedMediaConnection();
1247
1398
  }
1248
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;
1249
1454
  }
1250
1455
 
1251
1456
  /**
@@ -1279,23 +1484,87 @@ export default class Meeting extends StatelessWebexPlugin {
1279
1484
  }
1280
1485
 
1281
1486
  /**
1282
- * Fetches meeting information.
1283
- * @param {Object} options
1284
- * @param {String} [options.password] optional
1285
- * @param {String} [options.captchaCode] optional
1286
- * @public
1287
- * @memberof Meeting
1288
- * @returns {Promise}
1487
+ * Getter - Returns callStateForMetrics.correlationId
1488
+ * @returns {string}
1289
1489
  */
1290
- public async fetchMeetingInfo({
1291
- password = null,
1292
- captchaCode = null,
1293
- extraParams = {},
1294
- }: {
1295
- password?: string;
1296
- captchaCode?: string;
1297
- extraParams?: Record<string, any>;
1298
- }) {
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> {
1299
1568
  // when fetch meeting info is called directly by the client, we want to clear out the random timer for sdk to do it
1300
1569
  if (this.fetchMeetingInfoTimeoutId) {
1301
1570
  clearTimeout(this.fetchMeetingInfoTimeoutId);
@@ -1303,7 +1572,7 @@ export default class Meeting extends StatelessWebexPlugin {
1303
1572
  }
1304
1573
  if (captchaCode && !this.requiredCaptcha) {
1305
1574
  return Promise.reject(
1306
- new Error('fetchMeetingInfo() called with captchaCode when captcha was not required')
1575
+ new Error(`${caller}() called with captchaCode when captcha was not required`)
1307
1576
  );
1308
1577
  }
1309
1578
  if (
@@ -1312,50 +1581,47 @@ export default class Meeting extends StatelessWebexPlugin {
1312
1581
  this.passwordStatus !== PASSWORD_STATUS.UNKNOWN
1313
1582
  ) {
1314
1583
  return Promise.reject(
1315
- new Error('fetchMeetingInfo() called with password when password was not required')
1584
+ new Error(`${caller}() called with password when password was not required`)
1316
1585
  );
1317
1586
  }
1318
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> {
1319
1606
  try {
1320
1607
  const captchaInfo = captchaCode
1321
1608
  ? {code: captchaCode, id: this.requiredCaptcha.captchaId}
1322
1609
  : null;
1323
1610
 
1324
1611
  const info = await this.attrs.meetingInfoProvider.fetchMeetingInfo(
1325
- this.destination,
1326
- this.destinationType,
1612
+ destination,
1613
+ destinationType,
1327
1614
  password,
1328
1615
  captchaInfo,
1329
1616
  // @ts-ignore - config coming from registerPlugin
1330
1617
  this.config.installedOrgID,
1331
1618
  this.locusId,
1332
1619
  extraParams,
1333
- {meetingId: this.id}
1334
- );
1335
-
1336
- this.parseMeetingInfo(info, this.destination);
1337
- this.meetingInfo = info ? {...info.body, meetingLookupUrl: info?.url} : null;
1338
- this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NONE;
1339
- this.requiredCaptcha = null;
1340
- if (
1341
- this.passwordStatus === PASSWORD_STATUS.REQUIRED ||
1342
- this.passwordStatus === PASSWORD_STATUS.VERIFIED
1343
- ) {
1344
- this.passwordStatus = PASSWORD_STATUS.VERIFIED;
1345
- } else {
1346
- this.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
1347
- }
1348
-
1349
- Trigger.trigger(
1350
- this,
1351
- {
1352
- file: 'meetings',
1353
- function: 'fetchMeetingInfo',
1354
- },
1355
- EVENT_TRIGGERS.MEETING_INFO_AVAILABLE
1620
+ {meetingId: this.id, sendCAevents}
1356
1621
  );
1357
1622
 
1358
- this.updateMeetingActions();
1623
+ this.parseMeetingInfo(info?.body, this.destination, info?.errors);
1624
+ this.setMeetingInfo(info?.body, info?.url);
1359
1625
 
1360
1626
  return Promise.resolve();
1361
1627
  } catch (err) {
@@ -1417,19 +1683,113 @@ export default class Meeting extends StatelessWebexPlugin {
1417
1683
  }
1418
1684
  }
1419
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
+
1420
1778
  /**
1421
1779
  * Checks if the supplied password/host key is correct. It returns a promise with information whether the
1422
1780
  * password and captcha code were correct or not.
1423
1781
  * @param {String} password - this can be either a password or a host key, can be undefined if only captcha was required
1424
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
1425
1784
  * @public
1426
1785
  * @memberof Meeting
1427
1786
  * @returns {Promise<{isPasswordValid: boolean, requiredCaptcha: boolean, failureReason: MEETING_INFO_FAILURE_REASON}>}
1428
1787
  */
1429
- public verifyPassword(password: string, captchaCode: string) {
1788
+ public verifyPassword(password: string, captchaCode: string, sendCAevents = false) {
1430
1789
  return this.fetchMeetingInfo({
1431
1790
  password,
1432
1791
  captchaCode,
1792
+ sendCAevents,
1433
1793
  })
1434
1794
  .then(() => {
1435
1795
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.VERIFY_PASSWORD_SUCCESS);
@@ -1629,6 +1989,7 @@ export default class Meeting extends StatelessWebexPlugin {
1629
1989
  * @memberof Meeting
1630
1990
  */
1631
1991
  private setUpInterpretationListener() {
1992
+ // TODO: check if its getting used or not
1632
1993
  this.simultaneousInterpretation.on(INTERPRETATION.EVENTS.SUPPORT_LANGUAGES_UPDATE, () => {
1633
1994
  Trigger.trigger(
1634
1995
  this,
@@ -1639,7 +2000,7 @@ export default class Meeting extends StatelessWebexPlugin {
1639
2000
  EVENT_TRIGGERS.MEETING_INTERPRETATION_SUPPORT_LANGUAGES_UPDATE
1640
2001
  );
1641
2002
  });
1642
-
2003
+ // TODO: check if its getting used or not
1643
2004
  this.simultaneousInterpretation.on(
1644
2005
  INTERPRETATION.EVENTS.HANDOFF_REQUESTS_ARRIVED,
1645
2006
  (payload) => {
@@ -1656,6 +2017,43 @@ export default class Meeting extends StatelessWebexPlugin {
1656
2017
  );
1657
2018
  }
1658
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
+
1659
2057
  /**
1660
2058
  * Set up the locus info listener for meetings disconnected due to inactivity
1661
2059
  * @returns {undefined}
@@ -1760,12 +2158,12 @@ export default class Meeting extends StatelessWebexPlugin {
1760
2158
 
1761
2159
  /**
1762
2160
  * sets the network status on meeting object
1763
- * @param {String} networkStatus
2161
+ * @param {NETWORK_STATUS} networkStatus
1764
2162
  * @private
1765
2163
  * @returns {undefined}
1766
2164
  * @memberof Meeting
1767
2165
  */
1768
- private setNetworkStatus(networkStatus: string) {
2166
+ private setNetworkStatus(networkStatus?: NETWORK_STATUS) {
1769
2167
  if (networkStatus === NETWORK_STATUS.DISCONNECTED) {
1770
2168
  Trigger.trigger(
1771
2169
  this,
@@ -1945,7 +2343,6 @@ export default class Meeting extends StatelessWebexPlugin {
1945
2343
  modifiedBy,
1946
2344
  lastModified,
1947
2345
  };
1948
-
1949
2346
  Trigger.trigger(
1950
2347
  this,
1951
2348
  {
@@ -1976,19 +2373,22 @@ export default class Meeting extends StatelessWebexPlugin {
1976
2373
  this.locusInfo.on(
1977
2374
  LOCUSINFO.EVENTS.CONTROLS_MEETING_TRANSCRIBE_UPDATED,
1978
2375
  ({caption, transcribing}) => {
1979
- // @ts-ignore - config coming from registerPlugin
1980
- if (transcribing && this.transcription && this.config.receiveTranscription) {
1981
- this.receiveTranscription();
1982
- } else if (!transcribing && this.transcription) {
1983
- Trigger.trigger(
1984
- this,
1985
- {
1986
- file: 'meeting/index',
1987
- function: 'setupLocusControlsListener',
1988
- },
1989
- EVENT_TRIGGERS.MEETING_STOPPED_RECEIVING_TRANSCRIPTION,
1990
- {caption, transcribing}
1991
- );
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
+ }
1992
2392
  }
1993
2393
  }
1994
2394
  );
@@ -2020,16 +2420,6 @@ export default class Meeting extends StatelessWebexPlugin {
2020
2420
  }
2021
2421
  );
2022
2422
 
2023
- this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_JOIN_BREAKOUT_FROM_MAIN, ({mainLocusUrl}) => {
2024
- this.meetingRequest.getLocusStatusByUrl(mainLocusUrl).catch((error) => {
2025
- // clear main session cache when attendee join into breakout and forbidden to get locus from main locus url,
2026
- // which means main session is not active for the attendee
2027
- if (error?.statusCode === 403) {
2028
- this.locusInfo.clearMainSessionLocusCache();
2029
- }
2030
- });
2031
- });
2032
-
2033
2423
  this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_ENTRY_EXIT_TONE_UPDATED, ({entryExitTone}) => {
2034
2424
  Trigger.trigger(
2035
2425
  this,
@@ -2153,6 +2543,7 @@ export default class Meeting extends StatelessWebexPlugin {
2153
2543
  if (
2154
2544
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
2155
2545
  contentShare.disposition === previousContentShare?.disposition &&
2546
+ contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
2156
2547
  whiteboardShare.beneficiaryId === previousWhiteboardShare?.beneficiaryId &&
2157
2548
  whiteboardShare.disposition === previousWhiteboardShare?.disposition &&
2158
2549
  whiteboardShare.resourceUrl === previousWhiteboardShare?.resourceUrl
@@ -2175,11 +2566,21 @@ export default class Meeting extends StatelessWebexPlugin {
2175
2566
  // LOCAL - check if we started sharing content
2176
2567
  else if (
2177
2568
  this.selfId === contentShare.beneficiaryId &&
2178
- contentShare.disposition === FLOOR_ACTION.GRANTED
2569
+ contentShare.disposition === FLOOR_ACTION.GRANTED &&
2570
+ contentShare.deviceUrlSharing === this.deviceUrl
2179
2571
  ) {
2180
2572
  // CONTENT - sharing content local
2181
2573
  newShareStatus = SHARE_STATUS.LOCAL_SHARE_ACTIVE;
2182
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
+ }
2183
2584
  // If we did not hit the cases above, no one is sharng content, so we check if we are sharing whiteboard
2184
2585
  // There is no concept of local/remote share for whiteboard
2185
2586
  // It does not matter who requested to share the whiteboard, everyone gets the same view
@@ -2253,6 +2654,8 @@ export default class Meeting extends StatelessWebexPlugin {
2253
2654
  switch (newShareStatus) {
2254
2655
  case SHARE_STATUS.REMOTE_SHARE_ACTIVE: {
2255
2656
  const sendStartedSharingRemote = () => {
2657
+ this.remoteShareInstanceId = contentShare.shareInstanceId;
2658
+
2256
2659
  Trigger.trigger(
2257
2660
  this,
2258
2661
  {
@@ -2263,7 +2666,7 @@ export default class Meeting extends StatelessWebexPlugin {
2263
2666
  {
2264
2667
  memberId: contentShare.beneficiaryId,
2265
2668
  url: contentShare.url,
2266
- shareInstanceId: contentShare.shareInstanceId,
2669
+ shareInstanceId: this.remoteShareInstanceId,
2267
2670
  annotationInfo: contentShare.annotation,
2268
2671
  }
2269
2672
  );
@@ -2300,6 +2703,7 @@ export default class Meeting extends StatelessWebexPlugin {
2300
2703
  name: 'client.share.floor-granted.local',
2301
2704
  payload: {
2302
2705
  mediaType: 'share',
2706
+ shareInstanceId: this.localShareInstanceId,
2303
2707
  },
2304
2708
  options: {meetingId: this.id},
2305
2709
  });
@@ -2342,6 +2746,8 @@ export default class Meeting extends StatelessWebexPlugin {
2342
2746
  } else if (newShareStatus === SHARE_STATUS.REMOTE_SHARE_ACTIVE) {
2343
2747
  // if we got here, then some remote participant has stolen
2344
2748
  // the presentation from another remote participant
2749
+ this.remoteShareInstanceId = contentShare.shareInstanceId;
2750
+
2345
2751
  Trigger.trigger(
2346
2752
  this,
2347
2753
  {
@@ -2352,7 +2758,7 @@ export default class Meeting extends StatelessWebexPlugin {
2352
2758
  {
2353
2759
  memberId: contentShare.beneficiaryId,
2354
2760
  url: contentShare.url,
2355
- shareInstanceId: contentShare.shareInstanceId,
2761
+ shareInstanceId: this.remoteShareInstanceId,
2356
2762
  annotationInfo: contentShare.annotation,
2357
2763
  }
2358
2764
  );
@@ -2404,6 +2810,7 @@ export default class Meeting extends StatelessWebexPlugin {
2404
2810
  this.locusId = this.locusUrl?.split('/').pop();
2405
2811
  this.recordingController.setLocusUrl(this.locusUrl);
2406
2812
  this.controlsOptionsManager.setLocusUrl(this.locusUrl);
2813
+ this.webinar.locusUrlUpdate(payload);
2407
2814
 
2408
2815
  Trigger.trigger(
2409
2816
  this,
@@ -2433,6 +2840,10 @@ export default class Meeting extends StatelessWebexPlugin {
2433
2840
  this.breakouts.breakoutServiceUrlUpdate(payload?.services?.breakout?.url);
2434
2841
  this.annotation.approvalUrlUpdate(payload?.services?.approval?.url);
2435
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
+ );
2436
2847
  });
2437
2848
  }
2438
2849
 
@@ -2473,12 +2884,24 @@ export default class Meeting extends StatelessWebexPlugin {
2473
2884
  );
2474
2885
  }
2475
2886
  });
2476
- this.locusInfo.on(LOCUSINFO.EVENTS.MEETING_INFO_UPDATED, () => {
2887
+ this.locusInfo.on(LOCUSINFO.EVENTS.MEETING_INFO_UPDATED, ({isInitializing}) => {
2477
2888
  this.updateMeetingActions();
2478
2889
  this.recordingController.setDisplayHints(this.userDisplayHints);
2479
2890
  this.recordingController.setUserPolicy(this.selfUserPolicies);
2480
2891
  this.controlsOptionsManager.setDisplayHints(this.userDisplayHints);
2481
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
+ }
2482
2905
  });
2483
2906
  }
2484
2907
 
@@ -2624,7 +3047,7 @@ export default class Meeting extends StatelessWebexPlugin {
2624
3047
  });
2625
3048
  }
2626
3049
  });
2627
- this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, (payload) => {
3050
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, async (payload) => {
2628
3051
  this.stopKeepAlive();
2629
3052
 
2630
3053
  if (payload) {
@@ -2723,6 +3146,7 @@ export default class Meeting extends StatelessWebexPlugin {
2723
3146
  this.simultaneousInterpretation.updateCanManageInterpreters(
2724
3147
  payload.newRoles?.includes(SELF_ROLES.MODERATOR)
2725
3148
  );
3149
+ this.webinar.updateCanManageWebcast(payload.newRoles?.includes(SELF_ROLES.MODERATOR));
2726
3150
  Trigger.trigger(
2727
3151
  this,
2728
3152
  {
@@ -2964,30 +3388,40 @@ export default class Meeting extends StatelessWebexPlugin {
2964
3388
  /**
2965
3389
  * Sets the meeting info on the class instance
2966
3390
  * @param {Object} meetingInfo
2967
- * @param {Object} meetingInfo.body
2968
- * @param {String} meetingInfo.body.conversationUrl
2969
- * @param {String} meetingInfo.body.locusUrl
2970
- * @param {String} meetingInfo.body.sipUri
2971
- * @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
2972
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
2973
3404
  * @returns {undefined}
2974
3405
  * @private
2975
3406
  * @memberof Meeting
2976
3407
  */
2977
3408
  parseMeetingInfo(
2978
- meetingInfo:
2979
- | {
2980
- body: {
2981
- conversationUrl: string;
2982
- locusUrl: string;
2983
- sipUri: string;
2984
- owner: object;
2985
- };
2986
- }
2987
- | any,
2988
- 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
2989
3424
  ) {
2990
- const webexMeetingInfo = meetingInfo?.body;
2991
3425
  // We try to use as much info from Locus meeting object, stored in destination
2992
3426
 
2993
3427
  let locusMeetingObject;
@@ -2997,40 +3431,31 @@ export default class Meeting extends StatelessWebexPlugin {
2997
3431
  }
2998
3432
 
2999
3433
  // MeetingInfo will be undefined for 1:1 calls
3000
- if (
3001
- locusMeetingObject ||
3002
- (webexMeetingInfo && !(meetingInfo?.errors && meetingInfo?.errors.length > 0))
3003
- ) {
3434
+ if (locusMeetingObject || (meetingInfo && !(errors?.length > 0))) {
3004
3435
  this.conversationUrl =
3005
- locusMeetingObject?.conversationUrl ||
3006
- webexMeetingInfo?.conversationUrl ||
3007
- this.conversationUrl;
3008
- this.locusUrl = locusMeetingObject?.url || webexMeetingInfo?.locusUrl || this.locusUrl;
3436
+ locusMeetingObject?.conversationUrl || meetingInfo?.conversationUrl || this.conversationUrl;
3437
+ this.locusUrl = locusMeetingObject?.url || meetingInfo?.locusUrl || this.locusUrl;
3009
3438
  // @ts-ignore - config coming from registerPlugin
3010
3439
  this.setSipUri(
3011
3440
  // @ts-ignore
3012
3441
  this.config.experimental.enableUnifiedMeetings
3013
- ? locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipUrl
3014
- : locusMeetingObject?.info.sipUri || webexMeetingInfo?.sipMeetingUri || this.sipUri
3442
+ ? locusMeetingObject?.info.sipUri || meetingInfo?.sipUrl
3443
+ : locusMeetingObject?.info.sipUri || meetingInfo?.sipMeetingUri || this.sipUri
3015
3444
  );
3016
3445
  // @ts-ignore - config coming from registerPlugin
3017
3446
  if (this.config.experimental.enableUnifiedMeetings) {
3018
- this.meetingNumber =
3019
- locusMeetingObject?.info.webExMeetingId || webexMeetingInfo?.meetingNumber;
3020
- this.meetingJoinUrl = webexMeetingInfo?.meetingJoinUrl;
3447
+ this.meetingNumber = locusMeetingObject?.info.webExMeetingId || meetingInfo?.meetingNumber;
3448
+ this.meetingJoinUrl = meetingInfo?.meetingJoinUrl;
3021
3449
  }
3022
3450
  this.owner =
3023
- locusMeetingObject?.info.owner ||
3024
- webexMeetingInfo?.owner ||
3025
- webexMeetingInfo?.hostId ||
3026
- this.owner;
3027
- this.permissionToken = webexMeetingInfo?.permissionToken;
3028
- this.setPermissionTokenPayload(webexMeetingInfo?.permissionToken);
3451
+ locusMeetingObject?.info.owner || meetingInfo?.owner || meetingInfo?.hostId || this.owner;
3452
+ this.permissionToken = meetingInfo?.permissionToken;
3453
+ this.setPermissionTokenPayload(meetingInfo?.permissionToken);
3029
3454
  this.setSelfUserPolicies();
3030
3455
  // Need to populate environment when sending CA event
3031
- this.environment = locusMeetingObject?.info.channel || webexMeetingInfo?.channel;
3456
+ this.environment = locusMeetingObject?.info.channel || meetingInfo?.channel;
3032
3457
  }
3033
- MeetingUtil.parseInterpretationInfo(this, webexMeetingInfo);
3458
+ MeetingUtil.parseInterpretationInfo(this, meetingInfo);
3034
3459
  }
3035
3460
 
3036
3461
  /**
@@ -3082,6 +3507,11 @@ export default class Meeting extends StatelessWebexPlugin {
3082
3507
  }) &&
3083
3508
  this.meetingInfo?.video?.supportHDV) ||
3084
3509
  !this.arePolicyRestrictionsSupported(),
3510
+ enforceVirtualBackground:
3511
+ ControlsOptionsUtil.hasPolicies({
3512
+ requiredPolicies: [SELF_POLICY.ENFORCE_VIRTUAL_BACKGROUND],
3513
+ policies: this.selfUserPolicies,
3514
+ }) && this.arePolicyRestrictionsSupported(),
3085
3515
  supportHQV:
3086
3516
  (ControlsOptionsUtil.hasPolicies({
3087
3517
  requiredPolicies: [SELF_POLICY.SUPPORT_HQV],
@@ -3235,6 +3665,10 @@ export default class Meeting extends StatelessWebexPlugin {
3235
3665
  requiredPolicies: [SELF_POLICY.SUPPORT_FILE_TRANSFER],
3236
3666
  policies: this.selfUserPolicies,
3237
3667
  }),
3668
+ canChat: ControlsOptionsUtil.hasPolicies({
3669
+ requiredPolicies: [SELF_POLICY.SUPPORT_CHAT],
3670
+ policies: this.selfUserPolicies,
3671
+ }),
3238
3672
  canShareApplication:
3239
3673
  (ControlsOptionsUtil.hasHints({
3240
3674
  requiredHints: [DISPLAY_HINTS.SHARE_APPLICATION],
@@ -3294,6 +3728,7 @@ export default class Meeting extends StatelessWebexPlugin {
3294
3728
  */
3295
3729
  setSelfUserPolicies() {
3296
3730
  this.selfUserPolicies = this.permissionTokenPayload?.permission?.userPolicies;
3731
+ this.enforceVBGImagesURL = this.permissionTokenPayload?.permission?.enforceVBGImagesURL;
3297
3732
  }
3298
3733
 
3299
3734
  /**
@@ -3303,7 +3738,8 @@ export default class Meeting extends StatelessWebexPlugin {
3303
3738
  * @returns {void}
3304
3739
  */
3305
3740
  public setPermissionTokenPayload(permissionToken: string) {
3306
- this.permissionTokenPayload = jwt.decode(permissionToken);
3741
+ this.permissionTokenPayload = jwtDecode(permissionToken);
3742
+ this.permissionTokenReceivedLocalTime = new Date().getTime();
3307
3743
  }
3308
3744
 
3309
3745
  /**
@@ -3397,8 +3833,7 @@ export default class Meeting extends StatelessWebexPlugin {
3397
3833
  * @memberof Meeting
3398
3834
  */
3399
3835
  closeRemoteStreams() {
3400
- const {remoteAudioStream, remoteVideoStream, remoteShareStream, shareAudioStream} =
3401
- this.mediaProperties;
3836
+ const {remoteAudioStream, remoteVideoStream, remoteShareStream} = this.mediaProperties;
3402
3837
 
3403
3838
  /**
3404
3839
  * Triggers an event to the developer
@@ -3439,7 +3874,6 @@ export default class Meeting extends StatelessWebexPlugin {
3439
3874
  stopStream(remoteAudioStream, EVENT_TYPES.REMOTE_AUDIO),
3440
3875
  stopStream(remoteVideoStream, EVENT_TYPES.REMOTE_VIDEO),
3441
3876
  stopStream(remoteShareStream, EVENT_TYPES.REMOTE_SHARE),
3442
- stopStream(shareAudioStream, EVENT_TYPES.REMOTE_SHARE_AUDIO),
3443
3877
  ]);
3444
3878
  }
3445
3879
 
@@ -3510,11 +3944,16 @@ export default class Meeting extends StatelessWebexPlugin {
3510
3944
  private async setLocalShareVideoStream(localDisplayStream?: LocalDisplayStream) {
3511
3945
  const oldStream = this.mediaProperties.shareVideoStream;
3512
3946
 
3947
+ oldStream?.off(StreamEventNames.MuteStateChange, this.handleShareVideoStreamMuteStateChange);
3513
3948
  oldStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3514
3949
  oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3515
3950
 
3516
3951
  this.mediaProperties.setLocalShareVideoStream(localDisplayStream);
3517
3952
 
3953
+ localDisplayStream?.on(
3954
+ StreamEventNames.MuteStateChange,
3955
+ this.handleShareVideoStreamMuteStateChange
3956
+ );
3518
3957
  localDisplayStream?.on(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3519
3958
  localDisplayStream?.on(
3520
3959
  LocalStreamEventNames.OutputTrackChange,
@@ -3570,7 +4009,7 @@ export default class Meeting extends StatelessWebexPlugin {
3570
4009
  functionName: string;
3571
4010
  isPublished: boolean;
3572
4011
  mediaType: MediaType;
3573
- stream: MediaStream;
4012
+ stream: LocalStream;
3574
4013
  }) {
3575
4014
  const {functionName, isPublished, mediaType, stream} = options;
3576
4015
  Trigger.trigger(
@@ -3604,12 +4043,16 @@ export default class Meeting extends StatelessWebexPlugin {
3604
4043
  videoStream?.off(StreamEventNames.MuteStateChange, this.localVideoStreamMuteStateHandler);
3605
4044
  videoStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
3606
4045
 
3607
- shareAudioStream?.off(StreamEventNames.MuteStateChange, this.handleShareAudioStreamEnded);
4046
+ shareAudioStream?.off(StreamEventNames.Ended, this.handleShareAudioStreamEnded);
3608
4047
  shareAudioStream?.off(
3609
4048
  LocalStreamEventNames.OutputTrackChange,
3610
4049
  this.localOutputTrackChangeHandler
3611
4050
  );
3612
- shareVideoStream?.off(StreamEventNames.MuteStateChange, this.handleShareVideoStreamEnded);
4051
+ shareVideoStream?.off(
4052
+ StreamEventNames.MuteStateChange,
4053
+ this.handleShareVideoStreamMuteStateChange
4054
+ );
4055
+ shareVideoStream?.off(StreamEventNames.Ended, this.handleShareVideoStreamEnded);
3613
4056
  shareVideoStream?.off(
3614
4057
  LocalStreamEventNames.OutputTrackChange,
3615
4058
  this.localOutputTrackChangeHandler
@@ -3723,6 +4166,7 @@ export default class Meeting extends StatelessWebexPlugin {
3723
4166
  this.receiveSlotManager.reset();
3724
4167
  this.mediaProperties.webrtcMediaConnection.close();
3725
4168
  this.sendSlotManager.reset();
4169
+ this.setNetworkStatus(undefined);
3726
4170
  }
3727
4171
 
3728
4172
  this.audio = null;
@@ -3744,18 +4188,31 @@ export default class Meeting extends StatelessWebexPlugin {
3744
4188
  if (this.config.reconnection.detection) {
3745
4189
  // @ts-ignore
3746
4190
  this.webex.internal.mercury.off(ONLINE);
4191
+ // @ts-ignore
4192
+ this.webex.internal.mercury.off(OFFLINE);
3747
4193
  }
3748
4194
  }
3749
4195
 
3750
4196
  /**
3751
- * Convenience method to set the correlation id for the Meeting
3752
- * @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
3753
4199
  * @returns {undefined}
3754
- * @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
3755
4212
  * @memberof Meeting
3756
4213
  */
3757
- private setCorrelationId(id: string) {
3758
- this.correlationId = id;
4214
+ public updateCallStateForMetrics(callStateForMetrics: CallStateForMetrics) {
4215
+ this.callStateForMetrics = {...this.callStateForMetrics, ...callStateForMetrics};
3759
4216
  }
3760
4217
 
3761
4218
  /**
@@ -3992,6 +4449,14 @@ export default class Meeting extends StatelessWebexPlugin {
3992
4449
  ) {
3993
4450
  const {mediaOptions, joinOptions} = options;
3994
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
+
3995
4460
  return this.join(joinOptions)
3996
4461
  .then((joinResponse) =>
3997
4462
  this.addMedia(mediaOptions).then((mediaResponse) => ({
@@ -4073,6 +4538,8 @@ export default class Meeting extends StatelessWebexPlugin {
4073
4538
 
4074
4539
  return this.reconnectionManager
4075
4540
  .reconnect(options)
4541
+ .then(() => this.waitForRemoteSDPAnswer())
4542
+ .then(() => this.waitForMediaConnectionConnected())
4076
4543
  .then(() => {
4077
4544
  Trigger.trigger(
4078
4545
  this,
@@ -4083,6 +4550,18 @@ export default class Meeting extends StatelessWebexPlugin {
4083
4550
  EVENT_TRIGGERS.MEETING_RECONNECTION_SUCCESS
4084
4551
  );
4085
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);
4086
4565
  })
4087
4566
  .catch((error) => {
4088
4567
  Trigger.trigger(
@@ -4129,7 +4608,7 @@ export default class Meeting extends StatelessWebexPlugin {
4129
4608
  }
4130
4609
 
4131
4610
  LoggerProxy.logger.error(
4132
- 'Meeting:index#isTranscriptionSupported --> Webex Assistant is not supported'
4611
+ 'Meeting:index#isTranscriptionSupported --> Webex Assistant is not enabled/supported'
4133
4612
  );
4134
4613
 
4135
4614
  return false;
@@ -4150,109 +4629,139 @@ export default class Meeting extends StatelessWebexPlugin {
4150
4629
  }
4151
4630
 
4152
4631
  /**
4153
- * Monitor the Low-Latency Mercury (LLM) web socket connection on `onError` and `onClose` states
4154
- * @private
4155
- * @returns {void}
4632
+ * sets Caption language for the meeting
4633
+ * @param {string} language
4634
+ * @returns {Promise}
4156
4635
  */
4157
- private monitorTranscriptionSocketConnection() {
4158
- this.transcription.onCloseSocket((event) => {
4159
- LoggerProxy.logger.info(
4160
- `Meeting:index#onCloseSocket -->
4161
- unable to continue receiving transcription;
4162
- low-latency mercury web socket connection is closed now.
4163
- ${event}`
4164
- );
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
+ );
4165
4642
 
4166
- this.triggerStopReceivingTranscriptionEvent();
4167
- });
4643
+ reject(new Error('Webex Assistant is not enabled/supported'));
4644
+ }
4168
4645
 
4169
- this.transcription.onErrorSocket((event) => {
4170
- LoggerProxy.logger.error(
4171
- `Meeting:index#onErrorSocket -->
4172
- unable to continue receiving transcription;
4173
- low-latency mercury web socket connection error had occured.
4174
- ${event}`
4175
- );
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;
4176
4654
 
4177
- 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}`);
4178
4674
 
4179
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.RECEIVE_TRANSCRIPTION_FAILURE, {
4180
- correlation_id: this.correlationId,
4181
- reason: 'unexpected error: transcription LLM web socket connection error had occured.',
4182
- event,
4183
- });
4675
+ reject(error);
4676
+ }
4184
4677
  });
4185
4678
  }
4186
4679
 
4187
4680
  /**
4188
- * Request for a WebSocket Url, open and monitor the WebSocket connection
4189
- * @private
4190
- * @returns {Promise<void>} a promise to open the WebSocket connection
4681
+ * sets Spoken language for the meeting
4682
+ * @param {string} language
4683
+ * @returns {Promise}
4191
4684
  */
4192
- private async receiveTranscription() {
4193
- LoggerProxy.logger.info(
4194
- `Meeting:index#receiveTranscription -->
4195
- Attempting to generate a web socket url.`
4196
- );
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
+ };
4197
4714
 
4198
- try {
4199
- const {datachannelUrl} = this.locusInfo.info;
4200
- // @ts-ignore - fix type
4201
- const {
4202
- body: {webSocketUrl},
4203
4715
  // @ts-ignore
4204
- } = await this.request({
4205
- method: HTTP_VERBS.POST,
4206
- uri: datachannelUrl,
4207
- body: {deviceUrl: this.deviceUrl},
4208
- });
4716
+ this.webex.internal.voicea.on(
4717
+ VOICEAEVENTS.SPOKEN_LANGUAGE_UPDATE,
4718
+ voiceaListenerLanguageUpdate
4719
+ );
4209
4720
 
4210
- LoggerProxy.logger.info(
4211
- `Meeting:index#receiveTranscription -->
4212
- Generated web socket url succesfully.`
4213
- );
4721
+ // @ts-ignore
4722
+ this.webex.internal.voicea.setSpokenLanguage(language);
4723
+ } catch (error) {
4724
+ LoggerProxy.logger.error(`Meeting:index#setSpokenLanguage --> ${error}`);
4214
4725
 
4215
- this.transcription = new Transcription(
4216
- webSocketUrl,
4217
- // @ts-ignore - fix type
4218
- this.webex.sessionId,
4219
- this.members
4220
- );
4726
+ reject(error);
4727
+ }
4728
+ });
4729
+ }
4221
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()) {
4222
4739
  LoggerProxy.logger.info(
4223
- `Meeting:index#receiveTranscription -->
4224
- opened LLM web socket connection successfully.`
4740
+ 'Meeting:index#startTranscription --> Attempting to enable transcription!'
4225
4741
  );
4226
4742
 
4227
- if (!this.inMeetingActions.isClosedCaptionActive) {
4228
- LoggerProxy.logger.error(
4229
- `Meeting:index#receiveTranscription --> Transcription cannot be started until a licensed user enables it`
4230
- );
4231
- }
4232
-
4233
- // retrieve and pass the payload
4234
- this.transcription.subscribe((payload) => {
4235
- Trigger.trigger(
4236
- this,
4237
- {
4238
- file: 'meeting/index',
4239
- function: 'join',
4240
- },
4241
- EVENT_TRIGGERS.MEETING_STARTED_RECEIVING_TRANSCRIPTION,
4242
- payload
4243
- );
4244
- });
4743
+ try {
4744
+ if (!this.areVoiceaEventsSetup) {
4745
+ this.setUpVoiceaListeners();
4746
+ }
4245
4747
 
4246
- this.monitorTranscriptionSocketConnection();
4247
- // @ts-ignore - fix type
4248
- this.transcription.connect(this.webex.credentials.supertoken.access_token);
4249
- } catch (error) {
4250
- LoggerProxy.logger.error(`Meeting:index#receiveTranscription --> ${error}`);
4251
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.RECEIVE_TRANSCRIPTION_FAILURE, {
4252
- correlation_id: this.correlationId,
4253
- reason: error.message,
4254
- stack: error.stack,
4255
- });
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');
4256
4765
  }
4257
4766
  }
4258
4767
 
@@ -4295,13 +4804,37 @@ export default class Meeting extends StatelessWebexPlugin {
4295
4804
  };
4296
4805
 
4297
4806
  /**
4298
- * stop recieving Transcription by closing
4299
- * the web socket connection properly
4807
+ * This method stops receiving transcription for the current meeting
4300
4808
  * @returns {void}
4301
4809
  */
4302
- stopReceivingTranscription() {
4810
+ stopTranscription() {
4303
4811
  if (this.transcription) {
4304
- 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();
4305
4838
  }
4306
4839
  }
4307
4840
 
@@ -4314,12 +4847,12 @@ export default class Meeting extends StatelessWebexPlugin {
4314
4847
  private triggerStopReceivingTranscriptionEvent() {
4315
4848
  LoggerProxy.logger.info(`
4316
4849
  Meeting:index#stopReceivingTranscription -->
4317
- closed transcription LLM web socket connection successfully.`);
4850
+ closed voicea event listeners successfully.`);
4318
4851
 
4319
4852
  Trigger.trigger(
4320
4853
  this,
4321
4854
  {
4322
- file: 'meeting',
4855
+ file: 'meeting/index',
4323
4856
  function: 'triggerStopReceivingTranscriptionEvent',
4324
4857
  },
4325
4858
  EVENT_TRIGGERS.MEETING_STOPPED_RECEIVING_TRANSCRIPTION
@@ -4338,7 +4871,7 @@ export default class Meeting extends StatelessWebexPlugin {
4338
4871
  * if joining as host on second loop, pass pin and pass moderator if joining as guest on second loop
4339
4872
  * Scenario D: Joining any other way (sip, pstn, conversationUrl, link just need to specify resourceId)
4340
4873
  */
4341
- public join(options: any = {}) {
4874
+ public async join(options: any = {}) {
4342
4875
  // @ts-ignore - fix type
4343
4876
  if (!this.webex.meetings.registered) {
4344
4877
  const errorMessage = 'Meeting:index#join --> Device not registered';
@@ -4392,27 +4925,14 @@ export default class Meeting extends StatelessWebexPlugin {
4392
4925
  // @ts-ignore
4393
4926
  this.webex.internal.newMetrics.submitClientEvent({
4394
4927
  name: 'client.call.initiated',
4395
- payload: {trigger: 'user-interaction', isRoapCallEnabled: true},
4928
+ payload: {
4929
+ trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
4930
+ isRoapCallEnabled: true,
4931
+ pstnAudioType: options?.pstnAudioType,
4932
+ },
4396
4933
  options: {meetingId: this.id},
4397
4934
  });
4398
4935
 
4399
- if (!isEmpty(this.meetingInfo)) {
4400
- // @ts-ignore
4401
- this.webex.internal.newMetrics.submitClientEvent({
4402
- name: 'client.meetinginfo.request',
4403
- options: {meetingId: this.id},
4404
- });
4405
-
4406
- // @ts-ignore
4407
- this.webex.internal.newMetrics.submitClientEvent({
4408
- name: 'client.meetinginfo.response',
4409
- payload: {
4410
- identifiers: {meetingLookupUrl: this.meetingInfo?.meetingLookupUrl},
4411
- },
4412
- options: {meetingId: this.id},
4413
- });
4414
- }
4415
-
4416
4936
  LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
4417
4937
 
4418
4938
  if (this.meetingFiniteStateMachine.state === MEETING_STATE_MACHINE.STATES.ENDED) {
@@ -4466,44 +4986,55 @@ export default class Meeting extends StatelessWebexPlugin {
4466
4986
 
4467
4987
  this.isMultistream = !!options.enableMultistream;
4468
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
+
4469
5026
  return MeetingUtil.joinMeetingOptions(this, options)
4470
5027
  .then((join) => {
4471
5028
  this.meetingFiniteStateMachine.join();
4472
5029
  LoggerProxy.logger.log('Meeting:index#join --> Success');
4473
5030
 
4474
- return join;
4475
- })
4476
- .then((join) => {
4477
- joinSuccess(join);
4478
- this.deferJoin = undefined;
4479
5031
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.JOIN_SUCCESS, {
4480
5032
  correlation_id: this.correlationId,
4481
5033
  });
4482
5034
 
4483
- return join;
4484
- })
4485
- .then(async (join) => {
4486
- // @ts-ignore - config coming from registerPlugin
4487
- if (this.config.enableAutomaticLLM) {
4488
- await this.updateLLMConnection();
4489
- }
5035
+ joinSuccess(join);
4490
5036
 
4491
- return join;
4492
- })
4493
- .then(async (join) => {
4494
- if (isBrowser) {
4495
- // @ts-ignore - config coming from registerPlugin
4496
- if (this.config.receiveTranscription || options.receiveTranscription) {
4497
- if (this.isTranscriptionSupported()) {
4498
- await this.receiveTranscription();
4499
- LoggerProxy.logger.info('Meeting:index#join --> enabled to recieve transcription!');
4500
- }
4501
- }
4502
- } else {
4503
- LoggerProxy.logger.error(
4504
- 'Meeting:index#join --> Receving transcription is not supported on this platform'
4505
- );
4506
- }
5037
+ this.deferJoin = undefined;
4507
5038
 
4508
5039
  return join;
4509
5040
  })
@@ -4539,9 +5070,44 @@ export default class Meeting extends StatelessWebexPlugin {
4539
5070
  );
4540
5071
 
4541
5072
  joinFailed(error);
5073
+
4542
5074
  this.deferJoin = undefined;
4543
5075
 
4544
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;
4545
5111
  });
4546
5112
  }
4547
5113
 
@@ -4921,30 +5487,91 @@ export default class Meeting extends StatelessWebexPlugin {
4921
5487
  }
4922
5488
  };
4923
5489
 
4924
- setupMediaConnectionListeners = () => {
4925
- this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_STARTED, () => {
4926
- this.isRoapInProgress = true;
4927
- });
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);
4928
5498
 
4929
- this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_DONE, () => {
4930
- this.mediaNegotiatedEvent();
4931
- this.isRoapInProgress = false;
4932
- this.processNextQueuedMediaUpdate();
4933
- });
5499
+ this.mediaProperties.webrtcMediaConnection.roapMessageReceived(roapMessage);
4934
5500
 
4935
- this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_FAILURE, this.handleRoapFailure);
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
+ });
5561
+
5562
+ this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_DONE, () => {
5563
+ this.mediaNegotiatedEvent();
5564
+ this.isRoapInProgress = false;
5565
+ this.processNextQueuedMediaUpdate();
5566
+ });
5567
+
5568
+ this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_FAILURE, this.handleRoapFailure);
4936
5569
 
4937
5570
  this.mediaProperties.webrtcMediaConnection.on(Event.ROAP_MESSAGE_TO_SEND, (event) => {
4938
5571
  const LOG_HEADER = `Meeting:index#setupMediaConnectionListeners.ROAP_MESSAGE_TO_SEND --> correlationId=${this.correlationId}`;
4939
5572
 
4940
5573
  switch (event.roapMessage.messageType) {
4941
5574
  case 'OK':
4942
- // @ts-ignore
4943
- this.webex.internal.newMetrics.submitClientEvent({
4944
- name: 'client.media-engine.remote-sdp-received',
4945
- options: {meetingId: this.id},
4946
- });
4947
-
4948
5575
  logRequest(
4949
5576
  this.roap.sendRoapOK({
4950
5577
  seq: event.roapMessage.seq,
@@ -4958,33 +5585,32 @@ export default class Meeting extends StatelessWebexPlugin {
4958
5585
  break;
4959
5586
 
4960
5587
  case 'OFFER':
4961
- // @ts-ignore
4962
- this.webex.internal.newMetrics.submitClientEvent({
4963
- name: 'client.media-engine.local-sdp-generated',
4964
- options: {meetingId: this.id},
4965
- });
4966
-
4967
5588
  logRequest(
4968
- this.roap.sendRoapMediaRequest({
4969
- sdp: event.roapMessage.sdp,
4970
- seq: event.roapMessage.seq,
4971
- tieBreaker: event.roapMessage.tieBreaker,
4972
- meeting: this, // or can pass meeting ID
4973
- reconnect: this.reconnectionManager.isReconnectInProgress(),
4974
- }),
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
+ }),
4975
5603
  {
4976
5604
  logText: `${LOG_HEADER} Roap Offer`,
4977
5605
  }
4978
- );
5606
+ ).catch(() => {
5607
+ this.deferSDPAnswer.reject();
5608
+ clearTimeout(this.sdpResponseTimer);
5609
+ this.sdpResponseTimer = undefined;
5610
+ });
4979
5611
  break;
4980
5612
 
4981
5613
  case 'ANSWER':
4982
- // @ts-ignore
4983
- this.webex.internal.newMetrics.submitClientEvent({
4984
- name: 'client.media-engine.remote-sdp-received',
4985
- options: {meetingId: this.id},
4986
- });
4987
-
4988
5614
  logRequest(
4989
5615
  this.roap.sendRoapAnswer({
4990
5616
  sdp: event.roapMessage.sdp,
@@ -5097,70 +5723,71 @@ export default class Meeting extends StatelessWebexPlugin {
5097
5723
 
5098
5724
  this.mediaProperties.webrtcMediaConnection.on(Event.CONNECTION_STATE_CHANGED, (event) => {
5099
5725
  const connectionFailed = () => {
5100
- // we know the media connection failed and browser will not attempt to recover it any more
5101
- // so reset the timer as it's not needed anymore, we want to reconnect immediately
5102
- this.reconnectionManager.resetReconnectionTimer();
5103
-
5104
- this.reconnect({networkDisconnect: true});
5105
- // @ts-ignore
5106
- this.webex.internal.newMetrics.submitClientEvent({
5107
- name: 'client.ice.end',
5108
- payload: {
5109
- canProceed: false,
5110
- icePhase: 'IN_MEETING',
5111
- errors: [
5112
- // @ts-ignore
5113
- this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
5114
- {
5115
- clientErrorCode: CALL_DIAGNOSTIC_CONFIG.ICE_FAILURE_CLIENT_CODE,
5116
- }
5117
- ),
5118
- ],
5119
- },
5120
- options: {
5121
- meetingId: this.id,
5122
- },
5123
- });
5124
-
5125
- this.uploadLogs({
5126
- file: 'peer-connection-manager/index',
5127
- function: 'connectionFailed',
5128
- });
5129
-
5130
5726
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_FAILURE, {
5131
5727
  correlation_id: this.correlationId,
5132
5728
  locus_id: this.locusId,
5729
+ networkStatus: this.networkStatus,
5730
+ hasMediaConnectionConnectedAtLeastOnce: this.hasMediaConnectionConnectedAtLeastOnce,
5133
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
+ }
5134
5745
  };
5135
5746
 
5136
5747
  LoggerProxy.logger.info(
5137
5748
  `Meeting:index#setupMediaConnectionListeners --> correlationId=${this.correlationId} connection state changed to ${event.state}`
5138
5749
  );
5750
+
5751
+ // @ts-ignore
5752
+ const cdl = this.webex.internal.newMetrics.callDiagnosticLatencies;
5753
+
5139
5754
  switch (event.state) {
5140
5755
  case ConnectionState.Connecting:
5141
- // @ts-ignore
5142
- this.webex.internal.newMetrics.submitClientEvent({
5143
- name: 'client.ice.start',
5144
- options: {
5145
- meetingId: this.id,
5146
- },
5147
- });
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
+ }
5148
5766
  break;
5149
5767
  case ConnectionState.Connected:
5150
- // @ts-ignore
5151
- this.webex.internal.newMetrics.submitClientEvent({
5152
- name: 'client.ice.end',
5153
- options: {
5154
- meetingId: this.id,
5155
- },
5156
- });
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
+ }
5157
5782
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.CONNECTION_SUCCESS, {
5158
5783
  correlation_id: this.correlationId,
5159
5784
  locus_id: this.locusId,
5785
+ latency: cdl.getICESetupTime(),
5160
5786
  });
5161
5787
  this.setNetworkStatus(NETWORK_STATUS.CONNECTED);
5162
5788
  this.reconnectionManager.iceReconnected();
5163
5789
  this.statsAnalyzer.startAnalyzer(this.mediaProperties.webrtcMediaConnection);
5790
+ this.hasMediaConnectionConnectedAtLeastOnce = true;
5164
5791
  break;
5165
5792
  case ConnectionState.Disconnected:
5166
5793
  this.setNetworkStatus(NETWORK_STATUS.DISCONNECTED);
@@ -5281,7 +5908,10 @@ export default class Meeting extends StatelessWebexPlugin {
5281
5908
  // @ts-ignore
5282
5909
  this.webex.internal.newMetrics.submitClientEvent({
5283
5910
  name: 'client.media.tx.start',
5284
- payload: {mediaType: data.type},
5911
+ payload: {
5912
+ mediaType: data.type,
5913
+ shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
5914
+ },
5285
5915
  options: {
5286
5916
  meetingId: this.id,
5287
5917
  },
@@ -5291,7 +5921,10 @@ export default class Meeting extends StatelessWebexPlugin {
5291
5921
  // @ts-ignore
5292
5922
  this.webex.internal.newMetrics.submitClientEvent({
5293
5923
  name: 'client.media.tx.stop',
5294
- payload: {mediaType: data.type},
5924
+ payload: {
5925
+ mediaType: data.type,
5926
+ shareInstanceId: data.type === 'share' ? this.localShareInstanceId : undefined,
5927
+ },
5295
5928
  options: {
5296
5929
  meetingId: this.id,
5297
5930
  },
@@ -5310,7 +5943,10 @@ export default class Meeting extends StatelessWebexPlugin {
5310
5943
  // @ts-ignore
5311
5944
  this.webex.internal.newMetrics.submitClientEvent({
5312
5945
  name: 'client.media.rx.start',
5313
- payload: {mediaType: data.type},
5946
+ payload: {
5947
+ mediaType: data.type,
5948
+ shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
5949
+ },
5314
5950
  options: {
5315
5951
  meetingId: this.id,
5316
5952
  },
@@ -5320,7 +5956,10 @@ export default class Meeting extends StatelessWebexPlugin {
5320
5956
  // @ts-ignore
5321
5957
  this.webex.internal.newMetrics.submitClientEvent({
5322
5958
  name: 'client.media.rx.stop',
5323
- payload: {mediaType: data.type},
5959
+ payload: {
5960
+ mediaType: data.type,
5961
+ shareInstanceId: data.type === 'share' ? this.remoteShareInstanceId : undefined,
5962
+ },
5324
5963
  options: {
5325
5964
  meetingId: this.id,
5326
5965
  },
@@ -5373,8 +6012,8 @@ export default class Meeting extends StatelessWebexPlugin {
5373
6012
  this.mediaProperties.mediaDirection.receiveShare,
5374
6013
  ];
5375
6014
 
5376
- this.sendSlotManager.createSlot(mc, MediaType.VideoMain, audioEnabled);
5377
- this.sendSlotManager.createSlot(mc, MediaType.AudioMain, videoEnabled);
6015
+ this.sendSlotManager.createSlot(mc, MediaType.VideoMain, videoEnabled);
6016
+ this.sendSlotManager.createSlot(mc, MediaType.AudioMain, audioEnabled);
5378
6017
  this.sendSlotManager.createSlot(mc, MediaType.VideoSlides, shareEnabled);
5379
6018
  this.sendSlotManager.createSlot(mc, MediaType.AudioSlides, shareEnabled);
5380
6019
  }
@@ -5393,54 +6032,468 @@ export default class Meeting extends StatelessWebexPlugin {
5393
6032
  await this.publishStream(MediaType.AudioSlides, this.mediaProperties.shareAudioStream);
5394
6033
  }
5395
6034
 
5396
- return mc;
6035
+ return mc;
6036
+ }
6037
+
6038
+ /**
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
+ }
5397
6445
  }
5398
6446
 
5399
6447
  /**
5400
- * Listens for an event emitted by eventEmitter and emits it from the meeting object
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
5401
6450
  *
5402
6451
  * @private
5403
- * @param {*} eventEmitter object from which to forward the event
5404
- * @param {*} eventTypeToForward which event type to listen on and to forward
5405
- * @param {string} meetingEventType event type to be used in the event emitted from the meeting object
5406
- * @returns {void}
6452
+ * @returns {Promise<void>}
5407
6453
  */
5408
- forwardEvent(eventEmitter, eventTypeToForward, meetingEventType) {
5409
- eventEmitter.on(eventTypeToForward, (data) =>
5410
- Trigger.trigger(
5411
- this,
5412
- {
5413
- file: 'meetings',
5414
- function: 'addMedia',
5415
- },
5416
- meetingEventType,
5417
- data
5418
- )
5419
- );
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
+ }
5420
6475
  }
5421
6476
 
5422
6477
  /**
5423
6478
  * Creates a media connection to the server. Media connection is required for sending or receiving any audio/video.
5424
6479
  *
5425
6480
  * @param {AddMediaOptions} options
5426
- * @returns {Promise}
6481
+ * @returns {Promise<void>}
5427
6482
  * @public
5428
6483
  * @memberof Meeting
5429
6484
  */
5430
- addMedia(options: AddMediaOptions = {}) {
6485
+ async addMedia(options: AddMediaOptions = {}): Promise<void> {
6486
+ this.retriedWithTurnServer = false;
6487
+ this.hasMediaConnectionConnectedAtLeastOnce = false;
5431
6488
  const LOG_HEADER = 'Meeting:index#addMedia -->';
5432
-
5433
- let turnDiscoverySkippedReason;
5434
- let turnServerUsed = false;
5435
-
5436
6489
  LoggerProxy.logger.info(`${LOG_HEADER} called with: ${JSON.stringify(options)}`);
5437
6490
 
5438
- if (this.meetingState !== FULL_STATE.ACTIVE) {
5439
- return Promise.reject(new MeetingNotActiveError());
6491
+ if (options.allowMediaInLobby !== true && this.meetingState !== FULL_STATE.ACTIVE) {
6492
+ throw new MeetingNotActiveError();
5440
6493
  }
5441
6494
 
5442
6495
  if (MeetingUtil.isUserInLeftState(this.locusInfo)) {
5443
- return Promise.reject(new UserNotJoinedError());
6496
+ throw new UserNotJoinedError();
5444
6497
  }
5445
6498
 
5446
6499
  const {
@@ -5459,7 +6512,7 @@ export default class Meeting extends StatelessWebexPlugin {
5459
6512
  // If the user is unjoined or guest waiting in lobby dont allow the user to addMedia
5460
6513
  // @ts-ignore - isUserUnadmitted coming from SelfUtil
5461
6514
  if (this.isUserUnadmitted && !this.wirelessShare && !allowMediaInLobby) {
5462
- return Promise.reject(new UserInLobbyError());
6515
+ throw new UserInLobbyError();
5463
6516
  }
5464
6517
 
5465
6518
  // @ts-ignore
@@ -5519,240 +6572,100 @@ export default class Meeting extends StatelessWebexPlugin {
5519
6572
 
5520
6573
  this.audio = createMuteState(AUDIO, this, audioEnabled);
5521
6574
  this.video = createMuteState(VIDEO, this, videoEnabled);
5522
- const promises = [];
5523
-
5524
- // setup all the references to local streams in this.mediaProperties before creating media connection
5525
- // and before TURN discovery, so that the correct mute state is sent with TURN discovery roap messages
5526
- if (localStreams?.microphone) {
5527
- promises.push(this.setLocalAudioStream(localStreams.microphone));
5528
- }
5529
- if (localStreams?.camera) {
5530
- promises.push(this.setLocalVideoStream(localStreams.camera));
5531
- }
5532
- if (localStreams?.screenShare?.video) {
5533
- promises.push(this.setLocalShareVideoStream(localStreams.screenShare.video));
5534
- }
5535
- if (localStreams?.screenShare?.audio) {
5536
- promises.push(this.setLocalShareAudioStream(localStreams.screenShare.audio));
5537
- }
5538
-
5539
- return Promise.all(promises)
5540
- .then(() => this.roap.doTurnDiscovery(this, false))
5541
- .then(async (turnDiscoveryObject) => {
5542
- ({turnDiscoverySkippedReason} = turnDiscoveryObject);
5543
- turnServerUsed = !turnDiscoverySkippedReason;
5544
6575
 
5545
- const {turnServerInfo} = turnDiscoveryObject;
5546
-
5547
- const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
5548
-
5549
- if (this.isMultistream) {
5550
- this.remoteMediaManager = new RemoteMediaManager(
5551
- this.receiveSlotManager,
5552
- this.mediaRequestManagers,
5553
- remoteMediaManagerConfig
5554
- );
5555
-
5556
- this.forwardEvent(
5557
- this.remoteMediaManager,
5558
- RemoteMediaManagerEvent.AudioCreated,
5559
- EVENT_TRIGGERS.REMOTE_MEDIA_AUDIO_CREATED
5560
- );
5561
- this.forwardEvent(
5562
- this.remoteMediaManager,
5563
- RemoteMediaManagerEvent.ScreenShareAudioCreated,
5564
- EVENT_TRIGGERS.REMOTE_MEDIA_SCREEN_SHARE_AUDIO_CREATED
5565
- );
5566
- this.forwardEvent(
5567
- this.remoteMediaManager,
5568
- RemoteMediaManagerEvent.VideoLayoutChanged,
5569
- EVENT_TRIGGERS.REMOTE_MEDIA_VIDEO_LAYOUT_CHANGED
5570
- );
6576
+ try {
6577
+ await this.setUpLocalStreamReferences(localStreams);
5571
6578
 
5572
- await this.remoteMediaManager.start();
5573
- }
6579
+ this.setMercuryListener();
5574
6580
 
5575
- await mc.initiateOffer();
5576
- })
5577
- .then(() => {
5578
- this.setMercuryListener();
5579
- })
5580
- .then(
5581
- () =>
5582
- getDevices()
5583
- .then((devices) => {
5584
- MeetingUtil.handleDeviceLogging(devices);
5585
- })
5586
- .catch(() => {}) // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
5587
- )
5588
- .then(() => {
5589
- this.handleMediaLogging(this.mediaProperties);
5590
- LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
6581
+ this.createStatsAnalyzer();
5591
6582
 
5592
- // @ts-ignore - config coming from registerPlugin
5593
- if (this.config.stats.enableStatsAnalyzer) {
5594
- // @ts-ignore - config coming from registerPlugin
5595
- this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
5596
- this.statsAnalyzer = new StatsAnalyzer(
5597
- // @ts-ignore - config coming from registerPlugin
5598
- this.config.stats,
5599
- (ssrc: number) => this.receiveSlotManager.findReceiveSlotBySsrc(ssrc),
5600
- this.networkQualityMonitor
5601
- );
5602
- this.setupStatsAnalyzerEventHandlers();
5603
- this.networkQualityMonitor.on(
5604
- EVENT_TRIGGERS.NETWORK_QUALITY,
5605
- this.sendNetworkQualityEvent.bind(this)
5606
- );
5607
- }
5608
- })
5609
- .catch((error) => {
5610
- LoggerProxy.logger.error(
5611
- `${LOG_HEADER} Error adding media , setting up peerconnection, `,
5612
- error
5613
- );
6583
+ await this.establishMediaConnection(remoteMediaManagerConfig, bundlePolicy, false);
5614
6584
 
5615
- throw error;
5616
- })
5617
- .then(
5618
- () =>
5619
- new Promise<void>((resolve, reject) => {
5620
- let timerCount = 0;
5621
-
5622
- // eslint-disable-next-line func-names
5623
- // eslint-disable-next-line prefer-arrow-callback
5624
- if (this.type === _CALL_ || this.meetingState === FULL_STATE.ACTIVE) {
5625
- resolve();
5626
- }
5627
- const joiningTimer = setInterval(() => {
5628
- timerCount += 1;
5629
- if (this.meetingState === FULL_STATE.ACTIVE) {
5630
- clearInterval(joiningTimer);
5631
- resolve();
5632
- }
6585
+ await Meeting.handleDeviceLogging();
5633
6586
 
5634
- if (timerCount === 4) {
5635
- clearInterval(joiningTimer);
5636
- reject(new Error('Meeting is still not active '));
5637
- }
5638
- }, 1000);
5639
- })
5640
- )
5641
- .then(() =>
5642
- this.mediaProperties.waitForMediaConnectionConnected().catch(() => {
5643
- // @ts-ignore
5644
- this.webex.internal.newMetrics.submitClientEvent({
5645
- name: 'client.ice.end',
5646
- payload: {
5647
- canProceed: false,
5648
- icePhase: 'JOIN_MEETING_FINAL',
5649
- errors: [
5650
- // @ts-ignore
5651
- this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
5652
- {
5653
- clientErrorCode: CALL_DIAGNOSTIC_CONFIG.ICE_FAILURE_CLIENT_CODE,
5654
- }
5655
- ),
5656
- ],
5657
- },
5658
- options: {
5659
- meetingId: this.id,
5660
- },
5661
- });
5662
- throw new Error(
5663
- `Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
5664
- );
5665
- })
5666
- )
5667
- .then(() => {
5668
- if (this.mediaProperties.hasLocalShareStream()) {
5669
- return this.enqueueScreenShareFloorRequest();
5670
- }
6587
+ if (this.mediaProperties.hasLocalShareStream()) {
6588
+ await this.enqueueScreenShareFloorRequest();
6589
+ }
5671
6590
 
5672
- return Promise.resolve();
5673
- })
5674
- .then(() => this.mediaProperties.getCurrentConnectionType())
5675
- .then((connectionType) => {
5676
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, {
5677
- correlation_id: this.correlationId,
5678
- locus_id: this.locusUrl.split('/').pop(),
5679
- connectionType,
5680
- isMultistream: this.isMultistream,
5681
- });
5682
- // @ts-ignore
5683
- this.webex.internal.newMetrics.submitClientEvent({
5684
- name: 'client.media-engine.ready',
5685
- options: {
5686
- meetingId: this.id,
5687
- },
5688
- });
5689
- LoggerProxy.logger.info(
5690
- `${LOG_HEADER} successfully established media connection, type=${connectionType}`
5691
- );
6591
+ const connectionType = await this.mediaProperties.getCurrentConnectionType();
6592
+ // @ts-ignore
6593
+ const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics();
5692
6594
 
5693
- // We can log ReceiveSlot SSRCs only after the SDP exchange, so doing it here:
5694
- this.remoteMediaManager?.logAllReceiveSlots();
5695
- })
5696
- .catch((error) => {
5697
- LoggerProxy.logger.error(`${LOG_HEADER} failed to establish media connection: `, error);
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
+ );
5698
6613
 
5699
- Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
5700
- correlation_id: this.correlationId,
5701
- locus_id: this.locusUrl.split('/').pop(),
5702
- reason: error.message,
5703
- stack: error.stack,
5704
- code: error.code,
5705
- turnDiscoverySkippedReason,
5706
- turnServerUsed,
5707
- isMultistream: this.isMultistream,
5708
- signalingState:
5709
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5710
- ?.signalingState ||
5711
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
5712
- 'unknown',
5713
- connectionState:
5714
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5715
- ?.connectionState ||
5716
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.connectionState ||
5717
- 'unknown',
5718
- iceConnectionState:
5719
- this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
5720
- ?.iceConnectionState ||
5721
- this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
5722
- 'unknown',
5723
- });
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);
5724
6618
 
5725
- // Clean up stats analyzer, peer connection, and turn off listeners
5726
- const stopStatsAnalyzer = this.statsAnalyzer
5727
- ? this.statsAnalyzer.stopAnalyzer()
5728
- : Promise.resolve();
6619
+ // @ts-ignore
6620
+ const reachabilityMetrics = await this.webex.meetings.reachability.getReachabilityMetrics();
5729
6621
 
5730
- return stopStatsAnalyzer.then(() => {
5731
- this.statsAnalyzer = null;
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
+ });
5732
6649
 
5733
- if (this.mediaProperties.webrtcMediaConnection) {
5734
- this.closePeerConnections();
5735
- this.unsetPeerConnections();
5736
- }
6650
+ await this.cleanUpOnAddMediaFailure();
5737
6651
 
5738
- // Upload logs on error while adding media
5739
- Trigger.trigger(
5740
- this,
5741
- {
5742
- file: 'meeting/index',
5743
- function: 'addMedia',
5744
- },
5745
- EVENTS.REQUEST_UPLOAD_LOGS,
5746
- this
5747
- );
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
+ );
5748
6662
 
5749
- if (error instanceof Errors.SdpError) {
5750
- this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
5751
- }
6663
+ if (error instanceof Errors.SdpError) {
6664
+ this.leave({reason: MEETING_REMOVED_REASON.MEETING_CONNECTION_FAILED});
6665
+ }
5752
6666
 
5753
- throw error;
5754
- });
5755
- });
6667
+ throw error;
6668
+ }
5756
6669
  }
5757
6670
 
5758
6671
  /**
@@ -5766,6 +6679,24 @@ export default class Meeting extends StatelessWebexPlugin {
5766
6679
  return !this.isRoapInProgress;
5767
6680
  }
5768
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
+
5769
6700
  /**
5770
6701
  * Enqueues a media update operation.
5771
6702
  * @param {String} mediaUpdateType one of MEDIA_UPDATE_TYPE values
@@ -6231,17 +7162,13 @@ export default class Meeting extends StatelessWebexPlugin {
6231
7162
  .catch((error) => {
6232
7163
  LoggerProxy.logger.error('Meeting:index#stopWhiteboardShare --> Error ', error);
6233
7164
 
6234
- Metrics.sendBehavioralMetric(
6235
- // @ts-ignore - check if STOP_WHITEBOARD_SHARE_FAILURE exists
6236
- BEHAVIORAL_METRICS.STOP_WHITEBOARD_SHARE_FAILURE,
6237
- {
6238
- correlation_id: this.correlationId,
6239
- locus_id: this.locusUrl.split('/').pop(),
6240
- reason: error.message,
6241
- stack: error.stack,
6242
- board: {channelUrl},
6243
- }
6244
- );
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
+ });
6245
7172
 
6246
7173
  return Promise.reject(error);
6247
7174
  })
@@ -6278,11 +7205,14 @@ export default class Meeting extends StatelessWebexPlugin {
6278
7205
  if (content && this.shareStatus !== SHARE_STATUS.LOCAL_SHARE_ACTIVE) {
6279
7206
  // @ts-ignore
6280
7207
  this.webex.internal.newMetrics.submitClientEvent({
6281
- name: 'client.share.initiated',
7208
+ name: 'client.share.floor-grant.request',
6282
7209
  payload: {
6283
7210
  mediaType: 'share',
7211
+ shareInstanceId: this.localShareInstanceId,
7212
+ },
7213
+ options: {
7214
+ meetingId: this.id,
6284
7215
  },
6285
- options: {meetingId: this.id},
6286
7216
  });
6287
7217
 
6288
7218
  return this.meetingRequest
@@ -6292,10 +7222,16 @@ export default class Meeting extends StatelessWebexPlugin {
6292
7222
  deviceUrl: this.deviceUrl,
6293
7223
  uri: content.url,
6294
7224
  resourceUrl: this.resourceUrl,
7225
+ shareInstanceId: this.localShareInstanceId,
6295
7226
  })
6296
7227
  .then(() => {
6297
7228
  this.screenShareFloorState = ScreenShareFloorStatus.GRANTED;
6298
7229
 
7230
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEETING_SHARE_SUCCESS, {
7231
+ correlation_id: this.correlationId,
7232
+ locus_id: this.locusUrl.split('/').pop(),
7233
+ });
7234
+
6299
7235
  return Promise.resolve();
6300
7236
  })
6301
7237
  .catch((error) => {
@@ -6308,6 +7244,19 @@ export default class Meeting extends StatelessWebexPlugin {
6308
7244
  stack: error.stack,
6309
7245
  });
6310
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
+
6311
7260
  this.screenShareFloorState = ScreenShareFloorStatus.RELEASED;
6312
7261
 
6313
7262
  return Promise.reject(error);
@@ -6359,6 +7308,7 @@ export default class Meeting extends StatelessWebexPlugin {
6359
7308
  name: 'client.share.stopped',
6360
7309
  payload: {
6361
7310
  mediaType: 'share',
7311
+ shareInstanceId: this.localShareInstanceId,
6362
7312
  },
6363
7313
  options: {meetingId: this.id},
6364
7314
  });
@@ -6375,6 +7325,7 @@ export default class Meeting extends StatelessWebexPlugin {
6375
7325
  deviceUrl: this.deviceUrl,
6376
7326
  uri: content.url,
6377
7327
  resourceUrl: this.resourceUrl,
7328
+ shareInstanceId: this.localShareInstanceId,
6378
7329
  })
6379
7330
  .catch((error) => {
6380
7331
  LoggerProxy.logger.error('Meeting:index#releaseScreenShareFloor --> Error ', error);
@@ -6578,8 +7529,8 @@ export default class Meeting extends StatelessWebexPlugin {
6578
7529
 
6579
7530
  if (layoutType) {
6580
7531
  if (!LAYOUT_TYPES.includes(layoutType)) {
6581
- this.rejectWithErrorLog(
6582
- '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.'
6583
7534
  );
6584
7535
  }
6585
7536
 
@@ -6717,6 +7668,23 @@ export default class Meeting extends StatelessWebexPlugin {
6717
7668
  }
6718
7669
  };
6719
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
+
6720
7688
  /**
6721
7689
  * Functionality for when a share video is ended.
6722
7690
  * @private
@@ -6817,6 +7785,9 @@ export default class Meeting extends StatelessWebexPlugin {
6817
7785
  if (roles.includes(SELF_ROLES.COHOST)) {
6818
7786
  return 'cohost';
6819
7787
  }
7788
+ if (roles.includes(SELF_ROLES.PRESENTER)) {
7789
+ return 'presenter';
7790
+ }
6820
7791
  if (roles.includes(SELF_ROLES.ATTENDEE)) {
6821
7792
  return 'attendee';
6822
7793
  }
@@ -6907,8 +7878,7 @@ export default class Meeting extends StatelessWebexPlugin {
6907
7878
  this.queuedMediaUpdates = [];
6908
7879
 
6909
7880
  if (this.transcription) {
6910
- this.transcription.closeSocket();
6911
- this.triggerStopReceivingTranscriptionEvent();
7881
+ this.stopTranscription();
6912
7882
  this.transcription = undefined;
6913
7883
  }
6914
7884
  };
@@ -7083,10 +8053,12 @@ export default class Meeting extends StatelessWebexPlugin {
7083
8053
  .update({
7084
8054
  // TODO: RoapMediaConnection is not ready to use stream classes yet, so we pass the raw MediaStreamTrack for now
7085
8055
  localTracks: {
7086
- audio: this.mediaProperties.audioStream?.outputTrack || null,
7087
- video: this.mediaProperties.videoStream?.outputTrack || null,
7088
- screenShareVideo: this.mediaProperties.shareVideoStream?.outputTrack || null,
7089
- 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,
7090
8062
  },
7091
8063
  direction: {
7092
8064
  audio: Media.getDirection(
@@ -7220,6 +8192,23 @@ export default class Meeting extends StatelessWebexPlugin {
7220
8192
  }
7221
8193
 
7222
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
+ });
7223
8212
  // we're sending the http request to Locus to request the screen share floor
7224
8213
  // only after the SDP update, because that's how it's always been done for transcoded meetings
7225
8214
  // and also if sharing from the start, we need confluence to have been created
@@ -7268,6 +8257,12 @@ export default class Meeting extends StatelessWebexPlugin {
7268
8257
  if (!this.mediaProperties.hasLocalShareStream()) {
7269
8258
  try {
7270
8259
  this.releaseScreenShareFloor(); // we ignore the returned promise here on purpose
8260
+
8261
+ this.statsAnalyzer.updateMediaStatus({
8262
+ expected: {
8263
+ sendShare: false,
8264
+ },
8265
+ });
7271
8266
  } catch (e) {
7272
8267
  // nothing to do here, error is logged already inside releaseScreenShareFloor()
7273
8268
  }
@@ -7275,24 +8270,51 @@ export default class Meeting extends StatelessWebexPlugin {
7275
8270
  }
7276
8271
 
7277
8272
  /**
7278
- * Gets the time left in seconds till the permission token expires
8273
+ * Gets permission token expiry information including timeLeft, expiryTime, currentTime
7279
8274
  * (from the time the function has been fired)
7280
8275
  *
7281
- * @returns {number} time left in seconds
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
7282
8280
  */
7283
- public getPermissionTokenTimeLeftInSec(): number | undefined {
8281
+ public getPermissionTokenExpiryInfo() {
7284
8282
  if (!this.permissionTokenPayload) {
7285
8283
  return undefined;
7286
8284
  }
7287
8285
 
7288
- const permissionTokenExpValue = Number(this.permissionTokenPayload.exp);
8286
+ const permissionTokenExpiryFromServer = Number(this.permissionTokenPayload.exp);
8287
+ const permissionTokenIssuedTimeFromServer = Number(this.permissionTokenPayload.iat);
8288
+
8289
+ const shiftInTime = this.permissionTokenReceivedLocalTime - permissionTokenIssuedTimeFromServer;
7289
8290
 
7290
8291
  // using new Date instead of Date.now() to allow for accurate unit testing
7291
8292
  // https://github.com/sinonjs/fake-timers/issues/321
7292
- const now = new Date().getTime();
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
+ }
7293
8302
 
7294
- // substract current time from the permissionTokenExp
7295
- // (permissionTokenExp is a epoch timestamp, not a time to live duration)
7296
- return (permissionTokenExpValue - now) / 1000;
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();
7297
8319
  }
7298
8320
  }