@webex/plugin-meetings 3.11.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/dist/aiEnableRequest/index.js +184 -0
  2. package/dist/aiEnableRequest/index.js.map +1 -0
  3. package/dist/aiEnableRequest/utils.js +36 -0
  4. package/dist/aiEnableRequest/utils.js.map +1 -0
  5. package/dist/annotation/index.js +14 -5
  6. package/dist/annotation/index.js.map +1 -1
  7. package/dist/breakouts/breakout.js +1 -1
  8. package/dist/breakouts/index.js +1 -1
  9. package/dist/config.js +5 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +28 -6
  12. package/dist/constants.js.map +1 -1
  13. package/dist/hashTree/constants.js +3 -1
  14. package/dist/hashTree/constants.js.map +1 -1
  15. package/dist/hashTree/hashTree.js +18 -0
  16. package/dist/hashTree/hashTree.js.map +1 -1
  17. package/dist/hashTree/hashTreeParser.js +709 -380
  18. package/dist/hashTree/hashTreeParser.js.map +1 -1
  19. package/dist/hashTree/types.js +4 -2
  20. package/dist/hashTree/types.js.map +1 -1
  21. package/dist/hashTree/utils.js +10 -0
  22. package/dist/hashTree/utils.js.map +1 -1
  23. package/dist/index.js +11 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/interceptors/constant.js +12 -0
  26. package/dist/interceptors/constant.js.map +1 -0
  27. package/dist/interceptors/dataChannelAuthToken.js +290 -0
  28. package/dist/interceptors/dataChannelAuthToken.js.map +1 -0
  29. package/dist/interceptors/index.js +7 -0
  30. package/dist/interceptors/index.js.map +1 -1
  31. package/dist/interceptors/utils.js +27 -0
  32. package/dist/interceptors/utils.js.map +1 -0
  33. package/dist/interpretation/index.js +2 -2
  34. package/dist/interpretation/index.js.map +1 -1
  35. package/dist/interpretation/siLanguage.js +1 -1
  36. package/dist/locus-info/controlsUtils.js +5 -3
  37. package/dist/locus-info/controlsUtils.js.map +1 -1
  38. package/dist/locus-info/index.js +217 -79
  39. package/dist/locus-info/index.js.map +1 -1
  40. package/dist/locus-info/selfUtils.js +1 -0
  41. package/dist/locus-info/selfUtils.js.map +1 -1
  42. package/dist/locus-info/types.js.map +1 -1
  43. package/dist/media/MediaConnectionAwaiter.js +57 -1
  44. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  45. package/dist/media/properties.js +4 -2
  46. package/dist/media/properties.js.map +1 -1
  47. package/dist/meeting/in-meeting-actions.js +7 -1
  48. package/dist/meeting/in-meeting-actions.js.map +1 -1
  49. package/dist/meeting/index.js +1082 -861
  50. package/dist/meeting/index.js.map +1 -1
  51. package/dist/meeting/request.js +50 -0
  52. package/dist/meeting/request.js.map +1 -1
  53. package/dist/meeting/request.type.js.map +1 -1
  54. package/dist/meeting/util.js +133 -3
  55. package/dist/meeting/util.js.map +1 -1
  56. package/dist/meetings/index.js +100 -45
  57. package/dist/meetings/index.js.map +1 -1
  58. package/dist/member/index.js +10 -0
  59. package/dist/member/index.js.map +1 -1
  60. package/dist/member/util.js +10 -0
  61. package/dist/member/util.js.map +1 -1
  62. package/dist/metrics/constants.js +2 -1
  63. package/dist/metrics/constants.js.map +1 -1
  64. package/dist/multistream/mediaRequestManager.js +9 -60
  65. package/dist/multistream/mediaRequestManager.js.map +1 -1
  66. package/dist/multistream/remoteMediaManager.js +11 -0
  67. package/dist/multistream/remoteMediaManager.js.map +1 -1
  68. package/dist/reachability/index.js +18 -10
  69. package/dist/reachability/index.js.map +1 -1
  70. package/dist/reactions/reactions.type.js.map +1 -1
  71. package/dist/reconnection-manager/index.js +0 -1
  72. package/dist/reconnection-manager/index.js.map +1 -1
  73. package/dist/types/aiEnableRequest/index.d.ts +5 -0
  74. package/dist/types/aiEnableRequest/utils.d.ts +2 -0
  75. package/dist/types/config.d.ts +3 -0
  76. package/dist/types/constants.d.ts +23 -1
  77. package/dist/types/hashTree/constants.d.ts +1 -0
  78. package/dist/types/hashTree/hashTree.d.ts +7 -0
  79. package/dist/types/hashTree/hashTreeParser.d.ts +99 -14
  80. package/dist/types/hashTree/types.d.ts +3 -0
  81. package/dist/types/hashTree/utils.d.ts +6 -0
  82. package/dist/types/index.d.ts +1 -0
  83. package/dist/types/interceptors/constant.d.ts +5 -0
  84. package/dist/types/interceptors/dataChannelAuthToken.d.ts +43 -0
  85. package/dist/types/interceptors/index.d.ts +2 -1
  86. package/dist/types/interceptors/utils.d.ts +1 -0
  87. package/dist/types/locus-info/index.d.ts +21 -2
  88. package/dist/types/locus-info/types.d.ts +1 -0
  89. package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
  90. package/dist/types/media/properties.d.ts +2 -1
  91. package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
  92. package/dist/types/meeting/index.d.ts +38 -6
  93. package/dist/types/meeting/request.d.ts +16 -1
  94. package/dist/types/meeting/request.type.d.ts +5 -0
  95. package/dist/types/meeting/util.d.ts +31 -0
  96. package/dist/types/meetings/index.d.ts +4 -2
  97. package/dist/types/member/index.d.ts +1 -0
  98. package/dist/types/member/util.d.ts +5 -0
  99. package/dist/types/metrics/constants.d.ts +1 -0
  100. package/dist/types/multistream/mediaRequestManager.d.ts +0 -23
  101. package/dist/types/reactions/reactions.type.d.ts +1 -0
  102. package/dist/types/webinar/utils.d.ts +6 -0
  103. package/dist/webinar/index.js +260 -90
  104. package/dist/webinar/index.js.map +1 -1
  105. package/dist/webinar/utils.js +25 -0
  106. package/dist/webinar/utils.js.map +1 -0
  107. package/package.json +24 -23
  108. package/src/aiEnableRequest/README.md +84 -0
  109. package/src/aiEnableRequest/index.ts +170 -0
  110. package/src/aiEnableRequest/utils.ts +25 -0
  111. package/src/annotation/index.ts +27 -7
  112. package/src/config.ts +3 -0
  113. package/src/constants.ts +29 -1
  114. package/src/hashTree/constants.ts +1 -0
  115. package/src/hashTree/hashTree.ts +17 -0
  116. package/src/hashTree/hashTreeParser.ts +627 -249
  117. package/src/hashTree/types.ts +4 -0
  118. package/src/hashTree/utils.ts +9 -0
  119. package/src/index.ts +8 -1
  120. package/src/interceptors/constant.ts +6 -0
  121. package/src/interceptors/dataChannelAuthToken.ts +170 -0
  122. package/src/interceptors/index.ts +2 -1
  123. package/src/interceptors/utils.ts +16 -0
  124. package/src/interpretation/index.ts +2 -2
  125. package/src/locus-info/controlsUtils.ts +11 -0
  126. package/src/locus-info/index.ts +231 -61
  127. package/src/locus-info/selfUtils.ts +1 -0
  128. package/src/locus-info/types.ts +1 -0
  129. package/src/media/MediaConnectionAwaiter.ts +41 -1
  130. package/src/media/properties.ts +3 -1
  131. package/src/meeting/in-meeting-actions.ts +12 -0
  132. package/src/meeting/index.ts +205 -44
  133. package/src/meeting/request.ts +42 -0
  134. package/src/meeting/request.type.ts +6 -0
  135. package/src/meeting/util.ts +160 -2
  136. package/src/meetings/index.ts +135 -41
  137. package/src/member/index.ts +10 -0
  138. package/src/member/util.ts +12 -0
  139. package/src/metrics/constants.ts +1 -0
  140. package/src/multistream/mediaRequestManager.ts +4 -54
  141. package/src/multistream/remoteMediaManager.ts +13 -0
  142. package/src/reachability/index.ts +9 -0
  143. package/src/reactions/reactions.type.ts +1 -0
  144. package/src/reconnection-manager/index.ts +0 -1
  145. package/src/webinar/index.ts +162 -5
  146. package/src/webinar/utils.ts +16 -0
  147. package/test/unit/spec/aiEnableRequest/index.ts +981 -0
  148. package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
  149. package/test/unit/spec/annotation/index.ts +69 -7
  150. package/test/unit/spec/hashTree/hashTree.ts +66 -0
  151. package/test/unit/spec/hashTree/hashTreeParser.ts +1869 -189
  152. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +210 -0
  153. package/test/unit/spec/interceptors/utils.ts +75 -0
  154. package/test/unit/spec/locus-info/controlsUtils.js +29 -0
  155. package/test/unit/spec/locus-info/index.js +383 -46
  156. package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
  157. package/test/unit/spec/media/properties.ts +12 -3
  158. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -2
  159. package/test/unit/spec/meeting/index.js +716 -115
  160. package/test/unit/spec/meeting/request.js +70 -0
  161. package/test/unit/spec/meeting/utils.js +438 -26
  162. package/test/unit/spec/meetings/index.js +652 -31
  163. package/test/unit/spec/member/index.js +28 -4
  164. package/test/unit/spec/member/util.js +65 -27
  165. package/test/unit/spec/multistream/mediaRequestManager.ts +2 -85
  166. package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
  167. package/test/unit/spec/reachability/index.ts +23 -0
  168. package/test/unit/spec/reconnection-manager/index.js +4 -8
  169. package/test/unit/spec/webinar/index.ts +348 -36
  170. package/test/unit/spec/webinar/utils.ts +39 -0
@@ -1,4 +1,5 @@
1
1
  import {LocalCameraStream, LocalMicrophoneStream} from '@webex/media-helpers';
2
+ import url from 'url';
2
3
 
3
4
  import {cloneDeep} from 'lodash';
4
5
  import {MeetingNotActiveError, UserNotJoinedError} from '../common/errors/webex-errors';
@@ -24,6 +25,7 @@ import PermissionError from '../common/errors/permission';
24
25
  import PasswordError from '../common/errors/password-error';
25
26
  import CaptchaError from '../common/errors/captcha-error';
26
27
  import Trigger from '../common/events/trigger-proxy';
28
+ import {ServerRoles} from '../member/types';
27
29
 
28
30
  const MeetingUtil = {
29
31
  parseLocusJoin: (response) => {
@@ -32,6 +34,7 @@ const MeetingUtil = {
32
34
  // First todo: add check for existance
33
35
  parsed.locus = response.body.locus;
34
36
  parsed.dataSets = response.body.dataSets;
37
+ parsed.metadata = response.body.metaData;
35
38
  parsed.mediaConnections = response.body.mediaConnections;
36
39
  parsed.locusUrl = parsed.locus.url;
37
40
  parsed.locusId = parsed.locus.url.split('/').pop();
@@ -47,6 +50,124 @@ const MeetingUtil = {
47
50
  return parsed;
48
51
  },
49
52
 
53
+ /**
54
+ * Sanitizes a WebSocket URL by extracting only protocol, host, and pathname
55
+ * Returns concatenated protocol + host + pathname for safe logging
56
+ * Note: This is used for logging only; URL matching uses partial matching via _urlsPartiallyMatch
57
+ * @param {string} urlString - The URL to sanitize
58
+ * @returns {string} Sanitized URL or empty string if parsing fails
59
+ */
60
+ sanitizeWebSocketUrl: (urlString: string): string => {
61
+ if (!urlString || typeof urlString !== 'string') {
62
+ return '';
63
+ }
64
+
65
+ try {
66
+ const parsedUrl = url.parse(urlString);
67
+ const protocol = parsedUrl.protocol || '';
68
+ const host = parsedUrl.host || '';
69
+
70
+ // If we don't have at least protocol and host, it's not a valid URL
71
+ if (!protocol || !host) {
72
+ return '';
73
+ }
74
+
75
+ const pathname = parsedUrl.pathname || '';
76
+
77
+ // Strip trailing slash if pathname is just '/'
78
+ const normalizedPathname = pathname === '/' ? '' : pathname;
79
+
80
+ return `${protocol}//${host}${normalizedPathname}`;
81
+ } catch (error) {
82
+ LoggerProxy.logger.warn(
83
+ `Meeting:util#sanitizeWebSocketUrl --> unable to parse URL: ${error}`
84
+ );
85
+
86
+ return '';
87
+ }
88
+ },
89
+
90
+ /**
91
+ * Checks if two URLs partially match using an endsWith approach
92
+ * Combines host and pathname, then checks if one ends with the other
93
+ * This handles cases where one URL goes through a proxy (e.g., /webproxy/) while the other is direct
94
+ * @param {string} url1 - First URL to compare
95
+ * @param {string} url2 - Second URL to compare
96
+ * @returns {boolean} True if one URL path ends with the other (partial match), false otherwise
97
+ */
98
+ _urlsPartiallyMatch: (url1: string, url2: string): boolean => {
99
+ if (!url1 || !url2) {
100
+ return false;
101
+ }
102
+
103
+ try {
104
+ const parsedUrl1 = url.parse(url1);
105
+ const parsedUrl2 = url.parse(url2);
106
+
107
+ const host1 = parsedUrl1.host || '';
108
+ const host2 = parsedUrl2.host || '';
109
+ const pathname1 = parsedUrl1.pathname || '';
110
+ const pathname2 = parsedUrl2.pathname || '';
111
+
112
+ // If either failed to parse, they don't match
113
+ if (!host1 || !host2 || !pathname1 || !pathname2) {
114
+ return false;
115
+ }
116
+
117
+ // Combine host and pathname for comparison
118
+ const combined1 = host1 + pathname1;
119
+ const combined2 = host2 + pathname2;
120
+
121
+ // Check if one combined path ends with the other (handles proxy URLs)
122
+ return combined1.endsWith(combined2) || combined2.endsWith(combined1);
123
+ } catch (e) {
124
+ LoggerProxy.logger.warn('Meeting:util#_urlsPartiallyMatch --> error comparing URLs', e);
125
+
126
+ return false;
127
+ }
128
+ },
129
+
130
+ /**
131
+ * Gets socket URL information for metrics, including whether the socket URLs match
132
+ * Uses partial matching to handle proxy URLs (e.g., URLs with /webproxy/ prefix)
133
+ * @param {Object} webex - The webex instance
134
+ * @returns {Object} Object with hasMismatchedSocket, mercurySocketUrl, and deviceSocketUrl properties
135
+ */
136
+ getSocketUrlInfo: (
137
+ webex: any
138
+ ): {hasMismatchedSocket: boolean; mercurySocketUrl: string; deviceSocketUrl: string} => {
139
+ try {
140
+ const mercuryUrl = webex?.internal?.mercury?.socket?.url;
141
+ const deviceUrl = webex?.internal?.device?.webSocketUrl;
142
+
143
+ const sanitizedMercuryUrl = MeetingUtil.sanitizeWebSocketUrl(mercuryUrl);
144
+ const sanitizedDeviceUrl = MeetingUtil.sanitizeWebSocketUrl(deviceUrl);
145
+
146
+ // Only report a mismatch if both URLs are present and they don't match
147
+ // If either URL is missing, we can't determine if there's a mismatch, so return false
148
+ let hasMismatchedSocket = false;
149
+ if (sanitizedMercuryUrl && sanitizedDeviceUrl) {
150
+ hasMismatchedSocket = !MeetingUtil._urlsPartiallyMatch(mercuryUrl, deviceUrl);
151
+ }
152
+
153
+ return {
154
+ hasMismatchedSocket,
155
+ mercurySocketUrl: sanitizedMercuryUrl,
156
+ deviceSocketUrl: sanitizedDeviceUrl,
157
+ };
158
+ } catch (error) {
159
+ LoggerProxy.logger.warn(
160
+ `Meeting:util#getSocketUrlInfo --> error getting socket URL info: ${error}`
161
+ );
162
+
163
+ return {
164
+ hasMismatchedSocket: false,
165
+ mercurySocketUrl: '',
166
+ deviceSocketUrl: '',
167
+ };
168
+ }
169
+ },
170
+
50
171
  remoteUpdateAudioVideo: (meeting, audioMuted?: boolean, videoMuted?: boolean) => {
51
172
  if (!meeting) {
52
173
  return Promise.reject(new ParameterError('You need a meeting object.'));
@@ -203,6 +324,7 @@ const MeetingUtil = {
203
324
  const parsed = MeetingUtil.parseLocusJoin(res);
204
325
  meeting.setLocus(parsed);
205
326
  meeting.isoLocalClientMeetingJoinTime = res?.headers?.date; // read from header if exist, else fall back to system clock : https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-555657
327
+ const socketUrlInfo = MeetingUtil.getSocketUrlInfo(webex);
206
328
  webex.internal.newMetrics.submitClientEvent({
207
329
  name: 'client.locus.join.response',
208
330
  payload: {
@@ -210,6 +332,9 @@ const MeetingUtil = {
210
332
  identifiers: {
211
333
  trackingId: res.headers.trackingid,
212
334
  },
335
+ eventData: {
336
+ ...socketUrlInfo,
337
+ },
213
338
  },
214
339
  options: {
215
340
  meetingId: meeting.id,
@@ -220,12 +345,19 @@ const MeetingUtil = {
220
345
  return parsed;
221
346
  })
222
347
  .catch((err) => {
348
+ const socketUrlInfo = MeetingUtil.getSocketUrlInfo(webex);
223
349
  webex.internal.newMetrics.submitClientEvent({
224
350
  name: 'client.locus.join.response',
225
351
  payload: {
226
352
  identifiers: {meetingLookupUrl: meeting.meetingInfo?.meetingLookupUrl},
353
+ eventData: {
354
+ ...socketUrlInfo,
355
+ },
356
+ },
357
+ options: {
358
+ meetingId: meeting.id,
359
+ rawError: err,
227
360
  },
228
- options: {meetingId: meeting.id, rawError: err},
229
361
  });
230
362
 
231
363
  throw err;
@@ -237,6 +369,7 @@ const MeetingUtil = {
237
369
  meeting.stopPeriodicLogUpload();
238
370
 
239
371
  meeting.breakouts.cleanUp();
372
+ meeting.webinar.cleanUp();
240
373
  meeting.simultaneousInterpretation.cleanUp();
241
374
  meeting.locusMediaRequest = undefined;
242
375
 
@@ -261,8 +394,10 @@ const MeetingUtil = {
261
394
  .then(() => meeting.stopKeepAlive())
262
395
  .then(() => {
263
396
  if (meeting.config?.enableAutomaticLLM) {
264
- meeting.updateLLMConnection();
397
+ return meeting.cleanupLLMConneciton({throwOnError: false});
265
398
  }
399
+
400
+ return undefined;
266
401
  });
267
402
  },
268
403
 
@@ -528,6 +663,11 @@ const MeetingUtil = {
528
663
  displayHints.includes(DISPLAY_HINTS.LEAVE_TRANSFER_HOST_END_MEETING) ||
529
664
  displayHints.includes(DISPLAY_HINTS.LEAVE_END_MEETING),
530
665
 
666
+ requireHostEndMeetingBeforeLeave: (displayHints) =>
667
+ displayHints.includes(DISPLAY_HINTS.REQUIRE_HOST_END_MEETING_BEFORE_LEAVE) ||
668
+ (!displayHints.includes(DISPLAY_HINTS.LEAVE_TRANSFER_HOST_END_MEETING) &&
669
+ displayHints.includes(DISPLAY_HINTS.END_MEETING)),
670
+
531
671
  canManageBreakout: (displayHints) => displayHints.includes(DISPLAY_HINTS.BREAKOUT_MANAGEMENT),
532
672
 
533
673
  canStartBreakout: (displayHints) => !displayHints.includes(DISPLAY_HINTS.DISABLE_BREAKOUT_START),
@@ -772,6 +912,24 @@ const MeetingUtil = {
772
912
  return locusDeltaRequest;
773
913
  },
774
914
 
915
+ canAttendeeRequestAiAssistantEnabled: (displayHints = [], roles: any[] = []) => {
916
+ const isHostOrCoHost =
917
+ roles.includes(ServerRoles.Cohost) || roles.includes(ServerRoles.Moderator);
918
+
919
+ if (isHostOrCoHost) {
920
+ return false;
921
+ }
922
+
923
+ if (displayHints.includes(DISPLAY_HINTS.ATTENDEE_REQUEST_AI_ASSISTANT_ENABLED)) {
924
+ return true;
925
+ }
926
+
927
+ return false;
928
+ },
929
+
930
+ attendeeRequestAiAssistantDeclinedAll: (displayHints = []) =>
931
+ displayHints.includes(DISPLAY_HINTS.ATTENDEE_REQUEST_AI_ASSISTANT_DECLINED_ALL),
932
+
775
933
  selfSupportsFeature: (feature: SELF_POLICY, userPolicies: Record<SELF_POLICY, boolean>) => {
776
934
  if (!userPolicies) {
777
935
  return true;
@@ -69,6 +69,7 @@ import JoinForbiddenError from '../common/errors/join-forbidden-error';
69
69
  import {HashTreeMessage} from '../hashTree/hashTreeParser';
70
70
  import {HashTreeObject} from '../hashTree/types';
71
71
  import {isSelf} from '../hashTree/utils';
72
+ import {createLocusFromHashTreeMessage} from '../locus-info';
72
73
 
73
74
  let mediaLogger;
74
75
 
@@ -195,6 +196,8 @@ export default class Meetings extends WebexPlugin {
195
196
  preferredWebexSite: any;
196
197
  reachability: Reachability;
197
198
  registered: any;
199
+ registrationPromise: Promise<void>;
200
+ unregistrationPromise: Promise<void>;
198
201
  request: any;
199
202
  geoHintInfo: any;
200
203
  meetingInfo: any;
@@ -520,41 +523,45 @@ export default class Meetings extends WebexPlugin {
520
523
  // };
521
524
  // rather then locus object change to locus url
522
525
 
523
- if (data.eventType !== LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
524
- if (
525
- data.locus &&
526
- data.locus.fullState &&
527
- data.locus.fullState.state === LOCUS.STATE.INACTIVE
528
- ) {
529
- // just ignore the event as its already ended and not active
530
- LoggerProxy.logger.warn(
531
- 'Meetings:index#handleLocusEvent --> Locus event received for meeting, after it was ended.'
532
- );
533
-
534
- return;
535
- }
526
+ if (data.eventType === LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
527
+ // We're about to create a new meeting object from this hash tree message.
528
+ // There is some existing (pre-hash trees) SDK logic here that requires a locus object
529
+ // (at the very minimum we need locus.url to be set)
530
+ // so we try to create locus from the received hash tree message
531
+ // it will not be complete, in most cases it will only have the self part, but that's still better than nothing
532
+ const {locus} = createLocusFromHashTreeMessage(data.stateElementsMessage);
533
+
534
+ data.locus = locus;
535
+ }
536
536
 
537
- // When its wireless share or guest and user leaves the meeting we dont have to keep the meeting object
538
- // Any future events will be neglected
539
-
540
- if (
541
- data.locus &&
542
- data.locus.self &&
543
- data.locus.self.state === _LEFT_ &&
544
- data.locus.self.removed === true
545
- ) {
546
- // just ignore the event as its already ended and not active
547
- LoggerProxy.logger.warn(
548
- 'Meetings:index#handleLocusEvent --> Locus event received for meeting, after it was ended.'
549
- );
537
+ if (
538
+ data.locus &&
539
+ data.locus.fullState &&
540
+ data.locus.fullState.state === LOCUS.STATE.INACTIVE
541
+ ) {
542
+ // just ignore the event as its already ended and not active
543
+ LoggerProxy.logger.warn(
544
+ 'Meetings:index#handleLocusEvent --> Locus event received for meeting, after it was ended.'
545
+ );
550
546
 
551
- return;
552
- }
547
+ return;
553
548
  }
554
549
 
555
- if (data.eventType === LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
556
- // in hash tree messages we don't ge the locus object, but the meeting constructor needs at least locus.url
557
- set(data, 'locus.url', data.stateElementsMessage.locusUrl);
550
+ // When its wireless share or guest and user leaves the meeting we dont have to keep the meeting object
551
+ // Any future events will be neglected
552
+
553
+ if (
554
+ data.locus &&
555
+ data.locus.self &&
556
+ data.locus.self.state === _LEFT_ &&
557
+ data.locus.self.removed === true
558
+ ) {
559
+ // just ignore the event as its already ended and not active
560
+ LoggerProxy.logger.warn(
561
+ 'Meetings:index#handleLocusEvent --> Locus event received for meeting, after it was ended.'
562
+ );
563
+
564
+ return;
558
565
  }
559
566
 
560
567
  this.create(data.locus, DESTINATION_TYPE.LOCUS_ID, useRandomDelayForInfo)
@@ -624,7 +631,7 @@ export default class Meetings extends WebexPlugin {
624
631
  }
625
632
 
626
633
  /**
627
- * handles locus events through mercury that are not roap
634
+ * handles locus events through mercury that are not roap or approval request events
628
635
  * @param {Object} envelope
629
636
  * @param {Object} envelope.data
630
637
  * @param {String} envelope.data.eventType
@@ -637,7 +644,11 @@ export default class Meetings extends WebexPlugin {
637
644
  // eslint-disable-next-line @typescript-eslint/no-shadow
638
645
  const {eventType} = data;
639
646
 
640
- if (eventType && eventType !== LOCUSEVENT.MESSAGE_ROAP) {
647
+ if (
648
+ eventType &&
649
+ eventType !== LOCUSEVENT.MESSAGE_ROAP &&
650
+ eventType !== LOCUSEVENT.APPROVAL_REQUEST
651
+ ) {
641
652
  this.handleLocusEvent(data, true);
642
653
  }
643
654
  }
@@ -929,9 +940,20 @@ export default class Meetings extends WebexPlugin {
929
940
  * @returns {Promise} A promise that resolves when the step is completed.
930
941
  */
931
942
  executeRegistrationStep(step: () => Promise<any>, stepName: string) {
932
- return step().then(() => {
933
- this.registrationStatus[stepName] = true;
934
- });
943
+ return step()
944
+ .then(() => {
945
+ LoggerProxy.logger.info(
946
+ `Meetings:index#executeRegistrationStep --> INFO, ${stepName} completed`
947
+ );
948
+ this.registrationStatus[stepName] = true;
949
+ })
950
+ .catch((error) => {
951
+ LoggerProxy.logger.error(
952
+ `Meetings:index#executeRegistrationStep --> ERROR, ${stepName} failed: ${error.message}`
953
+ );
954
+
955
+ return Promise.reject(error);
956
+ });
935
957
  }
936
958
 
937
959
  /**
@@ -944,7 +966,33 @@ export default class Meetings extends WebexPlugin {
944
966
  * @memberof Meetings
945
967
  */
946
968
  public register(deviceRegistrationOptions?: DeviceRegistrationOptions): Promise<any> {
947
- this.registrationStatus = clone(INITIAL_REGISTRATION_STATUS);
969
+ if (this.unregistrationPromise) {
970
+ LoggerProxy.logger.info(
971
+ 'Meetings:index#register --> INFO, Meetings plugin unregistration in progress, waiting to register'
972
+ );
973
+
974
+ this.registrationPromise = this.unregistrationPromise
975
+ .catch(() => {}) // It doesn't matter what happened during unregistration
976
+ .finally(() => {
977
+ LoggerProxy.logger.info(
978
+ 'Meetings:index#register --> INFO, Meetings plugin unregistration completed, proceeding to register'
979
+ );
980
+
981
+ this.registrationPromise = null;
982
+
983
+ return this.register(deviceRegistrationOptions);
984
+ });
985
+
986
+ return this.registrationPromise;
987
+ }
988
+
989
+ if (this.registrationPromise) {
990
+ LoggerProxy.logger.info(
991
+ 'Meetings:index#register --> INFO, Meetings plugin registration in progress, returning existing promise'
992
+ );
993
+
994
+ return this.registrationPromise;
995
+ }
948
996
 
949
997
  // @ts-ignore
950
998
  if (!this.webex.canAuthorize) {
@@ -963,7 +1011,11 @@ export default class Meetings extends WebexPlugin {
963
1011
  return Promise.resolve();
964
1012
  }
965
1013
 
966
- return Promise.all([
1014
+ LoggerProxy.logger.info('Meetings:index#register --> INFO, Registering Meetings plugin');
1015
+
1016
+ this.registrationStatus = clone(INITIAL_REGISTRATION_STATUS);
1017
+
1018
+ this.registrationPromise = Promise.all([
967
1019
  this.executeRegistrationStep(() => this.fetchUserPreferredWebexSite(), 'fetchWebexSite'),
968
1020
  this.executeRegistrationStep(() => this.getGeoHint(), 'getGeoHint'),
969
1021
  this.executeRegistrationStep(
@@ -1022,7 +1074,12 @@ export default class Meetings extends WebexPlugin {
1022
1074
  });
1023
1075
 
1024
1076
  return Promise.reject(error);
1077
+ })
1078
+ .finally(() => {
1079
+ this.registrationPromise = null;
1025
1080
  });
1081
+
1082
+ return this.registrationPromise;
1026
1083
  }
1027
1084
 
1028
1085
  /**
@@ -1034,6 +1091,35 @@ export default class Meetings extends WebexPlugin {
1034
1091
  * @memberof Meetings
1035
1092
  */
1036
1093
  unregister() {
1094
+ if (this.unregistrationPromise) {
1095
+ LoggerProxy.logger.info(
1096
+ 'Meetings:index#unregister --> INFO, Meetings plugin unregistration in progress, returning existing promise'
1097
+ );
1098
+
1099
+ return this.unregistrationPromise;
1100
+ }
1101
+
1102
+ if (this.registrationPromise) {
1103
+ LoggerProxy.logger.info(
1104
+ 'Meetings:index#unregister --> INFO, Meetings plugin registration in progress, waiting to unregister'
1105
+ );
1106
+
1107
+ // Wait for registration to complete (success or failure), then call unregister again
1108
+ this.unregistrationPromise = this.registrationPromise
1109
+ .catch(() => {}) // It doesn't matter what happened during registration
1110
+ .finally(() => {
1111
+ LoggerProxy.logger.info(
1112
+ 'Meetings:index#unregister --> INFO, Meetings plugin registration completed, proceeding to unregister'
1113
+ );
1114
+
1115
+ this.unregistrationPromise = null;
1116
+
1117
+ return this.unregister();
1118
+ });
1119
+
1120
+ return this.unregistrationPromise;
1121
+ }
1122
+
1037
1123
  if (!this.registered) {
1038
1124
  LoggerProxy.logger.info(
1039
1125
  'Meetings:index#unregister --> INFO, Meetings plugin already unregistered'
@@ -1044,10 +1130,14 @@ export default class Meetings extends WebexPlugin {
1044
1130
 
1045
1131
  this.stopListeningForEvents();
1046
1132
 
1047
- return (
1133
+ this.unregistrationPromise =
1048
1134
  // @ts-ignore
1049
1135
  this.webex.internal.mercury
1050
- .disconnect()
1136
+ // Use code 3050 with a non-reconnecting reason to prevent Mercury auto-reconnect
1137
+ // during unregister. Without this, disconnect() defaults to code 1000/"Done" which
1138
+ // force-closes as "Done (forced)" - a normalReconnectReason that triggers auto-reconnect,
1139
+ // causing a race condition with device.unregister().
1140
+ .disconnect({code: 3050, reason: 'meetings unregister'})
1051
1141
  // @ts-ignore
1052
1142
  .then(() => this.webex.internal.device.unregister())
1053
1143
  .catch((error) => {
@@ -1077,7 +1167,11 @@ export default class Meetings extends WebexPlugin {
1077
1167
  this.registered = false;
1078
1168
  this.registrationStatus = clone(INITIAL_REGISTRATION_STATUS);
1079
1169
  })
1080
- );
1170
+ .finally(() => {
1171
+ this.unregistrationPromise = null;
1172
+ });
1173
+
1174
+ return this.unregistrationPromise;
1081
1175
  }
1082
1176
 
1083
1177
  /**
@@ -13,6 +13,7 @@ export type MemberId = string;
13
13
  export default class Member {
14
14
  associatedUser: MemberId | null; // deprecated, use associatedUsers instead
15
15
  associatedUsers: Set<MemberId>; // users associated with this device, empty if this member is not a device
16
+ canApproveAIEnablement: boolean;
16
17
  canReclaimHost: boolean;
17
18
  id: MemberId;
18
19
  isAudioMuted: any;
@@ -291,6 +292,14 @@ export default class Member {
291
292
  */
292
293
  this.isPresenterAssignmentProhibited = null;
293
294
 
295
+ /**
296
+ * @instance
297
+ * @type {Boolean}
298
+ * @public
299
+ * @memberof Member
300
+ */
301
+ this.canApproveAIEnablement = null;
302
+
294
303
  /**
295
304
  * @instance
296
305
  * @type {Boolean}
@@ -360,6 +369,7 @@ export default class Member {
360
369
  MemberUtil.isModeratorAssignmentProhibited(participant);
361
370
  this.isPresenterAssignmentProhibited =
362
371
  MemberUtil.isPresenterAssignmentProhibited(participant);
372
+ this.canApproveAIEnablement = MemberUtil.canApproveAIEnablement(participant);
363
373
  this.processStatus(participant);
364
374
  this.processRoles(participant);
365
375
  // must be done last
@@ -42,6 +42,18 @@ const MemberUtil = {
42
42
  return participant.canReclaimHostRole || false;
43
43
  },
44
44
 
45
+ /**
46
+ * @param {Object} participant - The locus participant object.
47
+ * @returns {Boolean}
48
+ */
49
+ canApproveAIEnablement: (participant) => {
50
+ if (!participant) {
51
+ return false;
52
+ }
53
+
54
+ return !participant.attendeeRequestAiAssistantNotAllowed;
55
+ },
56
+
45
57
  /**
46
58
  * @param {Object} participant - The locus participant object.
47
59
  * @returns {[ServerRoleShape]}
@@ -90,6 +90,7 @@ const BEHAVIORAL_METRICS = {
90
90
  MEDIA_ISSUE_DETECTED: 'js_sdk_media_issue_detected',
91
91
  LOCUS_CLASSIC_VS_HASH_TREE_MISMATCH: 'js_sdk_locus_classic_vs_hash_tree_mismatch',
92
92
  LOCUS_HASH_TREE_UNSUPPORTED_OPERATION: 'js_sdk_locus_hash_tree_unsupported_operation',
93
+ MEDIA_STILL_NOT_CONNECTED: 'js_sdk_media_still_not_connected',
93
94
  };
94
95
 
95
96
  export {BEHAVIORAL_METRICS as default};
@@ -10,7 +10,7 @@ import {
10
10
  RecommendedOpusBitrates,
11
11
  NamedMediaGroup,
12
12
  } from '@webex/internal-media-core';
13
- import {cloneDeepWith, debounce, isEmpty} from 'lodash';
13
+ import {cloneDeepWith, debounce} from 'lodash';
14
14
 
15
15
  import LoggerProxy from '../common/logs/logger-proxy';
16
16
 
@@ -94,8 +94,6 @@ export class MediaRequestManager {
94
94
 
95
95
  private debouncedSourceUpdateListener: () => void;
96
96
 
97
- private previousStreamRequests: Array<StreamRequest> = [];
98
-
99
97
  private trimRequestsToNumOfSources: boolean;
100
98
  private numTotalSources: number;
101
99
  private numLiveSources: number;
@@ -161,36 +159,6 @@ export class MediaRequestManager {
161
159
  }
162
160
  }
163
161
 
164
- /**
165
- * Returns true if two stream requests are the same, false otherwise.
166
- *
167
- * @param {StreamRequest} streamRequestA - Stream request A for comparison.
168
- * @param {StreamRequest} streamRequestB - Stream request B for comparison.
169
- * @returns {boolean} - Whether they are equal.
170
- */
171
- // eslint-disable-next-line class-methods-use-this
172
- public isEqual(streamRequestA: StreamRequest, streamRequestB: StreamRequest) {
173
- return (
174
- JSON.stringify(streamRequestA._toJmpStreamRequest()) ===
175
- JSON.stringify(streamRequestB._toJmpStreamRequest())
176
- );
177
- }
178
-
179
- /**
180
- * Compares new stream requests to previous ones and determines
181
- * if they are the same.
182
- *
183
- * @param {StreamRequest[]} newRequests - Array with new requests.
184
- * @returns {boolean} - True if they are equal, false otherwise.
185
- */
186
- private checkIsNewRequestsEqualToPrev(newRequests: StreamRequest[]) {
187
- return (
188
- !isEmpty(this.previousStreamRequests) &&
189
- this.previousStreamRequests.length === newRequests.length &&
190
- this.previousStreamRequests.every((req, idx) => this.isEqual(req, newRequests[idx]))
191
- );
192
- }
193
-
194
162
  /**
195
163
  * Returns the maxPayloadBitsPerSecond per Stream
196
164
  *
@@ -230,15 +198,6 @@ export class MediaRequestManager {
230
198
  return (mediaRequest.codecInfo.maxFs * maxFps) / 100;
231
199
  }
232
200
 
233
- /**
234
- * Clears the previous stream requests.
235
- *
236
- * @returns {void}
237
- */
238
- public clearPreviousRequests(): void {
239
- this.previousStreamRequests = [];
240
- }
241
-
242
201
  /** Modifies the passed in clientRequests and makes sure that in total they don't ask
243
202
  * for more streams than there are available.
244
203
  *
@@ -356,7 +315,7 @@ export class MediaRequestManager {
356
315
  mr.receiveSlots.map((receiveSlot) => receiveSlot.wcmeReceiveSlot),
357
316
  this.getMaxPayloadBitsPerSecond(mr),
358
317
  mr.codecInfo && [
359
- new WcmeCodecInfo(
318
+ WcmeCodecInfo.fromH264(
360
319
  0x80,
361
320
  new H264Codec(
362
321
  mr.codecInfo.maxFs,
@@ -372,17 +331,8 @@ export class MediaRequestManager {
372
331
  }
373
332
  });
374
333
 
375
- //! IMPORTANT: this is only a temporary fix. This will soon be done in the jmp layer (@webex/json-multistream)
376
- // https://jira-eng-gpk2.cisco.com/jira/browse/WEBEX-326713
377
- if (!this.checkIsNewRequestsEqualToPrev(streamRequests)) {
378
- this.sendMediaRequestsCallback(streamRequests);
379
- this.previousStreamRequests = streamRequests;
380
- LoggerProxy.logger.info(`multistream:sendRequests --> media requests sent. `);
381
- } else {
382
- LoggerProxy.logger.info(
383
- `multistream:sendRequests --> detected duplicate WCME requests, skipping them... `
384
- );
385
- }
334
+ this.sendMediaRequestsCallback(streamRequests);
335
+ LoggerProxy.logger.info(`multistream:sendRequests --> media requests sent. `);
386
336
  }
387
337
 
388
338
  public addRequest(mediaRequest: MediaRequest, commit = true): MediaRequestId {
@@ -67,6 +67,18 @@ const AllEqualLayout: VideoLayout = {
67
67
  ],
68
68
  };
69
69
 
70
+ // An "all equal" grid, with size up to 5 x 5 = 25:
71
+ const AllEqual25Layout: VideoLayout = {
72
+ activeSpeakerVideoPaneGroups: [
73
+ {
74
+ id: 'main',
75
+ numPanes: 25,
76
+ size: 'best',
77
+ priority: 255,
78
+ },
79
+ ],
80
+ };
81
+
70
82
  // A layout with just a single remote active speaker video pane:
71
83
  const SingleLayout: VideoLayout = {
72
84
  activeSpeakerVideoPaneGroups: [
@@ -164,6 +176,7 @@ export const DefaultConfiguration: Configuration = {
164
176
 
165
177
  layouts: {
166
178
  AllEqual: AllEqualLayout,
179
+ AllEqual25: AllEqual25Layout,
167
180
  OnePlusFive: OnePlusFiveLayout,
168
181
  Single: SingleLayout,
169
182
  Stage: Stage2x2With6ThumbnailsLayout,