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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +15 -2
  3. package/dist/aiEnableRequest/index.js.map +1 -1
  4. package/dist/breakouts/breakout.js +8 -3
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +26 -2
  7. package/dist/breakouts/index.js.map +1 -1
  8. package/dist/config.js +2 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/constants.js +30 -7
  11. package/dist/constants.js.map +1 -1
  12. package/dist/controls-options-manager/constants.js +11 -1
  13. package/dist/controls-options-manager/constants.js.map +1 -1
  14. package/dist/controls-options-manager/index.js +38 -24
  15. package/dist/controls-options-manager/index.js.map +1 -1
  16. package/dist/controls-options-manager/util.js +91 -0
  17. package/dist/controls-options-manager/util.js.map +1 -1
  18. package/dist/hashTree/constants.js +13 -1
  19. package/dist/hashTree/constants.js.map +1 -1
  20. package/dist/hashTree/hashTreeParser.js +880 -382
  21. package/dist/hashTree/hashTreeParser.js.map +1 -1
  22. package/dist/hashTree/utils.js +42 -0
  23. package/dist/hashTree/utils.js.map +1 -1
  24. package/dist/index.js +7 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/interceptors/dataChannelAuthToken.js +75 -15
  27. package/dist/interceptors/dataChannelAuthToken.js.map +1 -1
  28. package/dist/interceptors/locusRetry.js +23 -8
  29. package/dist/interceptors/locusRetry.js.map +1 -1
  30. package/dist/interpretation/index.js +10 -1
  31. package/dist/interpretation/index.js.map +1 -1
  32. package/dist/interpretation/interpretation.types.js +7 -0
  33. package/dist/interpretation/interpretation.types.js.map +1 -0
  34. package/dist/interpretation/siLanguage.js +1 -1
  35. package/dist/locus-info/controlsUtils.js +4 -1
  36. package/dist/locus-info/controlsUtils.js.map +1 -1
  37. package/dist/locus-info/index.js +298 -87
  38. package/dist/locus-info/index.js.map +1 -1
  39. package/dist/locus-info/types.js +19 -0
  40. package/dist/locus-info/types.js.map +1 -1
  41. package/dist/media/index.js +3 -1
  42. package/dist/media/index.js.map +1 -1
  43. package/dist/media/properties.js +1 -0
  44. package/dist/media/properties.js.map +1 -1
  45. package/dist/meeting/in-meeting-actions.js +3 -1
  46. package/dist/meeting/in-meeting-actions.js.map +1 -1
  47. package/dist/meeting/index.js +1046 -689
  48. package/dist/meeting/index.js.map +1 -1
  49. package/dist/meeting/muteState.js +10 -1
  50. package/dist/meeting/muteState.js.map +1 -1
  51. package/dist/meeting/request.js +5 -2
  52. package/dist/meeting/request.js.map +1 -1
  53. package/dist/meeting/util.js +20 -2
  54. package/dist/meeting/util.js.map +1 -1
  55. package/dist/meeting-info/meeting-info-v2.js +2 -2
  56. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  57. package/dist/meetings/index.js +231 -78
  58. package/dist/meetings/index.js.map +1 -1
  59. package/dist/meetings/meetings.types.js +6 -1
  60. package/dist/meetings/meetings.types.js.map +1 -1
  61. package/dist/meetings/request.js +39 -0
  62. package/dist/meetings/request.js.map +1 -1
  63. package/dist/meetings/util.js +79 -5
  64. package/dist/meetings/util.js.map +1 -1
  65. package/dist/member/index.js +10 -0
  66. package/dist/member/index.js.map +1 -1
  67. package/dist/member/types.js.map +1 -1
  68. package/dist/member/util.js +3 -0
  69. package/dist/member/util.js.map +1 -1
  70. package/dist/metrics/constants.js +4 -1
  71. package/dist/metrics/constants.js.map +1 -1
  72. package/dist/multistream/codec/constants.js +63 -0
  73. package/dist/multistream/codec/constants.js.map +1 -0
  74. package/dist/multistream/mediaRequestManager.js +62 -15
  75. package/dist/multistream/mediaRequestManager.js.map +1 -1
  76. package/dist/multistream/receiveSlot.js +9 -0
  77. package/dist/multistream/receiveSlot.js.map +1 -1
  78. package/dist/reactions/reactions.type.js.map +1 -1
  79. package/dist/recording-controller/index.js +1 -3
  80. package/dist/recording-controller/index.js.map +1 -1
  81. package/dist/types/config.d.ts +2 -0
  82. package/dist/types/constants.d.ts +9 -1
  83. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  84. package/dist/types/controls-options-manager/index.d.ts +10 -0
  85. package/dist/types/hashTree/constants.d.ts +2 -0
  86. package/dist/types/hashTree/hashTreeParser.d.ts +146 -17
  87. package/dist/types/hashTree/utils.d.ts +18 -0
  88. package/dist/types/index.d.ts +3 -0
  89. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  90. package/dist/types/interpretation/interpretation.types.d.ts +10 -0
  91. package/dist/types/locus-info/index.d.ts +50 -6
  92. package/dist/types/locus-info/types.d.ts +21 -1
  93. package/dist/types/media/properties.d.ts +1 -0
  94. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  95. package/dist/types/meeting/index.d.ts +78 -5
  96. package/dist/types/meeting/request.d.ts +1 -0
  97. package/dist/types/meeting/util.d.ts +8 -0
  98. package/dist/types/meetings/index.d.ts +30 -2
  99. package/dist/types/meetings/meetings.types.d.ts +15 -0
  100. package/dist/types/meetings/request.d.ts +14 -0
  101. package/dist/types/member/index.d.ts +1 -0
  102. package/dist/types/member/types.d.ts +1 -0
  103. package/dist/types/member/util.d.ts +1 -0
  104. package/dist/types/metrics/constants.d.ts +3 -0
  105. package/dist/types/multistream/codec/constants.d.ts +7 -0
  106. package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
  107. package/dist/types/reactions/reactions.type.d.ts +3 -0
  108. package/dist/webinar/index.js +305 -159
  109. package/dist/webinar/index.js.map +1 -1
  110. package/package.json +22 -22
  111. package/src/aiEnableRequest/index.ts +16 -0
  112. package/src/breakouts/breakout.ts +3 -1
  113. package/src/breakouts/index.ts +31 -0
  114. package/src/config.ts +2 -0
  115. package/src/constants.ts +13 -2
  116. package/src/controls-options-manager/constants.ts +14 -1
  117. package/src/controls-options-manager/index.ts +47 -24
  118. package/src/controls-options-manager/util.ts +81 -1
  119. package/src/hashTree/constants.ts +16 -0
  120. package/src/hashTree/hashTreeParser.ts +580 -196
  121. package/src/hashTree/utils.ts +36 -0
  122. package/src/index.ts +6 -0
  123. package/src/interceptors/dataChannelAuthToken.ts +88 -12
  124. package/src/interceptors/locusRetry.ts +25 -4
  125. package/src/interpretation/index.ts +27 -9
  126. package/src/interpretation/interpretation.types.ts +11 -0
  127. package/src/locus-info/controlsUtils.ts +3 -1
  128. package/src/locus-info/index.ts +293 -97
  129. package/src/locus-info/types.ts +25 -1
  130. package/src/media/index.ts +3 -0
  131. package/src/media/properties.ts +1 -0
  132. package/src/meeting/in-meeting-actions.ts +4 -0
  133. package/src/meeting/index.ts +386 -48
  134. package/src/meeting/muteState.ts +10 -1
  135. package/src/meeting/request.ts +11 -0
  136. package/src/meeting/util.ts +21 -2
  137. package/src/meeting-info/meeting-info-v2.ts +4 -2
  138. package/src/meetings/index.ts +134 -44
  139. package/src/meetings/meetings.types.ts +19 -0
  140. package/src/meetings/request.ts +43 -0
  141. package/src/meetings/util.ts +97 -1
  142. package/src/member/index.ts +10 -0
  143. package/src/member/types.ts +1 -0
  144. package/src/member/util.ts +3 -0
  145. package/src/metrics/constants.ts +3 -0
  146. package/src/multistream/codec/constants.ts +58 -0
  147. package/src/multistream/mediaRequestManager.ts +119 -28
  148. package/src/multistream/receiveSlot.ts +18 -0
  149. package/src/reactions/reactions.type.ts +3 -0
  150. package/src/recording-controller/index.ts +1 -2
  151. package/src/webinar/index.ts +214 -36
  152. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  153. package/test/unit/spec/breakouts/breakout.ts +9 -3
  154. package/test/unit/spec/breakouts/index.ts +49 -0
  155. package/test/unit/spec/controls-options-manager/index.js +140 -29
  156. package/test/unit/spec/controls-options-manager/util.js +165 -0
  157. package/test/unit/spec/hashTree/hashTreeParser.ts +1838 -180
  158. package/test/unit/spec/hashTree/utils.ts +125 -1
  159. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +196 -0
  160. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  161. package/test/unit/spec/interpretation/index.ts +26 -4
  162. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  163. package/test/unit/spec/locus-info/index.js +487 -81
  164. package/test/unit/spec/media/index.ts +31 -0
  165. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  166. package/test/unit/spec/meeting/index.js +1240 -37
  167. package/test/unit/spec/meeting/muteState.js +81 -0
  168. package/test/unit/spec/meeting/request.js +12 -0
  169. package/test/unit/spec/meeting/utils.js +33 -0
  170. package/test/unit/spec/meeting-info/meetinginfov2.js +19 -10
  171. package/test/unit/spec/meetings/index.js +360 -10
  172. package/test/unit/spec/meetings/request.js +141 -0
  173. package/test/unit/spec/meetings/utils.js +189 -0
  174. package/test/unit/spec/member/index.js +7 -0
  175. package/test/unit/spec/member/util.js +24 -0
  176. package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
  177. package/test/unit/spec/recording-controller/index.js +9 -8
  178. package/test/unit/spec/webinar/index.ts +329 -28
@@ -22,7 +22,6 @@ import {
22
22
  MediaConnectionEventNames,
23
23
  MediaContent,
24
24
  MediaType,
25
- MediaCodecMimeType,
26
25
  RemoteTrackType,
27
26
  RoapMessage,
28
27
  StatsAnalyzer,
@@ -31,7 +30,7 @@ import {
31
30
  NetworkQualityMonitor,
32
31
  StatsMonitor,
33
32
  StatsMonitorEventNames,
34
- InboundAudioIssueSubTypes,
33
+ MediaCodecMimeType,
35
34
  } from '@webex/internal-media-core';
36
35
 
37
36
  import {DataChannelTokenType} from '@webex/internal-plugin-llm';
@@ -137,6 +136,8 @@ import {
137
136
  STAGE_MANAGER_TYPE,
138
137
  LOCUSEVENT,
139
138
  LOCUS_LLM_EVENT,
139
+ LLM_DEFAULT_SESSION,
140
+ LLM_PRACTICE_SESSION,
140
141
  } from '../constants';
141
142
  import BEHAVIORAL_METRICS from '../metrics/constants';
142
143
  import ParameterError from '../common/errors/parameter';
@@ -612,7 +613,7 @@ export default class Meeting extends StatelessWebexPlugin {
612
613
  webinar: any;
613
614
  conversationUrl: string;
614
615
  callStateForMetrics: CallStateForMetrics;
615
- destination: string;
616
+ destination: string | LocusDTO;
616
617
  destinationType: DESTINATION_TYPE;
617
618
  deviceUrl: string;
618
619
  hostId: string;
@@ -651,6 +652,8 @@ export default class Meeting extends StatelessWebexPlugin {
651
652
  floorGrantPending: boolean;
652
653
  hasJoinedOnce: boolean;
653
654
  hasWebsocketConnected: boolean;
655
+ private mercuryOnlineHandler?: () => void;
656
+ private mercuryOfflineHandler?: () => void;
654
657
  inMeetingActions: InMeetingActions;
655
658
  isLocalShareLive: boolean;
656
659
  isRoapInProgress: boolean;
@@ -935,7 +938,7 @@ export default class Meeting extends StatelessWebexPlugin {
935
938
  this.simultaneousInterpretation = new SimultaneousInterpretation({}, {parent: this.webex});
936
939
 
937
940
  // @ts-ignore
938
- this.aiEnableRequest = new AIEnableRequest({}, {parent: this.webex});
941
+ this.aiEnableRequest = new AIEnableRequest({locusUrl: this.locusUrl}, {parent: this.webex});
939
942
 
940
943
  /**
941
944
  * @instance
@@ -966,6 +969,7 @@ export default class Meeting extends StatelessWebexPlugin {
966
969
  },
967
970
  (csi: CSI) => (this.members.findMemberByCsi(csi) as any)?.id
968
971
  );
972
+
969
973
  /**
970
974
  * Object containing helper classes for managing media requests for audio/video/screenshare (for multistream media connections)
971
975
  * All multistream media requests sent out for this meeting have to go through them.
@@ -985,6 +989,7 @@ export default class Meeting extends StatelessWebexPlugin {
985
989
  mediaRequests
986
990
  );
987
991
  },
992
+ this.getIngressPayloadTypeCallback.bind(this),
988
993
  {
989
994
  // @ts-ignore - config coming from registerPlugin
990
995
  degradationPreferences: this.config.degradationPreferences,
@@ -1006,6 +1011,7 @@ export default class Meeting extends StatelessWebexPlugin {
1006
1011
  mediaRequests
1007
1012
  );
1008
1013
  },
1014
+ this.getIngressPayloadTypeCallback.bind(this),
1009
1015
  {
1010
1016
  // @ts-ignore - config coming from registerPlugin
1011
1017
  degradationPreferences: this.config.degradationPreferences,
@@ -1027,6 +1033,7 @@ export default class Meeting extends StatelessWebexPlugin {
1027
1033
  mediaRequests
1028
1034
  );
1029
1035
  },
1036
+ this.getIngressPayloadTypeCallback.bind(this),
1030
1037
  {
1031
1038
  // @ts-ignore - config coming from registerPlugin
1032
1039
  degradationPreferences: this.config.degradationPreferences,
@@ -1048,11 +1055,14 @@ export default class Meeting extends StatelessWebexPlugin {
1048
1055
  mediaRequests
1049
1056
  );
1050
1057
  },
1058
+ this.getIngressPayloadTypeCallback.bind(this),
1051
1059
  {
1052
1060
  // @ts-ignore - config coming from registerPlugin
1053
1061
  degradationPreferences: this.config.degradationPreferences,
1054
1062
  kind: 'video',
1055
1063
  trimRequestsToNumOfSources: false,
1064
+ // @ts-ignore - config coming from registerPlugin
1065
+ enableAv1: this.config.enableAv1SlidesSupport,
1056
1066
  }
1057
1067
  ),
1058
1068
  };
@@ -1713,6 +1723,37 @@ export default class Meeting extends StatelessWebexPlugin {
1713
1723
  this.mediaServerIp = undefined;
1714
1724
  }
1715
1725
 
1726
+ /**
1727
+ * Get the ingress payload type for a given media type and codec mime type
1728
+ * @param {MediaType} mediaType - The media type
1729
+ * @param {MediaCodecMimeType} codecMimeType - The codec mime type
1730
+ * @returns {number | undefined} - The ingress payload type
1731
+ * @private
1732
+ * @memberof Meeting
1733
+ */
1734
+ private getIngressPayloadTypeCallback(
1735
+ mediaType: MediaType,
1736
+ codecMimeType: MediaCodecMimeType
1737
+ ): number | undefined {
1738
+ if (this.isMultistream) {
1739
+ try {
1740
+ return this.mediaProperties.webrtcMediaConnection.getIngressPayloadType(
1741
+ mediaType,
1742
+ codecMimeType
1743
+ );
1744
+ } catch (error) {
1745
+ LoggerProxy.logger.info(
1746
+ `Meeting:index#mediaRequestManager --> failed to get ingress payload type for mediaType=${mediaType}, codecMimeType=${codecMimeType}`,
1747
+ error
1748
+ );
1749
+
1750
+ return undefined;
1751
+ }
1752
+ }
1753
+
1754
+ return undefined;
1755
+ }
1756
+
1716
1757
  /**
1717
1758
  * Temporary func to return webex object,
1718
1759
  * in order to access internal plugin metrics
@@ -2789,7 +2830,7 @@ export default class Meeting extends StatelessWebexPlugin {
2789
2830
  private setupLocusControlsListener() {
2790
2831
  this.locusInfo.on(
2791
2832
  LOCUSINFO.EVENTS.CONTROLS_RECORDING_UPDATED,
2792
- ({state, modifiedBy, lastModified}) => {
2833
+ ({state, modifiedBy, lastModified, modifiedByServiceAppName, modifiedByServiceAppId}) => {
2793
2834
  let event;
2794
2835
 
2795
2836
  switch (state) {
@@ -2815,6 +2856,8 @@ export default class Meeting extends StatelessWebexPlugin {
2815
2856
  state: state === RECORDING_STATE.RESUMED ? RECORDING_STATE.RECORDING : state,
2816
2857
  modifiedBy,
2817
2858
  lastModified,
2859
+ modifiedByServiceAppName,
2860
+ modifiedByServiceAppId,
2818
2861
  };
2819
2862
  Trigger.trigger(
2820
2863
  this,
@@ -3459,13 +3502,12 @@ export default class Meeting extends StatelessWebexPlugin {
3459
3502
  this.breakouts.locusUrlUpdate(url);
3460
3503
  this.simultaneousInterpretation.locusUrlUpdate(url);
3461
3504
  this.annotation.locusUrlUpdate(url);
3505
+ this.aiEnableRequest.locusUrlUpdate(url);
3462
3506
  this.locusUrl = url;
3463
3507
  this.locusId = this.locusUrl?.split('/').pop();
3464
3508
  this.recordingController.setLocusUrl(this.locusUrl);
3465
3509
  this.controlsOptionsManager.setLocusUrl(this.locusUrl, !!isMainLocus);
3466
3510
  this.webinar.locusUrlUpdate(url);
3467
- // @ts-ignore
3468
- this.webex.internal.llm.setRefreshHandler(() => this.refreshDataChannelToken());
3469
3511
 
3470
3512
  Trigger.trigger(
3471
3513
  this,
@@ -3734,7 +3776,7 @@ export default class Meeting extends StatelessWebexPlugin {
3734
3776
  });
3735
3777
  this.updateLLMConnection();
3736
3778
  });
3737
- this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, (payload) => {
3779
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, async (payload) => {
3738
3780
  this.stopKeepAlive();
3739
3781
 
3740
3782
  if (payload) {
@@ -3761,13 +3803,15 @@ export default class Meeting extends StatelessWebexPlugin {
3761
3803
  }
3762
3804
  this.rtcMetrics?.sendNextMetrics();
3763
3805
 
3764
- this.ensureDefaultDatachannelTokenAfterAdmit().catch((error) => {
3806
+ try {
3807
+ await this.ensureDefaultDatachannelTokenAfterAdmit();
3808
+ } catch (error) {
3765
3809
  LoggerProxy.logger.warn(
3766
3810
  `Meeting:index#setUpLocusInfoSelfListener --> failed post-admit token prefetch flow: ${
3767
3811
  error?.message || String(error)
3768
3812
  }`
3769
3813
  );
3770
- });
3814
+ }
3771
3815
 
3772
3816
  this.updateLLMConnection();
3773
3817
  });
@@ -4611,6 +4655,9 @@ export default class Meeting extends StatelessWebexPlugin {
4611
4655
  ),
4612
4656
  isAttendeeRequestAiAssistantDeclinedAll:
4613
4657
  MeetingUtil.attendeeRequestAiAssistantDeclinedAll(this.userDisplayHints),
4658
+ isAnonymizeDisplayNamesEnabled: MeetingUtil.isAnonymizeDisplayNamesEnabled(
4659
+ this.userDisplayHints
4660
+ ),
4614
4661
  }) || changed;
4615
4662
  }
4616
4663
  if (changed) {
@@ -4659,6 +4706,34 @@ export default class Meeting extends StatelessWebexPlugin {
4659
4706
  this.sipUri = sipUri;
4660
4707
  }
4661
4708
 
4709
+ /**
4710
+ * After initial locus setup, refreshes destination with synced locus data and optionally
4711
+ * performs deferred meeting info fetch when initial locus was incomplete.
4712
+ * @param {LocusDTO} locus
4713
+ * @returns {void}
4714
+ */
4715
+ public async finalizeMeetingAfterInitialLocusSetup(locus: LocusDTO): Promise<void> {
4716
+ if (locus && this?.destinationType === DESTINATION_TYPE.LOCUS_ID) {
4717
+ // destination is initialized from the initial locus snapshot in constructor,
4718
+ // so refresh it after locus sync to avoid stale partial hash-tree data.
4719
+ this.destination = locus;
4720
+ }
4721
+ if (
4722
+ (!this.meetingInfo || isEmpty(this.meetingInfo)) &&
4723
+ (this.destination as LocusDTO)?.info &&
4724
+ !this.fetchMeetingInfoTimeoutId &&
4725
+ !MeetingsUtil.isOneOnOneCall(locus)
4726
+ ) {
4727
+ try {
4728
+ await this.fetchMeetingInfo({});
4729
+ } catch (error: any) {
4730
+ LoggerProxy.logger.info(
4731
+ `Meeting:index#finalizeMeetingAfterInitialLocusSetup --> deferred fetchMeetingInfo failed: ${error.message}`
4732
+ );
4733
+ }
4734
+ }
4735
+ }
4736
+
4662
4737
  /**
4663
4738
  * Set the locus info the class instance. Should be called with the parsed locus
4664
4739
  * we got in the join response.
@@ -5130,8 +5205,7 @@ export default class Meeting extends StatelessWebexPlugin {
5130
5205
  public setMercuryListener() {
5131
5206
  // Client will have a socket manager and handle reconnecting to mercury, when we reconnect to mercury
5132
5207
  // if the meeting has active peer connections, it should try to reconnect.
5133
- // @ts-ignore
5134
- this.webex.internal.mercury.on(ONLINE, () => {
5208
+ this.mercuryOnlineHandler = () => {
5135
5209
  LoggerProxy.logger.info('Meeting:index#setMercuryListener --> Web socket online');
5136
5210
 
5137
5211
  // Only send restore event when it was disconnected before and for connected later
@@ -5141,15 +5215,47 @@ export default class Meeting extends StatelessWebexPlugin {
5141
5215
  });
5142
5216
  }
5143
5217
  this.hasWebsocketConnected = true;
5144
- });
5218
+ };
5145
5219
 
5146
- // @ts-ignore
5147
- this.webex.internal.mercury.on(OFFLINE, () => {
5220
+ this.mercuryOfflineHandler = () => {
5148
5221
  LoggerProxy.logger.error('Meeting:index#setMercuryListener --> Web socket offline');
5149
5222
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MERCURY_CONNECTION_FAILURE, {
5150
5223
  correlation_id: this.correlationId,
5151
5224
  });
5152
- });
5225
+ };
5226
+
5227
+ // @ts-ignore
5228
+ this.webex.internal.mercury.on(ONLINE, this.mercuryOnlineHandler);
5229
+ // @ts-ignore
5230
+ this.webex.internal.mercury.on(OFFLINE, this.mercuryOfflineHandler);
5231
+ }
5232
+
5233
+ /**
5234
+ * Removes this meeting's Mercury ONLINE/OFFLINE event listeners registered
5235
+ * by setMercuryListener(). Must be called before Locus /leave to avoid
5236
+ * unnecessary syncs/metrics triggered by events received while leaving
5237
+ * (per Locus team recommendation).
5238
+ *
5239
+ * Mercury is a process-wide singleton shared with other plugins, so we
5240
+ * pass the bound handler refs to .off() to avoid clearing every listener
5241
+ * for ONLINE/OFFLINE on the shared emitter.
5242
+ *
5243
+ * Idempotent: subsequent calls are no-ops because the handler refs are
5244
+ * cleared after detaching.
5245
+ * @private
5246
+ * @returns {void}
5247
+ */
5248
+ private stopListeningForMercuryEvents() {
5249
+ if (this.mercuryOnlineHandler) {
5250
+ // @ts-ignore
5251
+ this.webex.internal.mercury.off(ONLINE, this.mercuryOnlineHandler);
5252
+ this.mercuryOnlineHandler = undefined;
5253
+ }
5254
+ if (this.mercuryOfflineHandler) {
5255
+ // @ts-ignore
5256
+ this.webex.internal.mercury.off(OFFLINE, this.mercuryOfflineHandler);
5257
+ this.mercuryOfflineHandler = undefined;
5258
+ }
5153
5259
  }
5154
5260
 
5155
5261
  /**
@@ -5865,6 +5971,31 @@ export default class Meeting extends StatelessWebexPlugin {
5865
5971
  }
5866
5972
  };
5867
5973
 
5974
+ /**
5975
+ * Verifies the relay event was delivered for the active LLM session binding.
5976
+ * @param {RelayEvent} event Event object coming from LLM Connection
5977
+ * @returns {boolean}
5978
+ */
5979
+ private isRelayEventRouteValid(event: RelayEvent): boolean {
5980
+ const route = event?.headers?.route;
5981
+
5982
+ if (!route) {
5983
+ return true;
5984
+ }
5985
+
5986
+ const {llm} = (this as any).webex.internal;
5987
+ const isPracticeSession = llm.isConnected(LLM_PRACTICE_SESSION);
5988
+ const expectedBinding = isPracticeSession
5989
+ ? llm.getBinding(LLM_PRACTICE_SESSION)
5990
+ : llm.getBinding();
5991
+
5992
+ if (!expectedBinding || route === expectedBinding) {
5993
+ return true;
5994
+ }
5995
+
5996
+ return false;
5997
+ }
5998
+
5868
5999
  /**
5869
6000
  * Callback called when a relay event is received from meeting LLM Connection
5870
6001
  * @param {RelayEvent} e Event object coming from LLM Connection
@@ -5872,6 +6003,9 @@ export default class Meeting extends StatelessWebexPlugin {
5872
6003
  * @returns {void}
5873
6004
  */
5874
6005
  private processRelayEvent = (e: RelayEvent): void => {
6006
+ if (!this.isRelayEventRouteValid(e)) {
6007
+ return;
6008
+ }
5875
6009
  switch (e.data.relayType) {
5876
6010
  case REACTION_RELAY_TYPES.REACTION:
5877
6011
  if (
@@ -6002,6 +6136,7 @@ export default class Meeting extends StatelessWebexPlugin {
6002
6136
  */
6003
6137
  private handleLLMOnline = (): void => {
6004
6138
  this.restoreLLMSubscriptionsIfNeeded();
6139
+ this.locusInfo.syncAllHashTreeDatasets({onlyLLM: true});
6005
6140
 
6006
6141
  Trigger.trigger(
6007
6142
  this,
@@ -6301,8 +6436,57 @@ export default class Meeting extends StatelessWebexPlugin {
6301
6436
  }
6302
6437
  }
6303
6438
 
6439
+ /**
6440
+ * Removes LLM event listeners and clears the health check timer.
6441
+ * Must be called before Locus /leave to avoid unnecessary syncs triggered
6442
+ * by events received while leaving (per Locus team recommendation).
6443
+ * Idempotent: safe to call multiple times; .off() is a no-op when no
6444
+ * matching listener is registered.
6445
+ * @private
6446
+ * @returns {void}
6447
+ */
6448
+ private stopListeningForLLMEvents() {
6449
+ // @ts-ignore - fix types
6450
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
6451
+ // @ts-ignore - fix types
6452
+ this.webex.internal.llm.off(LOCUS_LLM_EVENT, this.processLocusLLMEvent);
6453
+ this.clearLLMHealthCheckTimer();
6454
+ }
6455
+
6456
+ /**
6457
+ * Stops listening on every event bus (LLM, Mercury, voicea/transcription,
6458
+ * annotation) that could otherwise deliver events to this meeting while
6459
+ * Locus is processing /leave or /end. Per the Locus team recommendation,
6460
+ * this must run before the Locus request is dispatched to avoid
6461
+ * unnecessary syncs triggered by in-flight events.
6462
+ *
6463
+ * Voicea (transcription) subscribes to llm 'event:relay.event' internally,
6464
+ * and the annotation plugin subscribes to both mercury and llm, so both
6465
+ * must be torn down alongside the direct LLM/Mercury listeners.
6466
+ *
6467
+ * Idempotent: safe to call multiple times; .off() is a no-op when no
6468
+ * matching listener is registered, and stopTranscription is guarded.
6469
+ * @private
6470
+ * @returns {void}
6471
+ */
6472
+ private stopListeningForMeetingEvents() {
6473
+ this.stopListeningForLLMEvents();
6474
+ this.stopListeningForMercuryEvents();
6475
+ if (this.transcription) {
6476
+ this.stopTranscription();
6477
+ this.transcription = undefined;
6478
+ }
6479
+ this.annotation.deregisterEvents();
6480
+ }
6481
+
6304
6482
  /**
6305
6483
  * Disconnects and cleans up the default LLM session listeners/timers.
6484
+ *
6485
+ * Ownership-aware: only calls `disconnectLLM` when this meeting is the
6486
+ * current owner of the default LLM session (or when no owner is recorded).
6487
+ * Event listeners belonging to this meeting instance are always detached
6488
+ * so they do not receive another meeting's relay events.
6489
+ *
6306
6490
  * @param {Object} options
6307
6491
  * @param {boolean} [options.removeOnlineListener=true] removes the one-time online listener
6308
6492
  * @param {boolean} [options.throwOnError=true] rethrows disconnect errors when true
@@ -6315,12 +6499,29 @@ export default class Meeting extends StatelessWebexPlugin {
6315
6499
  removeOnlineListener?: boolean;
6316
6500
  throwOnError?: boolean;
6317
6501
  } = {}): Promise<void> => {
6502
+ // @ts-ignore - Fix type
6503
+ // @ts-ignore - Fix type
6504
+ const {currentOwner, isOwner} = this.webex.internal.llm.resolveSessionOwnership(
6505
+ this.id,
6506
+ LLM_DEFAULT_SESSION
6507
+ );
6508
+
6318
6509
  try {
6319
- // @ts-ignore - Fix type
6320
- await this.webex.internal.llm.disconnectLLM({
6321
- code: 3050,
6322
- reason: 'done (permanent)',
6323
- });
6510
+ if (isOwner) {
6511
+ // @ts-ignore - Fix type
6512
+ await this.webex.internal.llm.disconnectLLM(
6513
+ {
6514
+ code: 3050,
6515
+ reason: 'done (permanent)',
6516
+ },
6517
+ LLM_DEFAULT_SESSION,
6518
+ this.id
6519
+ );
6520
+ } else {
6521
+ LoggerProxy.logger.info(
6522
+ `Meeting:index#cleanupLLMConneciton --> skipping disconnect; LLM owned by meeting ${currentOwner}, not ${this.id}`
6523
+ );
6524
+ }
6324
6525
  } catch (error) {
6325
6526
  LoggerProxy.logger.error(
6326
6527
  'Meeting:index#cleanupLLMConneciton --> Failed to disconnect default LLM session',
@@ -6335,23 +6536,33 @@ export default class Meeting extends StatelessWebexPlugin {
6335
6536
  // @ts-ignore - Fix type
6336
6537
  this.webex.internal.llm.off('online', this.handleLLMOnline);
6337
6538
  }
6338
- // @ts-ignore - fix types
6339
- this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
6340
- // @ts-ignore - Fix type
6341
- this.webex.internal.llm.off(LOCUS_LLM_EVENT, this.processLocusLLMEvent);
6539
+ this.stopListeningForLLMEvents();
6342
6540
 
6343
- this.clearLLMHealthCheckTimer();
6541
+ // Re-check ownership after awaiting disconnectLLM. If ownership changed
6542
+ // while cleanup was in flight, do not clear another meeting's owner tag.
6543
+ if (isOwner) {
6544
+ const {currentOwner: currentOwnerAfterCleanup} =
6545
+ // @ts-ignore - Fix type
6546
+ this.webex.internal.llm.resolveSessionOwnership(this.id, LLM_DEFAULT_SESSION);
6547
+
6548
+ if (currentOwnerAfterCleanup === this.id) {
6549
+ // @ts-ignore - Fix type
6550
+ this.webex.internal.llm.setOwnerMeetingId?.(undefined);
6551
+ }
6552
+ }
6344
6553
  }
6345
6554
  };
6346
6555
 
6347
6556
  /**
6348
- * Clears all data channel tokens stored in LLM.
6349
- * Called during meeting cleanup to ensure stale tokens are not reused.
6557
+ * Clears data channel tokens associated with this meeting ownership.
6558
+ * Ownership checks are enforced in internal-plugin-llm.
6350
6559
  * @returns {void}
6351
6560
  */
6352
6561
  clearDataChannelToken(): void {
6353
6562
  // @ts-ignore
6354
- this.webex.internal.llm.resetDatachannelTokens();
6563
+ this.webex.internal.llm.clearDatachannelToken(LLM_DEFAULT_SESSION, this.id);
6564
+ // @ts-ignore
6565
+ this.webex.internal.llm.clearDatachannelToken(LLM_PRACTICE_SESSION, this.id);
6355
6566
  }
6356
6567
 
6357
6568
  /**
@@ -6366,14 +6577,15 @@ export default class Meeting extends StatelessWebexPlugin {
6366
6577
 
6367
6578
  if (datachannelToken) {
6368
6579
  // @ts-ignore
6369
- this.webex.internal.llm.setDatachannelToken(datachannelToken, DataChannelTokenType.Default);
6580
+ this.webex.internal.llm.setDatachannelToken(datachannelToken, LLM_DEFAULT_SESSION, this.id);
6370
6581
  }
6371
6582
 
6372
6583
  if (practiceSessionDatachannelToken) {
6373
6584
  // @ts-ignore
6374
6585
  this.webex.internal.llm.setDatachannelToken(
6375
6586
  practiceSessionDatachannelToken,
6376
- DataChannelTokenType.PracticeSession
6587
+ LLM_PRACTICE_SESSION,
6588
+ this.id
6377
6589
  );
6378
6590
  }
6379
6591
  }
@@ -6386,7 +6598,10 @@ export default class Meeting extends StatelessWebexPlugin {
6386
6598
  private async ensureDefaultDatachannelTokenAfterAdmit(): Promise<boolean> {
6387
6599
  try {
6388
6600
  // @ts-ignore
6389
- const datachannelToken = this.webex.internal.llm.getDatachannelToken();
6601
+ const datachannelToken = this.webex.internal.llm.getDatachannelToken(
6602
+ LLM_DEFAULT_SESSION,
6603
+ this.id
6604
+ );
6390
6605
  // @ts-ignore
6391
6606
  const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
6392
6607
 
@@ -6408,7 +6623,8 @@ export default class Meeting extends StatelessWebexPlugin {
6408
6623
  // @ts-ignore
6409
6624
  this.webex.internal.llm.setDatachannelToken(
6410
6625
  fetchedDatachannelToken,
6411
- DataChannelTokenType.Default
6626
+ LLM_DEFAULT_SESSION,
6627
+ this.id
6412
6628
  );
6413
6629
 
6414
6630
  return true;
@@ -6435,15 +6651,58 @@ export default class Meeting extends StatelessWebexPlugin {
6435
6651
 
6436
6652
  const isJoined = this.isJoined();
6437
6653
 
6438
- // @ts-ignore
6439
- const datachannelToken = this.webex.internal.llm.getDatachannelToken(
6440
- DataChannelTokenType.Default
6654
+ const dataChannelUrl = datachannelUrl;
6655
+
6656
+ // Ownership guard: when the default LLM session is already connected and
6657
+ // owned by a *different* Meeting instance, do not disconnect or reconfigure
6658
+ // it. Another meeting's `updateLLMConnection` must be ignored here to
6659
+ // avoid killing the socket it relies on. We only proceed to manage the
6660
+ // connection when this meeting is the current owner, or when no owner is
6661
+ // set yet (first claim).
6662
+ // @ts-ignore - Fix type
6663
+ const {currentOwner} = this.webex.internal.llm.resolveSessionOwnership(
6664
+ this.id,
6665
+ LLM_DEFAULT_SESSION
6441
6666
  );
6442
6667
 
6443
- const dataChannelUrl = datachannelUrl;
6668
+ // Capture connectivity before any reconnect attempt. If LLM was already
6669
+ // connected, we must respect current ownership. If it was disconnected,
6670
+ // this flow may reclaim stale owner tags after a fresh connect.
6671
+ // @ts-ignore - Fix type
6672
+ const wasConnected = this.webex.internal.llm.isConnected();
6444
6673
 
6674
+ // Prefer ownership-scoped token read. For disconnected stale-owner reclaim
6675
+ // flows, fallback to ownerless read so initial register can still carry a
6676
+ // token and recover from stale ownership without 401/403 dead-end.
6445
6677
  // @ts-ignore - Fix type
6446
- if (this.webex.internal.llm.isConnected()) {
6678
+ let datachannelToken = this.webex.internal.llm.getDatachannelToken(
6679
+ LLM_DEFAULT_SESSION,
6680
+ this.id
6681
+ );
6682
+
6683
+ if (!datachannelToken && !wasConnected && currentOwner && currentOwner !== this.id) {
6684
+ // @ts-ignore - Fix type
6685
+ datachannelToken = this.webex.internal.llm.getDatachannelToken(LLM_DEFAULT_SESSION);
6686
+ }
6687
+
6688
+ // @ts-ignore - Fix type
6689
+ if (wasConnected) {
6690
+ if (currentOwner && currentOwner !== this.id) {
6691
+ // Another meeting owns the live LLM socket. We must not disconnect
6692
+ // or reconfigure it -- doing so would tear down a session the
6693
+ // owning meeting still relies on. Locus/datachannel URL mismatch is
6694
+ // expected here (each meeting has its own locus URL) and is NOT a
6695
+ // valid signal of staleness, so we never reclaim from this path.
6696
+ // The only safe reclaim mechanism is the `finally`-block owner-tag
6697
+ // release in `cleanupLLMConneciton`, which fires when this meeting
6698
+ // itself is being torn down.
6699
+ LoggerProxy.logger.info(
6700
+ `Meeting:index#updateLLMConnection --> skipping; LLM owned by meeting ${currentOwner}, not ${this.id}`
6701
+ );
6702
+
6703
+ return undefined;
6704
+ }
6705
+
6447
6706
  if (
6448
6707
  // @ts-ignore - Fix type
6449
6708
  url === this.webex.internal.llm.getLocusUrl() &&
@@ -6460,10 +6719,56 @@ export default class Meeting extends StatelessWebexPlugin {
6460
6719
  return undefined;
6461
6720
  }
6462
6721
 
6722
+ // Bind refresh handler before registration so interceptor-triggered token
6723
+ // refresh during register POST can resolve a valid handler.
6724
+ // Prefer this meeting as owner, but allow owner-less fallback when a stale
6725
+ // foreign owner tag is present on a disconnected session.
6726
+ const refreshHandlerOwnerMeetingId =
6727
+ currentOwner && currentOwner !== this.id ? undefined : this.id;
6728
+ const shouldAlignRefreshHandlerAfterOwnershipClaim = refreshHandlerOwnerMeetingId !== this.id;
6729
+ // @ts-ignore - Fix type
6730
+ this.webex.internal.llm.setRefreshHandler(
6731
+ () => this.refreshDataChannelToken(),
6732
+ LLM_DEFAULT_SESSION,
6733
+ refreshHandlerOwnerMeetingId
6734
+ );
6735
+
6463
6736
  // @ts-ignore - Fix type
6464
6737
  return this.webex.internal.llm
6465
6738
  .registerAndConnect(url, dataChannelUrl, datachannelToken)
6466
6739
  .then((registerAndConnectResult) => {
6740
+ this.locusInfo.syncAllHashTreeDatasets({onlyLLM: true});
6741
+ // Record ownership of the default LLM session for this meeting so
6742
+ // subsequent cross-meeting `updateLLMConnection` / `cleanupLLMConneciton`
6743
+ // calls can detect and skip work that doesn't belong to them.
6744
+ // @ts-ignore - Fix type
6745
+ const {isOwner} = this.webex.internal.llm.resolveSessionOwnership(
6746
+ this.id,
6747
+ LLM_DEFAULT_SESSION
6748
+ );
6749
+ const canReclaimAfterDisconnectedStart = !wasConnected;
6750
+
6751
+ // Refresh handler is pre-bound before registerAndConnect so token
6752
+ // refresh can work even during the registration request itself.
6753
+ if (isOwner || canReclaimAfterDisconnectedStart) {
6754
+ // Record ownership of the default LLM session for this meeting so
6755
+ // subsequent cross-meeting `updateLLMConnection` / `cleanupLLMConneciton`
6756
+ // calls can detect and skip work that doesn't belong to them.
6757
+ // @ts-ignore - Fix type
6758
+ this.webex.internal.llm.setOwnerMeetingId?.(this.id);
6759
+
6760
+ // If we pre-bound refresh ownerlessly (stale-owner reclaim path),
6761
+ // align the handler with the newly claimed owner immediately after
6762
+ // ownership is updated.
6763
+ if (shouldAlignRefreshHandlerAfterOwnershipClaim) {
6764
+ // @ts-ignore - Fix type
6765
+ this.webex.internal.llm.setRefreshHandler(
6766
+ () => this.refreshDataChannelToken(),
6767
+ LLM_DEFAULT_SESSION,
6768
+ this.id
6769
+ );
6770
+ }
6771
+ }
6467
6772
  // @ts-ignore - Fix type
6468
6773
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
6469
6774
  // @ts-ignore - Fix type
@@ -7504,6 +7809,33 @@ export default class Meeting extends StatelessWebexPlugin {
7504
7809
  }
7505
7810
  }
7506
7811
  });
7812
+ this.statsAnalyzer.on(StatsAnalyzerEventNames.STATS_UPDATE, (data) => {
7813
+ // Extract srtpCipher from transport stats
7814
+ let srtpCipher: string | undefined;
7815
+ for (const stats of data.stats.values()) {
7816
+ if (stats.type === 'transport' && stats.srtpCipher) {
7817
+ srtpCipher = stats.srtpCipher as string;
7818
+ break;
7819
+ }
7820
+ }
7821
+
7822
+ // Only emit event if srtpCipher has changed
7823
+ if (srtpCipher && srtpCipher !== this.mediaProperties.srtpCipher) {
7824
+ LoggerProxy.logger.info(
7825
+ `Meeting:index#setupStatsAnalyzerEventHandlers --> SRTP cipher changed from ${this.mediaProperties.srtpCipher} to ${srtpCipher}`
7826
+ );
7827
+ this.mediaProperties.srtpCipher = srtpCipher;
7828
+ Trigger.trigger(
7829
+ this,
7830
+ {
7831
+ file: 'meeting/index',
7832
+ function: 'setupStatsAnalyzerEventHandlers',
7833
+ },
7834
+ EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
7835
+ {srtpCipher}
7836
+ );
7837
+ }
7838
+ });
7507
7839
  };
7508
7840
 
7509
7841
  getMediaConnectionDebugId() {
@@ -7550,6 +7882,8 @@ export default class Meeting extends StatelessWebexPlugin {
7550
7882
  disableAudioMainDtx: this.config.experimental.disableAudioMainDtx,
7551
7883
  // @ts-ignore - config coming from registerPlugin
7552
7884
  enableAudioTwcc: this.config.enableAudioTwccForMultistream,
7885
+ // @ts-ignore - config coming from registerPlugin
7886
+ enableAv1SlidesSupport: this.config.enableAv1SlidesSupport,
7553
7887
  stopIceGatheringAfterFirstRelayCandidate:
7554
7888
  // @ts-ignore - config coming from registerPlugin
7555
7889
  this.config.stopIceGatheringAfterFirstRelayCandidate,
@@ -8797,6 +9131,8 @@ export default class Meeting extends StatelessWebexPlugin {
8797
9131
  });
8798
9132
  LoggerProxy.logger.log('Meeting:index#leave --> Leaving a meeting');
8799
9133
 
9134
+ this.stopListeningForMeetingEvents();
9135
+
8800
9136
  return MeetingUtil.leaveMeeting(this, options)
8801
9137
  .then(async (leave) => {
8802
9138
  // CA team recommends submitting this *after* locus /leave
@@ -9661,6 +9997,8 @@ export default class Meeting extends StatelessWebexPlugin {
9661
9997
  locus_id: this.locusId,
9662
9998
  });
9663
9999
 
10000
+ this.stopListeningForMeetingEvents();
10001
+
9664
10002
  return MeetingUtil.endMeetingForAll(this)
9665
10003
  .then(async (end) => {
9666
10004
  this.meetingFiniteStateMachine.end();
@@ -9722,12 +10060,15 @@ export default class Meeting extends StatelessWebexPlugin {
9722
10060
  }
9723
10061
  this.queuedMediaUpdates = [];
9724
10062
 
9725
- this.stopTranscription();
9726
- this.transcription = undefined;
9727
-
9728
- this.annotation.deregisterEvents();
9729
-
10063
+ // Listener teardown (transcription, annotation, llm/mercury) runs in
10064
+ // stopListeningForMeetingEvents() before /leave and /end so events
10065
+ // received mid-teardown do not trigger Locus syncs. Calling it here
10066
+ // again would double-emit MEETING_STOPPED_RECEIVING_TRANSCRIPTION
10067
+ // because stopTranscription() always fires its trigger.
10068
+ //
10069
+ // Ownership-aware token clear is encapsulated inside clearDataChannelToken().
9730
10070
  this.clearDataChannelToken();
10071
+
9731
10072
  await this.cleanupLLMConneciton({throwOnError: false});
9732
10073
  };
9733
10074
 
@@ -9804,15 +10145,12 @@ export default class Meeting extends StatelessWebexPlugin {
9804
10145
  * @public
9805
10146
  * @memberof Meeting
9806
10147
  */
9807
- public sendReaction(reactionType: ReactionServerType, skinToneType?: SkinToneType) {
10148
+ public sendReaction(reactionType: string, skinToneType?: SkinToneType) {
9808
10149
  const reactionChannelUrl = this.locusInfo?.controls?.reactions?.reactionChannelUrl as string;
9809
10150
  const participantId = this.members.selfId;
9810
10151
 
9811
- const reactionData = Reactions[reactionType];
10152
+ const reactionData = Reactions[reactionType] || {type: reactionType};
9812
10153
 
9813
- if (!reactionData) {
9814
- return Promise.reject(new Error(`${reactionType} is not a valid reaction.`));
9815
- }
9816
10154
  const skinToneData = SkinTones[skinToneType] || SkinTones.normal;
9817
10155
  const reaction: Reaction = {
9818
10156
  ...reactionData,