@webex/plugin-meetings 3.12.0-next.6 → 3.12.0-next.60

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 (158) 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 +6 -3
  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 +10 -1
  19. package/dist/hashTree/constants.js.map +1 -1
  20. package/dist/hashTree/hashTreeParser.js +716 -370
  21. package/dist/hashTree/hashTreeParser.js.map +1 -1
  22. package/dist/hashTree/utils.js +22 -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/locusRetry.js +23 -8
  27. package/dist/interceptors/locusRetry.js.map +1 -1
  28. package/dist/interpretation/index.js +10 -1
  29. package/dist/interpretation/index.js.map +1 -1
  30. package/dist/interpretation/siLanguage.js +1 -1
  31. package/dist/locus-info/controlsUtils.js +4 -1
  32. package/dist/locus-info/controlsUtils.js.map +1 -1
  33. package/dist/locus-info/index.js +289 -87
  34. package/dist/locus-info/index.js.map +1 -1
  35. package/dist/locus-info/types.js +19 -0
  36. package/dist/locus-info/types.js.map +1 -1
  37. package/dist/media/index.js +3 -1
  38. package/dist/media/index.js.map +1 -1
  39. package/dist/media/properties.js +1 -0
  40. package/dist/media/properties.js.map +1 -1
  41. package/dist/meeting/in-meeting-actions.js +3 -1
  42. package/dist/meeting/in-meeting-actions.js.map +1 -1
  43. package/dist/meeting/index.js +907 -535
  44. package/dist/meeting/index.js.map +1 -1
  45. package/dist/meeting/util.js +19 -2
  46. package/dist/meeting/util.js.map +1 -1
  47. package/dist/meetings/index.js +231 -78
  48. package/dist/meetings/index.js.map +1 -1
  49. package/dist/meetings/meetings.types.js +6 -1
  50. package/dist/meetings/meetings.types.js.map +1 -1
  51. package/dist/meetings/request.js +39 -0
  52. package/dist/meetings/request.js.map +1 -1
  53. package/dist/meetings/util.js +79 -5
  54. package/dist/meetings/util.js.map +1 -1
  55. package/dist/member/index.js +10 -0
  56. package/dist/member/index.js.map +1 -1
  57. package/dist/member/types.js.map +1 -1
  58. package/dist/member/util.js +3 -0
  59. package/dist/member/util.js.map +1 -1
  60. package/dist/metrics/constants.js +4 -1
  61. package/dist/metrics/constants.js.map +1 -1
  62. package/dist/multistream/codec/constants.js +63 -0
  63. package/dist/multistream/codec/constants.js.map +1 -0
  64. package/dist/multistream/mediaRequestManager.js +62 -15
  65. package/dist/multistream/mediaRequestManager.js.map +1 -1
  66. package/dist/multistream/receiveSlot.js +9 -0
  67. package/dist/multistream/receiveSlot.js.map +1 -1
  68. package/dist/reactions/reactions.type.js.map +1 -1
  69. package/dist/recording-controller/index.js +1 -3
  70. package/dist/recording-controller/index.js.map +1 -1
  71. package/dist/types/config.d.ts +2 -0
  72. package/dist/types/constants.d.ts +2 -0
  73. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  74. package/dist/types/controls-options-manager/index.d.ts +10 -0
  75. package/dist/types/hashTree/constants.d.ts +1 -0
  76. package/dist/types/hashTree/hashTreeParser.d.ts +92 -16
  77. package/dist/types/hashTree/utils.d.ts +11 -0
  78. package/dist/types/index.d.ts +2 -0
  79. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  80. package/dist/types/locus-info/index.d.ts +46 -6
  81. package/dist/types/locus-info/types.d.ts +21 -1
  82. package/dist/types/media/properties.d.ts +1 -0
  83. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  84. package/dist/types/meeting/index.d.ts +87 -3
  85. package/dist/types/meeting/util.d.ts +8 -0
  86. package/dist/types/meetings/index.d.ts +30 -2
  87. package/dist/types/meetings/meetings.types.d.ts +15 -0
  88. package/dist/types/meetings/request.d.ts +14 -0
  89. package/dist/types/member/index.d.ts +1 -0
  90. package/dist/types/member/types.d.ts +1 -0
  91. package/dist/types/member/util.d.ts +1 -0
  92. package/dist/types/metrics/constants.d.ts +3 -0
  93. package/dist/types/multistream/codec/constants.d.ts +7 -0
  94. package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
  95. package/dist/types/reactions/reactions.type.d.ts +3 -0
  96. package/dist/webinar/index.js +361 -235
  97. package/dist/webinar/index.js.map +1 -1
  98. package/package.json +22 -22
  99. package/src/aiEnableRequest/index.ts +16 -0
  100. package/src/breakouts/breakout.ts +3 -1
  101. package/src/breakouts/index.ts +31 -0
  102. package/src/config.ts +2 -0
  103. package/src/constants.ts +5 -1
  104. package/src/controls-options-manager/constants.ts +14 -1
  105. package/src/controls-options-manager/index.ts +47 -24
  106. package/src/controls-options-manager/util.ts +81 -1
  107. package/src/hashTree/constants.ts +9 -0
  108. package/src/hashTree/hashTreeParser.ts +429 -183
  109. package/src/hashTree/utils.ts +17 -0
  110. package/src/index.ts +5 -0
  111. package/src/interceptors/locusRetry.ts +25 -4
  112. package/src/interpretation/index.ts +25 -8
  113. package/src/locus-info/controlsUtils.ts +3 -1
  114. package/src/locus-info/index.ts +291 -97
  115. package/src/locus-info/types.ts +25 -1
  116. package/src/media/index.ts +3 -0
  117. package/src/media/properties.ts +1 -0
  118. package/src/meeting/in-meeting-actions.ts +4 -0
  119. package/src/meeting/index.ts +388 -33
  120. package/src/meeting/util.ts +20 -2
  121. package/src/meetings/index.ts +134 -44
  122. package/src/meetings/meetings.types.ts +19 -0
  123. package/src/meetings/request.ts +43 -0
  124. package/src/meetings/util.ts +97 -1
  125. package/src/member/index.ts +10 -0
  126. package/src/member/types.ts +1 -0
  127. package/src/member/util.ts +3 -0
  128. package/src/metrics/constants.ts +3 -0
  129. package/src/multistream/codec/constants.ts +58 -0
  130. package/src/multistream/mediaRequestManager.ts +119 -28
  131. package/src/multistream/receiveSlot.ts +18 -0
  132. package/src/reactions/reactions.type.ts +3 -0
  133. package/src/recording-controller/index.ts +1 -2
  134. package/src/webinar/index.ts +162 -21
  135. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  136. package/test/unit/spec/breakouts/breakout.ts +9 -3
  137. package/test/unit/spec/breakouts/index.ts +49 -0
  138. package/test/unit/spec/controls-options-manager/index.js +140 -29
  139. package/test/unit/spec/controls-options-manager/util.js +165 -0
  140. package/test/unit/spec/hashTree/hashTreeParser.ts +1508 -149
  141. package/test/unit/spec/hashTree/utils.ts +88 -1
  142. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  143. package/test/unit/spec/interpretation/index.ts +26 -4
  144. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  145. package/test/unit/spec/locus-info/index.js +475 -81
  146. package/test/unit/spec/media/index.ts +31 -0
  147. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  148. package/test/unit/spec/meeting/index.js +1131 -49
  149. package/test/unit/spec/meeting/muteState.js +3 -0
  150. package/test/unit/spec/meeting/utils.js +33 -0
  151. package/test/unit/spec/meetings/index.js +360 -10
  152. package/test/unit/spec/meetings/request.js +141 -0
  153. package/test/unit/spec/meetings/utils.js +189 -0
  154. package/test/unit/spec/member/index.js +7 -0
  155. package/test/unit/spec/member/util.js +24 -0
  156. package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
  157. package/test/unit/spec/recording-controller/index.js +9 -8
  158. package/test/unit/spec/webinar/index.ts +141 -16
@@ -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,7 @@ import {
137
136
  STAGE_MANAGER_TYPE,
138
137
  LOCUSEVENT,
139
138
  LOCUS_LLM_EVENT,
139
+ LLM_PRACTICE_SESSION,
140
140
  } from '../constants';
141
141
  import BEHAVIORAL_METRICS from '../metrics/constants';
142
142
  import ParameterError from '../common/errors/parameter';
@@ -612,7 +612,7 @@ export default class Meeting extends StatelessWebexPlugin {
612
612
  webinar: any;
613
613
  conversationUrl: string;
614
614
  callStateForMetrics: CallStateForMetrics;
615
- destination: string;
615
+ destination: string | LocusDTO;
616
616
  destinationType: DESTINATION_TYPE;
617
617
  deviceUrl: string;
618
618
  hostId: string;
@@ -651,6 +651,8 @@ export default class Meeting extends StatelessWebexPlugin {
651
651
  floorGrantPending: boolean;
652
652
  hasJoinedOnce: boolean;
653
653
  hasWebsocketConnected: boolean;
654
+ private mercuryOnlineHandler?: () => void;
655
+ private mercuryOfflineHandler?: () => void;
654
656
  inMeetingActions: InMeetingActions;
655
657
  isLocalShareLive: boolean;
656
658
  isRoapInProgress: boolean;
@@ -935,7 +937,7 @@ export default class Meeting extends StatelessWebexPlugin {
935
937
  this.simultaneousInterpretation = new SimultaneousInterpretation({}, {parent: this.webex});
936
938
 
937
939
  // @ts-ignore
938
- this.aiEnableRequest = new AIEnableRequest({}, {parent: this.webex});
940
+ this.aiEnableRequest = new AIEnableRequest({locusUrl: this.locusUrl}, {parent: this.webex});
939
941
 
940
942
  /**
941
943
  * @instance
@@ -966,6 +968,7 @@ export default class Meeting extends StatelessWebexPlugin {
966
968
  },
967
969
  (csi: CSI) => (this.members.findMemberByCsi(csi) as any)?.id
968
970
  );
971
+
969
972
  /**
970
973
  * Object containing helper classes for managing media requests for audio/video/screenshare (for multistream media connections)
971
974
  * All multistream media requests sent out for this meeting have to go through them.
@@ -985,6 +988,7 @@ export default class Meeting extends StatelessWebexPlugin {
985
988
  mediaRequests
986
989
  );
987
990
  },
991
+ this.getIngressPayloadTypeCallback.bind(this),
988
992
  {
989
993
  // @ts-ignore - config coming from registerPlugin
990
994
  degradationPreferences: this.config.degradationPreferences,
@@ -1006,6 +1010,7 @@ export default class Meeting extends StatelessWebexPlugin {
1006
1010
  mediaRequests
1007
1011
  );
1008
1012
  },
1013
+ this.getIngressPayloadTypeCallback.bind(this),
1009
1014
  {
1010
1015
  // @ts-ignore - config coming from registerPlugin
1011
1016
  degradationPreferences: this.config.degradationPreferences,
@@ -1027,6 +1032,7 @@ export default class Meeting extends StatelessWebexPlugin {
1027
1032
  mediaRequests
1028
1033
  );
1029
1034
  },
1035
+ this.getIngressPayloadTypeCallback.bind(this),
1030
1036
  {
1031
1037
  // @ts-ignore - config coming from registerPlugin
1032
1038
  degradationPreferences: this.config.degradationPreferences,
@@ -1048,11 +1054,14 @@ export default class Meeting extends StatelessWebexPlugin {
1048
1054
  mediaRequests
1049
1055
  );
1050
1056
  },
1057
+ this.getIngressPayloadTypeCallback.bind(this),
1051
1058
  {
1052
1059
  // @ts-ignore - config coming from registerPlugin
1053
1060
  degradationPreferences: this.config.degradationPreferences,
1054
1061
  kind: 'video',
1055
1062
  trimRequestsToNumOfSources: false,
1063
+ // @ts-ignore - config coming from registerPlugin
1064
+ enableAv1: this.config.enableAv1SlidesSupport,
1056
1065
  }
1057
1066
  ),
1058
1067
  };
@@ -1713,6 +1722,37 @@ export default class Meeting extends StatelessWebexPlugin {
1713
1722
  this.mediaServerIp = undefined;
1714
1723
  }
1715
1724
 
1725
+ /**
1726
+ * Get the ingress payload type for a given media type and codec mime type
1727
+ * @param {MediaType} mediaType - The media type
1728
+ * @param {MediaCodecMimeType} codecMimeType - The codec mime type
1729
+ * @returns {number | undefined} - The ingress payload type
1730
+ * @private
1731
+ * @memberof Meeting
1732
+ */
1733
+ private getIngressPayloadTypeCallback(
1734
+ mediaType: MediaType,
1735
+ codecMimeType: MediaCodecMimeType
1736
+ ): number | undefined {
1737
+ if (this.isMultistream) {
1738
+ try {
1739
+ return this.mediaProperties.webrtcMediaConnection.getIngressPayloadType(
1740
+ mediaType,
1741
+ codecMimeType
1742
+ );
1743
+ } catch (error) {
1744
+ LoggerProxy.logger.info(
1745
+ `Meeting:index#mediaRequestManager --> failed to get ingress payload type for mediaType=${mediaType}, codecMimeType=${codecMimeType}`,
1746
+ error
1747
+ );
1748
+
1749
+ return undefined;
1750
+ }
1751
+ }
1752
+
1753
+ return undefined;
1754
+ }
1755
+
1716
1756
  /**
1717
1757
  * Temporary func to return webex object,
1718
1758
  * in order to access internal plugin metrics
@@ -2789,7 +2829,7 @@ export default class Meeting extends StatelessWebexPlugin {
2789
2829
  private setupLocusControlsListener() {
2790
2830
  this.locusInfo.on(
2791
2831
  LOCUSINFO.EVENTS.CONTROLS_RECORDING_UPDATED,
2792
- ({state, modifiedBy, lastModified}) => {
2832
+ ({state, modifiedBy, lastModified, modifiedByServiceAppName, modifiedByServiceAppId}) => {
2793
2833
  let event;
2794
2834
 
2795
2835
  switch (state) {
@@ -2815,6 +2855,8 @@ export default class Meeting extends StatelessWebexPlugin {
2815
2855
  state: state === RECORDING_STATE.RESUMED ? RECORDING_STATE.RECORDING : state,
2816
2856
  modifiedBy,
2817
2857
  lastModified,
2858
+ modifiedByServiceAppName,
2859
+ modifiedByServiceAppId,
2818
2860
  };
2819
2861
  Trigger.trigger(
2820
2862
  this,
@@ -3459,6 +3501,7 @@ export default class Meeting extends StatelessWebexPlugin {
3459
3501
  this.breakouts.locusUrlUpdate(url);
3460
3502
  this.simultaneousInterpretation.locusUrlUpdate(url);
3461
3503
  this.annotation.locusUrlUpdate(url);
3504
+ this.aiEnableRequest.locusUrlUpdate(url);
3462
3505
  this.locusUrl = url;
3463
3506
  this.locusId = this.locusUrl?.split('/').pop();
3464
3507
  this.recordingController.setLocusUrl(this.locusUrl);
@@ -3734,7 +3777,7 @@ export default class Meeting extends StatelessWebexPlugin {
3734
3777
  });
3735
3778
  this.updateLLMConnection();
3736
3779
  });
3737
- this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, async (payload) => {
3780
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, (payload) => {
3738
3781
  this.stopKeepAlive();
3739
3782
 
3740
3783
  if (payload) {
@@ -3760,6 +3803,15 @@ export default class Meeting extends StatelessWebexPlugin {
3760
3803
  });
3761
3804
  }
3762
3805
  this.rtcMetrics?.sendNextMetrics();
3806
+
3807
+ this.ensureDefaultDatachannelTokenAfterAdmit().catch((error) => {
3808
+ LoggerProxy.logger.warn(
3809
+ `Meeting:index#setUpLocusInfoSelfListener --> failed post-admit token prefetch flow: ${
3810
+ error?.message || String(error)
3811
+ }`
3812
+ );
3813
+ });
3814
+
3763
3815
  this.updateLLMConnection();
3764
3816
  });
3765
3817
 
@@ -4602,6 +4654,9 @@ export default class Meeting extends StatelessWebexPlugin {
4602
4654
  ),
4603
4655
  isAttendeeRequestAiAssistantDeclinedAll:
4604
4656
  MeetingUtil.attendeeRequestAiAssistantDeclinedAll(this.userDisplayHints),
4657
+ isAnonymizeDisplayNamesEnabled: MeetingUtil.isAnonymizeDisplayNamesEnabled(
4658
+ this.userDisplayHints
4659
+ ),
4605
4660
  }) || changed;
4606
4661
  }
4607
4662
  if (changed) {
@@ -4650,6 +4705,34 @@ export default class Meeting extends StatelessWebexPlugin {
4650
4705
  this.sipUri = sipUri;
4651
4706
  }
4652
4707
 
4708
+ /**
4709
+ * After initial locus setup, refreshes destination with synced locus data and optionally
4710
+ * performs deferred meeting info fetch when initial locus was incomplete.
4711
+ * @param {LocusDTO} locus
4712
+ * @returns {void}
4713
+ */
4714
+ public async finalizeMeetingAfterInitialLocusSetup(locus: LocusDTO): Promise<void> {
4715
+ if (locus && this?.destinationType === DESTINATION_TYPE.LOCUS_ID) {
4716
+ // destination is initialized from the initial locus snapshot in constructor,
4717
+ // so refresh it after locus sync to avoid stale partial hash-tree data.
4718
+ this.destination = locus;
4719
+ }
4720
+ if (
4721
+ (!this.meetingInfo || isEmpty(this.meetingInfo)) &&
4722
+ (this.destination as LocusDTO)?.info &&
4723
+ !this.fetchMeetingInfoTimeoutId &&
4724
+ !MeetingsUtil.isOneOnOneCall(locus)
4725
+ ) {
4726
+ try {
4727
+ await this.fetchMeetingInfo({});
4728
+ } catch (error: any) {
4729
+ LoggerProxy.logger.info(
4730
+ `Meeting:index#finalizeMeetingAfterInitialLocusSetup --> deferred fetchMeetingInfo failed: ${error.message}`
4731
+ );
4732
+ }
4733
+ }
4734
+ }
4735
+
4653
4736
  /**
4654
4737
  * Set the locus info the class instance. Should be called with the parsed locus
4655
4738
  * we got in the join response.
@@ -5121,8 +5204,7 @@ export default class Meeting extends StatelessWebexPlugin {
5121
5204
  public setMercuryListener() {
5122
5205
  // Client will have a socket manager and handle reconnecting to mercury, when we reconnect to mercury
5123
5206
  // if the meeting has active peer connections, it should try to reconnect.
5124
- // @ts-ignore
5125
- this.webex.internal.mercury.on(ONLINE, () => {
5207
+ this.mercuryOnlineHandler = () => {
5126
5208
  LoggerProxy.logger.info('Meeting:index#setMercuryListener --> Web socket online');
5127
5209
 
5128
5210
  // Only send restore event when it was disconnected before and for connected later
@@ -5132,15 +5214,47 @@ export default class Meeting extends StatelessWebexPlugin {
5132
5214
  });
5133
5215
  }
5134
5216
  this.hasWebsocketConnected = true;
5135
- });
5217
+ };
5136
5218
 
5137
- // @ts-ignore
5138
- this.webex.internal.mercury.on(OFFLINE, () => {
5219
+ this.mercuryOfflineHandler = () => {
5139
5220
  LoggerProxy.logger.error('Meeting:index#setMercuryListener --> Web socket offline');
5140
5221
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MERCURY_CONNECTION_FAILURE, {
5141
5222
  correlation_id: this.correlationId,
5142
5223
  });
5143
- });
5224
+ };
5225
+
5226
+ // @ts-ignore
5227
+ this.webex.internal.mercury.on(ONLINE, this.mercuryOnlineHandler);
5228
+ // @ts-ignore
5229
+ this.webex.internal.mercury.on(OFFLINE, this.mercuryOfflineHandler);
5230
+ }
5231
+
5232
+ /**
5233
+ * Removes this meeting's Mercury ONLINE/OFFLINE event listeners registered
5234
+ * by setMercuryListener(). Must be called before Locus /leave to avoid
5235
+ * unnecessary syncs/metrics triggered by events received while leaving
5236
+ * (per Locus team recommendation).
5237
+ *
5238
+ * Mercury is a process-wide singleton shared with other plugins, so we
5239
+ * pass the bound handler refs to .off() to avoid clearing every listener
5240
+ * for ONLINE/OFFLINE on the shared emitter.
5241
+ *
5242
+ * Idempotent: subsequent calls are no-ops because the handler refs are
5243
+ * cleared after detaching.
5244
+ * @private
5245
+ * @returns {void}
5246
+ */
5247
+ private stopListeningForMercuryEvents() {
5248
+ if (this.mercuryOnlineHandler) {
5249
+ // @ts-ignore
5250
+ this.webex.internal.mercury.off(ONLINE, this.mercuryOnlineHandler);
5251
+ this.mercuryOnlineHandler = undefined;
5252
+ }
5253
+ if (this.mercuryOfflineHandler) {
5254
+ // @ts-ignore
5255
+ this.webex.internal.mercury.off(OFFLINE, this.mercuryOfflineHandler);
5256
+ this.mercuryOfflineHandler = undefined;
5257
+ }
5144
5258
  }
5145
5259
 
5146
5260
  /**
@@ -5856,6 +5970,31 @@ export default class Meeting extends StatelessWebexPlugin {
5856
5970
  }
5857
5971
  };
5858
5972
 
5973
+ /**
5974
+ * Verifies the relay event was delivered for the active LLM session binding.
5975
+ * @param {RelayEvent} event Event object coming from LLM Connection
5976
+ * @returns {boolean}
5977
+ */
5978
+ private isRelayEventRouteValid(event: RelayEvent): boolean {
5979
+ const route = event?.headers?.route;
5980
+
5981
+ if (!route) {
5982
+ return true;
5983
+ }
5984
+
5985
+ const {llm} = (this as any).webex.internal;
5986
+ const isPracticeSession = llm.isConnected(LLM_PRACTICE_SESSION);
5987
+ const expectedBinding = isPracticeSession
5988
+ ? llm.getBinding(LLM_PRACTICE_SESSION)
5989
+ : llm.getBinding();
5990
+
5991
+ if (!expectedBinding || route === expectedBinding) {
5992
+ return true;
5993
+ }
5994
+
5995
+ return false;
5996
+ }
5997
+
5859
5998
  /**
5860
5999
  * Callback called when a relay event is received from meeting LLM Connection
5861
6000
  * @param {RelayEvent} e Event object coming from LLM Connection
@@ -5863,6 +6002,9 @@ export default class Meeting extends StatelessWebexPlugin {
5863
6002
  * @returns {void}
5864
6003
  */
5865
6004
  private processRelayEvent = (e: RelayEvent): void => {
6005
+ if (!this.isRelayEventRouteValid(e)) {
6006
+ return;
6007
+ }
5866
6008
  switch (e.data.relayType) {
5867
6009
  case REACTION_RELAY_TYPES.REACTION:
5868
6010
  if (
@@ -5960,6 +6102,30 @@ export default class Meeting extends StatelessWebexPlugin {
5960
6102
  );
5961
6103
  }
5962
6104
 
6105
+ /**
6106
+ * Restores LLM subchannel subscriptions after reconnect when captions are active.
6107
+ * @returns {void}
6108
+ */
6109
+ private restoreLLMSubscriptionsIfNeeded(): void {
6110
+ try {
6111
+ // @ts-ignore
6112
+ const isCaptionBoxOn = this.webex.internal.voicea?.getIsCaptionBoxOn?.();
6113
+
6114
+ if (!isCaptionBoxOn) {
6115
+ return;
6116
+ }
6117
+
6118
+ // @ts-ignore
6119
+ this.webex.internal.voicea.updateSubchannelSubscriptions({subscribe: ['transcription']});
6120
+ } catch (error) {
6121
+ const msg = error?.message || String(error);
6122
+
6123
+ LoggerProxy.logger.warn(
6124
+ `Meeting:index#restoreLLMSubscriptionsIfNeeded --> failed to restore subscriptions after LLM online: ${msg}`
6125
+ );
6126
+ }
6127
+ }
6128
+
5963
6129
  /**
5964
6130
  * This is a callback for the LLM event that is triggered when it comes online
5965
6131
  * This method in turn will trigger an event to the developers that the LLM is connected
@@ -5968,8 +6134,8 @@ export default class Meeting extends StatelessWebexPlugin {
5968
6134
  * @returns {null}
5969
6135
  */
5970
6136
  private handleLLMOnline = (): void => {
5971
- // @ts-ignore
5972
- this.webex.internal.llm.off('online', this.handleLLMOnline);
6137
+ this.restoreLLMSubscriptionsIfNeeded();
6138
+
5973
6139
  Trigger.trigger(
5974
6140
  this,
5975
6141
  {
@@ -6200,6 +6366,8 @@ export default class Meeting extends StatelessWebexPlugin {
6200
6366
  this.saveDataChannelToken(join);
6201
6367
  // @ts-ignore - config coming from registerPlugin
6202
6368
  if (this.config.enableAutomaticLLM) {
6369
+ // @ts-ignore
6370
+ this.webex.internal.llm.off('online', this.handleLLMOnline);
6203
6371
  // @ts-ignore
6204
6372
  this.webex.internal.llm.on('online', this.handleLLMOnline);
6205
6373
  this.updateLLMConnection()
@@ -6266,8 +6434,57 @@ export default class Meeting extends StatelessWebexPlugin {
6266
6434
  }
6267
6435
  }
6268
6436
 
6437
+ /**
6438
+ * Removes LLM event listeners and clears the health check timer.
6439
+ * Must be called before Locus /leave to avoid unnecessary syncs triggered
6440
+ * by events received while leaving (per Locus team recommendation).
6441
+ * Idempotent: safe to call multiple times; .off() is a no-op when no
6442
+ * matching listener is registered.
6443
+ * @private
6444
+ * @returns {void}
6445
+ */
6446
+ private stopListeningForLLMEvents() {
6447
+ // @ts-ignore - fix types
6448
+ this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
6449
+ // @ts-ignore - fix types
6450
+ this.webex.internal.llm.off(LOCUS_LLM_EVENT, this.processLocusLLMEvent);
6451
+ this.clearLLMHealthCheckTimer();
6452
+ }
6453
+
6454
+ /**
6455
+ * Stops listening on every event bus (LLM, Mercury, voicea/transcription,
6456
+ * annotation) that could otherwise deliver events to this meeting while
6457
+ * Locus is processing /leave or /end. Per the Locus team recommendation,
6458
+ * this must run before the Locus request is dispatched to avoid
6459
+ * unnecessary syncs triggered by in-flight events.
6460
+ *
6461
+ * Voicea (transcription) subscribes to llm 'event:relay.event' internally,
6462
+ * and the annotation plugin subscribes to both mercury and llm, so both
6463
+ * must be torn down alongside the direct LLM/Mercury listeners.
6464
+ *
6465
+ * Idempotent: safe to call multiple times; .off() is a no-op when no
6466
+ * matching listener is registered, and stopTranscription is guarded.
6467
+ * @private
6468
+ * @returns {void}
6469
+ */
6470
+ private stopListeningForMeetingEvents() {
6471
+ this.stopListeningForLLMEvents();
6472
+ this.stopListeningForMercuryEvents();
6473
+ if (this.transcription) {
6474
+ this.stopTranscription();
6475
+ this.transcription = undefined;
6476
+ }
6477
+ this.annotation.deregisterEvents();
6478
+ }
6479
+
6269
6480
  /**
6270
6481
  * Disconnects and cleans up the default LLM session listeners/timers.
6482
+ *
6483
+ * Ownership-aware: only calls `disconnectLLM` when this meeting is the
6484
+ * current owner of the default LLM session (or when no owner is recorded).
6485
+ * Event listeners belonging to this meeting instance are always detached
6486
+ * so they do not receive another meeting's relay events.
6487
+ *
6271
6488
  * @param {Object} options
6272
6489
  * @param {boolean} [options.removeOnlineListener=true] removes the one-time online listener
6273
6490
  * @param {boolean} [options.throwOnError=true] rethrows disconnect errors when true
@@ -6280,12 +6497,22 @@ export default class Meeting extends StatelessWebexPlugin {
6280
6497
  removeOnlineListener?: boolean;
6281
6498
  throwOnError?: boolean;
6282
6499
  } = {}): Promise<void> => {
6500
+ // @ts-ignore - Fix type
6501
+ const currentOwner = this.webex.internal.llm.getOwnerMeetingId();
6502
+ const isOwner = !currentOwner || currentOwner === this.id;
6503
+
6283
6504
  try {
6284
- // @ts-ignore - Fix type
6285
- await this.webex.internal.llm.disconnectLLM({
6286
- code: 3050,
6287
- reason: 'done (permanent)',
6288
- });
6505
+ if (isOwner) {
6506
+ // @ts-ignore - Fix type
6507
+ await this.webex.internal.llm.disconnectLLM({
6508
+ code: 3050,
6509
+ reason: 'done (permanent)',
6510
+ });
6511
+ } else {
6512
+ LoggerProxy.logger.info(
6513
+ `Meeting:index#cleanupLLMConneciton --> skipping disconnect; LLM owned by meeting ${currentOwner}, not ${this.id}`
6514
+ );
6515
+ }
6289
6516
  } catch (error) {
6290
6517
  LoggerProxy.logger.error(
6291
6518
  'Meeting:index#cleanupLLMConneciton --> Failed to disconnect default LLM session',
@@ -6300,12 +6527,18 @@ export default class Meeting extends StatelessWebexPlugin {
6300
6527
  // @ts-ignore - Fix type
6301
6528
  this.webex.internal.llm.off('online', this.handleLLMOnline);
6302
6529
  }
6303
- // @ts-ignore - fix types
6304
- this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
6305
- // @ts-ignore - Fix type
6306
- this.webex.internal.llm.off(LOCUS_LLM_EVENT, this.processLocusLLMEvent);
6530
+ this.stopListeningForLLMEvents();
6307
6531
 
6308
- this.clearLLMHealthCheckTimer();
6532
+ // If this meeting owned (or could have owned) the default LLM session,
6533
+ // always release the owner tag here regardless of whether disconnectLLM
6534
+ // resolved. `disconnectLLM` only clears the owner on its success path,
6535
+ // so a failed disconnect would otherwise leave a stale owner pointing
6536
+ // at a torn-down meeting and permanently block other meetings'
6537
+ // `updateLLMConnection` calls via the ownership guard.
6538
+ if (isOwner) {
6539
+ // @ts-ignore - Fix type
6540
+ this.webex.internal.llm.setOwnerMeetingId?.(undefined);
6541
+ }
6309
6542
  }
6310
6543
  };
6311
6544
 
@@ -6343,6 +6576,52 @@ export default class Meeting extends StatelessWebexPlugin {
6343
6576
  }
6344
6577
  }
6345
6578
 
6579
+ /**
6580
+ * Ensures default-session data channel token exists after lobby admission.
6581
+ * Some lobby users do not receive a token until they are admitted.
6582
+ * @returns {Promise<boolean>} true when a new token is fetched and cached
6583
+ */
6584
+ private async ensureDefaultDatachannelTokenAfterAdmit(): Promise<boolean> {
6585
+ try {
6586
+ // @ts-ignore
6587
+ const datachannelToken = this.webex.internal.llm.getDatachannelToken();
6588
+ // @ts-ignore
6589
+ const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
6590
+
6591
+ if (!isDataChannelTokenEnabled || datachannelToken) {
6592
+ return false;
6593
+ }
6594
+
6595
+ const response = await this.meetingRequest.fetchDatachannelToken({
6596
+ locusUrl: this.locusUrl,
6597
+ requestingParticipantId: this.members.selfId,
6598
+ isPracticeSession: false,
6599
+ });
6600
+ const fetchedDatachannelToken = response?.body?.datachannelToken;
6601
+
6602
+ if (!fetchedDatachannelToken) {
6603
+ return false;
6604
+ }
6605
+
6606
+ // @ts-ignore
6607
+ this.webex.internal.llm.setDatachannelToken(
6608
+ fetchedDatachannelToken,
6609
+ DataChannelTokenType.Default
6610
+ );
6611
+
6612
+ return true;
6613
+ } catch (error) {
6614
+ const msg = error?.message || String(error);
6615
+
6616
+ LoggerProxy.logger.warn(
6617
+ `Meeting:index#ensureDefaultDatachannelTokenAfterAdmit --> failed to proactively fetch default data channel token after admit: ${msg}`,
6618
+ {statusCode: error?.statusCode}
6619
+ );
6620
+
6621
+ return false;
6622
+ }
6623
+ }
6624
+
6346
6625
  /**
6347
6626
  * Connects to low latency mercury and reconnects if the address has changed
6348
6627
  * It will also disconnect if called when the meeting has ended
@@ -6361,8 +6640,33 @@ export default class Meeting extends StatelessWebexPlugin {
6361
6640
 
6362
6641
  const dataChannelUrl = datachannelUrl;
6363
6642
 
6643
+ // Ownership guard: when the default LLM session is already connected and
6644
+ // owned by a *different* Meeting instance, do not disconnect or reconfigure
6645
+ // it. Another meeting's `updateLLMConnection` must be ignored here to
6646
+ // avoid killing the socket it relies on. We only proceed to manage the
6647
+ // connection when this meeting is the current owner, or when no owner is
6648
+ // set yet (first claim).
6649
+ // @ts-ignore - Fix type
6650
+ const currentOwner = this.webex.internal.llm.getOwnerMeetingId();
6651
+
6364
6652
  // @ts-ignore - Fix type
6365
6653
  if (this.webex.internal.llm.isConnected()) {
6654
+ if (currentOwner && currentOwner !== this.id) {
6655
+ // Another meeting owns the live LLM socket. We must not disconnect
6656
+ // or reconfigure it -- doing so would tear down a session the
6657
+ // owning meeting still relies on. Locus/datachannel URL mismatch is
6658
+ // expected here (each meeting has its own locus URL) and is NOT a
6659
+ // valid signal of staleness, so we never reclaim from this path.
6660
+ // The only safe reclaim mechanism is the `finally`-block owner-tag
6661
+ // release in `cleanupLLMConneciton`, which fires when this meeting
6662
+ // itself is being torn down.
6663
+ LoggerProxy.logger.info(
6664
+ `Meeting:index#updateLLMConnection --> skipping; LLM owned by meeting ${currentOwner}, not ${this.id}`
6665
+ );
6666
+
6667
+ return undefined;
6668
+ }
6669
+
6366
6670
  if (
6367
6671
  // @ts-ignore - Fix type
6368
6672
  url === this.webex.internal.llm.getLocusUrl() &&
@@ -6383,6 +6687,11 @@ export default class Meeting extends StatelessWebexPlugin {
6383
6687
  return this.webex.internal.llm
6384
6688
  .registerAndConnect(url, dataChannelUrl, datachannelToken)
6385
6689
  .then((registerAndConnectResult) => {
6690
+ // Record ownership of the default LLM session for this meeting so
6691
+ // subsequent cross-meeting `updateLLMConnection` / `cleanupLLMConneciton`
6692
+ // calls can detect and skip work that doesn't belong to them.
6693
+ // @ts-ignore - Fix type
6694
+ this.webex.internal.llm.setOwnerMeetingId?.(this.id);
6386
6695
  // @ts-ignore - Fix type
6387
6696
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
6388
6697
  // @ts-ignore - Fix type
@@ -7423,6 +7732,33 @@ export default class Meeting extends StatelessWebexPlugin {
7423
7732
  }
7424
7733
  }
7425
7734
  });
7735
+ this.statsAnalyzer.on(StatsAnalyzerEventNames.STATS_UPDATE, (data) => {
7736
+ // Extract srtpCipher from transport stats
7737
+ let srtpCipher: string | undefined;
7738
+ for (const stats of data.stats.values()) {
7739
+ if (stats.type === 'transport' && stats.srtpCipher) {
7740
+ srtpCipher = stats.srtpCipher as string;
7741
+ break;
7742
+ }
7743
+ }
7744
+
7745
+ // Only emit event if srtpCipher has changed
7746
+ if (srtpCipher && srtpCipher !== this.mediaProperties.srtpCipher) {
7747
+ LoggerProxy.logger.info(
7748
+ `Meeting:index#setupStatsAnalyzerEventHandlers --> SRTP cipher changed from ${this.mediaProperties.srtpCipher} to ${srtpCipher}`
7749
+ );
7750
+ this.mediaProperties.srtpCipher = srtpCipher;
7751
+ Trigger.trigger(
7752
+ this,
7753
+ {
7754
+ file: 'meeting/index',
7755
+ function: 'setupStatsAnalyzerEventHandlers',
7756
+ },
7757
+ EVENT_TRIGGERS.MEETING_SRTP_CIPHER_UPDATED,
7758
+ {srtpCipher}
7759
+ );
7760
+ }
7761
+ });
7426
7762
  };
7427
7763
 
7428
7764
  getMediaConnectionDebugId() {
@@ -7469,6 +7805,8 @@ export default class Meeting extends StatelessWebexPlugin {
7469
7805
  disableAudioMainDtx: this.config.experimental.disableAudioMainDtx,
7470
7806
  // @ts-ignore - config coming from registerPlugin
7471
7807
  enableAudioTwcc: this.config.enableAudioTwccForMultistream,
7808
+ // @ts-ignore - config coming from registerPlugin
7809
+ enableAv1SlidesSupport: this.config.enableAv1SlidesSupport,
7472
7810
  stopIceGatheringAfterFirstRelayCandidate:
7473
7811
  // @ts-ignore - config coming from registerPlugin
7474
7812
  this.config.stopIceGatheringAfterFirstRelayCandidate,
@@ -8716,6 +9054,8 @@ export default class Meeting extends StatelessWebexPlugin {
8716
9054
  });
8717
9055
  LoggerProxy.logger.log('Meeting:index#leave --> Leaving a meeting');
8718
9056
 
9057
+ this.stopListeningForMeetingEvents();
9058
+
8719
9059
  return MeetingUtil.leaveMeeting(this, options)
8720
9060
  .then(async (leave) => {
8721
9061
  // CA team recommends submitting this *after* locus /leave
@@ -9580,6 +9920,8 @@ export default class Meeting extends StatelessWebexPlugin {
9580
9920
  locus_id: this.locusId,
9581
9921
  });
9582
9922
 
9923
+ this.stopListeningForMeetingEvents();
9924
+
9583
9925
  return MeetingUtil.endMeetingForAll(this)
9584
9926
  .then(async (end) => {
9585
9927
  this.meetingFiniteStateMachine.end();
@@ -9641,12 +9983,28 @@ export default class Meeting extends StatelessWebexPlugin {
9641
9983
  }
9642
9984
  this.queuedMediaUpdates = [];
9643
9985
 
9644
- this.stopTranscription();
9645
- this.transcription = undefined;
9986
+ // Listener teardown (transcription, annotation, llm/mercury) runs in
9987
+ // stopListeningForMeetingEvents() before /leave and /end so events
9988
+ // received mid-teardown do not trigger Locus syncs. Calling it here
9989
+ // again would double-emit MEETING_STOPPED_RECEIVING_TRANSCRIPTION
9990
+ // because stopTranscription() always fires its trigger.
9991
+ //
9992
+ // Ownership-aware token clear: only clear the shared LLM data channel
9993
+ // tokens when this meeting owns (or no meeting owns) the default LLM
9994
+ // session. Otherwise we would wipe tokens still in use by another
9995
+ // meeting's active LLM connection.
9996
+ // @ts-ignore - Fix type
9997
+ const currentOwner = this.webex.internal.llm.getOwnerMeetingId();
9998
+ const isOwner = !currentOwner || currentOwner === this.id;
9646
9999
 
9647
- this.annotation.deregisterEvents();
10000
+ if (isOwner) {
10001
+ this.clearDataChannelToken();
10002
+ } else {
10003
+ LoggerProxy.logger.info(
10004
+ `Meeting:index#clearMeetingData --> skipping clearDataChannelToken; LLM owned by meeting ${currentOwner}, not ${this.id}`
10005
+ );
10006
+ }
9648
10007
 
9649
- this.clearDataChannelToken();
9650
10008
  await this.cleanupLLMConneciton({throwOnError: false});
9651
10009
  };
9652
10010
 
@@ -9723,15 +10081,12 @@ export default class Meeting extends StatelessWebexPlugin {
9723
10081
  * @public
9724
10082
  * @memberof Meeting
9725
10083
  */
9726
- public sendReaction(reactionType: ReactionServerType, skinToneType?: SkinToneType) {
10084
+ public sendReaction(reactionType: string, skinToneType?: SkinToneType) {
9727
10085
  const reactionChannelUrl = this.locusInfo?.controls?.reactions?.reactionChannelUrl as string;
9728
10086
  const participantId = this.members.selfId;
9729
10087
 
9730
- const reactionData = Reactions[reactionType];
10088
+ const reactionData = Reactions[reactionType] || {type: reactionType};
9731
10089
 
9732
- if (!reactionData) {
9733
- return Promise.reject(new Error(`${reactionType} is not a valid reaction.`));
9734
- }
9735
10090
  const skinToneData = SkinTones[skinToneType] || SkinTones.normal;
9736
10091
  const reaction: Reaction = {
9737
10092
  ...reactionData,