@webex/plugin-meetings 3.9.0 → 3.10.0-next.2

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 (121) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +9 -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/index.js +5 -0
  22. package/dist/media/index.js.map +1 -1
  23. package/dist/media/properties.js +53 -5
  24. package/dist/media/properties.js.map +1 -1
  25. package/dist/meeting/in-meeting-actions.js +8 -0
  26. package/dist/meeting/in-meeting-actions.js.map +1 -1
  27. package/dist/meeting/index.js +339 -185
  28. package/dist/meeting/index.js.map +1 -1
  29. package/dist/meeting/muteState.js +2 -5
  30. package/dist/meeting/muteState.js.map +1 -1
  31. package/dist/meeting/request.js +177 -14
  32. package/dist/meeting/request.js.map +1 -1
  33. package/dist/meeting/util.js +39 -11
  34. package/dist/meeting/util.js.map +1 -1
  35. package/dist/meeting-info/meeting-info-v2.js +29 -21
  36. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  37. package/dist/meetings/index.js +31 -25
  38. package/dist/meetings/index.js.map +1 -1
  39. package/dist/member/index.js +9 -0
  40. package/dist/member/index.js.map +1 -1
  41. package/dist/member/types.js.map +1 -1
  42. package/dist/member/util.js +10 -0
  43. package/dist/member/util.js.map +1 -1
  44. package/dist/members/collection.js +13 -0
  45. package/dist/members/collection.js.map +1 -1
  46. package/dist/members/index.js +42 -20
  47. package/dist/members/index.js.map +1 -1
  48. package/dist/members/util.js +7 -2
  49. package/dist/members/util.js.map +1 -1
  50. package/dist/metrics/constants.js +2 -1
  51. package/dist/metrics/constants.js.map +1 -1
  52. package/dist/reachability/index.js +3 -3
  53. package/dist/reachability/index.js.map +1 -1
  54. package/dist/types/constants.d.ts +7 -0
  55. package/dist/types/controls-options-manager/index.d.ts +9 -1
  56. package/dist/types/interceptors/index.d.ts +2 -1
  57. package/dist/types/interceptors/locusRouteToken.d.ts +38 -0
  58. package/dist/types/locus-info/index.d.ts +56 -2
  59. package/dist/types/media/properties.d.ts +21 -0
  60. package/dist/types/meeting/in-meeting-actions.d.ts +8 -0
  61. package/dist/types/meeting/index.d.ts +41 -1
  62. package/dist/types/meeting/request.d.ts +42 -0
  63. package/dist/types/meeting/util.d.ts +13 -3
  64. package/dist/types/meeting-info/meeting-info-v2.d.ts +6 -3
  65. package/dist/types/meetings/index.d.ts +3 -1
  66. package/dist/types/member/index.d.ts +1 -0
  67. package/dist/types/member/types.d.ts +1 -0
  68. package/dist/types/member/util.d.ts +5 -0
  69. package/dist/types/members/collection.d.ts +6 -0
  70. package/dist/types/members/index.d.ts +12 -2
  71. package/dist/types/members/util.d.ts +6 -3
  72. package/dist/types/metrics/constants.d.ts +1 -0
  73. package/dist/webinar/index.js +1 -1
  74. package/package.json +24 -24
  75. package/src/constants.ts +10 -0
  76. package/src/controls-options-manager/index.ts +26 -5
  77. package/src/index.ts +2 -1
  78. package/src/interceptors/index.ts +2 -1
  79. package/src/interceptors/locusRouteToken.ts +80 -0
  80. package/src/locus-info/controlsUtils.ts +18 -0
  81. package/src/locus-info/index.ts +99 -17
  82. package/src/locus-info/parser.ts +5 -1
  83. package/src/media/index.ts +6 -0
  84. package/src/media/properties.ts +43 -0
  85. package/src/meeting/in-meeting-actions.ts +16 -0
  86. package/src/meeting/index.ts +205 -24
  87. package/src/meeting/muteState.ts +2 -6
  88. package/src/meeting/request.ts +141 -0
  89. package/src/meeting/util.ts +50 -20
  90. package/src/meeting-info/meeting-info-v2.ts +24 -5
  91. package/src/meetings/index.ts +9 -3
  92. package/src/member/index.ts +10 -0
  93. package/src/member/types.ts +1 -0
  94. package/src/member/util.ts +14 -0
  95. package/src/members/collection.ts +11 -0
  96. package/src/members/index.ts +38 -5
  97. package/src/members/util.ts +18 -2
  98. package/src/metrics/constants.ts +1 -0
  99. package/src/reachability/index.ts +3 -3
  100. package/test/unit/spec/common/browser-detection.js +0 -24
  101. package/test/unit/spec/controls-options-manager/index.js +47 -0
  102. package/test/unit/spec/fixture/locus.js +1 -0
  103. package/test/unit/spec/interceptors/locusRouteToken.ts +87 -0
  104. package/test/unit/spec/locus-info/index.js +91 -15
  105. package/test/unit/spec/locus-info/parser.js +3 -2
  106. package/test/unit/spec/media/index.ts +140 -9
  107. package/test/unit/spec/media/properties.ts +137 -0
  108. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -0
  109. package/test/unit/spec/meeting/index.js +398 -30
  110. package/test/unit/spec/meeting/muteState.js +32 -6
  111. package/test/unit/spec/meeting/request.js +21 -0
  112. package/test/unit/spec/meeting/utils.js +48 -16
  113. package/test/unit/spec/meeting-info/meetinginfov2.js +8 -3
  114. package/test/unit/spec/meetings/index.js +10 -7
  115. package/test/unit/spec/member/util.js +24 -0
  116. package/test/unit/spec/members/collection.js +120 -0
  117. package/test/unit/spec/members/index.js +72 -3
  118. package/test/unit/spec/members/request.js +55 -0
  119. package/test/unit/spec/members/utils.js +116 -14
  120. package/test/unit/spec/reachability/index.ts +158 -3
  121. package/test/unit/spec/roap/turnDiscovery.ts +3 -3
@@ -28,6 +28,9 @@ import {
28
28
  StatsAnalyzerEventNames,
29
29
  NetworkQualityEventNames,
30
30
  NetworkQualityMonitor,
31
+ StatsMonitor,
32
+ StatsMonitorEventNames,
33
+ InboundAudioIssueSubTypes,
31
34
  } from '@webex/internal-media-core';
32
35
 
33
36
  import {
@@ -269,6 +272,7 @@ export enum ScreenShareFloorStatus {
269
272
  type FetchMeetingInfoParams = {
270
273
  password?: string;
271
274
  registrationId?: string;
275
+ classificationId?: string;
272
276
  captchaCode?: string;
273
277
  extraParams?: Record<string, any>;
274
278
  sendCAevents?: boolean;
@@ -633,6 +637,7 @@ export default class Meeting extends StatelessWebexPlugin {
633
637
  shareStatus: string;
634
638
  screenShareFloorState: ScreenShareFloorStatus;
635
639
  statsAnalyzer: StatsAnalyzer;
640
+ statsMonitor: StatsMonitor;
636
641
  transcription: Transcription;
637
642
  updateMediaConnections: (mediaConnections: any[]) => void;
638
643
  userDisplayHints: any;
@@ -1286,6 +1291,13 @@ export default class Meeting extends StatelessWebexPlugin {
1286
1291
  * @memberof Meeting
1287
1292
  */
1288
1293
  this.networkQualityMonitor = null;
1294
+ /**
1295
+ * @instance
1296
+ * @type {StatsMonitor}
1297
+ * @private
1298
+ * @memberof Meeting
1299
+ */
1300
+ this.statsMonitor = null;
1289
1301
  /**
1290
1302
  * Indicates network status of the webrtc media connection
1291
1303
  * @instance
@@ -1902,6 +1914,7 @@ export default class Meeting extends StatelessWebexPlugin {
1902
1914
  extraParams = {},
1903
1915
  sendCAevents = false,
1904
1916
  registrationId = null,
1917
+ classificationId = null,
1905
1918
  }): Promise<void> {
1906
1919
  try {
1907
1920
  const captchaInfo = captchaCode
@@ -1918,7 +1931,9 @@ export default class Meeting extends StatelessWebexPlugin {
1918
1931
  this.locusId,
1919
1932
  extraParams,
1920
1933
  {meetingId: this.id, sendCAevents},
1921
- registrationId
1934
+ registrationId,
1935
+ null,
1936
+ classificationId
1922
1937
  );
1923
1938
 
1924
1939
  this.parseMeetingInfo(info?.body, this.destination, info?.errors);
@@ -2962,6 +2977,18 @@ export default class Meeting extends StatelessWebexPlugin {
2962
2977
  );
2963
2978
  });
2964
2979
 
2980
+ this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_AUTO_END_MEETING_WARNING_CHANGED, ({state}) => {
2981
+ Trigger.trigger(
2982
+ this,
2983
+ {
2984
+ file: 'meeting/index',
2985
+ function: 'setupLocusControlsListener',
2986
+ },
2987
+ EVENT_TRIGGERS.MEETING_CONTROLS_AUTO_END_MEETING_WARNING_UPDATED,
2988
+ {state}
2989
+ );
2990
+ });
2991
+
2965
2992
  this.locusInfo.on(LOCUSINFO.EVENTS.CONTROLS_ANNOTATION_CHANGED, ({state}) => {
2966
2993
  Trigger.trigger(
2967
2994
  this,
@@ -3149,6 +3176,23 @@ export default class Meeting extends StatelessWebexPlugin {
3149
3176
  },
3150
3177
  EVENT_TRIGGERS.MEETING_STOPPED_SHARING_WHITEBOARD
3151
3178
  );
3179
+ // @ts-ignore
3180
+ this.webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp({
3181
+ key: 'internal.client.share.stopped',
3182
+ });
3183
+ // @ts-ignore
3184
+ this.webex.internal.newMetrics.submitClientEvent({
3185
+ name: 'client.share.stopped',
3186
+ payload: {
3187
+ mediaType: 'whiteboard',
3188
+ shareDuration:
3189
+ // @ts-ignore
3190
+ this.webex.internal.newMetrics.callDiagnosticLatencies.getShareDuration(),
3191
+ },
3192
+ options: {
3193
+ meetingId: this.id,
3194
+ },
3195
+ });
3152
3196
  break;
3153
3197
 
3154
3198
  case SHARE_STATUS.NO_SHARE:
@@ -3167,6 +3211,14 @@ export default class Meeting extends StatelessWebexPlugin {
3167
3211
  this.shareCAEventSentStatus.receiveStart = false;
3168
3212
  this.shareCAEventSentStatus.receiveStop = false;
3169
3213
 
3214
+ let finalBeneficiaryId = contentShare.beneficiaryId;
3215
+ // In case of attendee in webinar, the whiteboard is shared by other participants
3216
+ if (this.locusInfo?.info?.isWebinar && this.webinar?.selfIsAttendee) {
3217
+ if (!finalBeneficiaryId && whiteboardShare.beneficiaryId) {
3218
+ finalBeneficiaryId = whiteboardShare.beneficiaryId;
3219
+ }
3220
+ }
3221
+
3170
3222
  Trigger.trigger(
3171
3223
  this,
3172
3224
  {
@@ -3175,7 +3227,7 @@ export default class Meeting extends StatelessWebexPlugin {
3175
3227
  },
3176
3228
  EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
3177
3229
  {
3178
- memberId: contentShare.beneficiaryId,
3230
+ memberId: finalBeneficiaryId,
3179
3231
  url: contentShare.url,
3180
3232
  shareInstanceId: this.remoteShareInstanceId,
3181
3233
  annotationInfo: contentShare.annotation,
@@ -3317,27 +3369,31 @@ export default class Meeting extends StatelessWebexPlugin {
3317
3369
  * @memberof Meeting
3318
3370
  */
3319
3371
  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);
3372
+ this.locusInfo.on(
3373
+ EVENTS.LOCUS_INFO_UPDATE_URL,
3374
+ (payload: {url: string; isMainLocus?: boolean}) => {
3375
+ const {url, isMainLocus} = payload;
3376
+ this.members.locusUrlUpdate(url);
3377
+ this.breakouts.locusUrlUpdate(url);
3378
+ this.simultaneousInterpretation.locusUrlUpdate(url);
3379
+ this.annotation.locusUrlUpdate(url);
3380
+ this.locusUrl = url;
3381
+ this.locusId = this.locusUrl?.split('/').pop();
3382
+ this.recordingController.setLocusUrl(this.locusUrl);
3383
+ this.controlsOptionsManager.setLocusUrl(this.locusUrl, !!isMainLocus);
3384
+ this.webinar.locusUrlUpdate(url);
3330
3385
 
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
- });
3386
+ Trigger.trigger(
3387
+ this,
3388
+ {
3389
+ file: 'meeting/index',
3390
+ function: 'setUpLocusSelfListener',
3391
+ },
3392
+ EVENT_TRIGGERS.MEETING_LOCUS_URL_UPDATE,
3393
+ {locusUrl: url}
3394
+ );
3395
+ }
3396
+ );
3341
3397
  }
3342
3398
 
3343
3399
  /**
@@ -4180,6 +4236,7 @@ export default class Meeting extends StatelessWebexPlugin {
4180
4236
  this.userDisplayHints,
4181
4237
  this.selfUserPolicies
4182
4238
  ),
4239
+ showAutoEndMeetingWarning: MeetingUtil.showAutoEndMeetingWarning(this.userDisplayHints),
4183
4240
  canRaiseHand: MeetingUtil.canUserRaiseHand(this.userDisplayHints),
4184
4241
  canLowerAllHands: MeetingUtil.canUserLowerAllHands(this.userDisplayHints),
4185
4242
  canLowerSomeoneElsesHand: MeetingUtil.canUserLowerSomeoneElsesHand(this.userDisplayHints),
@@ -4195,8 +4252,13 @@ export default class Meeting extends StatelessWebexPlugin {
4195
4252
  isLocalRecordingStarted: MeetingUtil.isLocalRecordingStarted(this.userDisplayHints),
4196
4253
  isLocalRecordingStopped: MeetingUtil.isLocalRecordingStopped(this.userDisplayHints),
4197
4254
  isLocalRecordingPaused: MeetingUtil.isLocalRecordingPaused(this.userDisplayHints),
4255
+ isLocalStreamingStarted: MeetingUtil.isLocalStreamingStarted(this.userDisplayHints),
4256
+ isLocalStreamingStopped: MeetingUtil.isLocalStreamingStopped(this.userDisplayHints),
4198
4257
  isManualCaptionActive: MeetingUtil.isManualCaptionActive(this.userDisplayHints),
4199
4258
  isSaveTranscriptsEnabled: MeetingUtil.isSaveTranscriptsEnabled(this.userDisplayHints),
4259
+ isSpokenLanguageAutoDetectionEnabled: MeetingUtil.isSpokenLanguageAutoDetectionEnabled(
4260
+ this.userDisplayHints
4261
+ ),
4200
4262
  isWebexAssistantActive: MeetingUtil.isWebexAssistantActive(this.userDisplayHints),
4201
4263
  canViewCaptionPanel: MeetingUtil.canViewCaptionPanel(this.userDisplayHints),
4202
4264
  isRealTimeTranslationEnabled: MeetingUtil.isRealTimeTranslationEnabled(
@@ -6779,6 +6841,10 @@ export default class Meeting extends StatelessWebexPlugin {
6779
6841
  // @ts-ignore
6780
6842
  this.webex.internal.newMetrics.submitClientEvent({
6781
6843
  name: 'client.ice.start',
6844
+ payload: {
6845
+ // @ts-ignore
6846
+ labels: MeetingUtil.getCaEventLabelsForIpVersion(this.webex),
6847
+ },
6782
6848
  options: {
6783
6849
  meetingId: this.id,
6784
6850
  },
@@ -6948,10 +7014,10 @@ export default class Meeting extends StatelessWebexPlugin {
6948
7014
  }
6949
7015
  }
6950
7016
 
6951
- // Count members that are in the meeting.
7017
+ // Count members that are in the meeting or in the lobby.
6952
7018
  const {members} = this.getMembers().membersCollection;
6953
7019
  event.data.intervalMetadata.meetingUserCount = Object.values(members).filter(
6954
- (member: Member) => member.isInMeeting
7020
+ (member: Member) => member.isInMeeting || member.isInLobby
6955
7021
  ).length;
6956
7022
 
6957
7023
  // @ts-ignore
@@ -7310,10 +7376,12 @@ export default class Meeting extends StatelessWebexPlugin {
7310
7376
  if (this.config.stats.enableStatsAnalyzer) {
7311
7377
  // @ts-ignore - config coming from registerPlugin
7312
7378
  this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
7379
+ this.statsMonitor = new StatsMonitor();
7313
7380
  this.statsAnalyzer = new StatsAnalyzer({
7314
7381
  // @ts-ignore - config coming from registerPlugin
7315
7382
  config: this.config.stats,
7316
7383
  networkQualityMonitor: this.networkQualityMonitor,
7384
+ statsMonitor: this.statsMonitor,
7317
7385
  isMultistream: this.isMultistream,
7318
7386
  });
7319
7387
  this.shareCAEventSentStatus = {
@@ -7327,6 +7395,33 @@ export default class Meeting extends StatelessWebexPlugin {
7327
7395
  NetworkQualityEventNames.NETWORK_QUALITY,
7328
7396
  this.sendNetworkQualityEvent.bind(this)
7329
7397
  );
7398
+
7399
+ this.statsMonitor.on(StatsMonitorEventNames.INBOUND_AUDIO_ISSUE, (data) => {
7400
+ // Before forwarding any inbound audio issues to the app, make sure that we have at least one other
7401
+ // participant in the meeting with unmuted audio.
7402
+ // We don't check this.mediaProperties.mediaDirection here, because that's already handled in statsAnalyzer,
7403
+ // so we won't get this event if we are not setup to receive any audio
7404
+ const atLeastOneUnmutedOtherMember = Object.values(
7405
+ this.members.membersCollection.getAll()
7406
+ ).find((member) => {
7407
+ return !member.isSelf && !member.isPairedWithSelf && !member.isAudioMuted;
7408
+ });
7409
+
7410
+ if (atLeastOneUnmutedOtherMember) {
7411
+ this.mediaProperties.sendMediaIssueMetric(
7412
+ 'inbound_audio',
7413
+ data.issueSubType,
7414
+ this.correlationId
7415
+ );
7416
+
7417
+ Trigger.trigger(
7418
+ this,
7419
+ {file: 'meeting/index', function: 'createStatsAnalyzer'},
7420
+ EVENT_TRIGGERS.MEDIA_INBOUND_AUDIO_ISSUE_DETECTED,
7421
+ data
7422
+ );
7423
+ }
7424
+ });
7330
7425
  }
7331
7426
  }
7332
7427
 
@@ -7625,6 +7720,10 @@ export default class Meeting extends StatelessWebexPlugin {
7625
7720
  }
7626
7721
 
7627
7722
  this.statsAnalyzer = null;
7723
+ this.networkQualityMonitor?.removeAllListeners();
7724
+ this.networkQualityMonitor = null;
7725
+ this.statsMonitor?.removeAllListeners();
7726
+ this.statsMonitor = null;
7628
7727
 
7629
7728
  // when media fails, we want to upload a webrtc dump to see whats going on
7630
7729
  // this function is async, but returns once the stats have been gathered
@@ -7648,6 +7747,10 @@ export default class Meeting extends StatelessWebexPlugin {
7648
7747
  await this.statsAnalyzer.stopAnalyzer();
7649
7748
  }
7650
7749
  this.statsAnalyzer = null;
7750
+ this.networkQualityMonitor?.removeAllListeners();
7751
+ this.networkQualityMonitor = null;
7752
+ this.statsMonitor?.removeAllListeners();
7753
+ this.statsMonitor = null;
7651
7754
 
7652
7755
  this.isMultistream = false;
7653
7756
 
@@ -7819,6 +7922,9 @@ export default class Meeting extends StatelessWebexPlugin {
7819
7922
 
7820
7923
  this.allowMediaInLobby = options?.allowMediaInLobby;
7821
7924
 
7925
+ // @ts-ignore
7926
+ const ipver = MeetingUtil.getIpVersion(this.webex); // used just for metrics
7927
+
7822
7928
  // If the user is unjoined or guest waiting in lobby dont allow the user to addMedia
7823
7929
  // @ts-ignore - isUserUnadmitted coming from SelfUtil
7824
7930
  if (this.isUserUnadmitted && !this.wirelessShare && !this.allowMediaInLobby) {
@@ -7917,6 +8023,7 @@ export default class Meeting extends StatelessWebexPlugin {
7917
8023
  locus_id: this.locusUrl.split('/').pop(),
7918
8024
  connectionType,
7919
8025
  ipVersion,
8026
+ ipver,
7920
8027
  selectedCandidatePairChanges,
7921
8028
  numTransports,
7922
8029
  isMultistream: this.isMultistream,
@@ -7985,6 +8092,7 @@ export default class Meeting extends StatelessWebexPlugin {
7985
8092
  ...reachabilityMetrics,
7986
8093
  ...iceCandidateErrors,
7987
8094
  iceCandidatesCount: this.iceCandidatesCount,
8095
+ ipver,
7988
8096
  });
7989
8097
 
7990
8098
  await this.cleanUpOnAddMediaFailure();
@@ -9361,6 +9469,36 @@ export default class Meeting extends StatelessWebexPlugin {
9361
9469
  return Promise.reject(new Error('Error sending reaction, service url not found.'));
9362
9470
  }
9363
9471
 
9472
+ /**
9473
+ * Extend the current meeting duration.
9474
+ *
9475
+ * @param {number} extensionMinutes - how many minutes to extend
9476
+ * @returns {Promise}
9477
+ * @public
9478
+ * @memberof Meeting
9479
+ */
9480
+ public extendMeeting({
9481
+ meetingPolicyUrl,
9482
+ meetingInstanceId,
9483
+ participantId,
9484
+ extensionMinutes = 30,
9485
+ }) {
9486
+ if (!meetingInstanceId || !participantId) {
9487
+ return Promise.reject(new Error('Missing meetingInstanceId or participantId'));
9488
+ }
9489
+
9490
+ if (!meetingPolicyUrl) {
9491
+ return Promise.reject(new Error('Missing meetingPolicyUrl'));
9492
+ }
9493
+
9494
+ return this.meetingRequest.extendMeeting({
9495
+ meetingInstanceId,
9496
+ participantId,
9497
+ extensionMinutes,
9498
+ meetingPolicyUrl,
9499
+ });
9500
+ }
9501
+
9364
9502
  /**
9365
9503
  * Method to enable or disable reactions inside the meeting.
9366
9504
  *
@@ -9890,4 +10028,47 @@ export default class Meeting extends StatelessWebexPlugin {
9890
10028
 
9891
10029
  return this.meetingRequest.synchronizeStage(this.locusUrl, videoLayout);
9892
10030
  }
10031
+
10032
+ /**
10033
+ * Notifies the host with the given meeting UUID and display names.
10034
+ *
10035
+ * @param {string} meetingUuid - The UUID of the meeting.
10036
+ * @param {string[]} displayName - An array of display names to notify the host with.
10037
+ * @returns {Promise<any>} The result of the notifyHost request.
10038
+ */
10039
+ notifyHost(meetingUuid: string, displayName: string[]) {
10040
+ return this.meetingRequest.notifyHost(
10041
+ this.meetingInfo.siteFullUrl,
10042
+ this.locusId,
10043
+ meetingUuid,
10044
+ displayName
10045
+ );
10046
+ }
10047
+
10048
+ /**
10049
+ * Call out a SIP participant to a meeting
10050
+ * @param {string} address - The SIP address or phone number
10051
+ * @param {string} displayName - The display name for the participant
10052
+ * @param {string} [correlationId] - Optional correlation ID
10053
+ * @returns {Promise} Promise that resolves when the call-out is initiated
10054
+ */
10055
+ sipCallOut(address: string, displayName: string) {
10056
+ return this.meetingRequest.sipCallOut(
10057
+ this.meetingInfo.meetingId,
10058
+ this.meetingInfo.meetingId,
10059
+ address,
10060
+ displayName
10061
+ );
10062
+ }
10063
+
10064
+ /**
10065
+ * Cancel an ongoing SIP call-out
10066
+ * @param {string} participantId - The participant ID to cancel
10067
+ * @returns {Promise} Promise that resolves when the call-out is cancelled
10068
+ * @public
10069
+ * @memberof Meetings
10070
+ */
10071
+ cancelSipCallOut(participantId: string) {
10072
+ return this.meetingRequest.cancelSipCallOut(participantId);
10073
+ }
9893
10074
  }
@@ -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;