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

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 (198) 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 +2 -1
  12. package/dist/config.js.map +1 -1
  13. package/dist/constants.js +68 -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 +1 -1
  18. package/dist/interpretation/siLanguage.js +1 -1
  19. package/dist/locus-info/index.js +14 -3
  20. package/dist/locus-info/index.js.map +1 -1
  21. package/dist/locus-info/selfUtils.js +35 -17
  22. package/dist/locus-info/selfUtils.js.map +1 -1
  23. package/dist/media/MediaConnectionAwaiter.js +1 -0
  24. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  25. package/dist/media/properties.js +30 -16
  26. package/dist/media/properties.js.map +1 -1
  27. package/dist/meeting/brbState.js +167 -0
  28. package/dist/meeting/brbState.js.map +1 -0
  29. package/dist/meeting/in-meeting-actions.js +13 -1
  30. package/dist/meeting/in-meeting-actions.js.map +1 -1
  31. package/dist/meeting/index.js +1335 -1052
  32. package/dist/meeting/index.js.map +1 -1
  33. package/dist/meeting/locusMediaRequest.js +11 -6
  34. package/dist/meeting/locusMediaRequest.js.map +1 -1
  35. package/dist/meeting/muteState.js +1 -6
  36. package/dist/meeting/muteState.js.map +1 -1
  37. package/dist/meeting/request.js +51 -29
  38. package/dist/meeting/request.js.map +1 -1
  39. package/dist/meeting/request.type.js.map +1 -1
  40. package/dist/meeting/util.js +103 -67
  41. package/dist/meeting/util.js.map +1 -1
  42. package/dist/meeting-info/meeting-info-v2.js +115 -45
  43. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  44. package/dist/meeting-info/utilv2.js +6 -2
  45. package/dist/meeting-info/utilv2.js.map +1 -1
  46. package/dist/meetings/index.js +107 -55
  47. package/dist/meetings/index.js.map +1 -1
  48. package/dist/meetings/meetings.types.js +2 -0
  49. package/dist/meetings/meetings.types.js.map +1 -1
  50. package/dist/meetings/util.js +1 -1
  51. package/dist/meetings/util.js.map +1 -1
  52. package/dist/member/index.js +9 -0
  53. package/dist/member/index.js.map +1 -1
  54. package/dist/member/types.js.map +1 -1
  55. package/dist/member/util.js +39 -28
  56. package/dist/member/util.js.map +1 -1
  57. package/dist/members/util.js +4 -2
  58. package/dist/members/util.js.map +1 -1
  59. package/dist/metrics/constants.js +6 -1
  60. package/dist/metrics/constants.js.map +1 -1
  61. package/dist/multistream/remoteMedia.js +30 -15
  62. package/dist/multistream/remoteMedia.js.map +1 -1
  63. package/dist/multistream/sendSlotManager.js +24 -0
  64. package/dist/multistream/sendSlotManager.js.map +1 -1
  65. package/dist/reachability/clusterReachability.js +12 -15
  66. package/dist/reachability/clusterReachability.js.map +1 -1
  67. package/dist/reachability/index.js +461 -136
  68. package/dist/reachability/index.js.map +1 -1
  69. package/dist/{rtcMetrics/constants.js → reachability/reachability.types.js} +1 -5
  70. package/dist/reachability/reachability.types.js.map +1 -0
  71. package/dist/reachability/request.js +21 -8
  72. package/dist/reachability/request.js.map +1 -1
  73. package/dist/recording-controller/enums.js +8 -4
  74. package/dist/recording-controller/enums.js.map +1 -1
  75. package/dist/recording-controller/index.js +18 -9
  76. package/dist/recording-controller/index.js.map +1 -1
  77. package/dist/recording-controller/util.js +13 -9
  78. package/dist/recording-controller/util.js.map +1 -1
  79. package/dist/roap/index.js +15 -15
  80. package/dist/roap/index.js.map +1 -1
  81. package/dist/roap/request.js +45 -79
  82. package/dist/roap/request.js.map +1 -1
  83. package/dist/roap/turnDiscovery.js +3 -6
  84. package/dist/roap/turnDiscovery.js.map +1 -1
  85. package/dist/types/annotation/index.d.ts +5 -0
  86. package/dist/types/common/errors/join-forbidden-error.d.ts +15 -0
  87. package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
  88. package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
  89. package/dist/types/config.d.ts +1 -0
  90. package/dist/types/constants.d.ts +53 -1
  91. package/dist/types/index.d.ts +3 -3
  92. package/dist/types/locus-info/index.d.ts +2 -1
  93. package/dist/types/meeting/brbState.d.ts +54 -0
  94. package/dist/types/meeting/in-meeting-actions.d.ts +12 -0
  95. package/dist/types/meeting/index.d.ts +64 -14
  96. package/dist/types/meeting/locusMediaRequest.d.ts +6 -3
  97. package/dist/types/meeting/request.d.ts +14 -3
  98. package/dist/types/meeting/request.type.d.ts +6 -0
  99. package/dist/types/meeting/util.d.ts +3 -3
  100. package/dist/types/meeting-info/meeting-info-v2.d.ts +30 -5
  101. package/dist/types/meetings/index.d.ts +20 -2
  102. package/dist/types/meetings/meetings.types.d.ts +8 -0
  103. package/dist/types/member/index.d.ts +1 -0
  104. package/dist/types/member/types.d.ts +7 -0
  105. package/dist/types/members/util.d.ts +2 -0
  106. package/dist/types/metrics/constants.d.ts +6 -1
  107. package/dist/types/multistream/sendSlotManager.d.ts +8 -1
  108. package/dist/types/reachability/clusterReachability.d.ts +1 -10
  109. package/dist/types/reachability/index.d.ts +83 -36
  110. package/dist/types/reachability/reachability.types.d.ts +64 -0
  111. package/dist/types/reachability/request.d.ts +5 -1
  112. package/dist/types/recording-controller/enums.d.ts +5 -2
  113. package/dist/types/recording-controller/index.d.ts +1 -0
  114. package/dist/types/recording-controller/util.d.ts +2 -1
  115. package/dist/types/roap/request.d.ts +1 -13
  116. package/dist/webinar/index.js +390 -7
  117. package/dist/webinar/index.js.map +1 -1
  118. package/package.json +23 -22
  119. package/src/annotation/index.ts +16 -0
  120. package/src/common/errors/join-forbidden-error.ts +26 -0
  121. package/src/common/errors/join-webinar-error.ts +24 -0
  122. package/src/common/errors/multistream-not-supported-error.ts +30 -0
  123. package/src/config.ts +1 -0
  124. package/src/constants.ts +61 -3
  125. package/src/index.ts +5 -3
  126. package/src/locus-info/index.ts +20 -3
  127. package/src/locus-info/selfUtils.ts +24 -6
  128. package/src/media/MediaConnectionAwaiter.ts +2 -0
  129. package/src/media/properties.ts +34 -13
  130. package/src/meeting/brbState.ts +169 -0
  131. package/src/meeting/in-meeting-actions.ts +25 -0
  132. package/src/meeting/index.ts +451 -88
  133. package/src/meeting/locusMediaRequest.ts +11 -8
  134. package/src/meeting/muteState.ts +1 -6
  135. package/src/meeting/request.ts +30 -12
  136. package/src/meeting/request.type.ts +7 -0
  137. package/src/meeting/util.ts +32 -13
  138. package/src/meeting-info/meeting-info-v2.ts +83 -12
  139. package/src/meeting-info/utilv2.ts +17 -3
  140. package/src/meetings/index.ts +79 -20
  141. package/src/meetings/meetings.types.ts +10 -0
  142. package/src/meetings/util.ts +2 -1
  143. package/src/member/index.ts +9 -0
  144. package/src/member/types.ts +8 -0
  145. package/src/member/util.ts +34 -24
  146. package/src/members/util.ts +1 -0
  147. package/src/metrics/constants.ts +6 -1
  148. package/src/multistream/remoteMedia.ts +28 -15
  149. package/src/multistream/sendSlotManager.ts +31 -0
  150. package/src/reachability/clusterReachability.ts +5 -15
  151. package/src/reachability/index.ts +311 -75
  152. package/src/reachability/reachability.types.ts +85 -0
  153. package/src/reachability/request.ts +55 -31
  154. package/src/recording-controller/enums.ts +5 -2
  155. package/src/recording-controller/index.ts +17 -4
  156. package/src/recording-controller/util.ts +20 -5
  157. package/src/roap/index.ts +14 -13
  158. package/src/roap/request.ts +30 -44
  159. package/src/roap/turnDiscovery.ts +2 -4
  160. package/src/webinar/index.ts +235 -9
  161. package/test/unit/spec/annotation/index.ts +46 -1
  162. package/test/unit/spec/locus-info/index.js +292 -60
  163. package/test/unit/spec/locus-info/selfConstant.js +7 -0
  164. package/test/unit/spec/locus-info/selfUtils.js +101 -1
  165. package/test/unit/spec/media/properties.ts +15 -0
  166. package/test/unit/spec/meeting/brbState.ts +114 -0
  167. package/test/unit/spec/meeting/in-meeting-actions.ts +15 -1
  168. package/test/unit/spec/meeting/index.js +860 -110
  169. package/test/unit/spec/meeting/locusMediaRequest.ts +18 -11
  170. package/test/unit/spec/meeting/muteState.js +0 -24
  171. package/test/unit/spec/meeting/request.js +3 -26
  172. package/test/unit/spec/meeting/utils.js +73 -28
  173. package/test/unit/spec/meeting-info/meetinginfov2.js +46 -4
  174. package/test/unit/spec/meeting-info/utilv2.js +26 -0
  175. package/test/unit/spec/meetings/index.js +159 -18
  176. package/test/unit/spec/meetings/utils.js +10 -0
  177. package/test/unit/spec/member/util.js +52 -11
  178. package/test/unit/spec/members/utils.js +95 -0
  179. package/test/unit/spec/multistream/remoteMedia.ts +11 -7
  180. package/test/unit/spec/reachability/clusterReachability.ts +7 -0
  181. package/test/unit/spec/reachability/index.ts +383 -9
  182. package/test/unit/spec/reachability/request.js +48 -12
  183. package/test/unit/spec/recording-controller/index.js +61 -5
  184. package/test/unit/spec/recording-controller/util.js +39 -3
  185. package/test/unit/spec/roap/index.ts +48 -1
  186. package/test/unit/spec/roap/request.ts +51 -109
  187. package/test/unit/spec/roap/turnDiscovery.ts +202 -147
  188. package/test/unit/spec/webinar/index.ts +509 -0
  189. package/dist/common/errors/webinar-registration-error.js.map +0 -1
  190. package/dist/networkQualityMonitor/index.js +0 -227
  191. package/dist/networkQualityMonitor/index.js.map +0 -1
  192. package/dist/rtcMetrics/constants.js.map +0 -1
  193. package/dist/rtcMetrics/index.js +0 -197
  194. package/dist/rtcMetrics/index.js.map +0 -1
  195. package/dist/types/networkQualityMonitor/index.d.ts +0 -70
  196. package/dist/types/rtcMetrics/constants.d.ts +0 -4
  197. package/dist/types/rtcMetrics/index.d.ts +0 -71
  198. 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,23 +5772,36 @@ 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;
5497
5795
  }
5498
5796
  // @ts-ignore - Fix type
5499
- await this.webex.internal.llm.disconnectLLM();
5797
+ await this.webex.internal.llm.disconnectLLM(
5798
+ isJoined
5799
+ ? {
5800
+ code: 3050,
5801
+ reason: 'done (permanent)',
5802
+ }
5803
+ : undefined
5804
+ );
5500
5805
  // @ts-ignore - Fix type
5501
5806
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
5502
5807
  }
@@ -5507,7 +5812,7 @@ export default class Meeting extends StatelessWebexPlugin {
5507
5812
 
5508
5813
  // @ts-ignore - Fix type
5509
5814
  return this.webex.internal.llm
5510
- .registerAndConnect(url, datachannelUrl)
5815
+ .registerAndConnect(url, dataChannelUrl)
5511
5816
  .then((registerAndConnectResult) => {
5512
5817
  // @ts-ignore - Fix type
5513
5818
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
@@ -5877,8 +6182,16 @@ export default class Meeting extends StatelessWebexPlugin {
5877
6182
  * @returns {undefined}
5878
6183
  */
5879
6184
  public roapMessageReceived = (roapMessage: RoapMessage) => {
5880
- const mediaServer = MeetingsUtil.getMediaServer(roapMessage.sdp);
5881
-
6185
+ const mediaServer =
6186
+ roapMessage.messageType === 'ANSWER'
6187
+ ? MeetingsUtil.getMediaServer(roapMessage.sdp)
6188
+ : undefined;
6189
+
6190
+ if (this.isMultistream && mediaServer && mediaServer !== 'homer') {
6191
+ throw new MultistreamNotSupportedError(
6192
+ `Client asked for multistream backend (Homer), but got ${mediaServer} instead`
6193
+ );
6194
+ }
5882
6195
  this.mediaProperties.webrtcMediaConnection.roapMessageReceived(roapMessage);
5883
6196
 
5884
6197
  if (mediaServer) {
@@ -6001,16 +6314,20 @@ export default class Meeting extends StatelessWebexPlugin {
6001
6314
  logText: `${LOG_HEADER} Roap Offer`,
6002
6315
  }
6003
6316
  ).catch((error) => {
6317
+ const multistreamNotSupported = error instanceof MultistreamNotSupportedError;
6318
+
6004
6319
  // @ts-ignore
6005
6320
  this.webex.internal.newMetrics.submitClientEvent({
6006
6321
  name: 'client.media-engine.remote-sdp-received',
6007
6322
  payload: {
6008
- canProceed: false,
6323
+ canProceed: multistreamNotSupported,
6009
6324
  errors: [
6010
6325
  // @ts-ignore
6011
6326
  this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
6012
6327
  {
6013
- clientErrorCode: CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6328
+ clientErrorCode: multistreamNotSupported
6329
+ ? CALL_DIAGNOSTIC_CONFIG.MULTISTREAM_NOT_AVAILABLE_CLIENT_CODE
6330
+ : CALL_DIAGNOSTIC_CONFIG.MISSING_ROAP_ANSWER_CLIENT_CODE,
6014
6331
  }
6015
6332
  ),
6016
6333
  ],
@@ -6018,7 +6335,7 @@ export default class Meeting extends StatelessWebexPlugin {
6018
6335
  options: {meetingId: this.id, rawError: error},
6019
6336
  });
6020
6337
 
6021
- this.deferSDPAnswer.reject(new Error('failed to send ROAP SDP offer'));
6338
+ this.deferSDPAnswer.reject(error);
6022
6339
  clearTimeout(this.sdpResponseTimer);
6023
6340
  this.sdpResponseTimer = undefined;
6024
6341
  });
@@ -6346,6 +6663,14 @@ export default class Meeting extends StatelessWebexPlugin {
6346
6663
  this.webex.meetings.geoHintInfo?.clientAddress ||
6347
6664
  options.data.intervalMetadata.peerReflexiveIP ||
6348
6665
  MQA_STATS.DEFAULT_IP;
6666
+
6667
+ const {members} = this.getMembers().membersCollection;
6668
+
6669
+ // Count members that are in the meeting
6670
+ options.data.intervalMetadata.meetingUserCount = Object.values(members).filter(
6671
+ (member: Member) => member.isInMeeting
6672
+ ).length;
6673
+
6349
6674
  // @ts-ignore
6350
6675
  this.webex.internal.newMetrics.submitMQE({
6351
6676
  name: 'client.mediaquality.event',
@@ -6477,6 +6802,9 @@ export default class Meeting extends StatelessWebexPlugin {
6477
6802
  new RtcMetrics(this.webex, {meetingId: this.id}, this.correlationId)
6478
6803
  : undefined;
6479
6804
 
6805
+ // ongoing reachability checks slow down new media connections especially on Firefox, so we stop them
6806
+ this.getWebexObject().meetings.reachability.stopReachability();
6807
+
6480
6808
  const mc = Media.createMediaConnection(
6481
6809
  this.isMultistream,
6482
6810
  this.getMediaConnectionDebugId(),
@@ -6677,32 +7005,6 @@ export default class Meeting extends StatelessWebexPlugin {
6677
7005
  }
6678
7006
  }
6679
7007
 
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
7008
  /**
6707
7009
  * Returns a promise. This promise is created once the local sdp offer has been successfully created and is resolved
6708
7010
  * once the remote sdp answer has been received.
@@ -6926,7 +7228,9 @@ export default class Meeting extends StatelessWebexPlugin {
6926
7228
 
6927
7229
  const mc = await this.createMediaConnection(turnServerInfo, bundlePolicy);
6928
7230
 
6929
- LoggerProxy.logger.info(`${LOG_HEADER} media connection created`);
7231
+ LoggerProxy.logger.info(
7232
+ `${LOG_HEADER} media connection created this.isMultistream=${this.isMultistream}`
7233
+ );
6930
7234
 
6931
7235
  if (this.isMultistream) {
6932
7236
  this.remoteMediaManager = new RemoteMediaManager(
@@ -7004,6 +7308,33 @@ export default class Meeting extends StatelessWebexPlugin {
7004
7308
  }
7005
7309
  }
7006
7310
 
7311
+ /**
7312
+ * Cleans up stats analyzer, peer connection and other things before
7313
+ * we can create a new transcoded media connection
7314
+ *
7315
+ * @private
7316
+ * @returns {Promise<void>}
7317
+ */
7318
+ private async downgradeFromMultistreamToTranscoded(): Promise<void> {
7319
+ if (this.statsAnalyzer) {
7320
+ await this.statsAnalyzer.stopAnalyzer();
7321
+ }
7322
+ this.statsAnalyzer = null;
7323
+
7324
+ this.isMultistream = false;
7325
+
7326
+ if (this.mediaProperties.webrtcMediaConnection) {
7327
+ // close peer connection, but don't reset mute state information, because we will want to use it on the retry
7328
+ this.closePeerConnections(false);
7329
+
7330
+ this.mediaProperties.unsetPeerConnection();
7331
+ }
7332
+
7333
+ this.locusMediaRequest?.downgradeFromMultistreamToTranscoded();
7334
+
7335
+ this.createStatsAnalyzer();
7336
+ }
7337
+
7007
7338
  /**
7008
7339
  * Sends stats report, closes peer connection and cleans up any media connection
7009
7340
  * related things before trying to establish media connection again with turn server
@@ -7190,6 +7521,7 @@ export default class Meeting extends StatelessWebexPlugin {
7190
7521
 
7191
7522
  this.audio = createMuteState(AUDIO, this, audioEnabled);
7192
7523
  this.video = createMuteState(VIDEO, this, videoEnabled);
7524
+ this.brbState = createBrbState(this, false);
7193
7525
 
7194
7526
  try {
7195
7527
  await this.setUpLocalStreamReferences(localStreams);
@@ -7198,19 +7530,36 @@ export default class Meeting extends StatelessWebexPlugin {
7198
7530
 
7199
7531
  this.createStatsAnalyzer();
7200
7532
 
7201
- await this.establishMediaConnection(
7202
- remoteMediaManagerConfig,
7203
- bundlePolicy,
7204
- forceTurnDiscovery,
7205
- turnServerInfo
7206
- );
7533
+ try {
7534
+ await this.establishMediaConnection(
7535
+ remoteMediaManagerConfig,
7536
+ bundlePolicy,
7537
+ forceTurnDiscovery,
7538
+ turnServerInfo
7539
+ );
7540
+ } catch (error) {
7541
+ if (error instanceof MultistreamNotSupportedError) {
7542
+ LoggerProxy.logger.warn(
7543
+ `${LOG_HEADER} we asked for multistream backend (Homer), but got transcoded backend, recreating media connection...`
7544
+ );
7207
7545
 
7208
- if (audioEnabled || videoEnabled) {
7209
- await Meeting.handleDeviceLogging(audioEnabled, videoEnabled);
7210
- } else {
7211
- LoggerProxy.logger.info(`${LOG_HEADER} device logging not required`);
7546
+ await this.downgradeFromMultistreamToTranscoded();
7547
+
7548
+ // Establish new media connection with forced TURN discovery
7549
+ // 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
7550
+ await this.establishMediaConnection(
7551
+ remoteMediaManagerConfig,
7552
+ bundlePolicy,
7553
+ true,
7554
+ undefined
7555
+ );
7556
+ } else {
7557
+ throw error;
7558
+ }
7212
7559
  }
7213
7560
 
7561
+ LoggerProxy.logger.info(`${LOG_HEADER} media connected, finalizing...`);
7562
+
7214
7563
  if (this.mediaProperties.hasLocalShareStream()) {
7215
7564
  await this.enqueueScreenShareFloorRequest();
7216
7565
  }
@@ -7247,6 +7596,7 @@ export default class Meeting extends StatelessWebexPlugin {
7247
7596
 
7248
7597
  // We can log ReceiveSlot SSRCs only after the SDP exchange, so doing it here:
7249
7598
  this.remoteMediaManager?.logAllReceiveSlots();
7599
+ this.startPeriodicLogUpload();
7250
7600
  } catch (error) {
7251
7601
  LoggerProxy.logger.error(`${LOG_HEADER} failed to establish media connection: `, error);
7252
7602
 
@@ -8179,7 +8529,7 @@ export default class Meeting extends StatelessWebexPlugin {
8179
8529
  if (layoutType) {
8180
8530
  if (!LAYOUT_TYPES.includes(layoutType)) {
8181
8531
  return this.rejectWithErrorLog(
8182
- 'Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType received.'
8532
+ `Meeting:index#changeVideoLayout --> cannot change video layout, invalid layoutType "${layoutType}" received.`
8183
8533
  );
8184
8534
  }
8185
8535
 
@@ -8335,6 +8685,12 @@ export default class Meeting extends StatelessWebexPlugin {
8335
8685
  correlationId: this.correlationId,
8336
8686
  muted,
8337
8687
  encoderImplementation: this.statsAnalyzer?.shareVideoEncoderImplementation,
8688
+ // TypeScript 4 does not recognize the `displaySurface` property. Instead of upgrading the
8689
+ // SDK to TypeScript 5, which may affect other packages, use bracket notation for now, since
8690
+ // all we're doing here is adding metrics.
8691
+ // eslint-disable-next-line dot-notation
8692
+ displaySurface: this.mediaProperties?.shareVideoStream?.getSettings()['displaySurface'],
8693
+ isMultistream: this.isMultistream,
8338
8694
  });
8339
8695
  };
8340
8696
 
@@ -8537,6 +8893,11 @@ export default class Meeting extends StatelessWebexPlugin {
8537
8893
  this.stopTranscription();
8538
8894
  this.transcription = undefined;
8539
8895
  }
8896
+
8897
+ this.annotation.deregisterEvents();
8898
+
8899
+ // @ts-ignore - fix types
8900
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
8540
8901
  };
8541
8902
 
8542
8903
  /**
@@ -8574,10 +8935,12 @@ export default class Meeting extends StatelessWebexPlugin {
8574
8935
 
8575
8936
  return;
8576
8937
  }
8577
- const {keepAliveUrl} = this.joinedWith;
8938
+
8578
8939
  const keepAliveInterval = (this.joinedWith.keepAliveSecs - 1) * 750; // taken from UCF
8579
8940
 
8580
8941
  this.keepAliveTimerId = setInterval(() => {
8942
+ const {keepAliveUrl} = this.joinedWith;
8943
+
8581
8944
  this.meetingRequest.keepAlive({keepAliveUrl}).catch((error) => {
8582
8945
  LoggerProxy.logger.warn(
8583
8946
  `Meeting:index#startKeepAlive --> Stopping sending keepAlives to ${keepAliveUrl} after error ${error}`