@webex/plugin-meetings 3.11.0 → 3.12.0-mobius-socket.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 (175) 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 +7 -2
  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 +868 -419
  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 +522 -131
  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 +1304 -928
  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 +117 -48
  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 +6 -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/multistream/sendSlotManager.js +116 -2
  69. package/dist/multistream/sendSlotManager.js.map +1 -1
  70. package/dist/reachability/index.js +18 -10
  71. package/dist/reachability/index.js.map +1 -1
  72. package/dist/reactions/reactions.type.js.map +1 -1
  73. package/dist/reconnection-manager/index.js +0 -1
  74. package/dist/reconnection-manager/index.js.map +1 -1
  75. package/dist/types/aiEnableRequest/index.d.ts +5 -0
  76. package/dist/types/aiEnableRequest/utils.d.ts +2 -0
  77. package/dist/types/config.d.ts +4 -0
  78. package/dist/types/constants.d.ts +23 -1
  79. package/dist/types/hashTree/constants.d.ts +1 -0
  80. package/dist/types/hashTree/hashTree.d.ts +7 -0
  81. package/dist/types/hashTree/hashTreeParser.d.ts +122 -14
  82. package/dist/types/hashTree/types.d.ts +3 -0
  83. package/dist/types/hashTree/utils.d.ts +6 -0
  84. package/dist/types/index.d.ts +1 -0
  85. package/dist/types/interceptors/constant.d.ts +5 -0
  86. package/dist/types/interceptors/dataChannelAuthToken.d.ts +43 -0
  87. package/dist/types/interceptors/index.d.ts +2 -1
  88. package/dist/types/interceptors/utils.d.ts +1 -0
  89. package/dist/types/locus-info/index.d.ts +60 -8
  90. package/dist/types/locus-info/types.d.ts +7 -0
  91. package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
  92. package/dist/types/media/properties.d.ts +2 -1
  93. package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
  94. package/dist/types/meeting/index.d.ts +72 -7
  95. package/dist/types/meeting/request.d.ts +16 -1
  96. package/dist/types/meeting/request.type.d.ts +5 -0
  97. package/dist/types/meeting/util.d.ts +31 -0
  98. package/dist/types/meetings/index.d.ts +4 -2
  99. package/dist/types/member/index.d.ts +1 -0
  100. package/dist/types/member/util.d.ts +5 -0
  101. package/dist/types/metrics/constants.d.ts +5 -0
  102. package/dist/types/multistream/mediaRequestManager.d.ts +0 -23
  103. package/dist/types/multistream/sendSlotManager.d.ts +23 -1
  104. package/dist/types/reactions/reactions.type.d.ts +1 -0
  105. package/dist/types/webinar/utils.d.ts +6 -0
  106. package/dist/webinar/index.js +438 -163
  107. package/dist/webinar/index.js.map +1 -1
  108. package/dist/webinar/utils.js +25 -0
  109. package/dist/webinar/utils.js.map +1 -0
  110. package/package.json +24 -23
  111. package/src/aiEnableRequest/README.md +84 -0
  112. package/src/aiEnableRequest/index.ts +170 -0
  113. package/src/aiEnableRequest/utils.ts +25 -0
  114. package/src/annotation/index.ts +27 -7
  115. package/src/config.ts +4 -0
  116. package/src/constants.ts +29 -1
  117. package/src/hashTree/constants.ts +1 -0
  118. package/src/hashTree/hashTree.ts +17 -0
  119. package/src/hashTree/hashTreeParser.ts +761 -260
  120. package/src/hashTree/types.ts +4 -0
  121. package/src/hashTree/utils.ts +9 -0
  122. package/src/index.ts +8 -1
  123. package/src/interceptors/constant.ts +6 -0
  124. package/src/interceptors/dataChannelAuthToken.ts +170 -0
  125. package/src/interceptors/index.ts +2 -1
  126. package/src/interceptors/utils.ts +16 -0
  127. package/src/interpretation/index.ts +2 -2
  128. package/src/locus-info/controlsUtils.ts +11 -0
  129. package/src/locus-info/index.ts +579 -113
  130. package/src/locus-info/selfUtils.ts +1 -0
  131. package/src/locus-info/types.ts +8 -0
  132. package/src/media/MediaConnectionAwaiter.ts +41 -1
  133. package/src/media/properties.ts +3 -1
  134. package/src/meeting/in-meeting-actions.ts +12 -0
  135. package/src/meeting/index.ts +389 -87
  136. package/src/meeting/request.ts +42 -0
  137. package/src/meeting/request.type.ts +6 -0
  138. package/src/meeting/util.ts +160 -2
  139. package/src/meetings/index.ts +157 -44
  140. package/src/member/index.ts +10 -0
  141. package/src/member/util.ts +12 -0
  142. package/src/metrics/constants.ts +6 -0
  143. package/src/multistream/mediaRequestManager.ts +4 -54
  144. package/src/multistream/remoteMediaManager.ts +13 -0
  145. package/src/multistream/sendSlotManager.ts +97 -3
  146. package/src/reachability/index.ts +9 -0
  147. package/src/reactions/reactions.type.ts +1 -0
  148. package/src/reconnection-manager/index.ts +0 -1
  149. package/src/webinar/index.ts +265 -6
  150. package/src/webinar/utils.ts +16 -0
  151. package/test/unit/spec/aiEnableRequest/index.ts +981 -0
  152. package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
  153. package/test/unit/spec/annotation/index.ts +69 -7
  154. package/test/unit/spec/hashTree/hashTree.ts +66 -0
  155. package/test/unit/spec/hashTree/hashTreeParser.ts +2321 -175
  156. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +210 -0
  157. package/test/unit/spec/interceptors/utils.ts +75 -0
  158. package/test/unit/spec/locus-info/controlsUtils.js +29 -0
  159. package/test/unit/spec/locus-info/index.js +1134 -55
  160. package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
  161. package/test/unit/spec/media/properties.ts +12 -3
  162. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -2
  163. package/test/unit/spec/meeting/index.js +884 -152
  164. package/test/unit/spec/meeting/request.js +70 -0
  165. package/test/unit/spec/meeting/utils.js +438 -26
  166. package/test/unit/spec/meetings/index.js +653 -32
  167. package/test/unit/spec/member/index.js +28 -4
  168. package/test/unit/spec/member/util.js +65 -27
  169. package/test/unit/spec/multistream/mediaRequestManager.ts +2 -85
  170. package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
  171. package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
  172. package/test/unit/spec/reachability/index.ts +23 -0
  173. package/test/unit/spec/reconnection-manager/index.js +4 -8
  174. package/test/unit/spec/webinar/index.ts +534 -37
  175. package/test/unit/spec/webinar/utils.ts +39 -0
@@ -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,
@@ -5,7 +5,11 @@ import {
5
5
  MultistreamRoapMediaConnection,
6
6
  NamedMediaGroup,
7
7
  StreamState,
8
+ MediaCodecMimeType,
9
+ CodecParameters,
8
10
  } from '@webex/internal-media-core';
11
+ import Metrics from '../metrics';
12
+ import BEHAVIORAL_METRICS from '../metrics/constants';
9
13
 
10
14
  /**
11
15
  * This class is used to manage the sendSlots for the given media types.
@@ -206,6 +210,8 @@ export default class SendSlotManager {
206
210
  }
207
211
 
208
212
  /**
213
+ * @deprecated Use {@link setCustomCodecParameters} instead, which requires specifying the codec MIME type.
214
+ *
209
215
  * This method is used to set the codec parameters for the sendSlot of the given mediaType
210
216
  * @param {MediaType} mediaType MediaType of the sendSlot for which the codec parameters needs to be set (AUDIO_MAIN/VIDEO_MAIN/AUDIO_SLIDES/VIDEO_SLIDES)
211
217
  * @param {Object} codecParameters
@@ -226,12 +232,19 @@ export default class SendSlotManager {
226
232
 
227
233
  await slot.setCodecParameters(codecParameters);
228
234
 
229
- this.LoggerProxy.logger.info(
230
- `SendSlotsManager->setCodecParameters#Set codec parameters for ${mediaType} to ${codecParameters}`
235
+ this.LoggerProxy.logger.warn(
236
+ 'SendSlotsManager->setCodecParameters --> [DEPRECATION WARNING]: setCodecParameters has been deprecated, use setCustomCodecParameters instead'
231
237
  );
238
+
239
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.DEPRECATED_SET_CODEC_PARAMETERS_USED, {
240
+ mediaType,
241
+ codecParameters,
242
+ });
232
243
  }
233
244
 
234
245
  /**
246
+ * @deprecated Use {@link markCustomCodecParametersForDeletion} instead, which requires specifying the codec MIME type.
247
+ *
235
248
  * This method is used to delete the codec parameters for the sendSlot of the given mediaType
236
249
  * @param {MediaType} mediaType MediaType of the sendSlot for which the codec parameters needs to be deleted (AUDIO_MAIN/VIDEO_MAIN/AUDIO_SLIDES/VIDEO_SLIDES)
237
250
  * @param {Array<String>} parameters Array of keys of the codec parameters to be deleted
@@ -246,8 +259,89 @@ export default class SendSlotManager {
246
259
 
247
260
  await slot.deleteCodecParameters(parameters);
248
261
 
262
+ this.LoggerProxy.logger.warn(
263
+ 'SendSlotsManager->deleteCodecParameters --> [DEPRECATION WARNING]: deleteCodecParameters has been deprecated, use markCustomCodecParametersForDeletion instead'
264
+ );
265
+
266
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.DEPRECATED_DELETE_CODEC_PARAMETERS_USED, {
267
+ mediaType,
268
+ parameters,
269
+ });
270
+ }
271
+
272
+ /**
273
+ * Sets custom codec parameters for the sendSlot of the given mediaType, scoped to a specific codec MIME type.
274
+ * Delegates to WCME's setCustomCodecParameters API.
275
+ * @param {MediaType} mediaType MediaType of the sendSlot
276
+ * @param {MediaCodecMimeType} codecMimeType The codec MIME type to apply parameters to (e.g. OPUS, H264, AV1)
277
+ * @param {CodecParameters} parameters Key-value pairs of codec parameters to set
278
+ * @returns {Promise<void>}
279
+ */
280
+ public async setCustomCodecParameters(
281
+ mediaType: MediaType,
282
+ codecMimeType: MediaCodecMimeType,
283
+ parameters: CodecParameters
284
+ ): Promise<void> {
285
+ const slot = this.slots.get(mediaType);
286
+
287
+ if (!slot) {
288
+ throw new Error(`Slot for ${mediaType} does not exist`);
289
+ }
290
+
291
+ try {
292
+ await slot.setCustomCodecParameters(codecMimeType, parameters);
293
+
294
+ this.LoggerProxy.logger.info(
295
+ `SendSlotsManager->setCustomCodecParameters#Set custom codec parameters for ${mediaType} (codec: ${codecMimeType}) to ${JSON.stringify(
296
+ parameters
297
+ )}`
298
+ );
299
+ } catch (error) {
300
+ this.LoggerProxy.logger.error(
301
+ `SendSlotsManager->setCustomCodecParameters#Failed to set custom codec parameters for ${mediaType} (codec: ${codecMimeType}): ${error}`
302
+ );
303
+ throw error;
304
+ } finally {
305
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.SET_CUSTOM_CODEC_PARAMETERS_USED, {
306
+ mediaType,
307
+ codecMimeType,
308
+ parameters,
309
+ });
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Marks custom codec parameters for deletion on the sendSlot of the given mediaType, scoped to a specific codec MIME type.
315
+ * Delegates to WCME's markCustomCodecParametersForDeletion API.
316
+ * @param {MediaType} mediaType MediaType of the sendSlot
317
+ * @param {MediaCodecMimeType} codecMimeType The codec MIME type whose parameters should be deleted (e.g. OPUS, H264, AV1)
318
+ * @param {string[]} parameters Array of parameter keys to delete
319
+ * @returns {Promise<void>}
320
+ */
321
+ public async markCustomCodecParametersForDeletion(
322
+ mediaType: MediaType,
323
+ codecMimeType: MediaCodecMimeType,
324
+ parameters: string[]
325
+ ): Promise<void> {
326
+ const slot = this.slots.get(mediaType);
327
+
328
+ if (!slot) {
329
+ throw new Error(`Slot for ${mediaType} does not exist`);
330
+ }
331
+
332
+ await slot.markCustomCodecParametersForDeletion(codecMimeType, parameters);
333
+
249
334
  this.LoggerProxy.logger.info(
250
- `SendSlotsManager->deleteCodecParameters#Deleted the following codec parameters -> ${parameters} for ${mediaType}`
335
+ `SendSlotsManager->markCustomCodecParametersForDeletion#Marked codec parameters for deletion -> ${parameters} for ${mediaType} (codec: ${codecMimeType})`
336
+ );
337
+
338
+ Metrics.sendBehavioralMetric(
339
+ BEHAVIORAL_METRICS.MARK_CUSTOM_CODEC_PARAMETERS_FOR_DELETION_USED,
340
+ {
341
+ mediaType,
342
+ codecMimeType,
343
+ parameters,
344
+ }
251
345
  );
252
346
  }
253
347
 
@@ -6,6 +6,7 @@
6
6
  import {isEqual, mapValues, mean} from 'lodash';
7
7
 
8
8
  import {Defer} from '@webex/common';
9
+ import {CapabilityState, WebCapabilities} from '@webex/web-capabilities';
9
10
  import LoggerProxy from '../common/logs/logger-proxy';
10
11
  import MeetingUtil from '../meeting/util';
11
12
 
@@ -196,6 +197,14 @@ export default class Reachability extends EventsScope {
196
197
  if (!this.webex.config.meetings.enableReachabilityChecks) {
197
198
  throw new Error('enableReachabilityChecks is disabled in config');
198
199
  }
200
+
201
+ if (WebCapabilities.supportsRTCPeerConnection() !== CapabilityState.CAPABLE) {
202
+ LoggerProxy.logger.warn(
203
+ 'Reachability:index#gatherReachability --> WebRTC API is not available, skipping reachability checks'
204
+ );
205
+
206
+ return {};
207
+ }
199
208
  // Fetch clusters and measure latency
200
209
  try {
201
210
  this.lastTrigger = trigger;
@@ -40,6 +40,7 @@ export enum SkinToneType {
40
40
  }
41
41
 
42
42
  export type Sender = {
43
+ displayName: string;
43
44
  participantId: string;
44
45
  };
45
46
 
@@ -609,7 +609,6 @@ export default class ReconnectionManager {
609
609
  if (this.meeting.isMultistream) {
610
610
  Object.values(this.meeting.mediaRequestManagers).forEach(
611
611
  (mediaRequestManager: MediaRequestManager) => {
612
- mediaRequestManager.clearPreviousRequests();
613
612
  mediaRequestManager.commit();
614
613
  }
615
614
  );
@@ -4,10 +4,21 @@
4
4
  import {WebexPlugin, config} from '@webex/webex-core';
5
5
  import uuid from 'uuid';
6
6
  import {get} from 'lodash';
7
- import {_ID_, HEADERS, HTTP_VERBS, MEETINGS, SELF_ROLES, SHARE_STATUS} from '../constants';
7
+ import {DataChannelTokenType} from '@webex/internal-plugin-llm';
8
+ import {
9
+ _ID_,
10
+ HEADERS,
11
+ HTTP_VERBS,
12
+ MEETINGS,
13
+ SELF_ROLES,
14
+ SHARE_STATUS,
15
+ DEFAULT_LARGE_SCALE_WEBINAR_ATTENDEE_SEARCH_LIMIT,
16
+ LLM_PRACTICE_SESSION,
17
+ } from '../constants';
8
18
 
9
19
  import WebinarCollection from './collection';
10
20
  import LoggerProxy from '../common/logs/logger-proxy';
21
+ import {sanitizeParams} from './utils';
11
22
 
12
23
  /**
13
24
  * @class Webinar
@@ -28,6 +39,14 @@ const Webinar = WebexPlugin.extend({
28
39
  meetingId: 'string',
29
40
  },
30
41
 
42
+ /**
43
+ * Calls this to clean up listeners
44
+ * @returns {void}
45
+ */
46
+ cleanUp() {
47
+ this.cleanupPSDataChannel();
48
+ },
49
+
31
50
  /**
32
51
  * Update the current locus url of the webinar
33
52
  * @param {string} locusUrl
@@ -96,10 +115,7 @@ const Webinar = WebexPlugin.extend({
96
115
  meeting?.locusInfo?.updateMediaShares(meeting?.locusInfo?.mediaShares, true);
97
116
  }
98
117
 
99
- if (this.practiceSessionEnabled) {
100
- // may need change data channel in practice session
101
- meeting?.updateLLMConnection();
102
- }
118
+ this.updatePSDataChannel();
103
119
  },
104
120
 
105
121
  /**
@@ -110,6 +126,215 @@ const Webinar = WebexPlugin.extend({
110
126
  return this.selfIsPanelist && this.practiceSessionEnabled;
111
127
  },
112
128
 
129
+ /**
130
+ * Disconnects the practice session data channel and removes its relay listener.
131
+ * @returns {Promise<void>}
132
+ */
133
+ async cleanupPSDataChannel() {
134
+ if (this._pendingOnlineListener) {
135
+ // @ts-ignore - Fix type
136
+ this.webex.internal.llm.off('online', this._pendingOnlineListener);
137
+ this._pendingOnlineListener = null;
138
+ }
139
+
140
+ const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
141
+
142
+ // @ts-ignore - Fix type
143
+ await this.webex.internal.llm.disconnectLLM(
144
+ {
145
+ code: 3050,
146
+ reason: 'done (permanent)',
147
+ },
148
+ LLM_PRACTICE_SESSION
149
+ );
150
+ // @ts-ignore - Fix type
151
+ this.webex.internal.llm.off(
152
+ `event:relay.event:${LLM_PRACTICE_SESSION}`,
153
+ meeting?.processRelayEvent
154
+ );
155
+ },
156
+
157
+ /**
158
+ * Ensures practice-session token exists before registering the practice LLM channel.
159
+ * @param {object} meeting
160
+ * @returns {Promise<string|undefined>}
161
+ */
162
+ async ensurePracticeSessionDatachannelToken(meeting) {
163
+ // @ts-ignore
164
+ const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
165
+
166
+ if (!isDataChannelTokenEnabled) {
167
+ return undefined;
168
+ }
169
+
170
+ // @ts-ignore
171
+ const cachedToken = this.webex.internal.llm.getDatachannelToken(
172
+ DataChannelTokenType.PracticeSession
173
+ );
174
+
175
+ if (cachedToken) {
176
+ return cachedToken;
177
+ }
178
+
179
+ try {
180
+ const refreshResponse = await meeting.refreshDataChannelToken();
181
+ const {datachannelToken, dataChannelTokenType} = refreshResponse?.body ?? {};
182
+
183
+ if (!datachannelToken) {
184
+ return undefined;
185
+ }
186
+
187
+ // @ts-ignore
188
+ this.webex.internal.llm.setDatachannelToken(
189
+ datachannelToken,
190
+ dataChannelTokenType || DataChannelTokenType.PracticeSession
191
+ );
192
+
193
+ return datachannelToken;
194
+ } catch (error) {
195
+ LoggerProxy.logger.warn(
196
+ `Webinar:index#ensurePracticeSessionDatachannelToken --> failed to proactively refresh practice-session token: ${
197
+ error?.message || String(error)
198
+ }`
199
+ );
200
+
201
+ return undefined;
202
+ }
203
+ },
204
+
205
+ /**
206
+ * Connects to low latency mercury and reconnects if the address has changed
207
+ * It will also disconnect if called when the meeting has ended
208
+ * @returns {Promise}
209
+ */
210
+ async updatePSDataChannel() {
211
+ this._updatePSDataChannelSequence = (this._updatePSDataChannelSequence || 0) + 1;
212
+ const invocationSequence = this._updatePSDataChannelSequence;
213
+
214
+ const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
215
+ const isPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
216
+
217
+ if (!isPracticeSession) {
218
+ await this.cleanupPSDataChannel();
219
+
220
+ return undefined;
221
+ }
222
+
223
+ // @ts-ignore - Fix type
224
+ const {url = undefined, info: {practiceSessionDatachannelUrl = undefined} = {}} =
225
+ meeting?.locusInfo || {};
226
+
227
+ // @ts-ignore
228
+ let practiceSessionDatachannelToken = this.webex.internal.llm.getDatachannelToken(
229
+ DataChannelTokenType.PracticeSession
230
+ );
231
+
232
+ const isCaptionBoxOn = this.webex.internal.voicea.getIsCaptionBoxOn();
233
+
234
+ if (!practiceSessionDatachannelUrl) {
235
+ return undefined;
236
+ }
237
+ // @ts-ignore - Fix type
238
+ if (this.webex.internal.llm.isConnected(LLM_PRACTICE_SESSION)) {
239
+ if (
240
+ // @ts-ignore - Fix type
241
+ url === this.webex.internal.llm.getLocusUrl(LLM_PRACTICE_SESSION) &&
242
+ // @ts-ignore - Fix type
243
+ practiceSessionDatachannelUrl ===
244
+ this.webex.internal.llm.getDatachannelUrl(LLM_PRACTICE_SESSION)
245
+ ) {
246
+ return undefined;
247
+ }
248
+
249
+ await this.cleanupPSDataChannel();
250
+ }
251
+
252
+ // Ensure the default session data channel is connected before connecting the practice session.
253
+ // Subscribe before checking isConnected() to avoid a race where the 'online' event fires
254
+ // between the check and the subscription — Mercury does not replay missed events.
255
+ if (!this._pendingOnlineListener) {
256
+ const onDefaultSessionConnected = () => {
257
+ this._pendingOnlineListener = null;
258
+ // @ts-ignore - Fix type
259
+ this.webex.internal.llm.off('online', onDefaultSessionConnected);
260
+ this.updatePSDataChannel();
261
+ };
262
+ this._pendingOnlineListener = onDefaultSessionConnected;
263
+ // @ts-ignore - Fix type
264
+ this.webex.internal.llm.on('online', onDefaultSessionConnected);
265
+ }
266
+
267
+ // @ts-ignore - Fix type
268
+ if (!this.webex.internal.llm.isConnected()) {
269
+ LoggerProxy.logger.info(
270
+ 'Webinar:index#updatePSDataChannel --> default session not yet connected, deferring practice session connect.'
271
+ );
272
+
273
+ return undefined;
274
+ }
275
+
276
+ // Default session is already connected — cancel the pending listener and proceed
277
+ if (this._pendingOnlineListener) {
278
+ // @ts-ignore - Fix type
279
+ this.webex.internal.llm.off('online', this._pendingOnlineListener);
280
+ this._pendingOnlineListener = null;
281
+ }
282
+
283
+ const refreshedPracticeSessionToken = await this.ensurePracticeSessionDatachannelToken(meeting);
284
+
285
+ const latestPracticeSessionDatachannelUrl = get(
286
+ meeting,
287
+ 'locusInfo.info.practiceSessionDatachannelUrl'
288
+ );
289
+ const isStillPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
290
+
291
+ // Skip stale invocations after async refresh to avoid reconnecting a session
292
+ // that was already updated/cleaned by a newer state transition.
293
+ if (
294
+ invocationSequence !== this._updatePSDataChannelSequence ||
295
+ !isStillPracticeSession ||
296
+ !latestPracticeSessionDatachannelUrl ||
297
+ latestPracticeSessionDatachannelUrl !== practiceSessionDatachannelUrl
298
+ ) {
299
+ return undefined;
300
+ }
301
+
302
+ if (refreshedPracticeSessionToken) {
303
+ practiceSessionDatachannelToken = refreshedPracticeSessionToken;
304
+ }
305
+
306
+ // @ts-ignore - Fix type
307
+ return this.webex.internal.llm
308
+ .registerAndConnect(
309
+ url,
310
+ practiceSessionDatachannelUrl,
311
+ practiceSessionDatachannelToken,
312
+ LLM_PRACTICE_SESSION
313
+ )
314
+ .then((registerAndConnectResult) => {
315
+ // @ts-ignore - Fix type
316
+ this.webex.internal.llm.off(
317
+ `event:relay.event:${LLM_PRACTICE_SESSION}`,
318
+ meeting?.processRelayEvent
319
+ );
320
+ // @ts-ignore - Fix type
321
+ this.webex.internal.llm.on(
322
+ `event:relay.event:${LLM_PRACTICE_SESSION}`,
323
+ meeting?.processRelayEvent
324
+ );
325
+ // @ts-ignore - Fix type
326
+ this.webex.internal.voicea?.announce?.();
327
+ if (isCaptionBoxOn) {
328
+ this.webex.internal.voicea.updateSubchannelSubscriptions({subscribe: ['transcription']});
329
+ }
330
+ LoggerProxy.logger.info(
331
+ `Webinar:index#updatePSDataChannel --> enabled to receive relay events for default session for ${LLM_PRACTICE_SESSION}!`
332
+ );
333
+
334
+ return Promise.resolve(registerAndConnectResult);
335
+ });
336
+ },
337
+
113
338
  /**
114
339
  * start or stop practice session for webinar
115
340
  * @param {boolean} enabled
@@ -137,6 +362,7 @@ const Webinar = WebexPlugin.extend({
137
362
  */
138
363
  updatePracticeSessionStatus(payload) {
139
364
  this.set('practiceSessionEnabled', !!payload?.enabled);
365
+ this.updatePSDataChannel().then(() => {});
140
366
  },
141
367
 
142
368
  /**
@@ -243,7 +469,6 @@ const Webinar = WebexPlugin.extend({
243
469
 
244
470
  /**
245
471
  * view all webcast attendees
246
- * @param {string} queryString
247
472
  * @returns {Promise}
248
473
  */
249
474
  async viewAllWebcastAttendees() {
@@ -297,6 +522,40 @@ const Webinar = WebexPlugin.extend({
297
522
  throw error;
298
523
  });
299
524
  },
525
+
526
+ /**
527
+ * search large scale webinar attendees
528
+ * @param {object} payload
529
+ * @param {string} payload.queryString
530
+ * @param {number} payload.limit
531
+ * @param {string} payload.next
532
+ * @returns {Promise}
533
+ */
534
+ async searchLargeScaleWebinarAttendees(payload) {
535
+ const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
536
+ const rawParams = {
537
+ search_text: payload?.queryString,
538
+ limit: payload?.limit ?? DEFAULT_LARGE_SCALE_WEBINAR_ATTENDEE_SEARCH_LIMIT,
539
+ next: payload?.next,
540
+ };
541
+ const attendeeSearchUrl = meeting?.locusInfo?.links?.resources?.attendeeSearch?.url;
542
+ if (!attendeeSearchUrl) {
543
+ LoggerProxy.logger.error(`Meeting:webinar5k#searchLargeScaleWebinarAttendees failed`);
544
+ throw new Error('Meeting:webinar5k#Attendee search url is not available');
545
+ }
546
+
547
+ return this.request({
548
+ method: HTTP_VERBS.GET,
549
+ uri: `${attendeeSearchUrl}?${new URLSearchParams(sanitizeParams(rawParams)).toString()}`,
550
+ headers: {
551
+ authorization: await this.webex.credentials.getUserToken(),
552
+ trackingId: `${config.trackingIdPrefix}_${uuid.v4().toString()}`,
553
+ },
554
+ }).catch((error) => {
555
+ LoggerProxy.logger.error('Meeting:webinar5k#searchLargeScaleWebinarAttendees failed', error);
556
+ throw error;
557
+ });
558
+ },
300
559
  });
301
560
 
302
561
  export default Webinar;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Remove null/undefined/empty string values from an object
3
+ * @param {object} params
4
+ * @returns {object}
5
+ */
6
+ export const sanitizeParams = (params: Record<string, any>) => {
7
+ const result: Record<string, any> = {};
8
+ Object.keys(params).forEach((key) => {
9
+ const value = params[key];
10
+ if (value !== null && value !== undefined && value !== '') {
11
+ result[key] = value;
12
+ }
13
+ });
14
+
15
+ return result;
16
+ };