@webex/plugin-meetings 3.7.0 → 3.8.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 (351) hide show
  1. package/dist/annotation/annotation.types.d.ts +42 -0
  2. package/dist/annotation/constants.d.ts +31 -0
  3. package/dist/annotation/index.d.ts +117 -0
  4. package/dist/annotation/index.js +17 -0
  5. package/dist/annotation/index.js.map +1 -1
  6. package/dist/breakouts/breakout.d.ts +8 -0
  7. package/dist/breakouts/breakout.js +1 -1
  8. package/dist/breakouts/collection.d.ts +5 -0
  9. package/dist/breakouts/edit-lock-error.d.ts +15 -0
  10. package/dist/breakouts/events.d.ts +8 -0
  11. package/dist/breakouts/index.d.ts +5 -0
  12. package/dist/breakouts/index.js +1 -1
  13. package/dist/breakouts/request.d.ts +22 -0
  14. package/dist/breakouts/utils.d.ts +15 -0
  15. package/dist/common/browser-detection.d.ts +9 -0
  16. package/dist/common/collection.d.ts +48 -0
  17. package/dist/common/config.d.ts +2 -0
  18. package/dist/common/errors/captcha-error.d.ts +15 -0
  19. package/dist/common/errors/intent-to-join.d.ts +16 -0
  20. package/dist/common/errors/join-forbidden-error.js +52 -0
  21. package/dist/common/errors/join-forbidden-error.js.map +1 -0
  22. package/dist/common/errors/join-meeting.d.ts +17 -0
  23. package/dist/common/errors/{webinar-registration-error.js → join-webinar-error.js} +12 -12
  24. package/dist/common/errors/join-webinar-error.js.map +1 -0
  25. package/dist/common/errors/media.d.ts +15 -0
  26. package/dist/common/errors/multistream-not-supported-error.js +53 -0
  27. package/dist/common/errors/multistream-not-supported-error.js.map +1 -0
  28. package/dist/common/errors/no-meeting-info.d.ts +14 -0
  29. package/dist/common/errors/parameter.d.ts +15 -0
  30. package/dist/common/errors/password-error.d.ts +15 -0
  31. package/dist/common/errors/permission.d.ts +14 -0
  32. package/dist/common/errors/reclaim-host-role-error.js +149 -0
  33. package/dist/common/errors/reclaim-host-role-error.js.map +1 -0
  34. package/dist/common/errors/reclaim-host-role-errors.d.ts +60 -0
  35. package/dist/common/errors/reconnection-in-progress.d.ts +9 -0
  36. package/dist/common/errors/reconnection-in-progress.js +33 -0
  37. package/dist/common/errors/reconnection-in-progress.js.map +1 -0
  38. package/dist/common/errors/reconnection.d.ts +15 -0
  39. package/dist/common/errors/stats.d.ts +15 -0
  40. package/dist/common/errors/webex-errors.d.ts +93 -0
  41. package/dist/common/errors/webex-meetings-error.d.ts +20 -0
  42. package/dist/common/events/events-scope.d.ts +17 -0
  43. package/dist/common/events/events.d.ts +12 -0
  44. package/dist/common/events/trigger-proxy.d.ts +2 -0
  45. package/dist/common/events/util.d.ts +2 -0
  46. package/dist/common/logs/logger-config.d.ts +2 -0
  47. package/dist/common/logs/logger-proxy.d.ts +2 -0
  48. package/dist/common/logs/request.d.ts +36 -0
  49. package/dist/common/queue.d.ts +34 -0
  50. package/dist/config.d.ts +72 -0
  51. package/dist/config.js +2 -1
  52. package/dist/config.js.map +1 -1
  53. package/dist/constants.d.ts +1088 -0
  54. package/dist/constants.js +68 -6
  55. package/dist/constants.js.map +1 -1
  56. package/dist/controls-options-manager/constants.d.ts +4 -0
  57. package/dist/controls-options-manager/enums.d.ts +15 -0
  58. package/dist/controls-options-manager/index.d.ts +136 -0
  59. package/dist/controls-options-manager/types.d.ts +43 -0
  60. package/dist/controls-options-manager/util.d.ts +1 -0
  61. package/dist/index.d.ts +7 -0
  62. package/dist/index.js +16 -11
  63. package/dist/index.js.map +1 -1
  64. package/dist/interceptors/index.d.ts +2 -0
  65. package/dist/interceptors/locusRetry.d.ts +27 -0
  66. package/dist/interpretation/collection.d.ts +5 -0
  67. package/dist/interpretation/index.d.ts +5 -0
  68. package/dist/interpretation/index.js +1 -1
  69. package/dist/interpretation/siLanguage.d.ts +5 -0
  70. package/dist/interpretation/siLanguage.js +1 -1
  71. package/dist/locus-info/controlsUtils.d.ts +2 -0
  72. package/dist/locus-info/embeddedAppsUtils.d.ts +2 -0
  73. package/dist/locus-info/fullState.d.ts +2 -0
  74. package/dist/locus-info/hostUtils.d.ts +2 -0
  75. package/dist/locus-info/index.d.ts +322 -0
  76. package/dist/locus-info/index.js +14 -3
  77. package/dist/locus-info/index.js.map +1 -1
  78. package/dist/locus-info/infoUtils.d.ts +2 -0
  79. package/dist/locus-info/mediaSharesUtils.d.ts +2 -0
  80. package/dist/locus-info/parser.d.ts +272 -0
  81. package/dist/locus-info/selfUtils.d.ts +2 -0
  82. package/dist/locus-info/selfUtils.js +35 -17
  83. package/dist/locus-info/selfUtils.js.map +1 -1
  84. package/dist/media/MediaConnectionAwaiter.js +1 -0
  85. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  86. package/dist/media/index.d.ts +34 -0
  87. package/dist/media/properties.d.ts +93 -0
  88. package/dist/media/properties.js +30 -16
  89. package/dist/media/properties.js.map +1 -1
  90. package/dist/media/util.d.ts +2 -0
  91. package/dist/mediaQualityMetrics/config.d.ts +241 -0
  92. package/dist/mediaQualityMetrics/config.js +502 -0
  93. package/dist/mediaQualityMetrics/config.js.map +1 -0
  94. package/dist/meeting/brbState.js +167 -0
  95. package/dist/meeting/brbState.js.map +1 -0
  96. package/dist/meeting/effectsState.js +260 -0
  97. package/dist/meeting/effectsState.js.map +1 -0
  98. package/dist/meeting/in-meeting-actions.d.ts +167 -0
  99. package/dist/meeting/in-meeting-actions.js +13 -1
  100. package/dist/meeting/in-meeting-actions.js.map +1 -1
  101. package/dist/meeting/index.d.ts +1825 -0
  102. package/dist/meeting/index.js +1331 -1051
  103. package/dist/meeting/index.js.map +1 -1
  104. package/dist/meeting/locusMediaRequest.d.ts +74 -0
  105. package/dist/meeting/locusMediaRequest.js +11 -6
  106. package/dist/meeting/locusMediaRequest.js.map +1 -1
  107. package/dist/meeting/muteState.d.ts +178 -0
  108. package/dist/meeting/muteState.js +1 -6
  109. package/dist/meeting/muteState.js.map +1 -1
  110. package/dist/meeting/request.d.ts +295 -0
  111. package/dist/meeting/request.js +51 -29
  112. package/dist/meeting/request.js.map +1 -1
  113. package/dist/meeting/request.type.d.ts +11 -0
  114. package/dist/meeting/request.type.js.map +1 -1
  115. package/dist/meeting/state.d.ts +9 -0
  116. package/dist/meeting/util.d.ts +119 -0
  117. package/dist/meeting/util.js +103 -67
  118. package/dist/meeting/util.js.map +1 -1
  119. package/dist/meeting/voicea-meeting.d.ts +16 -0
  120. package/dist/meeting-info/collection.d.ts +20 -0
  121. package/dist/meeting-info/index.d.ts +69 -0
  122. package/dist/meeting-info/meeting-info-v2.d.ts +123 -0
  123. package/dist/meeting-info/meeting-info-v2.js +115 -45
  124. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  125. package/dist/meeting-info/request.d.ts +22 -0
  126. package/dist/meeting-info/util.d.ts +2 -0
  127. package/dist/meeting-info/utilv2.d.ts +2 -0
  128. package/dist/meeting-info/utilv2.js +6 -2
  129. package/dist/meeting-info/utilv2.js.map +1 -1
  130. package/dist/meetings/collection.d.ts +40 -0
  131. package/dist/meetings/index.d.ts +390 -0
  132. package/dist/meetings/index.js +107 -55
  133. package/dist/meetings/index.js.map +1 -1
  134. package/dist/meetings/meetings.types.d.ts +4 -0
  135. package/dist/meetings/meetings.types.js +2 -0
  136. package/dist/meetings/meetings.types.js.map +1 -1
  137. package/dist/meetings/request.d.ts +27 -0
  138. package/dist/meetings/util.d.ts +18 -0
  139. package/dist/meetings/util.js +1 -1
  140. package/dist/meetings/util.js.map +1 -1
  141. package/dist/member/index.d.ts +160 -0
  142. package/dist/member/index.js +9 -0
  143. package/dist/member/index.js.map +1 -1
  144. package/dist/member/member.types.js +17 -0
  145. package/dist/member/member.types.js.map +1 -0
  146. package/dist/member/types.d.ts +32 -0
  147. package/dist/member/types.js.map +1 -1
  148. package/dist/member/util.d.ts +2 -0
  149. package/dist/member/util.js +39 -28
  150. package/dist/member/util.js.map +1 -1
  151. package/dist/members/collection.d.ts +29 -0
  152. package/dist/members/index.d.ts +353 -0
  153. package/dist/members/request.d.ts +114 -0
  154. package/dist/members/types.d.ts +25 -0
  155. package/dist/members/util.d.ts +215 -0
  156. package/dist/members/util.js +4 -2
  157. package/dist/members/util.js.map +1 -1
  158. package/dist/metrics/config.js +276 -0
  159. package/dist/metrics/config.js.map +1 -0
  160. package/dist/metrics/constants.d.ts +70 -0
  161. package/dist/metrics/constants.js +6 -1
  162. package/dist/metrics/constants.js.map +1 -1
  163. package/dist/metrics/index.d.ts +45 -0
  164. package/dist/multistream/mediaRequestManager.d.ts +119 -0
  165. package/dist/multistream/receiveSlot.d.ts +68 -0
  166. package/dist/multistream/receiveSlotManager.d.ts +56 -0
  167. package/dist/multistream/remoteMedia.d.ts +72 -0
  168. package/dist/multistream/remoteMedia.js +30 -15
  169. package/dist/multistream/remoteMedia.js.map +1 -1
  170. package/dist/multistream/remoteMediaGroup.d.ts +49 -0
  171. package/dist/multistream/remoteMediaManager.d.ts +300 -0
  172. package/dist/multistream/sendSlotManager.d.ts +69 -0
  173. package/dist/multistream/sendSlotManager.js +24 -0
  174. package/dist/multistream/sendSlotManager.js.map +1 -1
  175. package/dist/networkQualityMonitor/index.d.ts +70 -0
  176. package/dist/networkQualityMonitor/index.js +13 -19
  177. package/dist/networkQualityMonitor/index.js.map +1 -1
  178. package/dist/peer-connection-manager/index.js +671 -0
  179. package/dist/peer-connection-manager/index.js.map +1 -0
  180. package/dist/peer-connection-manager/util.js +109 -0
  181. package/dist/peer-connection-manager/util.js.map +1 -0
  182. package/dist/personal-meeting-room/index.d.ts +47 -0
  183. package/dist/personal-meeting-room/request.d.ts +14 -0
  184. package/dist/personal-meeting-room/util.d.ts +2 -0
  185. package/dist/reachability/clusterReachability.d.ts +109 -0
  186. package/dist/reachability/clusterReachability.js +12 -15
  187. package/dist/reachability/clusterReachability.js.map +1 -1
  188. package/dist/reachability/index.d.ts +105 -0
  189. package/dist/reachability/index.js +461 -136
  190. package/dist/reachability/index.js.map +1 -1
  191. package/dist/reachability/reachability.types.js +7 -0
  192. package/dist/reachability/reachability.types.js.map +1 -0
  193. package/dist/reachability/request.d.ts +39 -0
  194. package/dist/reachability/request.js +21 -8
  195. package/dist/reachability/request.js.map +1 -1
  196. package/dist/reachability/util.d.ts +8 -0
  197. package/dist/reactions/constants.d.ts +3 -0
  198. package/dist/reactions/reactions.d.ts +4 -0
  199. package/dist/reactions/reactions.type.d.ts +52 -0
  200. package/dist/reconnection-manager/index.d.ts +136 -0
  201. package/dist/recording-controller/enums.d.ts +7 -0
  202. package/dist/recording-controller/enums.js +8 -4
  203. package/dist/recording-controller/enums.js.map +1 -1
  204. package/dist/recording-controller/index.d.ts +207 -0
  205. package/dist/recording-controller/index.js +18 -9
  206. package/dist/recording-controller/index.js.map +1 -1
  207. package/dist/recording-controller/util.d.ts +14 -0
  208. package/dist/recording-controller/util.js +13 -9
  209. package/dist/recording-controller/util.js.map +1 -1
  210. package/dist/roap/collection.js +62 -0
  211. package/dist/roap/collection.js.map +1 -0
  212. package/dist/roap/handler.js +275 -0
  213. package/dist/roap/handler.js.map +1 -0
  214. package/dist/roap/index.d.ts +86 -0
  215. package/dist/roap/index.js +15 -15
  216. package/dist/roap/index.js.map +1 -1
  217. package/dist/roap/request.d.ts +39 -0
  218. package/dist/roap/request.js +45 -79
  219. package/dist/roap/request.js.map +1 -1
  220. package/dist/roap/state.js +126 -0
  221. package/dist/roap/state.js.map +1 -0
  222. package/dist/roap/turnDiscovery.d.ts +155 -0
  223. package/dist/roap/turnDiscovery.js +3 -6
  224. package/dist/roap/turnDiscovery.js.map +1 -1
  225. package/dist/roap/util.js +75 -0
  226. package/dist/roap/util.js.map +1 -0
  227. package/dist/rtcMetrics/constants.d.ts +4 -0
  228. package/dist/rtcMetrics/index.d.ts +61 -0
  229. package/dist/statsAnalyzer/global.d.ts +36 -0
  230. package/dist/statsAnalyzer/global.js +126 -0
  231. package/dist/statsAnalyzer/global.js.map +1 -0
  232. package/dist/statsAnalyzer/index.d.ts +217 -0
  233. package/dist/statsAnalyzer/index.js +1013 -0
  234. package/dist/statsAnalyzer/index.js.map +1 -0
  235. package/dist/statsAnalyzer/mqaUtil.d.ts +48 -0
  236. package/dist/statsAnalyzer/mqaUtil.js +179 -0
  237. package/dist/statsAnalyzer/mqaUtil.js.map +1 -0
  238. package/dist/transcription/index.d.ts +64 -0
  239. package/dist/types/annotation/index.d.ts +5 -0
  240. package/dist/types/common/errors/join-forbidden-error.d.ts +15 -0
  241. package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
  242. package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
  243. package/dist/types/common/errors/reconnection-in-progress.d.ts +9 -0
  244. package/dist/types/config.d.ts +1 -0
  245. package/dist/types/constants.d.ts +53 -1
  246. package/dist/types/index.d.ts +3 -3
  247. package/dist/types/locus-info/index.d.ts +2 -1
  248. package/dist/types/mediaQualityMetrics/config.d.ts +241 -0
  249. package/dist/types/meeting/brbState.d.ts +54 -0
  250. package/dist/types/meeting/in-meeting-actions.d.ts +12 -0
  251. package/dist/types/meeting/index.d.ts +64 -14
  252. package/dist/types/meeting/locusMediaRequest.d.ts +6 -3
  253. package/dist/types/meeting/request.d.ts +14 -3
  254. package/dist/types/meeting/request.type.d.ts +6 -0
  255. package/dist/types/meeting/util.d.ts +3 -3
  256. package/dist/types/meeting-info/meeting-info-v2.d.ts +30 -5
  257. package/dist/types/meetings/index.d.ts +20 -2
  258. package/dist/types/meetings/meetings.types.d.ts +8 -0
  259. package/dist/types/member/index.d.ts +1 -0
  260. package/dist/types/member/types.d.ts +7 -0
  261. package/dist/types/members/util.d.ts +2 -0
  262. package/dist/types/metrics/constants.d.ts +6 -1
  263. package/dist/types/multistream/sendSlotManager.d.ts +8 -1
  264. package/dist/types/reachability/clusterReachability.d.ts +1 -10
  265. package/dist/types/reachability/index.d.ts +83 -36
  266. package/dist/types/reachability/reachability.types.d.ts +64 -0
  267. package/dist/types/reachability/request.d.ts +5 -1
  268. package/dist/types/recording-controller/enums.d.ts +5 -2
  269. package/dist/types/recording-controller/index.d.ts +1 -0
  270. package/dist/types/recording-controller/util.d.ts +2 -1
  271. package/dist/types/roap/request.d.ts +1 -13
  272. package/dist/types/statsAnalyzer/global.d.ts +36 -0
  273. package/dist/types/statsAnalyzer/index.d.ts +217 -0
  274. package/dist/types/statsAnalyzer/mqaUtil.d.ts +48 -0
  275. package/dist/webinar/collection.d.ts +16 -0
  276. package/dist/webinar/index.d.ts +5 -0
  277. package/dist/webinar/index.js +390 -7
  278. package/dist/webinar/index.js.map +1 -1
  279. package/package.json +23 -22
  280. package/src/annotation/index.ts +16 -0
  281. package/src/common/errors/join-forbidden-error.ts +26 -0
  282. package/src/common/errors/join-webinar-error.ts +24 -0
  283. package/src/common/errors/multistream-not-supported-error.ts +30 -0
  284. package/src/config.ts +1 -0
  285. package/src/constants.ts +61 -3
  286. package/src/index.ts +5 -3
  287. package/src/locus-info/index.ts +20 -3
  288. package/src/locus-info/selfUtils.ts +24 -6
  289. package/src/media/MediaConnectionAwaiter.ts +2 -0
  290. package/src/media/properties.ts +34 -13
  291. package/src/meeting/brbState.ts +169 -0
  292. package/src/meeting/in-meeting-actions.ts +25 -0
  293. package/src/meeting/index.ts +443 -87
  294. package/src/meeting/locusMediaRequest.ts +11 -8
  295. package/src/meeting/muteState.ts +1 -6
  296. package/src/meeting/request.ts +30 -12
  297. package/src/meeting/request.type.ts +7 -0
  298. package/src/meeting/util.ts +32 -13
  299. package/src/meeting-info/meeting-info-v2.ts +83 -12
  300. package/src/meeting-info/utilv2.ts +17 -3
  301. package/src/meetings/index.ts +79 -20
  302. package/src/meetings/meetings.types.ts +10 -0
  303. package/src/meetings/util.ts +2 -1
  304. package/src/member/index.ts +9 -0
  305. package/src/member/types.ts +8 -0
  306. package/src/member/util.ts +34 -24
  307. package/src/members/util.ts +1 -0
  308. package/src/metrics/constants.ts +6 -1
  309. package/src/multistream/remoteMedia.ts +28 -15
  310. package/src/multistream/sendSlotManager.ts +31 -0
  311. package/src/reachability/clusterReachability.ts +5 -15
  312. package/src/reachability/index.ts +311 -75
  313. package/src/reachability/reachability.types.ts +85 -0
  314. package/src/reachability/request.ts +55 -31
  315. package/src/recording-controller/enums.ts +5 -2
  316. package/src/recording-controller/index.ts +17 -4
  317. package/src/recording-controller/util.ts +20 -5
  318. package/src/roap/index.ts +14 -13
  319. package/src/roap/request.ts +30 -44
  320. package/src/roap/turnDiscovery.ts +2 -4
  321. package/src/webinar/index.ts +235 -9
  322. package/test/unit/spec/annotation/index.ts +46 -1
  323. package/test/unit/spec/locus-info/index.js +292 -60
  324. package/test/unit/spec/locus-info/selfConstant.js +7 -0
  325. package/test/unit/spec/locus-info/selfUtils.js +101 -1
  326. package/test/unit/spec/media/properties.ts +15 -0
  327. package/test/unit/spec/meeting/brbState.ts +114 -0
  328. package/test/unit/spec/meeting/in-meeting-actions.ts +15 -1
  329. package/test/unit/spec/meeting/index.js +851 -107
  330. package/test/unit/spec/meeting/locusMediaRequest.ts +18 -11
  331. package/test/unit/spec/meeting/muteState.js +0 -24
  332. package/test/unit/spec/meeting/request.js +3 -26
  333. package/test/unit/spec/meeting/utils.js +73 -28
  334. package/test/unit/spec/meeting-info/meetinginfov2.js +46 -4
  335. package/test/unit/spec/meeting-info/utilv2.js +26 -0
  336. package/test/unit/spec/meetings/index.js +159 -18
  337. package/test/unit/spec/meetings/utils.js +10 -0
  338. package/test/unit/spec/member/util.js +52 -11
  339. package/test/unit/spec/members/utils.js +95 -0
  340. package/test/unit/spec/multistream/remoteMedia.ts +11 -7
  341. package/test/unit/spec/reachability/clusterReachability.ts +7 -0
  342. package/test/unit/spec/reachability/index.ts +383 -9
  343. package/test/unit/spec/reachability/request.js +48 -12
  344. package/test/unit/spec/recording-controller/index.js +61 -5
  345. package/test/unit/spec/recording-controller/util.js +39 -3
  346. package/test/unit/spec/roap/index.ts +48 -1
  347. package/test/unit/spec/roap/request.ts +51 -109
  348. package/test/unit/spec/roap/turnDiscovery.ts +202 -147
  349. package/test/unit/spec/webinar/index.ts +504 -0
  350. package/dist/common/errors/webinar-registration-error.js.map +0 -1
  351. package/src/common/errors/webinar-registration-error.ts +0 -27
@@ -5,6 +5,7 @@ import jwtDecode from 'jwt-decode';
5
5
  import {StatelessWebexPlugin} from '@webex/webex-core';
6
6
  // @ts-ignore - Types not available for @webex/common
7
7
  import {Defer} from '@webex/common';
8
+ import {safeSetTimeout, safeSetInterval} from '@webex/common-timers';
8
9
  import {
9
10
  ClientEvent,
10
11
  ClientEventLeaveReason,
@@ -30,7 +31,6 @@ import {
30
31
  } from '@webex/internal-media-core';
31
32
 
32
33
  import {
33
- getDevices,
34
34
  LocalStream,
35
35
  LocalCameraStream,
36
36
  LocalDisplayStream,
@@ -121,6 +121,10 @@ import {
121
121
  MEETING_PERMISSION_TOKEN_REFRESH_REASON,
122
122
  ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT,
123
123
  NAMED_MEDIA_GROUP_TYPE_AUDIO,
124
+ WEBINAR_ERROR_WEBCAST,
125
+ WEBINAR_ERROR_REGISTRATION_ID,
126
+ JOIN_BEFORE_HOST,
127
+ REGISTRATION_ID_STATUS,
124
128
  } from '../constants';
125
129
  import BEHAVIORAL_METRICS from '../metrics/constants';
126
130
  import ParameterError from '../common/errors/parameter';
@@ -128,7 +132,8 @@ import {
128
132
  MeetingInfoV2PasswordError,
129
133
  MeetingInfoV2CaptchaError,
130
134
  MeetingInfoV2PolicyError,
131
- MeetingInfoV2WebinarRegistrationError,
135
+ MeetingInfoV2JoinWebinarError,
136
+ MeetingInfoV2JoinForbiddenError,
132
137
  } from '../meeting-info/meeting-info-v2';
133
138
  import {CSI, ReceiveSlotManager} from '../multistream/receiveSlotManager';
134
139
  import SendSlotManager from '../multistream/sendSlotManager';
@@ -157,7 +162,11 @@ import ControlsOptionsManager from '../controls-options-manager';
157
162
  import PermissionError from '../common/errors/permission';
158
163
  import {LocusMediaRequest} from './locusMediaRequest';
159
164
  import {ConnectionStateHandler, ConnectionStateEvent} from './connectionStateHandler';
160
- import WebinarRegistrationError from '../common/errors/webinar-registration-error';
165
+ import JoinWebinarError from '../common/errors/join-webinar-error';
166
+ import Member from '../member';
167
+ import {BrbState, createBrbState} from './brbState';
168
+ import MultistreamNotSupportedError from '../common/errors/multistream-not-supported-error';
169
+ import JoinForbiddenError from '../common/errors/join-forbidden-error';
161
170
 
162
171
  // default callback so we don't call an undefined function, but in practice it should never be used
163
172
  const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL';
@@ -248,6 +257,7 @@ export enum ScreenShareFloorStatus {
248
257
 
249
258
  type FetchMeetingInfoParams = {
250
259
  password?: string;
260
+ registrationId?: string;
251
261
  captchaCode?: string;
252
262
  extraParams?: Record<string, any>;
253
263
  sendCAevents?: boolean;
@@ -642,6 +652,8 @@ export default class Meeting extends StatelessWebexPlugin {
642
652
  turnServerUsed: boolean;
643
653
  areVoiceaEventsSetup = false;
644
654
  isMoveToInProgress = false;
655
+ registrationIdStatus: string;
656
+ brbState: BrbState;
645
657
 
646
658
  voiceaListenerCallbacks: object = {
647
659
  [VOICEAEVENTS.VOICEA_ANNOUNCEMENT]: (payload: Transcription['languageOptions']) => {
@@ -702,6 +714,8 @@ export default class Meeting extends StatelessWebexPlugin {
702
714
  private iceCandidateErrors: Map<string, number>;
703
715
  private iceCandidatesCount: number;
704
716
  private rtcMetrics?: RtcMetrics;
717
+ private uploadLogsTimer?: ReturnType<typeof setTimeout>;
718
+ private logUploadIntervalIndex: number;
705
719
 
706
720
  /**
707
721
  * @param {Object} attrs
@@ -770,6 +784,8 @@ export default class Meeting extends StatelessWebexPlugin {
770
784
  );
771
785
  this.callStateForMetrics.correlationId = this.id;
772
786
  }
787
+ this.logUploadIntervalIndex = 0;
788
+
773
789
  /**
774
790
  * @instance
775
791
  * @type {String}
@@ -843,7 +859,7 @@ export default class Meeting extends StatelessWebexPlugin {
843
859
  * @memberof Meeting
844
860
  */
845
861
  // @ts-ignore
846
- this.webinar = new Webinar({}, {parent: this.webex});
862
+ this.webinar = new Webinar({meetingId: this.id}, {parent: this.webex});
847
863
  /**
848
864
  * helper class for managing receive slots (for multistream media connections)
849
865
  */
@@ -1334,6 +1350,16 @@ export default class Meeting extends StatelessWebexPlugin {
1334
1350
  */
1335
1351
  this.passwordStatus = PASSWORD_STATUS.UNKNOWN;
1336
1352
 
1353
+ /**
1354
+ * registrationId status. If it's REGISTRATIONID_STATUS.REQUIRED then verifyRegistrationId() needs to be called
1355
+ * with the correct registrationId before calling join()
1356
+ * @instance
1357
+ * @type {REGISTRATION_ID_STATUS}
1358
+ * @public
1359
+ * @memberof Meeting
1360
+ */
1361
+ this.registrationIdStatus = REGISTRATION_ID_STATUS.UNKNOWN;
1362
+
1337
1363
  /**
1338
1364
  * Information about required captcha. If null, then no captcha is required. status. If it's PASSWORD_STATUS.REQUIRED then verifyPassword() needs to be called
1339
1365
  * with the correct password before calling join()
@@ -1646,6 +1672,15 @@ export default class Meeting extends StatelessWebexPlugin {
1646
1672
  this.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
1647
1673
  }
1648
1674
 
1675
+ if (
1676
+ this.registrationIdStatus === REGISTRATION_ID_STATUS.REQUIRED ||
1677
+ this.registrationIdStatus === REGISTRATION_ID_STATUS.VERIFIED
1678
+ ) {
1679
+ this.registrationIdStatus = REGISTRATION_ID_STATUS.VERIFIED;
1680
+ } else {
1681
+ this.registrationIdStatus = REGISTRATION_ID_STATUS.NOT_REQUIRED;
1682
+ }
1683
+
1649
1684
  Trigger.trigger(
1650
1685
  this,
1651
1686
  {
@@ -1689,7 +1724,12 @@ export default class Meeting extends StatelessWebexPlugin {
1689
1724
  * @private
1690
1725
  */
1691
1726
  private prepForFetchMeetingInfo(
1692
- {password = null, captchaCode = null, extraParams = {}}: FetchMeetingInfoParams,
1727
+ {
1728
+ password = null,
1729
+ registrationId = null,
1730
+ captchaCode = null,
1731
+ extraParams = {},
1732
+ }: FetchMeetingInfoParams,
1693
1733
  caller: string
1694
1734
  ): Promise<void> {
1695
1735
  // when fetch meeting info is called directly by the client, we want to clear out the random timer for sdk to do it
@@ -1729,6 +1769,7 @@ export default class Meeting extends StatelessWebexPlugin {
1729
1769
  captchaCode = null,
1730
1770
  extraParams = {},
1731
1771
  sendCAevents = false,
1772
+ registrationId = null,
1732
1773
  }): Promise<void> {
1733
1774
  try {
1734
1775
  const captchaInfo = captchaCode
@@ -1744,7 +1785,8 @@ export default class Meeting extends StatelessWebexPlugin {
1744
1785
  this.config.installedOrgID,
1745
1786
  this.locusId,
1746
1787
  extraParams,
1747
- {meetingId: this.id, sendCAevents}
1788
+ {meetingId: this.id, sendCAevents},
1789
+ registrationId
1748
1790
  );
1749
1791
 
1750
1792
  this.parseMeetingInfo(info?.body, this.destination, info?.errors);
@@ -1762,15 +1804,35 @@ export default class Meeting extends StatelessWebexPlugin {
1762
1804
  this.meetingInfo = err.meetingInfo;
1763
1805
  }
1764
1806
  throw new PermissionError();
1765
- } else if (err instanceof MeetingInfoV2WebinarRegistrationError) {
1807
+ } else if (err instanceof MeetingInfoV2JoinWebinarError) {
1766
1808
  this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WEBINAR_REGISTRATION;
1809
+ if (WEBINAR_ERROR_WEBCAST.includes(err.wbxAppApiCode)) {
1810
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NEED_JOIN_WITH_WEBCAST;
1811
+ } else if (WEBINAR_ERROR_REGISTRATION_ID.includes(err.wbxAppApiCode)) {
1812
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WEBINAR_NEED_REGISTRATION_ID;
1813
+ }
1814
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1815
+
1816
+ if (err.meetingInfo) {
1817
+ this.meetingInfo = err.meetingInfo;
1818
+ }
1819
+ this.requiredCaptcha = null;
1820
+
1821
+ throw new JoinWebinarError();
1822
+ } else if (err instanceof MeetingInfoV2JoinForbiddenError) {
1823
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.JOIN_FORBIDDEN;
1767
1824
  this.meetingInfoFailureCode = err.wbxAppApiCode;
1768
1825
 
1769
1826
  if (err.meetingInfo) {
1770
1827
  this.meetingInfo = err.meetingInfo;
1771
1828
  }
1772
1829
 
1773
- throw new WebinarRegistrationError();
1830
+ // Handle the case where user hasn't reached Join Before Host (JBH) time (error code 403003)
1831
+ if (JOIN_BEFORE_HOST === err.wbxAppApiCode) {
1832
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NOT_REACH_JBH;
1833
+ }
1834
+
1835
+ throw new JoinForbiddenError(this.meetingInfoFailureReason, err);
1774
1836
  } else if (err instanceof MeetingInfoV2PasswordError) {
1775
1837
  LoggerProxy.logger.info(
1776
1838
  // @ts-ignore
@@ -1799,9 +1861,13 @@ export default class Meeting extends StatelessWebexPlugin {
1799
1861
  `Meeting:index#fetchMeetingInfo --> Info Unable to fetch meeting info for ${this.destination} - captcha required (code=${err?.body?.code}).`
1800
1862
  );
1801
1863
 
1802
- this.meetingInfoFailureReason = this.requiredCaptcha
1803
- ? MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA
1804
- : MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD;
1864
+ if (this.requiredCaptcha) {
1865
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA;
1866
+ } else if (err.isRegistrationIdRequired) {
1867
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_REGISTRATION_ID;
1868
+ } else {
1869
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD;
1870
+ }
1805
1871
 
1806
1872
  this.meetingInfoFailureCode = err.wbxAppApiCode;
1807
1873
 
@@ -1809,6 +1875,10 @@ export default class Meeting extends StatelessWebexPlugin {
1809
1875
  this.passwordStatus = PASSWORD_STATUS.REQUIRED;
1810
1876
  }
1811
1877
 
1878
+ if (err.isRegistrationIdRequired) {
1879
+ this.registrationIdStatus = REGISTRATION_ID_STATUS.REQUIRED;
1880
+ }
1881
+
1812
1882
  this.requiredCaptcha = err.captchaInfo;
1813
1883
  throw new CaptchaError();
1814
1884
  } else {
@@ -1949,6 +2019,48 @@ export default class Meeting extends StatelessWebexPlugin {
1949
2019
  });
1950
2020
  }
1951
2021
 
2022
+ /**
2023
+ * Checks if the supplied registrationId is correct. It returns a promise with information whether the
2024
+ * registrationId and captcha code were correct or not.
2025
+ * @param {String | undefined} registrationId - can be undefined if only captcha was required
2026
+ * @param {String | undefined} captchaCode - can be undefined if captcha was not required by the server
2027
+ * @param {Boolean} sendCAevents - whether Call Analyzer events should be sent when fetching meeting information
2028
+ * @public
2029
+ * @memberof Meeting
2030
+ * @returns {Promise<{isRegistrationIdValid: boolean, requiredCaptcha: boolean, failureReason: MEETING_INFO_FAILURE_REASON}>}
2031
+ */
2032
+ public verifyRegistrationId(registrationId: string, captchaCode: string, sendCAevents = false) {
2033
+ return this.fetchMeetingInfo({
2034
+ registrationId,
2035
+ captchaCode,
2036
+ sendCAevents,
2037
+ })
2038
+ .then(() => {
2039
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.VERIFY_REGISTRATION_ID_SUCCESS);
2040
+
2041
+ return {
2042
+ isRegistrationIdValid: true,
2043
+ requiredCaptcha: null,
2044
+ failureReason: MEETING_INFO_FAILURE_REASON.NONE,
2045
+ };
2046
+ })
2047
+ .catch((error) => {
2048
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.VERIFY_REGISTRATION_ID_ERROR);
2049
+
2050
+ if (error instanceof JoinWebinarError || error instanceof CaptchaError) {
2051
+ return {
2052
+ isRegistrationIdValid: this.registrationIdStatus === REGISTRATION_ID_STATUS.VERIFIED,
2053
+ requiredCaptcha: this.requiredCaptcha,
2054
+ failureReason:
2055
+ error instanceof JoinWebinarError
2056
+ ? MEETING_INFO_FAILURE_REASON.WRONG_REGISTRATION_ID
2057
+ : this.meetingInfoFailureReason,
2058
+ };
2059
+ }
2060
+ throw error;
2061
+ });
2062
+ }
2063
+
1952
2064
  /**
1953
2065
  * Refreshes the captcha. As a result the meeting will have new captcha id, image and audio.
1954
2066
  * If the refresh operation fails, meeting remains with the old captcha properties.
@@ -2655,6 +2767,7 @@ export default class Meeting extends StatelessWebexPlugin {
2655
2767
  });
2656
2768
 
2657
2769
  this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_PRACTICE_SESSION_STATUS_UPDATED, ({state}) => {
2770
+ this.webinar.updatePracticeSessionStatus(state);
2658
2771
  Trigger.trigger(
2659
2772
  this,
2660
2773
  {file: 'meeting/index', function: 'setupLocusControlsListener'},
@@ -2728,6 +2841,7 @@ export default class Meeting extends StatelessWebexPlugin {
2728
2841
  this.triggerAnnotationInfoEvent(contentShare, previousContentShare);
2729
2842
 
2730
2843
  if (
2844
+ !payload.forceUpdate &&
2731
2845
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
2732
2846
  contentShare.disposition === previousContentShare?.disposition &&
2733
2847
  contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
@@ -2774,7 +2888,11 @@ export default class Meeting extends StatelessWebexPlugin {
2774
2888
  // It does not matter who requested to share the whiteboard, everyone gets the same view
2775
2889
  else if (whiteboardShare.disposition === FLOOR_ACTION.GRANTED) {
2776
2890
  // WHITEBOARD - sharing whiteboard
2777
- newShareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
2891
+ // Webinar attendee should receive whiteboard as remote share
2892
+ newShareStatus =
2893
+ this.locusInfo?.info?.isWebinar && this.webinar?.selfIsAttendee
2894
+ ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
2895
+ : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
2778
2896
  }
2779
2897
  // or if content share is either released or null and whiteboard share is either released or null, no one is sharing
2780
2898
  else if (
@@ -2789,6 +2907,7 @@ export default class Meeting extends StatelessWebexPlugin {
2789
2907
  LoggerProxy.logger.info(
2790
2908
  `Meeting:index#setUpLocusInfoMediaInactiveListener --> this.shareStatus=${this.shareStatus} newShareStatus=${newShareStatus}`
2791
2909
  );
2910
+
2792
2911
  if (newShareStatus !== this.shareStatus) {
2793
2912
  const oldShareStatus = this.shareStatus;
2794
2913
 
@@ -3046,7 +3165,20 @@ export default class Meeting extends StatelessWebexPlugin {
3046
3165
  */
3047
3166
  private setUpLocusResourcesListener() {
3048
3167
  this.locusInfo.on(LOCUSINFO.EVENTS.LINKS_RESOURCES, (payload) => {
3049
- this.webinar.updateWebcastUrl(payload);
3168
+ if (payload) {
3169
+ this.webinar.updateWebcastUrl(payload);
3170
+ Trigger.trigger(
3171
+ this,
3172
+ {
3173
+ file: 'meeting/index',
3174
+ function: 'setUpLocusInfoMeetingInfoListener',
3175
+ },
3176
+ EVENT_TRIGGERS.MEETING_RESOURCE_LINKS_UPDATE,
3177
+ {
3178
+ payload,
3179
+ }
3180
+ );
3181
+ }
3050
3182
  });
3051
3183
  }
3052
3184
 
@@ -3249,6 +3381,9 @@ export default class Meeting extends StatelessWebexPlugin {
3249
3381
  options: {meetingId: this.id},
3250
3382
  });
3251
3383
  }
3384
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.GUEST_ENTERED_LOBBY, {
3385
+ correlation_id: this.correlationId,
3386
+ });
3252
3387
  this.updateLLMConnection();
3253
3388
  });
3254
3389
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, async (payload) => {
@@ -3272,6 +3407,9 @@ export default class Meeting extends StatelessWebexPlugin {
3272
3407
  name: 'client.lobby.exited',
3273
3408
  options: {meetingId: this.id},
3274
3409
  });
3410
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.GUEST_EXITED_LOBBY, {
3411
+ correlation_id: this.correlationId,
3412
+ });
3275
3413
  }
3276
3414
  this.rtcMetrics?.sendNextMetrics();
3277
3415
  this.updateLLMConnection();
@@ -3293,6 +3431,10 @@ export default class Meeting extends StatelessWebexPlugin {
3293
3431
  // The second on is if the audio is muted, we need to tell the statsAnalyzer when
3294
3432
  // the audio is muted or the user is not willing to send media
3295
3433
  this.locusInfo.on(LOCUSINFO.EVENTS.MEDIA_STATUS_CHANGE, (status) => {
3434
+ LoggerProxy.logger.info(
3435
+ 'Meeting:index#setUpLocusInfoSelfListener --> MEDIA_STATUS_CHANGE received, processing...'
3436
+ );
3437
+
3296
3438
  if (this.statsAnalyzer) {
3297
3439
  this.statsAnalyzer.updateMediaStatus({
3298
3440
  actual: status,
@@ -3306,6 +3448,10 @@ export default class Meeting extends StatelessWebexPlugin {
3306
3448
  receiveShare: this.mediaProperties.mediaDirection?.receiveShare,
3307
3449
  },
3308
3450
  });
3451
+ } else {
3452
+ LoggerProxy.logger.warn(
3453
+ 'Meeting:index#setUpLocusInfoSelfListener --> MEDIA_STATUS_CHANGE, statsAnalyzer is not available.'
3454
+ );
3309
3455
  }
3310
3456
  });
3311
3457
 
@@ -3350,6 +3496,21 @@ export default class Meeting extends StatelessWebexPlugin {
3350
3496
  }
3351
3497
  });
3352
3498
 
3499
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED, (payload) => {
3500
+ this.brbState?.handleServerBrbUpdate(payload?.brb?.enabled);
3501
+ Trigger.trigger(
3502
+ this,
3503
+ {
3504
+ file: 'meeting/index',
3505
+ function: 'setUpLocusInfoSelfListener',
3506
+ },
3507
+ EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
3508
+ {
3509
+ payload,
3510
+ }
3511
+ );
3512
+ });
3513
+
3353
3514
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ROLES_CHANGED, (payload) => {
3354
3515
  const isModeratorOrCohost =
3355
3516
  payload.newRoles?.includes(SELF_ROLES.MODERATOR) ||
@@ -3359,6 +3520,7 @@ export default class Meeting extends StatelessWebexPlugin {
3359
3520
  payload.newRoles?.includes(SELF_ROLES.MODERATOR)
3360
3521
  );
3361
3522
  this.webinar.updateRoleChanged(payload);
3523
+
3362
3524
  Trigger.trigger(
3363
3525
  this,
3364
3526
  {
@@ -3505,6 +3667,7 @@ export default class Meeting extends StatelessWebexPlugin {
3505
3667
  emailAddress: string;
3506
3668
  email: string;
3507
3669
  phoneNumber: string;
3670
+ roles: Array<string>;
3508
3671
  },
3509
3672
  alertIfActive = true
3510
3673
  ) {
@@ -3552,6 +3715,35 @@ export default class Meeting extends StatelessWebexPlugin {
3552
3715
  return this.members.admitMembers(memberIds, locusUrls);
3553
3716
  }
3554
3717
 
3718
+ /**
3719
+ * Manages be right back status updates for the current participant.
3720
+ *
3721
+ * @param {boolean} enabled - Indicates whether the user enabled brb or not.
3722
+ * @returns {Promise<void>} resolves when the brb status is updated or does nothing if not in a multistream meeting.
3723
+ * @throws {Error} - Throws an error if the request fails.
3724
+ */
3725
+ public async beRightBack(enabled: boolean): Promise<void> {
3726
+ if (!this.isMultistream) {
3727
+ const errorMessage = 'Meeting:index#beRightBack --> Not a multistream meeting';
3728
+ const error = new Error(errorMessage);
3729
+
3730
+ LoggerProxy.logger.error(error);
3731
+
3732
+ return Promise.reject(error);
3733
+ }
3734
+
3735
+ if (!this.mediaProperties.webrtcMediaConnection) {
3736
+ const errorMessage = 'Meeting:index#beRightBack --> WebRTC media connection is not defined';
3737
+ const error = new Error(errorMessage);
3738
+
3739
+ LoggerProxy.logger.error(error);
3740
+
3741
+ return Promise.reject(error);
3742
+ }
3743
+
3744
+ return this.brbState.enable(enabled, this.sendSlotManager);
3745
+ }
3746
+
3555
3747
  /**
3556
3748
  * Remove the member from the meeting, boot them
3557
3749
  * @param {String} memberId
@@ -3761,6 +3953,10 @@ export default class Meeting extends StatelessWebexPlugin {
3761
3953
  this.userDisplayHints,
3762
3954
  this.selfUserPolicies
3763
3955
  ),
3956
+ isPremiseRecordingEnabled: RecordingUtil.isPremiseRecordingEnabled(
3957
+ this.userDisplayHints,
3958
+ this.selfUserPolicies
3959
+ ),
3764
3960
  canRaiseHand: MeetingUtil.canUserRaiseHand(this.userDisplayHints),
3765
3961
  canLowerAllHands: MeetingUtil.canUserLowerAllHands(this.userDisplayHints),
3766
3962
  canLowerSomeoneElsesHand: MeetingUtil.canUserLowerSomeoneElsesHand(this.userDisplayHints),
@@ -3787,6 +3983,7 @@ export default class Meeting extends StatelessWebexPlugin {
3787
3983
  this.userDisplayHints
3788
3984
  ),
3789
3985
  canManageBreakout: MeetingUtil.canManageBreakout(this.userDisplayHints),
3986
+ canStartBreakout: MeetingUtil.canStartBreakout(this.userDisplayHints),
3790
3987
  canBroadcastMessageToBreakout: MeetingUtil.canBroadcastMessageToBreakout(
3791
3988
  this.userDisplayHints,
3792
3989
  this.selfUserPolicies
@@ -3904,6 +4101,22 @@ export default class Meeting extends StatelessWebexPlugin {
3904
4101
  requiredHints: [DISPLAY_HINTS.DISABLE_STAGE_VIEW],
3905
4102
  displayHints: this.userDisplayHints,
3906
4103
  }),
4104
+ isPracticeSessionOn: ControlsOptionsUtil.hasHints({
4105
+ requiredHints: [DISPLAY_HINTS.PRACTICE_SESSION_ON],
4106
+ displayHints: this.userDisplayHints,
4107
+ }),
4108
+ isPracticeSessionOff: ControlsOptionsUtil.hasHints({
4109
+ requiredHints: [DISPLAY_HINTS.PRACTICE_SESSION_OFF],
4110
+ displayHints: this.userDisplayHints,
4111
+ }),
4112
+ canStartPracticeSession: ControlsOptionsUtil.hasHints({
4113
+ requiredHints: [DISPLAY_HINTS.SHOW_PRACTICE_SESSION_START],
4114
+ displayHints: this.userDisplayHints,
4115
+ }),
4116
+ canStopPracticeSession: ControlsOptionsUtil.hasHints({
4117
+ requiredHints: [DISPLAY_HINTS.SHOW_PRACTICE_SESSION_STOP],
4118
+ displayHints: this.userDisplayHints,
4119
+ }),
3907
4120
  canShareFile:
3908
4121
  (ControlsOptionsUtil.hasHints({
3909
4122
  requiredHints: [DISPLAY_HINTS.SHARE_FILE],
@@ -4060,6 +4273,66 @@ export default class Meeting extends StatelessWebexPlugin {
4060
4273
  Trigger.trigger(this, options, EVENTS.REQUEST_UPLOAD_LOGS, this);
4061
4274
  }
4062
4275
 
4276
+ /**
4277
+ * sets the timer for periodic log upload
4278
+ * @returns {void}
4279
+ */
4280
+ private setLogUploadTimer() {
4281
+ // start with short timeouts and increase them later on so in case users have very long multi-hour meetings we don't get too fragmented logs
4282
+ const LOG_UPLOAD_INTERVALS = [0.1, 15, 30, 60]; // in minutes
4283
+
4284
+ const delay =
4285
+ 1000 *
4286
+ 60 *
4287
+ // @ts-ignore - config coming from registerPlugin
4288
+ this.config.logUploadIntervalMultiplicationFactor *
4289
+ LOG_UPLOAD_INTERVALS[this.logUploadIntervalIndex];
4290
+
4291
+ if (this.logUploadIntervalIndex < LOG_UPLOAD_INTERVALS.length - 1) {
4292
+ this.logUploadIntervalIndex += 1;
4293
+ }
4294
+
4295
+ this.uploadLogsTimer = safeSetTimeout(() => {
4296
+ this.uploadLogsTimer = undefined;
4297
+
4298
+ this.uploadLogs();
4299
+
4300
+ // just as an extra precaution, to avoid uploading logs forever in case something goes wrong
4301
+ // and the page remains opened, we stop it if there is no media connection
4302
+ if (!this.mediaProperties.webrtcMediaConnection) {
4303
+ return;
4304
+ }
4305
+
4306
+ this.setLogUploadTimer();
4307
+ }, delay);
4308
+ }
4309
+
4310
+ /**
4311
+ * Starts a periodic upload of logs
4312
+ *
4313
+ * @returns {undefined}
4314
+ */
4315
+ public startPeriodicLogUpload() {
4316
+ // @ts-ignore - config coming from registerPlugin
4317
+ if (this.config.logUploadIntervalMultiplicationFactor && !this.uploadLogsTimer) {
4318
+ this.logUploadIntervalIndex = 0;
4319
+
4320
+ this.setLogUploadTimer();
4321
+ }
4322
+ }
4323
+
4324
+ /**
4325
+ * Stops the periodic upload of logs
4326
+ *
4327
+ * @returns {undefined}
4328
+ */
4329
+ public stopPeriodicLogUpload() {
4330
+ if (this.uploadLogsTimer) {
4331
+ clearTimeout(this.uploadLogsTimer);
4332
+ this.uploadLogsTimer = undefined;
4333
+ }
4334
+ }
4335
+
4063
4336
  /**
4064
4337
  * Removes remote audio, video and share streams from class instance's mediaProperties
4065
4338
  * @returns {undefined}
@@ -4449,11 +4722,12 @@ export default class Meeting extends StatelessWebexPlugin {
4449
4722
  * Close the peer connections and remove them from the class.
4450
4723
  * Cleanup any media connection related things.
4451
4724
  *
4725
+ * @param {boolean} resetMuteStates whether to also reset the audio/video mute state information
4452
4726
  * @returns {Promise}
4453
4727
  * @public
4454
4728
  * @memberof Meeting
4455
4729
  */
4456
- public closePeerConnections() {
4730
+ public closePeerConnections(resetMuteStates = true) {
4457
4731
  if (this.mediaProperties.webrtcMediaConnection) {
4458
4732
  if (this.remoteMediaManager) {
4459
4733
  this.remoteMediaManager.stop();
@@ -4466,12 +4740,15 @@ export default class Meeting extends StatelessWebexPlugin {
4466
4740
 
4467
4741
  this.receiveSlotManager.reset();
4468
4742
  this.mediaProperties.webrtcMediaConnection.close();
4743
+ this.mediaProperties.unsetPeerConnection();
4469
4744
  this.sendSlotManager.reset();
4470
4745
  this.setNetworkStatus(undefined);
4471
4746
  }
4472
4747
 
4473
- this.audio = null;
4474
- this.video = null;
4748
+ if (resetMuteStates) {
4749
+ this.audio = null;
4750
+ this.video = null;
4751
+ }
4475
4752
 
4476
4753
  return Promise.resolve();
4477
4754
  }
@@ -4731,7 +5008,7 @@ export default class Meeting extends StatelessWebexPlugin {
4731
5008
  * @param {Object} options - options to join with media
4732
5009
  * @param {JoinOptions} [options.joinOptions] - see #join()
4733
5010
  * @param {AddMediaOptions} [options.mediaOptions] - see #addMedia()
4734
- * @returns {Promise} -- {join: see join(), media: see addMedia()}
5011
+ * @returns {Promise} -- {join: see join(), media: see addMedia(), multistreamEnabled: flag to indicate if we managed to join in multistream mode}
4735
5012
  * @public
4736
5013
  * @memberof Meeting
4737
5014
  * @example
@@ -4771,8 +5048,6 @@ export default class Meeting extends StatelessWebexPlugin {
4771
5048
  if (!joinResponse) {
4772
5049
  // This is the 1st attempt or a retry after join request failed -> we need to do a join with TURN discovery
4773
5050
 
4774
- // @ts-ignore
4775
- joinOptions.reachability = await this.webex.meetings.reachability.getReachabilityResults();
4776
5051
  const turnDiscoveryRequest = await this.roap.generateTurnDiscoveryRequestMessage(
4777
5052
  this,
4778
5053
  true
@@ -4823,6 +5098,7 @@ export default class Meeting extends StatelessWebexPlugin {
4823
5098
  return {
4824
5099
  join: joinResponse,
4825
5100
  media: mediaResponse,
5101
+ multistreamEnabled: this.isMultistream,
4826
5102
  };
4827
5103
  } catch (error) {
4828
5104
  LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
@@ -4831,7 +5107,17 @@ export default class Meeting extends StatelessWebexPlugin {
4831
5107
 
4832
5108
  this.roap.abortTurnDiscovery();
4833
5109
 
4834
- if (joined && isRetry) {
5110
+ // if this was the first attempt, let's do a retry
5111
+ let shouldRetry = !isRetry;
5112
+
5113
+ if (CallDiagnosticUtils.isSdpOfferCreationError(error)) {
5114
+ // errors related to offer creation (for example missing H264 codec) will happen again no matter how many times we try,
5115
+ // so there is no point doing a retry
5116
+ shouldRetry = false;
5117
+ }
5118
+
5119
+ // we only want to call leave if join was successful and this was a retry or we won't be doing any more retries
5120
+ if (joined && (isRetry || !shouldRetry)) {
4835
5121
  try {
4836
5122
  await this.leave({resourceId: joinOptions?.resourceId, reason: 'joinWithMedia failure'});
4837
5123
  } catch (e) {
@@ -4855,15 +5141,6 @@ export default class Meeting extends StatelessWebexPlugin {
4855
5141
  }
4856
5142
  );
4857
5143
 
4858
- // if this was the first attempt, let's do a retry
4859
- let shouldRetry = !isRetry;
4860
-
4861
- if (CallDiagnosticUtils.isSdpOfferCreationError(error)) {
4862
- // errors related to offer creation (for example missing H264 codec) will happen again no matter how many times we try,
4863
- // so there is no point doing a retry
4864
- shouldRetry = false;
4865
- }
4866
-
4867
5144
  if (shouldRetry) {
4868
5145
  LoggerProxy.logger.warn('Meeting:index#joinWithMedia --> retrying call to joinWithMedia');
4869
5146
  this.joinWithMediaRetryInfo.isRetry = true;
@@ -5119,7 +5396,16 @@ export default class Meeting extends StatelessWebexPlugin {
5119
5396
  (this.config.receiveReactions || options.receiveReactions) &&
5120
5397
  this.isReactionsSupported()
5121
5398
  ) {
5122
- const {name} = this.members.membersCollection.get(e.data.sender.participantId);
5399
+ const member = this.members.membersCollection.get(e.data.sender.participantId);
5400
+ if (!member) {
5401
+ // @ts-ignore -- fix type
5402
+ LoggerProxy.logger.warn(
5403
+ `Meeting:index#processRelayEvent --> Skipping handling of ${REACTION_RELAY_TYPES.REACTION} for ${this.id}. participantId ${e.data.sender.participantId} does not exist in membersCollection.`
5404
+ );
5405
+ break;
5406
+ }
5407
+
5408
+ const {name} = member;
5123
5409
  const processedReaction: ProcessedReaction = {
5124
5410
  reaction: e.data.reaction,
5125
5411
  sender: {
@@ -5173,6 +5459,9 @@ export default class Meeting extends StatelessWebexPlugin {
5173
5459
  this.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]
5174
5460
  );
5175
5461
 
5462
+ // @ts-ignore
5463
+ this.webex.internal.voicea.deregisterEvents();
5464
+
5176
5465
  this.areVoiceaEventsSetup = false;
5177
5466
  this.triggerStopReceivingTranscriptionEvent();
5178
5467
  }
@@ -5283,16 +5572,19 @@ export default class Meeting extends StatelessWebexPlugin {
5283
5572
  this.meetingFiniteStateMachine.reset();
5284
5573
  }
5285
5574
 
5286
- // @ts-ignore
5287
- this.webex.internal.newMetrics.submitClientEvent({
5288
- name: 'client.call.initiated',
5289
- payload: {
5290
- trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
5291
- isRoapCallEnabled: true,
5292
- pstnAudioType: options?.pstnAudioType,
5293
- },
5294
- options: {meetingId: this.id},
5295
- });
5575
+ // send client.call.initiated unless told not to
5576
+ if (options.sendCallInitiated !== false) {
5577
+ // @ts-ignore
5578
+ this.webex.internal.newMetrics.submitClientEvent({
5579
+ name: 'client.call.initiated',
5580
+ payload: {
5581
+ trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
5582
+ isRoapCallEnabled: true,
5583
+ pstnAudioType: options?.pstnAudioType,
5584
+ },
5585
+ options: {meetingId: this.id},
5586
+ });
5587
+ }
5296
5588
 
5297
5589
  LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
5298
5590
 
@@ -5480,17 +5772,23 @@ export default class Meeting extends StatelessWebexPlugin {
5480
5772
  */
5481
5773
  async updateLLMConnection() {
5482
5774
  // @ts-ignore - Fix type
5483
- const {url, info: {datachannelUrl} = {}} = this.locusInfo;
5775
+ const {url, info: {datachannelUrl, practiceSessionDatachannelUrl} = {}} = this.locusInfo;
5484
5776
 
5485
5777
  const isJoined = this.isJoined();
5486
5778
 
5779
+ // webinar panelist should use new data channel in practice session
5780
+ const dataChannelUrl =
5781
+ this.webinar.isJoinPracticeSessionDataChannel() && practiceSessionDatachannelUrl
5782
+ ? practiceSessionDatachannelUrl
5783
+ : datachannelUrl;
5784
+
5487
5785
  // @ts-ignore - Fix type
5488
5786
  if (this.webex.internal.llm.isConnected()) {
5489
5787
  if (
5490
5788
  // @ts-ignore - Fix type
5491
5789
  url === this.webex.internal.llm.getLocusUrl() &&
5492
5790
  // @ts-ignore - Fix type
5493
- datachannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5791
+ dataChannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5494
5792
  isJoined
5495
5793
  ) {
5496
5794
  return undefined;
@@ -5507,7 +5805,7 @@ export default class Meeting extends StatelessWebexPlugin {
5507
5805
 
5508
5806
  // @ts-ignore - Fix type
5509
5807
  return this.webex.internal.llm
5510
- .registerAndConnect(url, datachannelUrl)
5808
+ .registerAndConnect(url, dataChannelUrl)
5511
5809
  .then((registerAndConnectResult) => {
5512
5810
  // @ts-ignore - Fix type
5513
5811
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
@@ -5877,8 +6175,16 @@ export default class Meeting extends StatelessWebexPlugin {
5877
6175
  * @returns {undefined}
5878
6176
  */
5879
6177
  public roapMessageReceived = (roapMessage: RoapMessage) => {
5880
- const mediaServer = MeetingsUtil.getMediaServer(roapMessage.sdp);
5881
-
6178
+ const mediaServer =
6179
+ roapMessage.messageType === 'ANSWER'
6180
+ ? MeetingsUtil.getMediaServer(roapMessage.sdp)
6181
+ : undefined;
6182
+
6183
+ if (this.isMultistream && mediaServer && mediaServer !== 'homer') {
6184
+ throw new MultistreamNotSupportedError(
6185
+ `Client asked for multistream backend (Homer), but got ${mediaServer} instead`
6186
+ );
6187
+ }
5882
6188
  this.mediaProperties.webrtcMediaConnection.roapMessageReceived(roapMessage);
5883
6189
 
5884
6190
  if (mediaServer) {
@@ -6001,16 +6307,20 @@ export default class Meeting extends StatelessWebexPlugin {
6001
6307
  logText: `${LOG_HEADER} Roap Offer`,
6002
6308
  }
6003
6309
  ).catch((error) => {
6310
+ const multistreamNotSupported = error instanceof MultistreamNotSupportedError;
6311
+
6004
6312
  // @ts-ignore
6005
6313
  this.webex.internal.newMetrics.submitClientEvent({
6006
6314
  name: 'client.media-engine.remote-sdp-received',
6007
6315
  payload: {
6008
- canProceed: false,
6316
+ canProceed: multistreamNotSupported,
6009
6317
  errors: [
6010
6318
  // @ts-ignore
6011
6319
  this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
6012
6320
  {
6013
- clientErrorCode: CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6321
+ clientErrorCode: multistreamNotSupported
6322
+ ? CALL_DIAGNOSTIC_CONFIG.MULTISTREAM_NOT_AVAILABLE_CLIENT_CODE
6323
+ : CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6014
6324
  }
6015
6325
  ),
6016
6326
  ],
@@ -6018,7 +6328,7 @@ export default class Meeting extends StatelessWebexPlugin {
6018
6328
  options: {meetingId: this.id, rawError: error},
6019
6329
  });
6020
6330
 
6021
- this.deferSDPAnswer.reject(new Error('failed to send ROAP SDP offer'));
6331
+ this.deferSDPAnswer.reject(error);
6022
6332
  clearTimeout(this.sdpResponseTimer);
6023
6333
  this.sdpResponseTimer = undefined;
6024
6334
  });
@@ -6346,6 +6656,14 @@ export default class Meeting extends StatelessWebexPlugin {
6346
6656
  this.webex.meetings.geoHintInfo?.clientAddress ||
6347
6657
  options.data.intervalMetadata.peerReflexiveIP ||
6348
6658
  MQA_STATS.DEFAULT_IP;
6659
+
6660
+ const {members} = this.getMembers().membersCollection;
6661
+
6662
+ // Count members that are in the meeting
6663
+ options.data.intervalMetadata.meetingUserCount = Object.values(members).filter(
6664
+ (member: Member) => member.isInMeeting
6665
+ ).length;
6666
+
6349
6667
  // @ts-ignore
6350
6668
  this.webex.internal.newMetrics.submitMQE({
6351
6669
  name: 'client.mediaquality.event',
@@ -6477,6 +6795,9 @@ export default class Meeting extends StatelessWebexPlugin {
6477
6795
  new RtcMetrics(this.webex, {meetingId: this.id}, this.correlationId)
6478
6796
  : undefined;
6479
6797
 
6798
+ // ongoing reachability checks slow down new media connections especially on Firefox, so we stop them
6799
+ this.getWebexObject().meetings.reachability.stopReachability();
6800
+
6480
6801
  const mc = Media.createMediaConnection(
6481
6802
  this.isMultistream,
6482
6803
  this.getMediaConnectionDebugId(),
@@ -6677,32 +6998,6 @@ export default class Meeting extends StatelessWebexPlugin {
6677
6998
  }
6678
6999
  }
6679
7000
 
6680
- /**
6681
- * Handles device logging
6682
- *
6683
- * @private
6684
- * @static
6685
- * @param {boolean} isAudioEnabled
6686
- * @param {boolean} isVideoEnabled
6687
- * @returns {Promise<void>}
6688
- */
6689
-
6690
- private static async handleDeviceLogging(isAudioEnabled, isVideoEnabled): Promise<void> {
6691
- try {
6692
- let devices = [];
6693
- if (isVideoEnabled && isAudioEnabled) {
6694
- devices = await getDevices();
6695
- } else if (isVideoEnabled) {
6696
- devices = await getDevices(Media.DeviceKind.VIDEO_INPUT);
6697
- } else if (isAudioEnabled) {
6698
- devices = await getDevices(Media.DeviceKind.AUDIO_INPUT);
6699
- }
6700
- MeetingUtil.handleDeviceLogging(devices);
6701
- } catch {
6702
- // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection
6703
- }
6704
- }
6705
-
6706
7001
  /**
6707
7002
  * Returns a promise. This promise is created once the local sdp offer has been successfully created and is resolved
6708
7003
  * once the remote sdp answer has been received.
@@ -6926,7 +7221,9 @@ export default class Meeting extends StatelessWebexPlugin {
6926
7221
 
6927
7222
  const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
6928
7223
 
6929
- LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
7224
+ LoggerProxy.logger.info(
7225
+ `${LOG_HEADER} media connection created this.isMultistream=${this.isMultistream}`
7226
+ );
6930
7227
 
6931
7228
  if (this.isMultistream) {
6932
7229
  this.remoteMediaManager = new RemoteMediaManager(
@@ -7004,6 +7301,33 @@ export default class Meeting extends StatelessWebexPlugin {
7004
7301
  }
7005
7302
  }
7006
7303
 
7304
+ /**
7305
+ * Cleans up stats analyzer, peer connection and other things before
7306
+ * we can create a new transcoded media connection
7307
+ *
7308
+ * @private
7309
+ * @returns {Promise<void>}
7310
+ */
7311
+ private async downgradeFromMultistreamToTranscoded(): Promise<void> {
7312
+ if (this.statsAnalyzer) {
7313
+ await this.statsAnalyzer.stopAnalyzer();
7314
+ }
7315
+ this.statsAnalyzer = null;
7316
+
7317
+ this.isMultistream = false;
7318
+
7319
+ if (this.mediaProperties.webrtcMediaConnection) {
7320
+ // close peer connection, but don't reset mute state information, because we will want to use it on the retry
7321
+ this.closePeerConnections(false);
7322
+
7323
+ this.mediaProperties.unsetPeerConnection();
7324
+ }
7325
+
7326
+ this.locusMediaRequest?.downgradeFromMultistreamToTranscoded();
7327
+
7328
+ this.createStatsAnalyzer();
7329
+ }
7330
+
7007
7331
  /**
7008
7332
  * Sends stats report, closes peer connection and cleans up any media connection
7009
7333
  * related things before trying to establish media connection again with turn server
@@ -7190,6 +7514,7 @@ export default class Meeting extends StatelessWebexPlugin {
7190
7514
 
7191
7515
  this.audio = createMuteState(AUDIO, this, audioEnabled);
7192
7516
  this.video = createMuteState(VIDEO, this, videoEnabled);
7517
+ this.brbState = createBrbState(this, false);
7193
7518
 
7194
7519
  try {
7195
7520
  await this.setUpLocalStreamReferences(localStreams);
@@ -7198,19 +7523,36 @@ export default class Meeting extends StatelessWebexPlugin {
7198
7523
 
7199
7524
  this.createStatsAnalyzer();
7200
7525
 
7201
- await this.establishMediaConnection(
7202
- remoteMediaManagerConfig,
7203
- bundlePolicy,
7204
- forceTurnDiscovery,
7205
- turnServerInfo
7206
- );
7526
+ try {
7527
+ await this.establishMediaConnection(
7528
+ remoteMediaManagerConfig,
7529
+ bundlePolicy,
7530
+ forceTurnDiscovery,
7531
+ turnServerInfo
7532
+ );
7533
+ } catch (error) {
7534
+ if (error instanceof MultistreamNotSupportedError) {
7535
+ LoggerProxy.logger.warn(
7536
+ `${LOG_HEADER} we asked for multistream backend (Homer), but got transcoded backend, recreating media connection...`
7537
+ );
7207
7538
 
7208
- if (audioEnabled || videoEnabled) {
7209
- await Meeting.handleDeviceLogging(audioEnabled, videoEnabled);
7210
- } else {
7211
- LoggerProxy.logger.info(`${LOG_HEADER} device logging not required`);
7539
+ await this.downgradeFromMultistreamToTranscoded();
7540
+
7541
+ // Establish new media connection with forced TURN discovery
7542
+ // We need to do TURN discovery again, because backend will be creating a new confluence, so it might land on a different node or cluster
7543
+ await this.establishMediaConnection(
7544
+ remoteMediaManagerConfig,
7545
+ bundlePolicy,
7546
+ true,
7547
+ undefined
7548
+ );
7549
+ } else {
7550
+ throw error;
7551
+ }
7212
7552
  }
7213
7553
 
7554
+ LoggerProxy.logger.info(`${LOG_HEADER} media connected, finalizing...`);
7555
+
7214
7556
  if (this.mediaProperties.hasLocalShareStream()) {
7215
7557
  await this.enqueueScreenShareFloorRequest();
7216
7558
  }
@@ -7247,6 +7589,7 @@ export default class Meeting extends StatelessWebexPlugin {
7247
7589
 
7248
7590
  // We can log ReceiveSlot SSRCs only after the SDP exchange, so doing it here:
7249
7591
  this.remoteMediaManager?.logAllReceiveSlots();
7592
+ this.startPeriodicLogUpload();
7250
7593
  } catch (error) {
7251
7594
  LoggerProxy.logger.error(`${LOG_HEADER} failed to establish media connection: `, error);
7252
7595
 
@@ -8179,7 +8522,7 @@ export default class Meeting extends StatelessWebexPlugin {
8179
8522
  if (layoutType) {
8180
8523
  if (!LAYOUT_TYPES.includes(layoutType)) {
8181
8524
  return this.rejectWithErrorLog(
8182
- 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType received.'
8525
+ `Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType "${layoutType}" received.`
8183
8526
  );
8184
8527
  }
8185
8528
 
@@ -8335,6 +8678,12 @@ export default class Meeting extends StatelessWebexPlugin {
8335
8678
  correlationId: this.correlationId,
8336
8679
  muted,
8337
8680
  encoderImplementation: this.statsAnalyzer?.shareVideoEncoderImplementation,
8681
+ // TypeScript 4 does not recognize the `displaySurface` property. Instead of upgrading the
8682
+ // SDK to TypeScript 5, which may affect other packages, use bracket notation for now, since
8683
+ // all we're doing here is adding metrics.
8684
+ // eslint-disable-next-line dot-notation
8685
+ displaySurface: this.mediaProperties?.shareVideoStream?.getSettings()['displaySurface'],
8686
+ isMultistream: this.isMultistream,
8338
8687
  });
8339
8688
  };
8340
8689
 
@@ -8537,6 +8886,11 @@ export default class Meeting extends StatelessWebexPlugin {
8537
8886
  this.stopTranscription();
8538
8887
  this.transcription = undefined;
8539
8888
  }
8889
+
8890
+ this.annotation.deregisterEvents();
8891
+
8892
+ // @ts-ignore - fix types
8893
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
8540
8894
  };
8541
8895
 
8542
8896
  /**
@@ -8574,10 +8928,12 @@ export default class Meeting extends StatelessWebexPlugin {
8574
8928
 
8575
8929
  return;
8576
8930
  }
8577
- const {keepAliveUrl} = this.joinedWith;
8931
+
8578
8932
  const keepAliveInterval = (this.joinedWith.keepAliveSecs - 1) * 750; // taken from UCF
8579
8933
 
8580
8934
  this.keepAliveTimerId = setInterval(() => {
8935
+ const {keepAliveUrl} = this.joinedWith;
8936
+
8581
8937
  this.meetingRequest.keepAlive({keepAliveUrl}).catch((error) => {
8582
8938
  LoggerProxy.logger.warn(
8583
8939
  `Meeting:index#startKeepAlive --> Stopping sending keepAlives to ${keepAliveUrl} after error ${error}`