@webex/plugin-meetings 3.3.1-next.19 → 3.3.1-next.20

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.
@@ -457,6 +457,7 @@ export default class Meeting extends StatelessWebexPlugin {
457
457
  private hasMediaConnectionConnectedAtLeastOnce;
458
458
  private joinWithMediaRetryInfo?;
459
459
  private connectionStateHandler?;
460
+ private iceCandidateErrors;
460
461
  /**
461
462
  * @param {Object} attrs
462
463
  * @param {Object} options
@@ -62,7 +62,7 @@ var Webinar = _webexCore.WebexPlugin.extend({
62
62
  updateCanManageWebcast: function updateCanManageWebcast(canManageWebcast) {
63
63
  this.set('canManageWebcast', canManageWebcast);
64
64
  },
65
- version: "3.3.1-next.19"
65
+ version: "3.3.1-next.20"
66
66
  });
67
67
  var _default = exports.default = Webinar;
68
68
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -43,7 +43,7 @@
43
43
  "@webex/eslint-config-legacy": "0.0.0",
44
44
  "@webex/jest-config-legacy": "0.0.0",
45
45
  "@webex/legacy-tools": "0.0.0",
46
- "@webex/plugin-meetings": "3.3.1-next.19",
46
+ "@webex/plugin-meetings": "3.3.1-next.20",
47
47
  "@webex/plugin-rooms": "3.3.1-next.5",
48
48
  "@webex/test-helper-chai": "3.3.1-next.4",
49
49
  "@webex/test-helper-mocha": "3.3.1-next.4",
@@ -70,7 +70,7 @@
70
70
  "@webex/internal-plugin-metrics": "3.3.1-next.4",
71
71
  "@webex/internal-plugin-support": "3.3.1-next.5",
72
72
  "@webex/internal-plugin-user": "3.3.1-next.4",
73
- "@webex/internal-plugin-voicea": "3.3.1-next.19",
73
+ "@webex/internal-plugin-voicea": "3.3.1-next.20",
74
74
  "@webex/media-helpers": "3.3.1-next.7",
75
75
  "@webex/plugin-people": "3.3.1-next.5",
76
76
  "@webex/plugin-rooms": "3.3.1-next.5",
@@ -92,5 +92,5 @@
92
92
  "//": [
93
93
  "TODO: upgrade jwt-decode when moving to node 18"
94
94
  ],
95
- "version": "3.3.1-next.19"
95
+ "version": "3.3.1-next.20"
96
96
  }
@@ -682,6 +682,7 @@ export default class Meeting extends StatelessWebexPlugin {
682
682
  private hasMediaConnectionConnectedAtLeastOnce: boolean;
683
683
  private joinWithMediaRetryInfo?: {isRetry: boolean; prevJoinResponse?: any};
684
684
  private connectionStateHandler?: ConnectionStateHandler;
685
+ private iceCandidateErrors: Map<string, number>;
685
686
 
686
687
  /**
687
688
  * @param {Object} attrs
@@ -1481,6 +1482,15 @@ export default class Meeting extends StatelessWebexPlugin {
1481
1482
  * @memberof Meeting
1482
1483
  */
1483
1484
  this.connectionStateHandler = undefined;
1485
+
1486
+ /**
1487
+ * ICE Candidates errors map
1488
+ * @instance
1489
+ * @type {Map<[number, string], number>}
1490
+ * @private
1491
+ * @memberof Meeting
1492
+ */
1493
+ this.iceCandidateErrors = new Map();
1484
1494
  }
1485
1495
 
1486
1496
  /**
@@ -6010,6 +6020,32 @@ export default class Meeting extends StatelessWebexPlugin {
6010
6020
  );
6011
6021
  }
6012
6022
  );
6023
+
6024
+ this.iceCandidateErrors.clear();
6025
+ this.mediaProperties.webrtcMediaConnection.on(Event.ICE_CANDIDATE_ERROR, (event) => {
6026
+ const {errorCode} = event.error;
6027
+ let {errorText} = event.error;
6028
+
6029
+ if (
6030
+ errorCode === 600 &&
6031
+ errorText === 'Address not associated with the desired network interface.'
6032
+ ) {
6033
+ return;
6034
+ }
6035
+
6036
+ if (errorText.endsWith('.')) {
6037
+ errorText = errorText.slice(0, -1);
6038
+ }
6039
+
6040
+ errorText = errorText.toLowerCase();
6041
+ errorText = errorText.replace(/ /g, '_');
6042
+
6043
+ const error = `${errorCode}_${errorText}`;
6044
+
6045
+ const count = this.iceCandidateErrors.get(error) || 0;
6046
+
6047
+ this.iceCandidateErrors.set(error, count + 1);
6048
+ });
6013
6049
  };
6014
6050
 
6015
6051
  /**
@@ -6848,6 +6884,8 @@ export default class Meeting extends StatelessWebexPlugin {
6848
6884
  const {selectedCandidatePairChanges, numTransports} =
6849
6885
  await this.mediaProperties.getCurrentConnectionInfo();
6850
6886
 
6887
+ const iceCandidateErrors = Object.fromEntries(this.iceCandidateErrors);
6888
+
6851
6889
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE, {
6852
6890
  correlation_id: this.correlationId,
6853
6891
  locus_id: this.locusUrl.split('/').pop(),
@@ -6877,6 +6915,7 @@ export default class Meeting extends StatelessWebexPlugin {
6877
6915
  this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.iceConnectionState ||
6878
6916
  'unknown',
6879
6917
  ...reachabilityMetrics,
6918
+ ...iceCandidateErrors,
6880
6919
  });
6881
6920
 
6882
6921
  await this.cleanUpOnAddMediaFailure();
@@ -2762,6 +2762,66 @@ describe('plugin-meetings', () => {
2762
2762
  assert.isOk(errorThrown);
2763
2763
  });
2764
2764
 
2765
+ it('should send ICE_CANDIDATE_ERROR metric if media connection fails and ice candidate errors have been gathered', async () => {
2766
+ let errorThrown = undefined;
2767
+
2768
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
2769
+ turnServerInfo: undefined,
2770
+ turnDiscoverySkippedReason: undefined,
2771
+ });
2772
+ meeting.meetingState = 'ACTIVE';
2773
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2774
+
2775
+ const forceRtcMetricsSend = sinon.stub().resolves();
2776
+ const closeMediaConnectionStub = sinon.stub();
2777
+ Media.createMediaConnection = sinon.stub().returns({
2778
+ close: closeMediaConnectionStub,
2779
+ forceRtcMetricsSend,
2780
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
2781
+ initiateOffer: sinon.stub().resolves({}),
2782
+ on: sinon.stub(),
2783
+ });
2784
+
2785
+ meeting.iceCandidateErrors.set('701_error', 2);
2786
+ meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
2787
+
2788
+ await meeting
2789
+ .addMedia({
2790
+ mediaSettings: {},
2791
+ })
2792
+ .catch((err) => {
2793
+ errorThrown = err;
2794
+ assert.instanceOf(err, AddMediaFailed);
2795
+ });
2796
+
2797
+ // Check that the only metric sent is ADD_MEDIA_FAILURE
2798
+ assert.calledOnceWithExactly(
2799
+ Metrics.sendBehavioralMetric,
2800
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
2801
+ {
2802
+ correlation_id: meeting.correlationId,
2803
+ locus_id: meeting.locusUrl.split('/').pop(),
2804
+ reason: errorThrown.message,
2805
+ stack: errorThrown.stack,
2806
+ code: errorThrown.code,
2807
+ turnDiscoverySkippedReason: undefined,
2808
+ turnServerUsed: true,
2809
+ retriedWithTurnServer: false,
2810
+ isMultistream: false,
2811
+ isJoinWithMediaRetry: false,
2812
+ signalingState: 'unknown',
2813
+ connectionState: 'unknown',
2814
+ iceConnectionState: 'unknown',
2815
+ selectedCandidatePairChanges: 2,
2816
+ numTransports: 1,
2817
+ '701_error': 2,
2818
+ '701_turn_host_lookup_received_error': 1
2819
+ }
2820
+ );
2821
+
2822
+ assert.isOk(errorThrown);
2823
+ });
2824
+
2765
2825
  describe('handles StatsAnalyzer events', () => {
2766
2826
  let prevConfigValue;
2767
2827
  let statsAnalyzerStub;
@@ -7106,6 +7166,7 @@ describe('plugin-meetings', () => {
7106
7166
  assert.isFunction(eventListeners[Event.REMOTE_TRACK_ADDED]);
7107
7167
  assert.isFunction(eventListeners[Event.PEER_CONNECTION_STATE_CHANGED]);
7108
7168
  assert.isFunction(eventListeners[Event.ICE_CONNECTION_STATE_CHANGED]);
7169
+ assert.isFunction(eventListeners[Event.ICE_CANDIDATE_ERROR]);
7109
7170
  });
7110
7171
 
7111
7172
  it('should trigger a media:ready event when REMOTE_TRACK_ADDED is fired', () => {
@@ -7141,6 +7202,39 @@ describe('plugin-meetings', () => {
7141
7202
  });
7142
7203
  });
7143
7204
 
7205
+ describe('should react on a ICE_CANDIDATE_ERROR event', () => {
7206
+ beforeEach(() => {
7207
+ meeting.setupMediaConnectionListeners();
7208
+
7209
+ });
7210
+
7211
+ it('should not collect skipped ice candidates error', () => {
7212
+ eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 600, errorText: 'Address not associated with the desired network interface.' }});
7213
+
7214
+ assert.equal(meeting.iceCandidateErrors.size, 0);
7215
+ });
7216
+
7217
+ it('should collect valid ice candidates error', () => {
7218
+ eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: '' }});
7219
+
7220
+ assert.equal(meeting.iceCandidateErrors.size, 1);
7221
+ assert.equal(meeting.iceCandidateErrors.has('701_'), true);
7222
+ });
7223
+
7224
+ it('should increment counter if same valid ice candidates error collected', () => {
7225
+ eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: '' }});
7226
+
7227
+ eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: 'STUN host lookup received error.' }});
7228
+ eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: 'STUN host lookup received error.' }});
7229
+
7230
+ assert.equal(meeting.iceCandidateErrors.size, 2);
7231
+ assert.equal(meeting.iceCandidateErrors.has('701_'), true);
7232
+ assert.equal(meeting.iceCandidateErrors.get('701_'), 1);
7233
+ assert.equal(meeting.iceCandidateErrors.has('701_stun_host_lookup_received_error'), true);
7234
+ assert.equal(meeting.iceCandidateErrors.get('701_stun_host_lookup_received_error'), 2);
7235
+ });
7236
+ });
7237
+
7144
7238
  describe('CONNECTION_STATE_CHANGED event when state = "Connecting"', () => {
7145
7239
  it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = true', () => {
7146
7240
  meeting.hasMediaConnectionConnectedAtLeastOnce = true;