@webex/plugin-meetings 3.11.0-next.4 → 3.11.0-next.41

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 (146) 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 +3 -3
  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 +26 -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 +233 -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/interpretation/index.js +2 -2
  32. package/dist/interpretation/index.js.map +1 -1
  33. package/dist/interpretation/siLanguage.js +1 -1
  34. package/dist/locus-info/controlsUtils.js +5 -3
  35. package/dist/locus-info/controlsUtils.js.map +1 -1
  36. package/dist/locus-info/index.js +125 -68
  37. package/dist/locus-info/index.js.map +1 -1
  38. package/dist/locus-info/selfUtils.js +1 -0
  39. package/dist/locus-info/selfUtils.js.map +1 -1
  40. package/dist/locus-info/types.js.map +1 -1
  41. package/dist/media/MediaConnectionAwaiter.js +57 -1
  42. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  43. package/dist/media/properties.js +4 -2
  44. package/dist/media/properties.js.map +1 -1
  45. package/dist/meeting/in-meeting-actions.js +7 -1
  46. package/dist/meeting/in-meeting-actions.js.map +1 -1
  47. package/dist/meeting/index.js +209 -90
  48. package/dist/meeting/index.js.map +1 -1
  49. package/dist/meeting/request.js +50 -0
  50. package/dist/meeting/request.js.map +1 -1
  51. package/dist/meeting/request.type.js.map +1 -1
  52. package/dist/meeting/util.js +128 -2
  53. package/dist/meeting/util.js.map +1 -1
  54. package/dist/meetings/index.js +78 -36
  55. package/dist/meetings/index.js.map +1 -1
  56. package/dist/member/index.js +10 -0
  57. package/dist/member/index.js.map +1 -1
  58. package/dist/member/util.js +10 -0
  59. package/dist/member/util.js.map +1 -1
  60. package/dist/metrics/constants.js +2 -1
  61. package/dist/metrics/constants.js.map +1 -1
  62. package/dist/multistream/mediaRequestManager.js +1 -1
  63. package/dist/multistream/mediaRequestManager.js.map +1 -1
  64. package/dist/multistream/remoteMediaManager.js +11 -0
  65. package/dist/multistream/remoteMediaManager.js.map +1 -1
  66. package/dist/reactions/reactions.type.js.map +1 -1
  67. package/dist/types/aiEnableRequest/index.d.ts +5 -0
  68. package/dist/types/aiEnableRequest/utils.d.ts +2 -0
  69. package/dist/types/config.d.ts +3 -0
  70. package/dist/types/constants.d.ts +21 -1
  71. package/dist/types/hashTree/constants.d.ts +1 -0
  72. package/dist/types/hashTree/hashTree.d.ts +7 -0
  73. package/dist/types/hashTree/hashTreeParser.d.ts +99 -14
  74. package/dist/types/hashTree/types.d.ts +3 -0
  75. package/dist/types/hashTree/utils.d.ts +6 -0
  76. package/dist/types/index.d.ts +1 -0
  77. package/dist/types/interceptors/constant.d.ts +5 -0
  78. package/dist/types/interceptors/dataChannelAuthToken.d.ts +35 -0
  79. package/dist/types/interceptors/index.d.ts +2 -1
  80. package/dist/types/locus-info/index.d.ts +9 -2
  81. package/dist/types/locus-info/types.d.ts +1 -0
  82. package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
  83. package/dist/types/media/properties.d.ts +2 -1
  84. package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
  85. package/dist/types/meeting/index.d.ts +24 -2
  86. package/dist/types/meeting/request.d.ts +16 -1
  87. package/dist/types/meeting/request.type.d.ts +5 -0
  88. package/dist/types/meeting/util.d.ts +31 -0
  89. package/dist/types/meetings/index.d.ts +4 -2
  90. package/dist/types/member/index.d.ts +1 -0
  91. package/dist/types/member/util.d.ts +5 -0
  92. package/dist/types/metrics/constants.d.ts +1 -0
  93. package/dist/types/reactions/reactions.type.d.ts +1 -0
  94. package/dist/webinar/index.js +1 -1
  95. package/package.json +22 -22
  96. package/src/aiEnableRequest/README.md +84 -0
  97. package/src/aiEnableRequest/index.ts +170 -0
  98. package/src/aiEnableRequest/utils.ts +25 -0
  99. package/src/annotation/index.ts +7 -4
  100. package/src/config.ts +3 -0
  101. package/src/constants.ts +26 -1
  102. package/src/hashTree/constants.ts +1 -0
  103. package/src/hashTree/hashTree.ts +17 -0
  104. package/src/hashTree/hashTreeParser.ts +627 -249
  105. package/src/hashTree/types.ts +4 -0
  106. package/src/hashTree/utils.ts +9 -0
  107. package/src/index.ts +8 -1
  108. package/src/interceptors/constant.ts +6 -0
  109. package/src/interceptors/dataChannelAuthToken.ts +142 -0
  110. package/src/interceptors/index.ts +2 -1
  111. package/src/interpretation/index.ts +2 -2
  112. package/src/locus-info/controlsUtils.ts +11 -0
  113. package/src/locus-info/index.ts +146 -58
  114. package/src/locus-info/selfUtils.ts +1 -0
  115. package/src/locus-info/types.ts +1 -0
  116. package/src/media/MediaConnectionAwaiter.ts +41 -1
  117. package/src/media/properties.ts +3 -1
  118. package/src/meeting/in-meeting-actions.ts +12 -0
  119. package/src/meeting/index.ts +127 -17
  120. package/src/meeting/request.ts +42 -0
  121. package/src/meeting/request.type.ts +6 -0
  122. package/src/meeting/util.ts +156 -1
  123. package/src/meetings/index.ts +94 -9
  124. package/src/member/index.ts +10 -0
  125. package/src/member/util.ts +12 -0
  126. package/src/metrics/constants.ts +1 -0
  127. package/src/multistream/mediaRequestManager.ts +1 -1
  128. package/src/multistream/remoteMediaManager.ts +13 -0
  129. package/src/reactions/reactions.type.ts +1 -0
  130. package/test/unit/spec/aiEnableRequest/index.ts +981 -0
  131. package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
  132. package/test/unit/spec/hashTree/hashTree.ts +66 -0
  133. package/test/unit/spec/hashTree/hashTreeParser.ts +1869 -189
  134. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +141 -0
  135. package/test/unit/spec/locus-info/controlsUtils.js +29 -0
  136. package/test/unit/spec/locus-info/index.js +201 -45
  137. package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
  138. package/test/unit/spec/media/properties.ts +12 -3
  139. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -2
  140. package/test/unit/spec/meeting/index.js +441 -75
  141. package/test/unit/spec/meeting/request.js +64 -0
  142. package/test/unit/spec/meeting/utils.js +433 -22
  143. package/test/unit/spec/meetings/index.js +550 -10
  144. package/test/unit/spec/member/index.js +28 -4
  145. package/test/unit/spec/member/util.js +65 -27
  146. package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
@@ -13,7 +13,7 @@ import {
13
13
  CALL_DIAGNOSTIC_CONFIG,
14
14
  RtcMetrics,
15
15
  } from '@webex/internal-plugin-metrics';
16
- import {ClientEvent as RawClientEvent} from '@webex/event-dictionary-ts';
16
+ import type {ClientEvent as RawClientEvent} from '@webex/event-dictionary-ts';
17
17
 
18
18
  import {
19
19
  ConnectionState,
@@ -33,6 +33,8 @@ import {
33
33
  InboundAudioIssueSubTypes,
34
34
  } from '@webex/internal-media-core';
35
35
 
36
+ import {DataChannelTokenType} from '@webex/internal-plugin-llm';
37
+
36
38
  import {
37
39
  LocalStream,
38
40
  LocalCameraStream,
@@ -179,8 +181,9 @@ import JoinForbiddenError from '../common/errors/join-forbidden-error';
179
181
  import {ReachabilityMetrics} from '../reachability/reachability.types';
180
182
  import {SetStageOptions, SetStageVideoLayout, UnsetStageVideoLayout} from './request.type';
181
183
  import {Invitee} from './type';
182
- import {DataSet} from '../hashTree/hashTreeParser';
184
+ import {DataSet, Metadata} from '../hashTree/hashTreeParser';
183
185
  import {LocusDTO} from '../locus-info/types';
186
+ import AIEnableRequest from '../aiEnableRequest';
184
187
 
185
188
  // default callback so we don't call an undefined function, but in practice it should never be used
186
189
  const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL';
@@ -576,6 +579,7 @@ export default class Meeting extends StatelessWebexPlugin {
576
579
  breakouts: any;
577
580
  simultaneousInterpretation: any;
578
581
  annotation: any;
582
+ aiEnableRequest: any;
579
583
  webinar: any;
580
584
  conversationUrl: string;
581
585
  callStateForMetrics: CallStateForMetrics;
@@ -683,6 +687,7 @@ export default class Meeting extends StatelessWebexPlugin {
683
687
  localAudioStreamMuteStateHandler: () => void;
684
688
  localVideoStreamMuteStateHandler: () => void;
685
689
  localOutputTrackChangeHandler: () => void;
690
+ localConstraintsChangeHandler: () => void;
686
691
  environment: string;
687
692
  namespace = MEETINGS;
688
693
  allowMediaInLobby: boolean;
@@ -899,6 +904,10 @@ export default class Meeting extends StatelessWebexPlugin {
899
904
  */
900
905
  // @ts-ignore
901
906
  this.simultaneousInterpretation = new SimultaneousInterpretation({}, {parent: this.webex});
907
+
908
+ // @ts-ignore
909
+ this.aiEnableRequest = new AIEnableRequest({}, {parent: this.webex});
910
+
902
911
  /**
903
912
  * @instance
904
913
  * @type {Annotation}
@@ -1548,6 +1557,12 @@ export default class Meeting extends StatelessWebexPlugin {
1548
1557
  }
1549
1558
  };
1550
1559
 
1560
+ this.localConstraintsChangeHandler = () => {
1561
+ if (!this.isMultistream) {
1562
+ this.mediaProperties.webrtcMediaConnection?.updatePreferredBitrateKbps();
1563
+ }
1564
+ };
1565
+
1551
1566
  /**
1552
1567
  * Promise that exists if SDP offer has been generated, and resolves once sdp answer is received.
1553
1568
  * @instance
@@ -2957,6 +2972,18 @@ export default class Meeting extends StatelessWebexPlugin {
2957
2972
  );
2958
2973
  });
2959
2974
 
2975
+ this.locusInfo.on(
2976
+ LOCUSINFO.EVENTS.CONTROLS_AI_SUMMARY_NOTIFICATION_UPDATED,
2977
+ ({aiSummaryNotification}) => {
2978
+ Trigger.trigger(
2979
+ this,
2980
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
2981
+ EVENT_TRIGGERS.MEETING_CONTROLS_AI_SUMMARY_NOTIFICATION_UPDATED,
2982
+ {aiSummaryNotification}
2983
+ );
2984
+ }
2985
+ );
2986
+
2960
2987
  this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_WEBCAST_CHANGED, ({state}) => {
2961
2988
  Trigger.trigger(
2962
2989
  this,
@@ -3408,6 +3435,8 @@ export default class Meeting extends StatelessWebexPlugin {
3408
3435
  this.recordingController.setLocusUrl(this.locusUrl);
3409
3436
  this.controlsOptionsManager.setLocusUrl(this.locusUrl, !!isMainLocus);
3410
3437
  this.webinar.locusUrlUpdate(url);
3438
+ // @ts-ignore
3439
+ this.webex.internal.llm.setRefreshHandler(() => this.refreshDataChannelToken());
3411
3440
 
3412
3441
  Trigger.trigger(
3413
3442
  this,
@@ -3438,6 +3467,7 @@ export default class Meeting extends StatelessWebexPlugin {
3438
3467
  this.breakouts.breakoutServiceUrlUpdate(payload?.services?.breakout?.url);
3439
3468
  this.annotation.approvalUrlUpdate(payload?.services?.approval?.url);
3440
3469
  this.simultaneousInterpretation.approvalUrlUpdate(payload?.services?.approval?.url);
3470
+ this.aiEnableRequest.approvalUrlUpdate(payload?.services?.approval?.url);
3441
3471
  });
3442
3472
  }
3443
3473
 
@@ -3767,6 +3797,10 @@ export default class Meeting extends StatelessWebexPlugin {
3767
3797
  );
3768
3798
  });
3769
3799
 
3800
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ID_CHANGED, (payload) => {
3801
+ this.aiEnableRequest.selfParticipantIdUpdate(payload.selfId);
3802
+ });
3803
+
3770
3804
  this.locusInfo.on(LOCUSINFO.EVENTS.SELF_MEETING_INTERPRETATION_CHANGED, (payload) => {
3771
3805
  const targetChanged = this.simultaneousInterpretation.updateSelfInterpretation(payload);
3772
3806
  Trigger.trigger(
@@ -4269,6 +4303,9 @@ export default class Meeting extends StatelessWebexPlugin {
4269
4303
  bothLeaveAndEndMeetingAvailable: MeetingUtil.bothLeaveAndEndMeetingAvailable(
4270
4304
  this.userDisplayHints
4271
4305
  ),
4306
+ requireHostEndMeetingBeforeLeave: MeetingUtil.requireHostEndMeetingBeforeLeave(
4307
+ this.userDisplayHints
4308
+ ),
4272
4309
  canEnableClosedCaption: MeetingUtil.canEnableClosedCaption(this.userDisplayHints),
4273
4310
  canStartTranscribing: MeetingUtil.canStartTranscribing(this.userDisplayHints),
4274
4311
  canStopTranscribing: MeetingUtil.canStopTranscribing(this.userDisplayHints),
@@ -4527,6 +4564,12 @@ export default class Meeting extends StatelessWebexPlugin {
4527
4564
  requiredHints: [DISPLAY_HINTS.DISABLE_ATTENDEE_START_POLLING_QA],
4528
4565
  displayHints: this.userDisplayHints,
4529
4566
  }),
4567
+ canAttendeeRequestAiAssistantEnabled: MeetingUtil.canAttendeeRequestAiAssistantEnabled(
4568
+ this.userDisplayHints,
4569
+ this.roles
4570
+ ),
4571
+ isAttendeeRequestAiAssistantDeclinedAll:
4572
+ MeetingUtil.attendeeRequestAiAssistantDeclinedAll(this.userDisplayHints),
4530
4573
  }) || changed;
4531
4574
  }
4532
4575
  if (changed) {
@@ -4598,7 +4641,8 @@ export default class Meeting extends StatelessWebexPlugin {
4598
4641
  mediaId: string;
4599
4642
  host: object;
4600
4643
  selfId: string;
4601
- dataSets: DataSet[];
4644
+ dataSets: DataSet[]; // only sent by Locus when hash trees are used
4645
+ metadata: Metadata; // only sent by Locus when hash trees are used
4602
4646
  }) {
4603
4647
  const mtgLocus: any = data.locus;
4604
4648
 
@@ -4614,6 +4658,7 @@ export default class Meeting extends StatelessWebexPlugin {
4614
4658
  trigger: 'join-response',
4615
4659
  locus: mtgLocus,
4616
4660
  dataSets: data.dataSets,
4661
+ metadata: data.metadata,
4617
4662
  });
4618
4663
  }
4619
4664
 
@@ -4823,6 +4868,7 @@ export default class Meeting extends StatelessWebexPlugin {
4823
4868
  this.localVideoStreamMuteStateHandler
4824
4869
  );
4825
4870
  oldStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
4871
+ oldStream?.off(LocalStreamEventNames.ConstraintsChange, this.localConstraintsChangeHandler);
4826
4872
 
4827
4873
  // we don't update this.mediaProperties.mediaDirection.sendVideo, because we always keep it as true to avoid extra SDP exchanges
4828
4874
  this.mediaProperties.setLocalVideoStream(localStream);
@@ -4838,6 +4884,7 @@ export default class Meeting extends StatelessWebexPlugin {
4838
4884
  this.localVideoStreamMuteStateHandler
4839
4885
  );
4840
4886
  localStream?.on(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
4887
+ localStream?.on(LocalStreamEventNames.ConstraintsChange, this.localConstraintsChangeHandler);
4841
4888
 
4842
4889
  if (!this.isMultistream || !localStream) {
4843
4890
  // for multistream WCME automatically un-publishes the old stream when we publish a new one
@@ -4972,6 +5019,7 @@ export default class Meeting extends StatelessWebexPlugin {
4972
5019
  this.localVideoStreamMuteStateHandler
4973
5020
  );
4974
5021
  videoStream?.off(LocalStreamEventNames.OutputTrackChange, this.localOutputTrackChangeHandler);
5022
+ videoStream?.off(LocalStreamEventNames.ConstraintsChange, this.localConstraintsChangeHandler);
4975
5023
 
4976
5024
  shareAudioStream?.off(StreamEventNames.Ended, this.handleShareAudioStreamEnded);
4977
5025
  shareAudioStream?.off(
@@ -5786,7 +5834,7 @@ export default class Meeting extends StatelessWebexPlugin {
5786
5834
  this.isReactionsSupported()
5787
5835
  ) {
5788
5836
  const member = this.members.membersCollection.get(e.data.sender.participantId);
5789
- if (!member) {
5837
+ if (!member && !this.locusInfo?.info?.isWebinar) {
5790
5838
  // @ts-ignore -- fix type
5791
5839
  LoggerProxy.logger.warn(
5792
5840
  `Meeting:index#processRelayEvent --> Skipping handling of ${REACTION_RELAY_TYPES.REACTION} for ${this.id}. participantId ${e.data.sender.participantId} does not exist in membersCollection.`
@@ -5794,7 +5842,7 @@ export default class Meeting extends StatelessWebexPlugin {
5794
5842
  break;
5795
5843
  }
5796
5844
 
5797
- const {name} = member;
5845
+ const name = (member && member.name) || e.data.sender.displayName;
5798
5846
  const processedReaction: ProcessedReaction = {
5799
5847
  reaction: e.data.reaction,
5800
5848
  sender: {
@@ -6190,13 +6238,31 @@ export default class Meeting extends StatelessWebexPlugin {
6190
6238
  */
6191
6239
  async updateLLMConnection() {
6192
6240
  // @ts-ignore - Fix type
6193
- const {url, info: {datachannelUrl, practiceSessionDatachannelUrl} = {}} = this.locusInfo;
6241
+ const {
6242
+ url = undefined,
6243
+ info: {datachannelUrl = undefined, practiceSessionDatachannelUrl = undefined} = {},
6244
+ self: {datachannelToken = undefined, practiceSessionDatachannelToken = undefined} = {},
6245
+ } = this.locusInfo || {};
6194
6246
 
6195
6247
  const isJoined = this.isJoined();
6196
6248
 
6249
+ const dataChannelTokenType = this.getDataChannelTokenType();
6250
+ const isPracticeSession = dataChannelTokenType === DataChannelTokenType.PracticeSession;
6251
+ // @ts-ignore
6252
+ const currentToken = this.webex.internal.llm.getDatachannelToken(dataChannelTokenType);
6253
+
6254
+ const locusToken = isPracticeSession ? practiceSessionDatachannelToken : datachannelToken;
6255
+
6256
+ const finalToken = currentToken ?? locusToken;
6257
+
6258
+ if (!currentToken && locusToken) {
6259
+ // @ts-ignore
6260
+ this.webex.internal.llm.setDatachannelToken(locusToken, dataChannelTokenType);
6261
+ }
6262
+
6197
6263
  // webinar panelist should use new data channel in practice session
6198
6264
  const dataChannelUrl =
6199
- this.webinar.isJoinPracticeSessionDataChannel() && practiceSessionDatachannelUrl
6265
+ isPracticeSession && practiceSessionDatachannelUrl
6200
6266
  ? practiceSessionDatachannelUrl
6201
6267
  : datachannelUrl;
6202
6268
 
@@ -6212,14 +6278,10 @@ export default class Meeting extends StatelessWebexPlugin {
6212
6278
  return undefined;
6213
6279
  }
6214
6280
  // @ts-ignore - Fix type
6215
- await this.webex.internal.llm.disconnectLLM(
6216
- isJoined
6217
- ? {
6218
- code: 3050,
6219
- reason: 'done (permanent)',
6220
- }
6221
- : undefined
6222
- );
6281
+ await this.webex.internal.llm.disconnectLLM({
6282
+ code: 3050,
6283
+ reason: 'done (permanent)',
6284
+ });
6223
6285
  // @ts-ignore - Fix type
6224
6286
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
6225
6287
  // @ts-ignore - Fix type
@@ -6234,7 +6296,7 @@ export default class Meeting extends StatelessWebexPlugin {
6234
6296
 
6235
6297
  // @ts-ignore - Fix type
6236
6298
  return this.webex.internal.llm
6237
- .registerAndConnect(url, dataChannelUrl)
6299
+ .registerAndConnect(url, dataChannelUrl, finalToken)
6238
6300
  .then((registerAndConnectResult) => {
6239
6301
  // @ts-ignore - Fix type
6240
6302
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
@@ -7438,7 +7500,7 @@ export default class Meeting extends StatelessWebexPlugin {
7438
7500
  */
7439
7501
  private async waitForMediaConnectionConnected(): Promise<void> {
7440
7502
  try {
7441
- await this.mediaProperties.waitForMediaConnectionConnected();
7503
+ await this.mediaProperties.waitForMediaConnectionConnected(this.correlationId);
7442
7504
  } catch (error) {
7443
7505
  const {iceConnected} = error;
7444
7506
 
@@ -10206,4 +10268,52 @@ export default class Meeting extends StatelessWebexPlugin {
10206
10268
  cancelSipCallOut(participantId: string) {
10207
10269
  return this.meetingRequest.cancelSipCallOut(participantId);
10208
10270
  }
10271
+
10272
+ /**
10273
+ * Method to get new data
10274
+ * @returns {Promise}
10275
+ */
10276
+ public async refreshDataChannelToken() {
10277
+ const isPracticeSession = this.webinar.isJoinPracticeSessionDataChannel();
10278
+ const dataChannelTokenType = this.getDataChannelTokenType();
10279
+
10280
+ try {
10281
+ const res = await this.meetingRequest.fetchDatachannelToken({
10282
+ locusUrl: this.locusUrl,
10283
+ requestingParticipantId: this.members.selfId,
10284
+ isPracticeSession,
10285
+ });
10286
+
10287
+ return {
10288
+ body: {
10289
+ datachannelToken: res.body.datachannelToken,
10290
+ dataChannelTokenType,
10291
+ },
10292
+ };
10293
+ } catch (e) {
10294
+ const msg = e?.message || String(e);
10295
+
10296
+ const err = Object.assign(new Error(`Failed to refresh data channel token: ${msg}`), {
10297
+ statusCode: e?.statusCode,
10298
+ original: e,
10299
+ });
10300
+
10301
+ throw err;
10302
+ }
10303
+ }
10304
+
10305
+ /**
10306
+ * Determines the current data channel token type based on the meeting state.
10307
+ *
10308
+ * variant should be used when connecting to the LLM data channel.
10309
+ *
10310
+ * @returns {DataChannelTokenType} The token type representing the current session mode.
10311
+ */
10312
+ public getDataChannelTokenType(): DataChannelTokenType {
10313
+ if (this.webinar.isJoinPracticeSessionDataChannel()) {
10314
+ return DataChannelTokenType.PracticeSession;
10315
+ }
10316
+
10317
+ return DataChannelTokenType.Default;
10318
+ }
10209
10319
  }
@@ -33,6 +33,7 @@ import {
33
33
  ToggleReactionsOptions,
34
34
  PostMeetingDataConsentOptions,
35
35
  SynchronizeVideoLayout,
36
+ fetchDataChannelTokenOptions,
36
37
  } from './request.type';
37
38
  import MeetingUtil from './util';
38
39
  import {AnnotationInfo} from '../annotation/annotation.types';
@@ -1126,4 +1127,45 @@ export default class MeetingRequest extends StatelessWebexPlugin {
1126
1127
  throw err;
1127
1128
  }
1128
1129
  }
1130
+
1131
+ /**
1132
+ * Sends a request to retrieve the datachannel authorization token for a participant.
1133
+ *
1134
+ * For regular meeting data channel:
1135
+ * GET /locus/api/v1/loci/{uuid:lid}/participant/{uuid:pid}/datachannel/token
1136
+ *
1137
+ * For practice session data channel:
1138
+ * GET /locus/api/v1/loci/{uuid:lid}/participant/{uuid:pid}/practiceSession/datachannel/token
1139
+ *
1140
+ * @param {string} locusUrl - The locus url.
1141
+ * @param {string} requestingParticipantId - The participant UUID.
1142
+ * @param {boolean} [isPracticeSession=false] - Whether to get the practice session token.
1143
+ * @returns {Promise<{datachannelToken: string}>}
1144
+ */
1145
+ public async fetchDatachannelToken({
1146
+ locusUrl,
1147
+ requestingParticipantId,
1148
+ isPracticeSession = false,
1149
+ }: fetchDataChannelTokenOptions) {
1150
+ if (!locusUrl || !requestingParticipantId) {
1151
+ return Promise.reject(new Error('locusUrl and participantId are required'));
1152
+ }
1153
+ const practicePrefix = isPracticeSession ? '/practiceSession' : '';
1154
+
1155
+ const uri = `${locusUrl}/${PARTICIPANT}/${requestingParticipantId}${practicePrefix}/datachannel/token`;
1156
+
1157
+ // @ts-ignore
1158
+ return this.locusDeltaRequest({
1159
+ method: HTTP_VERBS.GET,
1160
+ uri,
1161
+ }).catch((err) => {
1162
+ LoggerProxy.logger.error(
1163
+ `Meeting:request#fetchDatachannelToken --> Error retrieving ${
1164
+ isPracticeSession ? 'practice session ' : ''
1165
+ }datachannel token, error ${err}`
1166
+ );
1167
+
1168
+ throw err;
1169
+ });
1170
+ }
1129
1171
  }
@@ -88,4 +88,10 @@ export type UnsetStageVideoLayout = {
88
88
  overrideDefault: false;
89
89
  };
90
90
 
91
+ export type fetchDataChannelTokenOptions = {
92
+ locusUrl: string;
93
+ requestingParticipantId: string;
94
+ isPracticeSession: boolean;
95
+ };
96
+
91
97
  export type SynchronizeVideoLayout = SetStageVideoLayout | UnsetStageVideoLayout;
@@ -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;
@@ -528,6 +660,11 @@ const MeetingUtil = {
528
660
  displayHints.includes(DISPLAY_HINTS.LEAVE_TRANSFER_HOST_END_MEETING) ||
529
661
  displayHints.includes(DISPLAY_HINTS.LEAVE_END_MEETING),
530
662
 
663
+ requireHostEndMeetingBeforeLeave: (displayHints) =>
664
+ displayHints.includes(DISPLAY_HINTS.REQUIRE_HOST_END_MEETING_BEFORE_LEAVE) ||
665
+ (!displayHints.includes(DISPLAY_HINTS.LEAVE_TRANSFER_HOST_END_MEETING) &&
666
+ displayHints.includes(DISPLAY_HINTS.END_MEETING)),
667
+
531
668
  canManageBreakout: (displayHints) => displayHints.includes(DISPLAY_HINTS.BREAKOUT_MANAGEMENT),
532
669
 
533
670
  canStartBreakout: (displayHints) => !displayHints.includes(DISPLAY_HINTS.DISABLE_BREAKOUT_START),
@@ -772,6 +909,24 @@ const MeetingUtil = {
772
909
  return locusDeltaRequest;
773
910
  },
774
911
 
912
+ canAttendeeRequestAiAssistantEnabled: (displayHints = [], roles: any[] = []) => {
913
+ const isHostOrCoHost =
914
+ roles.includes(ServerRoles.Cohost) || roles.includes(ServerRoles.Moderator);
915
+
916
+ if (isHostOrCoHost) {
917
+ return false;
918
+ }
919
+
920
+ if (displayHints.includes(DISPLAY_HINTS.ATTENDEE_REQUEST_AI_ASSISTANT_ENABLED)) {
921
+ return true;
922
+ }
923
+
924
+ return false;
925
+ },
926
+
927
+ attendeeRequestAiAssistantDeclinedAll: (displayHints = []) =>
928
+ displayHints.includes(DISPLAY_HINTS.ATTENDEE_REQUEST_AI_ASSISTANT_DECLINED_ALL),
929
+
775
930
  selfSupportsFeature: (feature: SELF_POLICY, userPolicies: Record<SELF_POLICY, boolean>) => {
776
931
  if (!userPolicies) {
777
932
  return true;