@webex/plugin-meetings 3.12.0-next.7 → 3.12.0-next.71

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 (178) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +15 -2
  3. package/dist/aiEnableRequest/index.js.map +1 -1
  4. package/dist/breakouts/breakout.js +8 -3
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +26 -2
  7. package/dist/breakouts/index.js.map +1 -1
  8. package/dist/config.js +2 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/constants.js +30 -7
  11. package/dist/constants.js.map +1 -1
  12. package/dist/controls-options-manager/constants.js +11 -1
  13. package/dist/controls-options-manager/constants.js.map +1 -1
  14. package/dist/controls-options-manager/index.js +38 -24
  15. package/dist/controls-options-manager/index.js.map +1 -1
  16. package/dist/controls-options-manager/util.js +91 -0
  17. package/dist/controls-options-manager/util.js.map +1 -1
  18. package/dist/hashTree/constants.js +13 -1
  19. package/dist/hashTree/constants.js.map +1 -1
  20. package/dist/hashTree/hashTreeParser.js +880 -382
  21. package/dist/hashTree/hashTreeParser.js.map +1 -1
  22. package/dist/hashTree/utils.js +42 -0
  23. package/dist/hashTree/utils.js.map +1 -1
  24. package/dist/index.js +7 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/interceptors/dataChannelAuthToken.js +75 -15
  27. package/dist/interceptors/dataChannelAuthToken.js.map +1 -1
  28. package/dist/interceptors/locusRetry.js +23 -8
  29. package/dist/interceptors/locusRetry.js.map +1 -1
  30. package/dist/interpretation/index.js +10 -1
  31. package/dist/interpretation/index.js.map +1 -1
  32. package/dist/interpretation/interpretation.types.js +7 -0
  33. package/dist/interpretation/interpretation.types.js.map +1 -0
  34. package/dist/interpretation/siLanguage.js +1 -1
  35. package/dist/locus-info/controlsUtils.js +4 -1
  36. package/dist/locus-info/controlsUtils.js.map +1 -1
  37. package/dist/locus-info/index.js +298 -87
  38. package/dist/locus-info/index.js.map +1 -1
  39. package/dist/locus-info/types.js +19 -0
  40. package/dist/locus-info/types.js.map +1 -1
  41. package/dist/media/index.js +3 -1
  42. package/dist/media/index.js.map +1 -1
  43. package/dist/media/properties.js +1 -0
  44. package/dist/media/properties.js.map +1 -1
  45. package/dist/meeting/in-meeting-actions.js +3 -1
  46. package/dist/meeting/in-meeting-actions.js.map +1 -1
  47. package/dist/meeting/index.js +1046 -689
  48. package/dist/meeting/index.js.map +1 -1
  49. package/dist/meeting/muteState.js +10 -1
  50. package/dist/meeting/muteState.js.map +1 -1
  51. package/dist/meeting/request.js +5 -2
  52. package/dist/meeting/request.js.map +1 -1
  53. package/dist/meeting/util.js +20 -2
  54. package/dist/meeting/util.js.map +1 -1
  55. package/dist/meeting-info/meeting-info-v2.js +2 -2
  56. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  57. package/dist/meetings/index.js +231 -78
  58. package/dist/meetings/index.js.map +1 -1
  59. package/dist/meetings/meetings.types.js +6 -1
  60. package/dist/meetings/meetings.types.js.map +1 -1
  61. package/dist/meetings/request.js +39 -0
  62. package/dist/meetings/request.js.map +1 -1
  63. package/dist/meetings/util.js +79 -5
  64. package/dist/meetings/util.js.map +1 -1
  65. package/dist/member/index.js +10 -0
  66. package/dist/member/index.js.map +1 -1
  67. package/dist/member/types.js.map +1 -1
  68. package/dist/member/util.js +3 -0
  69. package/dist/member/util.js.map +1 -1
  70. package/dist/metrics/constants.js +4 -1
  71. package/dist/metrics/constants.js.map +1 -1
  72. package/dist/multistream/codec/constants.js +63 -0
  73. package/dist/multistream/codec/constants.js.map +1 -0
  74. package/dist/multistream/mediaRequestManager.js +62 -15
  75. package/dist/multistream/mediaRequestManager.js.map +1 -1
  76. package/dist/multistream/receiveSlot.js +9 -0
  77. package/dist/multistream/receiveSlot.js.map +1 -1
  78. package/dist/reactions/reactions.type.js.map +1 -1
  79. package/dist/recording-controller/index.js +1 -3
  80. package/dist/recording-controller/index.js.map +1 -1
  81. package/dist/types/config.d.ts +2 -0
  82. package/dist/types/constants.d.ts +9 -1
  83. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  84. package/dist/types/controls-options-manager/index.d.ts +10 -0
  85. package/dist/types/hashTree/constants.d.ts +2 -0
  86. package/dist/types/hashTree/hashTreeParser.d.ts +146 -17
  87. package/dist/types/hashTree/utils.d.ts +18 -0
  88. package/dist/types/index.d.ts +3 -0
  89. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  90. package/dist/types/interpretation/interpretation.types.d.ts +10 -0
  91. package/dist/types/locus-info/index.d.ts +50 -6
  92. package/dist/types/locus-info/types.d.ts +21 -1
  93. package/dist/types/media/properties.d.ts +1 -0
  94. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  95. package/dist/types/meeting/index.d.ts +78 -5
  96. package/dist/types/meeting/request.d.ts +1 -0
  97. package/dist/types/meeting/util.d.ts +8 -0
  98. package/dist/types/meetings/index.d.ts +30 -2
  99. package/dist/types/meetings/meetings.types.d.ts +15 -0
  100. package/dist/types/meetings/request.d.ts +14 -0
  101. package/dist/types/member/index.d.ts +1 -0
  102. package/dist/types/member/types.d.ts +1 -0
  103. package/dist/types/member/util.d.ts +1 -0
  104. package/dist/types/metrics/constants.d.ts +3 -0
  105. package/dist/types/multistream/codec/constants.d.ts +7 -0
  106. package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
  107. package/dist/types/reactions/reactions.type.d.ts +3 -0
  108. package/dist/webinar/index.js +305 -159
  109. package/dist/webinar/index.js.map +1 -1
  110. package/package.json +22 -22
  111. package/src/aiEnableRequest/index.ts +16 -0
  112. package/src/breakouts/breakout.ts +3 -1
  113. package/src/breakouts/index.ts +31 -0
  114. package/src/config.ts +2 -0
  115. package/src/constants.ts +13 -2
  116. package/src/controls-options-manager/constants.ts +14 -1
  117. package/src/controls-options-manager/index.ts +47 -24
  118. package/src/controls-options-manager/util.ts +81 -1
  119. package/src/hashTree/constants.ts +16 -0
  120. package/src/hashTree/hashTreeParser.ts +580 -196
  121. package/src/hashTree/utils.ts +36 -0
  122. package/src/index.ts +6 -0
  123. package/src/interceptors/dataChannelAuthToken.ts +88 -12
  124. package/src/interceptors/locusRetry.ts +25 -4
  125. package/src/interpretation/index.ts +27 -9
  126. package/src/interpretation/interpretation.types.ts +11 -0
  127. package/src/locus-info/controlsUtils.ts +3 -1
  128. package/src/locus-info/index.ts +293 -97
  129. package/src/locus-info/types.ts +25 -1
  130. package/src/media/index.ts +3 -0
  131. package/src/media/properties.ts +1 -0
  132. package/src/meeting/in-meeting-actions.ts +4 -0
  133. package/src/meeting/index.ts +386 -48
  134. package/src/meeting/muteState.ts +10 -1
  135. package/src/meeting/request.ts +11 -0
  136. package/src/meeting/util.ts +21 -2
  137. package/src/meeting-info/meeting-info-v2.ts +4 -2
  138. package/src/meetings/index.ts +134 -44
  139. package/src/meetings/meetings.types.ts +19 -0
  140. package/src/meetings/request.ts +43 -0
  141. package/src/meetings/util.ts +97 -1
  142. package/src/member/index.ts +10 -0
  143. package/src/member/types.ts +1 -0
  144. package/src/member/util.ts +3 -0
  145. package/src/metrics/constants.ts +3 -0
  146. package/src/multistream/codec/constants.ts +58 -0
  147. package/src/multistream/mediaRequestManager.ts +119 -28
  148. package/src/multistream/receiveSlot.ts +18 -0
  149. package/src/reactions/reactions.type.ts +3 -0
  150. package/src/recording-controller/index.ts +1 -2
  151. package/src/webinar/index.ts +214 -36
  152. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  153. package/test/unit/spec/breakouts/breakout.ts +9 -3
  154. package/test/unit/spec/breakouts/index.ts +49 -0
  155. package/test/unit/spec/controls-options-manager/index.js +140 -29
  156. package/test/unit/spec/controls-options-manager/util.js +165 -0
  157. package/test/unit/spec/hashTree/hashTreeParser.ts +1838 -180
  158. package/test/unit/spec/hashTree/utils.ts +125 -1
  159. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +196 -0
  160. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  161. package/test/unit/spec/interpretation/index.ts +26 -4
  162. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  163. package/test/unit/spec/locus-info/index.js +487 -81
  164. package/test/unit/spec/media/index.ts +31 -0
  165. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  166. package/test/unit/spec/meeting/index.js +1240 -37
  167. package/test/unit/spec/meeting/muteState.js +81 -0
  168. package/test/unit/spec/meeting/request.js +12 -0
  169. package/test/unit/spec/meeting/utils.js +33 -0
  170. package/test/unit/spec/meeting-info/meetinginfov2.js +19 -10
  171. package/test/unit/spec/meetings/index.js +360 -10
  172. package/test/unit/spec/meetings/request.js +141 -0
  173. package/test/unit/spec/meetings/utils.js +189 -0
  174. package/test/unit/spec/member/index.js +7 -0
  175. package/test/unit/spec/member/util.js +24 -0
  176. package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
  177. package/test/unit/spec/recording-controller/index.js +9 -8
  178. package/test/unit/spec/webinar/index.ts +329 -28
@@ -375,7 +375,16 @@ export class MuteState {
375
375
  }
376
376
  if (muted !== undefined) {
377
377
  this.state.server.remoteMute = muted;
378
- this.muteLocalStream(meeting, muted, 'remotelyMuted');
378
+ // BO->Main may replay a stale remoteMute=false from the locus cache.
379
+ // Only enforce the mute on muted=true (also locking client.localMute so a later stale
380
+ // false can't flip isMuted()), and never touch the stream on muted=false — the
381
+ // legitimate server-driven unmute path is LOCAL_UNMUTE_REQUIRED. Always sync client
382
+ // intent back so other participants' tiles still show the mute icon.
383
+ if (muted) {
384
+ this.state.client.localMute = true;
385
+ this.muteLocalStream(meeting, true, 'remotelyMuted');
386
+ }
387
+ this.applyClientStateToServer(meeting);
379
388
  }
380
389
  }
381
390
 
@@ -26,6 +26,7 @@ import {
26
26
  SEND_DTMF_ENDPOINT,
27
27
  _SLIDES_,
28
28
  ANNOTATION,
29
+ INTERPRETATION,
29
30
  } from '../constants';
30
31
  import {
31
32
  SendReactionOptions,
@@ -135,6 +136,7 @@ export default class MeetingRequest extends StatelessWebexPlugin {
135
136
  locale?: string;
136
137
  deviceCapabilities?: Array<string>;
137
138
  liveAnnotationSupported: boolean;
139
+ enableSimultaneousInterpretation: boolean;
138
140
  alias?: string;
139
141
  clientMediaPreferences: ClientMediaPreferences;
140
142
  }) {
@@ -158,6 +160,7 @@ export default class MeetingRequest extends StatelessWebexPlugin {
158
160
  locale,
159
161
  deviceCapabilities = [],
160
162
  liveAnnotationSupported,
163
+ enableSimultaneousInterpretation,
161
164
  clientMediaPreferences,
162
165
  alias,
163
166
  } = options;
@@ -193,6 +196,14 @@ export default class MeetingRequest extends StatelessWebexPlugin {
193
196
  if (liveAnnotationSupported) {
194
197
  deviceCapabilities.push(ANNOTATION.ANNOTATION_ON_SHARE_SUPPORTED);
195
198
  }
199
+ if (enableSimultaneousInterpretation) {
200
+ deviceCapabilities.push(
201
+ INTERPRETATION.CAPABILITIES.HOST_CONTROL_SI_SUPPORTED,
202
+ INTERPRETATION.CAPABILITIES.INTERPRETER_CONTROL_SI_SUPPORTED,
203
+ INTERPRETATION.CAPABILITIES.SI_HANDOVER_SUPPORTED,
204
+ INTERPRETATION.CAPABILITIES.SIGN_INTERPRETER_SUPPORTED
205
+ );
206
+ }
196
207
 
197
208
  // append installationId to device config if it exists
198
209
  // @ts-ignore
@@ -317,6 +317,7 @@ const MeetingUtil = {
317
317
  locale: options.locale,
318
318
  deviceCapabilities: options.deviceCapabilities,
319
319
  liveAnnotationSupported: options.liveAnnotationSupported,
320
+ enableSimultaneousInterpretation: options.enableSimultaneousInterpretation,
320
321
  clientMediaPreferences,
321
322
  alias: options.alias,
322
323
  })
@@ -371,6 +372,7 @@ const MeetingUtil = {
371
372
  meeting.breakouts.cleanUp();
372
373
  meeting.webinar.cleanUp();
373
374
  meeting.simultaneousInterpretation.cleanUp();
375
+ meeting.locusInfo.cleanUp();
374
376
  meeting.locusMediaRequest = undefined;
375
377
 
376
378
  meeting.webex?.internal?.newMetrics?.callDiagnosticMetrics?.clearEventLimitsForCorrelationId(
@@ -845,6 +847,19 @@ const MeetingUtil = {
845
847
  requestBody.sequence = sequence;
846
848
  },
847
849
 
850
+ /**
851
+ * Checks if Locus API response contains a Locus DTO
852
+ *
853
+ * @param {any} response http response from Locus API call
854
+ * @returns {boolean} true if response contains a Locus DTO
855
+ */
856
+ isLocusDtoInAPIResponse(response: any) {
857
+ return (
858
+ response?.body?.locus || // for APIs called on our participant - locus is one of props in the response body
859
+ response?.body?.url // for APIs that act on locus itself (like mute all), the body is the locus
860
+ );
861
+ },
862
+
848
863
  /**
849
864
  * Updates the locus info for the meeting with the locus
850
865
  * information returned from API requests made to Locus
@@ -853,12 +868,13 @@ const MeetingUtil = {
853
868
  * @param {Object} response The response of the http request
854
869
  * @returns {Object}
855
870
  */
856
- updateLocusFromApiResponse: (meeting, response) => {
871
+ updateLocusFromApiResponse: (meeting: any, response: any) => {
857
872
  if (!meeting) {
858
873
  return response;
859
874
  }
860
875
 
861
- if (response?.body?.locus) {
876
+ // locus API responses can come in different shapes:
877
+ if (MeetingUtil.isLocusDtoInAPIResponse(response)) {
862
878
  meeting.locusInfo.handleLocusAPIResponse(meeting, response.body);
863
879
  }
864
880
 
@@ -930,6 +946,9 @@ const MeetingUtil = {
930
946
  attendeeRequestAiAssistantDeclinedAll: (displayHints = []) =>
931
947
  displayHints.includes(DISPLAY_HINTS.ATTENDEE_REQUEST_AI_ASSISTANT_DECLINED_ALL),
932
948
 
949
+ isAnonymizeDisplayNamesEnabled: (displayHints) =>
950
+ displayHints.includes(DISPLAY_HINTS.ANONYMOUS_DISPLAY_NAMES_ENABLED),
951
+
933
952
  selfSupportsFeature: (feature: SELF_POLICY, userPolicies: Record<SELF_POLICY, boolean>) => {
934
953
  if (!userPolicies) {
935
954
  return true;
@@ -19,7 +19,7 @@ const ADHOC_MEETING_DEFAULT_ERROR =
19
19
  const MEETING_IS_IN_PROGRESS_MESSAGE = 'Meeting is in progress';
20
20
  const STATIC_MEETING_LINK_ALREADY_EXISTS_MESSAGE = 'Static meeting link already exists';
21
21
  const FETCH_STATIC_MEETING_LINK = 'Meeting link does not exists for conversation';
22
- const CAPTCHA_ERROR_REQUIRES_PASSWORD_CODES = [423005, 423006];
22
+ const CAPTCHA_ERROR_REQUIRES_PASSWORD_CODES = [423005, 423006, 423008];
23
23
  const CAPTCHA_ERROR_REQUIRES_REGISTRATION_ID_CODES = [423007];
24
24
 
25
25
  const POLICY_ERROR_CODES = [403049, 403104, 403103, 403048, 403102, 403101];
@@ -35,7 +35,9 @@ const JOIN_FORBIDDEN_CODES = [403003];
35
35
  * 403137 - Registration ID verified failure
36
36
  *
37
37
  */
38
- const JOIN_WEBINAR_ERROR_CODES = [403021, 403022, 403024, 403137, 423007, 403026, 403037, 403137];
38
+ const JOIN_WEBINAR_ERROR_CODES = [
39
+ 403021, 403022, 403024, 403137, 423007, 403026, 403037, 403137, 403106,
40
+ ];
39
41
 
40
42
  /**
41
43
  * Error to indicate that wbxappapi requires a password
@@ -55,10 +55,12 @@ import PasswordError from '../common/errors/password-error';
55
55
  import CaptchaError from '../common/errors/captcha-error';
56
56
  import MeetingCollection from './collection';
57
57
  import {
58
+ FetchSitePreferencesMeViaSiteOptions,
58
59
  MEETING_KEY,
59
60
  INoiseReductionEffect,
60
61
  IVirtualBackgroundEffect,
61
62
  MeetingRegistrationStatus,
63
+ SitePreferencesResponse,
62
64
  } from './meetings.types';
63
65
  import MeetingsUtil from './util';
64
66
  import PermissionError from '../common/errors/permission';
@@ -69,7 +71,9 @@ import JoinForbiddenError from '../common/errors/join-forbidden-error';
69
71
  import {HashTreeMessage} from '../hashTree/hashTreeParser';
70
72
  import {HashTreeObject} from '../hashTree/types';
71
73
  import {isSelf} from '../hashTree/utils';
74
+
72
75
  import {createLocusFromHashTreeMessage, findMeetingForHashTreeMessage} from '../locus-info';
76
+ import {LocusDTO} from '../locus-info/types';
73
77
 
74
78
  let mediaLogger;
75
79
 
@@ -313,7 +317,7 @@ export default class Meetings extends WebexPlugin {
313
317
  const breakoutLocus = this.meetingCollection.getActiveBreakoutLocus(breakoutUrl);
314
318
 
315
319
  const isSelfJoined = newLocus?.self?.state === _JOINED_;
316
- const isSelfMoved = newLocus?.self?.state === _LEFT_ && newLocus?.self?.reason === _MOVED_;
320
+ const isSelfMoved = MeetingsUtil.isSelfMovedOrBreakoutEnded(newLocus);
317
321
  // @ts-ignore
318
322
  const deviceFromNewLocus = MeetingsUtil.getThisDevice(newLocus, this.webex.internal.device.url);
319
323
  const isResourceMovedOnThisDevice =
@@ -390,7 +394,7 @@ export default class Meetings extends WebexPlugin {
390
394
  private isNeedHandleLocusDTO(meeting: any, newLocus: any) {
391
395
  if (newLocus) {
392
396
  const isNewLocusAsBreakout = MeetingsUtil.isBreakoutLocusDTO(newLocus);
393
- const isSelfMoved = newLocus?.self?.state === _LEFT_ && newLocus?.self?.reason === _MOVED_;
397
+ const isSelfMoved = MeetingsUtil.isSelfMovedOrBreakoutEnded(newLocus);
394
398
  const isSelfMovedToLobby =
395
399
  newLocus?.self?.devices[0]?.intent?.reason === _ON_HOLD_LOBBY_ &&
396
400
  newLocus?.self?.devices[0]?.intent?.type === _WAIT_;
@@ -435,14 +439,11 @@ export default class Meetings extends WebexPlugin {
435
439
  if (existingMeeting) {
436
440
  return existingMeeting;
437
441
  }
438
-
439
442
  if (data.eventType === LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
440
443
  // need to check if maybe this event indicates a move to/from breakout
441
444
  const meetingForHashTreeMessage = findMeetingForHashTreeMessage(
442
- data.stateElementsMessage,
443
- this.meetingCollection,
444
- // @ts-ignore
445
- this.webex.internal.device.url
445
+ data?.stateElementsMessage,
446
+ this.meetingCollection
446
447
  );
447
448
 
448
449
  if (meetingForHashTreeMessage) {
@@ -492,7 +493,6 @@ export default class Meetings extends WebexPlugin {
492
493
  */
493
494
  private handleLocusEvent(data: LocusEvent, useRandomDelayForInfo = false) {
494
495
  let meeting = this.getCorrespondingMeetingByLocus(data);
495
-
496
496
  // @ts-ignore
497
497
  if (this.config.experimental.storeLocusHashTreeEventsForDebugging) {
498
498
  storeEventForDebugging('mercury', data);
@@ -586,17 +586,21 @@ export default class Meetings extends WebexPlugin {
586
586
  this.create(data.locus, DESTINATION_TYPE.LOCUS_ID, useRandomDelayForInfo)
587
587
  .then(async (newMeeting) => {
588
588
  meeting = newMeeting;
589
-
590
589
  try {
591
590
  // It's a new meeting so initialize the locus data
592
- await meeting.locusInfo.initialSetup({
593
- trigger:
594
- data.eventType === LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS
595
- ? 'get-loci-response'
596
- : 'locus-message',
597
- locus: data.locus,
598
- hashTreeMessage: data.stateElementsMessage,
599
- });
591
+ await meeting.locusInfo.initialSetup(
592
+ {
593
+ trigger:
594
+ data.eventType === LOCUSEVENT.SDK_LOCUS_FROM_SYNC_MEETINGS
595
+ ? 'get-loci-response'
596
+ : 'locus-message',
597
+ locus: data.locus,
598
+ hashTreeMessage: data.stateElementsMessage,
599
+ },
600
+ (locus: LocusDTO) => {
601
+ meeting.finalizeMeetingAfterInitialLocusSetup(locus);
602
+ }
603
+ );
600
604
  } catch (error) {
601
605
  LoggerProxy.logger.warn(
602
606
  `Meetings:index#handleLocusEvent --> Error initializing locus data: ${error.message}`
@@ -931,6 +935,27 @@ export default class Meetings extends WebexPlugin {
931
935
  }
932
936
  }
933
937
 
938
+ /**
939
+ * API to toggle AV1 codec support for video slides in multistream,
940
+ * needs to be called before webex.meetings.joinWithMedia()
941
+ *
942
+ * @param {Boolean} newValue
943
+ * @private
944
+ * @memberof Meetings
945
+ * @returns {undefined}
946
+ */
947
+ private _toggleEnableAv1SlidesSupport(newValue: boolean) {
948
+ if (typeof newValue !== 'boolean') {
949
+ return;
950
+ }
951
+
952
+ // @ts-ignore
953
+ if (this.config.enableAv1SlidesSupport !== newValue) {
954
+ // @ts-ignore
955
+ this.config.enableAv1SlidesSupport = newValue;
956
+ }
957
+ }
958
+
934
959
  /**
935
960
  * API to toggle stopping ICE Candidates Gathering after first relay candidate,
936
961
  * needs to be called before webex.meetings.joinWithMedia()
@@ -1403,6 +1428,31 @@ export default class Meetings extends WebexPlugin {
1403
1428
  return this.personalMeetingRoom;
1404
1429
  }
1405
1430
 
1431
+ /**
1432
+ * Fetches site preferences for the provided Webex site, or the preferred Webex site.
1433
+ * This is used to determine capabilities of the site, such as whether scheduling a webinar is supported.
1434
+ *
1435
+ * @param {object} [options]
1436
+ * @param {string} [options.siteUrl] - Webex site URL. Defaults to preferredWebexSite, for example "cisco.webex.com".
1437
+ * @param {string} [options.siteName] - Site name query override. Defaults to the site name derived from siteUrl, for example "cisco" for "cisco.webex.com".
1438
+ * @param {SitePreferenceSelectOption[]} [options.selectOptions] - Preference sections to fetch. Defaults to 'scheduling'.
1439
+ * @returns {Promise<SitePreferencesResponse>} site preferences response body
1440
+ * @throws {ParameterError}
1441
+ * @public
1442
+ * @memberof Meetings
1443
+ * @example
1444
+ * const preferences = await webex.meetings.fetchSitePreferencesMeViaSite();
1445
+ * const supportScheduleWebinar = preferences?.scheduling?.supportScheduleWebinar;
1446
+ */
1447
+ public fetchSitePreferencesMeViaSite(
1448
+ options: FetchSitePreferencesMeViaSiteOptions = {}
1449
+ ): Promise<SitePreferencesResponse> {
1450
+ return this.request.fetchSitePreferencesMeViaSite({
1451
+ ...options,
1452
+ siteUrl: options.siteUrl || this.preferredWebexSite,
1453
+ });
1454
+ }
1455
+
1406
1456
  /**
1407
1457
  * Returns basic information about a meeting that exists or
1408
1458
  * used to exist in the MeetingCollection
@@ -1765,10 +1815,14 @@ export default class Meetings extends WebexPlugin {
1765
1815
  extraParams: infoExtraParams,
1766
1816
  sendCAevents: !!callStateForMetrics?.correlationId, // if client sends correlation id as argument of public create(), then it means that this meeting creation is part of a pre-join intent from user
1767
1817
  };
1818
+ const shouldDeferMeetingInfoFetch = type === DESTINATION_TYPE.LOCUS_ID && !destination?.info;
1819
+
1820
+ const isOneOnOneCallLocus =
1821
+ type === DESTINATION_TYPE.LOCUS_ID && MeetingsUtil.isOneOnOneCall(destination);
1768
1822
 
1769
1823
  if (meetingInfo) {
1770
1824
  meeting.injectMeetingInfo(meetingInfo, meetingInfoOptions, meetingLookupUrl);
1771
- } else if (type !== DESTINATION_TYPE.ONE_ON_ONE_CALL) {
1825
+ } else if (type !== DESTINATION_TYPE.ONE_ON_ONE_CALL && !isOneOnOneCallLocus) {
1772
1826
  // ignore fetchMeetingInfo for 1:1 meetings
1773
1827
  if (enableUnifiedMeetings && !isMeetingActive && useRandomDelayForInfo && waitingTime > 0) {
1774
1828
  meeting.fetchMeetingInfoTimeoutId = setTimeout(
@@ -1776,8 +1830,12 @@ export default class Meetings extends WebexPlugin {
1776
1830
  waitingTime
1777
1831
  );
1778
1832
  meeting.parseMeetingInfo(undefined, destination);
1779
- } else {
1833
+ } else if (!shouldDeferMeetingInfoFetch) {
1780
1834
  await meeting.fetchMeetingInfo(meetingInfoOptions);
1835
+ } else {
1836
+ LoggerProxy.logger.info(
1837
+ 'Meetings:index#createMeeting --> defer fetchMeetingInfo for incomplete locus, will do it after locus initialSetup'
1838
+ );
1781
1839
  }
1782
1840
  }
1783
1841
  } catch (err) {
@@ -1811,7 +1869,11 @@ export default class Meetings extends WebexPlugin {
1811
1869
  // For type LOCUS_ID we need to parse the locus object to get the information
1812
1870
  // about the caller and callee
1813
1871
  // Meeting Added event will be created in `handleLocusEvent`
1814
- if (type !== DESTINATION_TYPE.LOCUS_ID) {
1872
+ // Only emit MEETING_ADDED if the meeting still exists in the collection.
1873
+ // If fetchMeetingInfo failed and the meeting was destroyed in the catch block,
1874
+ // skip emitting to prevent orphaned meeting references on the consumer side.
1875
+ // @ts-ignore - getMeetingByType types value as object but accepts strings (same as handleLocusEvent)
1876
+ if (type !== DESTINATION_TYPE.LOCUS_ID && this.getMeetingByType(_ID_, meeting.id)) {
1815
1877
  if (!meeting.sipUri) {
1816
1878
  meeting.setSipUri(destination);
1817
1879
  }
@@ -1886,23 +1948,23 @@ export default class Meetings extends WebexPlugin {
1886
1948
  * @public
1887
1949
  * @memberof Meetings
1888
1950
  */
1889
- public syncMeetings({keepOnlyLocusMeetings = true} = {}): Promise<void> {
1951
+ public async syncMeetings({
1952
+ keepOnlyLocusMeetings = true,
1953
+ skipHashTreeSync = false,
1954
+ } = {}): Promise<void> {
1890
1955
  // @ts-ignore
1891
1956
  if (this.webex.credentials.isUnverifiedGuest) {
1892
1957
  LoggerProxy.logger.info(
1893
- 'Meetings:index#syncMeetings --> skipping meeting sync as unverified guest'
1958
+ 'Meetings:index#syncMeetings --> user is unverified guest, skipping calling Locus for meeting sync'
1894
1959
  );
1895
-
1896
- return Promise.resolve();
1897
- }
1898
-
1899
- return this.request
1900
- .getActiveMeetings()
1901
- .then((locusArray) => {
1902
- const activeLocusUrl = [];
1960
+ } else {
1961
+ try {
1962
+ const locusArray = await this.request.getActiveMeetings();
1963
+ const activeLocusUrl: string[] = [];
1903
1964
 
1904
1965
  if (locusArray?.loci && locusArray.loci.length > 0) {
1905
1966
  const lociToUpdate = this.sortLocusArrayToUpdate(locusArray.loci);
1967
+
1906
1968
  lociToUpdate.forEach((locus) => {
1907
1969
  activeLocusUrl.push(locus.url);
1908
1970
  this.handleLocusEvent({
@@ -1920,21 +1982,50 @@ export default class Meetings extends WebexPlugin {
1920
1982
  // (they had a locusUrl previously but are no longer active) in the sync
1921
1983
  for (const meeting of Object.values(meetingsCollection)) {
1922
1984
  // @ts-ignore
1923
- const {locusUrl} = meeting;
1985
+ const {locusUrl, locusInfo} = meeting;
1924
1986
  if ((keepOnlyLocusMeetings || locusUrl) && !activeLocusUrl.includes(locusUrl)) {
1925
- // destroy function also uploads logs
1926
- // @ts-ignore
1927
- this.destroy(meeting, MEETING_REMOVED_REASON.NO_MEETINGS_TO_SYNC);
1987
+ const globalMeetingId = locusInfo?.info?.globalMeetingId;
1988
+
1989
+ if (
1990
+ globalMeetingId &&
1991
+ locusArray?.loci?.some(
1992
+ (locus: LocusDTO) => locus.info?.globalMeetingId === globalMeetingId
1993
+ )
1994
+ ) {
1995
+ // don't destroy the meeting as Locus API still returned some Locus that shares
1996
+ // the same globalMeetingId - that happens for example if a webinar user (who hasn't scheduled it)
1997
+ // is in a breakout and gets moved to a different breakout while we were offline
1998
+ } else {
1999
+ // destroy function also uploads logs
2000
+ // @ts-ignore
2001
+ this.destroy(meeting, MEETING_REMOVED_REASON.NO_MEETINGS_TO_SYNC);
2002
+ }
1928
2003
  }
1929
2004
  }
1930
2005
  }
1931
- })
1932
- .catch((error) => {
2006
+ } catch (error) {
1933
2007
  LoggerProxy.logger.error(
1934
2008
  `Meetings:index#syncMeetings --> failed to sync meetings, ${error}`
1935
2009
  );
1936
- throw new Error(error);
1937
- });
2010
+ throw error;
2011
+ }
2012
+ }
2013
+
2014
+ if (!skipHashTreeSync) {
2015
+ // Trigger hash tree syncs for all remaining meetings
2016
+ const remainingMeetings = this.meetingCollection.getAll();
2017
+ const syncPromises = [];
2018
+
2019
+ for (const meeting of Object.values(remainingMeetings) as any[]) {
2020
+ if (meeting.locusInfo) {
2021
+ syncPromises.push(meeting.locusInfo.syncAllHashTreeDatasets());
2022
+ }
2023
+ }
2024
+
2025
+ if (syncPromises.length > 0) {
2026
+ await Promise.all(syncPromises);
2027
+ }
2028
+ }
1938
2029
  }
1939
2030
 
1940
2031
  /**
@@ -1950,8 +2041,8 @@ export default class Meetings extends WebexPlugin {
1950
2041
  this.breakoutLocusForHandleLater = [];
1951
2042
  const lociToUpdate = [...mainLoci];
1952
2043
  breakoutLoci.forEach((breakoutLocus) => {
1953
- const associateMainLocus = mainLoci.find(
1954
- (mainLocus) => mainLocus.controls?.breakout?.url === breakoutLocus.controls?.breakout?.url
2044
+ const associateMainLocus = mainLoci.find((mainLocus) =>
2045
+ MeetingsUtil.isMainAssociatedWithBreakout(mainLocus, breakoutLocus)
1955
2046
  );
1956
2047
  const existCorrespondingMeeting = this.getCorrespondingMeetingByLocus({
1957
2048
  eventType: LOCUSEVENT.SDK_NO_EVENT,
@@ -1979,7 +2070,7 @@ export default class Meetings extends WebexPlugin {
1979
2070
  * @public
1980
2071
  * @memberof Meetings
1981
2072
  */
1982
- checkHandleBreakoutLocus(newCreatedLocus) {
2073
+ checkHandleBreakoutLocus(newCreatedLocus: any) {
1983
2074
  if (
1984
2075
  !newCreatedLocus ||
1985
2076
  !this.breakoutLocusForHandleLater ||
@@ -1990,9 +2081,8 @@ export default class Meetings extends WebexPlugin {
1990
2081
  if (MeetingsUtil.isBreakoutLocusDTO(newCreatedLocus)) {
1991
2082
  return;
1992
2083
  }
1993
- const existIndex = this.breakoutLocusForHandleLater.findIndex(
1994
- (breakoutLocus) =>
1995
- breakoutLocus.controls?.breakout?.url === newCreatedLocus.controls?.breakout?.url
2084
+ const existIndex = this.breakoutLocusForHandleLater.findIndex((breakoutLocus: any) =>
2085
+ MeetingsUtil.isMainAssociatedWithBreakout(newCreatedLocus, breakoutLocus)
1996
2086
  );
1997
2087
 
1998
2088
  if (existIndex < 0) {
@@ -31,3 +31,22 @@ export type MeetingRegistrationStatus = {
31
31
  mercuryConnect: boolean;
32
32
  checkH264Support: boolean;
33
33
  };
34
+
35
+ export enum SitePreferenceSelectOption {
36
+ SCHEDULING = 'scheduling',
37
+ }
38
+
39
+ export type FetchSitePreferencesMeViaSiteOptions = {
40
+ siteUrl?: string;
41
+ siteName?: string;
42
+ selectOptions?: SitePreferenceSelectOption[];
43
+ };
44
+
45
+ export const DEFAULT_SITE_PREFERENCE_SELECT_OPTIONS = [SitePreferenceSelectOption.SCHEDULING];
46
+
47
+ export type SitePreferencesResponse = {
48
+ scheduling?: {
49
+ supportScheduleWebinar?: boolean;
50
+ webinarWebLink?: string;
51
+ };
52
+ };
@@ -2,7 +2,14 @@
2
2
  import {StatelessWebexPlugin} from '@webex/webex-core';
3
3
 
4
4
  import LoggerProxy from '../common/logs/logger-proxy';
5
+ import ParameterError from '../common/errors/parameter';
5
6
  import {HTTP_VERBS, API, RESOURCE} from '../constants';
7
+ import {
8
+ DEFAULT_SITE_PREFERENCE_SELECT_OPTIONS,
9
+ type FetchSitePreferencesMeViaSiteOptions,
10
+ type SitePreferencesResponse,
11
+ } from './meetings.types';
12
+ import MeetingsUtil from './util';
6
13
 
7
14
  /**
8
15
  * @class MeetingRequest
@@ -45,6 +52,42 @@ export default class MeetingRequest extends StatelessWebexPlugin {
45
52
  return this.webex.internal.services.getMeetingPreferences();
46
53
  }
47
54
 
55
+ /**
56
+ * Fetches site preferences from a given site given a select option and a siteUrl with an optional siteName. If siteName is not provided, it will be derived from the siteUrl. If siteUrl is not provided, it will throw an error. If selectOptions is not provided, it will default to scheduling.
57
+ *
58
+ * @param {object} [options]
59
+ * @param {string} [options.siteUrl] - Webex site URL, for example "cisco.webex.com".
60
+ * @param {string} [options.siteName] - Site name query override. Defaults to the site name derived from options.siteUrl, e.g., "cisco".
61
+ * @param {SitePreferenceSelectOption[]} [options.selectOptions] - Preference sections to fetch. Defaults to 'scheduling'.
62
+ * @returns {Promise<SitePreferencesResponse>} site preferences response body
63
+ * @throws {ParameterError}
64
+ * @public
65
+ * @memberof MeetingRequest
66
+ */
67
+ fetchSitePreferencesMeViaSite(
68
+ options: FetchSitePreferencesMeViaSiteOptions = {}
69
+ ): Promise<SitePreferencesResponse> {
70
+ const {siteUrl, selectOptions = DEFAULT_SITE_PREFERENCE_SELECT_OPTIONS} = options;
71
+
72
+ if (!siteUrl) {
73
+ throw new ParameterError(
74
+ 'No siteUrl available. Call register() before fetching site preferences or provide options.siteUrl.'
75
+ );
76
+ }
77
+
78
+ // @ts-ignore - config comes from registerPlugin
79
+ const multipartSitePrefixList = this.config.meetings.multipartSitePrefixList || [];
80
+ const siteName = options.siteName || MeetingsUtil.getSiteName(siteUrl, multipartSitePrefixList);
81
+
82
+ // @ts-ignore
83
+ return this.request({
84
+ method: HTTP_VERBS.GET,
85
+ uri: `https://${siteUrl}/wbxappapi/v1/users/me/preference?select=${encodeURIComponent(
86
+ selectOptions.join(',')
87
+ )}&siteurl=${encodeURIComponent(siteName)}`,
88
+ }).then((res: any) => res.body);
89
+ }
90
+
48
91
  // locus federation, determines and populate locus if the responseBody has remote URLs to fetch locus details
49
92
 
50
93
  /**
@@ -1,12 +1,15 @@
1
1
  /* globals window */
2
2
 
3
3
  import {
4
+ _CALL_,
4
5
  _CREATED_,
5
6
  _INCOMING_,
6
7
  _JOINED_,
7
8
  _LEFT_,
8
9
  DESTINATION_TYPE,
9
10
  _MOVED_,
11
+ _SIP_BRIDGE_,
12
+ _SPACE_SHARE_,
10
13
  BREAKOUTS,
11
14
  EVENT_TRIGGERS,
12
15
  LOCUS,
@@ -18,6 +21,7 @@ import Trigger from '../common/events/trigger-proxy';
18
21
  import BEHAVIORAL_METRICS from '../metrics/constants';
19
22
  import Metrics from '../metrics';
20
23
  import {MEETING_KEY} from './meetings.types';
24
+ import {EndMeetingReason, LocusFullState} from '../locus-info/types';
21
25
 
22
26
  /**
23
27
  * Meetings Media Codec Missing Event
@@ -152,6 +156,30 @@ MeetingsUtil.parseDefaultSiteFromMeetingPreferences = (userPreferences) => {
152
156
  return result;
153
157
  };
154
158
 
159
+ MeetingsUtil.getSiteName = (site: string, multipartSitePrefixList: string[] = []) => {
160
+ if (!site) {
161
+ return null;
162
+ }
163
+
164
+ let siteName: string | undefined;
165
+
166
+ multipartSitePrefixList.forEach((multipartSitePrefix) => {
167
+ if (!siteName && site.includes(multipartSitePrefix)) {
168
+ const secondDot = site.indexOf('.', site.indexOf('.') + 1);
169
+
170
+ siteName = site.substring(0, secondDot);
171
+ }
172
+ });
173
+
174
+ if (siteName) {
175
+ return siteName;
176
+ }
177
+
178
+ siteName = site.substring(0, site.indexOf('.'));
179
+
180
+ return siteName;
181
+ };
182
+
155
183
  /**
156
184
  * Will check to see if the H.264 media codec is supported.
157
185
  * @async
@@ -266,6 +294,49 @@ MeetingsUtil.getThisDevice = (newLocus: any, deviceUrl: string) => {
266
294
  return null;
267
295
  };
268
296
 
297
+ /**
298
+ * Checks if the fullState indicates the meeting has fully ended (not just a breakout move).
299
+ * @param {Object} fullState locus fullState data
300
+ * @returns {boolean}
301
+ */
302
+ MeetingsUtil.isWholeMeetingEnded = (fullState: LocusFullState): boolean => {
303
+ return (
304
+ fullState.state === LOCUS.STATE.INACTIVE &&
305
+ fullState.endMeetingReason !== EndMeetingReason.breakoutEnded
306
+ );
307
+ };
308
+
309
+ /**
310
+ * Checks if the self state in a locus indicates a breakout move or breakout end.
311
+ * Returns true when:
312
+ * - self state is LEFT with reason MOVED (regular breakout move), OR
313
+ * - fullState is INACTIVE with endMeetingReason BREAKOUT_ENDED (breakout session ended)
314
+ * @param {Object} locus locus data
315
+ * @returns {boolean}
316
+ */
317
+ MeetingsUtil.isSelfMovedOrBreakoutEnded = (locus: any): boolean => {
318
+ const isSelfLeftMoved = locus?.self?.state === _LEFT_ && locus?.self?.reason === _MOVED_;
319
+ const isBreakoutEnded =
320
+ locus?.fullState?.state === LOCUS.STATE.INACTIVE &&
321
+ locus?.fullState?.endMeetingReason === EndMeetingReason.breakoutEnded;
322
+
323
+ return isSelfLeftMoved || isBreakoutEnded;
324
+ };
325
+
326
+ /**
327
+ * Checks if a locus is a 1:1 call using locus.fullState.type.
328
+ * Returns true when fullState.type is CALL, SIP_BRIDGE, or SPACE_SHARE.
329
+ * @param {Object} locus locus data
330
+ * @returns {boolean}
331
+ */
332
+ MeetingsUtil.isOneOnOneCall = (locus: any): boolean => {
333
+ const fullStateType = locus?.fullState?.type;
334
+
335
+ return (
336
+ fullStateType === _CALL_ || fullStateType === _SIP_BRIDGE_ || fullStateType === _SPACE_SHARE_
337
+ );
338
+ };
339
+
269
340
  /**
270
341
  * get self device joined status from locus data
271
342
  * @param {Object} meeting current meeting data
@@ -294,7 +365,10 @@ MeetingsUtil.joinedOnThisDevice = (meeting: any, newLocus: any, deviceUrl: strin
294
365
  * @private
295
366
  */
296
367
  MeetingsUtil.isBreakoutLocusDTO = (newLocus: any) => {
297
- return newLocus?.controls?.breakout?.sessionType === BREAKOUTS.SESSION_TYPES.BREAKOUT;
368
+ return (
369
+ newLocus?.controls?.breakout?.sessionType === BREAKOUTS.SESSION_TYPES.BREAKOUT ||
370
+ !!newLocus?.info?.isBreakout
371
+ );
298
372
  };
299
373
 
300
374
  /**
@@ -310,4 +384,26 @@ MeetingsUtil.isValidBreakoutLocus = (locus: any) => {
310
384
 
311
385
  return isLocusAsBreakout && !inActiveStatus && selfJoined;
312
386
  };
387
+ /**
388
+ * check if the breakout locus is associated with the main locus by comparing the breakout control url or the replaces info in self device
389
+ * @param {Object} mainLocus main locus data
390
+ * @param {Object} breakoutLocus breakout locus data
391
+ * @returns {boolean}
392
+ * @private
393
+ */
394
+ MeetingsUtil.isMainAssociatedWithBreakout = (mainLocus: any, breakoutLocus: any) => {
395
+ if (
396
+ mainLocus.controls?.breakout?.url &&
397
+ mainLocus.controls?.breakout?.url === breakoutLocus.controls?.breakout?.url
398
+ ) {
399
+ return true;
400
+ }
401
+ const deviceUrl = breakoutLocus?.self?.deviceUrl;
402
+ const replaceInfo = MeetingsUtil.getThisDevice(breakoutLocus, deviceUrl)?.replaces?.[0];
403
+ if (replaceInfo?.locusUrl && replaceInfo.locusUrl === mainLocus.url) {
404
+ return true;
405
+ }
406
+
407
+ return false;
408
+ };
313
409
  export default MeetingsUtil;