@webex/plugin-meetings 2.34.0 → 3.0.0-beta.0
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.
- package/dist/constants.js +4 -4
- package/dist/constants.js.map +1 -1
- package/dist/media/properties.js +139 -0
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/index.js +17 -3
- package/dist/meeting/index.js.map +1 -1
- package/package.json +23 -19
- package/src/constants.ts +8 -8
- package/src/media/properties.js +97 -0
- package/src/meeting/index.js +12 -2
- package/test/unit/spec/media/properties.ts +305 -0
- package/test/unit/spec/meeting/index.js +38 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webex/plugin-meetings",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0-beta.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "Cisco EULA (https://www.cisco.com/c/en/us/products/end-user-license-agreement.html)",
|
|
6
6
|
"contributors": [
|
|
@@ -13,9 +13,13 @@
|
|
|
13
13
|
],
|
|
14
14
|
"main": "dist/index.js",
|
|
15
15
|
"devMain": "src/index.js",
|
|
16
|
-
"repository":
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/webex/webex-js-sdk.git",
|
|
19
|
+
"directory": "packages/@webex/plugin-meetings"
|
|
20
|
+
},
|
|
17
21
|
"engines": {
|
|
18
|
-
"node": ">=
|
|
22
|
+
"node": ">=16"
|
|
19
23
|
},
|
|
20
24
|
"browserify": {
|
|
21
25
|
"transform": [
|
|
@@ -24,30 +28,30 @@
|
|
|
24
28
|
]
|
|
25
29
|
},
|
|
26
30
|
"devDependencies": {
|
|
27
|
-
"@webex/plugin-meetings": "
|
|
28
|
-
"@webex/test-helper-chai": "
|
|
29
|
-
"@webex/test-helper-mocha": "
|
|
30
|
-
"@webex/test-helper-mock-webex": "
|
|
31
|
-
"@webex/test-helper-retry": "
|
|
32
|
-
"@webex/test-helper-test-users": "
|
|
31
|
+
"@webex/plugin-meetings": "3.0.0-beta.0",
|
|
32
|
+
"@webex/test-helper-chai": "3.0.0-beta.0",
|
|
33
|
+
"@webex/test-helper-mocha": "3.0.0-beta.0",
|
|
34
|
+
"@webex/test-helper-mock-webex": "3.0.0-beta.0",
|
|
35
|
+
"@webex/test-helper-retry": "3.0.0-beta.0",
|
|
36
|
+
"@webex/test-helper-test-users": "3.0.0-beta.0",
|
|
33
37
|
"chai": "^4.3.4",
|
|
34
38
|
"chai-as-promised": "^7.1.1",
|
|
35
39
|
"jsdom-global": "3.0.2",
|
|
36
40
|
"sinon": "^9.2.4"
|
|
37
41
|
},
|
|
38
42
|
"dependencies": {
|
|
39
|
-
"@webex/common": "
|
|
43
|
+
"@webex/common": "3.0.0-beta.0",
|
|
40
44
|
"@webex/internal-media-core": "^0.0.7-beta",
|
|
41
|
-
"@webex/internal-plugin-conversation": "
|
|
42
|
-
"@webex/internal-plugin-device": "
|
|
43
|
-
"@webex/internal-plugin-mercury": "
|
|
44
|
-
"@webex/internal-plugin-metrics": "
|
|
45
|
-
"@webex/internal-plugin-support": "
|
|
46
|
-
"@webex/internal-plugin-user": "
|
|
47
|
-
"@webex/plugin-people": "
|
|
48
|
-
"@webex/plugin-rooms": "
|
|
45
|
+
"@webex/internal-plugin-conversation": "3.0.0-beta.0",
|
|
46
|
+
"@webex/internal-plugin-device": "3.0.0-beta.0",
|
|
47
|
+
"@webex/internal-plugin-mercury": "3.0.0-beta.0",
|
|
48
|
+
"@webex/internal-plugin-metrics": "3.0.0-beta.0",
|
|
49
|
+
"@webex/internal-plugin-support": "3.0.0-beta.0",
|
|
50
|
+
"@webex/internal-plugin-user": "3.0.0-beta.0",
|
|
51
|
+
"@webex/plugin-people": "3.0.0-beta.0",
|
|
52
|
+
"@webex/plugin-rooms": "3.0.0-beta.0",
|
|
49
53
|
"@webex/ts-sdp": "^1.0.1",
|
|
50
|
-
"@webex/webex-core": "
|
|
54
|
+
"@webex/webex-core": "3.0.0-beta.0",
|
|
51
55
|
"bowser": "^2.11.0",
|
|
52
56
|
"btoa": "^1.2.1",
|
|
53
57
|
"dotenv": "^4.0.0",
|
package/src/constants.ts
CHANGED
|
@@ -972,7 +972,7 @@ export const QUALITY_LEVELS = {
|
|
|
972
972
|
};
|
|
973
973
|
|
|
974
974
|
|
|
975
|
-
export const
|
|
975
|
+
export const AVAILABLE_RESOLUTIONS = {
|
|
976
976
|
'360p': {
|
|
977
977
|
video: {
|
|
978
978
|
width: {
|
|
@@ -1024,13 +1024,13 @@ export const AVALIABLE_RESOLUTIONS = {
|
|
|
1024
1024
|
};
|
|
1025
1025
|
|
|
1026
1026
|
export const VIDEO_RESOLUTIONS = {
|
|
1027
|
-
[QUALITY_LEVELS.LOW]:
|
|
1028
|
-
[QUALITY_LEVELS.MEDIUM]:
|
|
1029
|
-
[QUALITY_LEVELS.HIGH]:
|
|
1030
|
-
[QUALITY_LEVELS['360p']]:
|
|
1031
|
-
[QUALITY_LEVELS['480p']]:
|
|
1032
|
-
[QUALITY_LEVELS['720p']]:
|
|
1033
|
-
[QUALITY_LEVELS['1080p']]:
|
|
1027
|
+
[QUALITY_LEVELS.LOW]: AVAILABLE_RESOLUTIONS['480p'],
|
|
1028
|
+
[QUALITY_LEVELS.MEDIUM]: AVAILABLE_RESOLUTIONS['720p'],
|
|
1029
|
+
[QUALITY_LEVELS.HIGH]: AVAILABLE_RESOLUTIONS['1080p'],
|
|
1030
|
+
[QUALITY_LEVELS['360p']]: AVAILABLE_RESOLUTIONS['360p'],
|
|
1031
|
+
[QUALITY_LEVELS['480p']]: AVAILABLE_RESOLUTIONS['480p'],
|
|
1032
|
+
[QUALITY_LEVELS['720p']]: AVAILABLE_RESOLUTIONS['720p'],
|
|
1033
|
+
[QUALITY_LEVELS['1080p']]: AVAILABLE_RESOLUTIONS['1080p'],
|
|
1034
1034
|
};
|
|
1035
1035
|
|
|
1036
1036
|
/**
|
package/src/media/properties.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ICE_STATE,
|
|
2
3
|
MEETINGS,
|
|
4
|
+
PC_BAIL_TIMEOUT,
|
|
3
5
|
QUALITY_LEVELS
|
|
4
6
|
} from '../constants';
|
|
5
7
|
import LoggerProxy from '../common/logs/logger-proxy';
|
|
@@ -195,4 +197,99 @@ export default class MediaProperties {
|
|
|
195
197
|
this.unsetLocalVideoTrack();
|
|
196
198
|
this.unsetRemoteMedia();
|
|
197
199
|
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Waits until ice connection is established
|
|
203
|
+
*
|
|
204
|
+
* @returns {Promise<void>}
|
|
205
|
+
*/
|
|
206
|
+
waitForIceConnectedState() {
|
|
207
|
+
const isIceConnected = () => (
|
|
208
|
+
this.peerConnection.iceConnectionState === ICE_STATE.CONNECTED ||
|
|
209
|
+
this.peerConnection.iceConnectionState === ICE_STATE.COMPLETED
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (isIceConnected()) {
|
|
213
|
+
return Promise.resolve();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
let timer;
|
|
218
|
+
|
|
219
|
+
const iceListener = () => {
|
|
220
|
+
LoggerProxy.logger.log(`Media:properties#waitForIceConnectedState --> ice state: ${this.peerConnection.iceConnectionState}, conn state: ${this.peerConnection.connectionState}`);
|
|
221
|
+
|
|
222
|
+
if (isIceConnected()) {
|
|
223
|
+
clearTimeout(timer);
|
|
224
|
+
this.peerConnection.removeEventListener('iceconnectionstatechange', iceListener);
|
|
225
|
+
resolve();
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
timer = setTimeout(() => {
|
|
230
|
+
this.peerConnection.removeEventListener('iceconnectionstatechange', iceListener);
|
|
231
|
+
reject();
|
|
232
|
+
}, PC_BAIL_TIMEOUT);
|
|
233
|
+
|
|
234
|
+
this.peerConnection.addEventListener('iceconnectionstatechange', iceListener);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Returns the type of a connection that has been established
|
|
240
|
+
*
|
|
241
|
+
* @returns {Promise<'UDP' | 'TCP' | 'TURN-TLS' | 'TURN-TCP' | 'TURN-UDP' | 'unknown'>}
|
|
242
|
+
*/
|
|
243
|
+
async getCurrentConnectionType() {
|
|
244
|
+
// we can only get the connection type after ICE connection has been established
|
|
245
|
+
await this.waitForIceConnectedState();
|
|
246
|
+
|
|
247
|
+
const allStatsReports = [];
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
// eslint-disable-next-line no-await-in-loop
|
|
251
|
+
const statsResult = await this.peerConnection.getStats();
|
|
252
|
+
|
|
253
|
+
statsResult.forEach((report) => allStatsReports.push(report));
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
LoggerProxy.logger.warn(`Media:properties#getCurrentConnectionType --> getStats() failed: ${error}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const successfulCandidatePairs = allStatsReports.filter(
|
|
260
|
+
(report) => report.type === 'candidate-pair' && report.state?.toLowerCase() === 'succeeded'
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
let foundConnectionType = 'unknown';
|
|
264
|
+
|
|
265
|
+
// all of the successful pairs should have the same connection type, so just return the type for the first one
|
|
266
|
+
successfulCandidatePairs.some((pair) => {
|
|
267
|
+
const localCandidate = allStatsReports.find((report) => report.type === 'local-candidate' && report.id === pair.localCandidateId);
|
|
268
|
+
|
|
269
|
+
if (localCandidate === undefined) {
|
|
270
|
+
LoggerProxy.logger.warn(`Media:properties#getCurrentConnectionType --> failed to find local candidate "${pair.localCandidateId}" in getStats() results`);
|
|
271
|
+
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let connectionType;
|
|
276
|
+
|
|
277
|
+
if (localCandidate.relayProtocol) {
|
|
278
|
+
connectionType = `TURN-${localCandidate.relayProtocol.toUpperCase()}`;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
connectionType = localCandidate.protocol?.toUpperCase(); // it will be UDP or TCP
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (connectionType) {
|
|
285
|
+
foundConnectionType = connectionType;
|
|
286
|
+
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return false;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return foundConnectionType;
|
|
294
|
+
}
|
|
198
295
|
}
|
package/src/meeting/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import uuid from 'uuid';
|
|
2
|
-
import {cloneDeep, isEqual, pick} from 'lodash';
|
|
2
|
+
import {cloneDeep, isEqual, pick, isString} from 'lodash';
|
|
3
3
|
import {StatelessWebexPlugin} from '@webex/webex-core';
|
|
4
4
|
import {Media as WebRTCMedia} from '@webex/internal-media-core';
|
|
5
5
|
|
|
@@ -2746,7 +2746,7 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
2746
2746
|
const {localQualityLevel} = this.mediaProperties;
|
|
2747
2747
|
|
|
2748
2748
|
if (Number(localQualityLevel.slice(0, -1)) > height) {
|
|
2749
|
-
LoggerProxy.logger.
|
|
2749
|
+
LoggerProxy.logger.warn(`Meeting:index#setLocalVideoTrack --> Local video quality of ${localQualityLevel} not supported,
|
|
2750
2750
|
downscaling to highest possible resolution of ${height}p`);
|
|
2751
2751
|
|
|
2752
2752
|
this.mediaProperties.setLocalQualityLevel(`${height}p`);
|
|
@@ -4014,6 +4014,16 @@ export default class Meeting extends StatelessWebexPlugin {
|
|
|
4014
4014
|
LoggerProxy.logger.warn('Meeting:index#getMediaStreams --> Please use `meeting.shareScreen()` to manually start the screen share after successfully joining the meeting');
|
|
4015
4015
|
}
|
|
4016
4016
|
|
|
4017
|
+
if (audioVideo && isString(audioVideo)) {
|
|
4018
|
+
if (Object.keys(VIDEO_RESOLUTIONS).includes(audioVideo)) {
|
|
4019
|
+
this.mediaProperties.setLocalQualityLevel(audioVideo);
|
|
4020
|
+
audioVideo = {video: VIDEO_RESOLUTIONS[audioVideo].video};
|
|
4021
|
+
}
|
|
4022
|
+
else {
|
|
4023
|
+
throw new ParameterError(`${audioVideo} not supported. Either pass level from pre-defined resolutions or pass complete audioVideo object`);
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
|
|
4017
4027
|
if (!audioVideo.video) {
|
|
4018
4028
|
audioVideo = {...audioVideo, video: {...audioVideo.video, ...VIDEO_RESOLUTIONS[this.mediaProperties.localQualityLevel].video}};
|
|
4019
4029
|
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import {assert} from '@webex/test-helper-chai';
|
|
2
|
+
import sinon from 'sinon';
|
|
3
|
+
import MediaProperties from '@webex/plugin-meetings/src/media/properties';
|
|
4
|
+
import MediaUtil from '@webex/plugin-meetings/src/media/util';
|
|
5
|
+
import testUtils from '../../../utils/testUtils';
|
|
6
|
+
import {PC_BAIL_TIMEOUT} from '@webex/plugin-meetings/src/constants';
|
|
7
|
+
import {Defer} from '@webex/common';
|
|
8
|
+
|
|
9
|
+
describe('MediaProperties', () => {
|
|
10
|
+
let mediaProperties;
|
|
11
|
+
let mockPc;
|
|
12
|
+
let clock;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
clock = sinon.useFakeTimers();
|
|
16
|
+
|
|
17
|
+
mockPc = {
|
|
18
|
+
getStats: sinon.stub().resolves([]),
|
|
19
|
+
addEventListener: sinon.stub(),
|
|
20
|
+
removeEventListener: sinon.stub(),
|
|
21
|
+
iceConnectionState: 'connected',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
sinon.stub(MediaUtil, 'createPeerConnection').returns(mockPc);
|
|
25
|
+
|
|
26
|
+
mediaProperties = new MediaProperties();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
clock.restore();
|
|
31
|
+
sinon.restore();
|
|
32
|
+
});
|
|
33
|
+
describe('waitForIceConnectedState', () => {
|
|
34
|
+
it('resolves immediately if ice state is connected', async () => {
|
|
35
|
+
mockPc.iceConnectionState = 'connected';
|
|
36
|
+
|
|
37
|
+
await mediaProperties.waitForIceConnectedState();
|
|
38
|
+
});
|
|
39
|
+
it('resolves immediately if ice state is completed', async () => {
|
|
40
|
+
mockPc.iceConnectionState = 'completed';
|
|
41
|
+
|
|
42
|
+
await mediaProperties.waitForIceConnectedState();
|
|
43
|
+
});
|
|
44
|
+
it('rejects after timeout if ice state does not reach connected/completed', async () => {
|
|
45
|
+
mockPc.iceConnectionState = 'connecting';
|
|
46
|
+
|
|
47
|
+
let promiseResolved = false;
|
|
48
|
+
let promiseRejected = false;
|
|
49
|
+
|
|
50
|
+
mediaProperties
|
|
51
|
+
.waitForIceConnectedState()
|
|
52
|
+
.then(() => {
|
|
53
|
+
promiseResolved = true;
|
|
54
|
+
})
|
|
55
|
+
.catch(() => {
|
|
56
|
+
promiseRejected = true;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
assert.equal(promiseResolved, false);
|
|
60
|
+
assert.equal(promiseRejected, false);
|
|
61
|
+
|
|
62
|
+
await clock.tickAsync(PC_BAIL_TIMEOUT);
|
|
63
|
+
await testUtils.flushPromises();
|
|
64
|
+
|
|
65
|
+
assert.equal(promiseResolved, false);
|
|
66
|
+
assert.equal(promiseRejected, true);
|
|
67
|
+
|
|
68
|
+
// check that listener was registered and removed
|
|
69
|
+
assert.calledOnce(mockPc.addEventListener);
|
|
70
|
+
assert.equal(mockPc.addEventListener.getCall(0).args[0], 'iceconnectionstatechange');
|
|
71
|
+
const listener = mockPc.addEventListener.getCall(0).args[1];
|
|
72
|
+
|
|
73
|
+
assert.calledOnce(mockPc.removeEventListener);
|
|
74
|
+
assert.calledWith(mockPc.removeEventListener, 'iceconnectionstatechange', listener);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
['connected', 'completed'].forEach((successIceState) =>
|
|
78
|
+
it(`resolves when ice state reaches ${successIceState}`, async () => {
|
|
79
|
+
mockPc.iceConnectionState = 'connecting';
|
|
80
|
+
|
|
81
|
+
const clearTimeoutSpy = sinon.spy(clock, 'clearTimeout');
|
|
82
|
+
|
|
83
|
+
let promiseResolved = false;
|
|
84
|
+
let promiseRejected = false;
|
|
85
|
+
|
|
86
|
+
mediaProperties
|
|
87
|
+
.waitForIceConnectedState()
|
|
88
|
+
.then(() => {
|
|
89
|
+
promiseResolved = true;
|
|
90
|
+
})
|
|
91
|
+
.catch(() => {
|
|
92
|
+
promiseRejected = true;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
assert.equal(promiseResolved, false);
|
|
96
|
+
assert.equal(promiseRejected, false);
|
|
97
|
+
|
|
98
|
+
// check the right listener was registered
|
|
99
|
+
assert.calledOnce(mockPc.addEventListener);
|
|
100
|
+
assert.equal(mockPc.addEventListener.getCall(0).args[0], 'iceconnectionstatechange');
|
|
101
|
+
const listener = mockPc.addEventListener.getCall(0).args[1];
|
|
102
|
+
|
|
103
|
+
// call the listener and pretend we are now connected
|
|
104
|
+
mockPc.iceConnectionState = successIceState;
|
|
105
|
+
listener();
|
|
106
|
+
await testUtils.flushPromises();
|
|
107
|
+
|
|
108
|
+
assert.equal(promiseResolved, true);
|
|
109
|
+
assert.equal(promiseRejected, false);
|
|
110
|
+
|
|
111
|
+
// check that listener was removed
|
|
112
|
+
assert.calledOnce(mockPc.removeEventListener);
|
|
113
|
+
assert.calledWith(mockPc.removeEventListener, 'iceconnectionstatechange', listener);
|
|
114
|
+
|
|
115
|
+
assert.calledOnce(clearTimeoutSpy);
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('getCurrentConnectionType', () => {
|
|
121
|
+
it('calls waitForIceConnectedState', async () => {
|
|
122
|
+
const spy = sinon.stub(mediaProperties, 'waitForIceConnectedState');
|
|
123
|
+
|
|
124
|
+
await mediaProperties.getCurrentConnectionType();
|
|
125
|
+
|
|
126
|
+
assert.calledOnce(spy);
|
|
127
|
+
});
|
|
128
|
+
it('calls getStats() only after waitForIceConnectedState resolves', async () => {
|
|
129
|
+
const waitForIceConnectedStateResult = new Defer();
|
|
130
|
+
|
|
131
|
+
const waitForIceConnectedStateStub = sinon
|
|
132
|
+
.stub(mediaProperties, 'waitForIceConnectedState')
|
|
133
|
+
.returns(waitForIceConnectedStateResult.promise);
|
|
134
|
+
|
|
135
|
+
const result = mediaProperties.getCurrentConnectionType();
|
|
136
|
+
|
|
137
|
+
await testUtils.flushPromises();
|
|
138
|
+
|
|
139
|
+
assert.called(waitForIceConnectedStateStub);
|
|
140
|
+
assert.notCalled(mockPc.getStats);
|
|
141
|
+
|
|
142
|
+
waitForIceConnectedStateResult.resolve();
|
|
143
|
+
await testUtils.flushPromises();
|
|
144
|
+
|
|
145
|
+
assert.called(mockPc.getStats);
|
|
146
|
+
await result;
|
|
147
|
+
});
|
|
148
|
+
it('rejects if waitForIceConnectedState rejects', async () => {
|
|
149
|
+
const waitForIceConnectedStateResult = new Defer();
|
|
150
|
+
|
|
151
|
+
const waitForIceConnectedStateStub = sinon
|
|
152
|
+
.stub(mediaProperties, 'waitForIceConnectedState')
|
|
153
|
+
.returns(waitForIceConnectedStateResult.promise);
|
|
154
|
+
|
|
155
|
+
const result = mediaProperties.getCurrentConnectionType();
|
|
156
|
+
|
|
157
|
+
await testUtils.flushPromises();
|
|
158
|
+
|
|
159
|
+
assert.called(waitForIceConnectedStateStub);
|
|
160
|
+
|
|
161
|
+
waitForIceConnectedStateResult.reject(new Error('fake error'));
|
|
162
|
+
await testUtils.flushPromises();
|
|
163
|
+
|
|
164
|
+
assert.notCalled(mockPc.getStats);
|
|
165
|
+
|
|
166
|
+
await assert.isRejected(result);
|
|
167
|
+
});
|
|
168
|
+
it('returns "unknown" if getStats() fails', async () => {
|
|
169
|
+
mockPc.getStats.rejects(new Error());
|
|
170
|
+
|
|
171
|
+
const connectionType = await mediaProperties.getCurrentConnectionType();
|
|
172
|
+
assert.equal(connectionType, 'unknown');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('returns "unknown" if getStats() returns no candidate pairs', async () => {
|
|
176
|
+
mockPc.getStats.resolves([{type: 'something', id: '1234'}]);
|
|
177
|
+
|
|
178
|
+
const connectionType = await mediaProperties.getCurrentConnectionType();
|
|
179
|
+
assert.equal(connectionType, 'unknown');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('returns "unknown" if getStats() returns no successful candidate pair', async () => {
|
|
183
|
+
mockPc.getStats.resolves([{type: 'candidate-pair', id: '1234', state: 'inprogress'}]);
|
|
184
|
+
|
|
185
|
+
const connectionType = await mediaProperties.getCurrentConnectionType();
|
|
186
|
+
assert.equal(connectionType, 'unknown');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('returns "unknown" if getStats() returns a successful candidate pair but local candidate is missing', async () => {
|
|
190
|
+
mockPc.getStats.resolves([
|
|
191
|
+
{type: 'candidate-pair', id: '1234', state: 'succeeded', localCandidateId: 'wrong id'},
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
const connectionType = await mediaProperties.getCurrentConnectionType();
|
|
195
|
+
assert.equal(connectionType, 'unknown');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('returns "UDP" if getStats() returns a successful candidate pair with udp local candidate', async () => {
|
|
199
|
+
mockPc.getStats.resolves([
|
|
200
|
+
{
|
|
201
|
+
type: 'candidate-pair',
|
|
202
|
+
id: 'some candidate pair id',
|
|
203
|
+
state: 'succeeded',
|
|
204
|
+
localCandidateId: 'local candidate id',
|
|
205
|
+
},
|
|
206
|
+
{type: 'local-candidate', id: 'some other candidate id', protocol: 'tcp'},
|
|
207
|
+
{type: 'local-candidate', id: 'local candidate id', protocol: 'udp'},
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
const connectionType = await mediaProperties.getCurrentConnectionType();
|
|
211
|
+
assert.equal(connectionType, 'UDP');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('returns "TCP" if getStats() returns a successful candidate pair with tcp local candidate', async () => {
|
|
215
|
+
mockPc.getStats.resolves([
|
|
216
|
+
{
|
|
217
|
+
type: 'candidate-pair',
|
|
218
|
+
id: 'some candidate pair id',
|
|
219
|
+
state: 'succeeded',
|
|
220
|
+
localCandidateId: 'some candidate id',
|
|
221
|
+
},
|
|
222
|
+
{type: 'local-candidate', id: 'some other candidate id', protocol: 'udp'},
|
|
223
|
+
{type: 'local-candidate', id: 'some candidate id', protocol: 'tcp'},
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
const connectionType = await mediaProperties.getCurrentConnectionType();
|
|
227
|
+
assert.equal(connectionType, 'TCP');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
[
|
|
231
|
+
{relayProtocol: 'tls', expectedConnectionType: 'TURN-TLS'},
|
|
232
|
+
{relayProtocol: 'tcp', expectedConnectionType: 'TURN-TCP'},
|
|
233
|
+
{relayProtocol: 'udp', expectedConnectionType: 'TURN-UDP'},
|
|
234
|
+
].forEach(({relayProtocol, expectedConnectionType}) =>
|
|
235
|
+
it(`returns "${expectedConnectionType}" if getStats() returns a successful candidate pair with a local candidate with relayProtocol=${relayProtocol}`, async () => {
|
|
236
|
+
mockPc.getStats.resolves([
|
|
237
|
+
{
|
|
238
|
+
type: 'candidate-pair',
|
|
239
|
+
id: 'some candidate pair id',
|
|
240
|
+
state: 'succeeded',
|
|
241
|
+
localCandidateId: 'selected candidate id',
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
type: 'candidate-pair',
|
|
245
|
+
id: 'some other candidate pair id',
|
|
246
|
+
state: 'failed',
|
|
247
|
+
localCandidateId: 'some other candidate id 1',
|
|
248
|
+
},
|
|
249
|
+
{type: 'local-candidate', id: 'some other candidate id 1', protocol: 'udp'},
|
|
250
|
+
{type: 'local-candidate', id: 'some other candidate id 2', protocol: 'tcp'},
|
|
251
|
+
{
|
|
252
|
+
type: 'local-candidate',
|
|
253
|
+
id: 'selected candidate id',
|
|
254
|
+
protocol: 'udp',
|
|
255
|
+
relayProtocol,
|
|
256
|
+
},
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
const connectionType = await mediaProperties.getCurrentConnectionType();
|
|
260
|
+
assert.equal(connectionType, expectedConnectionType);
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
it('returns connection type of the first successful candidate pair', async () => {
|
|
265
|
+
// in real life this will never happen and all active candidate pairs will have same transport,
|
|
266
|
+
// but here we're simulating a situation where they have different transports and just checking
|
|
267
|
+
// that the code still works and just returns the first one
|
|
268
|
+
mockPc.getStats.resolves([
|
|
269
|
+
{
|
|
270
|
+
type: 'inbound-rtp',
|
|
271
|
+
id: 'whatever',
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
type: 'candidate-pair',
|
|
275
|
+
id: 'some candidate pair id',
|
|
276
|
+
state: 'succeeded',
|
|
277
|
+
localCandidateId: '1st selected candidate id',
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
type: 'candidate-pair',
|
|
281
|
+
id: 'some other candidate pair id',
|
|
282
|
+
state: 'succeeded',
|
|
283
|
+
localCandidateId: '2nd selected candidate id',
|
|
284
|
+
},
|
|
285
|
+
{type: 'local-candidate', id: 'some other candidate id 1', protocol: 'udp'},
|
|
286
|
+
{type: 'local-candidate', id: 'some other candidate id 2', protocol: 'tcp'},
|
|
287
|
+
{
|
|
288
|
+
type: 'local-candidate',
|
|
289
|
+
id: '1st selected candidate id',
|
|
290
|
+
protocol: 'udp',
|
|
291
|
+
relayProtocol: 'tls',
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
type: 'local-candidate',
|
|
295
|
+
id: '2nd selected candidate id',
|
|
296
|
+
protocol: 'udp',
|
|
297
|
+
relayProtocol: 'tcp',
|
|
298
|
+
},
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
const connectionType = await mediaProperties.getCurrentConnectionType();
|
|
302
|
+
assert.equal(connectionType, 'TURN-TLS');
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
});
|
|
@@ -42,6 +42,7 @@ import BrowserDetection from '@webex/plugin-meetings/src/common/browser-detectio
|
|
|
42
42
|
import Metrics from '@webex/plugin-meetings/src/metrics';
|
|
43
43
|
import {trigger, eventType} from '@webex/plugin-meetings/src/metrics/config';
|
|
44
44
|
import BEHAVIORAL_METRICS from '@webex/plugin-meetings/src/metrics/constants';
|
|
45
|
+
import {IceGatheringFailed} from '@webex/plugin-meetings/src/common/errors/webex-errors';
|
|
45
46
|
|
|
46
47
|
import locus from '../fixture/locus';
|
|
47
48
|
import {
|
|
@@ -910,6 +911,8 @@ describe('plugin-meetings', () => {
|
|
|
910
911
|
|
|
911
912
|
beforeEach(() => {
|
|
912
913
|
meeting.mediaProperties.setMediaDirection = sinon.stub().returns(true);
|
|
914
|
+
meeting.mediaProperties.waitForIceConnectedState = sinon.stub().resolves();
|
|
915
|
+
meeting.mediaProperties.getCurrentConnectionType = sinon.stub().resolves('udp');
|
|
913
916
|
meeting.audio = muteStateStub;
|
|
914
917
|
meeting.video = muteStateStub;
|
|
915
918
|
Media.attachMedia = sinon.stub().returns(Promise.resolve([test1, test2]));
|
|
@@ -1151,6 +1154,41 @@ describe('plugin-meetings', () => {
|
|
|
1151
1154
|
});
|
|
1152
1155
|
});
|
|
1153
1156
|
|
|
1157
|
+
it('should reject if waitForIceConnectedState() rejects', async () => {
|
|
1158
|
+
meeting.meetingState = 'ACTIVE';
|
|
1159
|
+
meeting.mediaProperties.waitForIceConnectedState.rejects(new Error('fake error'));
|
|
1160
|
+
|
|
1161
|
+
let errorThrown = false;
|
|
1162
|
+
|
|
1163
|
+
await meeting.addMedia({
|
|
1164
|
+
mediaSettings: {}
|
|
1165
|
+
})
|
|
1166
|
+
.catch((error) => {
|
|
1167
|
+
assert.equal(error.code, IceGatheringFailed.CODE);
|
|
1168
|
+
errorThrown = true;
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
assert.isTrue(errorThrown);
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
it('should send ADD_MEDIA_SUCCESS metrics', async () => {
|
|
1175
|
+
meeting.meetingState = 'ACTIVE';
|
|
1176
|
+
await meeting.addMedia({
|
|
1177
|
+
mediaSettings: {}
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
assert.calledOnce(Metrics.sendBehavioralMetric);
|
|
1181
|
+
assert.calledWith(
|
|
1182
|
+
Metrics.sendBehavioralMetric,
|
|
1183
|
+
BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS,
|
|
1184
|
+
{
|
|
1185
|
+
correlation_id: meeting.correlationId,
|
|
1186
|
+
locus_id: meeting.locusUrl.split('/').pop(),
|
|
1187
|
+
connectionType: 'udp'
|
|
1188
|
+
}
|
|
1189
|
+
);
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1154
1192
|
describe('handles StatsAnalyzer events', () => {
|
|
1155
1193
|
let prevConfigValue;
|
|
1156
1194
|
let statsAnalyzerStub;
|