@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.
- package/dist/common/errors/captcha-error.js +64 -0
- package/dist/common/errors/captcha-error.js.map +1 -0
- package/dist/common/errors/password-error.js +64 -0
- package/dist/common/errors/password-error.js.map +1 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.js +33 -1
- package/dist/constants.js.map +1 -1
- package/dist/meeting/index.js +668 -432
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/request.js +59 -32
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/util.js +12 -0
- package/dist/meeting/util.js.map +1 -1
- package/dist/meeting-info/meeting-info-v2.js +142 -8
- package/dist/meeting-info/meeting-info-v2.js.map +1 -1
- package/dist/meeting-info/utilv2.js +12 -1
- package/dist/meeting-info/utilv2.js.map +1 -1
- package/dist/meetings/index.js +47 -19
- package/dist/meetings/index.js.map +1 -1
- package/dist/peer-connection-manager/index.js +16 -1
- package/dist/peer-connection-manager/index.js.map +1 -1
- package/dist/peer-connection-manager/util.js +28 -0
- package/dist/peer-connection-manager/util.js.map +1 -0
- package/package.json +5 -5
- package/src/common/errors/captcha-error.js +21 -0
- package/src/common/errors/password-error.js +21 -0
- package/src/config.js +2 -1
- package/src/constants.js +24 -0
- package/src/meeting/index.js +173 -3
- package/src/meeting/request.js +27 -0
- package/src/meeting/util.js +15 -4
- package/src/meeting-info/meeting-info-v2.js +67 -3
- package/src/meeting-info/utilv2.js +12 -1
- package/src/meetings/index.js +27 -8
- package/src/peer-connection-manager/index.js +19 -2
- package/src/peer-connection-manager/util.js +19 -0
- package/test/unit/spec/meeting/index.js +294 -3
- package/test/unit/spec/meeting-info/meetinginfov2.js +74 -1
- package/test/unit/spec/meetings/index.js +29 -0
- package/test/unit/spec/peerconnection-manager/index.js +66 -0
- 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
|
+
};
|
package/src/meeting/index.js
CHANGED
|
@@ -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.
|
|
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.
|
|
3330
|
-
this.
|
|
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}`);
|
package/src/meeting/request.js
CHANGED
|
@@ -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
|
package/src/meeting/util.js
CHANGED
|
@@ -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 {
|
|
10
|
+
import {
|
|
11
|
+
INTENT_TO_JOIN,
|
|
11
12
|
_LEFT_,
|
|
12
13
|
_IDLE_,
|
|
13
14
|
_JOINED_,
|
|
14
15
|
STATS,
|
|
15
|
-
EVENT_TRIGGERS
|
|
16
|
-
,
|
|
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 {
|
|
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
|
|
package/src/meetings/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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-
|
|
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
|
-
|
|
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;
|