@webex/plugin-meetings 3.12.0-next.3 → 3.12.0-next.31

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 (90) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +1 -1
  3. package/dist/breakouts/breakout.js +1 -1
  4. package/dist/breakouts/index.js +1 -1
  5. package/dist/constants.js +3 -1
  6. package/dist/constants.js.map +1 -1
  7. package/dist/controls-options-manager/constants.js +11 -1
  8. package/dist/controls-options-manager/constants.js.map +1 -1
  9. package/dist/controls-options-manager/index.js +23 -21
  10. package/dist/controls-options-manager/index.js.map +1 -1
  11. package/dist/controls-options-manager/util.js +91 -0
  12. package/dist/controls-options-manager/util.js.map +1 -1
  13. package/dist/hashTree/constants.js +10 -1
  14. package/dist/hashTree/constants.js.map +1 -1
  15. package/dist/hashTree/hashTreeParser.js +550 -346
  16. package/dist/hashTree/hashTreeParser.js.map +1 -1
  17. package/dist/hashTree/utils.js +22 -0
  18. package/dist/hashTree/utils.js.map +1 -1
  19. package/dist/interceptors/locusRetry.js +23 -8
  20. package/dist/interceptors/locusRetry.js.map +1 -1
  21. package/dist/interpretation/index.js +1 -1
  22. package/dist/interpretation/siLanguage.js +1 -1
  23. package/dist/locus-info/index.js +222 -61
  24. package/dist/locus-info/index.js.map +1 -1
  25. package/dist/meeting/index.js +372 -292
  26. package/dist/meeting/index.js.map +1 -1
  27. package/dist/meeting/util.js +1 -0
  28. package/dist/meeting/util.js.map +1 -1
  29. package/dist/meetings/index.js +146 -62
  30. package/dist/meetings/index.js.map +1 -1
  31. package/dist/meetings/util.js +39 -5
  32. package/dist/meetings/util.js.map +1 -1
  33. package/dist/member/index.js +10 -0
  34. package/dist/member/index.js.map +1 -1
  35. package/dist/member/types.js.map +1 -1
  36. package/dist/member/util.js +3 -0
  37. package/dist/member/util.js.map +1 -1
  38. package/dist/metrics/constants.js +5 -1
  39. package/dist/metrics/constants.js.map +1 -1
  40. package/dist/multistream/sendSlotManager.js +116 -2
  41. package/dist/multistream/sendSlotManager.js.map +1 -1
  42. package/dist/types/constants.d.ts +1 -0
  43. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  44. package/dist/types/hashTree/constants.d.ts +1 -0
  45. package/dist/types/hashTree/hashTreeParser.d.ts +53 -15
  46. package/dist/types/hashTree/utils.d.ts +11 -0
  47. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  48. package/dist/types/locus-info/index.d.ts +38 -5
  49. package/dist/types/meeting/index.d.ts +11 -0
  50. package/dist/types/member/index.d.ts +1 -0
  51. package/dist/types/member/types.d.ts +1 -0
  52. package/dist/types/member/util.d.ts +1 -0
  53. package/dist/types/metrics/constants.d.ts +4 -0
  54. package/dist/types/multistream/sendSlotManager.d.ts +23 -1
  55. package/dist/webinar/index.js +301 -226
  56. package/dist/webinar/index.js.map +1 -1
  57. package/package.json +16 -16
  58. package/src/constants.ts +1 -0
  59. package/src/controls-options-manager/constants.ts +14 -1
  60. package/src/controls-options-manager/index.ts +26 -19
  61. package/src/controls-options-manager/util.ts +81 -1
  62. package/src/hashTree/constants.ts +9 -0
  63. package/src/hashTree/hashTreeParser.ts +273 -154
  64. package/src/hashTree/utils.ts +17 -0
  65. package/src/interceptors/locusRetry.ts +25 -4
  66. package/src/locus-info/index.ts +233 -79
  67. package/src/meeting/index.ts +98 -11
  68. package/src/meeting/util.ts +1 -0
  69. package/src/meetings/index.ts +58 -34
  70. package/src/meetings/util.ts +44 -1
  71. package/src/member/index.ts +10 -0
  72. package/src/member/types.ts +1 -0
  73. package/src/member/util.ts +3 -0
  74. package/src/metrics/constants.ts +5 -0
  75. package/src/multistream/sendSlotManager.ts +97 -3
  76. package/src/webinar/index.ts +75 -1
  77. package/test/unit/spec/controls-options-manager/index.js +114 -6
  78. package/test/unit/spec/controls-options-manager/util.js +165 -0
  79. package/test/unit/spec/hashTree/hashTreeParser.ts +839 -37
  80. package/test/unit/spec/hashTree/utils.ts +88 -1
  81. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  82. package/test/unit/spec/locus-info/index.js +262 -64
  83. package/test/unit/spec/meeting/index.js +54 -36
  84. package/test/unit/spec/meeting/utils.js +4 -0
  85. package/test/unit/spec/meetings/index.js +190 -8
  86. package/test/unit/spec/meetings/utils.js +124 -0
  87. package/test/unit/spec/member/index.js +7 -0
  88. package/test/unit/spec/member/util.js +24 -0
  89. package/test/unit/spec/multistream/sendSlotManager.ts +135 -36
  90. package/test/unit/spec/webinar/index.ts +60 -0
@@ -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
 
@@ -5959,6 +5969,30 @@ export default class Meeting extends StatelessWebexPlugin {
5959
5969
  );
5960
5970
  }
5961
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
+
5962
5996
  /**
5963
5997
  * This is a callback for the LLM event that is triggered when it comes online
5964
5998
  * This method in turn will trigger an event to the developers that the LLM is connected
@@ -5967,8 +6001,8 @@ export default class Meeting extends StatelessWebexPlugin {
5967
6001
  * @returns {null}
5968
6002
  */
5969
6003
  private handleLLMOnline = (): void => {
5970
- // @ts-ignore
5971
- this.webex.internal.llm.off('online', this.handleLLMOnline);
6004
+ this.restoreLLMSubscriptionsIfNeeded();
6005
+
5972
6006
  Trigger.trigger(
5973
6007
  this,
5974
6008
  {
@@ -6199,6 +6233,8 @@ export default class Meeting extends StatelessWebexPlugin {
6199
6233
  this.saveDataChannelToken(join);
6200
6234
  // @ts-ignore - config coming from registerPlugin
6201
6235
  if (this.config.enableAutomaticLLM) {
6236
+ // @ts-ignore
6237
+ this.webex.internal.llm.off('online', this.handleLLMOnline);
6202
6238
  // @ts-ignore
6203
6239
  this.webex.internal.llm.on('online', this.handleLLMOnline);
6204
6240
  this.updateLLMConnection()
@@ -6342,6 +6378,52 @@ export default class Meeting extends StatelessWebexPlugin {
6342
6378
  }
6343
6379
  }
6344
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
+
6345
6427
  /**
6346
6428
  * Connects to low latency mercury and reconnects if the address has changed
6347
6429
  * It will also disconnect if called when the meeting has ended
@@ -9839,15 +9921,20 @@ export default class Meeting extends StatelessWebexPlugin {
9839
9921
  }
9840
9922
 
9841
9923
  if (shouldEnableMusicMode) {
9842
- await this.sendSlotManager.setCodecParameters(MediaType.AudioMain, {
9843
- maxaveragebitrate: '64000',
9844
- maxplaybackrate: '48000',
9845
- });
9924
+ await this.sendSlotManager.setCustomCodecParameters(
9925
+ MediaType.AudioMain,
9926
+ MediaCodecMimeType.OPUS,
9927
+ {
9928
+ maxaveragebitrate: '64000',
9929
+ maxplaybackrate: '48000',
9930
+ }
9931
+ );
9846
9932
  } else {
9847
- await this.sendSlotManager.deleteCodecParameters(MediaType.AudioMain, [
9848
- 'maxaveragebitrate',
9849
- 'maxplaybackrate',
9850
- ]);
9933
+ await this.sendSlotManager.markCustomCodecParametersForDeletion(
9934
+ MediaType.AudioMain,
9935
+ MediaCodecMimeType.OPUS,
9936
+ ['maxaveragebitrate', 'maxplaybackrate']
9937
+ );
9851
9938
  }
9852
9939
  }
9853
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(
@@ -69,7 +69,9 @@ import JoinForbiddenError from '../common/errors/join-forbidden-error';
69
69
  import {HashTreeMessage} from '../hashTree/hashTreeParser';
70
70
  import {HashTreeObject} from '../hashTree/types';
71
71
  import {isSelf} from '../hashTree/utils';
72
+
72
73
  import {createLocusFromHashTreeMessage, findMeetingForHashTreeMessage} from '../locus-info';
74
+ import {LocusDTO} from '../locus-info/types';
73
75
 
74
76
  let mediaLogger;
75
77
 
@@ -313,7 +315,7 @@ export default class Meetings extends WebexPlugin {
313
315
  const breakoutLocus = this.meetingCollection.getActiveBreakoutLocus(breakoutUrl);
314
316
 
315
317
  const isSelfJoined = newLocus?.self?.state === _JOINED_;
316
- const isSelfMoved = newLocus?.self?.state === _LEFT_ && newLocus?.self?.reason === _MOVED_;
318
+ const isSelfMoved = MeetingsUtil.isSelfMovedOrBreakoutEnded(newLocus);
317
319
  // @ts-ignore
318
320
  const deviceFromNewLocus = MeetingsUtil.getThisDevice(newLocus, this.webex.internal.device.url);
319
321
  const isResourceMovedOnThisDevice =
@@ -390,7 +392,7 @@ export default class Meetings extends WebexPlugin {
390
392
  private isNeedHandleLocusDTO(meeting: any, newLocus: any) {
391
393
  if (newLocus) {
392
394
  const isNewLocusAsBreakout = MeetingsUtil.isBreakoutLocusDTO(newLocus);
393
- const isSelfMoved = newLocus?.self?.state === _LEFT_ && newLocus?.self?.reason === _MOVED_;
395
+ const isSelfMoved = MeetingsUtil.isSelfMovedOrBreakoutEnded(newLocus);
394
396
  const isSelfMovedToLobby =
395
397
  newLocus?.self?.devices[0]?.intent?.reason === _ON_HOLD_LOBBY_ &&
396
398
  newLocus?.self?.devices[0]?.intent?.type === _WAIT_;
@@ -435,14 +437,11 @@ export default class Meetings extends WebexPlugin {
435
437
  if (existingMeeting) {
436
438
  return existingMeeting;
437
439
  }
438
-
439
440
  if (data.eventType === LOCUSEVENT.HASH_TREE_DATA_UPDATED) {
440
441
  // need to check if maybe this event indicates a move to/from breakout
441
442
  const meetingForHashTreeMessage = findMeetingForHashTreeMessage(
442
- data.stateElementsMessage,
443
- this.meetingCollection,
444
- // @ts-ignore
445
- this.webex.internal.device.url
443
+ data?.stateElementsMessage,
444
+ this.meetingCollection
446
445
  );
447
446
 
448
447
  if (meetingForHashTreeMessage) {
@@ -492,7 +491,6 @@ export default class Meetings extends WebexPlugin {
492
491
  */
493
492
  private handleLocusEvent(data: LocusEvent, useRandomDelayForInfo = false) {
494
493
  let meeting = this.getCorrespondingMeetingByLocus(data);
495
-
496
494
  // @ts-ignore
497
495
  if (this.config.experimental.storeLocusHashTreeEventsForDebugging) {
498
496
  storeEventForDebugging('mercury', data);
@@ -604,7 +602,6 @@ export default class Meetings extends WebexPlugin {
604
602
  // @ts-ignore
605
603
  this.destroy(meeting, MEETING_REMOVED_REASON.LOCUS_DTO_SYNC_FAILED);
606
604
  }
607
-
608
605
  this.checkHandleBreakoutLocus(data.locus);
609
606
  })
610
607
  .catch((e) => {
@@ -1811,7 +1808,11 @@ export default class Meetings extends WebexPlugin {
1811
1808
  // For type LOCUS_ID we need to parse the locus object to get the information
1812
1809
  // about the caller and callee
1813
1810
  // Meeting Added event will be created in `handleLocusEvent`
1814
- if (type !== DESTINATION_TYPE.LOCUS_ID) {
1811
+ // Only emit MEETING_ADDED if the meeting still exists in the collection.
1812
+ // If fetchMeetingInfo failed and the meeting was destroyed in the catch block,
1813
+ // skip emitting to prevent orphaned meeting references on the consumer side.
1814
+ // @ts-ignore - getMeetingByType types value as object but accepts strings (same as handleLocusEvent)
1815
+ if (type !== DESTINATION_TYPE.LOCUS_ID && this.getMeetingByType(_ID_, meeting.id)) {
1815
1816
  if (!meeting.sipUri) {
1816
1817
  meeting.setSipUri(destination);
1817
1818
  }
@@ -1886,23 +1887,20 @@ export default class Meetings extends WebexPlugin {
1886
1887
  * @public
1887
1888
  * @memberof Meetings
1888
1889
  */
1889
- public syncMeetings({keepOnlyLocusMeetings = true} = {}): Promise<void> {
1890
+ public async syncMeetings({keepOnlyLocusMeetings = true} = {}): Promise<void> {
1890
1891
  // @ts-ignore
1891
1892
  if (this.webex.credentials.isUnverifiedGuest) {
1892
1893
  LoggerProxy.logger.info(
1893
- 'Meetings:index#syncMeetings --> skipping meeting sync as unverified guest'
1894
+ 'Meetings:index#syncMeetings --> user is unverified guest, skipping calling Locus for meeting sync'
1894
1895
  );
1895
-
1896
- return Promise.resolve();
1897
- }
1898
-
1899
- return this.request
1900
- .getActiveMeetings()
1901
- .then((locusArray) => {
1902
- const activeLocusUrl = [];
1896
+ } else {
1897
+ try {
1898
+ const locusArray = await this.request.getActiveMeetings();
1899
+ const activeLocusUrl: string[] = [];
1903
1900
 
1904
1901
  if (locusArray?.loci && locusArray.loci.length > 0) {
1905
1902
  const lociToUpdate = this.sortLocusArrayToUpdate(locusArray.loci);
1903
+
1906
1904
  lociToUpdate.forEach((locus) => {
1907
1905
  activeLocusUrl.push(locus.url);
1908
1906
  this.handleLocusEvent({
@@ -1920,21 +1918,48 @@ export default class Meetings extends WebexPlugin {
1920
1918
  // (they had a locusUrl previously but are no longer active) in the sync
1921
1919
  for (const meeting of Object.values(meetingsCollection)) {
1922
1920
  // @ts-ignore
1923
- const {locusUrl} = meeting;
1921
+ const {locusUrl, locusInfo} = meeting;
1924
1922
  if ((keepOnlyLocusMeetings || locusUrl) && !activeLocusUrl.includes(locusUrl)) {
1925
- // destroy function also uploads logs
1926
- // @ts-ignore
1927
- this.destroy(meeting, MEETING_REMOVED_REASON.NO_MEETINGS_TO_SYNC);
1923
+ const globalMeetingId = locusInfo?.info?.globalMeetingId;
1924
+
1925
+ if (
1926
+ globalMeetingId &&
1927
+ locusArray?.loci?.some(
1928
+ (locus: LocusDTO) => locus.info?.globalMeetingId === globalMeetingId
1929
+ )
1930
+ ) {
1931
+ // don't destroy the meeting as Locus API still returned some Locus that shares
1932
+ // the same globalMeetingId - that happens for example if a webinar user (who hasn't scheduled it)
1933
+ // is in a breakout and gets moved to a different breakout while we were offline
1934
+ } else {
1935
+ // destroy function also uploads logs
1936
+ // @ts-ignore
1937
+ this.destroy(meeting, MEETING_REMOVED_REASON.NO_MEETINGS_TO_SYNC);
1938
+ }
1928
1939
  }
1929
1940
  }
1930
1941
  }
1931
- })
1932
- .catch((error) => {
1942
+ } catch (error) {
1933
1943
  LoggerProxy.logger.error(
1934
1944
  `Meetings:index#syncMeetings --> failed to sync meetings, ${error}`
1935
1945
  );
1936
- throw new Error(error);
1937
- });
1946
+ throw error;
1947
+ }
1948
+ }
1949
+
1950
+ // Trigger hash tree syncs for all remaining meetings
1951
+ const remainingMeetings = this.meetingCollection.getAll();
1952
+ const syncPromises = [];
1953
+
1954
+ for (const meeting of Object.values(remainingMeetings) as any[]) {
1955
+ if (meeting.locusInfo) {
1956
+ syncPromises.push(meeting.locusInfo.syncAllHashTreeDatasets());
1957
+ }
1958
+ }
1959
+
1960
+ if (syncPromises.length > 0) {
1961
+ await Promise.all(syncPromises);
1962
+ }
1938
1963
  }
1939
1964
 
1940
1965
  /**
@@ -1950,8 +1975,8 @@ export default class Meetings extends WebexPlugin {
1950
1975
  this.breakoutLocusForHandleLater = [];
1951
1976
  const lociToUpdate = [...mainLoci];
1952
1977
  breakoutLoci.forEach((breakoutLocus) => {
1953
- const associateMainLocus = mainLoci.find(
1954
- (mainLocus) => mainLocus.controls?.breakout?.url === breakoutLocus.controls?.breakout?.url
1978
+ const associateMainLocus = mainLoci.find((mainLocus) =>
1979
+ MeetingsUtil.isMainAssociatedWithBreakout(mainLocus, breakoutLocus)
1955
1980
  );
1956
1981
  const existCorrespondingMeeting = this.getCorrespondingMeetingByLocus({
1957
1982
  eventType: LOCUSEVENT.SDK_NO_EVENT,
@@ -1979,7 +2004,7 @@ export default class Meetings extends WebexPlugin {
1979
2004
  * @public
1980
2005
  * @memberof Meetings
1981
2006
  */
1982
- checkHandleBreakoutLocus(newCreatedLocus) {
2007
+ checkHandleBreakoutLocus(newCreatedLocus: any) {
1983
2008
  if (
1984
2009
  !newCreatedLocus ||
1985
2010
  !this.breakoutLocusForHandleLater ||
@@ -1990,9 +2015,8 @@ export default class Meetings extends WebexPlugin {
1990
2015
  if (MeetingsUtil.isBreakoutLocusDTO(newCreatedLocus)) {
1991
2016
  return;
1992
2017
  }
1993
- const existIndex = this.breakoutLocusForHandleLater.findIndex(
1994
- (breakoutLocus) =>
1995
- breakoutLocus.controls?.breakout?.url === newCreatedLocus.controls?.breakout?.url
2018
+ const existIndex = this.breakoutLocusForHandleLater.findIndex((breakoutLocus: any) =>
2019
+ MeetingsUtil.isMainAssociatedWithBreakout(newCreatedLocus, breakoutLocus)
1996
2020
  );
1997
2021
 
1998
2022
  if (existIndex < 0) {
@@ -7,6 +7,7 @@ import {
7
7
  _LEFT_,
8
8
  DESTINATION_TYPE,
9
9
  _MOVED_,
10
+ _BREAKOUT_ENDED_,
10
11
  BREAKOUTS,
11
12
  EVENT_TRIGGERS,
12
13
  LOCUS,
@@ -266,6 +267,23 @@ MeetingsUtil.getThisDevice = (newLocus: any, deviceUrl: string) => {
266
267
  return null;
267
268
  };
268
269
 
270
+ /**
271
+ * Checks if the self state in a locus indicates a breakout move or breakout end.
272
+ * Returns true when:
273
+ * - self state is LEFT with reason MOVED (regular breakout move), OR
274
+ * - fullState is INACTIVE with endMeetingReason BREAKOUT_ENDED (breakout session ended)
275
+ * @param {Object} locus locus data
276
+ * @returns {boolean}
277
+ */
278
+ MeetingsUtil.isSelfMovedOrBreakoutEnded = (locus: any): boolean => {
279
+ const isSelfLeftMoved = locus?.self?.state === _LEFT_ && locus?.self?.reason === _MOVED_;
280
+ const isBreakoutEnded =
281
+ locus?.fullState?.state === LOCUS.STATE.INACTIVE &&
282
+ locus?.fullState?.endMeetingReason === _BREAKOUT_ENDED_;
283
+
284
+ return isSelfLeftMoved || isBreakoutEnded;
285
+ };
286
+
269
287
  /**
270
288
  * get self device joined status from locus data
271
289
  * @param {Object} meeting current meeting data
@@ -294,7 +312,10 @@ MeetingsUtil.joinedOnThisDevice = (meeting: any, newLocus: any, deviceUrl: strin
294
312
  * @private
295
313
  */
296
314
  MeetingsUtil.isBreakoutLocusDTO = (newLocus: any) => {
297
- return newLocus?.controls?.breakout?.sessionType === BREAKOUTS.SESSION_TYPES.BREAKOUT;
315
+ return (
316
+ newLocus?.controls?.breakout?.sessionType === BREAKOUTS.SESSION_TYPES.BREAKOUT ||
317
+ !!newLocus?.info?.isBreakout
318
+ );
298
319
  };
299
320
 
300
321
  /**
@@ -310,4 +331,26 @@ MeetingsUtil.isValidBreakoutLocus = (locus: any) => {
310
331
 
311
332
  return isLocusAsBreakout && !inActiveStatus && selfJoined;
312
333
  };
334
+ /**
335
+ * check if the breakout locus is associated with the main locus by comparing the breakout control url or the replaces info in self device
336
+ * @param {Object} mainLocus main locus data
337
+ * @param {Object} breakoutLocus breakout locus data
338
+ * @returns {boolean}
339
+ * @private
340
+ */
341
+ MeetingsUtil.isMainAssociatedWithBreakout = (mainLocus: any, breakoutLocus: any) => {
342
+ if (
343
+ mainLocus.controls?.breakout?.url &&
344
+ mainLocus.controls?.breakout?.url === breakoutLocus.controls?.breakout?.url
345
+ ) {
346
+ return true;
347
+ }
348
+ const deviceUrl = breakoutLocus?.self?.deviceUrl;
349
+ const replaceInfo = MeetingsUtil.getThisDevice(breakoutLocus, deviceUrl)?.replaces?.[0];
350
+ if (replaceInfo?.locusUrl && replaceInfo.locusUrl === mainLocus.url) {
351
+ return true;
352
+ }
353
+
354
+ return false;
355
+ };
313
356
  export default MeetingsUtil;
@@ -27,6 +27,7 @@ export default class Member {
27
27
  isModerator: any;
28
28
  isModeratorAssignmentProhibited: any;
29
29
  isPresenterAssignmentProhibited: any;
30
+ isAttendeeAssignmentProhibited: any;
30
31
  isMutable: any;
31
32
  isNotAdmitted: any;
32
33
  isRecording: any;
@@ -292,6 +293,14 @@ export default class Member {
292
293
  */
293
294
  this.isPresenterAssignmentProhibited = null;
294
295
 
296
+ /**
297
+ * @instance
298
+ * @type {Boolean}
299
+ * @public
300
+ * @memberof Member
301
+ */
302
+ this.isAttendeeAssignmentProhibited = null;
303
+
295
304
  /**
296
305
  * @instance
297
306
  * @type {Boolean}
@@ -369,6 +378,7 @@ export default class Member {
369
378
  MemberUtil.isModeratorAssignmentProhibited(participant);
370
379
  this.isPresenterAssignmentProhibited =
371
380
  MemberUtil.isPresenterAssignmentProhibited(participant);
381
+ this.isAttendeeAssignmentProhibited = MemberUtil.isAttendeeAssignmentProhibited(participant);
372
382
  this.canApproveAIEnablement = MemberUtil.canApproveAIEnablement(participant);
373
383
  this.processStatus(participant);
374
384
  this.processRoles(participant);
@@ -103,6 +103,7 @@ export interface Participant {
103
103
  moderator: boolean; // Locus docs say this is deprecated and role control should be used instead
104
104
  moderatorAssignmentNotAllowed: boolean;
105
105
  presenterAssignmentNotAllowed: boolean;
106
+ attendeeAssignmentNotAllowed?: boolean;
106
107
  person: ParticipantPerson;
107
108
  resourceGuest: boolean;
108
109
  state: string; // probably one of MEETING_STATE.STATES
@@ -140,6 +140,9 @@ const MemberUtil = {
140
140
  isPresenterAssignmentProhibited: (participant: Participant) =>
141
141
  participant && participant.presenterAssignmentNotAllowed,
142
142
 
143
+ isAttendeeAssignmentProhibited: (participant: Participant) =>
144
+ !!(participant && participant.attendeeAssignmentNotAllowed),
145
+
143
146
  /**
144
147
  * checks to see if the participant id is the same as the passed id
145
148
  * there are multiple ids that can be used
@@ -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