@webex/plugin-meetings 1.153.0 → 1.153.4

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.
@@ -10,9 +10,8 @@ import Metrics from '../metrics';
10
10
  import LoggerProxy from '../common/logs/logger-proxy';
11
11
  import StaticConfig from '../common/config';
12
12
  import {
13
- RETRY_TIMEOUT,
14
- ICE_TIMEOUT,
15
- HOST,
13
+ COMPLETE,
14
+ GATHERING,
16
15
  AUDIO,
17
16
  SDP,
18
17
  ICE_STATE,
@@ -22,15 +21,16 @@ import {
22
21
  OFFER,
23
22
  QUALITY_LEVELS,
24
23
  MAX_FRAMESIZES,
25
- METRICS_OPERATIONAL_MEASURES,
26
- IPV4_REGEX
24
+ METRICS_OPERATIONAL_MEASURES
27
25
  } from '../constants';
28
26
  import {error, eventType} from '../metrics/config';
29
27
  import MediaError from '../common/errors/media';
30
28
  import ParameterError from '../common/errors/parameter';
31
- import {InvalidSdpError} from '../common/errors/webex-errors';
29
+ import {InvalidSdpError, IceGatheringFailed} from '../common/errors/webex-errors';
32
30
  import BrowserDetection from '../common/browser-detection';
33
31
 
32
+ import PeerConnectionUtils from './util';
33
+
34
34
  const {isBrowser} = BrowserDetection();
35
35
 
36
36
  /**
@@ -120,18 +120,10 @@ const isSdpInvalid = (sdp) => {
120
120
  const parsedSdp = sdpTransform.parse(sdp);
121
121
 
122
122
  for (const mediaLine of parsedSdp.media) {
123
- if (mediaLine.candidates && mediaLine.candidates.length === 0) {
123
+ if (!mediaLine.candidates || mediaLine.candidates?.length === 0) {
124
124
  LoggerProxy.logger.error('PeerConnectionManager:index#isSdpInvalid --> iceCandidate: Ice candadate never completed');
125
125
 
126
- return 'iceCandidate: Ice candadate never completed';
127
- }
128
- // Sometimes the candidates might be there but only IPV6 we need to makes sure we have IPV4
129
- const hostCandidate = mediaLine.candidates.filter((candidate) => !!(candidate.type === HOST && candidate.ip.match(IPV4_REGEX)));
130
-
131
- if (hostCandidate.length === 0) {
132
- LoggerProxy.logger.error('PeerConnectionManager:index#isSdpInvalid --> iceCandidate: no IPV4 candidate present');
133
-
134
- return 'iceCandidate: no IPV4 candidate present';
126
+ return 'iceCandidate: Ice gathering never completed';
135
127
  }
136
128
 
137
129
  if (SDP.BAD_MEDIA_PORTS.includes(mediaLine.port)) {
@@ -191,43 +183,52 @@ pc.setContentSlides = (screenPc) => {
191
183
  */
192
184
  pc.iceCandidate = (peerConnection, {remoteQualityLevel}) =>
193
185
  new Promise((resolve, reject) => {
194
- // TODO: we dont need timeout as we can check the api state and validate.
195
- const timeout = setTimeout(() => {
186
+ const now = Date.now();
187
+ const doneGatheringIceCandidate = () => {
188
+ const miliseconds = parseInt(Math.abs(Date.now() - now), 4);
189
+
196
190
  peerConnection.sdp = limitBandwidth(peerConnection.localDescription.sdp);
197
191
  peerConnection.sdp = setMaxFs(peerConnection.sdp, remoteQualityLevel);
192
+ peerConnection.sdp = PeerConnectionUtils.convertCLineToIpv4(peerConnection.sdp);
198
193
 
199
- if (isSdpInvalid(peerConnection.sdp)) {
200
- setTimeout(() => {
201
- // peerconnection does gather ice candidate IP but in some cases due to firewall
202
- // or proxy the ice candidate does not get gathered so we need to wait and then retry
203
- // if still not valid then throw an error saying missing ice candidate
204
- // if ice candidate still not present after retry
205
- const invalidSdpPresent = isSdpInvalid(peerConnection.sdp);
194
+ const invalidSdpPresent = isSdpInvalid(peerConnection.sdp);
206
195
 
207
- if (!invalidSdpPresent) {
208
- resolve(peerConnection);
209
- }
210
- else {
211
- LoggerProxy.logger.error('PeerConnectionManager:index#iceCandidate --> SDP not valid after waiting.');
212
- reject(new InvalidSdpError(invalidSdpPresent));
213
- }
214
- }, RETRY_TIMEOUT);
196
+ if (invalidSdpPresent) {
197
+ LoggerProxy.logger.error('PeerConnectionManager:index#iceCandidate --> SDP not valid after waiting.');
198
+ reject(new InvalidSdpError(invalidSdpPresent));
215
199
  }
216
- else {
217
- resolve(peerConnection);
200
+ LoggerProxy.logger.log(`PeerConnectionManager:index#iceCandidate --> Time to gather ice candidate ${miliseconds} miliseconds`);
201
+
202
+
203
+ resolve();
204
+ };
205
+
206
+ // If ice has already been gathered
207
+ if (peerConnection.iceGatheringState === COMPLETE) {
208
+ doneGatheringIceCandidate();
209
+ }
210
+
211
+ peerConnection.onIceGatheringStateChange = () => {
212
+ if (peerConnection.iceGatheringState === COMPLETE) {
213
+ doneGatheringIceCandidate(peerConnection);
214
+ }
215
+ if (peerConnection.iceGatheringState === GATHERING) {
216
+ LoggerProxy.logger.log('PeerConnectionManager:index#onIceGatheringStateChange --> Ice state changed to gathering');
218
217
  }
219
- }, ICE_TIMEOUT);
218
+ };
220
219
 
221
220
  peerConnection.onicecandidate = (evt) => {
222
- if (!evt.candidate && !peerConnection.sdp) {
223
- peerConnection.sdp = limitBandwidth(peerConnection.localDescription.sdp);
224
- peerConnection.sdp = setMaxFs(peerConnection.sdp, remoteQualityLevel);
225
-
226
- if (evt.candidate === null && !isSdpInvalid(peerConnection.sdp)) {
227
- clearTimeout(timeout);
228
- resolve(peerConnection);
229
- }
221
+ if (evt.candidate === null) {
222
+ doneGatheringIceCandidate(peerConnection);
230
223
  }
224
+ else {
225
+ LoggerProxy.logger.log(`PeerConnectionManager:index#onicecandidate --> Candidate ${evt.candidate?.type} ${evt.candidate?.protocol} ${evt.candidate?.address}:${evt.candidate?.port}`);
226
+ }
227
+ };
228
+
229
+ peerConnection.onicecandidateerror = (event) => {
230
+ LoggerProxy.logger.error('PeerConnectionManager:index#onicecandidateerror --> Failed to gather ice candidate.', event);
231
+ reject(new IceGatheringFailed());
231
232
  };
232
233
  });
233
234
 
@@ -402,8 +403,6 @@ pc.createOffer = (peerConnection, {
402
403
  })
403
404
  .then(() => pc.iceCandidate(peerConnection, {remoteQualityLevel}))
404
405
  .then(() => {
405
- peerConnection.sdp = limitBandwidth(peerConnection.localDescription.sdp);
406
- peerConnection.sdp = setMaxFs(peerConnection.sdp, remoteQualityLevel);
407
406
  if (!checkH264Support(peerConnection.sdp)) {
408
407
  throw new MediaError('openH264 is downloading please Wait. Upload logs if not working on second try');
409
408
  }
@@ -530,6 +529,7 @@ pc.createAnswer = (params, {meetingId, remoteQualityLevel}) => {
530
529
  .then(() => {
531
530
  peerConnection.sdp = limitBandwidth(peerConnection.localDescription.sdp);
532
531
  peerConnection.sdp = setMaxFs(peerConnection.sdp, remoteQualityLevel);
532
+ peerConnection.sdp = PeerConnectionUtils.convertCLineToIpv4(peerConnection.sdp);
533
533
  if (!checkH264Support(peerConnection.sdp)) {
534
534
  throw new MediaError('openH264 is downloading please Wait. Upload logs if not working on second try');
535
535
  }
@@ -0,0 +1,19 @@
1
+
2
+ const PeerConnectionUtils = {};
3
+
4
+ /**
5
+ * Convert C line to IPv4
6
+ * @param {String} sdp
7
+ * @returns {String}
8
+ */
9
+ PeerConnectionUtils.convertCLineToIpv4 = (sdp) => {
10
+ let replaceSdp = sdp;
11
+
12
+ // TODO: remove this once linus supports Ipv6 c line.currently linus rejects SDP with c line having ipv6 candidates we are
13
+ // mocking ipv6 to ipv4 candidates
14
+ // https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-299232
15
+ replaceSdp = replaceSdp.replace(/c=IN IP6 .*/gi, 'c=IN IP4 0.0.0.0');
16
+
17
+ return replaceSdp;
18
+ };
19
+ export default PeerConnectionUtils;
@@ -48,6 +48,7 @@ import WebExMeetingsErrors from '../../../../src/common/errors/webex-meetings-er
48
48
  import ParameterError from '../../../../src/common/errors/parameter';
49
49
  import PasswordError from '../../../../src/common/errors/password-error';
50
50
  import CaptchaError from '../../../../src/common/errors/captcha-error';
51
+ import IntentToJoinError from '../../../../src/common/errors/intent-to-join';
51
52
  import DefaultSDKConfig from '../../../../src/config';
52
53
  import testUtils from '../../../utils/testUtils';
53
54
  import {MeetingInfoV2CaptchaError, MeetingInfoV2PasswordError} from '../../../../src/meeting-info/meeting-info-v2';
@@ -609,6 +610,17 @@ describe('plugin-meetings', () => {
609
610
  });
610
611
  });
611
612
  describe('#join', () => {
613
+ let sandbox = null;
614
+
615
+ beforeEach(() => {
616
+ sandbox = sinon.createSandbox();
617
+ });
618
+
619
+ afterEach(() => {
620
+ sandbox.restore();
621
+ sandbox = null;
622
+ });
623
+
612
624
  it('should have #join', () => {
613
625
  assert.exists(meeting.join);
614
626
  });
@@ -618,8 +630,9 @@ describe('plugin-meetings', () => {
618
630
  });
619
631
  describe('successful', () => {
620
632
  beforeEach(() => {
621
- MeetingUtil.joinMeeting = sinon.stub().returns(Promise.resolve());
633
+ sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.resolve());
622
634
  });
635
+
623
636
  it('should join the meeting and return promise', async () => {
624
637
  const join = meeting.join();
625
638
 
@@ -642,16 +655,17 @@ describe('plugin-meetings', () => {
642
655
  });
643
656
 
644
657
  it('should create new correlation ID when already joined', async () => {
645
- meeting.isCreated = false;
658
+ meeting.hasJoinedOnce = true;
646
659
  await meeting.join();
647
660
  sinon.assert.called(meeting.setCorrelationId);
648
661
  });
649
662
  });
650
663
  describe('failure', () => {
651
664
  beforeEach(() => {
652
- MeetingUtil.joinMeeting = sinon.stub().returns(Promise.reject());
665
+ sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.reject());
653
666
  meeting.logger.log = sinon.stub().returns(true);
654
667
  });
668
+
655
669
  describe('guest join', () => {
656
670
  beforeEach(() => {
657
671
  MeetingUtil.isPinOrGuest = sinon.stub().returns(true);
@@ -662,6 +676,24 @@ describe('plugin-meetings', () => {
662
676
  assert.calledOnce(MeetingUtil.joinMeeting);
663
677
  });
664
678
  });
679
+ it('should succeed when called again after IntentToJoinError error', async () => {
680
+ let joinSucceeded = false;
681
+
682
+ try {
683
+ await meeting.join();
684
+ joinSucceeded = true;
685
+ }
686
+ catch (e) {
687
+ assert.instanceOf(e, IntentToJoinError);
688
+ }
689
+ assert.isFalse(joinSucceeded);
690
+
691
+ // IntentToJoinError means that client should call join() again
692
+ // with moderator and pin explicitly set
693
+ MeetingUtil.joinMeeting = sinon.stub().returns(Promise.resolve());
694
+ await meeting.join({pin: '1234', moderator: false});
695
+ assert.calledWith(MeetingUtil.joinMeeting, meeting, {moderator: false, pin: '1234'});
696
+ });
665
697
  });
666
698
  it('should fail if password is required', async () => {
667
699
  meeting.passwordStatus = PASSWORD_STATUS.REQUIRED;
@@ -19,10 +19,18 @@ describe('plugin-meetings', () => {
19
19
  regionCode: 'WEST-COAST'
20
20
  };
21
21
 
22
+ webex.internal = {
23
+ services: {
24
+ get: sinon.mock().returns('locusUrl'),
25
+ waitForCatalog: sinon.mock().returns(Promise.resolve({}))
26
+ }
27
+ };
28
+
22
29
  meetingsRequest = new MeetingRequest({}, {
23
30
  parent: webex
24
31
  });
25
32
 
33
+
26
34
  meetingsRequest.request = sinon.mock().returns(Promise.resolve({}));
27
35
  });
28
36
 
@@ -112,12 +120,15 @@ describe('plugin-meetings', () => {
112
120
  const deviceUrl = 'deviceUrl';
113
121
  const correlationId = 'random-uuid';
114
122
  const roapMessage = 'roap-message';
123
+ const permissionToken = 'permission-token';
115
124
 
116
125
  await meetingsRequest.joinMeeting({
117
126
  locusUrl,
118
127
  deviceUrl,
119
128
  correlationId,
120
- roapMessage
129
+ roapMessage,
130
+ permissionToken
131
+
121
132
  });
122
133
  const requestParams = meetingsRequest.request.getCall(0).args[0];
123
134
 
@@ -125,8 +136,49 @@ describe('plugin-meetings', () => {
125
136
  assert.equal(requestParams.uri, `${locusUrl}/participant?alternateRedirect=true`);
126
137
  assert.equal(requestParams.body.device.url, deviceUrl);
127
138
  assert.equal(requestParams.body.device.countryCode, 'US');
139
+ assert.equal(requestParams.body.permissionToken, 'permission-token');
128
140
  assert.equal(requestParams.body.device.regionCode, 'WEST-COAST');
129
141
  });
142
+
143
+ it('sends /call with meetingNumber if inviteeAddress does not exist', async () => {
144
+ const deviceUrl = 'deviceUrl';
145
+ const correlationId = 'random-uuid';
146
+ const roapMessage = 'roap-message';
147
+ const meetingNumber = 'meetingNumber';
148
+
149
+ await meetingsRequest.joinMeeting({
150
+ deviceUrl,
151
+ correlationId,
152
+ roapMessage,
153
+ meetingNumber
154
+ });
155
+ const requestParams = meetingsRequest.request.getCall(0).args[0];
156
+
157
+ assert.equal(requestParams.method, 'POST');
158
+ assert.equal(requestParams.uri, 'locusUrl/loci/call?alternateRedirect=true');
159
+ assert.equal(requestParams.body.invitee.address, 'wbxmn:meetingNumber');
160
+ });
161
+
162
+ it('sends /call with inviteeAddress over meetingNumber as preference', async () => {
163
+ const deviceUrl = 'deviceUrl';
164
+ const correlationId = 'random-uuid';
165
+ const roapMessage = 'roap-message';
166
+ const meetingNumber = 'meetingNumber';
167
+ const inviteeAddress = 'sipUrl';
168
+
169
+ await meetingsRequest.joinMeeting({
170
+ deviceUrl,
171
+ correlationId,
172
+ roapMessage,
173
+ meetingNumber,
174
+ inviteeAddress
175
+ });
176
+ const requestParams = meetingsRequest.request.getCall(0).args[0];
177
+
178
+ assert.equal(requestParams.method, 'POST');
179
+ assert.equal(requestParams.uri, 'locusUrl/loci/call?alternateRedirect=true');
180
+ assert.equal(requestParams.body.invitee.address, 'sipUrl');
181
+ });
130
182
  });
131
183
 
132
184
  describe('#pstn', () => {
@@ -4,6 +4,7 @@ import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
4
4
  import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
5
5
  import LoggerConfig
6
6
  from '@webex/plugin-meetings/src/common/logs/logger-config';
7
+ import Metrics from '@webex/plugin-meetings/src/metrics/index';
7
8
 
8
9
  describe('plugin-meetings', () => {
9
10
  describe('Meeting utils function', () => {
@@ -11,6 +12,7 @@ describe('plugin-meetings', () => {
11
12
  const meeting = {};
12
13
 
13
14
  beforeEach(() => {
15
+ Metrics.postEvent = sinon.stub();
14
16
  const logger = {
15
17
  info: sandbox.stub(),
16
18
  log: sandbox.stub(),
@@ -132,6 +134,45 @@ describe('plugin-meetings', () => {
132
134
  });
133
135
  });
134
136
  });
137
+
138
+ describe('joinMeeting', () => {
139
+ it('#Should call `meetingRequest.joinMeeting', async () => {
140
+ const meeting = {meetingJoinUrl: 'meetingJoinUrl', locusUrl: 'locusUrl', meetingRequest: {joinMeeting: sinon.stub().returns(Promise.resolve({body: {}, headers: {}}))}};
141
+
142
+ MeetingUtil.parseLocusJoin = sinon.stub();
143
+ await MeetingUtil.joinMeeting(meeting, {});
144
+
145
+ assert.calledOnce(meeting.meetingRequest.joinMeeting);
146
+ const parameter = meeting.meetingRequest.joinMeeting.getCall(0).args[0];
147
+
148
+ assert.equal(parameter.inviteeAddress, 'meetingJoinUrl');
149
+ });
150
+
151
+ it('#Should fallback sipUrl if meetingJoinUrl does not exists', async () => {
152
+ const meeting = {sipUri: 'sipUri', locusUrl: 'locusUrl', meetingRequest: {joinMeeting: sinon.stub().returns(Promise.resolve({body: {}, headers: {}}))}};
153
+
154
+ MeetingUtil.parseLocusJoin = sinon.stub();
155
+ await MeetingUtil.joinMeeting(meeting, {});
156
+
157
+ assert.calledOnce(meeting.meetingRequest.joinMeeting);
158
+ const parameter = meeting.meetingRequest.joinMeeting.getCall(0).args[0];
159
+
160
+ assert.equal(parameter.inviteeAddress, 'sipUri');
161
+ });
162
+
163
+ it('#Should fallback to meetingNumber if meetingJoinUrl/sipUrl does not exists', async () => {
164
+ const meeting = {meetingNumber: 'meetingNumber', locusUrl: 'locusUrl', meetingRequest: {joinMeeting: sinon.stub().returns(Promise.resolve({body: {}, headers: {}}))}};
165
+
166
+ MeetingUtil.parseLocusJoin = sinon.stub();
167
+ await MeetingUtil.joinMeeting(meeting, {});
168
+
169
+ assert.calledOnce(meeting.meetingRequest.joinMeeting);
170
+ const parameter = meeting.meetingRequest.joinMeeting.getCall(0).args[0];
171
+
172
+ assert.isUndefined(parameter.inviteeAddress);
173
+ assert.equal(parameter.meetingNumber, 'meetingNumber');
174
+ });
175
+ });
135
176
  });
136
177
  });
137
178
 
@@ -468,7 +468,7 @@ skipInBrowser(describe)('plugin-meetings', () => {
468
468
  });
469
469
  describe('successful MeetingInfo.#fetchMeetingInfo', () => {
470
470
  beforeEach(() => {
471
- webex.meetings.meetingInfo.fetchMeetingInfo = sinon.stub().returns(Promise.resolve(true));
471
+ webex.meetings.meetingInfo.fetchMeetingInfo = sinon.stub().returns(Promise.resolve({body: {permissionToken: 'PT', meetingJoinUrl: 'meetingJoinUrl'}}));
472
472
  });
473
473
  it('creates the meeting from a successful meeting info fetch promise testing', async () => {
474
474
  const meeting = webex.meetings.createMeeting('test', 'test');
@@ -482,6 +482,8 @@ skipInBrowser(describe)('plugin-meetings', () => {
482
482
  assert.calledWith(webex.meetings.meetingInfo.fetchMeetingInfo, 'test');
483
483
  assert.calledWith(MeetingsUtil.extractDestination, 'test', 'test');
484
484
  assert.calledWith(MeetingsUtil.getMeetingAddedType, 'test');
485
+ assert.equal(meeting.permissionToken, 'PT');
486
+ assert.equal(meeting.meetingJoinUrl, 'meetingJoinUrl');
485
487
  });
486
488
 
487
489
  it('creates the meeting from a successful meeting info fetch meeting resolve testing', async () => {
@@ -3,9 +3,23 @@ import {assert} from '@webex/test-helper-chai';
3
3
  import sinon from 'sinon';
4
4
  import PeerConnectionManager from '@webex/plugin-meetings/src/peer-connection-manager/index';
5
5
  import StaticConfig from '@webex/plugin-meetings/src/common/config';
6
+ import {IceGatheringFailed, InvalidSdpError} from '@webex/plugin-meetings/src/common/errors/webex-errors';
6
7
 
7
8
  describe('Peerconnection Manager', () => {
8
9
  describe('Methods', () => {
10
+ let peerConnection = null;
11
+ let sdp = null;
12
+
13
+ beforeEach(() => {
14
+ sdp = 'v=0\r\no=- 1026633665396855335 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1 2\r\nr\na=msid-semantic: WMS\r\nm=audio 40903 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 106 105 13 110 112 113 126\r\nb=TIAS:64000\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:602151403 1 udp 2122262783 2401:4900:2301:c2cb:5ea9:46d5:7321:2531 47974 typ host generation 0 network-id 2 network-cost 900\r\na=candidate:675071982 1 udp 2122197247 2401:4900:33fc:314c:ad57:3e1d:befe:34b7 41234 typ host generation 0 network-id 3 network-cost 900\r\na=candidate:1835525403 1 tcp 1518283007 2401:4900:2301:c2cb:5ea9:46d5:7321:2531 9 typ host tcptype active generation 0 network-id 2 network-cost 900\r\na=candidate:1723808542 1 tcp 1518217471 2401:4900:33fc:314c:ad57:3e1d:befe:34b7 9 typ host tcptype active generation 0 network-id 3 network-cost 900\r\na=ice-ufrag:zK3G\r\na=ice-pwd:e9xgQIGnRsJvaFpvTAenr5JQ\r\na=ice-options:trickle\r\nm=video 43875 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 122 121 127\r\nb=TIAS:4000000\r\nc=IN IP4 0.0.0.0\r\na=periodic-keyframes:20\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:602151403 1 udp 2122262783 2401:4900:2301:c2cb:5ea9:46d5:7321:2531 41619 typ host generation 0 network-id 2 network-cost 900\r\na=candidate:675071982 1 udp 2122197247 2401:4900:33fc:314c:ad57:3e1d:befe:34b7 47098 typ host generation 0 network-id 3 network-cost 900\r\na=candidate:1835525403 1 tcp 1518283007 2401:4900:2301:c2cb:5ea9:46d5:7321:2531 9 typ host tcptype active generation 0 network-id 2 network-cost 900\r\na=candidate:1723808542 1 tcp 1518217471 2401:4900:33fc:314c:ad57:3e1d:befe:34b7 9 typ host tcptype active generation 0 network-id 3 network-cost 900\r\na=ice-ufrag:zK3G\r\na=ice-pwd:e9xgQIGnRsJvaFpvTAenr5JQ\r\na=ice-options:trickle\r\na=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\nm=video 49298 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 122 121 127\r\nb=TIAS:4000000\r\nc=IN IP4 0.0.0.0\r\na=periodic-keyframes:20\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:602151403 1 udp 2122262783 2401:4900:2301:c2cb:5ea9:46d5:7321:2531 37670 typ host generation 0 network-id 2 network-cost 900\r\na=candidate:675071982 1 udp 2122197247 2401:4900:33fc:314c:ad57:3e1d:befe:34b7 37790 typ host generation 0 network-id 3 network-cost 900\r\na=candidate:1835525403 1 tcp 1518283007 2401:4900:2301:c2cb:5ea9:46d5:7321:2531 9 typ host tcptype active generation 0 network-id 2 network-cost 900\r\na=candidate:1723808542 1 tcp 1518217471 2401:4900:33fc:314c:ad57:3e1d:befe:34b7 9 typ host tcptype active generation 0 network-id 3 network-cost 900\r\na=ice-ufrag:zK3G\r\na=ice-pwd:e9xgQIGnRsJvaFpvTAenr5JQ\r\na=ice-options:trickle\r\na=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f;\r\na=content:slides\r\n';
15
+
16
+ peerConnection = {
17
+ iceGatheringState: 'new',
18
+ onicecandidate: null,
19
+ onicecandidateerror: null,
20
+ localDescription: {sdp}
21
+ };
22
+ });
9
23
  describe('setRemoteSessionDetails', () => {
10
24
  it('change the start bitrate on remoteSDP', async () => {
11
25
  StaticConfig.set({bandwidth: {audio: 50, video: 500, startBitrate: 2000}});
@@ -62,5 +76,60 @@ describe('Peerconnection Manager', () => {
62
76
  assert.equal(result.sdp, remoteSdp);
63
77
  });
64
78
  });
79
+
80
+ describe('iceCandidate', () => {
81
+ beforeEach(() => {
82
+ StaticConfig.set({bandwidth: {audio: 50, video: 500, startBitrate: 0}});
83
+ peerConnection.sdp = null;
84
+ });
85
+ it('ice gathering already completed', async () => {
86
+ peerConnection.iceGatheringState = 'complete';
87
+
88
+ await PeerConnectionManager.iceCandidate(peerConnection, {remoteQualityLevel: 'HIGH'});
89
+ console.error('Came her with sdp resolved');
90
+ assert(peerConnection.sdp.search('max-fs:8192'), true);
91
+ });
92
+
93
+ it('listen onIceCandidate,onicecandidateerror and onIceGatheringStateChange', async () => {
94
+ peerConnection.iceGatheringState = 'none';
95
+ setTimeout(() => {
96
+ peerConnection.onicecandidate({candidate: null});
97
+ }, 1000);
98
+ await PeerConnectionManager.iceCandidate(peerConnection, {remoteQualityLevel: 'HIGH'});
99
+ assert.isFunction(peerConnection.onIceGatheringStateChange);
100
+ assert.isFunction(peerConnection.onicecandidate);
101
+ assert.isFunction(peerConnection.onicecandidateerror);
102
+ });
103
+
104
+ it('generate sdp with iceGatheringstate is `complet`', async () => {
105
+ peerConnection.iceGatheringState = 'none';
106
+ setTimeout(() => {
107
+ peerConnection.iceGatheringState = 'complete';
108
+ peerConnection.onIceGatheringStateChange();
109
+ }, 1000);
110
+ await PeerConnectionManager.iceCandidate(peerConnection, {remoteQualityLevel: 'HIGH'})
111
+ .then(() => {
112
+ assert(peerConnection.sdp.search('max-fs:8192'), true);
113
+ });
114
+ });
115
+
116
+ it('should not generate sdp if onicecandidateerror errors out ', async () => {
117
+ peerConnection.iceGatheringState = 'none';
118
+ setTimeout(() => {
119
+ peerConnection.onicecandidateerror();
120
+ }, 1000);
121
+ await assert.isRejected(PeerConnectionManager.iceCandidate(peerConnection, {remoteQualityLevel: 'HIGH'}), IceGatheringFailed);
122
+ assert.equal(peerConnection.sdp, null);
123
+ });
124
+
125
+ it('should throw generated SDP does not have candidates ', async () => {
126
+ peerConnection.iceGatheringState = 'none';
127
+ peerConnection.localDescription.sdp = 'v=0\r\no=- 1026633665396855335 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0 1 2\r\nr\na=msid-semantic: WMS\r\nm=audio 40903 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 106 105 13 110 112 113 126\r\nb=TIAS:64000\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:zK3G\r\na=ice-pwd:e9xgQIGnRsJvaFpvTAenr5JQ\r\na=ice-options:trickle\r\nm=video 43875 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 122 121 127\r\nb=TIAS:4000000\r\nc=IN IP4 0.0.0.0\r\na=periodic-keyframes:20\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:zK3G\r\na=ice-pwd:e9xgQIGnRsJvaFpvTAenr5JQ\r\na=ice-options:trickle\r\na=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\nm=video 49298 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 122 121 127\r\nb=TIAS:4000000\r\nc=IN IP4 0.0.0.0\r\na=periodic-keyframes:20\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:zK3G\r\na=ice-pwd:e9xgQIGnRsJvaFpvTAenr5JQ\r\na=ice-options:trickle\r\na=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f;\r\na=content:slides\r\n';
128
+ setTimeout(() => {
129
+ peerConnection.onicecandidate({candidate: null});
130
+ }, 1000);
131
+ await assert.isRejected(PeerConnectionManager.iceCandidate(peerConnection, {remoteQualityLevel: 'HIGH'}), InvalidSdpError);
132
+ });
133
+ });
65
134
  });
66
135
  });
@@ -0,0 +1,25 @@
1
+
2
+ import {assert} from '@webex/test-helper-chai';
3
+ import PeerConnectionUtils from '@webex/plugin-meetings/src/peer-connection-manager/util';
4
+
5
+ describe('Peerconnection Manager', () => {
6
+ describe('Utils', () => {
7
+ describe('convertCLineToIpv4', () => {
8
+ it('changes ipv6 to ipv4 default', () => {
9
+ const localSdp = 'v=0\r\n' +
10
+ 'm=video 5004 UDP/TLS/RTP/SAVPF 102 127 97 99\r\n' +
11
+ 'c=IN IP6 2607:fb90:d27c:b314:211a:32dd:c47f:ffe\r\n' +
12
+ 'a=rtpmap:127 H264/90000\r\n';
13
+ const resultSdp = 'v=0\r\n' +
14
+ 'm=video 5004 UDP/TLS/RTP/SAVPF 102 127 97 99\r\n' +
15
+ 'c=IN IP4 0.0.0.0\r\n' +
16
+ 'a=rtpmap:127 H264/90000\r\n';
17
+
18
+
19
+ const temp = PeerConnectionUtils.convertCLineToIpv4(localSdp);
20
+
21
+ assert.equal(temp, resultSdp);
22
+ });
23
+ });
24
+ });
25
+ });