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

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 (87) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +2 -0
  4. package/dist/constants.js.map +1 -1
  5. package/dist/interpretation/index.js +1 -1
  6. package/dist/interpretation/siLanguage.js +1 -1
  7. package/dist/locus-info/index.js +38 -10
  8. package/dist/locus-info/index.js.map +1 -1
  9. package/dist/locus-info/parser.js +4 -1
  10. package/dist/locus-info/parser.js.map +1 -1
  11. package/dist/media/properties.js +53 -5
  12. package/dist/media/properties.js.map +1 -1
  13. package/dist/meeting/in-meeting-actions.js +2 -0
  14. package/dist/meeting/in-meeting-actions.js.map +1 -1
  15. package/dist/meeting/index.js +189 -122
  16. package/dist/meeting/index.js.map +1 -1
  17. package/dist/meeting/muteState.js +2 -5
  18. package/dist/meeting/muteState.js.map +1 -1
  19. package/dist/meeting/request.js +25 -0
  20. package/dist/meeting/request.js.map +1 -1
  21. package/dist/meeting/util.js +30 -11
  22. package/dist/meeting/util.js.map +1 -1
  23. package/dist/meeting-info/meeting-info-v2.js +29 -21
  24. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  25. package/dist/meetings/index.js +31 -25
  26. package/dist/meetings/index.js.map +1 -1
  27. package/dist/member/types.js.map +1 -1
  28. package/dist/members/collection.js +13 -0
  29. package/dist/members/collection.js.map +1 -1
  30. package/dist/members/index.js +42 -20
  31. package/dist/members/index.js.map +1 -1
  32. package/dist/members/util.js +7 -2
  33. package/dist/members/util.js.map +1 -1
  34. package/dist/metrics/constants.js +2 -1
  35. package/dist/metrics/constants.js.map +1 -1
  36. package/dist/reachability/index.js +3 -3
  37. package/dist/reachability/index.js.map +1 -1
  38. package/dist/types/constants.d.ts +2 -0
  39. package/dist/types/locus-info/index.d.ts +54 -1
  40. package/dist/types/media/properties.d.ts +21 -0
  41. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  42. package/dist/types/meeting/index.d.ts +11 -1
  43. package/dist/types/meeting/request.d.ts +9 -0
  44. package/dist/types/meeting/util.d.ts +10 -3
  45. package/dist/types/meeting-info/meeting-info-v2.d.ts +6 -3
  46. package/dist/types/meetings/index.d.ts +3 -1
  47. package/dist/types/member/types.d.ts +1 -0
  48. package/dist/types/members/collection.d.ts +6 -0
  49. package/dist/types/members/index.d.ts +12 -2
  50. package/dist/types/members/util.d.ts +6 -3
  51. package/dist/types/metrics/constants.d.ts +1 -0
  52. package/dist/webinar/index.js +1 -1
  53. package/package.json +17 -17
  54. package/src/constants.ts +2 -0
  55. package/src/locus-info/index.ts +84 -9
  56. package/src/locus-info/parser.ts +5 -1
  57. package/src/media/properties.ts +43 -0
  58. package/src/meeting/in-meeting-actions.ts +4 -0
  59. package/src/meeting/index.ts +91 -4
  60. package/src/meeting/muteState.ts +2 -6
  61. package/src/meeting/request.ts +23 -0
  62. package/src/meeting/util.ts +41 -20
  63. package/src/meeting-info/meeting-info-v2.ts +24 -5
  64. package/src/meetings/index.ts +9 -3
  65. package/src/member/types.ts +1 -0
  66. package/src/members/collection.ts +11 -0
  67. package/src/members/index.ts +38 -5
  68. package/src/members/util.ts +18 -2
  69. package/src/metrics/constants.ts +1 -0
  70. package/src/reachability/index.ts +3 -3
  71. package/test/unit/spec/common/browser-detection.js +0 -24
  72. package/test/unit/spec/locus-info/index.js +30 -15
  73. package/test/unit/spec/locus-info/parser.js +3 -2
  74. package/test/unit/spec/media/properties.ts +137 -0
  75. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  76. package/test/unit/spec/meeting/index.js +255 -27
  77. package/test/unit/spec/meeting/muteState.js +32 -6
  78. package/test/unit/spec/meeting/request.js +21 -0
  79. package/test/unit/spec/meeting/utils.js +45 -16
  80. package/test/unit/spec/meeting-info/meetinginfov2.js +8 -3
  81. package/test/unit/spec/meetings/index.js +10 -5
  82. package/test/unit/spec/members/collection.js +120 -0
  83. package/test/unit/spec/members/index.js +72 -3
  84. package/test/unit/spec/members/request.js +55 -0
  85. package/test/unit/spec/members/utils.js +116 -14
  86. package/test/unit/spec/reachability/index.ts +158 -3
  87. package/test/unit/spec/roap/turnDiscovery.ts +3 -3
@@ -31,6 +31,51 @@ import LocusDeltaParser from './parser';
31
31
  import Metrics from '../metrics';
32
32
  import BEHAVIORAL_METRICS from '../metrics/constants';
33
33
 
34
+ export type LocusDTO = {
35
+ controls?: any;
36
+ fullState?: {
37
+ active: boolean;
38
+ count: number;
39
+ lastActive: string;
40
+ locked: boolean;
41
+ sessionId: string;
42
+ seessionIds: string[];
43
+ startTime: number;
44
+ state: string;
45
+ type: string;
46
+ };
47
+ host?: {
48
+ id: string;
49
+ incomingCallProtocols: any[];
50
+ isExternal: boolean;
51
+ name: string;
52
+ orgId: string;
53
+ };
54
+ info?: any;
55
+ links?: any;
56
+ mediaShares?: any[];
57
+ meetings?: any[];
58
+ participants: any[];
59
+ replaces?: any[];
60
+ self?: any;
61
+ sequence?: {
62
+ dirtyParticipants: number;
63
+ entries: number[];
64
+ rangeEnd: number;
65
+ rangeStart: number;
66
+ sequenceHash: number;
67
+ sessionToken: string;
68
+ since: string;
69
+ totalParticipants: number;
70
+ };
71
+ syncUrl?: string;
72
+ url?: string;
73
+ };
74
+
75
+ export type LocusApiResponseBody = {
76
+ locus: LocusDTO; // this LocusDTO here might not be the full one (for example it won't have all the participants, but it should have self)
77
+ };
78
+
34
79
  /**
35
80
  * @description LocusInfo extends ChildEmitter to convert locusInfo info a private emitter to parent object
36
81
  * @export
@@ -93,19 +138,26 @@ export default class LocusInfo extends EventsScope {
93
138
  * Does a Locus sync. It tries to get the latest delta DTO or if it can't, it falls back to getting the full Locus DTO.
94
139
  *
95
140
  * @param {Meeting} meeting
141
+ * @param {boolean} isLocusUrlChanged
142
+ * @param {Locus} locus
96
143
  * @returns {undefined}
97
144
  */
98
- private doLocusSync(meeting: any) {
99
- let isDelta;
145
+ private doLocusSync(meeting: any, isLocusUrlChanged: boolean, locus: any) {
100
146
  let url;
147
+ let isDelta = false;
101
148
  let meetingDestroyed = false;
102
149
 
103
- if (this.locusParser.workingCopy.syncUrl) {
150
+ if (isLocusUrlChanged) {
151
+ // for the locus url changed case from breakout to main session, we should always do a full sync, in this case, the url from locus is always on main session,
152
+ // so use the main session locus url to get the full locus(full participants list in the response).
153
+ // for the locus url changed case from main session to breakout, we don't need to care about it here,
154
+ // because it is a USE_INCOMING case, it will not be executed here.
155
+ url = locus.url;
156
+ } else if (this.locusParser.workingCopy?.syncUrl) {
104
157
  url = this.locusParser.workingCopy.syncUrl;
105
158
  isDelta = true;
106
159
  } else {
107
160
  url = meeting.locusUrl;
108
- isDelta = false;
109
161
  }
110
162
 
111
163
  LoggerProxy.logger.info(
@@ -217,6 +269,7 @@ export default class LocusInfo extends EventsScope {
217
269
  */
218
270
  applyLocusDeltaData(action: string, locus: any, meeting: any) {
219
271
  const {DESYNC, USE_CURRENT, USE_INCOMING, WAIT, LOCUS_URL_CHANGED} = LocusDeltaParser.loci;
272
+ const isLocusUrlChanged = action === LOCUS_URL_CHANGED;
220
273
 
221
274
  switch (action) {
222
275
  case USE_INCOMING:
@@ -228,7 +281,7 @@ export default class LocusInfo extends EventsScope {
228
281
  break;
229
282
  case DESYNC:
230
283
  case LOCUS_URL_CHANGED:
231
- this.doLocusSync(meeting);
284
+ this.doLocusSync(meeting, isLocusUrlChanged, locus);
232
285
  break;
233
286
  default:
234
287
  LoggerProxy.logger.info(
@@ -286,7 +339,7 @@ export default class LocusInfo extends EventsScope {
286
339
  this.updateLocusCache(locus);
287
340
  // above section only updates the locusInfo object
288
341
  // The below section makes sure it updates the locusInfo as well as updates the meeting object
289
- this.updateParticipants(locus.participants);
342
+ this.updateParticipants(locus.participants, []);
290
343
  // For 1:1 space meeting the conversation Url does not exist in locus.conversation
291
344
  this.updateConversationUrl(locus.conversationUrl, locus.info);
292
345
  this.updateControls(locus.controls, locus.self);
@@ -315,6 +368,16 @@ export default class LocusInfo extends EventsScope {
315
368
  this.emitChange = true;
316
369
  }
317
370
 
371
+ /**
372
+ * Handles HTTP response from Locus API call.
373
+ * @param {Meeting} meeting meeting object
374
+ * @param {LocusApiResponseBody} responseBody body of the http response from Locus API call
375
+ * @returns {void}
376
+ */
377
+ handleLocusAPIResponse(meeting, responseBody: LocusApiResponseBody): void {
378
+ this.handleLocusDelta(responseBody.locus, meeting);
379
+ }
380
+
318
381
  /**
319
382
  * @param {Meeting} meeting
320
383
  * @param {Object} data
@@ -327,6 +390,8 @@ export default class LocusInfo extends EventsScope {
327
390
  const locus = this.getTheLocusToUpdate(data.locus);
328
391
  LoggerProxy.logger.info(`Locus-info:index#parse --> received locus data: ${eventType}`);
329
392
 
393
+ locus.jsSdkMeta = {removedParticipantIds: []};
394
+
330
395
  switch (eventType) {
331
396
  case LOCUSEVENT.PARTICIPANT_JOIN:
332
397
  case LOCUSEVENT.PARTICIPANT_LEFT:
@@ -392,7 +457,11 @@ export default class LocusInfo extends EventsScope {
392
457
  this.participants = locus.participants;
393
458
  const isReplaceMembers = ControlsUtils.isNeedReplaceMembers(this.controls, locus.controls);
394
459
  this.updateLocusInfo(locus);
395
- this.updateParticipants(locus.participants, isReplaceMembers);
460
+ this.updateParticipants(
461
+ locus.participants,
462
+ locus.jsSdkMeta?.removedParticipantIds,
463
+ isReplaceMembers
464
+ );
396
465
  this.isMeetingActive();
397
466
  this.handleOneOnOneEvent(eventType);
398
467
  this.updateEmbeddedApps(locus.embeddedApps);
@@ -454,7 +523,11 @@ export default class LocusInfo extends EventsScope {
454
523
  const isReplaceMembers = ControlsUtils.isNeedReplaceMembers(this.controls, locus.controls);
455
524
  this.mergeParticipants(this.participants, locus.participants);
456
525
  this.updateLocusInfo(locus);
457
- this.updateParticipants(locus.participants, isReplaceMembers);
526
+ this.updateParticipants(
527
+ locus.participants,
528
+ locus.jsSdkMeta?.removedParticipantIds,
529
+ isReplaceMembers
530
+ );
458
531
  this.isMeetingActive();
459
532
  }
460
533
 
@@ -745,11 +818,12 @@ export default class LocusInfo extends EventsScope {
745
818
  /**
746
819
  * update meeting's members
747
820
  * @param {Object} participants new participants object
821
+ * @param {Array} removedParticipantIds list of removed participants
748
822
  * @param {Boolean} isReplace is replace the whole members
749
823
  * @returns {Array} updatedParticipants
750
824
  * @memberof LocusInfo
751
825
  */
752
- updateParticipants(participants: object, isReplace?: boolean) {
826
+ updateParticipants(participants: object, removedParticipantIds?: string[], isReplace?: boolean) {
753
827
  this.emitScoped(
754
828
  {
755
829
  file: 'locus-info',
@@ -758,6 +832,7 @@ export default class LocusInfo extends EventsScope {
758
832
  EVENTS.LOCUS_INFO_UPDATE_PARTICIPANTS,
759
833
  {
760
834
  participants,
835
+ removedParticipantIds,
761
836
  recordingId: this.parsedLocus.controls && this.parsedLocus.controls.record?.modifiedBy,
762
837
  selfIdentity: this.parsedLocus.self && this.parsedLocus.self.selfIdentity,
763
838
  selfId: this.parsedLocus.self && this.parsedLocus.self.selfId,
@@ -728,13 +728,17 @@ export default class Parser {
728
728
  break;
729
729
 
730
730
  case USE_INCOMING:
731
- case LOCUS_URL_CHANGED:
732
731
  // update working copy for future comparisons.
733
732
  // Note: The working copy of parser gets updated in .onFullLocus()
734
733
  // and here when USE_INCOMING or LOCUS_URL_CHANGED locus.
735
734
  this.workingCopy = newLoci;
736
735
  break;
737
736
 
737
+ case LOCUS_URL_CHANGED:
738
+ // clear the working copy completely, do a full locus sync
739
+ this.workingCopy = null;
740
+ break;
741
+
738
742
  case WAIT:
739
743
  // we've taken newLoci from the front of the queue, so put it back there as we have to wait
740
744
  // for the one that should be in front of it, before we can process it
@@ -9,9 +9,12 @@ import {
9
9
 
10
10
  import {parse} from '@webex/ts-sdp';
11
11
  import {ClientEvent} from '@webex/internal-plugin-metrics';
12
+ import {throttle} from 'lodash';
13
+ import Metrics from '../metrics';
12
14
  import {MEETINGS, QUALITY_LEVELS} from '../constants';
13
15
  import LoggerProxy from '../common/logs/logger-proxy';
14
16
  import MediaConnectionAwaiter from './MediaConnectionAwaiter';
17
+ import BEHAVIORAL_METRICS from '../metrics/constants';
15
18
 
16
19
  export type MediaDirection = {
17
20
  sendAudio: boolean;
@@ -41,6 +44,8 @@ export default class MediaProperties {
41
44
  videoDeviceId: any;
42
45
  videoStream?: LocalCameraStream;
43
46
  namespace = MEETINGS;
47
+ mediaIssueCounters: {[key: string]: number} = {};
48
+ throttledSendMediaIssueMetric: ReturnType<typeof throttle>;
44
49
 
45
50
  /**
46
51
  * @param {Object} [options] -- to auto construct
@@ -66,6 +71,15 @@ export default class MediaProperties {
66
71
  this.remoteQualityLevel = QUALITY_LEVELS.HIGH;
67
72
  this.mediaSettings = {};
68
73
  this.videoDeviceId = null;
74
+
75
+ this.throttledSendMediaIssueMetric = throttle((eventPayload) => {
76
+ Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.MEDIA_ISSUE_DETECTED, {
77
+ ...eventPayload,
78
+ });
79
+ Object.keys(this.mediaIssueCounters).forEach((key) => {
80
+ this.mediaIssueCounters[key] = 0;
81
+ });
82
+ }, 1000 * 60 * 5); // at most once every 5 minutes
69
83
  }
70
84
 
71
85
  /**
@@ -139,8 +153,14 @@ export default class MediaProperties {
139
153
  this.videoDeviceId = deviceId;
140
154
  }
141
155
 
156
+ /**
157
+ * Clears the webrtcMediaConnection. This method should be called after
158
+ * peer connection is closed and no longer needed.
159
+ * @returns {void}
160
+ */
142
161
  unsetPeerConnection() {
143
162
  this.webrtcMediaConnection = null;
163
+ this.throttledSendMediaIssueMetric.flush();
144
164
  }
145
165
 
146
166
  /**
@@ -424,4 +444,27 @@ export default class MediaProperties {
424
444
  };
425
445
  }
426
446
  }
447
+
448
+ /**
449
+ * Sends a metric about a media issue. Metrics are throttled so that we don't
450
+ * send too many of them, but include a count so that we know how many issues
451
+ * were detected.
452
+ *
453
+ * @param {string} issueType
454
+ * @param {string} issueSubType
455
+ * @param {string} correlationId
456
+ * @returns {void}
457
+ */
458
+ public sendMediaIssueMetric(issueType: string, issueSubType: string, correlationId) {
459
+ const key = `${issueType}_${issueSubType}`;
460
+
461
+ const count = (this.mediaIssueCounters[key] || 0) + 1;
462
+
463
+ this.mediaIssueCounters[key] = count;
464
+
465
+ this.throttledSendMediaIssueMetric({
466
+ correlationId,
467
+ ...this.mediaIssueCounters,
468
+ });
469
+ }
427
470
  }
@@ -44,6 +44,7 @@ interface IInMeetingActions {
44
44
 
45
45
  isManualCaptionActive?: boolean;
46
46
  isSaveTranscriptsEnabled?: boolean;
47
+ isSpokenLanguageAutoDetectionEnabled?: boolean;
47
48
  isWebexAssistantActive?: boolean;
48
49
  canViewCaptionPanel?: boolean;
49
50
  isRealTimeTranslationEnabled?: boolean;
@@ -187,6 +188,8 @@ export default class InMeetingActions implements IInMeetingActions {
187
188
 
188
189
  isSaveTranscriptsEnabled = null;
189
190
 
191
+ isSpokenLanguageAutoDetectionEnabled = null;
192
+
190
193
  isWebexAssistantActive = null;
191
194
 
192
195
  canViewCaptionPanel = null;
@@ -363,6 +366,7 @@ export default class InMeetingActions implements IInMeetingActions {
363
366
  canStopManualCaption: this.canStopManualCaption,
364
367
  isManualCaptionActive: this.isManualCaptionActive,
365
368
  isSaveTranscriptsEnabled: this.isSaveTranscriptsEnabled,
369
+ isSpokenLanguageAutoDetectionEnabled: this.isSpokenLanguageAutoDetectionEnabled,
366
370
  isWebexAssistantActive: this.isWebexAssistantActive,
367
371
  canViewCaptionPanel: this.canViewCaptionPanel,
368
372
  isRealTimeTranslationEnabled: this.isRealTimeTranslationEnabled,
@@ -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);
@@ -3184,6 +3198,14 @@ export default class Meeting extends StatelessWebexPlugin {
3184
3198
  this.shareCAEventSentStatus.receiveStart = false;
3185
3199
  this.shareCAEventSentStatus.receiveStop = false;
3186
3200
 
3201
+ let finalBeneficiaryId = contentShare.beneficiaryId;
3202
+ // In case of attendee in webinar, the whiteboard is shared by other participants
3203
+ if (this.locusInfo?.info?.isWebinar && this.webinar?.selfIsAttendee) {
3204
+ if (!finalBeneficiaryId && whiteboardShare.beneficiaryId) {
3205
+ finalBeneficiaryId = whiteboardShare.beneficiaryId;
3206
+ }
3207
+ }
3208
+
3187
3209
  Trigger.trigger(
3188
3210
  this,
3189
3211
  {
@@ -3192,7 +3214,7 @@ export default class Meeting extends StatelessWebexPlugin {
3192
3214
  },
3193
3215
  EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
3194
3216
  {
3195
- memberId: contentShare.beneficiaryId,
3217
+ memberId: finalBeneficiaryId,
3196
3218
  url: contentShare.url,
3197
3219
  shareInstanceId: this.remoteShareInstanceId,
3198
3220
  annotationInfo: contentShare.annotation,
@@ -4214,6 +4236,9 @@ export default class Meeting extends StatelessWebexPlugin {
4214
4236
  isLocalRecordingPaused: MeetingUtil.isLocalRecordingPaused(this.userDisplayHints),
4215
4237
  isManualCaptionActive: MeetingUtil.isManualCaptionActive(this.userDisplayHints),
4216
4238
  isSaveTranscriptsEnabled: MeetingUtil.isSaveTranscriptsEnabled(this.userDisplayHints),
4239
+ isSpokenLanguageAutoDetectionEnabled: MeetingUtil.isSpokenLanguageAutoDetectionEnabled(
4240
+ this.userDisplayHints
4241
+ ),
4217
4242
  isWebexAssistantActive: MeetingUtil.isWebexAssistantActive(this.userDisplayHints),
4218
4243
  canViewCaptionPanel: MeetingUtil.canViewCaptionPanel(this.userDisplayHints),
4219
4244
  isRealTimeTranslationEnabled: MeetingUtil.isRealTimeTranslationEnabled(
@@ -6796,6 +6821,10 @@ export default class Meeting extends StatelessWebexPlugin {
6796
6821
  // @ts-ignore
6797
6822
  this.webex.internal.newMetrics.submitClientEvent({
6798
6823
  name: 'client.ice.start',
6824
+ payload: {
6825
+ // @ts-ignore
6826
+ labels: MeetingUtil.getCaEventLabelsForIpVersion(this.webex),
6827
+ },
6799
6828
  options: {
6800
6829
  meetingId: this.id,
6801
6830
  },
@@ -6965,10 +6994,10 @@ export default class Meeting extends StatelessWebexPlugin {
6965
6994
  }
6966
6995
  }
6967
6996
 
6968
- // Count members that are in the meeting.
6997
+ // Count members that are in the meeting or in the lobby.
6969
6998
  const {members} = this.getMembers().membersCollection;
6970
6999
  event.data.intervalMetadata.meetingUserCount = Object.values(members).filter(
6971
- (member: Member) => member.isInMeeting
7000
+ (member: Member) => member.isInMeeting || member.isInLobby
6972
7001
  ).length;
6973
7002
 
6974
7003
  // @ts-ignore
@@ -7327,10 +7356,12 @@ export default class Meeting extends StatelessWebexPlugin {
7327
7356
  if (this.config.stats.enableStatsAnalyzer) {
7328
7357
  // @ts-ignore - config coming from registerPlugin
7329
7358
  this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
7359
+ this.statsMonitor = new StatsMonitor();
7330
7360
  this.statsAnalyzer = new StatsAnalyzer({
7331
7361
  // @ts-ignore - config coming from registerPlugin
7332
7362
  config: this.config.stats,
7333
7363
  networkQualityMonitor: this.networkQualityMonitor,
7364
+ statsMonitor: this.statsMonitor,
7334
7365
  isMultistream: this.isMultistream,
7335
7366
  });
7336
7367
  this.shareCAEventSentStatus = {
@@ -7344,6 +7375,33 @@ export default class Meeting extends StatelessWebexPlugin {
7344
7375
  NetworkQualityEventNames.NETWORK_QUALITY,
7345
7376
  this.sendNetworkQualityEvent.bind(this)
7346
7377
  );
7378
+
7379
+ this.statsMonitor.on(StatsMonitorEventNames.INBOUND_AUDIO_ISSUE, (data) => {
7380
+ // Before forwarding any inbound audio issues to the app, make sure that we have at least one other
7381
+ // participant in the meeting with unmuted audio.
7382
+ // We don't check this.mediaProperties.mediaDirection here, because that's already handled in statsAnalyzer,
7383
+ // so we won't get this event if we are not setup to receive any audio
7384
+ const atLeastOneUnmutedOtherMember = Object.values(
7385
+ this.members.membersCollection.getAll()
7386
+ ).find((member) => {
7387
+ return !member.isSelf && !member.isPairedWithSelf && !member.isAudioMuted;
7388
+ });
7389
+
7390
+ if (atLeastOneUnmutedOtherMember) {
7391
+ this.mediaProperties.sendMediaIssueMetric(
7392
+ 'inbound_audio',
7393
+ data.issueSubType,
7394
+ this.correlationId
7395
+ );
7396
+
7397
+ Trigger.trigger(
7398
+ this,
7399
+ {file: 'meeting/index', function: 'createStatsAnalyzer'},
7400
+ EVENT_TRIGGERS.MEDIA_INBOUND_AUDIO_ISSUE_DETECTED,
7401
+ data
7402
+ );
7403
+ }
7404
+ });
7347
7405
  }
7348
7406
  }
7349
7407
 
@@ -7642,6 +7700,10 @@ export default class Meeting extends StatelessWebexPlugin {
7642
7700
  }
7643
7701
 
7644
7702
  this.statsAnalyzer = null;
7703
+ this.networkQualityMonitor?.removeAllListeners();
7704
+ this.networkQualityMonitor = null;
7705
+ this.statsMonitor?.removeAllListeners();
7706
+ this.statsMonitor = null;
7645
7707
 
7646
7708
  // when media fails, we want to upload a webrtc dump to see whats going on
7647
7709
  // this function is async, but returns once the stats have been gathered
@@ -7665,6 +7727,10 @@ export default class Meeting extends StatelessWebexPlugin {
7665
7727
  await this.statsAnalyzer.stopAnalyzer();
7666
7728
  }
7667
7729
  this.statsAnalyzer = null;
7730
+ this.networkQualityMonitor?.removeAllListeners();
7731
+ this.networkQualityMonitor = null;
7732
+ this.statsMonitor?.removeAllListeners();
7733
+ this.statsMonitor = null;
7668
7734
 
7669
7735
  this.isMultistream = false;
7670
7736
 
@@ -7836,6 +7902,9 @@ export default class Meeting extends StatelessWebexPlugin {
7836
7902
 
7837
7903
  this.allowMediaInLobby = options?.allowMediaInLobby;
7838
7904
 
7905
+ // @ts-ignore
7906
+ const ipver = MeetingUtil.getIpVersion(this.webex); // used just for metrics
7907
+
7839
7908
  // If the user is unjoined or guest waiting in lobby dont allow the user to addMedia
7840
7909
  // @ts-ignore - isUserUnadmitted coming from SelfUtil
7841
7910
  if (this.isUserUnadmitted && !this.wirelessShare && !this.allowMediaInLobby) {
@@ -7934,6 +8003,7 @@ export default class Meeting extends StatelessWebexPlugin {
7934
8003
  locus_id: this.locusUrl.split('/').pop(),
7935
8004
  connectionType,
7936
8005
  ipVersion,
8006
+ ipver,
7937
8007
  selectedCandidatePairChanges,
7938
8008
  numTransports,
7939
8009
  isMultistream: this.isMultistream,
@@ -8002,6 +8072,7 @@ export default class Meeting extends StatelessWebexPlugin {
8002
8072
  ...reachabilityMetrics,
8003
8073
  ...iceCandidateErrors,
8004
8074
  iceCandidatesCount: this.iceCandidatesCount,
8075
+ ipver,
8005
8076
  });
8006
8077
 
8007
8078
  await this.cleanUpOnAddMediaFailure();
@@ -9907,4 +9978,20 @@ export default class Meeting extends StatelessWebexPlugin {
9907
9978
 
9908
9979
  return this.meetingRequest.synchronizeStage(this.locusUrl, videoLayout);
9909
9980
  }
9981
+
9982
+ /**
9983
+ * Notifies the host with the given meeting UUID and display names.
9984
+ *
9985
+ * @param {string} meetingUuid - The UUID of the meeting.
9986
+ * @param {string[]} displayName - An array of display names to notify the host with.
9987
+ * @returns {Promise<any>} The result of the notifyHost request.
9988
+ */
9989
+ notifyHost(meetingUuid: string, displayName: string[]) {
9990
+ return this.meetingRequest.notifyHost(
9991
+ this.meetingInfo.siteFullUrl,
9992
+ this.locusId,
9993
+ meetingUuid,
9994
+ displayName
9995
+ );
9996
+ }
9910
9997
  }
@@ -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(
@@ -985,4 +985,27 @@ export default class MeetingRequest extends StatelessWebexPlugin {
985
985
  body: {videoLayout},
986
986
  });
987
987
  }
988
+
989
+ /**
990
+ * Sends a request to notify the host of a meeting.
991
+ * @param {string} siteFullUrl - The site URL.
992
+ * @param {string} locusId - The locus ID.
993
+ * @param {string} meetingUuid - The meeting UUID.
994
+ * @param {Array<string>} displayName - The display names to notify the host about.
995
+ * @returns {Promise}
996
+ */
997
+ notifyHost(siteFullUrl: string, locusId: string, meetingUuid: string, displayName: string[]) {
998
+ // @ts-ignore
999
+ return this.request({
1000
+ method: HTTP_VERBS.POST,
1001
+ uri: `https://${siteFullUrl}/wbxappapi/v1/meetings/${meetingUuid}/notifyhost`,
1002
+ body: {
1003
+ displayName,
1004
+ size: displayName?.length,
1005
+ },
1006
+ headers: {
1007
+ locusId,
1008
+ },
1009
+ });
1010
+ }
988
1011
  }
@@ -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.'));
@@ -618,6 +638,9 @@ const MeetingUtil = {
618
638
  isManualCaptionActive: (displayHints) =>
619
639
  displayHints.includes(DISPLAY_HINTS.MANUAL_CAPTION_STATUS_ACTIVE),
620
640
 
641
+ isSpokenLanguageAutoDetectionEnabled: (displayHints) =>
642
+ displayHints.includes(DISPLAY_HINTS.SPOKEN_LANGUAGE_AUTO_DETECTION_ENABLED),
643
+
621
644
  isWebexAssistantActive: (displayHints) =>
622
645
  displayHints.includes(DISPLAY_HINTS.WEBEX_ASSISTANT_STATUS_ACTIVE),
623
646
 
@@ -673,22 +696,20 @@ const MeetingUtil = {
673
696
  },
674
697
 
675
698
  /**
676
- * Updates the locus info for the meeting with the delta locus
677
- * returned from requests that include the sequence information
699
+ * Updates the locus info for the meeting with the locus
700
+ * information returned from API requests made to Locus
678
701
  * Returns the original response object
679
702
  * @param {Object} meeting The meeting object
680
703
  * @param {Object} response The response of the http request
681
704
  * @returns {Object}
682
705
  */
683
- updateLocusWithDelta: (meeting, response) => {
706
+ updateLocusFromApiResponse: (meeting, response) => {
684
707
  if (!meeting) {
685
708
  return response;
686
709
  }
687
710
 
688
- const locus = response?.body?.locus;
689
-
690
- if (locus) {
691
- meeting.locusInfo.handleLocusDelta(locus, meeting);
711
+ if (response?.body?.locus) {
712
+ meeting.locusInfo.handleLocusAPIResponse(meeting, response.body);
692
713
  }
693
714
 
694
715
  return response;
@@ -735,7 +756,7 @@ const MeetingUtil = {
735
756
 
736
757
  return meeting
737
758
  .request(options)
738
- .then((response) => MeetingUtil.updateLocusWithDelta(meeting, response));
759
+ .then((response) => MeetingUtil.updateLocusFromApiResponse(meeting, response));
739
760
  };
740
761
 
741
762
  return locusDeltaRequest;