@webex/plugin-meetings 2.60.0-next.1 → 2.60.0-next.3

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 (62) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +1 -1
  4. package/dist/constants.js.map +1 -1
  5. package/dist/controls-options-manager/enums.js +2 -1
  6. package/dist/controls-options-manager/enums.js.map +1 -1
  7. package/dist/interpretation/index.js +1 -1
  8. package/dist/interpretation/siLanguage.js +1 -1
  9. package/dist/locus-info/parser.js +5 -5
  10. package/dist/locus-info/parser.js.map +1 -1
  11. package/dist/media/index.js +6 -5
  12. package/dist/media/index.js.map +1 -1
  13. package/dist/meeting/in-meeting-actions.js +4 -0
  14. package/dist/meeting/in-meeting-actions.js.map +1 -1
  15. package/dist/meeting/index.js +276 -150
  16. package/dist/meeting/index.js.map +1 -1
  17. package/dist/meeting-info/meeting-info-v2.js +3 -0
  18. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  19. package/dist/meeting-info/utilv2.js +14 -29
  20. package/dist/meeting-info/utilv2.js.map +1 -1
  21. package/dist/meetings/collection.js +17 -0
  22. package/dist/meetings/collection.js.map +1 -1
  23. package/dist/meetings/index.js +30 -9
  24. package/dist/meetings/index.js.map +1 -1
  25. package/dist/metrics/constants.js +3 -0
  26. package/dist/metrics/constants.js.map +1 -1
  27. package/dist/reconnection-manager/index.js +27 -28
  28. package/dist/reconnection-manager/index.js.map +1 -1
  29. package/dist/rtcMetrics/index.js +25 -0
  30. package/dist/rtcMetrics/index.js.map +1 -1
  31. package/dist/statsAnalyzer/index.js +21 -1
  32. package/dist/statsAnalyzer/index.js.map +1 -1
  33. package/dist/statsAnalyzer/mqaUtil.js +16 -16
  34. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  35. package/dist/webinar/index.js +1 -1
  36. package/package.json +21 -22
  37. package/src/constants.ts +10 -4
  38. package/src/controls-options-manager/enums.ts +2 -0
  39. package/src/locus-info/parser.ts +6 -6
  40. package/src/media/index.ts +5 -5
  41. package/src/meeting/in-meeting-actions.ts +8 -0
  42. package/src/meeting/index.ts +249 -120
  43. package/src/meeting-info/meeting-info-v2.ts +4 -0
  44. package/src/meeting-info/utilv2.ts +6 -19
  45. package/src/meetings/collection.ts +13 -0
  46. package/src/meetings/index.ts +28 -10
  47. package/src/metrics/constants.ts +3 -0
  48. package/src/reconnection-manager/index.ts +63 -68
  49. package/src/rtcMetrics/index.ts +24 -0
  50. package/src/statsAnalyzer/index.ts +30 -1
  51. package/src/statsAnalyzer/mqaUtil.ts +17 -14
  52. package/test/unit/spec/media/index.ts +20 -4
  53. package/test/unit/spec/meeting/in-meeting-actions.ts +4 -0
  54. package/test/unit/spec/meeting/index.js +1253 -157
  55. package/test/unit/spec/meeting/muteState.js +2 -1
  56. package/test/unit/spec/meeting-info/meetinginfov2.js +28 -0
  57. package/test/unit/spec/meetings/collection.js +12 -0
  58. package/test/unit/spec/meetings/index.js +382 -118
  59. package/test/unit/spec/member/util.js +0 -31
  60. package/test/unit/spec/reconnection-manager/index.js +42 -12
  61. package/test/unit/spec/rtcMetrics/index.ts +20 -0
  62. package/test/unit/spec/stats-analyzer/index.js +12 -2
@@ -216,6 +216,10 @@ export default class MeetingInfoV2 {
216
216
  installedOrgID,
217
217
  };
218
218
 
219
+ if (installedOrgID) {
220
+ body.installedOrgID = installedOrgID;
221
+ }
222
+
219
223
  const uri = this.webex.meetings.preferredWebexSite
220
224
  ? `https://${this.webex.meetings.preferredWebexSite}/wbxappapi/v2/meetings/spaceInstant`
221
225
  : '';
@@ -203,25 +203,12 @@ MeetingInfoUtil.getDestinationType = async (from) => {
203
203
  return Promise.resolve(options);
204
204
  }
205
205
  );
206
- } else if (hydraId && hydraId.room) {
207
- options.type = _CONVERSATION_URL_;
208
- try {
209
- await webex.internal.services.waitForCatalog('postauth');
210
-
211
- const serviceUrl = webex.internal.services.getServiceUrlFromClusterId(
212
- {
213
- cluster: hydraId.cluster,
214
- },
215
- webex
216
- );
217
-
218
- options.destination = hydraId.destination
219
- ? `${serviceUrl}/conversations/${hydraId.destination}`
220
- : serviceUrl;
221
- } catch (e) {
222
- LoggerProxy.logger.error(`Meeting-info:util#getDestinationType --> ${e}`);
223
- throw e;
224
- }
206
+ } else if (hydraId.room) {
207
+ LoggerProxy.logger.error(
208
+ `Meeting-info:util#getDestinationType --> Using the space ID as a destination is no longer supported. Please refer to the [migration guide](https://github.com/webex/webex-js-sdk/wiki/Migration-to-Unified-Space-Meetings) to migrate to use the meeting ID or SIP address.`
209
+ );
210
+ // Error code 30105 added as Space ID deprecated as of beta, Please refer migration guide.
211
+ throw new SpaceIDDeprecatedError();
225
212
  } else {
226
213
  LoggerProxy.logger.warn(`Meeting-info:util#getDestinationType --> ${meetingInfoError}`);
227
214
  throw new ParameterError(`${meetingInfoError}`);
@@ -60,4 +60,17 @@ export default class MeetingCollection extends Collection {
60
60
 
61
61
  return null;
62
62
  }
63
+
64
+ /**
65
+ * Gets the meeting that has a webrtc media connection
66
+ * NOTE: this function assumes there is no more than 1 such meeting
67
+ *
68
+ * @returns {Meeting} first meeting found, else undefined
69
+ * @public
70
+ * @memberof MeetingCollection
71
+ */
72
+ public getActiveWebrtcMeeting() {
73
+ // @ts-ignore
74
+ return find(this.meetings, (meeting) => meeting.mediaProperties.webrtcMediaConnection);
75
+ }
63
76
  }
@@ -49,7 +49,7 @@ import {
49
49
  import BEHAVIORAL_METRICS from '../metrics/constants';
50
50
  import MeetingInfo from '../meeting-info';
51
51
  import MeetingInfoV2 from '../meeting-info/meeting-info-v2';
52
- import Meeting from '../meeting';
52
+ import Meeting, {CallStateForMetrics} from '../meeting';
53
53
  import PersonalMeetingRoom from '../personal-meeting-room';
54
54
  import Reachability from '../reachability';
55
55
  import Request from './request';
@@ -1025,13 +1025,14 @@ export default class Meetings extends WebexPlugin {
1025
1025
  }
1026
1026
 
1027
1027
  /**
1028
- * Create a meeting.
1028
+ * Create a meeting or return an existing meeting.
1029
1029
  * @param {string} destination - sipURL, phonenumber, or locus object}
1030
1030
  * @param {string} [type] - the optional specified type, such as locusId
1031
1031
  * @param {Boolean} useRandomDelayForInfo - whether a random delay should be added to fetching meeting info
1032
1032
  * @param {Object} infoExtraParams extra parameters to be provided when fetching meeting info
1033
- * @param {string} correlationId - the optional specified correlationId
1033
+ * @param {string} correlationId - the optional specified correlationId (callStateForMetrics.correlationId can be provided instead)
1034
1034
  * @param {Boolean} failOnMissingMeetingInfo - whether to throw an error if meeting info fails to fetch (for calls that are not 1:1 or content share)
1035
+ * @param {CallStateForMetrics} callStateForMetrics - information about call state for metrics
1035
1036
  * @returns {Promise<Meeting>} A new Meeting.
1036
1037
  * @public
1037
1038
  * @memberof Meetings
@@ -1042,7 +1043,8 @@ export default class Meetings extends WebexPlugin {
1042
1043
  useRandomDelayForInfo = false,
1043
1044
  infoExtraParams = {},
1044
1045
  correlationId: string = undefined,
1045
- failOnMissingMeetingInfo = false
1046
+ failOnMissingMeetingInfo = false,
1047
+ callStateForMetrics: CallStateForMetrics = undefined
1046
1048
  ) {
1047
1049
  // TODO: type should be from a dictionary
1048
1050
 
@@ -1050,6 +1052,10 @@ export default class Meetings extends WebexPlugin {
1050
1052
  // type. This must be performed prior to determining if the meeting is
1051
1053
  // found in the collection, as we mutate the destination for hydra person
1052
1054
  // id values.
1055
+ if (correlationId) {
1056
+ callStateForMetrics = {...(callStateForMetrics || {}), correlationId};
1057
+ }
1058
+
1053
1059
  return (
1054
1060
  this.meetingInfo
1055
1061
  .fetchInfoOptions(destination, type)
@@ -1096,7 +1102,7 @@ export default class Meetings extends WebexPlugin {
1096
1102
  type,
1097
1103
  useRandomDelayForInfo,
1098
1104
  infoExtraParams,
1099
- correlationId,
1105
+ callStateForMetrics,
1100
1106
  failOnMissingMeetingInfo
1101
1107
  ).then((createdMeeting: any) => {
1102
1108
  // If the meeting was successfully created.
@@ -1143,6 +1149,7 @@ export default class Meetings extends WebexPlugin {
1143
1149
  return Promise.resolve(createdMeeting);
1144
1150
  });
1145
1151
  }
1152
+ meeting.setCallStateForMetrics(callStateForMetrics);
1146
1153
 
1147
1154
  // Return the existing meeting.
1148
1155
  return Promise.resolve(meeting);
@@ -1155,7 +1162,7 @@ export default class Meetings extends WebexPlugin {
1155
1162
  * @param {String} type see create()
1156
1163
  * @param {Boolean} useRandomDelayForInfo whether a random delay should be added to fetching meeting info
1157
1164
  * @param {Object} infoExtraParams extra parameters to be provided when fetching meeting info
1158
- * @param {String} correlationId the optional specified correlationId
1165
+ * @param {CallStateForMetrics} callStateForMetrics - information about call state for metrics
1159
1166
  * @param {Boolean} failOnMissingMeetingInfo - whether to throw an error if meeting info fails to fetch (for calls that are not 1:1 or content share)
1160
1167
  * @returns {Promise} a new meeting instance complete with meeting info and destination
1161
1168
  * @private
@@ -1166,7 +1173,7 @@ export default class Meetings extends WebexPlugin {
1166
1173
  type: string = null,
1167
1174
  useRandomDelayForInfo = false,
1168
1175
  infoExtraParams = {},
1169
- correlationId: string = undefined,
1176
+ callStateForMetrics: CallStateForMetrics = undefined,
1170
1177
  failOnMissingMeetingInfo = false
1171
1178
  ) {
1172
1179
  const meeting = new Meeting(
@@ -1181,7 +1188,7 @@ export default class Meetings extends WebexPlugin {
1181
1188
  meetingInfoProvider: this.meetingInfo,
1182
1189
  destination,
1183
1190
  destinationType: type,
1184
- correlationId,
1191
+ callStateForMetrics,
1185
1192
  },
1186
1193
  {
1187
1194
  // @ts-ignore
@@ -1219,7 +1226,7 @@ export default class Meetings extends WebexPlugin {
1219
1226
  () =>
1220
1227
  meeting.fetchMeetingInfo({
1221
1228
  extraParams: infoExtraParams,
1222
- sendCAevents: !!correlationId, // if client sends correlation id as argument of public create(), then it means that this meeting creation is part of a pre-join intent from user
1229
+ sendCAevents: !!callStateForMetrics?.correlationId, // if client sends correlation id as argument of public create(), then it means that this meeting creation is part of a pre-join intent from user
1223
1230
  }),
1224
1231
  waitingTime
1225
1232
  );
@@ -1227,7 +1234,7 @@ export default class Meetings extends WebexPlugin {
1227
1234
  } else {
1228
1235
  await meeting.fetchMeetingInfo({
1229
1236
  extraParams: infoExtraParams,
1230
- sendCAevents: !!correlationId, // if client sends correlation id as argument of public create(), then it means that this meeting creation is part of a pre-join intent from user
1237
+ sendCAevents: !!callStateForMetrics?.correlationId, // if client sends correlation id as argument of public create(), then it means that this meeting creation is part of a pre-join intent from user
1231
1238
  });
1232
1239
  }
1233
1240
  } catch (err) {
@@ -1467,4 +1474,15 @@ export default class Meetings extends WebexPlugin {
1467
1474
  getLogger() {
1468
1475
  return LoggerProxy.get();
1469
1476
  }
1477
+
1478
+ /**
1479
+ * Returns the first meeting it finds that has the webrtc media connection created.
1480
+ * Useful for debugging in the console.
1481
+ *
1482
+ * @private
1483
+ * @returns {Meeting} Meeting object that has a webrtc media connection, else undefined
1484
+ */
1485
+ getActiveWebrtcMeeting() {
1486
+ return this.meetingCollection.getActiveWebrtcMeeting();
1487
+ }
1470
1488
  }
@@ -9,6 +9,7 @@ const BEHAVIORAL_METRICS = {
9
9
  JOIN_FAILURE: 'js_sdk_join_failures',
10
10
  ADD_MEDIA_SUCCESS: 'js_sdk_add_media_success',
11
11
  ADD_MEDIA_FAILURE: 'js_sdk_add_media_failures',
12
+ ADD_MEDIA_RETRY: 'js_sdk_add_media_retry',
12
13
  ROAP_MERCURY_EVENT_RECEIVED: 'js_sdk_roap_mercury_received',
13
14
  CONNECTION_SUCCESS: 'js_sdk_connection_success',
14
15
  CONNECTION_FAILURE: 'js_sdk_connection_failures',
@@ -25,9 +26,11 @@ const BEHAVIORAL_METRICS = {
25
26
  MEETING_MEDIA_INACTIVE: 'js_sdk_meeting_media_inactive',
26
27
  MEETING_RECONNECT_FAILURE: 'js_sdk_meeting_reconnect_failures',
27
28
  MEETING_MAX_REJOIN_FAILURE: 'js_sdk_meeting_max_rejoin_failure',
29
+ MEETING_SHARE_SUCCESS: 'js_sdk_meeting_share_success',
28
30
  MEETING_SHARE_FAILURE: 'js_sdk_meeting_share_failures',
29
31
  MEETING_START_WHITEBOARD_SHARE_FAILURE: 'js_sdk_meeting_start_whiteboard_share_failures',
30
32
  MEETING_STOP_WHITEBOARD_SHARE_FAILURE: 'js_sdk_meeting_stop_whiteboard_share_failures',
33
+ MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE: 'js_sdk_meeting_share_video_mute_state_change',
31
34
  MUTE_AUDIO_FAILURE: 'js_sdk_mute_audio_failures',
32
35
  MUTE_VIDEO_FAILURE: 'js_sdk_mute_video_failures',
33
36
  SET_MEETING_QUALITY_FAILURE: 'js_sdk_set_meeting_quality_failures',
@@ -14,13 +14,14 @@ import {
14
14
  _CALL_,
15
15
  _LEFT_,
16
16
  _ID_,
17
+ RECONNECTION_STATE,
17
18
  } from '../constants';
18
19
  import BEHAVIORAL_METRICS from '../metrics/constants';
19
- import ReconnectionError from '../common/errors/reconnection';
20
20
  import ReconnectInProgress from '../common/errors/reconnection-in-progress';
21
21
  import Metrics from '../metrics';
22
22
  import Meeting from '../meeting';
23
23
  import {MediaRequestManager} from '../multistream/mediaRequestManager';
24
+ import ReconnectionError from '../common/errors/reconnection';
24
25
 
25
26
  /**
26
27
  * Used to indicate that the reconnect logic needs to be retried.
@@ -96,7 +97,7 @@ export default class ReconnectionManager {
96
97
 
97
98
  /**
98
99
  * @instance
99
- * @type {String}
100
+ * @type {RECONNECTION_STATE}
100
101
  * @private
101
102
  * @memberof ReconnectionManager
102
103
  */
@@ -227,7 +228,6 @@ export default class ReconnectionManager {
227
228
  */
228
229
  public cleanUp() {
229
230
  this.reset();
230
- this.meeting = null;
231
231
  }
232
232
 
233
233
  /**
@@ -265,6 +265,18 @@ export default class ReconnectionManager {
265
265
  return this.status === RECONNECTION.STATE.IN_PROGRESS;
266
266
  }
267
267
 
268
+ /**
269
+ * Sets the reconnection status
270
+ *
271
+ * @public
272
+ * @param {RECONNECTION_STATE} status
273
+ * @memberof ReconnectionManager
274
+ * @returns {undefined}
275
+ */
276
+ public setStatus(status: RECONNECTION_STATE) {
277
+ this.status = status;
278
+ }
279
+
268
280
  /**
269
281
  * @returns {Boolean}
270
282
  * @throws {ReconnectionError}
@@ -337,73 +349,55 @@ export default class ReconnectionManager {
337
349
  });
338
350
  }
339
351
 
340
- return this.executeReconnection({networkDisconnect})
341
- .then(() => {
342
- LoggerProxy.logger.info('ReconnectionManager:index#reconnect --> Reconnection successful.');
352
+ return this.executeReconnection({networkDisconnect}).catch((reconnectError) => {
353
+ if (reconnectError instanceof NeedsRetryError) {
343
354
  LoggerProxy.logger.info(
344
- 'ReconnectionManager:index#reconnect --> Sending reconnect success metric.'
355
+ 'ReconnectionManager:index#reconnect --> Reconnection not successful, retrying.'
345
356
  );
357
+ // Reset our reconnect status since we are looping back to the beginning
358
+ this.status = RECONNECTION.STATE.DEFAULT_STATUS;
346
359
 
347
- // @ts-ignore
348
- this.webex.internal.newMetrics.submitClientEvent({
349
- name: 'client.media.recovered',
350
- payload: {
351
- recoveredBy: 'new',
352
- },
353
- options: {
354
- meetingId: this.meeting.id,
355
- },
356
- });
357
- })
358
- .catch((reconnectError) => {
359
- if (reconnectError instanceof NeedsRetryError) {
360
- LoggerProxy.logger.info(
361
- 'ReconnectionManager:index#reconnect --> Reconnection not successful, retrying.'
362
- );
363
- // Reset our reconnect status since we are looping back to the beginning
364
- this.status = RECONNECTION.STATE.DEFAULT_STATUS;
365
-
366
- // This is a network retry, so we should not log START metrics again
367
- return this.reconnect({networkDisconnect: true, networkRetry: true});
368
- }
360
+ // This is a network retry, so we should not log START metrics again
361
+ return this.reconnect({networkDisconnect: true, networkRetry: true});
362
+ }
369
363
 
370
- // Reconnect has failed
371
- LoggerProxy.logger.error(
372
- 'ReconnectionManager:index#reconnect --> Reconnection failed.',
373
- reconnectError.message
374
- );
375
- LoggerProxy.logger.info(
376
- 'ReconnectionManager:index#reconnect --> Sending reconnect abort metric.'
377
- );
364
+ // Reconnect has failed
365
+ LoggerProxy.logger.error(
366
+ 'ReconnectionManager:index#reconnect --> Reconnection failed.',
367
+ reconnectError.message
368
+ );
369
+ LoggerProxy.logger.info(
370
+ 'ReconnectionManager:index#reconnect --> Sending reconnect abort metric.'
371
+ );
378
372
 
379
- // @ts-ignore
380
- this.webex.internal.newMetrics.submitClientEvent({
381
- name: 'client.call.aborted',
382
- payload: {
383
- errors: [
384
- {
385
- category: 'expected',
386
- errorCode: 2008,
387
- fatal: true,
388
- name: 'media-engine',
389
- shownToUser: false,
390
- },
391
- ],
392
- },
393
- options: {
394
- meetingId: this.meeting.id,
395
- },
396
- });
397
- if (reconnectError instanceof NeedsRejoinError) {
398
- // send call aborded event with catogery as expected as we are trying to rejoin
373
+ // @ts-ignore
374
+ this.webex.internal.newMetrics.submitClientEvent({
375
+ name: 'client.call.aborted',
376
+ payload: {
377
+ errors: [
378
+ {
379
+ category: 'expected',
380
+ errorCode: 2008,
381
+ fatal: true,
382
+ name: 'media-engine',
383
+ shownToUser: false,
384
+ },
385
+ ],
386
+ },
387
+ options: {
388
+ meetingId: this.meeting.id,
389
+ },
390
+ });
391
+ if (reconnectError instanceof NeedsRejoinError) {
392
+ // send call aborded event with catogery as expected as we are trying to rejoin
399
393
 
400
- if (this.autoRejoinEnabled) {
401
- return this.rejoinMeeting(reconnectError.wasSharing);
402
- }
394
+ if (this.autoRejoinEnabled) {
395
+ return this.rejoinMeeting(reconnectError.wasSharing);
403
396
  }
397
+ }
404
398
 
405
- throw reconnectError;
406
- });
399
+ throw reconnectError;
400
+ });
407
401
  }
408
402
 
409
403
  /**
@@ -485,14 +479,13 @@ export default class ReconnectionManager {
485
479
  const media = await this.reconnectMedia();
486
480
 
487
481
  LoggerProxy.logger.log(
488
- 'ReconnectionManager:index#executeReconnection --> Media reestablished'
482
+ 'ReconnectionManager:index#executeReconnection --> webRTC media connection renewed and local sdp offer sent'
489
483
  );
490
- this.status = RECONNECTION.STATE.COMPLETE;
491
484
 
492
485
  return media;
493
486
  } catch (error) {
494
487
  LoggerProxy.logger.error(
495
- 'ReconnectionManager:index#executeReconnection --> Media reestablishment failed'
488
+ 'ReconnectionManager:index#executeReconnection --> failed to renew webRTC media connection or initiate offer'
496
489
  );
497
490
  this.status = RECONNECTION.STATE.FAILURE;
498
491
 
@@ -559,9 +552,7 @@ export default class ReconnectionManager {
559
552
  * @memberof ReconnectionManager
560
553
  */
561
554
  async reconnectMedia() {
562
- LoggerProxy.logger.log(
563
- 'ReconnectionManager:index#reconnectMedia --> Begin reestablishment of media'
564
- );
555
+ LoggerProxy.logger.log('ReconnectionManager:index#reconnectMedia --> do turn discovery');
565
556
 
566
557
  // do the TURN server discovery again and ignore reachability results since the TURN server might change
567
558
  const turnServerResult = await this.meeting.roap.doTurnDiscovery(this.meeting, true, true);
@@ -576,6 +567,10 @@ export default class ReconnectionManager {
576
567
  });
577
568
  }
578
569
 
570
+ LoggerProxy.logger.log(
571
+ 'ReconnectionManager:index#reconnectMedia --> renew webRTC media connection and send local sdp offer'
572
+ );
573
+
579
574
  await this.meeting.mediaProperties.webrtcMediaConnection.reconnect(iceServers);
580
575
 
581
576
  // resend media requests
@@ -1,3 +1,5 @@
1
+ /* eslint-disable class-methods-use-this */
2
+ import {CallDiagnosticUtils} from '@webex/internal-plugin-metrics';
1
3
  import RTC_METRICS from './constants';
2
4
 
3
5
  /**
@@ -55,6 +57,9 @@ export default class RtcMetrics {
55
57
  */
56
58
  addMetrics(data) {
57
59
  if (data.payload.length) {
60
+ if (data.name === 'stats-report') {
61
+ data.payload = data.payload.map(this.anonymizeIp);
62
+ }
58
63
  this.metricsQueue.push(data);
59
64
  }
60
65
  }
@@ -69,6 +74,25 @@ export default class RtcMetrics {
69
74
  clearInterval(this.intervalId);
70
75
  }
71
76
 
77
+ /**
78
+ * Anonymize IP addresses.
79
+ *
80
+ * @param {array} stats - An RTCStatsReport organized into an array of strings.
81
+ * @returns {string}
82
+ */
83
+ anonymizeIp(stats: string): string {
84
+ const data = JSON.parse(stats);
85
+ // on local and remote candidates, anonymize the last 4 bits.
86
+ if (data.type === 'local-candidate' || data.type === 'remote-candidate') {
87
+ data.ip = CallDiagnosticUtils.anonymizeIPAddress(data.ip) || undefined;
88
+ data.address = CallDiagnosticUtils.anonymizeIPAddress(data.address) || undefined;
89
+ data.relatedAddress =
90
+ CallDiagnosticUtils.anonymizeIPAddress(data.relatedAddress) || undefined;
91
+ }
92
+
93
+ return JSON.stringify(data);
94
+ }
95
+
72
96
  /**
73
97
  * Send metrics to the metrics service.
74
98
  *
@@ -103,7 +103,7 @@ export class StatsAnalyzer extends EventsScope {
103
103
  this.networkQualityMonitor = networkQualityMonitor;
104
104
  this.correlationId = config.correlationId;
105
105
  this.mqaSentCount = -1;
106
- this.lastMqaDataSent = {};
106
+ this.lastMqaDataSent = {resolutions: {}};
107
107
  this.lastEmittedStartStopEvent = {};
108
108
  this.receiveSlotCallback = receiveSlotCallback;
109
109
  this.successfulCandidatePair = {};
@@ -152,6 +152,21 @@ export class StatsAnalyzer extends EventsScope {
152
152
  const newMqa = cloneDeep(emptyMqaInterval);
153
153
 
154
154
  Object.keys(this.statsResults).forEach((mediaType) => {
155
+ if (!this.lastMqaDataSent[mediaType]) {
156
+ this.lastMqaDataSent[mediaType] = {};
157
+ this.lastMqaDataSent.resolutions[mediaType] = {};
158
+ }
159
+
160
+ if (!this.lastMqaDataSent[mediaType].send && mediaType.includes('-send')) {
161
+ this.lastMqaDataSent[mediaType].send = {};
162
+ this.lastMqaDataSent.resolutions[mediaType].send = {};
163
+ }
164
+
165
+ if (!this.lastMqaDataSent[mediaType].recv && mediaType.includes('-recv')) {
166
+ this.lastMqaDataSent[mediaType].recv = {};
167
+ this.lastMqaDataSent.resolutions[mediaType].recv = {};
168
+ }
169
+
155
170
  if (mediaType.includes('audio-send') || mediaType.includes('audio-share-send')) {
156
171
  const audioSender = cloneDeep(emptyAudioTransmit);
157
172
 
@@ -162,6 +177,8 @@ export class StatsAnalyzer extends EventsScope {
162
177
  mediaType,
163
178
  });
164
179
  newMqa.audioTransmit.push(audioSender);
180
+
181
+ this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send);
165
182
  } else if (mediaType.includes('audio-recv') || mediaType.includes('audio-share-recv')) {
166
183
  const audioReceiver = cloneDeep(emptyAudioReceive);
167
184
 
@@ -172,6 +189,8 @@ export class StatsAnalyzer extends EventsScope {
172
189
  mediaType,
173
190
  });
174
191
  newMqa.audioReceive.push(audioReceiver);
192
+
193
+ this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv);
175
194
  } else if (mediaType.includes('video-send') || mediaType.includes('video-share-send')) {
176
195
  const videoSender = cloneDeep(emptyVideoTransmit);
177
196
 
@@ -182,6 +201,11 @@ export class StatsAnalyzer extends EventsScope {
182
201
  mediaType,
183
202
  });
184
203
  newMqa.videoTransmit.push(videoSender);
204
+
205
+ this.lastMqaDataSent[mediaType].send = cloneDeep(this.statsResults[mediaType].send);
206
+ this.lastMqaDataSent.resolutions[mediaType].send = cloneDeep(
207
+ this.statsResults.resolutions[mediaType].send
208
+ );
185
209
  } else if (mediaType.includes('video-recv') || mediaType.includes('video-share-recv')) {
186
210
  const videoReceiver = cloneDeep(emptyVideoReceive);
187
211
 
@@ -192,6 +216,11 @@ export class StatsAnalyzer extends EventsScope {
192
216
  mediaType,
193
217
  });
194
218
  newMqa.videoReceive.push(videoReceiver);
219
+
220
+ this.lastMqaDataSent[mediaType].recv = cloneDeep(this.statsResults[mediaType].recv);
221
+ this.lastMqaDataSent.resolutions[mediaType].recv = cloneDeep(
222
+ this.statsResults.resolutions[mediaType].recv
223
+ );
195
224
  }
196
225
  });
197
226
 
@@ -134,9 +134,12 @@ export const getVideoReceiverMqa = ({videoReceiver, statsResults, lastMqaDataSen
134
134
  const lastPacketsReceived = lastMqaDataSent[mediaType]?.[sendrecvType].totalPacketsReceived || 0;
135
135
  const lastPacketsLost = lastMqaDataSent[mediaType]?.[sendrecvType].totalPacketsLost || 0;
136
136
  const lastBytesReceived = lastMqaDataSent[mediaType]?.[sendrecvType].totalBytesReceived || 0;
137
- const lastFramesReceived = lastMqaDataSent[mediaType]?.[sendrecvType].framesReceived || 0;
138
- const lastFramesDecoded = lastMqaDataSent[mediaType]?.[sendrecvType].framesDecoded || 0;
139
- const lastFramesDropped = lastMqaDataSent[mediaType]?.[sendrecvType].framesDropped || 0;
137
+ const lastFramesReceived =
138
+ lastMqaDataSent.resolutions[mediaType]?.[sendrecvType].framesReceived || 0;
139
+ const lastFramesDecoded =
140
+ lastMqaDataSent.resolutions[mediaType]?.[sendrecvType].framesDecoded || 0;
141
+ const lastFramesDropped =
142
+ lastMqaDataSent.resolutions[mediaType]?.[sendrecvType].framesDropped || 0;
140
143
  const lastKeyFramesDecoded = lastMqaDataSent[mediaType]?.[sendrecvType].keyFramesDecoded || 0;
141
144
  const lastPliCount = lastMqaDataSent[mediaType]?.[sendrecvType].totalPliCount || 0;
142
145
 
@@ -190,12 +193,12 @@ export const getVideoReceiverMqa = ({videoReceiver, statsResults, lastMqaDataSen
190
193
  const totalFrameDecodedInaMin =
191
194
  statsResults.resolutions[mediaType][sendrecvType].framesDecoded - lastFramesDecoded;
192
195
 
193
- videoReceiver.streams[0].common.receivedFrameRate = totalFrameReceivedInaMin
194
- ? (totalFrameReceivedInaMin * 100) / 60
195
- : 0;
196
- videoReceiver.streams[0].common.renderedFrameRate = totalFrameDecodedInaMin
197
- ? (totalFrameDecodedInaMin * 100) / 60
198
- : 0;
196
+ videoReceiver.streams[0].common.receivedFrameRate = Math.round(
197
+ totalFrameReceivedInaMin ? totalFrameReceivedInaMin / 60 : 0
198
+ );
199
+ videoReceiver.streams[0].common.renderedFrameRate = Math.round(
200
+ totalFrameDecodedInaMin ? totalFrameDecodedInaMin / 60 : 0
201
+ );
199
202
 
200
203
  videoReceiver.streams[0].common.framesDropped =
201
204
  statsResults.resolutions[mediaType][sendrecvType].framesDropped - lastFramesDropped;
@@ -220,9 +223,9 @@ export const getVideoSenderMqa = ({videoSender, statsResults, lastMqaDataSent, m
220
223
  lastMqaDataSent[mediaType]?.[sendrecvType].totalPacketsLostOnReceiver || 0;
221
224
  const lastBytesSent = lastMqaDataSent[mediaType]?.[sendrecvType].totalBytesSent || 0;
222
225
  const lastKeyFramesEncoded =
223
- lastMqaDataSent[mediaType]?.[sendrecvType].totalKeyFramesEncoded || 0;
226
+ lastMqaDataSent.resolutions[mediaType]?.[sendrecvType].totalKeyFramesEncoded || 0;
224
227
  const lastFirCount = lastMqaDataSent[mediaType]?.[sendrecvType].totalFirCount || 0;
225
- const lastFramesSent = lastMqaDataSent[mediaType]?.[sendrecvType].framesSent || 0;
228
+ const lastFramesSent = lastMqaDataSent.resolutions[mediaType]?.[sendrecvType].framesSent || 0;
226
229
  const {csi} = statsResults[mediaType];
227
230
  if (csi && !videoSender.streams[0].common.csi.includes(csi)) {
228
231
  videoSender.streams[0].common.csi.push(csi);
@@ -280,9 +283,9 @@ export const getVideoSenderMqa = ({videoSender, statsResults, lastMqaDataSent, m
280
283
  const totalFrameSentInaMin =
281
284
  statsResults.resolutions[mediaType][sendrecvType].framesSent - (lastFramesSent || 0);
282
285
 
283
- videoSender.streams[0].common.transmittedFrameRate = totalFrameSentInaMin
284
- ? (totalFrameSentInaMin * 100) / 60
285
- : 0;
286
+ videoSender.streams[0].common.transmittedFrameRate = Math.round(
287
+ totalFrameSentInaMin ? totalFrameSentInaMin / 60 : 0
288
+ );
286
289
  videoSender.streams[0].transmittedHeight =
287
290
  statsResults.resolutions[mediaType][sendrecvType].height || 0;
288
291
  videoSender.streams[0].transmittedWidth =
@@ -16,16 +16,32 @@ describe('createMediaConnection', () => {
16
16
  id: 'any fake track'
17
17
  }
18
18
  const fakeAudioStream = {
19
- outputTrack: fakeTrack,
19
+ outputStream: {
20
+ getTracks: () => {
21
+ return [fakeTrack];
22
+ }
23
+ }
20
24
  };
21
25
  const fakeVideoStream = {
22
- outputTrack: fakeTrack,
26
+ outputStream: {
27
+ getTracks: () => {
28
+ return [fakeTrack];
29
+ }
30
+ }
23
31
  };
24
32
  const fakeShareVideoStream = {
25
- outputTrack: fakeTrack,
33
+ outputStream: {
34
+ getTracks: () => {
35
+ return [fakeTrack];
36
+ }
37
+ }
26
38
  };
27
39
  const fakeShareAudioStream = {
28
- outputTrack: fakeTrack,
40
+ outputStream: {
41
+ getTracks: () => {
42
+ return [fakeTrack];
43
+ }
44
+ }
29
45
  };
30
46
  afterEach(() => {
31
47
  sinon.restore();
@@ -67,12 +67,14 @@ describe('plugin-meetings', () => {
67
67
  canShareDesktop: null,
68
68
  canShareContent: null,
69
69
  canTransferFile: null,
70
+ canChat: null,
70
71
  canDoVideo: null,
71
72
  canAnnotate: null,
72
73
  canUseVoip: null,
73
74
  supportHQV: null,
74
75
  supportHDV: null,
75
76
  canShareWhiteBoard: null,
77
+ enforceVirtualBackground: null,
76
78
  ...expected,
77
79
  };
78
80
 
@@ -145,12 +147,14 @@ describe('plugin-meetings', () => {
145
147
  'canShareDesktop',
146
148
  'canShareContent',
147
149
  'canTransferFile',
150
+ 'canChat',
148
151
  'canDoVideo',
149
152
  'canAnnotate',
150
153
  'canUseVoip',
151
154
  'supportHQV',
152
155
  'supportHDV',
153
156
  'canShareWhiteBoard',
157
+ 'enforceVirtualBackground',
154
158
  ].forEach((key) => {
155
159
  it(`get and set for ${key} work as expected`, () => {
156
160
  const inMeetingActions = new InMeetingActions();