@webex/plugin-meetings 3.12.0-next.1 → 3.12.0-next.11

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 (48) hide show
  1. package/dist/aiEnableRequest/index.js +1 -1
  2. package/dist/breakouts/breakout.js +1 -1
  3. package/dist/breakouts/index.js +1 -1
  4. package/dist/hashTree/constants.js +10 -1
  5. package/dist/hashTree/constants.js.map +1 -1
  6. package/dist/hashTree/hashTreeParser.js +56 -31
  7. package/dist/hashTree/hashTreeParser.js.map +1 -1
  8. package/dist/hashTree/utils.js +22 -0
  9. package/dist/hashTree/utils.js.map +1 -1
  10. package/dist/interpretation/index.js +1 -1
  11. package/dist/interpretation/siLanguage.js +1 -1
  12. package/dist/locus-info/index.js +38 -14
  13. package/dist/locus-info/index.js.map +1 -1
  14. package/dist/meeting/index.js +427 -323
  15. package/dist/meeting/index.js.map +1 -1
  16. package/dist/meeting/util.js +1 -0
  17. package/dist/meeting/util.js.map +1 -1
  18. package/dist/metrics/constants.js +5 -1
  19. package/dist/metrics/constants.js.map +1 -1
  20. package/dist/multistream/sendSlotManager.js +116 -2
  21. package/dist/multistream/sendSlotManager.js.map +1 -1
  22. package/dist/types/hashTree/constants.d.ts +1 -0
  23. package/dist/types/hashTree/hashTreeParser.d.ts +12 -2
  24. package/dist/types/hashTree/utils.d.ts +11 -0
  25. package/dist/types/locus-info/index.d.ts +8 -3
  26. package/dist/types/meeting/index.d.ts +24 -1
  27. package/dist/types/metrics/constants.d.ts +4 -0
  28. package/dist/types/multistream/sendSlotManager.d.ts +23 -1
  29. package/dist/webinar/index.js +325 -220
  30. package/dist/webinar/index.js.map +1 -1
  31. package/package.json +15 -15
  32. package/src/hashTree/constants.ts +9 -0
  33. package/src/hashTree/hashTreeParser.ts +60 -36
  34. package/src/hashTree/utils.ts +17 -0
  35. package/src/locus-info/index.ts +48 -24
  36. package/src/meeting/index.ts +165 -57
  37. package/src/meeting/util.ts +1 -0
  38. package/src/metrics/constants.ts +5 -0
  39. package/src/multistream/sendSlotManager.ts +97 -3
  40. package/src/webinar/index.ts +120 -18
  41. package/test/unit/spec/hashTree/hashTreeParser.ts +295 -30
  42. package/test/unit/spec/hashTree/utils.ts +88 -1
  43. package/test/unit/spec/locus-info/index.js +47 -22
  44. package/test/unit/spec/meeting/index.js +179 -48
  45. package/test/unit/spec/meeting/utils.js +4 -0
  46. package/test/unit/spec/meetings/index.js +3 -3
  47. package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
  48. package/test/unit/spec/webinar/index.ts +193 -8
@@ -34,6 +34,7 @@ import BEHAVIORAL_METRICS from '../metrics/constants';
34
34
  import HashTreeParser, {
35
35
  DataSet,
36
36
  HashTreeMessage,
37
+ LocusInfoUpdate,
37
38
  LocusInfoUpdateType,
38
39
  Metadata,
39
40
  } from '../hashTree/hashTreeParser';
@@ -545,7 +546,7 @@ export default class LocusInfo extends EventsScope {
545
546
  dataSets: Array<DataSet>;
546
547
  locus: any;
547
548
  };
548
- metadata: Metadata;
549
+ metadata: Metadata | null;
549
550
  replacedAt?: string;
550
551
  }): HashTreeParser {
551
552
  const parser = new HashTreeParser({
@@ -553,7 +554,7 @@ export default class LocusInfo extends EventsScope {
553
554
  metadata,
554
555
  webexRequest: this.webex.request.bind(this.webex),
555
556
  locusInfoUpdateCallback: this.updateFromHashTree.bind(this, locusUrl),
556
- debugId: `HT-${locusUrl.split('/').pop().substring(0, 4)}`,
557
+ debugId: `HT-${locusUrl.split('/')?.pop()?.substring(0, 4)}`,
557
558
  excludedDataSets: this.webex.config.meetings.locus?.excludedDataSets,
558
559
  });
559
560
 
@@ -656,7 +657,7 @@ export default class LocusInfo extends EventsScope {
656
657
  );
657
658
  // first create the HashTreeParser, but don't initialize it with any data yet
658
659
  const hashTreeParser = this.createHashTreeParser({
659
- locusUrl: data.locus.url,
660
+ locusUrl: data.locus.url as string,
660
661
  initialLocus: {
661
662
  locus: null,
662
663
  dataSets: [], // empty, because we don't have them yet
@@ -965,7 +966,7 @@ export default class LocusInfo extends EventsScope {
965
966
  // but it's buried inside the message, we need to find it and pass it to HashTreeParser constructor
966
967
  const metadata = message.locusStateElements?.find((el) => isMetadata(el));
967
968
 
968
- if (metadata?.data?.visibleDataSets?.length > 0) {
969
+ if (metadata && metadata.data?.visibleDataSets?.length > 0) {
969
970
  LoggerProxy.logger.info(
970
971
  `Locus-info:index#handleHashTreeParserSwitch --> no hash tree parser found for locusUrl ${message.locusUrl}, creating a new one`
971
972
  );
@@ -1056,7 +1057,10 @@ export default class LocusInfo extends EventsScope {
1056
1057
 
1057
1058
  const entry = this.hashTreeParsers.get(message.locusUrl);
1058
1059
 
1059
- entry.parser.handleMessage(message);
1060
+ // the check is just for typescript, the case of no entry in hashTreeParsers is handled in handleHashTreeParserSwitch() above
1061
+ if (entry) {
1062
+ entry.parser.handleMessage(message);
1063
+ }
1060
1064
  }
1061
1065
 
1062
1066
  /**
@@ -1064,16 +1068,11 @@ export default class LocusInfo extends EventsScope {
1064
1068
  * Updates our locus info based on the data parsed by the hash tree parser.
1065
1069
  *
1066
1070
  * @param {string} locusUrl - the locus URL for which the update is received
1067
- * @param {LocusInfoUpdateType} updateType - The type of update received.
1068
- * @param {Object} [data] - Additional data for the update, if applicable.
1071
+ * @param {LocusInfoUpdate} update - Details about the update.
1069
1072
  * @returns {void}
1070
1073
  */
1071
- private updateFromHashTree(
1072
- locusUrl: string,
1073
- updateType: LocusInfoUpdateType,
1074
- data?: {updatedObjects: HashTreeObject[]}
1075
- ) {
1076
- switch (updateType) {
1074
+ private updateFromHashTree(locusUrl: string, update: LocusInfoUpdate) {
1075
+ switch (update.updateType) {
1077
1076
  case LocusInfoUpdateType.OBJECTS_UPDATED: {
1078
1077
  // initialize our new locus
1079
1078
  let locus: LocusDTO = {
@@ -1087,7 +1086,7 @@ export default class LocusInfo extends EventsScope {
1087
1086
  // first go over all the updates and check what happens with the main locus object
1088
1087
  let locusObjectStateAfterUpdates: LocusObjectStateAfterUpdates =
1089
1088
  LocusObjectStateAfterUpdates.unchanged;
1090
- data.updatedObjects.forEach((object) => {
1089
+ update.updatedObjects.forEach((object) => {
1091
1090
  if (object.htMeta.elementId.type.toLowerCase() === ObjectType.locus) {
1092
1091
  if (locusObjectStateAfterUpdates === LocusObjectStateAfterUpdates.updated) {
1093
1092
  // this code doesn't supported it right now,
@@ -1116,6 +1115,14 @@ export default class LocusInfo extends EventsScope {
1116
1115
 
1117
1116
  const hashTreeParserEntry = this.hashTreeParsers.get(locusUrl);
1118
1117
 
1118
+ if (!hashTreeParserEntry) {
1119
+ LoggerProxy.logger.warn(
1120
+ `Locus-info:index#updateFromHashTree --> no HashTreeParser found for locusUrl ${locusUrl} when trying to apply updates from hash tree`
1121
+ );
1122
+
1123
+ return;
1124
+ }
1125
+
1119
1126
  if (!hashTreeParserEntry.initializedFromHashTree) {
1120
1127
  // this is the first time we're getting an update for this locusUrl,
1121
1128
  // so it's probably a move to/from breakout. We need to start from a clean state,
@@ -1124,7 +1131,8 @@ export default class LocusInfo extends EventsScope {
1124
1131
  `Locus-info:index#updateFromHashTree --> first INITIAL update for locusUrl ${locusUrl}, starting from empty state`
1125
1132
  );
1126
1133
  hashTreeParserEntry.initializedFromHashTree = true;
1127
- locus.jsSdkMeta.forceReplaceMembers = true;
1134
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1135
+ locus.jsSdkMeta!.forceReplaceMembers = true;
1128
1136
  } else if (
1129
1137
  // if Locus object is unchanged or removed, we need to keep using the existing locus
1130
1138
  // because the rest of the locusInfo code expects locus to always be present (with at least some of the fields)
@@ -1137,7 +1145,7 @@ export default class LocusInfo extends EventsScope {
1137
1145
  // copy over all of existing locus except participants
1138
1146
  LocusDtoTopLevelKeys.forEach((key) => {
1139
1147
  if (key !== 'participants') {
1140
- locus[key] = cloneDeep(this[key]);
1148
+ (locus as Record<string, any>)[key] = cloneDeep((this as Record<string, any>)[key]);
1141
1149
  }
1142
1150
  });
1143
1151
  } else {
@@ -1145,14 +1153,16 @@ export default class LocusInfo extends EventsScope {
1145
1153
  // (except participants, which need to stay empty - that means "no participant changes")
1146
1154
  Object.values(ObjectTypeToLocusKeyMap).forEach((locusDtoKey) => {
1147
1155
  if (locusDtoKey !== 'participants') {
1148
- locus[locusDtoKey] = cloneDeep(this[locusDtoKey]);
1156
+ (locus as Record<string, any>)[locusDtoKey] = cloneDeep(
1157
+ (this as Record<string, any>)[locusDtoKey]
1158
+ );
1149
1159
  }
1150
1160
  });
1151
1161
  }
1152
1162
 
1153
1163
  LoggerProxy.logger.info(
1154
1164
  `Locus-info:index#updateFromHashTree --> LOCUS object is ${locusObjectStateAfterUpdates}, all updates: ${JSON.stringify(
1155
- data.updatedObjects.map((o) => ({
1165
+ update.updatedObjects.map((o) => ({
1156
1166
  type: o.htMeta.elementId.type,
1157
1167
  id: o.htMeta.elementId.id,
1158
1168
  hasData: !!o.data,
@@ -1160,7 +1170,7 @@ export default class LocusInfo extends EventsScope {
1160
1170
  )}`
1161
1171
  );
1162
1172
  // now apply all the updates from the hash tree onto the locus
1163
- data.updatedObjects.forEach((object) => {
1173
+ update.updatedObjects.forEach((object) => {
1164
1174
  locus = this.updateLocusFromHashTreeObject(object, locus);
1165
1175
  });
1166
1176
 
@@ -1260,16 +1270,16 @@ export default class LocusInfo extends EventsScope {
1260
1270
  * @param {string} debugText string explaining the trigger for this call, added to logs for debugging purposes
1261
1271
  * @param {object} locus locus object
1262
1272
  * @param {object} metadata locus hash trees metadata
1263
- * @param {string} eventType locus event
1264
1273
  * @param {DataSet[]} dataSets
1274
+ * @param {string} eventType locus event
1265
1275
  * @returns {void}
1266
1276
  */
1267
1277
  private onFullLocusWithHashTrees(
1268
1278
  debugText: string,
1269
1279
  locus: any,
1270
1280
  metadata: Metadata,
1271
- eventType?: string,
1272
- dataSets?: Array<DataSet>
1281
+ dataSets: Array<DataSet>,
1282
+ eventType?: string
1273
1283
  ) {
1274
1284
  if (!this.hashTreeParsers.has(locus.url)) {
1275
1285
  LoggerProxy.logger.info(
@@ -1289,7 +1299,8 @@ export default class LocusInfo extends EventsScope {
1289
1299
  metadata,
1290
1300
  });
1291
1301
  // we have a full locus to start with, so we consider Locus info to be "initialized"
1292
- this.hashTreeParsers.get(locus.url).initializedFromHashTree = true;
1302
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1303
+ this.hashTreeParsers.get(locus.url)!.initializedFromHashTree = true;
1293
1304
  this.onFullLocusCommon(locus, eventType);
1294
1305
  } else {
1295
1306
  // in this case the Locus we're getting is not necessarily the full one
@@ -1351,7 +1362,7 @@ export default class LocusInfo extends EventsScope {
1351
1362
  );
1352
1363
  }
1353
1364
  // this is the new hashmap Locus DTO format (only applicable to webinars for now)
1354
- this.onFullLocusWithHashTrees(debugText, locus, metadata, eventType, dataSets);
1365
+ this.onFullLocusWithHashTrees(debugText, locus, metadata, dataSets, eventType);
1355
1366
  } else {
1356
1367
  this.onFullLocusClassic(debugText, locus, eventType);
1357
1368
  }
@@ -2859,4 +2870,17 @@ export default class LocusInfo extends EventsScope {
2859
2870
  clearMainSessionLocusCache() {
2860
2871
  this.mainSessionLocusCache = null;
2861
2872
  }
2873
+
2874
+ /**
2875
+ * Cleans up all hash tree parsers and clears internal maps.
2876
+ * @returns {void}
2877
+ * @memberof LocusInfo
2878
+ */
2879
+ cleanUp() {
2880
+ this.hashTreeParsers.forEach((entry) => {
2881
+ entry.parser.cleanUp();
2882
+ });
2883
+ this.hashTreeParsers.clear();
2884
+ this.hashTreeObjectId2ParticipantId.clear();
2885
+ }
2862
2886
  }
@@ -22,6 +22,7 @@ import {
22
22
  MediaConnectionEventNames,
23
23
  MediaContent,
24
24
  MediaType,
25
+ MediaCodecMimeType,
25
26
  RemoteTrackType,
26
27
  RoapMessage,
27
28
  StatsAnalyzer,
@@ -3733,7 +3734,7 @@ export default class Meeting extends StatelessWebexPlugin {
3733
3734
  });
3734
3735
  this.updateLLMConnection();
3735
3736
  });
3736
- this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, async (payload) => {
3737
+ this.locusInfo.on(LOCUSINFO.EVENTS.SELF_ADMITTED_GUEST, (payload) => {
3737
3738
  this.stopKeepAlive();
3738
3739
 
3739
3740
  if (payload) {
@@ -3759,6 +3760,15 @@ export default class Meeting extends StatelessWebexPlugin {
3759
3760
  });
3760
3761
  }
3761
3762
  this.rtcMetrics?.sendNextMetrics();
3763
+
3764
+ this.ensureDefaultDatachannelTokenAfterAdmit().catch((error) => {
3765
+ LoggerProxy.logger.warn(
3766
+ `Meeting:index#setUpLocusInfoSelfListener --> failed post-admit token prefetch flow: ${
3767
+ error?.message || String(error)
3768
+ }`
3769
+ );
3770
+ });
3771
+
3762
3772
  this.updateLLMConnection();
3763
3773
  });
3764
3774
 
@@ -5907,37 +5917,35 @@ export default class Meeting extends StatelessWebexPlugin {
5907
5917
  * @returns {void}
5908
5918
  */
5909
5919
  stopTranscription() {
5910
- if (this.transcription) {
5911
- // @ts-ignore
5912
- this.webex.internal.voicea.off(
5913
- VOICEAEVENTS.VOICEA_ANNOUNCEMENT,
5914
- this.voiceaListenerCallbacks[VOICEAEVENTS.VOICEA_ANNOUNCEMENT]
5915
- );
5920
+ // @ts-ignore
5921
+ this.webex.internal.voicea.off(
5922
+ VOICEAEVENTS.VOICEA_ANNOUNCEMENT,
5923
+ this.voiceaListenerCallbacks[VOICEAEVENTS.VOICEA_ANNOUNCEMENT]
5924
+ );
5916
5925
 
5917
- // @ts-ignore
5918
- this.webex.internal.voicea.off(
5919
- VOICEAEVENTS.CAPTIONS_TURNED_ON,
5920
- this.voiceaListenerCallbacks[VOICEAEVENTS.CAPTIONS_TURNED_ON]
5921
- );
5926
+ // @ts-ignore
5927
+ this.webex.internal.voicea.off(
5928
+ VOICEAEVENTS.CAPTIONS_TURNED_ON,
5929
+ this.voiceaListenerCallbacks[VOICEAEVENTS.CAPTIONS_TURNED_ON]
5930
+ );
5922
5931
 
5923
- // @ts-ignore
5924
- this.webex.internal.voicea.off(
5925
- VOICEAEVENTS.EVA_COMMAND,
5926
- this.voiceaListenerCallbacks[VOICEAEVENTS.EVA_COMMAND]
5927
- );
5932
+ // @ts-ignore
5933
+ this.webex.internal.voicea.off(
5934
+ VOICEAEVENTS.EVA_COMMAND,
5935
+ this.voiceaListenerCallbacks[VOICEAEVENTS.EVA_COMMAND]
5936
+ );
5928
5937
 
5929
- // @ts-ignore
5930
- this.webex.internal.voicea.off(
5931
- VOICEAEVENTS.NEW_CAPTION,
5932
- this.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]
5933
- );
5938
+ // @ts-ignore
5939
+ this.webex.internal.voicea.off(
5940
+ VOICEAEVENTS.NEW_CAPTION,
5941
+ this.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]
5942
+ );
5934
5943
 
5935
- // @ts-ignore
5936
- this.webex.internal.voicea.deregisterEvents();
5944
+ // @ts-ignore
5945
+ this.webex.internal.voicea.deregisterEvents();
5937
5946
 
5938
- this.areVoiceaEventsSetup = false;
5939
- this.triggerStopReceivingTranscriptionEvent();
5940
- }
5947
+ this.areVoiceaEventsSetup = false;
5948
+ this.triggerStopReceivingTranscriptionEvent();
5941
5949
  }
5942
5950
 
5943
5951
  /**
@@ -5961,6 +5969,30 @@ export default class Meeting extends StatelessWebexPlugin {
5961
5969
  );
5962
5970
  }
5963
5971
 
5972
+ /**
5973
+ * Restores LLM subchannel subscriptions after reconnect when captions are active.
5974
+ * @returns {void}
5975
+ */
5976
+ private restoreLLMSubscriptionsIfNeeded(): void {
5977
+ try {
5978
+ // @ts-ignore
5979
+ const isCaptionBoxOn = this.webex.internal.voicea?.getIsCaptionBoxOn?.();
5980
+
5981
+ if (!isCaptionBoxOn) {
5982
+ return;
5983
+ }
5984
+
5985
+ // @ts-ignore
5986
+ this.webex.internal.voicea.updateSubchannelSubscriptions({subscribe: ['transcription']});
5987
+ } catch (error) {
5988
+ const msg = error?.message || String(error);
5989
+
5990
+ LoggerProxy.logger.warn(
5991
+ `Meeting:index#restoreLLMSubscriptionsIfNeeded --> failed to restore subscriptions after LLM online: ${msg}`
5992
+ );
5993
+ }
5994
+ }
5995
+
5964
5996
  /**
5965
5997
  * This is a callback for the LLM event that is triggered when it comes online
5966
5998
  * This method in turn will trigger an event to the developers that the LLM is connected
@@ -5969,8 +6001,8 @@ export default class Meeting extends StatelessWebexPlugin {
5969
6001
  * @returns {null}
5970
6002
  */
5971
6003
  private handleLLMOnline = (): void => {
5972
- // @ts-ignore
5973
- this.webex.internal.llm.off('online', this.handleLLMOnline);
6004
+ this.restoreLLMSubscriptionsIfNeeded();
6005
+
5974
6006
  Trigger.trigger(
5975
6007
  this,
5976
6008
  {
@@ -6198,8 +6230,11 @@ export default class Meeting extends StatelessWebexPlugin {
6198
6230
  return Promise.reject(error);
6199
6231
  })
6200
6232
  .then((join) => {
6233
+ this.saveDataChannelToken(join);
6201
6234
  // @ts-ignore - config coming from registerPlugin
6202
6235
  if (this.config.enableAutomaticLLM) {
6236
+ // @ts-ignore
6237
+ this.webex.internal.llm.off('online', this.handleLLMOnline);
6203
6238
  // @ts-ignore
6204
6239
  this.webex.internal.llm.on('online', this.handleLLMOnline);
6205
6240
  this.updateLLMConnection()
@@ -6309,33 +6344,102 @@ export default class Meeting extends StatelessWebexPlugin {
6309
6344
  }
6310
6345
  };
6311
6346
 
6347
+ /**
6348
+ * Clears all data channel tokens stored in LLM.
6349
+ * Called during meeting cleanup to ensure stale tokens are not reused.
6350
+ * @returns {void}
6351
+ */
6352
+ clearDataChannelToken(): void {
6353
+ // @ts-ignore
6354
+ this.webex.internal.llm.resetDatachannelTokens();
6355
+ }
6356
+
6357
+ /**
6358
+ * Saves the data channel tokens from the join response into LLM so that
6359
+ * updateLLMConnection / updatePSDataChannel don't need to fetch them from locusInfo.
6360
+ * @param {Object} join - The parsed join response (from MeetingUtil.parseLocusJoin)
6361
+ * @returns {void}
6362
+ */
6363
+ saveDataChannelToken(join: any): void {
6364
+ const datachannelToken = join?.locus?.self?.datachannelToken;
6365
+ const practiceSessionDatachannelToken = join?.locus?.self?.practiceSessionDatachannelToken;
6366
+
6367
+ if (datachannelToken) {
6368
+ // @ts-ignore
6369
+ this.webex.internal.llm.setDatachannelToken(datachannelToken, DataChannelTokenType.Default);
6370
+ }
6371
+
6372
+ if (practiceSessionDatachannelToken) {
6373
+ // @ts-ignore
6374
+ this.webex.internal.llm.setDatachannelToken(
6375
+ practiceSessionDatachannelToken,
6376
+ DataChannelTokenType.PracticeSession
6377
+ );
6378
+ }
6379
+ }
6380
+
6381
+ /**
6382
+ * Ensures default-session data channel token exists after lobby admission.
6383
+ * Some lobby users do not receive a token until they are admitted.
6384
+ * @returns {Promise<boolean>} true when a new token is fetched and cached
6385
+ */
6386
+ private async ensureDefaultDatachannelTokenAfterAdmit(): Promise<boolean> {
6387
+ try {
6388
+ // @ts-ignore
6389
+ const datachannelToken = this.webex.internal.llm.getDatachannelToken();
6390
+ // @ts-ignore
6391
+ const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
6392
+
6393
+ if (!isDataChannelTokenEnabled || datachannelToken) {
6394
+ return false;
6395
+ }
6396
+
6397
+ const response = await this.meetingRequest.fetchDatachannelToken({
6398
+ locusUrl: this.locusUrl,
6399
+ requestingParticipantId: this.members.selfId,
6400
+ isPracticeSession: false,
6401
+ });
6402
+ const fetchedDatachannelToken = response?.body?.datachannelToken;
6403
+
6404
+ if (!fetchedDatachannelToken) {
6405
+ return false;
6406
+ }
6407
+
6408
+ // @ts-ignore
6409
+ this.webex.internal.llm.setDatachannelToken(
6410
+ fetchedDatachannelToken,
6411
+ DataChannelTokenType.Default
6412
+ );
6413
+
6414
+ return true;
6415
+ } catch (error) {
6416
+ const msg = error?.message || String(error);
6417
+
6418
+ LoggerProxy.logger.warn(
6419
+ `Meeting:index#ensureDefaultDatachannelTokenAfterAdmit --> failed to proactively fetch default data channel token after admit: ${msg}`,
6420
+ {statusCode: error?.statusCode}
6421
+ );
6422
+
6423
+ return false;
6424
+ }
6425
+ }
6426
+
6312
6427
  /**
6313
6428
  * Connects to low latency mercury and reconnects if the address has changed
6314
6429
  * It will also disconnect if called when the meeting has ended
6315
- * @param {String} datachannelUrl
6316
6430
  * @returns {Promise}
6317
6431
  */
6318
6432
  async updateLLMConnection() {
6319
6433
  // @ts-ignore - Fix type
6320
- const {
6321
- url = undefined,
6322
- info: {datachannelUrl = undefined} = {},
6323
- self: {datachannelToken = undefined} = {},
6324
- } = this.locusInfo || {};
6434
+ const {url = undefined, info: {datachannelUrl = undefined} = {}} = this.locusInfo || {};
6325
6435
 
6326
6436
  const isJoined = this.isJoined();
6327
6437
 
6328
6438
  // @ts-ignore
6329
- const currentToken = this.webex.internal.llm.getDatachannelToken(DataChannelTokenType.Default);
6330
-
6331
- const finalToken = currentToken ?? datachannelToken;
6332
-
6333
- if (!currentToken && datachannelToken) {
6334
- // @ts-ignore
6335
- this.webex.internal.llm.setDatachannelToken(datachannelToken, DataChannelTokenType.Default);
6336
- }
6439
+ const datachannelToken = this.webex.internal.llm.getDatachannelToken(
6440
+ DataChannelTokenType.Default
6441
+ );
6337
6442
 
6338
- // webinar panelist should use new data channel in practice session
6339
6443
  const dataChannelUrl = datachannelUrl;
6340
6444
 
6341
6445
  // @ts-ignore - Fix type
@@ -6358,7 +6462,7 @@ export default class Meeting extends StatelessWebexPlugin {
6358
6462
 
6359
6463
  // @ts-ignore - Fix type
6360
6464
  return this.webex.internal.llm
6361
- .registerAndConnect(url, dataChannelUrl, finalToken)
6465
+ .registerAndConnect(url, dataChannelUrl, datachannelToken)
6362
6466
  .then((registerAndConnectResult) => {
6363
6467
  // @ts-ignore - Fix type
6364
6468
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
@@ -9618,13 +9722,12 @@ export default class Meeting extends StatelessWebexPlugin {
9618
9722
  }
9619
9723
  this.queuedMediaUpdates = [];
9620
9724
 
9621
- if (this.transcription) {
9622
- this.stopTranscription();
9623
- this.transcription = undefined;
9624
- }
9725
+ this.stopTranscription();
9726
+ this.transcription = undefined;
9625
9727
 
9626
9728
  this.annotation.deregisterEvents();
9627
9729
 
9730
+ this.clearDataChannelToken();
9628
9731
  await this.cleanupLLMConneciton({throwOnError: false});
9629
9732
  };
9630
9733
 
@@ -9818,15 +9921,20 @@ export default class Meeting extends StatelessWebexPlugin {
9818
9921
  }
9819
9922
 
9820
9923
  if (shouldEnableMusicMode) {
9821
- await this.sendSlotManager.setCodecParameters(MediaType.AudioMain, {
9822
- maxaveragebitrate: '64000',
9823
- maxplaybackrate: '48000',
9824
- });
9924
+ await this.sendSlotManager.setCustomCodecParameters(
9925
+ MediaType.AudioMain,
9926
+ MediaCodecMimeType.OPUS,
9927
+ {
9928
+ maxaveragebitrate: '64000',
9929
+ maxplaybackrate: '48000',
9930
+ }
9931
+ );
9825
9932
  } else {
9826
- await this.sendSlotManager.deleteCodecParameters(MediaType.AudioMain, [
9827
- 'maxaveragebitrate',
9828
- 'maxplaybackrate',
9829
- ]);
9933
+ await this.sendSlotManager.markCustomCodecParametersForDeletion(
9934
+ MediaType.AudioMain,
9935
+ MediaCodecMimeType.OPUS,
9936
+ ['maxaveragebitrate', 'maxplaybackrate']
9937
+ );
9830
9938
  }
9831
9939
  }
9832
9940
 
@@ -371,6 +371,7 @@ const MeetingUtil = {
371
371
  meeting.breakouts.cleanUp();
372
372
  meeting.webinar.cleanUp();
373
373
  meeting.simultaneousInterpretation.cleanUp();
374
+ meeting.locusInfo.cleanUp();
374
375
  meeting.locusMediaRequest = undefined;
375
376
 
376
377
  meeting.webex?.internal?.newMetrics?.callDiagnosticMetrics?.clearEventLimitsForCorrelationId(
@@ -91,6 +91,11 @@ const BEHAVIORAL_METRICS = {
91
91
  LOCUS_CLASSIC_VS_HASH_TREE_MISMATCH: 'js_sdk_locus_classic_vs_hash_tree_mismatch',
92
92
  LOCUS_HASH_TREE_UNSUPPORTED_OPERATION: 'js_sdk_locus_hash_tree_unsupported_operation',
93
93
  MEDIA_STILL_NOT_CONNECTED: 'js_sdk_media_still_not_connected',
94
+ DEPRECATED_SET_CODEC_PARAMETERS_USED: 'js_sdk_deprecated_set_codec_parameters_used',
95
+ DEPRECATED_DELETE_CODEC_PARAMETERS_USED: 'js_sdk_deprecated_delete_codec_parameters_used',
96
+ SET_CUSTOM_CODEC_PARAMETERS_USED: 'js_sdk_set_custom_codec_parameters_used',
97
+ MARK_CUSTOM_CODEC_PARAMETERS_FOR_DELETION_USED:
98
+ 'js_sdk_mark_custom_codec_parameters_for_deletion_used',
94
99
  };
95
100
 
96
101
  export {BEHAVIORAL_METRICS as default};
@@ -5,7 +5,11 @@ import {
5
5
  MultistreamRoapMediaConnection,
6
6
  NamedMediaGroup,
7
7
  StreamState,
8
+ MediaCodecMimeType,
9
+ CodecParameters,
8
10
  } from '@webex/internal-media-core';
11
+ import Metrics from '../metrics';
12
+ import BEHAVIORAL_METRICS from '../metrics/constants';
9
13
 
10
14
  /**
11
15
  * This class is used to manage the sendSlots for the given media types.
@@ -206,6 +210,8 @@ export default class SendSlotManager {
206
210
  }
207
211
 
208
212
  /**
213
+ * @deprecated Use {@link setCustomCodecParameters} instead, which requires specifying the codec MIME type.
214
+ *
209
215
  * This method is used to set the codec parameters for the sendSlot of the given mediaType
210
216
  * @param {MediaType} mediaType MediaType of the sendSlot for which the codec parameters needs to be set (AUDIO_MAIN/VIDEO_MAIN/AUDIO_SLIDES/VIDEO_SLIDES)
211
217
  * @param {Object} codecParameters
@@ -226,12 +232,19 @@ export default class SendSlotManager {
226
232
 
227
233
  await slot.setCodecParameters(codecParameters);
228
234
 
229
- this.LoggerProxy.logger.info(
230
- `SendSlotsManager->setCodecParameters#Set codec parameters for ${mediaType} to ${codecParameters}`
235
+ this.LoggerProxy.logger.warn(
236
+ 'SendSlotsManager->setCodecParameters --> [DEPRECATION WARNING]: setCodecParameters has been deprecated, use setCustomCodecParameters instead'
231
237
  );
238
+
239
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.DEPRECATED_SET_CODEC_PARAMETERS_USED, {
240
+ mediaType,
241
+ codecParameters,
242
+ });
232
243
  }
233
244
 
234
245
  /**
246
+ * @deprecated Use {@link markCustomCodecParametersForDeletion} instead, which requires specifying the codec MIME type.
247
+ *
235
248
  * This method is used to delete the codec parameters for the sendSlot of the given mediaType
236
249
  * @param {MediaType} mediaType MediaType of the sendSlot for which the codec parameters needs to be deleted (AUDIO_MAIN/VIDEO_MAIN/AUDIO_SLIDES/VIDEO_SLIDES)
237
250
  * @param {Array<String>} parameters Array of keys of the codec parameters to be deleted
@@ -246,8 +259,89 @@ export default class SendSlotManager {
246
259
 
247
260
  await slot.deleteCodecParameters(parameters);
248
261
 
262
+ this.LoggerProxy.logger.warn(
263
+ 'SendSlotsManager->deleteCodecParameters --> [DEPRECATION WARNING]: deleteCodecParameters has been deprecated, use markCustomCodecParametersForDeletion instead'
264
+ );
265
+
266
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.DEPRECATED_DELETE_CODEC_PARAMETERS_USED, {
267
+ mediaType,
268
+ parameters,
269
+ });
270
+ }
271
+
272
+ /**
273
+ * Sets custom codec parameters for the sendSlot of the given mediaType, scoped to a specific codec MIME type.
274
+ * Delegates to WCME's setCustomCodecParameters API.
275
+ * @param {MediaType} mediaType MediaType of the sendSlot
276
+ * @param {MediaCodecMimeType} codecMimeType The codec MIME type to apply parameters to (e.g. OPUS, H264, AV1)
277
+ * @param {CodecParameters} parameters Key-value pairs of codec parameters to set
278
+ * @returns {Promise<void>}
279
+ */
280
+ public async setCustomCodecParameters(
281
+ mediaType: MediaType,
282
+ codecMimeType: MediaCodecMimeType,
283
+ parameters: CodecParameters
284
+ ): Promise<void> {
285
+ const slot = this.slots.get(mediaType);
286
+
287
+ if (!slot) {
288
+ throw new Error(`Slot for ${mediaType} does not exist`);
289
+ }
290
+
291
+ try {
292
+ await slot.setCustomCodecParameters(codecMimeType, parameters);
293
+
294
+ this.LoggerProxy.logger.info(
295
+ `SendSlotsManager->setCustomCodecParameters#Set custom codec parameters for ${mediaType} (codec: ${codecMimeType}) to ${JSON.stringify(
296
+ parameters
297
+ )}`
298
+ );
299
+ } catch (error) {
300
+ this.LoggerProxy.logger.error(
301
+ `SendSlotsManager->setCustomCodecParameters#Failed to set custom codec parameters for ${mediaType} (codec: ${codecMimeType}): ${error}`
302
+ );
303
+ throw error;
304
+ } finally {
305
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.SET_CUSTOM_CODEC_PARAMETERS_USED, {
306
+ mediaType,
307
+ codecMimeType,
308
+ parameters,
309
+ });
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Marks custom codec parameters for deletion on the sendSlot of the given mediaType, scoped to a specific codec MIME type.
315
+ * Delegates to WCME's markCustomCodecParametersForDeletion API.
316
+ * @param {MediaType} mediaType MediaType of the sendSlot
317
+ * @param {MediaCodecMimeType} codecMimeType The codec MIME type whose parameters should be deleted (e.g. OPUS, H264, AV1)
318
+ * @param {string[]} parameters Array of parameter keys to delete
319
+ * @returns {Promise<void>}
320
+ */
321
+ public async markCustomCodecParametersForDeletion(
322
+ mediaType: MediaType,
323
+ codecMimeType: MediaCodecMimeType,
324
+ parameters: string[]
325
+ ): Promise<void> {
326
+ const slot = this.slots.get(mediaType);
327
+
328
+ if (!slot) {
329
+ throw new Error(`Slot for ${mediaType} does not exist`);
330
+ }
331
+
332
+ await slot.markCustomCodecParametersForDeletion(codecMimeType, parameters);
333
+
249
334
  this.LoggerProxy.logger.info(
250
- `SendSlotsManager->deleteCodecParameters#Deleted the following codec parameters -> ${parameters} for ${mediaType}`
335
+ `SendSlotsManager->markCustomCodecParametersForDeletion#Marked codec parameters for deletion -> ${parameters} for ${mediaType} (codec: ${codecMimeType})`
336
+ );
337
+
338
+ Metrics.sendBehavioralMetric(
339
+ BEHAVIORAL_METRICS.MARK_CUSTOM_CODEC_PARAMETERS_FOR_DELETION_USED,
340
+ {
341
+ mediaType,
342
+ codecMimeType,
343
+ parameters,
344
+ }
251
345
  );
252
346
  }
253
347