@webex/plugin-meetings 1.151.7 → 1.153.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.
Files changed (42) hide show
  1. package/dist/common/errors/captcha-error.js +64 -0
  2. package/dist/common/errors/captcha-error.js.map +1 -0
  3. package/dist/common/errors/password-error.js +64 -0
  4. package/dist/common/errors/password-error.js.map +1 -0
  5. package/dist/config.js +2 -1
  6. package/dist/config.js.map +1 -1
  7. package/dist/constants.js +33 -1
  8. package/dist/constants.js.map +1 -1
  9. package/dist/meeting/index.js +668 -432
  10. package/dist/meeting/index.js.map +1 -1
  11. package/dist/meeting/request.js +59 -32
  12. package/dist/meeting/request.js.map +1 -1
  13. package/dist/meeting/util.js +12 -0
  14. package/dist/meeting/util.js.map +1 -1
  15. package/dist/meeting-info/meeting-info-v2.js +142 -8
  16. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  17. package/dist/meeting-info/utilv2.js +12 -1
  18. package/dist/meeting-info/utilv2.js.map +1 -1
  19. package/dist/meetings/index.js +47 -19
  20. package/dist/meetings/index.js.map +1 -1
  21. package/dist/peer-connection-manager/index.js +16 -1
  22. package/dist/peer-connection-manager/index.js.map +1 -1
  23. package/dist/peer-connection-manager/util.js +28 -0
  24. package/dist/peer-connection-manager/util.js.map +1 -0
  25. package/package.json +5 -5
  26. package/src/common/errors/captcha-error.js +21 -0
  27. package/src/common/errors/password-error.js +21 -0
  28. package/src/config.js +2 -1
  29. package/src/constants.js +24 -0
  30. package/src/meeting/index.js +173 -3
  31. package/src/meeting/request.js +27 -0
  32. package/src/meeting/util.js +15 -4
  33. package/src/meeting-info/meeting-info-v2.js +67 -3
  34. package/src/meeting-info/utilv2.js +12 -1
  35. package/src/meetings/index.js +27 -8
  36. package/src/peer-connection-manager/index.js +19 -2
  37. package/src/peer-connection-manager/util.js +19 -0
  38. package/test/unit/spec/meeting/index.js +294 -3
  39. package/test/unit/spec/meeting-info/meetinginfov2.js +74 -1
  40. package/test/unit/spec/meetings/index.js +29 -0
  41. package/test/unit/spec/peerconnection-manager/index.js +66 -0
  42. package/test/unit/spec/peerconnection-manager/utils.js +25 -0
@@ -0,0 +1,21 @@
1
+ import {ERROR_DICTIONARY} from '../../constants';
2
+
3
+ /**
4
+ * Extended Error object to signify captcha related errors
5
+ */
6
+ export default class CaptchaError extends Error {
7
+ /**
8
+ *
9
+ * @constructor
10
+ * @param {String} [message]
11
+ * @param {Object} [error]
12
+ */
13
+ constructor(message = ERROR_DICTIONARY.CAPTCHA.MESSAGE, error = null) {
14
+ super(message);
15
+ this.name = ERROR_DICTIONARY.CAPTCHA.NAME;
16
+ this.sdkMessage = ERROR_DICTIONARY.CAPTCHA.MESSAGE;
17
+ this.error = error;
18
+ this.stack = error ? error.stack : (new Error()).stack;
19
+ this.code = ERROR_DICTIONARY.CAPTCHA.CODE;
20
+ }
21
+ }
@@ -0,0 +1,21 @@
1
+ import {ERROR_DICTIONARY} from '../../constants';
2
+
3
+ /**
4
+ * Extended Error object to signify password related errors
5
+ */
6
+ export default class PasswordError extends Error {
7
+ /**
8
+ *
9
+ * @constructor
10
+ * @param {String} [message]
11
+ * @param {Object} [error]
12
+ */
13
+ constructor(message = ERROR_DICTIONARY.PASSWORD.MESSAGE, error = null) {
14
+ super(message);
15
+ this.name = ERROR_DICTIONARY.PASSWORD.NAME;
16
+ this.sdkMessage = ERROR_DICTIONARY.PASSWORD.MESSAGE;
17
+ this.error = error;
18
+ this.stack = error ? error.stack : (new Error()).stack;
19
+ this.code = ERROR_DICTIONARY.PASSWORD.CODE;
20
+ }
21
+ }
package/src/config.js CHANGED
@@ -76,7 +76,8 @@ export default {
76
76
  // please note, these are the maximum bandwidth values
77
77
  // the server supports, minimums have to be tested
78
78
  audio: 64000,
79
- video: 4000000
79
+ video: 4000000,
80
+ startBitrate: 2000
80
81
  },
81
82
  screenFrameRate: 10,
82
83
  videoShareFrameRate: 30,
package/src/constants.js CHANGED
@@ -559,6 +559,16 @@ export const ERROR_DICTIONARY = {
559
559
  NAME: 'StatsError',
560
560
  MESSAGE: 'An error occurred with getStats, stats may not continue for this data stream.',
561
561
  CODE: 6
562
+ },
563
+ PASSWORD: {
564
+ NAME: 'PasswordError',
565
+ MESSAGE: 'Password is required, please use verifyPassword()',
566
+ CODE: 7
567
+ },
568
+ CAPTCHA: {
569
+ NAME: 'CaptchaError',
570
+ MESSAGE: 'Captcha is required.',
571
+ CODE: 8
562
572
  }
563
573
  };
564
574
 
@@ -1251,3 +1261,17 @@ export const PSTN_STATUS = {
1251
1261
  SUCCESS: 'SUCCESS', // happens after the transfer (TRANSFERRING) is successful
1252
1262
  UNKNOWN: '' // placeholder if we haven't been told what the status is
1253
1263
  };
1264
+
1265
+ export const PASSWORD_STATUS = {
1266
+ NOT_REQUIRED: 'NOT_REQUIRED', // password is not required to join the meeting
1267
+ REQUIRED: 'REQUIRED', // client needs to provide the password by calling verifyPassword() before calling join()
1268
+ UNKNOWN: 'UNKNOWN', // we are waiting for information from the backend if password is required or not
1269
+ VERIFIED: 'VERIFIED' // client has already provided the password and it has been verified, client can proceed to call join()
1270
+ };
1271
+
1272
+ export const MEETING_INFO_FAILURE_REASON = {
1273
+ NONE: 'NONE', // meeting info was retrieved succesfully
1274
+ WRONG_PASSWORD: 'WRONG_PASSWORD', // meeting requires password and no password or wrong one was provided
1275
+ WRONG_CAPTCHA: 'WRONG_CAPTCHA', // wbxappapi requires a captcha code or a wrong captcha code was provided
1276
+ OTHER: 'OTHER' // any other error (network, etc)
1277
+ };
@@ -29,12 +29,15 @@ import WebRTCStats from '../stats/index';
29
29
  import StatsMetrics from '../stats/metrics';
30
30
  import StatsUtil from '../stats/util';
31
31
  import Transcription from '../transcription';
32
+ import PasswordError from '../common/errors/password-error';
33
+ import CaptchaError from '../common/errors/captcha-error';
32
34
  import ReconnectionError from '../common/errors/reconnection';
33
35
  import ReconnectInProgress from '../common/errors/reconnection-in-progress';
34
36
  import {
35
37
  _CALL_,
36
38
  _INCOMING_,
37
39
  _JOIN_,
40
+ _SIP_URI_,
38
41
  AUDIO,
39
42
  CONNECTION_STATE,
40
43
  CONTENT,
@@ -47,6 +50,7 @@ import {
47
50
  LAYOUT_TYPES,
48
51
  LIVE,
49
52
  LOCUSINFO,
53
+ MEETING_INFO_FAILURE_REASON,
50
54
  MEETING_REMOVED_REASON,
51
55
  MEETING_STATE_MACHINE,
52
56
  MEETING_STATE,
@@ -57,6 +61,7 @@ import {
57
61
  NETWORK_STATUS,
58
62
  ONLINE,
59
63
  OFFLINE,
64
+ PASSWORD_STATUS,
60
65
  PC_BAIL_TIMEOUT,
61
66
  PSTN_STATUS,
62
67
  QUALITY_LEVELS,
@@ -73,6 +78,7 @@ import {
73
78
  } from '../constants';
74
79
  import ParameterError from '../common/errors/parameter';
75
80
  import MediaError from '../common/errors/media';
81
+ import {MeetingInfoV2PasswordError, MeetingInfoV2CaptchaError} from '../meeting-info/meeting-info-v2';
76
82
  import MQAProcessor from '../metrics/mqa-processor';
77
83
  import BrowserDetection from '../common/browser-detection';
78
84
  import RoapCollection from '../roap/collection';
@@ -846,9 +852,173 @@ export default class Meeting extends StatelessWebexPlugin {
846
852
  */
847
853
  this.transcription = undefined;
848
854
 
855
+ /**
856
+ * Password status. If it's PASSWORD_STATUS.REQUIRED then verifyPassword() needs to be called
857
+ * with the correct password before calling join()
858
+ * @instance
859
+ * @type {PASSWORD_STATUS}
860
+ * @public
861
+ * @memberof Meeting
862
+ */
863
+ this.passwordStatus = PASSWORD_STATUS.UNKNOWN;
864
+
865
+ /**
866
+ * Information about required captcha. If null, then no captcha is required. status. If it's PASSWORD_STATUS.REQUIRED then verifyPassword() needs to be called
867
+ * with the correct password before calling join()
868
+ * @instance
869
+ * @type {Object}
870
+ * @property {string} captchaId captcha id
871
+ * @property {string} verificationImageURL Url of the captcha image
872
+ * @property {string} verificationAudioURL Url of the captcha audio file
873
+ * @property {string} refreshURL Url used for refreshing the captcha (don't use it directly, call refreshCaptcha() instead)
874
+ * @public
875
+ * @memberof Meeting
876
+ */
877
+ this.requiredCaptcha = null;
878
+
879
+ /**
880
+ * Indicates the reason for last failure to obtain meeting.meetingInfo. MEETING_INFO_FAILURE_REASON.NONE if meeting info was
881
+ * retrieved successfully
882
+ * @instance
883
+ * @type {MEETING_INFO_FAILURE_REASON}
884
+ * @private
885
+ * @memberof Meeting
886
+ */
887
+ this.meetingInfoFailureReason = undefined;
888
+
849
889
  this.setUpLocusInfoListeners();
850
890
  this.locusInfo.init(attrs.locus ? attrs.locus : {});
851
- this.isCreated = true;
891
+ this.hasJoinedOnce = false;
892
+ }
893
+
894
+ /**
895
+ * Fetches meeting information.
896
+ * @param {Object} options
897
+ * @param {String} options.destination
898
+ * @param {String} options.type
899
+ * @private
900
+ * @memberof Meeting
901
+ * @returns {Promise}
902
+ */
903
+ async fetchMeetingInfo({
904
+ destination, type, password = null, captchaCode = null
905
+ }) {
906
+ if (captchaCode && !this.requiredCaptcha) {
907
+ return Promise.reject(new Error('fetchMeetingInfo() called with captchaCode when captcha was not required'));
908
+ }
909
+ if (password && (this.passwordStatus !== PASSWORD_STATUS.REQUIRED && this.passwordStatus !== PASSWORD_STATUS.UNKNOWN)) {
910
+ return Promise.reject(new Error('fetchMeetingInfo() called with password when password was not required'));
911
+ }
912
+
913
+ try {
914
+ const captchaInfo = captchaCode ? {code: captchaCode, id: this.requiredCaptcha.captchaId} : null;
915
+
916
+ const info = await this.attrs.meetingInfoProvider.fetchMeetingInfo(destination, type, password, captchaInfo);
917
+
918
+ this.parseMeetingInfo(info);
919
+ this.meetingInfo = info ? info.body : null;
920
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.NONE;
921
+ this.requiredCaptcha = null;
922
+ if ((this.passwordStatus === PASSWORD_STATUS.REQUIRED) || (this.passwordStatus === PASSWORD_STATUS.VERIFIED)) {
923
+ this.passwordStatus = PASSWORD_STATUS.VERIFIED;
924
+ }
925
+ else {
926
+ this.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
927
+ }
928
+
929
+ return Promise.resolve();
930
+ }
931
+ catch (err) {
932
+ if (err instanceof MeetingInfoV2PasswordError) {
933
+ LoggerProxy.logger.info(`Meeting:index#fetchMeetingInfo --> Info Unable to fetch meeting info for ${destination} - password required (code=${err?.body?.code}).`);
934
+
935
+ // when wbxappapi requires password it still populates partial meeting info in the response
936
+ if (err.meetingInfo) {
937
+ this.meetingInfo = err.meetingInfo;
938
+ this.meetingNumber = err.meetingInfo.meetingNumber;
939
+ }
940
+
941
+ this.passwordStatus = PASSWORD_STATUS.REQUIRED;
942
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD;
943
+ if (this.requiredCaptcha) {
944
+ // this is a workaround for captcha service bug, see WEBEX-224862
945
+ await this.refreshCaptcha();
946
+ }
947
+
948
+ throw (new PasswordError());
949
+ }
950
+ else if (err instanceof MeetingInfoV2CaptchaError) {
951
+ LoggerProxy.logger.info(`Meeting:index#fetchMeetingInfo --> Info Unable to fetch meeting info for ${destination} - captcha required (code=${err?.body?.code}).`);
952
+
953
+ this.meetingInfoFailureReason = (this.requiredCaptcha) ?
954
+ MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA :
955
+ MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD;
956
+
957
+ if (err.isPasswordRequired) {
958
+ this.passwordStatus = PASSWORD_STATUS.REQUIRED;
959
+ }
960
+
961
+ this.requiredCaptcha = err.captchaInfo;
962
+ throw (new CaptchaError());
963
+ }
964
+ else {
965
+ this.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.OTHER;
966
+ throw (err);
967
+ }
968
+ }
969
+ }
970
+
971
+ /**
972
+ * Checks if the supplied password/host key is correct. It returns a promise with information whether the
973
+ * password and captcha code were correct or not.
974
+ * @param {String} password - this can be either a password or a host key, can be undefined if only captcha was required
975
+ * @param {String} captchaCode - can be undefined if captcha was not required by the server
976
+ * @public
977
+ * @memberof Meeting
978
+ * @returns {Promise<{isPasswordValid: boolean, requiredCaptcha: boolean, failureReason: MEETING_INFO_FAILURE_REASON}>}
979
+ */
980
+ verifyPassword(password, captchaCode) {
981
+ return this.fetchMeetingInfo({
982
+ destination: this.sipUri, type: _SIP_URI_, password, captchaCode
983
+ })
984
+ .then(() => ({isPasswordValid: true, requiredCaptcha: null, failureReason: MEETING_INFO_FAILURE_REASON.NONE}))
985
+ .catch((error) => {
986
+ if (error instanceof PasswordError || error instanceof CaptchaError) {
987
+ return {
988
+ isPasswordValid: this.passwordStatus === PASSWORD_STATUS.VERIFIED,
989
+ requiredCaptcha: this.requiredCaptcha,
990
+ failureReason: this.meetingInfoFailureReason
991
+ };
992
+ }
993
+ throw (error);
994
+ });
995
+ }
996
+
997
+ /**
998
+ * Refreshes the captcha. As a result the meeting will have new captcha id, image and audio.
999
+ * If the refresh operation fails, meeting remains with the old captcha properties.
1000
+ * @public
1001
+ * @memberof Meeting
1002
+ * @returns {Promise}
1003
+ */
1004
+ refreshCaptcha() {
1005
+ if (!this.requiredCaptcha) {
1006
+ return Promise.reject(new Error('There is no captcha to refresh'));
1007
+ }
1008
+
1009
+ // in order to get fully populated uris for captcha audio and image in response to refresh captcha request
1010
+ // we have to pass the wbxappapi hostname as the siteFullName parameter
1011
+ const {hostname} = new URL(this.requiredCaptcha.refreshURL);
1012
+
1013
+ return this.meetingRequest.refreshCaptcha({
1014
+ captchaRefreshUrl: `${this.requiredCaptcha.refreshURL}&siteFullName=${hostname}`,
1015
+ captchaId: this.requiredCaptcha.captchaId
1016
+ })
1017
+ .then((response) => {
1018
+ this.requiredCaptcha.captchaId = response.body.captchaID;
1019
+ this.requiredCaptcha.verificationImageURL = response.body.verificationImageURL;
1020
+ this.requiredCaptcha.verificationAudioURL = response.body.verificationAudioURL;
1021
+ });
852
1022
  }
853
1023
 
854
1024
  /**
@@ -3326,8 +3496,8 @@ export default class Meeting extends StatelessWebexPlugin {
3326
3496
  joinSuccess = resolve;
3327
3497
  });
3328
3498
 
3329
- if (this.isCreated) {
3330
- this.isCreated = false;
3499
+ if (!this.hasJoinedOnce) {
3500
+ this.hasJoinedOnce = true;
3331
3501
  }
3332
3502
  else {
3333
3503
  LoggerProxy.logger.log(`Meeting:index#join --> Generating a new correlation id for meeting ${this.id}`);
@@ -140,6 +140,33 @@ export default class MeetingRequest extends StatelessWebexPlugin {
140
140
  });
141
141
  }
142
142
 
143
+ /**
144
+ * Send a request to refresh the captcha
145
+ * @param {Object} options
146
+ * @param {String} options.captchaRefreshUrl
147
+ * @param {String} options.captchaId
148
+ * @returns {Promise}
149
+ * @private
150
+ */
151
+ refreshCaptcha({
152
+ captchaRefreshUrl,
153
+ captchaId
154
+ }) {
155
+ const body = {
156
+ captchaId
157
+ };
158
+
159
+ return this.request({
160
+ method: HTTP_VERBS.POST,
161
+ uri: captchaRefreshUrl,
162
+ body
163
+ }).catch((err) => {
164
+ LoggerProxy.logger.error(`Meeting:request#refreshCaptcha --> Error: ${err}`);
165
+
166
+ throw err;
167
+ });
168
+ }
169
+
143
170
  /**
144
171
  * Make a network request to add a dial in device
145
172
  * @param {Object} options
@@ -7,19 +7,23 @@ import {eventType, trigger, mediaType} from '../metrics/config';
7
7
  import Media from '../media';
8
8
  import LoggerProxy from '../common/logs/logger-proxy';
9
9
  import WebRTCStats from '../stats/index';
10
- import {INTENT_TO_JOIN,
10
+ import {
11
+ INTENT_TO_JOIN,
11
12
  _LEFT_,
12
13
  _IDLE_,
13
14
  _JOINED_,
14
15
  STATS,
15
- EVENT_TRIGGERS
16
- , FULL_STATE} from '../constants';
16
+ EVENT_TRIGGERS,
17
+ FULL_STATE,
18
+ PASSWORD_STATUS
19
+ } from '../constants';
17
20
  import Trigger from '../common/events/trigger-proxy';
18
21
  import IntentToJoinError from '../common/errors/intent-to-join';
19
22
  import JoinMeetingError from '../common/errors/join-meeting';
20
23
  import ParameterError from '../common/errors/parameter';
21
24
  import PermissionError from '../common/errors/permission';
22
-
25
+ import PasswordError from '../common/errors/password-error';
26
+ import CaptchaError from '../common/errors/captcha-error';
23
27
 
24
28
  const MeetingUtil = {};
25
29
 
@@ -240,6 +244,13 @@ MeetingUtil.isMediaEstablished = (currentMediaStatus) =>
240
244
  MeetingUtil.joinMeetingOptions = (meeting, options = {}) => {
241
245
  meeting.resourceId = meeting.resourceId || options.resourceId;
242
246
 
247
+ if (meeting.requiredCaptcha) {
248
+ return Promise.reject(new CaptchaError());
249
+ }
250
+ if (meeting.passwordStatus === PASSWORD_STATUS.REQUIRED) {
251
+ return Promise.reject(new PasswordError());
252
+ }
253
+
243
254
  if (options.pin) {
244
255
  Metrics.postEvent({
245
256
  event: eventType.PIN_COLLECTED,
@@ -3,6 +3,52 @@ import {HTTP_VERBS} from '../constants';
3
3
 
4
4
  import MeetingInfoUtil from './utilv2';
5
5
 
6
+ const PASSWORD_ERROR_DEFAULT_MESSAGE = 'Password required. Call fetchMeetingInfo() with password argument';
7
+ const CAPTCHA_ERROR_DEFAULT_MESSAGE = 'Captcha required. Call fetchMeetingInfo() with captchaInfo argument';
8
+
9
+ /**
10
+ * Error to indicate that wbxappapi requires a password
11
+ */
12
+ export class MeetingInfoV2PasswordError extends Error {
13
+ /**
14
+ *
15
+ * @constructor
16
+ * @param {Number} [wbxAppApiErrorCode]
17
+ * @param {Object} [meetingInfo]
18
+ * @param {String} [message]
19
+ */
20
+ constructor(wbxAppApiErrorCode, meetingInfo, message = PASSWORD_ERROR_DEFAULT_MESSAGE) {
21
+ super(`${message}, code=${wbxAppApiErrorCode}`);
22
+ this.name = 'MeetingInfoV2PasswordError';
23
+ this.sdkMessage = message;
24
+ this.stack = (new Error()).stack;
25
+ this.wbxAppApiCode = wbxAppApiErrorCode;
26
+ this.meetingInfo = meetingInfo;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Error to indicate that wbxappapi requires a captcha
32
+ */
33
+ export class MeetingInfoV2CaptchaError extends Error {
34
+ /**
35
+ *
36
+ * @constructor
37
+ * @param {Number} [wbxAppApiErrorCode]
38
+ * @param {Object} [captchaInfo]
39
+ * @param {String} [message]
40
+ */
41
+ constructor(wbxAppApiErrorCode, captchaInfo, message = CAPTCHA_ERROR_DEFAULT_MESSAGE) {
42
+ super(`${message}, code=${wbxAppApiErrorCode}`);
43
+ this.name = 'MeetingInfoV2PasswordError';
44
+ this.sdkMessage = message;
45
+ this.stack = (new Error()).stack;
46
+ this.wbxAppApiCode = wbxAppApiErrorCode;
47
+ this.isPasswordRequired = wbxAppApiErrorCode === 423005;
48
+ this.captchaInfo = captchaInfo;
49
+ }
50
+ }
51
+
6
52
  /**
7
53
  * @class MeetingInfo
8
54
  */
@@ -35,24 +81,42 @@ export default class MeetingInfoV2 {
35
81
  * Fetches meeting info from the server
36
82
  * @param {String} destination one of many different types of destinations to look up info for
37
83
  * @param {String} [type] to match up with the destination value
84
+ * @param {String} password
85
+ * @param {Object} captchaInfo
86
+ * @param {String} captchaInfo.code
87
+ * @param {String} captchaInfo.id
38
88
  * @returns {Promise} returns a meeting info object
39
89
  * @public
40
90
  * @memberof MeetingInfo
41
91
  */
42
- async fetchMeetingInfo(destination, type = null) {
92
+ async fetchMeetingInfo(destination, type = null, password = null, captchaInfo = null) {
43
93
  const destinationType = await MeetingInfoUtil.getDestinationType({
44
94
  destination,
45
95
  type,
46
96
  webex: this.webex
47
97
  });
48
- const body = await MeetingInfoUtil.getRequestBody(destinationType);
98
+ const body = await MeetingInfoUtil.getRequestBody({...destinationType, password, captchaInfo});
49
99
 
50
100
  return this.webex.request({
51
101
  method: HTTP_VERBS.POST,
52
102
  service: 'webex-appapi-service',
53
103
  resource: 'meetingInfo',
54
104
  body
55
- });
105
+ })
106
+ .catch((err) => {
107
+ if (err?.statusCode === 403) {
108
+ throw new MeetingInfoV2PasswordError(err.body?.code, err.body?.data?.meetingInfo);
109
+ }
110
+ if (err?.statusCode === 423) {
111
+ throw new MeetingInfoV2CaptchaError(err.body?.code, {
112
+ captchaId: err.body.captchaID,
113
+ verificationImageURL: err.body.verificationImageURL,
114
+ verificationAudioURL: err.body.verificationAudioURL,
115
+ refreshURL: err.body.refreshURL
116
+ });
117
+ }
118
+ throw err;
119
+ });
56
120
  }
57
121
  }
58
122
 
@@ -212,7 +212,9 @@ MeetingInfoUtil.getDestinationType = async (from) => {
212
212
  * @returns {Object} returns an object with {resource, method}
213
213
  */
214
214
  MeetingInfoUtil.getRequestBody = (options) => {
215
- const {type, destination} = options;
215
+ const {
216
+ type, destination, password, captchaInfo
217
+ } = options;
216
218
  const body = {
217
219
  supportHostKey: true
218
220
  };
@@ -250,6 +252,15 @@ MeetingInfoUtil.getRequestBody = (options) => {
250
252
  default:
251
253
  }
252
254
 
255
+ if (password) {
256
+ body.password = password;
257
+ }
258
+
259
+ if (captchaInfo) {
260
+ body.captchaID = captchaInfo.id;
261
+ body.captchaVerifyCode = captchaInfo.code;
262
+ }
263
+
253
264
  return body;
254
265
  };
255
266
 
@@ -48,6 +48,8 @@ import Reachability from '../reachability';
48
48
  import Request from '../meetings/request';
49
49
  import StatsAnalyzer from '../analyzer/analyzer';
50
50
  import StatsCalculator from '../analyzer/calculator';
51
+ import PasswordError from '../common/errors/password-error';
52
+ import CaptchaError from '../common/errors/captcha-error';
51
53
 
52
54
  import MeetingCollection from './collection';
53
55
  import MeetingsUtil from './util';
@@ -395,6 +397,23 @@ export default class Meetings extends WebexPlugin {
395
397
  });
396
398
  }
397
399
 
400
+ /**
401
+ * API to toggle unified meetings
402
+ * @param {Boolean} changeState
403
+ * @private
404
+ * @memberof Meetings
405
+ * @returns {undefined}
406
+ */
407
+ _toggleUnifiedMeetings(changeState) {
408
+ if (typeof changeState !== 'boolean') {
409
+ return;
410
+ }
411
+ if (this.config?.experimental?.enableUnifiedMeetings !== changeState) {
412
+ this.config.experimental.enableUnifiedMeetings = changeState;
413
+ this.meetingInfo = changeState ? new MeetingInfoV2(this.webex) : new MeetingInfo(this.webex);
414
+ }
415
+ }
416
+
398
417
  /**
399
418
  * Explicitly sets up the meetings plugin by registering
400
419
  * the device, connecting to mercury, and listening for locus events.
@@ -716,7 +735,8 @@ export default class Meetings extends WebexPlugin {
716
735
  deviceUrl: this.webex.internal.device.url,
717
736
  orgId: this.webex.internal.device.orgId,
718
737
  roapSeq: 0,
719
- locus: type === _LOCUS_ID_ ? destination : null // pass the locus object if present
738
+ locus: type === _LOCUS_ID_ ? destination : null, // pass the locus object if present
739
+ meetingInfoProvider: this.meetingInfo
720
740
  },
721
741
  {
722
742
  parent: this.webex
@@ -726,15 +746,14 @@ export default class Meetings extends WebexPlugin {
726
746
  this.meetingCollection.set(meeting);
727
747
 
728
748
  try {
729
- const info = await this.meetingInfo.fetchMeetingInfo(destination, type);
730
-
731
- meeting.parseMeetingInfo(info);
732
- meeting.meetingInfo = info ? info.body : null;
749
+ await meeting.fetchMeetingInfo({destination, type});
733
750
  }
734
751
  catch (err) {
735
- // if there is no meeting info we assume its a 1:1 call or wireless share
736
- LoggerProxy.logger.info(`Meetings:index#createMeeting --> Info Unable to fetch meeting info for ${destination}.`);
737
- LoggerProxy.logger.info('Meetings:index#createMeeting --> Info assuming this destination is a 1:1 or wireless share');
752
+ if (!(err instanceof CaptchaError) && !(err instanceof PasswordError)) {
753
+ // if there is no meeting info we assume its a 1:1 call or wireless share
754
+ LoggerProxy.logger.info(`Meetings:index#createMeeting --> Info Unable to fetch meeting info for ${destination}.`);
755
+ LoggerProxy.logger.info('Meetings:index#createMeeting --> Info assuming this destination is a 1:1 or wireless share');
756
+ }
738
757
  LoggerProxy.logger.debug(`Meetings:index#createMeeting --> Debug ${err} fetching /meetingInfo for creation.`);
739
758
  // We need to save this info for future reference
740
759
  meeting.destination = destination;
@@ -31,6 +31,8 @@ import ParameterError from '../common/errors/parameter';
31
31
  import {InvalidSdpError} from '../common/errors/webex-errors';
32
32
  import BrowserDetection from '../common/browser-detection';
33
33
 
34
+ import PeerConnectionUtils from './util';
35
+
34
36
  const {isBrowser} = BrowserDetection();
35
37
 
36
38
  /**
@@ -79,11 +81,20 @@ const setMaxFs = (sdp, level = QUALITY_LEVELS.HIGH) => {
79
81
  let replaceSdp = sdp;
80
82
  const maxFsLine = `${SDP.MAX_FS}${MAX_FRAMESIZES[level]}`;
81
83
 
82
- replaceSdp = replaceSdp.replace(/(\na=fmtp:(\d+).*level-asymmetry-allowed=1.*)/gi, `$1;${maxFsLine}`);
84
+ replaceSdp = replaceSdp.replace(/(\na=fmtp:(\d+).*profile-level-id=.*)/gi, `$1;${maxFsLine}`);
83
85
 
84
86
  return replaceSdp;
85
87
  };
86
88
 
89
+
90
+ const setStartBitrateOnRemoteSdp = (sdp) => {
91
+ if (StaticConfig.meetings.bandwidth.startBitrate) {
92
+ sdp = sdp.replace(/(\na=fmtp:(\d+).*profile-level-id=.*)/gi, `$1;x-google-start-bitrate=${StaticConfig.meetings.bandwidth.startBitrate}`);
93
+ }
94
+
95
+ return sdp;
96
+ };
97
+
87
98
  /**
88
99
  * checks that sdp has h264 codec in it
89
100
  * @param {String} sdp
@@ -186,6 +197,7 @@ pc.iceCandidate = (peerConnection, {remoteQualityLevel}) =>
186
197
  const timeout = setTimeout(() => {
187
198
  peerConnection.sdp = limitBandwidth(peerConnection.localDescription.sdp);
188
199
  peerConnection.sdp = setMaxFs(peerConnection.sdp, remoteQualityLevel);
200
+ peerConnection.sdp = PeerConnectionUtils.convertCLineToIpv4(peerConnection.sdp);
189
201
 
190
202
  if (isSdpInvalid(peerConnection.sdp)) {
191
203
  setTimeout(() => {
@@ -213,6 +225,7 @@ pc.iceCandidate = (peerConnection, {remoteQualityLevel}) =>
213
225
  if (!evt.candidate && !peerConnection.sdp) {
214
226
  peerConnection.sdp = limitBandwidth(peerConnection.localDescription.sdp);
215
227
  peerConnection.sdp = setMaxFs(peerConnection.sdp, remoteQualityLevel);
228
+ peerConnection.sdp = PeerConnectionUtils.convertCLineToIpv4(peerConnection.sdp);
216
229
 
217
230
  if (evt.candidate === null && !isSdpInvalid(peerConnection.sdp)) {
218
231
  clearTimeout(timeout);
@@ -295,7 +308,7 @@ pc.setRemoteSessionDetails = (
295
308
  meetingId,
296
309
  ) => {
297
310
  LoggerProxy.logger.log(`PeerConnectionManager:index#setRemoteSessionDetails --> Setting the remote description type: ${typeStr}State: ${peerConnection.signalingState}`);
298
- const sdp = remoteSdp;
311
+ let sdp = remoteSdp;
299
312
 
300
313
  // making sure that the remoteDescription is only set when there is a answer for offer
301
314
  // or there is a offer from the server
@@ -312,6 +325,8 @@ pc.setRemoteSessionDetails = (
312
325
  });
313
326
  }
314
327
  if (peerConnection.signalingState === SDP.HAVE_LOCAL_OFFER || (peerConnection.signalingState === SDP.STABLE && typeStr === SDP.OFFER)) {
328
+ sdp = setStartBitrateOnRemoteSdp(sdp);
329
+
315
330
  return peerConnection.setRemoteDescription(
316
331
  new window.RTCSessionDescription({
317
332
  type: typeStr,
@@ -393,6 +408,7 @@ pc.createOffer = (peerConnection, {
393
408
  .then(() => {
394
409
  peerConnection.sdp = limitBandwidth(peerConnection.localDescription.sdp);
395
410
  peerConnection.sdp = setMaxFs(peerConnection.sdp, remoteQualityLevel);
411
+ peerConnection.sdp = PeerConnectionUtils.convertCLineToIpv4(peerConnection.sdp);
396
412
  if (!checkH264Support(peerConnection.sdp)) {
397
413
  throw new MediaError('openH264 is downloading please Wait. Upload logs if not working on second try');
398
414
  }
@@ -519,6 +535,7 @@ pc.createAnswer = (params, {meetingId, remoteQualityLevel}) => {
519
535
  .then(() => {
520
536
  peerConnection.sdp = limitBandwidth(peerConnection.localDescription.sdp);
521
537
  peerConnection.sdp = setMaxFs(peerConnection.sdp, remoteQualityLevel);
538
+ peerConnection.sdp = PeerConnectionUtils.convertCLineToIpv4(peerConnection.sdp);
522
539
  if (!checkH264Support(peerConnection.sdp)) {
523
540
  throw new MediaError('openH264 is downloading please Wait. Upload logs if not working on second try');
524
541
  }
@@ -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;