@webex/plugin-meetings 3.9.0 → 3.10.0

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 (117) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +8 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/controls-options-manager/index.js +22 -5
  6. package/dist/controls-options-manager/index.js.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/interceptors/index.js +7 -0
  10. package/dist/interceptors/index.js.map +1 -1
  11. package/dist/interceptors/locusRouteToken.js +116 -0
  12. package/dist/interceptors/locusRouteToken.js.map +1 -0
  13. package/dist/interpretation/index.js +1 -1
  14. package/dist/interpretation/siLanguage.js +1 -1
  15. package/dist/locus-info/controlsUtils.js +11 -2
  16. package/dist/locus-info/controlsUtils.js.map +1 -1
  17. package/dist/locus-info/index.js +56 -14
  18. package/dist/locus-info/index.js.map +1 -1
  19. package/dist/locus-info/parser.js +4 -1
  20. package/dist/locus-info/parser.js.map +1 -1
  21. package/dist/media/properties.js +53 -5
  22. package/dist/media/properties.js.map +1 -1
  23. package/dist/meeting/in-meeting-actions.js +8 -0
  24. package/dist/meeting/in-meeting-actions.js.map +1 -1
  25. package/dist/meeting/index.js +339 -185
  26. package/dist/meeting/index.js.map +1 -1
  27. package/dist/meeting/muteState.js +2 -5
  28. package/dist/meeting/muteState.js.map +1 -1
  29. package/dist/meeting/request.js +177 -14
  30. package/dist/meeting/request.js.map +1 -1
  31. package/dist/meeting/util.js +39 -11
  32. package/dist/meeting/util.js.map +1 -1
  33. package/dist/meeting-info/meeting-info-v2.js +29 -21
  34. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  35. package/dist/meetings/index.js +31 -25
  36. package/dist/meetings/index.js.map +1 -1
  37. package/dist/member/index.js +9 -0
  38. package/dist/member/index.js.map +1 -1
  39. package/dist/member/types.js.map +1 -1
  40. package/dist/member/util.js +10 -0
  41. package/dist/member/util.js.map +1 -1
  42. package/dist/members/collection.js +13 -0
  43. package/dist/members/collection.js.map +1 -1
  44. package/dist/members/index.js +42 -20
  45. package/dist/members/index.js.map +1 -1
  46. package/dist/members/util.js +7 -2
  47. package/dist/members/util.js.map +1 -1
  48. package/dist/metrics/constants.js +2 -1
  49. package/dist/metrics/constants.js.map +1 -1
  50. package/dist/reachability/index.js +3 -3
  51. package/dist/reachability/index.js.map +1 -1
  52. package/dist/types/constants.d.ts +7 -0
  53. package/dist/types/controls-options-manager/index.d.ts +9 -1
  54. package/dist/types/interceptors/index.d.ts +2 -1
  55. package/dist/types/interceptors/locusRouteToken.d.ts +38 -0
  56. package/dist/types/locus-info/index.d.ts +56 -2
  57. package/dist/types/media/properties.d.ts +21 -0
  58. package/dist/types/meeting/in-meeting-actions.d.ts +8 -0
  59. package/dist/types/meeting/index.d.ts +41 -1
  60. package/dist/types/meeting/request.d.ts +42 -0
  61. package/dist/types/meeting/util.d.ts +13 -3
  62. package/dist/types/meeting-info/meeting-info-v2.d.ts +6 -3
  63. package/dist/types/meetings/index.d.ts +3 -1
  64. package/dist/types/member/index.d.ts +1 -0
  65. package/dist/types/member/types.d.ts +1 -0
  66. package/dist/types/member/util.d.ts +5 -0
  67. package/dist/types/members/collection.d.ts +6 -0
  68. package/dist/types/members/index.d.ts +12 -2
  69. package/dist/types/members/util.d.ts +6 -3
  70. package/dist/types/metrics/constants.d.ts +1 -0
  71. package/dist/webinar/index.js +1 -1
  72. package/package.json +23 -23
  73. package/src/constants.ts +10 -0
  74. package/src/controls-options-manager/index.ts +26 -5
  75. package/src/index.ts +2 -1
  76. package/src/interceptors/index.ts +2 -1
  77. package/src/interceptors/locusRouteToken.ts +80 -0
  78. package/src/locus-info/controlsUtils.ts +18 -0
  79. package/src/locus-info/index.ts +99 -17
  80. package/src/locus-info/parser.ts +5 -1
  81. package/src/media/properties.ts +43 -0
  82. package/src/meeting/in-meeting-actions.ts +16 -0
  83. package/src/meeting/index.ts +204 -24
  84. package/src/meeting/muteState.ts +2 -6
  85. package/src/meeting/request.ts +141 -0
  86. package/src/meeting/util.ts +50 -20
  87. package/src/meeting-info/meeting-info-v2.ts +24 -5
  88. package/src/meetings/index.ts +9 -3
  89. package/src/member/index.ts +10 -0
  90. package/src/member/types.ts +1 -0
  91. package/src/member/util.ts +14 -0
  92. package/src/members/collection.ts +11 -0
  93. package/src/members/index.ts +38 -5
  94. package/src/members/util.ts +18 -2
  95. package/src/metrics/constants.ts +1 -0
  96. package/src/reachability/index.ts +3 -3
  97. package/test/unit/spec/common/browser-detection.js +0 -24
  98. package/test/unit/spec/controls-options-manager/index.js +47 -0
  99. package/test/unit/spec/fixture/locus.js +1 -0
  100. package/test/unit/spec/interceptors/locusRouteToken.ts +87 -0
  101. package/test/unit/spec/locus-info/index.js +91 -15
  102. package/test/unit/spec/locus-info/parser.js +3 -2
  103. package/test/unit/spec/media/properties.ts +137 -0
  104. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -0
  105. package/test/unit/spec/meeting/index.js +398 -30
  106. package/test/unit/spec/meeting/muteState.js +32 -6
  107. package/test/unit/spec/meeting/request.js +21 -0
  108. package/test/unit/spec/meeting/utils.js +49 -17
  109. package/test/unit/spec/meeting-info/meetinginfov2.js +8 -3
  110. package/test/unit/spec/meetings/index.js +10 -5
  111. package/test/unit/spec/member/util.js +24 -0
  112. package/test/unit/spec/members/collection.js +120 -0
  113. package/test/unit/spec/members/index.js +72 -3
  114. package/test/unit/spec/members/request.js +55 -0
  115. package/test/unit/spec/members/utils.js +116 -14
  116. package/test/unit/spec/reachability/index.ts +158 -3
  117. package/test/unit/spec/roap/turnDiscovery.ts +3 -3
@@ -28,6 +28,8 @@ import {
28
28
  StatsAnalyzerEventNames,
29
29
  NetworkQualityEventNames,
30
30
  NetworkQualityMonitor,
31
+ StatsMonitor,
32
+ StatsMonitorEventNames,
31
33
  } from '@webex/internal-media-core';
32
34
 
33
35
  import {
@@ -269,6 +271,7 @@ export enum ScreenShareFloorStatus {
269
271
  type FetchMeetingInfoParams = {
270
272
  password?: string;
271
273
  registrationId?: string;
274
+ classificationId?: string;
272
275
  captchaCode?: string;
273
276
  extraParams?: Record<string, any>;
274
277
  sendCAevents?: boolean;
@@ -633,6 +636,7 @@ export default class Meeting extends StatelessWebexPlugin {
633
636
  shareStatus: string;
634
637
  screenShareFloorState: ScreenShareFloorStatus;
635
638
  statsAnalyzer: StatsAnalyzer;
639
+ statsMonitor: StatsMonitor;
636
640
  transcription: Transcription;
637
641
  updateMediaConnections: (mediaConnections: any[]) => void;
638
642
  userDisplayHints: any;
@@ -1286,6 +1290,13 @@ export default class Meeting extends StatelessWebexPlugin {
1286
1290
  * @memberof Meeting
1287
1291
  */
1288
1292
  this.networkQualityMonitor = null;
1293
+ /**
1294
+ * @instance
1295
+ * @type {StatsMonitor}
1296
+ * @private
1297
+ * @memberof Meeting
1298
+ */
1299
+ this.statsMonitor = null;
1289
1300
  /**
1290
1301
  * Indicates network status of the webrtc media connection
1291
1302
  * @instance
@@ -1902,6 +1913,7 @@ export default class Meeting extends StatelessWebexPlugin {
1902
1913
  extraParams = {},
1903
1914
  sendCAevents = false,
1904
1915
  registrationId = null,
1916
+ classificationId = null,
1905
1917
  }): Promise<void> {
1906
1918
  try {
1907
1919
  const captchaInfo = captchaCode
@@ -1918,7 +1930,9 @@ export default class Meeting extends StatelessWebexPlugin {
1918
1930
  this.locusId,
1919
1931
  extraParams,
1920
1932
  {meetingId: this.id, sendCAevents},
1921
- registrationId
1933
+ registrationId,
1934
+ null,
1935
+ classificationId
1922
1936
  );
1923
1937
 
1924
1938
  this.parseMeetingInfo(info?.body, this.destination, info?.errors);
@@ -2962,6 +2976,18 @@ export default class Meeting extends StatelessWebexPlugin {
2962
2976
  );
2963
2977
  });
2964
2978
 
2979
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_AUTO_END_MEETING_WARNING_CHANGED, ({state}) => {
2980
+ Trigger.trigger(
2981
+ this,
2982
+ {
2983
+ file: 'meeting/index',
2984
+ function: 'setupLocusControlsListener',
2985
+ },
2986
+ EVENT_TRIGGERS.MEETING_CONTROLS_AUTO_END_MEETING_WARNING_UPDATED,
2987
+ {state}
2988
+ );
2989
+ });
2990
+
2965
2991
  this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_ANNOTATION_CHANGED, ({state}) => {
2966
2992
  Trigger.trigger(
2967
2993
  this,
@@ -3149,6 +3175,23 @@ export default class Meeting extends StatelessWebexPlugin {
3149
3175
  },
3150
3176
  EVENT_TRIGGERS.MEETING_STOPPED_SHARING_WHITEBOARD
3151
3177
  );
3178
+ // @ts-ignore
3179
+ this.webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp({
3180
+ key: 'internal.client.share.stopped',
3181
+ });
3182
+ // @ts-ignore
3183
+ this.webex.internal.newMetrics.submitClientEvent({
3184
+ name: 'client.share.stopped',
3185
+ payload: {
3186
+ mediaType: 'whiteboard',
3187
+ shareDuration:
3188
+ // @ts-ignore
3189
+ this.webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration(),
3190
+ },
3191
+ options: {
3192
+ meetingId: this.id,
3193
+ },
3194
+ });
3152
3195
  break;
3153
3196
 
3154
3197
  case SHARE_STATUS.NO_SHARE:
@@ -3167,6 +3210,14 @@ export default class Meeting extends StatelessWebexPlugin {
3167
3210
  this.shareCAEventSentStatus.receiveStart = false;
3168
3211
  this.shareCAEventSentStatus.receiveStop = false;
3169
3212
 
3213
+ let finalBeneficiaryId = contentShare.beneficiaryId;
3214
+ // In case of attendee in webinar, the whiteboard is shared by other participants
3215
+ if (this.locusInfo?.info?.isWebinar && this.webinar?.selfIsAttendee) {
3216
+ if (!finalBeneficiaryId && whiteboardShare.beneficiaryId) {
3217
+ finalBeneficiaryId = whiteboardShare.beneficiaryId;
3218
+ }
3219
+ }
3220
+
3170
3221
  Trigger.trigger(
3171
3222
  this,
3172
3223
  {
@@ -3175,7 +3226,7 @@ export default class Meeting extends StatelessWebexPlugin {
3175
3226
  },
3176
3227
  EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
3177
3228
  {
3178
- memberId: contentShare.beneficiaryId,
3229
+ memberId: finalBeneficiaryId,
3179
3230
  url: contentShare.url,
3180
3231
  shareInstanceId: this.remoteShareInstanceId,
3181
3232
  annotationInfo: contentShare.annotation,
@@ -3317,27 +3368,31 @@ export default class Meeting extends StatelessWebexPlugin {
3317
3368
  * @memberof Meeting
3318
3369
  */
3319
3370
  private setUpLocusUrlListener() {
3320
- this.locusInfo.on(EVENTS.LOCUS_INFO_UPDATE_URL, (payload) => {
3321
- this.members.locusUrlUpdate(payload);
3322
- this.breakouts.locusUrlUpdate(payload);
3323
- this.simultaneousInterpretation.locusUrlUpdate(payload);
3324
- this.annotation.locusUrlUpdate(payload);
3325
- this.locusUrl = payload;
3326
- this.locusId = this.locusUrl?.split('/').pop();
3327
- this.recordingController.setLocusUrl(this.locusUrl);
3328
- this.controlsOptionsManager.setLocusUrl(this.locusUrl);
3329
- this.webinar.locusUrlUpdate(payload);
3371
+ this.locusInfo.on(
3372
+ EVENTS.LOCUS_INFO_UPDATE_URL,
3373
+ (payload: {url: string; isMainLocus?: boolean}) => {
3374
+ const {url, isMainLocus} = payload;
3375
+ this.members.locusUrlUpdate(url);
3376
+ this.breakouts.locusUrlUpdate(url);
3377
+ this.simultaneousInterpretation.locusUrlUpdate(url);
3378
+ this.annotation.locusUrlUpdate(url);
3379
+ this.locusUrl = url;
3380
+ this.locusId = this.locusUrl?.split('/').pop();
3381
+ this.recordingController.setLocusUrl(this.locusUrl);
3382
+ this.controlsOptionsManager.setLocusUrl(this.locusUrl, !!isMainLocus);
3383
+ this.webinar.locusUrlUpdate(url);
3330
3384
 
3331
- Trigger.trigger(
3332
- this,
3333
- {
3334
- file: 'meeting/index',
3335
- function: 'setUpLocusSelfListener',
3336
- },
3337
- EVENT_TRIGGERS.MEETING_LOCUS_URL_UPDATE,
3338
- {locusUrl: payload}
3339
- );
3340
- });
3385
+ Trigger.trigger(
3386
+ this,
3387
+ {
3388
+ file: 'meeting/index',
3389
+ function: 'setUpLocusSelfListener',
3390
+ },
3391
+ EVENT_TRIGGERS.MEETING_LOCUS_URL_UPDATE,
3392
+ {locusUrl: url}
3393
+ );
3394
+ }
3395
+ );
3341
3396
  }
3342
3397
 
3343
3398
  /**
@@ -4180,6 +4235,7 @@ export default class Meeting extends StatelessWebexPlugin {
4180
4235
  this.userDisplayHints,
4181
4236
  this.selfUserPolicies
4182
4237
  ),
4238
+ showAutoEndMeetingWarning: MeetingUtil.showAutoEndMeetingWarning(this.userDisplayHints),
4183
4239
  canRaiseHand: MeetingUtil.canUserRaiseHand(this.userDisplayHints),
4184
4240
  canLowerAllHands: MeetingUtil.canUserLowerAllHands(this.userDisplayHints),
4185
4241
  canLowerSomeoneElsesHand: MeetingUtil.canUserLowerSomeoneElsesHand(this.userDisplayHints),
@@ -4195,8 +4251,13 @@ export default class Meeting extends StatelessWebexPlugin {
4195
4251
  isLocalRecordingStarted: MeetingUtil.isLocalRecordingStarted(this.userDisplayHints),
4196
4252
  isLocalRecordingStopped: MeetingUtil.isLocalRecordingStopped(this.userDisplayHints),
4197
4253
  isLocalRecordingPaused: MeetingUtil.isLocalRecordingPaused(this.userDisplayHints),
4254
+ isLocalStreamingStarted: MeetingUtil.isLocalStreamingStarted(this.userDisplayHints),
4255
+ isLocalStreamingStopped: MeetingUtil.isLocalStreamingStopped(this.userDisplayHints),
4198
4256
  isManualCaptionActive: MeetingUtil.isManualCaptionActive(this.userDisplayHints),
4199
4257
  isSaveTranscriptsEnabled: MeetingUtil.isSaveTranscriptsEnabled(this.userDisplayHints),
4258
+ isSpokenLanguageAutoDetectionEnabled: MeetingUtil.isSpokenLanguageAutoDetectionEnabled(
4259
+ this.userDisplayHints
4260
+ ),
4200
4261
  isWebexAssistantActive: MeetingUtil.isWebexAssistantActive(this.userDisplayHints),
4201
4262
  canViewCaptionPanel: MeetingUtil.canViewCaptionPanel(this.userDisplayHints),
4202
4263
  isRealTimeTranslationEnabled: MeetingUtil.isRealTimeTranslationEnabled(
@@ -6779,6 +6840,10 @@ export default class Meeting extends StatelessWebexPlugin {
6779
6840
  // @ts-ignore
6780
6841
  this.webex.internal.newMetrics.submitClientEvent({
6781
6842
  name: 'client.ice.start',
6843
+ payload: {
6844
+ // @ts-ignore
6845
+ labels: MeetingUtil.getCaEventLabelsForIpVersion(this.webex),
6846
+ },
6782
6847
  options: {
6783
6848
  meetingId: this.id,
6784
6849
  },
@@ -6948,10 +7013,10 @@ export default class Meeting extends StatelessWebexPlugin {
6948
7013
  }
6949
7014
  }
6950
7015
 
6951
- // Count members that are in the meeting.
7016
+ // Count members that are in the meeting or in the lobby.
6952
7017
  const {members} = this.getMembers().membersCollection;
6953
7018
  event.data.intervalMetadata.meetingUserCount = Object.values(members).filter(
6954
- (member: Member) => member.isInMeeting
7019
+ (member: Member) => member.isInMeeting || member.isInLobby
6955
7020
  ).length;
6956
7021
 
6957
7022
  // @ts-ignore
@@ -7310,10 +7375,12 @@ export default class Meeting extends StatelessWebexPlugin {
7310
7375
  if (this.config.stats.enableStatsAnalyzer) {
7311
7376
  // @ts-ignore - config coming from registerPlugin
7312
7377
  this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
7378
+ this.statsMonitor = new StatsMonitor();
7313
7379
  this.statsAnalyzer = new StatsAnalyzer({
7314
7380
  // @ts-ignore - config coming from registerPlugin
7315
7381
  config: this.config.stats,
7316
7382
  networkQualityMonitor: this.networkQualityMonitor,
7383
+ statsMonitor: this.statsMonitor,
7317
7384
  isMultistream: this.isMultistream,
7318
7385
  });
7319
7386
  this.shareCAEventSentStatus = {
@@ -7327,6 +7394,33 @@ export default class Meeting extends StatelessWebexPlugin {
7327
7394
  NetworkQualityEventNames.NETWORK_QUALITY,
7328
7395
  this.sendNetworkQualityEvent.bind(this)
7329
7396
  );
7397
+
7398
+ this.statsMonitor.on(StatsMonitorEventNames.INBOUND_AUDIO_ISSUE, (data) => {
7399
+ // Before forwarding any inbound audio issues to the app, make sure that we have at least one other
7400
+ // participant in the meeting with unmuted audio.
7401
+ // We don't check this.mediaProperties.mediaDirection here, because that's already handled in statsAnalyzer,
7402
+ // so we won't get this event if we are not setup to receive any audio
7403
+ const atLeastOneUnmutedOtherMember = Object.values(
7404
+ this.members.membersCollection.getAll()
7405
+ ).find((member) => {
7406
+ return !member.isSelf && !member.isPairedWithSelf && !member.isAudioMuted;
7407
+ });
7408
+
7409
+ if (atLeastOneUnmutedOtherMember) {
7410
+ this.mediaProperties.sendMediaIssueMetric(
7411
+ 'inbound_audio',
7412
+ data.issueSubType,
7413
+ this.correlationId
7414
+ );
7415
+
7416
+ Trigger.trigger(
7417
+ this,
7418
+ {file: 'meeting/index', function: 'createStatsAnalyzer'},
7419
+ EVENT_TRIGGERS.MEDIA_INBOUND_AUDIO_ISSUE_DETECTED,
7420
+ data
7421
+ );
7422
+ }
7423
+ });
7330
7424
  }
7331
7425
  }
7332
7426
 
@@ -7625,6 +7719,10 @@ export default class Meeting extends StatelessWebexPlugin {
7625
7719
  }
7626
7720
 
7627
7721
  this.statsAnalyzer = null;
7722
+ this.networkQualityMonitor?.removeAllListeners();
7723
+ this.networkQualityMonitor = null;
7724
+ this.statsMonitor?.removeAllListeners();
7725
+ this.statsMonitor = null;
7628
7726
 
7629
7727
  // when media fails, we want to upload a webrtc dump to see whats going on
7630
7728
  // this function is async, but returns once the stats have been gathered
@@ -7648,6 +7746,10 @@ export default class Meeting extends StatelessWebexPlugin {
7648
7746
  await this.statsAnalyzer.stopAnalyzer();
7649
7747
  }
7650
7748
  this.statsAnalyzer = null;
7749
+ this.networkQualityMonitor?.removeAllListeners();
7750
+ this.networkQualityMonitor = null;
7751
+ this.statsMonitor?.removeAllListeners();
7752
+ this.statsMonitor = null;
7651
7753
 
7652
7754
  this.isMultistream = false;
7653
7755
 
@@ -7819,6 +7921,9 @@ export default class Meeting extends StatelessWebexPlugin {
7819
7921
 
7820
7922
  this.allowMediaInLobby = options?.allowMediaInLobby;
7821
7923
 
7924
+ // @ts-ignore
7925
+ const ipver = MeetingUtil.getIpVersion(this.webex); // used just for metrics
7926
+
7822
7927
  // If the user is unjoined or guest waiting in lobby dont allow the user to addMedia
7823
7928
  // @ts-ignore - isUserUnadmitted coming from SelfUtil
7824
7929
  if (this.isUserUnadmitted && !this.wirelessShare && !this.allowMediaInLobby) {
@@ -7917,6 +8022,7 @@ export default class Meeting extends StatelessWebexPlugin {
7917
8022
  locus_id: this.locusUrl.split('/').pop(),
7918
8023
  connectionType,
7919
8024
  ipVersion,
8025
+ ipver,
7920
8026
  selectedCandidatePairChanges,
7921
8027
  numTransports,
7922
8028
  isMultistream: this.isMultistream,
@@ -7985,6 +8091,7 @@ export default class Meeting extends StatelessWebexPlugin {
7985
8091
  ...reachabilityMetrics,
7986
8092
  ...iceCandidateErrors,
7987
8093
  iceCandidatesCount: this.iceCandidatesCount,
8094
+ ipver,
7988
8095
  });
7989
8096
 
7990
8097
  await this.cleanUpOnAddMediaFailure();
@@ -9361,6 +9468,36 @@ export default class Meeting extends StatelessWebexPlugin {
9361
9468
  return Promise.reject(new Error('Error sending reaction, service url not found.'));
9362
9469
  }
9363
9470
 
9471
+ /**
9472
+ * Extend the current meeting duration.
9473
+ *
9474
+ * @param {number} extensionMinutes - how many minutes to extend
9475
+ * @returns {Promise}
9476
+ * @public
9477
+ * @memberof Meeting
9478
+ */
9479
+ public extendMeeting({
9480
+ meetingPolicyUrl,
9481
+ meetingInstanceId,
9482
+ participantId,
9483
+ extensionMinutes = 30,
9484
+ }) {
9485
+ if (!meetingInstanceId || !participantId) {
9486
+ return Promise.reject(new Error('Missing meetingInstanceId or participantId'));
9487
+ }
9488
+
9489
+ if (!meetingPolicyUrl) {
9490
+ return Promise.reject(new Error('Missing meetingPolicyUrl'));
9491
+ }
9492
+
9493
+ return this.meetingRequest.extendMeeting({
9494
+ meetingInstanceId,
9495
+ participantId,
9496
+ extensionMinutes,
9497
+ meetingPolicyUrl,
9498
+ });
9499
+ }
9500
+
9364
9501
  /**
9365
9502
  * Method to enable or disable reactions inside the meeting.
9366
9503
  *
@@ -9890,4 +10027,47 @@ export default class Meeting extends StatelessWebexPlugin {
9890
10027
 
9891
10028
  return this.meetingRequest.synchronizeStage(this.locusUrl, videoLayout);
9892
10029
  }
10030
+
10031
+ /**
10032
+ * Notifies the host with the given meeting UUID and display names.
10033
+ *
10034
+ * @param {string} meetingUuid - The UUID of the meeting.
10035
+ * @param {string[]} displayName - An array of display names to notify the host with.
10036
+ * @returns {Promise<any>} The result of the notifyHost request.
10037
+ */
10038
+ notifyHost(meetingUuid: string, displayName: string[]) {
10039
+ return this.meetingRequest.notifyHost(
10040
+ this.meetingInfo.siteFullUrl,
10041
+ this.locusId,
10042
+ meetingUuid,
10043
+ displayName
10044
+ );
10045
+ }
10046
+
10047
+ /**
10048
+ * Call out a SIP participant to a meeting
10049
+ * @param {string} address - The SIP address or phone number
10050
+ * @param {string} displayName - The display name for the participant
10051
+ * @param {string} [correlationId] - Optional correlation ID
10052
+ * @returns {Promise} Promise that resolves when the call-out is initiated
10053
+ */
10054
+ sipCallOut(address: string, displayName: string) {
10055
+ return this.meetingRequest.sipCallOut(
10056
+ this.meetingInfo.meetingId,
10057
+ this.meetingInfo.meetingId,
10058
+ address,
10059
+ displayName
10060
+ );
10061
+ }
10062
+
10063
+ /**
10064
+ * Cancel an ongoing SIP call-out
10065
+ * @param {string} participantId - The participant ID to cancel
10066
+ * @returns {Promise} Promise that resolves when the call-out is cancelled
10067
+ * @public
10068
+ * @memberof Meetings
10069
+ */
10070
+ cancelSipCallOut(participantId: string) {
10071
+ return this.meetingRequest.cancelSipCallOut(participantId);
10072
+ }
9893
10073
  }
@@ -291,18 +291,14 @@ export class MuteState {
291
291
  );
292
292
 
293
293
  return MeetingUtil.remoteUpdateAudioVideo(meeting, audioMuted, videoMuted)
294
- .then((locus) => {
294
+ .then((response) => {
295
295
  LoggerProxy.logger.info(
296
296
  `Meeting:muteState#sendLocalMuteRequestToServer --> ${this.type}: local mute (audio=${audioMuted}, video=${videoMuted}) applied to server`
297
297
  );
298
298
 
299
299
  this.state.server.localMute = this.type === AUDIO ? audioMuted : videoMuted;
300
300
 
301
- if (locus) {
302
- meeting.locusInfo.handleLocusDelta(locus, meeting);
303
- }
304
-
305
- return locus;
301
+ return MeetingUtil.updateLocusFromApiResponse(meeting, response);
306
302
  })
307
303
  .catch((remoteUpdateError) => {
308
304
  LoggerProxy.logger.warn(
@@ -886,6 +886,44 @@ export default class MeetingRequest extends StatelessWebexPlugin {
886
886
  });
887
887
  }
888
888
 
889
+ /**
890
+ * Extend the current meeting duration.
891
+ *
892
+ * @param {Object} params - Parameters for extending the meeting.
893
+ * @param {string} params.meetingInstanceId - The unique ID of the meeting instance.
894
+ * @param {string} params.participantId - The ID of the participant requesting the extension.
895
+ * @param {number} params.extensionMinutes - The number of minutes to extend the meeting by.
896
+ * @param {string} params.meetingPolicyUrl - The base URL for meeting policy service (dynamic, from locus links)
897
+ * @returns {Promise<any>} A promise that resolves with the server response.
898
+ */
899
+ extendMeeting({
900
+ meetingInstanceId,
901
+ participantId,
902
+ extensionMinutes,
903
+ meetingPolicyUrl,
904
+ }: {
905
+ meetingInstanceId: string;
906
+ participantId: string;
907
+ extensionMinutes: number;
908
+ meetingPolicyUrl: string;
909
+ }) {
910
+ if (!meetingPolicyUrl) {
911
+ return Promise.reject(new Error('meetingPolicyUrl is required'));
912
+ }
913
+ const uri = `${meetingPolicyUrl}/continueMeeting`;
914
+
915
+ // @ts-ignore
916
+ return this.request({
917
+ method: HTTP_VERBS.POST,
918
+ uri,
919
+ body: {
920
+ meetingInstanceId,
921
+ requestParticipantId: participantId,
922
+ extensionMinutes,
923
+ },
924
+ });
925
+ }
926
+
889
927
  /**
890
928
  * Make a network request to enable or disable reactions.
891
929
  * @param {boolean} options.enable - determines if we need to enable or disable.
@@ -985,4 +1023,107 @@ export default class MeetingRequest extends StatelessWebexPlugin {
985
1023
  body: {videoLayout},
986
1024
  });
987
1025
  }
1026
+
1027
+ /**
1028
+ * Sends a request to notify the host of a meeting.
1029
+ * @param {string} siteFullUrl - The site URL.
1030
+ * @param {string} locusId - The locus ID.
1031
+ * @param {string} meetingUuid - The meeting UUID.
1032
+ * @param {Array<string>} displayName - The display names to notify the host about.
1033
+ * @returns {Promise}
1034
+ */
1035
+ notifyHost(siteFullUrl: string, locusId: string, meetingUuid: string, displayName: string[]) {
1036
+ // @ts-ignore
1037
+ return this.request({
1038
+ method: HTTP_VERBS.POST,
1039
+ uri: `https://${siteFullUrl}/wbxappapi/v1/meetings/${meetingUuid}/notifyhost`,
1040
+ body: {
1041
+ displayName,
1042
+ size: displayName?.length,
1043
+ },
1044
+ headers: {
1045
+ locusId,
1046
+ },
1047
+ });
1048
+ }
1049
+
1050
+ /**
1051
+ * Call out to a SIP participant
1052
+ *
1053
+ * @param {any} meetingId - The meeting ID.
1054
+ * @param {any} meetingNumber - The meeting number.
1055
+ * @param {string} address - The SIP address to call out.
1056
+ * @param {string} displayName - The display name for the participant.
1057
+ * @returns {Promise} The API response
1058
+ */
1059
+ public async sipCallOut(meetingId, meetingNumber, address, displayName) {
1060
+ const body: any = {
1061
+ meetingId,
1062
+ meetingNumber,
1063
+ address,
1064
+ displayName,
1065
+ };
1066
+ try {
1067
+ // @ts-ignore
1068
+ const response = await this.request({
1069
+ method: HTTP_VERBS.POST,
1070
+ service: 'hydra',
1071
+ resource: 'meetingParticipants/callout',
1072
+ body,
1073
+ headers: {
1074
+ Accept: 'application/json',
1075
+ },
1076
+ });
1077
+
1078
+ LoggerProxy.logger.info('Meetings:request#sipCallOut --> SIP call-out successful', response);
1079
+
1080
+ return response.body;
1081
+ } catch (err) {
1082
+ LoggerProxy.logger.error(
1083
+ `Meetings:request#sipCallOut --> Error calling out SIP participant, error ${JSON.stringify(
1084
+ err
1085
+ )}`
1086
+ );
1087
+ throw err;
1088
+ }
1089
+ }
1090
+
1091
+ /**
1092
+ * Cancel an ongoing SIP call-out
1093
+ *
1094
+ * @param {string} participantId - The ID of the participant whose SIP call-out should be cancelled.
1095
+ * @returns {Promise} The API response
1096
+ */
1097
+ public async cancelSipCallOut(participantId) {
1098
+ const body = {
1099
+ participantId,
1100
+ };
1101
+
1102
+ try {
1103
+ // @ts-ignore
1104
+ const response = await this.request({
1105
+ method: HTTP_VERBS.POST,
1106
+ service: 'hydra',
1107
+ resource: 'meetingParticipants/cancelCallout',
1108
+ body,
1109
+ headers: {
1110
+ Accept: 'application/json',
1111
+ },
1112
+ });
1113
+
1114
+ LoggerProxy.logger.info(
1115
+ 'Meetings:request#cancelSipCallOut --> SIP call-out cancelled successfully',
1116
+ response
1117
+ );
1118
+
1119
+ return response.body;
1120
+ } catch (err) {
1121
+ LoggerProxy.logger.error(
1122
+ `Meetings:request#cancelSipCallOut --> Error cancelling SIP participant call-out, error ${JSON.stringify(
1123
+ err
1124
+ )}`
1125
+ );
1126
+ throw err;
1127
+ }
1128
+ }
988
1129
  }
@@ -59,18 +59,16 @@ const MeetingUtil = {
59
59
  );
60
60
  }
61
61
 
62
- return meeting.locusMediaRequest
63
- .send({
64
- type: 'LocalMute',
65
- selfUrl: meeting.selfUrl,
66
- mediaId: meeting.mediaId,
67
- sequence: meeting.locusInfo.sequence,
68
- muteOptions: {
69
- audioMuted,
70
- videoMuted,
71
- },
72
- })
73
- .then((response) => response?.body?.locus);
62
+ return meeting.locusMediaRequest.send({
63
+ type: 'LocalMute',
64
+ selfUrl: meeting.selfUrl,
65
+ mediaId: meeting.mediaId,
66
+ sequence: meeting.locusInfo.sequence,
67
+ muteOptions: {
68
+ audioMuted,
69
+ videoMuted,
70
+ },
71
+ });
74
72
  },
75
73
 
76
74
  hasOwner: (info) => info && info.owner,
@@ -115,6 +113,28 @@ const MeetingUtil = {
115
113
  return IP_VERSION.unknown;
116
114
  },
117
115
 
116
+ /**
117
+ * Returns CA event labels related to Orpheus ipver parameter that can be sent to CA with any CA event
118
+ * @param {any} webex instance
119
+ * @returns {Array<string>|undefined} array of CA event labels or undefined if no labels should be sent
120
+ */
121
+ getCaEventLabelsForIpVersion(webex: any): Array<string> | undefined {
122
+ const ipver = MeetingUtil.getIpVersion(webex);
123
+
124
+ switch (ipver) {
125
+ case IP_VERSION.unknown:
126
+ return undefined;
127
+ case IP_VERSION.only_ipv4:
128
+ return ['hasIpv4_true'];
129
+ case IP_VERSION.only_ipv6:
130
+ return ['hasIpv6_true'];
131
+ case IP_VERSION.ipv4_and_ipv6:
132
+ return ['hasIpv4_true', 'hasIpv6_true'];
133
+ default:
134
+ return undefined;
135
+ }
136
+ },
137
+
118
138
  joinMeeting: async (meeting, options) => {
119
139
  if (!meeting) {
120
140
  return Promise.reject(new ParameterError('You need a meeting object.'));
@@ -613,11 +633,20 @@ const MeetingUtil = {
613
633
  isLocalRecordingPaused: (displayHints) =>
614
634
  displayHints.includes(DISPLAY_HINTS.LOCAL_RECORDING_STATUS_PAUSED),
615
635
 
636
+ isLocalStreamingStarted: (displayHints) =>
637
+ displayHints.includes(DISPLAY_HINTS.STREAMING_STATUS_STARTED),
638
+
639
+ isLocalStreamingStopped: (displayHints) =>
640
+ displayHints.includes(DISPLAY_HINTS.STREAMING_STATUS_STOPPED),
641
+
616
642
  canStopManualCaption: (displayHints) => displayHints.includes(DISPLAY_HINTS.MANUAL_CAPTION_STOP),
617
643
 
618
644
  isManualCaptionActive: (displayHints) =>
619
645
  displayHints.includes(DISPLAY_HINTS.MANUAL_CAPTION_STATUS_ACTIVE),
620
646
 
647
+ isSpokenLanguageAutoDetectionEnabled: (displayHints) =>
648
+ displayHints.includes(DISPLAY_HINTS.SPOKEN_LANGUAGE_AUTO_DETECTION_ENABLED),
649
+
621
650
  isWebexAssistantActive: (displayHints) =>
622
651
  displayHints.includes(DISPLAY_HINTS.WEBEX_ASSISTANT_STATUS_ACTIVE),
623
652
 
@@ -631,6 +660,9 @@ const MeetingUtil = {
631
660
 
632
661
  waitingForOthersToJoin: (displayHints) => displayHints.includes(DISPLAY_HINTS.WAITING_FOR_OTHERS),
633
662
 
663
+ showAutoEndMeetingWarning: (displayHints) =>
664
+ displayHints.includes(DISPLAY_HINTS.SHOW_AUTO_END_MEETING_WARNING),
665
+
634
666
  canSendReactions: (originalValue, displayHints) => {
635
667
  if (displayHints.includes(DISPLAY_HINTS.REACTIONS_ACTIVE)) {
636
668
  return true;
@@ -673,22 +705,20 @@ const MeetingUtil = {
673
705
  },
674
706
 
675
707
  /**
676
- * Updates the locus info for the meeting with the delta locus
677
- * returned from requests that include the sequence information
708
+ * Updates the locus info for the meeting with the locus
709
+ * information returned from API requests made to Locus
678
710
  * Returns the original response object
679
711
  * @param {Object} meeting The meeting object
680
712
  * @param {Object} response The response of the http request
681
713
  * @returns {Object}
682
714
  */
683
- updateLocusWithDelta: (meeting, response) => {
715
+ updateLocusFromApiResponse: (meeting, response) => {
684
716
  if (!meeting) {
685
717
  return response;
686
718
  }
687
719
 
688
- const locus = response?.body?.locus;
689
-
690
- if (locus) {
691
- meeting.locusInfo.handleLocusDelta(locus, meeting);
720
+ if (response?.body?.locus) {
721
+ meeting.locusInfo.handleLocusAPIResponse(meeting, response.body);
692
722
  }
693
723
 
694
724
  return response;
@@ -735,7 +765,7 @@ const MeetingUtil = {
735
765
 
736
766
  return meeting
737
767
  .request(options)
738
- .then((response) => MeetingUtil.updateLocusWithDelta(meeting, response));
768
+ .then((response) => MeetingUtil.updateLocusFromApiResponse(meeting, response));
739
769
  };
740
770
 
741
771
  return locusDeltaRequest;