@webex/plugin-meetings 2.34.0 → 2.35.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webex/plugin-meetings",
3
- "version": "2.34.0",
3
+ "version": "2.35.1",
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,7 +13,11 @@
13
13
  ],
14
14
  "main": "dist/index.js",
15
15
  "devMain": "src/index.js",
16
- "repository": "https://github.com/webex/webex-js-sdk/tree/master/packages/@webex/plugin-meetings",
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
22
  "node": ">=14"
19
23
  },
@@ -24,30 +28,30 @@
24
28
  ]
25
29
  },
26
30
  "devDependencies": {
27
- "@webex/plugin-meetings": "2.34.0",
28
- "@webex/test-helper-chai": "2.34.0",
29
- "@webex/test-helper-mocha": "2.34.0",
30
- "@webex/test-helper-mock-webex": "2.34.0",
31
- "@webex/test-helper-retry": "2.34.0",
32
- "@webex/test-helper-test-users": "2.34.0",
31
+ "@webex/plugin-meetings": "2.35.1",
32
+ "@webex/test-helper-chai": "2.35.1",
33
+ "@webex/test-helper-mocha": "2.35.1",
34
+ "@webex/test-helper-mock-webex": "2.35.1",
35
+ "@webex/test-helper-retry": "2.35.1",
36
+ "@webex/test-helper-test-users": "2.35.1",
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": "2.34.0",
43
+ "@webex/common": "2.35.1",
40
44
  "@webex/internal-media-core": "^0.0.7-beta",
41
- "@webex/internal-plugin-conversation": "2.34.0",
42
- "@webex/internal-plugin-device": "2.34.0",
43
- "@webex/internal-plugin-mercury": "2.34.0",
44
- "@webex/internal-plugin-metrics": "2.34.0",
45
- "@webex/internal-plugin-support": "2.34.0",
46
- "@webex/internal-plugin-user": "2.34.0",
47
- "@webex/plugin-people": "2.34.0",
48
- "@webex/plugin-rooms": "2.34.0",
45
+ "@webex/internal-plugin-conversation": "2.35.1",
46
+ "@webex/internal-plugin-device": "2.35.1",
47
+ "@webex/internal-plugin-mercury": "2.35.1",
48
+ "@webex/internal-plugin-metrics": "2.35.1",
49
+ "@webex/internal-plugin-support": "2.35.1",
50
+ "@webex/internal-plugin-user": "2.35.1",
51
+ "@webex/plugin-people": "2.35.1",
52
+ "@webex/plugin-rooms": "2.35.1",
49
53
  "@webex/ts-sdp": "^1.0.1",
50
- "@webex/webex-core": "2.34.0",
54
+ "@webex/webex-core": "2.35.1",
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 AVALIABLE_RESOLUTIONS = {
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]: AVALIABLE_RESOLUTIONS['480p'],
1028
- [QUALITY_LEVELS.MEDIUM]: AVALIABLE_RESOLUTIONS['720p'],
1029
- [QUALITY_LEVELS.HIGH]: AVALIABLE_RESOLUTIONS['1080p'],
1030
- [QUALITY_LEVELS['360p']]: AVALIABLE_RESOLUTIONS['360p'],
1031
- [QUALITY_LEVELS['480p']]: AVALIABLE_RESOLUTIONS['480p'],
1032
- [QUALITY_LEVELS['720p']]: AVALIABLE_RESOLUTIONS['720p'],
1033
- [QUALITY_LEVELS['1080p']]: AVALIABLE_RESOLUTIONS['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
  /**
@@ -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
  }
@@ -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
 
@@ -36,7 +36,6 @@ import {
36
36
  _INCOMING_,
37
37
  _JOIN_,
38
38
  AUDIO,
39
- CONNECTION_STATE,
40
39
  CONTENT,
41
40
  ENDED,
42
41
  EVENT_TRIGGERS,
@@ -58,7 +57,6 @@ import {
58
57
  ONLINE,
59
58
  OFFLINE,
60
59
  PASSWORD_STATUS,
61
- PC_BAIL_TIMEOUT,
62
60
  PSTN_STATUS,
63
61
  QUALITY_LEVELS,
64
62
  RECORDING_STATE,
@@ -2746,7 +2744,7 @@ export default class Meeting extends StatelessWebexPlugin {
2746
2744
  const {localQualityLevel} = this.mediaProperties;
2747
2745
 
2748
2746
  if (Number(localQualityLevel.slice(0, -1)) > height) {
2749
- LoggerProxy.logger.error(`Meeting:index#setLocalVideoTrack --> Local video quality of ${localQualityLevel} not supported,
2747
+ LoggerProxy.logger.warn(`Meeting:index#setLocalVideoTrack --> Local video quality of ${localQualityLevel} not supported,
2750
2748
  downscaling to highest possible resolution of ${height}p`);
2751
2749
 
2752
2750
  this.mediaProperties.setLocalQualityLevel(`${height}p`);
@@ -4014,6 +4012,16 @@ export default class Meeting extends StatelessWebexPlugin {
4014
4012
  LoggerProxy.logger.warn('Meeting:index#getMediaStreams --> Please use `meeting.shareScreen()` to manually start the screen share after successfully joining the meeting');
4015
4013
  }
4016
4014
 
4015
+ if (audioVideo && isString(audioVideo)) {
4016
+ if (Object.keys(VIDEO_RESOLUTIONS).includes(audioVideo)) {
4017
+ this.mediaProperties.setLocalQualityLevel(audioVideo);
4018
+ audioVideo = {video: VIDEO_RESOLUTIONS[audioVideo].video};
4019
+ }
4020
+ else {
4021
+ throw new ParameterError(`${audioVideo} not supported. Either pass level from pre-defined resolutions or pass complete audioVideo object`);
4022
+ }
4023
+ }
4024
+
4017
4025
  if (!audioVideo.video) {
4018
4026
  audioVideo = {...audioVideo, video: {...audioVideo.video, ...VIDEO_RESOLUTIONS[this.mediaProperties.localQualityLevel].video}};
4019
4027
  }
@@ -4246,129 +4254,110 @@ export default class Meeting extends StatelessWebexPlugin {
4246
4254
  enableRtx: this.config.enableRtx,
4247
4255
  enableExtmap: this.config.enableExtmap,
4248
4256
  setStartLocalSDPGenRemoteSDPRecvDelay: this.setStartLocalSDPGenRemoteSDPRecvDelay.bind(this)
4249
- })
4250
- .then((peerConnection) => this.getDevices().then((devices) => {
4251
- MeetingUtil.handleDeviceLogging(devices);
4257
+ }))
4258
+ .then((peerConnection) => this.getDevices().then((devices) => {
4259
+ MeetingUtil.handleDeviceLogging(devices);
4252
4260
 
4253
- return peerConnection;
4254
- }))
4255
- .then((peerConnection) => {
4256
- this.handleMediaLogging(this.mediaProperties);
4257
- LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection Received from attachMedia `);
4261
+ return peerConnection;
4262
+ }))
4263
+ .then((peerConnection) => {
4264
+ this.handleMediaLogging(this.mediaProperties);
4265
+ LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection Received from attachMedia `);
4266
+
4267
+ this.setRemoteStream(peerConnection);
4268
+ if (this.config.stats.enableStatsAnalyzer) {
4269
+ // TODO: ** Dont re create StatsAnalyzer on reconnect or rejoin
4270
+ this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
4271
+ this.statsAnalyzer = new StatsAnalyzer(this.config.stats, this.networkQualityMonitor);
4272
+ this.setupStatsAnalyzerEventHandlers();
4273
+ this.networkQualityMonitor.on(EVENT_TRIGGERS.NETWORK_QUALITY, this.sendNetworkQualityEvent.bind(this));
4274
+ }
4275
+ })
4276
+ .catch((error) => {
4277
+ LoggerProxy.logger.error(`${LOG_HEADER} Error adding media , setting up peerconnection, `, error);
4258
4278
 
4259
- this.setRemoteStream(peerConnection);
4260
- if (this.config.stats.enableStatsAnalyzer) {
4261
- // TODO: ** Dont re create StatsAnalyzer on reconnect or rejoin
4262
- this.networkQualityMonitor = new NetworkQualityMonitor(this.config.stats);
4263
- this.statsAnalyzer = new StatsAnalyzer(this.config.stats, this.networkQualityMonitor);
4264
- this.setupStatsAnalyzerEventHandlers();
4265
- this.networkQualityMonitor.on(EVENT_TRIGGERS.NETWORK_QUALITY, this.sendNetworkQualityEvent.bind(this));
4279
+ Metrics.sendBehavioralMetric(
4280
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
4281
+ {
4282
+ correlation_id: this.correlationId,
4283
+ locus_id: this.locusUrl.split('/').pop(),
4284
+ reason: error.message,
4285
+ stack: error.stack,
4286
+ turnDiscoverySkippedReason,
4287
+ turnServerUsed
4266
4288
  }
4267
- })
4268
- .catch((error) => {
4269
- LoggerProxy.logger.error(`${LOG_HEADER} Error adding media , setting up peerconnection, `, error);
4270
-
4271
- Metrics.sendBehavioralMetric(
4272
- BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
4273
- {
4274
- correlation_id: this.correlationId,
4275
- locus_id: this.locusUrl.split('/').pop(),
4276
- reason: error.message,
4277
- stack: error.stack,
4278
- turnDiscoverySkippedReason,
4279
- turnServerUsed
4280
- }
4281
- );
4289
+ );
4282
4290
 
4283
- throw error;
4284
- })
4285
- .then(() => new Promise((resolve, reject) => {
4286
- let timerCount = 0;
4291
+ throw error;
4292
+ })
4293
+ .then(() => new Promise((resolve, reject) => {
4294
+ let timerCount = 0;
4287
4295
 
4288
- // eslint-disable-next-line func-names
4289
- // eslint-disable-next-line prefer-arrow-callback
4290
- if (this.type === _CALL_) {
4296
+ // eslint-disable-next-line func-names
4297
+ // eslint-disable-next-line prefer-arrow-callback
4298
+ if (this.type === _CALL_) {
4299
+ resolve();
4300
+ }
4301
+ const joiningTimer = setInterval(() => {
4302
+ timerCount += 1;
4303
+ if (this.meetingState === FULL_STATE.ACTIVE) {
4304
+ clearInterval(joiningTimer);
4291
4305
  resolve();
4292
4306
  }
4293
- const joiningTimer = setInterval(() => {
4294
- timerCount += 1;
4295
- if (this.meetingState === FULL_STATE.ACTIVE) {
4296
- clearInterval(joiningTimer);
4297
- resolve();
4298
- }
4299
4307
 
4300
- if (timerCount === 4) {
4301
- clearInterval(joiningTimer);
4302
- reject(new Error('Meeting is still not active '));
4303
- }
4304
- }, 1000);
4308
+ if (timerCount === 4) {
4309
+ clearInterval(joiningTimer);
4310
+ reject(new Error('Meeting is still not active '));
4311
+ }
4312
+ }, 1000);
4313
+ }))
4314
+ .then(() =>
4315
+ logRequest(this.roap
4316
+ .sendRoapMediaRequest({
4317
+ sdp: this.mediaProperties.peerConnection.sdp,
4318
+ roapSeq: this.roapSeq,
4319
+ meeting: this // or can pass meeting ID
4320
+ }), {
4321
+ header: `${LOG_HEADER} Send Roap Media Request.`,
4322
+ success: `${LOG_HEADER} Successfully send roap media request`,
4323
+ failure: `${LOG_HEADER} Error joining the call on send roap media request, `
4305
4324
  }))
4306
- .then(() =>
4307
- logRequest(this.roap
4308
- .sendRoapMediaRequest({
4309
- sdp: this.mediaProperties.peerConnection.sdp,
4310
- roapSeq: this.roapSeq,
4311
- meeting: this // or can pass meeting ID
4312
- }), {
4313
- header: `${LOG_HEADER} Send Roap Media Request.`,
4314
- success: `${LOG_HEADER} Successfully send roap media request`,
4315
- failure: `${LOG_HEADER} Error joining the call on send roap media request, `
4316
- }))
4317
- .then(() => {
4318
- const {peerConnection} = this.mediaProperties;
4319
-
4320
- return new Promise((resolve, reject) => {
4321
- if (peerConnection.connectionState === CONNECTION_STATE.CONNECTED) {
4322
- LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection CONNECTED`);
4323
-
4324
- resolve(peerConnection);
4325
-
4326
- return;
4327
- }
4328
- // Check if Peer Connection is STABLE (connected)
4329
- const stabilityTimeout = setTimeout(() => {
4330
- if (peerConnection.connectionState !== CONNECTION_STATE.CONNECTED) {
4331
- // TODO: Fix this after the error code pr goes in
4332
- reject(createMeetingsError(30202, 'Meeting connection failed'));
4333
- }
4334
- else {
4335
- LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection CONNECTED`);
4336
- resolve(peerConnection);
4337
- }
4338
- }, PC_BAIL_TIMEOUT);
4339
-
4340
- this.once(EVENT_TRIGGERS.MEDIA_READY, () => {
4341
- LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection CONNECTED, clearing stability timer.`);
4342
- clearTimeout(stabilityTimeout);
4343
- resolve(peerConnection);
4344
- });
4345
- });
4346
- })
4347
- .then(() => {
4348
- if (mediaSettings && mediaSettings.sendShare && localShare) {
4349
- if (this.state === MEETING_STATE.STATES.JOINED) {
4350
- return this.share();
4351
- }
4325
+ .then(
4326
+ () => this.mediaProperties.waitForIceConnectedState()
4327
+ .catch(() => {
4328
+ throw createMeetingsError(30202, 'Meeting connection failed');
4329
+ })
4330
+ )
4331
+ .then(() => {
4332
+ LoggerProxy.logger.info(`${LOG_HEADER} PeerConnection CONNECTED`);
4352
4333
 
4353
- // When the self state changes to JOINED then request the floor
4354
- this.floorGrantPending = true;
4334
+ if (mediaSettings && mediaSettings.sendShare && localShare) {
4335
+ if (this.state === MEETING_STATE.STATES.JOINED) {
4336
+ return this.share();
4355
4337
  }
4356
4338
 
4357
- Metrics.sendBehavioralMetric(
4358
- BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS,
4359
- {
4360
- correlation_id: this.correlationId,
4361
- locus_id: this.locusUrl.split('/').pop()
4362
- }
4363
- );
4339
+ // When the self state changes to JOINED then request the floor
4340
+ this.floorGrantPending = true;
4341
+ }
4364
4342
 
4365
- return Promise.resolve();
4366
- }))
4343
+ return {};
4344
+ })
4345
+ .then(() => this.mediaProperties.getCurrentConnectionType())
4346
+ .then((connectionType) => {
4347
+ Metrics.sendBehavioralMetric(
4348
+ BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS,
4349
+ {
4350
+ correlation_id: this.correlationId,
4351
+ locus_id: this.locusUrl.split('/').pop(),
4352
+ connectionType
4353
+ }
4354
+ );
4355
+ })
4367
4356
  .catch((error) => {
4368
4357
  // Clean up stats analyzer, peer connection, and turn off listeners
4369
4358
  const stopStatsAnalyzer = (this.statsAnalyzer) ? this.statsAnalyzer.stopAnalyzer() : Promise.resolve();
4370
4359
 
4371
- stopStatsAnalyzer
4360
+ return stopStatsAnalyzer
4372
4361
  .then(() => {
4373
4362
  this.statsAnalyzer = null;
4374
4363