@webex/plugin-meetings 3.7.0 → 3.8.0-next.10

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 (206) hide show
  1. package/dist/annotation/index.js +17 -0
  2. package/dist/annotation/index.js.map +1 -1
  3. package/dist/breakouts/breakout.js +1 -1
  4. package/dist/breakouts/index.js +1 -1
  5. package/dist/common/errors/join-forbidden-error.js +52 -0
  6. package/dist/common/errors/join-forbidden-error.js.map +1 -0
  7. package/dist/common/errors/{webinar-registration-error.js → join-webinar-error.js} +12 -12
  8. package/dist/common/errors/join-webinar-error.js.map +1 -0
  9. package/dist/common/errors/multistream-not-supported-error.js +53 -0
  10. package/dist/common/errors/multistream-not-supported-error.js.map +1 -0
  11. package/dist/config.js +3 -1
  12. package/dist/config.js.map +1 -1
  13. package/dist/constants.js +69 -6
  14. package/dist/constants.js.map +1 -1
  15. package/dist/index.js +16 -11
  16. package/dist/index.js.map +1 -1
  17. package/dist/interpretation/index.js +4 -4
  18. package/dist/interpretation/index.js.map +1 -1
  19. package/dist/interpretation/siLanguage.js +1 -1
  20. package/dist/locus-info/index.js +14 -3
  21. package/dist/locus-info/index.js.map +1 -1
  22. package/dist/locus-info/selfUtils.js +35 -17
  23. package/dist/locus-info/selfUtils.js.map +1 -1
  24. package/dist/media/MediaConnectionAwaiter.js +1 -0
  25. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  26. package/dist/media/properties.js +30 -16
  27. package/dist/media/properties.js.map +1 -1
  28. package/dist/meeting/brbState.js +167 -0
  29. package/dist/meeting/brbState.js.map +1 -0
  30. package/dist/meeting/in-meeting-actions.js +13 -1
  31. package/dist/meeting/in-meeting-actions.js.map +1 -1
  32. package/dist/meeting/index.js +1373 -1052
  33. package/dist/meeting/index.js.map +1 -1
  34. package/dist/meeting/locusMediaRequest.js +32 -11
  35. package/dist/meeting/locusMediaRequest.js.map +1 -1
  36. package/dist/meeting/muteState.js +1 -6
  37. package/dist/meeting/muteState.js.map +1 -1
  38. package/dist/meeting/request.js +51 -29
  39. package/dist/meeting/request.js.map +1 -1
  40. package/dist/meeting/request.type.js.map +1 -1
  41. package/dist/meeting/util.js +103 -67
  42. package/dist/meeting/util.js.map +1 -1
  43. package/dist/meeting-info/meeting-info-v2.js +115 -45
  44. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  45. package/dist/meeting-info/utilv2.js +6 -2
  46. package/dist/meeting-info/utilv2.js.map +1 -1
  47. package/dist/meetings/index.js +107 -55
  48. package/dist/meetings/index.js.map +1 -1
  49. package/dist/meetings/meetings.types.js +2 -0
  50. package/dist/meetings/meetings.types.js.map +1 -1
  51. package/dist/meetings/util.js +1 -1
  52. package/dist/meetings/util.js.map +1 -1
  53. package/dist/member/index.js +9 -0
  54. package/dist/member/index.js.map +1 -1
  55. package/dist/member/types.js.map +1 -1
  56. package/dist/member/util.js +39 -28
  57. package/dist/member/util.js.map +1 -1
  58. package/dist/members/util.js +4 -2
  59. package/dist/members/util.js.map +1 -1
  60. package/dist/metrics/constants.js +6 -1
  61. package/dist/metrics/constants.js.map +1 -1
  62. package/dist/multistream/remoteMedia.js +30 -15
  63. package/dist/multistream/remoteMedia.js.map +1 -1
  64. package/dist/multistream/remoteMediaManager.js +40 -8
  65. package/dist/multistream/remoteMediaManager.js.map +1 -1
  66. package/dist/multistream/sendSlotManager.js +24 -0
  67. package/dist/multistream/sendSlotManager.js.map +1 -1
  68. package/dist/reachability/clusterReachability.js +12 -15
  69. package/dist/reachability/clusterReachability.js.map +1 -1
  70. package/dist/reachability/index.js +471 -140
  71. package/dist/reachability/index.js.map +1 -1
  72. package/dist/{rtcMetrics/constants.js → reachability/reachability.types.js} +1 -5
  73. package/dist/reachability/reachability.types.js.map +1 -0
  74. package/dist/reachability/request.js +21 -8
  75. package/dist/reachability/request.js.map +1 -1
  76. package/dist/recording-controller/enums.js +8 -4
  77. package/dist/recording-controller/enums.js.map +1 -1
  78. package/dist/recording-controller/index.js +18 -9
  79. package/dist/recording-controller/index.js.map +1 -1
  80. package/dist/recording-controller/util.js +13 -9
  81. package/dist/recording-controller/util.js.map +1 -1
  82. package/dist/roap/index.js +15 -15
  83. package/dist/roap/index.js.map +1 -1
  84. package/dist/roap/request.js +45 -79
  85. package/dist/roap/request.js.map +1 -1
  86. package/dist/roap/turnDiscovery.js +3 -6
  87. package/dist/roap/turnDiscovery.js.map +1 -1
  88. package/dist/types/annotation/index.d.ts +5 -0
  89. package/dist/types/common/errors/join-forbidden-error.d.ts +15 -0
  90. package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
  91. package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
  92. package/dist/types/config.d.ts +2 -0
  93. package/dist/types/constants.d.ts +54 -1
  94. package/dist/types/index.d.ts +3 -3
  95. package/dist/types/locus-info/index.d.ts +2 -1
  96. package/dist/types/meeting/brbState.d.ts +54 -0
  97. package/dist/types/meeting/in-meeting-actions.d.ts +12 -0
  98. package/dist/types/meeting/index.d.ts +86 -14
  99. package/dist/types/meeting/locusMediaRequest.d.ts +6 -3
  100. package/dist/types/meeting/request.d.ts +14 -3
  101. package/dist/types/meeting/request.type.d.ts +6 -0
  102. package/dist/types/meeting/util.d.ts +3 -3
  103. package/dist/types/meeting-info/meeting-info-v2.d.ts +30 -5
  104. package/dist/types/meetings/index.d.ts +20 -2
  105. package/dist/types/meetings/meetings.types.d.ts +8 -0
  106. package/dist/types/member/index.d.ts +1 -0
  107. package/dist/types/member/types.d.ts +7 -0
  108. package/dist/types/members/util.d.ts +2 -0
  109. package/dist/types/metrics/constants.d.ts +6 -1
  110. package/dist/types/multistream/remoteMediaManager.d.ts +10 -1
  111. package/dist/types/multistream/sendSlotManager.d.ts +8 -1
  112. package/dist/types/reachability/clusterReachability.d.ts +1 -10
  113. package/dist/types/reachability/index.d.ts +83 -36
  114. package/dist/types/reachability/reachability.types.d.ts +64 -0
  115. package/dist/types/reachability/request.d.ts +5 -1
  116. package/dist/types/recording-controller/enums.d.ts +5 -2
  117. package/dist/types/recording-controller/index.d.ts +1 -0
  118. package/dist/types/recording-controller/util.d.ts +2 -1
  119. package/dist/types/roap/request.d.ts +1 -13
  120. package/dist/webinar/index.js +390 -7
  121. package/dist/webinar/index.js.map +1 -1
  122. package/package.json +23 -22
  123. package/src/annotation/index.ts +16 -0
  124. package/src/common/errors/join-forbidden-error.ts +26 -0
  125. package/src/common/errors/join-webinar-error.ts +24 -0
  126. package/src/common/errors/multistream-not-supported-error.ts +30 -0
  127. package/src/config.ts +2 -0
  128. package/src/constants.ts +62 -3
  129. package/src/index.ts +5 -3
  130. package/src/interpretation/index.ts +3 -3
  131. package/src/locus-info/index.ts +20 -3
  132. package/src/locus-info/selfUtils.ts +24 -6
  133. package/src/media/MediaConnectionAwaiter.ts +2 -0
  134. package/src/media/properties.ts +34 -13
  135. package/src/meeting/brbState.ts +169 -0
  136. package/src/meeting/in-meeting-actions.ts +25 -0
  137. package/src/meeting/index.ts +485 -88
  138. package/src/meeting/locusMediaRequest.ts +38 -12
  139. package/src/meeting/muteState.ts +1 -6
  140. package/src/meeting/request.ts +30 -12
  141. package/src/meeting/request.type.ts +7 -0
  142. package/src/meeting/util.ts +32 -13
  143. package/src/meeting-info/meeting-info-v2.ts +83 -12
  144. package/src/meeting-info/utilv2.ts +17 -3
  145. package/src/meetings/index.ts +79 -20
  146. package/src/meetings/meetings.types.ts +10 -0
  147. package/src/meetings/util.ts +2 -1
  148. package/src/member/index.ts +9 -0
  149. package/src/member/types.ts +8 -0
  150. package/src/member/util.ts +34 -24
  151. package/src/members/util.ts +1 -0
  152. package/src/metrics/constants.ts +6 -1
  153. package/src/multistream/remoteMedia.ts +28 -15
  154. package/src/multistream/remoteMediaManager.ts +32 -10
  155. package/src/multistream/sendSlotManager.ts +31 -0
  156. package/src/reachability/clusterReachability.ts +5 -15
  157. package/src/reachability/index.ts +315 -75
  158. package/src/reachability/reachability.types.ts +85 -0
  159. package/src/reachability/request.ts +55 -31
  160. package/src/recording-controller/enums.ts +5 -2
  161. package/src/recording-controller/index.ts +17 -4
  162. package/src/recording-controller/util.ts +28 -9
  163. package/src/roap/index.ts +14 -13
  164. package/src/roap/request.ts +30 -44
  165. package/src/roap/turnDiscovery.ts +2 -4
  166. package/src/webinar/index.ts +235 -9
  167. package/test/unit/spec/annotation/index.ts +46 -1
  168. package/test/unit/spec/interpretation/index.ts +39 -1
  169. package/test/unit/spec/locus-info/index.js +292 -60
  170. package/test/unit/spec/locus-info/selfConstant.js +7 -0
  171. package/test/unit/spec/locus-info/selfUtils.js +101 -1
  172. package/test/unit/spec/media/properties.ts +15 -0
  173. package/test/unit/spec/meeting/brbState.ts +114 -0
  174. package/test/unit/spec/meeting/in-meeting-actions.ts +15 -1
  175. package/test/unit/spec/meeting/index.js +908 -124
  176. package/test/unit/spec/meeting/locusMediaRequest.ts +111 -66
  177. package/test/unit/spec/meeting/muteState.js +0 -24
  178. package/test/unit/spec/meeting/request.js +3 -26
  179. package/test/unit/spec/meeting/utils.js +73 -28
  180. package/test/unit/spec/meeting-info/meetinginfov2.js +46 -4
  181. package/test/unit/spec/meeting-info/utilv2.js +26 -0
  182. package/test/unit/spec/meetings/index.js +172 -18
  183. package/test/unit/spec/meetings/utils.js +10 -0
  184. package/test/unit/spec/member/util.js +52 -11
  185. package/test/unit/spec/members/utils.js +95 -0
  186. package/test/unit/spec/multistream/remoteMedia.ts +11 -7
  187. package/test/unit/spec/multistream/remoteMediaManager.ts +397 -118
  188. package/test/unit/spec/reachability/clusterReachability.ts +7 -0
  189. package/test/unit/spec/reachability/index.ts +391 -9
  190. package/test/unit/spec/reachability/request.js +48 -12
  191. package/test/unit/spec/recording-controller/index.js +61 -5
  192. package/test/unit/spec/recording-controller/util.js +39 -3
  193. package/test/unit/spec/roap/index.ts +48 -1
  194. package/test/unit/spec/roap/request.ts +51 -109
  195. package/test/unit/spec/roap/turnDiscovery.ts +202 -147
  196. package/test/unit/spec/webinar/index.ts +509 -0
  197. package/dist/common/errors/webinar-registration-error.js.map +0 -1
  198. package/dist/networkQualityMonitor/index.js +0 -227
  199. package/dist/networkQualityMonitor/index.js.map +0 -1
  200. package/dist/rtcMetrics/constants.js.map +0 -1
  201. package/dist/rtcMetrics/index.js +0 -197
  202. package/dist/rtcMetrics/index.js.map +0 -1
  203. package/dist/types/networkQualityMonitor/index.d.ts +0 -70
  204. package/dist/types/rtcMetrics/constants.d.ts +0 -4
  205. package/dist/types/rtcMetrics/index.d.ts +0 -71
  206. 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';
@@ -232,6 +241,8 @@ export type CallStateForMetrics = {
232
241
  sessionCorrelationId?: string;
233
242
  joinTrigger?: string;
234
243
  loginType?: string;
244
+ userNameInput?: string;
245
+ emailInput?: string;
235
246
  };
236
247
 
237
248
  export const MEDIA_UPDATE_TYPE = {
@@ -248,6 +259,7 @@ export enum ScreenShareFloorStatus {
248
259
 
249
260
  type FetchMeetingInfoParams = {
250
261
  password?: string;
262
+ registrationId?: string;
251
263
  captchaCode?: string;
252
264
  extraParams?: Record<string, any>;
253
265
  sendCAevents?: boolean;
@@ -642,6 +654,8 @@ export default class Meeting extends StatelessWebexPlugin {
642
654
  turnServerUsed: boolean;
643
655
  areVoiceaEventsSetup = false;
644
656
  isMoveToInProgress = false;
657
+ registrationIdStatus: string;
658
+ brbState: BrbState;
645
659
 
646
660
  voiceaListenerCallbacks: object = {
647
661
  [VOICEAEVENTS.VOICEA_ANNOUNCEMENT]: (payload: Transcription['languageOptions']) => {
@@ -702,6 +716,8 @@ export default class Meeting extends StatelessWebexPlugin {
702
716
  private iceCandidateErrors: Map<string, number>;
703
717
  private iceCandidatesCount: number;
704
718
  private rtcMetrics?: RtcMetrics;
719
+ private uploadLogsTimer?: ReturnType<typeof setTimeout>;
720
+ private logUploadIntervalIndex: number;
705
721
 
706
722
  /**
707
723
  * @param {Object} attrs
@@ -770,6 +786,8 @@ export default class Meeting extends StatelessWebexPlugin {
770
786
  );
771
787
  this.callStateForMetrics.correlationId = this.id;
772
788
  }
789
+ this.logUploadIntervalIndex = 0;
790
+
773
791
  /**
774
792
  * @instance
775
793
  * @type {String}
@@ -843,7 +861,7 @@ export default class Meeting extends StatelessWebexPlugin {
843
861
  * @memberof Meeting
844
862
  */
845
863
  // @ts-ignore
846
- this.webinar = new Webinar({}, {parent: this.webex});
864
+ this.webinar = new Webinar({meetingId: this.id}, {parent: this.webex});
847
865
  /**
848
866
  * helper class for managing receive slots (for multistream media connections)
849
867
  */
@@ -1334,6 +1352,16 @@ export default class Meeting extends StatelessWebexPlugin {
1334
1352
  */
1335
1353
  this.passwordStatus = PASSWORD_STATUS.UNKNOWN;
1336
1354
 
1355
+ /**
1356
+ * registrationId status. If it's REGISTRATIONID_STATUS.REQUIRED then verifyRegistrationId() needs to be called
1357
+ * with the correct registrationId before calling join()
1358
+ * @instance
1359
+ * @type {REGISTRATION_ID_STATUS}
1360
+ * @public
1361
+ * @memberof Meeting
1362
+ */
1363
+ this.registrationIdStatus = REGISTRATION_ID_STATUS.UNKNOWN;
1364
+
1337
1365
  /**
1338
1366
  * 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
1367
  * with the correct password before calling join()
@@ -1601,6 +1629,38 @@ export default class Meeting extends StatelessWebexPlugin {
1601
1629
  this.callStateForMetrics.correlationId = correlationId;
1602
1630
  }
1603
1631
 
1632
+ /**
1633
+ * Getter - Returns callStateForMetrics.userNameInput
1634
+ * @returns {string}
1635
+ */
1636
+ get userNameInput() {
1637
+ return this.callStateForMetrics?.userNameInput;
1638
+ }
1639
+
1640
+ /**
1641
+ * Setter - sets callStateForMetrics.userNameInput
1642
+ * @param {string} userNameInput
1643
+ */
1644
+ set userNameInput(userNameInput: string) {
1645
+ this.callStateForMetrics.userNameInput = userNameInput;
1646
+ }
1647
+
1648
+ /**
1649
+ * Getter - Returns callStateForMetrics.emailInput
1650
+ * @returns {string}
1651
+ */
1652
+ get emailInput() {
1653
+ return this.callStateForMetrics?.emailInput;
1654
+ }
1655
+
1656
+ /**
1657
+ * Setter - sets callStateForMetrics.emailInput
1658
+ * @param {string} emailInput
1659
+ */
1660
+ set emailInput(emailInput: string) {
1661
+ this.callStateForMetrics.emailInput = emailInput;
1662
+ }
1663
+
1604
1664
  /**
1605
1665
  * Getter - Returns callStateForMetrics.sessionCorrelationId
1606
1666
  * @returns {string}
@@ -1646,6 +1706,15 @@ export default class Meeting extends StatelessWebexPlugin {
1646
1706
  this.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
1647
1707
  }
1648
1708
 
1709
+ if (
1710
+ this.registrationIdStatus === REGISTRATION_ID_STATUS.REQUIRED ||
1711
+ this.registrationIdStatus === REGISTRATION_ID_STATUS.VERIFIED
1712
+ ) {
1713
+ this.registrationIdStatus = REGISTRATION_ID_STATUS.VERIFIED;
1714
+ } else {
1715
+ this.registrationIdStatus = REGISTRATION_ID_STATUS.NOT_REQUIRED;
1716
+ }
1717
+
1649
1718
  Trigger.trigger(
1650
1719
  this,
1651
1720
  {
@@ -1689,7 +1758,12 @@ export default class Meeting extends StatelessWebexPlugin {
1689
1758
  * @private
1690
1759
  */
1691
1760
  private prepForFetchMeetingInfo(
1692
- {password = null, captchaCode = null, extraParams = {}}: FetchMeetingInfoParams,
1761
+ {
1762
+ password = null,
1763
+ registrationId = null,
1764
+ captchaCode = null,
1765
+ extraParams = {},
1766
+ }: FetchMeetingInfoParams,
1693
1767
  caller: string
1694
1768
  ): Promise<void> {
1695
1769
  // 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 +1803,7 @@ export default class Meeting extends StatelessWebexPlugin {
1729
1803
  captchaCode = null,
1730
1804
  extraParams = {},
1731
1805
  sendCAevents = false,
1806
+ registrationId = null,
1732
1807
  }): Promise<void> {
1733
1808
  try {
1734
1809
  const captchaInfo = captchaCode
@@ -1744,7 +1819,8 @@ export default class Meeting extends StatelessWebexPlugin {
1744
1819
  this.config.installedOrgID,
1745
1820
  this.locusId,
1746
1821
  extraParams,
1747
- {meetingId: this.id, sendCAevents}
1822
+ {meetingId: this.id, sendCAevents},
1823
+ registrationId
1748
1824
  );
1749
1825
 
1750
1826
  this.parseMeetingInfo(info?.body, this.destination, info?.errors);
@@ -1762,15 +1838,35 @@ export default class Meeting extends StatelessWebexPlugin {
1762
1838
  this.meetingInfo = err.meetingInfo;
1763
1839
  }
1764
1840
  throw new PermissionError();
1765
- } else if (err instanceof MeetingInfoV2WebinarRegistrationError) {
1841
+ } else if (err instanceof MeetingInfoV2JoinWebinarError) {
1766
1842
  this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WEBINAR_REGISTRATION;
1843
+ if (WEBINAR_ERROR_WEBCAST.includes(err.wbxAppApiCode)) {
1844
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NEED_JOIN_WITH_WEBCAST;
1845
+ } else if (WEBINAR_ERROR_REGISTRATION_ID.includes(err.wbxAppApiCode)) {
1846
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WEBINAR_NEED_REGISTRATION_ID;
1847
+ }
1767
1848
  this.meetingInfoFailureCode = err.wbxAppApiCode;
1768
1849
 
1769
1850
  if (err.meetingInfo) {
1770
1851
  this.meetingInfo = err.meetingInfo;
1771
1852
  }
1853
+ this.requiredCaptcha = null;
1772
1854
 
1773
- throw new WebinarRegistrationError();
1855
+ throw new JoinWebinarError();
1856
+ } else if (err instanceof MeetingInfoV2JoinForbiddenError) {
1857
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.JOIN_FORBIDDEN;
1858
+ this.meetingInfoFailureCode = err.wbxAppApiCode;
1859
+
1860
+ if (err.meetingInfo) {
1861
+ this.meetingInfo = err.meetingInfo;
1862
+ }
1863
+
1864
+ // Handle the case where user hasn't reached Join Before Host (JBH) time (error code 403003)
1865
+ if (JOIN_BEFORE_HOST === err.wbxAppApiCode) {
1866
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NOT_REACH_JBH;
1867
+ }
1868
+
1869
+ throw new JoinForbiddenError(this.meetingInfoFailureReason, err);
1774
1870
  } else if (err instanceof MeetingInfoV2PasswordError) {
1775
1871
  LoggerProxy.logger.info(
1776
1872
  // @ts-ignore
@@ -1799,9 +1895,13 @@ export default class Meeting extends StatelessWebexPlugin {
1799
1895
  `Meeting:index#fetchMeetingInfo --> Info Unable to fetch meeting info for ${this.destination} - captcha required (code=${err?.body?.code}).`
1800
1896
  );
1801
1897
 
1802
- this.meetingInfoFailureReason = this.requiredCaptcha
1803
- ? MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA
1804
- : MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD;
1898
+ if (this.requiredCaptcha) {
1899
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA;
1900
+ } else if (err.isRegistrationIdRequired) {
1901
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_REGISTRATION_ID;
1902
+ } else {
1903
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD;
1904
+ }
1805
1905
 
1806
1906
  this.meetingInfoFailureCode = err.wbxAppApiCode;
1807
1907
 
@@ -1809,6 +1909,10 @@ export default class Meeting extends StatelessWebexPlugin {
1809
1909
  this.passwordStatus = PASSWORD_STATUS.REQUIRED;
1810
1910
  }
1811
1911
 
1912
+ if (err.isRegistrationIdRequired) {
1913
+ this.registrationIdStatus = REGISTRATION_ID_STATUS.REQUIRED;
1914
+ }
1915
+
1812
1916
  this.requiredCaptcha = err.captchaInfo;
1813
1917
  throw new CaptchaError();
1814
1918
  } else {
@@ -1949,6 +2053,48 @@ export default class Meeting extends StatelessWebexPlugin {
1949
2053
  });
1950
2054
  }
1951
2055
 
2056
+ /**
2057
+ * Checks if the supplied registrationId is correct. It returns a promise with information whether the
2058
+ * registrationId and captcha code were correct or not.
2059
+ * @param {String | undefined} registrationId - can be undefined if only captcha was required
2060
+ * @param {String | undefined} captchaCode - can be undefined if captcha was not required by the server
2061
+ * @param {Boolean} sendCAevents - whether Call Analyzer events should be sent when fetching meeting information
2062
+ * @public
2063
+ * @memberof Meeting
2064
+ * @returns {Promise<{isRegistrationIdValid: boolean, requiredCaptcha: boolean, failureReason: MEETING_INFO_FAILURE_REASON}>}
2065
+ */
2066
+ public verifyRegistrationId(registrationId: string, captchaCode: string, sendCAevents = false) {
2067
+ return this.fetchMeetingInfo({
2068
+ registrationId,
2069
+ captchaCode,
2070
+ sendCAevents,
2071
+ })
2072
+ .then(() => {
2073
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.VERIFY_REGISTRATION_ID_SUCCESS);
2074
+
2075
+ return {
2076
+ isRegistrationIdValid: true,
2077
+ requiredCaptcha: null,
2078
+ failureReason: MEETING_INFO_FAILURE_REASON.NONE,
2079
+ };
2080
+ })
2081
+ .catch((error) => {
2082
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.VERIFY_REGISTRATION_ID_ERROR);
2083
+
2084
+ if (error instanceof JoinWebinarError || error instanceof CaptchaError) {
2085
+ return {
2086
+ isRegistrationIdValid: this.registrationIdStatus === REGISTRATION_ID_STATUS.VERIFIED,
2087
+ requiredCaptcha: this.requiredCaptcha,
2088
+ failureReason:
2089
+ error instanceof JoinWebinarError
2090
+ ? MEETING_INFO_FAILURE_REASON.WRONG_REGISTRATION_ID
2091
+ : this.meetingInfoFailureReason,
2092
+ };
2093
+ }
2094
+ throw error;
2095
+ });
2096
+ }
2097
+
1952
2098
  /**
1953
2099
  * Refreshes the captcha. As a result the meeting will have new captcha id, image and audio.
1954
2100
  * If the refresh operation fails, meeting remains with the old captcha properties.
@@ -2655,6 +2801,7 @@ export default class Meeting extends StatelessWebexPlugin {
2655
2801
  });
2656
2802
 
2657
2803
  this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_PRACTICE_SESSION_STATUS_UPDATED, ({state}) => {
2804
+ this.webinar.updatePracticeSessionStatus(state);
2658
2805
  Trigger.trigger(
2659
2806
  this,
2660
2807
  {file: 'meeting/index', function: 'setupLocusControlsListener'},
@@ -2728,6 +2875,7 @@ export default class Meeting extends StatelessWebexPlugin {
2728
2875
  this.triggerAnnotationInfoEvent(contentShare, previousContentShare);
2729
2876
 
2730
2877
  if (
2878
+ !payload.forceUpdate &&
2731
2879
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
2732
2880
  contentShare.disposition === previousContentShare?.disposition &&
2733
2881
  contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
@@ -2774,7 +2922,11 @@ export default class Meeting extends StatelessWebexPlugin {
2774
2922
  // It does not matter who requested to share the whiteboard, everyone gets the same view
2775
2923
  else if (whiteboardShare.disposition === FLOOR_ACTION.GRANTED) {
2776
2924
  // WHITEBOARD - sharing whiteboard
2777
- newShareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
2925
+ // Webinar attendee should receive whiteboard as remote share
2926
+ newShareStatus =
2927
+ this.locusInfo?.info?.isWebinar && this.webinar?.selfIsAttendee
2928
+ ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
2929
+ : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
2778
2930
  }
2779
2931
  // or if content share is either released or null and whiteboard share is either released or null, no one is sharing
2780
2932
  else if (
@@ -2789,6 +2941,7 @@ export default class Meeting extends StatelessWebexPlugin {
2789
2941
  LoggerProxy.logger.info(
2790
2942
  `Meeting:index#setUpLocusInfoMediaInactiveListener --> this.shareStatus=${this.shareStatus} newShareStatus=${newShareStatus}`
2791
2943
  );
2944
+
2792
2945
  if (newShareStatus !== this.shareStatus) {
2793
2946
  const oldShareStatus = this.shareStatus;
2794
2947
 
@@ -3046,7 +3199,20 @@ export default class Meeting extends StatelessWebexPlugin {
3046
3199
  */
3047
3200
  private setUpLocusResourcesListener() {
3048
3201
  this.locusInfo.on(LOCUSINFO.EVENTS.LINKS_RESOURCES, (payload) => {
3049
- this.webinar.updateWebcastUrl(payload);
3202
+ if (payload) {
3203
+ this.webinar.updateWebcastUrl(payload);
3204
+ Trigger.trigger(
3205
+ this,
3206
+ {
3207
+ file: 'meeting/index',
3208
+ function: 'setUpLocusInfoMeetingInfoListener',
3209
+ },
3210
+ EVENT_TRIGGERS.MEETING_RESOURCE_LINKS_UPDATE,
3211
+ {
3212
+ payload,
3213
+ }
3214
+ );
3215
+ }
3050
3216
  });
3051
3217
  }
3052
3218
 
@@ -3249,6 +3415,9 @@ export default class Meeting extends StatelessWebexPlugin {
3249
3415
  options: {meetingId: this.id},
3250
3416
  });
3251
3417
  }
3418
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.GUEST_ENTERED_LOBBY, {
3419
+ correlation_id: this.correlationId,
3420
+ });
3252
3421
  this.updateLLMConnection();
3253
3422
  });
3254
3423
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, async (payload) => {
@@ -3272,6 +3441,9 @@ export default class Meeting extends StatelessWebexPlugin {
3272
3441
  name: 'client.lobby.exited',
3273
3442
  options: {meetingId: this.id},
3274
3443
  });
3444
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.GUEST_EXITED_LOBBY, {
3445
+ correlation_id: this.correlationId,
3446
+ });
3275
3447
  }
3276
3448
  this.rtcMetrics?.sendNextMetrics();
3277
3449
  this.updateLLMConnection();
@@ -3293,6 +3465,10 @@ export default class Meeting extends StatelessWebexPlugin {
3293
3465
  // The second on is if the audio is muted, we need to tell the statsAnalyzer when
3294
3466
  // the audio is muted or the user is not willing to send media
3295
3467
  this.locusInfo.on(LOCUSINFO.EVENTS.MEDIA_STATUS_CHANGE, (status) => {
3468
+ LoggerProxy.logger.info(
3469
+ 'Meeting:index#setUpLocusInfoSelfListener --> MEDIA_STATUS_CHANGE received, processing...'
3470
+ );
3471
+
3296
3472
  if (this.statsAnalyzer) {
3297
3473
  this.statsAnalyzer.updateMediaStatus({
3298
3474
  actual: status,
@@ -3306,6 +3482,10 @@ export default class Meeting extends StatelessWebexPlugin {
3306
3482
  receiveShare: this.mediaProperties.mediaDirection?.receiveShare,
3307
3483
  },
3308
3484
  });
3485
+ } else {
3486
+ LoggerProxy.logger.warn(
3487
+ 'Meeting:index#setUpLocusInfoSelfListener --> MEDIA_STATUS_CHANGE, statsAnalyzer is not available.'
3488
+ );
3309
3489
  }
3310
3490
  });
3311
3491
 
@@ -3350,6 +3530,21 @@ export default class Meeting extends StatelessWebexPlugin {
3350
3530
  }
3351
3531
  });
3352
3532
 
3533
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED, (payload) => {
3534
+ this.brbState?.handleServerBrbUpdate(payload?.brb?.enabled);
3535
+ Trigger.trigger(
3536
+ this,
3537
+ {
3538
+ file: 'meeting/index',
3539
+ function: 'setUpLocusInfoSelfListener',
3540
+ },
3541
+ EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
3542
+ {
3543
+ payload,
3544
+ }
3545
+ );
3546
+ });
3547
+
3353
3548
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ROLES_CHANGED, (payload) => {
3354
3549
  const isModeratorOrCohost =
3355
3550
  payload.newRoles?.includes(SELF_ROLES.MODERATOR) ||
@@ -3359,6 +3554,7 @@ export default class Meeting extends StatelessWebexPlugin {
3359
3554
  payload.newRoles?.includes(SELF_ROLES.MODERATOR)
3360
3555
  );
3361
3556
  this.webinar.updateRoleChanged(payload);
3557
+
3362
3558
  Trigger.trigger(
3363
3559
  this,
3364
3560
  {
@@ -3505,6 +3701,7 @@ export default class Meeting extends StatelessWebexPlugin {
3505
3701
  emailAddress: string;
3506
3702
  email: string;
3507
3703
  phoneNumber: string;
3704
+ roles: Array<string>;
3508
3705
  },
3509
3706
  alertIfActive = true
3510
3707
  ) {
@@ -3552,6 +3749,35 @@ export default class Meeting extends StatelessWebexPlugin {
3552
3749
  return this.members.admitMembers(memberIds, locusUrls);
3553
3750
  }
3554
3751
 
3752
+ /**
3753
+ * Manages be right back status updates for the current participant.
3754
+ *
3755
+ * @param {boolean} enabled - Indicates whether the user enabled brb or not.
3756
+ * @returns {Promise<void>} resolves when the brb status is updated or does nothing if not in a multistream meeting.
3757
+ * @throws {Error} - Throws an error if the request fails.
3758
+ */
3759
+ public async beRightBack(enabled: boolean): Promise<void> {
3760
+ if (!this.isMultistream) {
3761
+ const errorMessage = 'Meeting:index#beRightBack --> Not a multistream meeting';
3762
+ const error = new Error(errorMessage);
3763
+
3764
+ LoggerProxy.logger.error(error);
3765
+
3766
+ return Promise.reject(error);
3767
+ }
3768
+
3769
+ if (!this.mediaProperties.webrtcMediaConnection) {
3770
+ const errorMessage = 'Meeting:index#beRightBack --> WebRTC media connection is not defined';
3771
+ const error = new Error(errorMessage);
3772
+
3773
+ LoggerProxy.logger.error(error);
3774
+
3775
+ return Promise.reject(error);
3776
+ }
3777
+
3778
+ return this.brbState.enable(enabled, this.sendSlotManager);
3779
+ }
3780
+
3555
3781
  /**
3556
3782
  * Remove the member from the meeting, boot them
3557
3783
  * @param {String} memberId
@@ -3761,6 +3987,10 @@ export default class Meeting extends StatelessWebexPlugin {
3761
3987
  this.userDisplayHints,
3762
3988
  this.selfUserPolicies
3763
3989
  ),
3990
+ isPremiseRecordingEnabled: RecordingUtil.isPremiseRecordingEnabled(
3991
+ this.userDisplayHints,
3992
+ this.selfUserPolicies
3993
+ ),
3764
3994
  canRaiseHand: MeetingUtil.canUserRaiseHand(this.userDisplayHints),
3765
3995
  canLowerAllHands: MeetingUtil.canUserLowerAllHands(this.userDisplayHints),
3766
3996
  canLowerSomeoneElsesHand: MeetingUtil.canUserLowerSomeoneElsesHand(this.userDisplayHints),
@@ -3787,6 +4017,7 @@ export default class Meeting extends StatelessWebexPlugin {
3787
4017
  this.userDisplayHints
3788
4018
  ),
3789
4019
  canManageBreakout: MeetingUtil.canManageBreakout(this.userDisplayHints),
4020
+ canStartBreakout: MeetingUtil.canStartBreakout(this.userDisplayHints),
3790
4021
  canBroadcastMessageToBreakout: MeetingUtil.canBroadcastMessageToBreakout(
3791
4022
  this.userDisplayHints,
3792
4023
  this.selfUserPolicies
@@ -3904,6 +4135,22 @@ export default class Meeting extends StatelessWebexPlugin {
3904
4135
  requiredHints: [DISPLAY_HINTS.DISABLE_STAGE_VIEW],
3905
4136
  displayHints: this.userDisplayHints,
3906
4137
  }),
4138
+ isPracticeSessionOn: ControlsOptionsUtil.hasHints({
4139
+ requiredHints: [DISPLAY_HINTS.PRACTICE_SESSION_ON],
4140
+ displayHints: this.userDisplayHints,
4141
+ }),
4142
+ isPracticeSessionOff: ControlsOptionsUtil.hasHints({
4143
+ requiredHints: [DISPLAY_HINTS.PRACTICE_SESSION_OFF],
4144
+ displayHints: this.userDisplayHints,
4145
+ }),
4146
+ canStartPracticeSession: ControlsOptionsUtil.hasHints({
4147
+ requiredHints: [DISPLAY_HINTS.SHOW_PRACTICE_SESSION_START],
4148
+ displayHints: this.userDisplayHints,
4149
+ }),
4150
+ canStopPracticeSession: ControlsOptionsUtil.hasHints({
4151
+ requiredHints: [DISPLAY_HINTS.SHOW_PRACTICE_SESSION_STOP],
4152
+ displayHints: this.userDisplayHints,
4153
+ }),
3907
4154
  canShareFile:
3908
4155
  (ControlsOptionsUtil.hasHints({
3909
4156
  requiredHints: [DISPLAY_HINTS.SHARE_FILE],
@@ -4060,6 +4307,66 @@ export default class Meeting extends StatelessWebexPlugin {
4060
4307
  Trigger.trigger(this, options, EVENTS.REQUEST_UPLOAD_LOGS, this);
4061
4308
  }
4062
4309
 
4310
+ /**
4311
+ * sets the timer for periodic log upload
4312
+ * @returns {void}
4313
+ */
4314
+ private setLogUploadTimer() {
4315
+ // 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
4316
+ const LOG_UPLOAD_INTERVALS = [0.1, 15, 30, 60]; // in minutes
4317
+
4318
+ const delay =
4319
+ 1000 *
4320
+ 60 *
4321
+ // @ts-ignore - config coming from registerPlugin
4322
+ this.config.logUploadIntervalMultiplicationFactor *
4323
+ LOG_UPLOAD_INTERVALS[this.logUploadIntervalIndex];
4324
+
4325
+ if (this.logUploadIntervalIndex < LOG_UPLOAD_INTERVALS.length - 1) {
4326
+ this.logUploadIntervalIndex += 1;
4327
+ }
4328
+
4329
+ this.uploadLogsTimer = safeSetTimeout(() => {
4330
+ this.uploadLogsTimer = undefined;
4331
+
4332
+ this.uploadLogs();
4333
+
4334
+ // just as an extra precaution, to avoid uploading logs forever in case something goes wrong
4335
+ // and the page remains opened, we stop it if there is no media connection
4336
+ if (!this.mediaProperties.webrtcMediaConnection) {
4337
+ return;
4338
+ }
4339
+
4340
+ this.setLogUploadTimer();
4341
+ }, delay);
4342
+ }
4343
+
4344
+ /**
4345
+ * Starts a periodic upload of logs
4346
+ *
4347
+ * @returns {undefined}
4348
+ */
4349
+ public startPeriodicLogUpload() {
4350
+ // @ts-ignore - config coming from registerPlugin
4351
+ if (this.config.logUploadIntervalMultiplicationFactor && !this.uploadLogsTimer) {
4352
+ this.logUploadIntervalIndex = 0;
4353
+
4354
+ this.setLogUploadTimer();
4355
+ }
4356
+ }
4357
+
4358
+ /**
4359
+ * Stops the periodic upload of logs
4360
+ *
4361
+ * @returns {undefined}
4362
+ */
4363
+ public stopPeriodicLogUpload() {
4364
+ if (this.uploadLogsTimer) {
4365
+ clearTimeout(this.uploadLogsTimer);
4366
+ this.uploadLogsTimer = undefined;
4367
+ }
4368
+ }
4369
+
4063
4370
  /**
4064
4371
  * Removes remote audio, video and share streams from class instance's mediaProperties
4065
4372
  * @returns {undefined}
@@ -4449,11 +4756,12 @@ export default class Meeting extends StatelessWebexPlugin {
4449
4756
  * Close the peer connections and remove them from the class.
4450
4757
  * Cleanup any media connection related things.
4451
4758
  *
4759
+ * @param {boolean} resetMuteStates whether to also reset the audio/video mute state information
4452
4760
  * @returns {Promise}
4453
4761
  * @public
4454
4762
  * @memberof Meeting
4455
4763
  */
4456
- public closePeerConnections() {
4764
+ public closePeerConnections(resetMuteStates = true) {
4457
4765
  if (this.mediaProperties.webrtcMediaConnection) {
4458
4766
  if (this.remoteMediaManager) {
4459
4767
  this.remoteMediaManager.stop();
@@ -4466,12 +4774,15 @@ export default class Meeting extends StatelessWebexPlugin {
4466
4774
 
4467
4775
  this.receiveSlotManager.reset();
4468
4776
  this.mediaProperties.webrtcMediaConnection.close();
4777
+ this.mediaProperties.unsetPeerConnection();
4469
4778
  this.sendSlotManager.reset();
4470
4779
  this.setNetworkStatus(undefined);
4471
4780
  }
4472
4781
 
4473
- this.audio = null;
4474
- this.video = null;
4782
+ if (resetMuteStates) {
4783
+ this.audio = null;
4784
+ this.video = null;
4785
+ }
4475
4786
 
4476
4787
  return Promise.resolve();
4477
4788
  }
@@ -4731,7 +5042,7 @@ export default class Meeting extends StatelessWebexPlugin {
4731
5042
  * @param {Object} options - options to join with media
4732
5043
  * @param {JoinOptions} [options.joinOptions] - see #join()
4733
5044
  * @param {AddMediaOptions} [options.mediaOptions] - see #addMedia()
4734
- * @returns {Promise} -- {join: see join(), media: see addMedia()}
5045
+ * @returns {Promise} -- {join: see join(), media: see addMedia(), multistreamEnabled: flag to indicate if we managed to join in multistream mode}
4735
5046
  * @public
4736
5047
  * @memberof Meeting
4737
5048
  * @example
@@ -4771,8 +5082,6 @@ export default class Meeting extends StatelessWebexPlugin {
4771
5082
  if (!joinResponse) {
4772
5083
  // This is the 1st attempt or a retry after join request failed -> we need to do a join with TURN discovery
4773
5084
 
4774
- // @ts-ignore
4775
- joinOptions.reachability = await this.webex.meetings.reachability.getReachabilityResults();
4776
5085
  const turnDiscoveryRequest = await this.roap.generateTurnDiscoveryRequestMessage(
4777
5086
  this,
4778
5087
  true
@@ -4823,6 +5132,7 @@ export default class Meeting extends StatelessWebexPlugin {
4823
5132
  return {
4824
5133
  join: joinResponse,
4825
5134
  media: mediaResponse,
5135
+ multistreamEnabled: this.isMultistream,
4826
5136
  };
4827
5137
  } catch (error) {
4828
5138
  LoggerProxy.logger.error('Meeting:index#joinWithMedia --> ', error);
@@ -4831,7 +5141,17 @@ export default class Meeting extends StatelessWebexPlugin {
4831
5141
 
4832
5142
  this.roap.abortTurnDiscovery();
4833
5143
 
4834
- if (joined && isRetry) {
5144
+ // if this was the first attempt, let's do a retry
5145
+ let shouldRetry = !isRetry;
5146
+
5147
+ if (CallDiagnosticUtils.isSdpOfferCreationError(error)) {
5148
+ // errors related to offer creation (for example missing H264 codec) will happen again no matter how many times we try,
5149
+ // so there is no point doing a retry
5150
+ shouldRetry = false;
5151
+ }
5152
+
5153
+ // we only want to call leave if join was successful and this was a retry or we won't be doing any more retries
5154
+ if (joined && (isRetry || !shouldRetry)) {
4835
5155
  try {
4836
5156
  await this.leave({resourceId: joinOptions?.resourceId, reason: 'joinWithMedia failure'});
4837
5157
  } catch (e) {
@@ -4855,15 +5175,6 @@ export default class Meeting extends StatelessWebexPlugin {
4855
5175
  }
4856
5176
  );
4857
5177
 
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
5178
  if (shouldRetry) {
4868
5179
  LoggerProxy.logger.warn('Meeting:index#joinWithMedia --> retrying call to joinWithMedia');
4869
5180
  this.joinWithMediaRetryInfo.isRetry = true;
@@ -5119,7 +5430,16 @@ export default class Meeting extends StatelessWebexPlugin {
5119
5430
  (this.config.receiveReactions || options.receiveReactions) &&
5120
5431
  this.isReactionsSupported()
5121
5432
  ) {
5122
- const {name} = this.members.membersCollection.get(e.data.sender.participantId);
5433
+ const member = this.members.membersCollection.get(e.data.sender.participantId);
5434
+ if (!member) {
5435
+ // @ts-ignore -- fix type
5436
+ LoggerProxy.logger.warn(
5437
+ `Meeting:index#processRelayEvent --> Skipping handling of ${REACTION_RELAY_TYPES.REACTION} for ${this.id}. participantId ${e.data.sender.participantId} does not exist in membersCollection.`
5438
+ );
5439
+ break;
5440
+ }
5441
+
5442
+ const {name} = member;
5123
5443
  const processedReaction: ProcessedReaction = {
5124
5444
  reaction: e.data.reaction,
5125
5445
  sender: {
@@ -5173,6 +5493,9 @@ export default class Meeting extends StatelessWebexPlugin {
5173
5493
  this.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]
5174
5494
  );
5175
5495
 
5496
+ // @ts-ignore
5497
+ this.webex.internal.voicea.deregisterEvents();
5498
+
5176
5499
  this.areVoiceaEventsSetup = false;
5177
5500
  this.triggerStopReceivingTranscriptionEvent();
5178
5501
  }
@@ -5283,16 +5606,19 @@ export default class Meeting extends StatelessWebexPlugin {
5283
5606
  this.meetingFiniteStateMachine.reset();
5284
5607
  }
5285
5608
 
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
- });
5609
+ // send client.call.initiated unless told not to
5610
+ if (options.sendCallInitiated !== false) {
5611
+ // @ts-ignore
5612
+ this.webex.internal.newMetrics.submitClientEvent({
5613
+ name: 'client.call.initiated',
5614
+ payload: {
5615
+ trigger: this.callStateForMetrics.joinTrigger || 'user-interaction',
5616
+ isRoapCallEnabled: true,
5617
+ pstnAudioType: options?.pstnAudioType,
5618
+ },
5619
+ options: {meetingId: this.id},
5620
+ });
5621
+ }
5296
5622
 
5297
5623
  LoggerProxy.logger.log('Meeting:index#join --> Joining a meeting');
5298
5624
 
@@ -5480,23 +5806,36 @@ export default class Meeting extends StatelessWebexPlugin {
5480
5806
  */
5481
5807
  async updateLLMConnection() {
5482
5808
  // @ts-ignore - Fix type
5483
- const {url, info: {datachannelUrl} = {}} = this.locusInfo;
5809
+ const {url, info: {datachannelUrl, practiceSessionDatachannelUrl} = {}} = this.locusInfo;
5484
5810
 
5485
5811
  const isJoined = this.isJoined();
5486
5812
 
5813
+ // webinar panelist should use new data channel in practice session
5814
+ const dataChannelUrl =
5815
+ this.webinar.isJoinPracticeSessionDataChannel() && practiceSessionDatachannelUrl
5816
+ ? practiceSessionDatachannelUrl
5817
+ : datachannelUrl;
5818
+
5487
5819
  // @ts-ignore - Fix type
5488
5820
  if (this.webex.internal.llm.isConnected()) {
5489
5821
  if (
5490
5822
  // @ts-ignore - Fix type
5491
5823
  url === this.webex.internal.llm.getLocusUrl() &&
5492
5824
  // @ts-ignore - Fix type
5493
- datachannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5825
+ dataChannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5494
5826
  isJoined
5495
5827
  ) {
5496
5828
  return undefined;
5497
5829
  }
5498
5830
  // @ts-ignore - Fix type
5499
- await this.webex.internal.llm.disconnectLLM();
5831
+ await this.webex.internal.llm.disconnectLLM(
5832
+ isJoined
5833
+ ? {
5834
+ code: 3050,
5835
+ reason: 'done (permanent)',
5836
+ }
5837
+ : undefined
5838
+ );
5500
5839
  // @ts-ignore - Fix type
5501
5840
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
5502
5841
  }
@@ -5507,7 +5846,7 @@ export default class Meeting extends StatelessWebexPlugin {
5507
5846
 
5508
5847
  // @ts-ignore - Fix type
5509
5848
  return this.webex.internal.llm
5510
- .registerAndConnect(url, datachannelUrl)
5849
+ .registerAndConnect(url, dataChannelUrl)
5511
5850
  .then((registerAndConnectResult) => {
5512
5851
  // @ts-ignore - Fix type
5513
5852
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
@@ -5877,8 +6216,16 @@ export default class Meeting extends StatelessWebexPlugin {
5877
6216
  * @returns {undefined}
5878
6217
  */
5879
6218
  public roapMessageReceived = (roapMessage: RoapMessage) => {
5880
- const mediaServer = MeetingsUtil.getMediaServer(roapMessage.sdp);
5881
-
6219
+ const mediaServer =
6220
+ roapMessage.messageType === 'ANSWER'
6221
+ ? MeetingsUtil.getMediaServer(roapMessage.sdp)
6222
+ : undefined;
6223
+
6224
+ if (this.isMultistream && mediaServer && mediaServer !== 'homer') {
6225
+ throw new MultistreamNotSupportedError(
6226
+ `Client asked for multistream backend (Homer), but got ${mediaServer} instead`
6227
+ );
6228
+ }
5882
6229
  this.mediaProperties.webrtcMediaConnection.roapMessageReceived(roapMessage);
5883
6230
 
5884
6231
  if (mediaServer) {
@@ -6001,16 +6348,20 @@ export default class Meeting extends StatelessWebexPlugin {
6001
6348
  logText: `${LOG_HEADER} Roap Offer`,
6002
6349
  }
6003
6350
  ).catch((error) => {
6351
+ const multistreamNotSupported = error instanceof MultistreamNotSupportedError;
6352
+
6004
6353
  // @ts-ignore
6005
6354
  this.webex.internal.newMetrics.submitClientEvent({
6006
6355
  name: 'client.media-engine.remote-sdp-received',
6007
6356
  payload: {
6008
- canProceed: false,
6357
+ canProceed: multistreamNotSupported,
6009
6358
  errors: [
6010
6359
  // @ts-ignore
6011
6360
  this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
6012
6361
  {
6013
- clientErrorCode: CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6362
+ clientErrorCode: multistreamNotSupported
6363
+ ? CALL_DIAGNOSTIC_CONFIG.MULTISTREAM_NOT_AVAILABLE_CLIENT_CODE
6364
+ : CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6014
6365
  }
6015
6366
  ),
6016
6367
  ],
@@ -6018,7 +6369,7 @@ export default class Meeting extends StatelessWebexPlugin {
6018
6369
  options: {meetingId: this.id, rawError: error},
6019
6370
  });
6020
6371
 
6021
- this.deferSDPAnswer.reject(new Error('failed to send ROAP SDP offer'));
6372
+ this.deferSDPAnswer.reject(error);
6022
6373
  clearTimeout(this.sdpResponseTimer);
6023
6374
  this.sdpResponseTimer = undefined;
6024
6375
  });
@@ -6346,6 +6697,14 @@ export default class Meeting extends StatelessWebexPlugin {
6346
6697
  this.webex.meetings.geoHintInfo?.clientAddress ||
6347
6698
  options.data.intervalMetadata.peerReflexiveIP ||
6348
6699
  MQA_STATS.DEFAULT_IP;
6700
+
6701
+ const {members} = this.getMembers().membersCollection;
6702
+
6703
+ // Count members that are in the meeting
6704
+ options.data.intervalMetadata.meetingUserCount = Object.values(members).filter(
6705
+ (member: Member) => member.isInMeeting
6706
+ ).length;
6707
+
6349
6708
  // @ts-ignore
6350
6709
  this.webex.internal.newMetrics.submitMQE({
6351
6710
  name: 'client.mediaquality.event',
@@ -6477,6 +6836,9 @@ export default class Meeting extends StatelessWebexPlugin {
6477
6836
  new RtcMetrics(this.webex, {meetingId: this.id}, this.correlationId)
6478
6837
  : undefined;
6479
6838
 
6839
+ // ongoing reachability checks slow down new media connections especially on Firefox, so we stop them
6840
+ this.getWebexObject().meetings.reachability.stopReachability();
6841
+
6480
6842
  const mc = Media.createMediaConnection(
6481
6843
  this.isMultistream,
6482
6844
  this.getMediaConnectionDebugId(),
@@ -6677,32 +7039,6 @@ export default class Meeting extends StatelessWebexPlugin {
6677
7039
  }
6678
7040
  }
6679
7041
 
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
7042
  /**
6707
7043
  * Returns a promise. This promise is created once the local sdp offer has been successfully created and is resolved
6708
7044
  * once the remote sdp answer has been received.
@@ -6926,7 +7262,9 @@ export default class Meeting extends StatelessWebexPlugin {
6926
7262
 
6927
7263
  const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
6928
7264
 
6929
- LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
7265
+ LoggerProxy.logger.info(
7266
+ `${LOG_HEADER} media connection created this.isMultistream=${this.isMultistream}`
7267
+ );
6930
7268
 
6931
7269
  if (this.isMultistream) {
6932
7270
  this.remoteMediaManager = new RemoteMediaManager(
@@ -7004,6 +7342,33 @@ export default class Meeting extends StatelessWebexPlugin {
7004
7342
  }
7005
7343
  }
7006
7344
 
7345
+ /**
7346
+ * Cleans up stats analyzer, peer connection and other things before
7347
+ * we can create a new transcoded media connection
7348
+ *
7349
+ * @private
7350
+ * @returns {Promise<void>}
7351
+ */
7352
+ private async downgradeFromMultistreamToTranscoded(): Promise<void> {
7353
+ if (this.statsAnalyzer) {
7354
+ await this.statsAnalyzer.stopAnalyzer();
7355
+ }
7356
+ this.statsAnalyzer = null;
7357
+
7358
+ this.isMultistream = false;
7359
+
7360
+ if (this.mediaProperties.webrtcMediaConnection) {
7361
+ // close peer connection, but don't reset mute state information, because we will want to use it on the retry
7362
+ this.closePeerConnections(false);
7363
+
7364
+ this.mediaProperties.unsetPeerConnection();
7365
+ }
7366
+
7367
+ this.locusMediaRequest?.downgradeFromMultistreamToTranscoded();
7368
+
7369
+ this.createStatsAnalyzer();
7370
+ }
7371
+
7007
7372
  /**
7008
7373
  * Sends stats report, closes peer connection and cleans up any media connection
7009
7374
  * related things before trying to establish media connection again with turn server
@@ -7190,6 +7555,7 @@ export default class Meeting extends StatelessWebexPlugin {
7190
7555
 
7191
7556
  this.audio = createMuteState(AUDIO, this, audioEnabled);
7192
7557
  this.video = createMuteState(VIDEO, this, videoEnabled);
7558
+ this.brbState = createBrbState(this, false);
7193
7559
 
7194
7560
  try {
7195
7561
  await this.setUpLocalStreamReferences(localStreams);
@@ -7198,19 +7564,36 @@ export default class Meeting extends StatelessWebexPlugin {
7198
7564
 
7199
7565
  this.createStatsAnalyzer();
7200
7566
 
7201
- await this.establishMediaConnection(
7202
- remoteMediaManagerConfig,
7203
- bundlePolicy,
7204
- forceTurnDiscovery,
7205
- turnServerInfo
7206
- );
7567
+ try {
7568
+ await this.establishMediaConnection(
7569
+ remoteMediaManagerConfig,
7570
+ bundlePolicy,
7571
+ forceTurnDiscovery,
7572
+ turnServerInfo
7573
+ );
7574
+ } catch (error) {
7575
+ if (error instanceof MultistreamNotSupportedError) {
7576
+ LoggerProxy.logger.warn(
7577
+ `${LOG_HEADER} we asked for multistream backend (Homer), but got transcoded backend, recreating media connection...`
7578
+ );
7207
7579
 
7208
- if (audioEnabled || videoEnabled) {
7209
- await Meeting.handleDeviceLogging(audioEnabled, videoEnabled);
7210
- } else {
7211
- LoggerProxy.logger.info(`${LOG_HEADER} device logging not required`);
7580
+ await this.downgradeFromMultistreamToTranscoded();
7581
+
7582
+ // Establish new media connection with forced TURN discovery
7583
+ // 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
7584
+ await this.establishMediaConnection(
7585
+ remoteMediaManagerConfig,
7586
+ bundlePolicy,
7587
+ true,
7588
+ undefined
7589
+ );
7590
+ } else {
7591
+ throw error;
7592
+ }
7212
7593
  }
7213
7594
 
7595
+ LoggerProxy.logger.info(`${LOG_HEADER} media connected, finalizing...`);
7596
+
7214
7597
  if (this.mediaProperties.hasLocalShareStream()) {
7215
7598
  await this.enqueueScreenShareFloorRequest();
7216
7599
  }
@@ -7247,6 +7630,7 @@ export default class Meeting extends StatelessWebexPlugin {
7247
7630
 
7248
7631
  // We can log ReceiveSlot SSRCs only after the SDP exchange, so doing it here:
7249
7632
  this.remoteMediaManager?.logAllReceiveSlots();
7633
+ this.startPeriodicLogUpload();
7250
7634
  } catch (error) {
7251
7635
  LoggerProxy.logger.error(`${LOG_HEADER} failed to establish media connection: `, error);
7252
7636
 
@@ -8179,7 +8563,7 @@ export default class Meeting extends StatelessWebexPlugin {
8179
8563
  if (layoutType) {
8180
8564
  if (!LAYOUT_TYPES.includes(layoutType)) {
8181
8565
  return this.rejectWithErrorLog(
8182
- 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType received.'
8566
+ `Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType "${layoutType}" received.`
8183
8567
  );
8184
8568
  }
8185
8569
 
@@ -8335,6 +8719,12 @@ export default class Meeting extends StatelessWebexPlugin {
8335
8719
  correlationId: this.correlationId,
8336
8720
  muted,
8337
8721
  encoderImplementation: this.statsAnalyzer?.shareVideoEncoderImplementation,
8722
+ // TypeScript 4 does not recognize the `displaySurface` property. Instead of upgrading the
8723
+ // SDK to TypeScript 5, which may affect other packages, use bracket notation for now, since
8724
+ // all we're doing here is adding metrics.
8725
+ // eslint-disable-next-line dot-notation
8726
+ displaySurface: this.mediaProperties?.shareVideoStream?.getSettings()['displaySurface'],
8727
+ isMultistream: this.isMultistream,
8338
8728
  });
8339
8729
  };
8340
8730
 
@@ -8537,6 +8927,11 @@ export default class Meeting extends StatelessWebexPlugin {
8537
8927
  this.stopTranscription();
8538
8928
  this.transcription = undefined;
8539
8929
  }
8930
+
8931
+ this.annotation.deregisterEvents();
8932
+
8933
+ // @ts-ignore - fix types
8934
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
8540
8935
  };
8541
8936
 
8542
8937
  /**
@@ -8574,10 +8969,12 @@ export default class Meeting extends StatelessWebexPlugin {
8574
8969
 
8575
8970
  return;
8576
8971
  }
8577
- const {keepAliveUrl} = this.joinedWith;
8972
+
8578
8973
  const keepAliveInterval = (this.joinedWith.keepAliveSecs - 1) * 750; // taken from UCF
8579
8974
 
8580
8975
  this.keepAliveTimerId = setInterval(() => {
8976
+ const {keepAliveUrl} = this.joinedWith;
8977
+
8581
8978
  this.meetingRequest.keepAlive({keepAliveUrl}).catch((error) => {
8582
8979
  LoggerProxy.logger.warn(
8583
8980
  `Meeting:index#startKeepAlive --> Stopping sending keepAlives to ${keepAliveUrl} after error ${error}`