@webex/plugin-meetings 3.8.0-next.3 → 3.8.0-next.31
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/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +1 -0
- package/dist/constants.js.map +1 -1
- package/dist/interpretation/index.js +4 -4
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/controlsUtils.js +1 -1
- package/dist/locus-info/controlsUtils.js.map +1 -1
- package/dist/media/index.js +3 -15
- package/dist/media/index.js.map +1 -1
- package/dist/meeting/index.js +89 -5
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/locusMediaRequest.js +21 -5
- package/dist/meeting/locusMediaRequest.js.map +1 -1
- package/dist/meeting/util.js +4 -1
- package/dist/meeting/util.js.map +1 -1
- package/dist/meeting-info/meeting-info-v2.js +359 -60
- package/dist/meeting-info/meeting-info-v2.js.map +1 -1
- package/dist/meetings/index.js +60 -1
- package/dist/meetings/index.js.map +1 -1
- package/dist/member/index.js +10 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/util.js +3 -0
- package/dist/member/util.js.map +1 -1
- package/dist/metrics/constants.js +9 -0
- package/dist/metrics/constants.js.map +1 -1
- package/dist/reachability/clusterReachability.js +52 -8
- package/dist/reachability/clusterReachability.js.map +1 -1
- package/dist/reachability/index.js +70 -45
- package/dist/reachability/index.js.map +1 -1
- package/dist/reachability/reachability.types.js +14 -0
- package/dist/reachability/reachability.types.js.map +1 -1
- package/dist/reachability/request.js +19 -3
- package/dist/reachability/request.js.map +1 -1
- package/dist/reconnection-manager/index.js +2 -2
- package/dist/reconnection-manager/index.js.map +1 -1
- package/dist/recording-controller/util.js +5 -5
- package/dist/recording-controller/util.js.map +1 -1
- package/dist/roap/index.js.map +1 -1
- package/dist/roap/turnDiscovery.js +31 -23
- package/dist/roap/turnDiscovery.js.map +1 -1
- package/dist/roap/types.js +17 -0
- package/dist/roap/types.js.map +1 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/constants.d.ts +1 -0
- package/dist/types/meeting/index.d.ts +32 -1
- package/dist/types/meeting-info/meeting-info-v2.d.ts +80 -0
- package/dist/types/meetings/index.d.ts +29 -0
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/metrics/constants.d.ts +9 -0
- package/dist/types/reachability/clusterReachability.d.ts +13 -1
- package/dist/types/reachability/index.d.ts +2 -1
- package/dist/types/reachability/reachability.types.d.ts +5 -0
- package/dist/types/roap/index.d.ts +3 -2
- package/dist/types/roap/turnDiscovery.d.ts +1 -17
- package/dist/types/roap/types.d.ts +16 -0
- package/dist/webinar/index.js +1 -1
- package/package.json +22 -22
- package/src/config.ts +1 -0
- package/src/constants.ts +1 -0
- package/src/interpretation/index.ts +3 -3
- package/src/locus-info/controlsUtils.ts +2 -2
- package/src/media/index.ts +5 -21
- package/src/meeting/index.ts +91 -13
- package/src/meeting/locusMediaRequest.ts +27 -4
- package/src/meeting/util.ts +2 -1
- package/src/meeting-info/meeting-info-v2.ts +247 -6
- package/src/meetings/index.ts +72 -1
- package/src/member/index.ts +11 -0
- package/src/member/util.ts +3 -0
- package/src/metrics/constants.ts +9 -0
- package/src/reachability/clusterReachability.ts +47 -1
- package/src/reachability/index.ts +15 -0
- package/src/reachability/reachability.types.ts +6 -0
- package/src/reachability/request.ts +7 -0
- package/src/reconnection-manager/index.ts +2 -2
- package/src/recording-controller/util.ts +17 -13
- package/src/roap/index.ts +3 -7
- package/src/roap/turnDiscovery.ts +21 -35
- package/src/roap/types.ts +23 -0
- package/test/unit/spec/interpretation/index.ts +39 -1
- package/test/unit/spec/locus-info/controlsUtils.js +8 -0
- package/test/unit/spec/media/index.ts +6 -16
- package/test/unit/spec/meeting/index.js +212 -125
- package/test/unit/spec/meeting/locusMediaRequest.ts +96 -58
- package/test/unit/spec/meeting/utils.js +55 -0
- package/test/unit/spec/meeting-info/meetinginfov2.js +443 -114
- package/test/unit/spec/meetings/index.js +78 -1
- package/test/unit/spec/member/index.js +7 -0
- package/test/unit/spec/member/util.js +24 -0
- package/test/unit/spec/reachability/clusterReachability.ts +47 -1
- package/test/unit/spec/reachability/index.ts +12 -0
- package/test/unit/spec/reachability/request.js +47 -2
- package/test/unit/spec/reconnection-manager/index.js +4 -4
- package/test/unit/spec/roap/turnDiscovery.ts +72 -28
|
@@ -93,13 +93,14 @@ import CaptchaError from '../../../../src/common/errors/captcha-error';
|
|
|
93
93
|
import PermissionError from '../../../../src/common/errors/permission';
|
|
94
94
|
import JoinWebinarError from '../../../../src/common/errors/join-webinar-error';
|
|
95
95
|
import IntentToJoinError from '../../../../src/common/errors/intent-to-join';
|
|
96
|
-
import MultistreamNotSupportedError from '../../../../src/common/errors/multistream-not-supported-error'
|
|
96
|
+
import MultistreamNotSupportedError from '../../../../src/common/errors/multistream-not-supported-error';
|
|
97
97
|
import testUtils from '../../../utils/testUtils';
|
|
98
98
|
import {
|
|
99
99
|
MeetingInfoV2CaptchaError,
|
|
100
100
|
MeetingInfoV2PasswordError,
|
|
101
101
|
MeetingInfoV2PolicyError,
|
|
102
|
-
MeetingInfoV2JoinWebinarError,
|
|
102
|
+
MeetingInfoV2JoinWebinarError,
|
|
103
|
+
MeetingInfoV2JoinForbiddenError,
|
|
103
104
|
} from '../../../../src/meeting-info/meeting-info-v2';
|
|
104
105
|
import {
|
|
105
106
|
DTLS_HANDSHAKE_FAILED_CLIENT_CODE,
|
|
@@ -115,7 +116,8 @@ import MeetingCollection from '@webex/plugin-meetings/src/meetings/collection';
|
|
|
115
116
|
|
|
116
117
|
import {EVENT_TRIGGERS as VOICEAEVENTS} from '@webex/internal-plugin-voicea';
|
|
117
118
|
import { createBrbState } from '@webex/plugin-meetings/src/meeting/brbState';
|
|
118
|
-
import JoinForbiddenError
|
|
119
|
+
import JoinForbiddenError from '../../../../src/common/errors/join-forbidden-error';
|
|
120
|
+
import { EventEmitter } from 'stream';
|
|
119
121
|
|
|
120
122
|
describe('plugin-meetings', () => {
|
|
121
123
|
const logger = {
|
|
@@ -208,6 +210,8 @@ describe('plugin-meetings', () => {
|
|
|
208
210
|
let membersSpy;
|
|
209
211
|
let meetingRequestSpy;
|
|
210
212
|
let correlationId;
|
|
213
|
+
let isoLocalClientMeetingJoinTime;
|
|
214
|
+
let uploadEvent;
|
|
211
215
|
|
|
212
216
|
beforeEach(() => {
|
|
213
217
|
webex = new MockWebex({
|
|
@@ -277,6 +281,8 @@ describe('plugin-meetings', () => {
|
|
|
277
281
|
test4 = `test4-${uuid.v4()}`;
|
|
278
282
|
testDestination = `testDestination-${uuid.v4()}`;
|
|
279
283
|
correlationId = uuid.v4();
|
|
284
|
+
uploadEvent = new EventEmitter();
|
|
285
|
+
uploadEvent.addListener('progress', () => {})
|
|
280
286
|
|
|
281
287
|
meeting = new Meeting(
|
|
282
288
|
{
|
|
@@ -667,7 +673,7 @@ describe('plugin-meetings', () => {
|
|
|
667
673
|
beforeEach(() => {
|
|
668
674
|
meeting.join = sinon.stub().callsFake((joinOptions) => {
|
|
669
675
|
meeting.isMultistream = joinOptions.enableMultistream;
|
|
670
|
-
return Promise.resolve(fakeJoinResult)
|
|
676
|
+
return Promise.resolve(fakeJoinResult);
|
|
671
677
|
});
|
|
672
678
|
addMediaInternalStub = sinon
|
|
673
679
|
.stub(meeting, 'addMediaInternal')
|
|
@@ -1070,7 +1076,11 @@ describe('plugin-meetings', () => {
|
|
|
1070
1076
|
mediaOptions,
|
|
1071
1077
|
});
|
|
1072
1078
|
|
|
1073
|
-
assert.deepEqual(result, {
|
|
1079
|
+
assert.deepEqual(result, {
|
|
1080
|
+
join: fakeJoinResult,
|
|
1081
|
+
media: undefined,
|
|
1082
|
+
multistreamEnabled: false,
|
|
1083
|
+
});
|
|
1074
1084
|
|
|
1075
1085
|
assert.calledOnce(meeting.join);
|
|
1076
1086
|
|
|
@@ -1174,7 +1184,10 @@ describe('plugin-meetings', () => {
|
|
|
1174
1184
|
type: addMediaError.name,
|
|
1175
1185
|
}
|
|
1176
1186
|
);
|
|
1177
|
-
assert.calledOnceWithExactly(meeting.leave, {
|
|
1187
|
+
assert.calledOnceWithExactly(meeting.leave, {
|
|
1188
|
+
resourceId: undefined,
|
|
1189
|
+
reason: 'joinWithMedia failure',
|
|
1190
|
+
});
|
|
1178
1191
|
});
|
|
1179
1192
|
});
|
|
1180
1193
|
|
|
@@ -1680,10 +1693,6 @@ describe('plugin-meetings', () => {
|
|
|
1680
1693
|
sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.resolve(joinMeetingResult));
|
|
1681
1694
|
});
|
|
1682
1695
|
|
|
1683
|
-
afterEach(() => {
|
|
1684
|
-
assert.exists(meeting.isoLocalClientMeetingJoinTime);
|
|
1685
|
-
});
|
|
1686
|
-
|
|
1687
1696
|
it('should join the meeting and return promise', async () => {
|
|
1688
1697
|
const join = meeting.join({pstnAudioType: 'dial-in'});
|
|
1689
1698
|
meeting.config.enableAutomaticLLM = true;
|
|
@@ -2655,7 +2664,7 @@ describe('plugin-meetings', () => {
|
|
|
2655
2664
|
|
|
2656
2665
|
meeting.roap.doTurnDiscovery = sinon.stub().resolves({
|
|
2657
2666
|
turnServerInfo: {
|
|
2658
|
-
|
|
2667
|
+
urls: [FAKE_TURN_URL],
|
|
2659
2668
|
username: FAKE_TURN_USER,
|
|
2660
2669
|
password: FAKE_TURN_PASSWORD,
|
|
2661
2670
|
},
|
|
@@ -2677,7 +2686,7 @@ describe('plugin-meetings', () => {
|
|
|
2677
2686
|
meeting.id,
|
|
2678
2687
|
sinon.match({
|
|
2679
2688
|
turnServerInfo: {
|
|
2680
|
-
|
|
2689
|
+
urls: [FAKE_TURN_URL],
|
|
2681
2690
|
username: FAKE_TURN_USER,
|
|
2682
2691
|
password: FAKE_TURN_PASSWORD,
|
|
2683
2692
|
},
|
|
@@ -2735,7 +2744,7 @@ describe('plugin-meetings', () => {
|
|
|
2735
2744
|
.onSecondCall()
|
|
2736
2745
|
.returns({
|
|
2737
2746
|
turnServerInfo: {
|
|
2738
|
-
|
|
2747
|
+
urls: [FAKE_TURN_URL],
|
|
2739
2748
|
username: FAKE_TURN_USER,
|
|
2740
2749
|
password: FAKE_TURN_PASSWORD,
|
|
2741
2750
|
},
|
|
@@ -2947,7 +2956,7 @@ describe('plugin-meetings', () => {
|
|
|
2947
2956
|
.onSecondCall()
|
|
2948
2957
|
.returns({
|
|
2949
2958
|
turnServerInfo: {
|
|
2950
|
-
|
|
2959
|
+
urls: [FAKE_TURN_URL],
|
|
2951
2960
|
username: FAKE_TURN_USER,
|
|
2952
2961
|
password: FAKE_TURN_PASSWORD,
|
|
2953
2962
|
},
|
|
@@ -3124,7 +3133,7 @@ describe('plugin-meetings', () => {
|
|
|
3124
3133
|
.onSecondCall()
|
|
3125
3134
|
.returns({
|
|
3126
3135
|
turnServerInfo: {
|
|
3127
|
-
|
|
3136
|
+
urls: [FAKE_TURN_URL],
|
|
3128
3137
|
username: FAKE_TURN_USER,
|
|
3129
3138
|
password: FAKE_TURN_PASSWORD,
|
|
3130
3139
|
},
|
|
@@ -3176,7 +3185,7 @@ describe('plugin-meetings', () => {
|
|
|
3176
3185
|
.onSecondCall()
|
|
3177
3186
|
.returns({
|
|
3178
3187
|
turnServerInfo: {
|
|
3179
|
-
|
|
3188
|
+
urls: [FAKE_TURN_URL],
|
|
3180
3189
|
username: FAKE_TURN_USER,
|
|
3181
3190
|
password: FAKE_TURN_PASSWORD,
|
|
3182
3191
|
},
|
|
@@ -3550,18 +3559,18 @@ describe('plugin-meetings', () => {
|
|
|
3550
3559
|
it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
|
|
3551
3560
|
let fakeMembersCollection = {
|
|
3552
3561
|
members: {
|
|
3553
|
-
member1: {
|
|
3554
|
-
member2: {
|
|
3555
|
-
member3: {
|
|
3562
|
+
member1: {isInMeeting: true},
|
|
3563
|
+
member2: {isInMeeting: true},
|
|
3564
|
+
member3: {isInMeeting: false},
|
|
3556
3565
|
},
|
|
3557
3566
|
};
|
|
3558
|
-
sinon.stub(meeting, 'getMembers').returns({
|
|
3559
|
-
const fakeData = {
|
|
3567
|
+
sinon.stub(meeting, 'getMembers').returns({membersCollection: fakeMembersCollection});
|
|
3568
|
+
const fakeData = {intervalMetadata: {}, networkType: 'wifi'};
|
|
3560
3569
|
|
|
3561
3570
|
statsAnalyzerStub.emit(
|
|
3562
|
-
{
|
|
3571
|
+
{file: 'test', function: 'test'},
|
|
3563
3572
|
StatsAnalyzerEventNames.MEDIA_QUALITY,
|
|
3564
|
-
{
|
|
3573
|
+
{data: fakeData}
|
|
3565
3574
|
);
|
|
3566
3575
|
|
|
3567
3576
|
assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
|
|
@@ -3570,15 +3579,17 @@ describe('plugin-meetings', () => {
|
|
|
3570
3579
|
meetingId: meeting.id,
|
|
3571
3580
|
},
|
|
3572
3581
|
payload: {
|
|
3573
|
-
intervals: [
|
|
3582
|
+
intervals: [
|
|
3583
|
+
sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2)),
|
|
3584
|
+
],
|
|
3574
3585
|
},
|
|
3575
3586
|
});
|
|
3576
3587
|
fakeMembersCollection.members.member2.isInMeeting = false;
|
|
3577
3588
|
|
|
3578
3589
|
statsAnalyzerStub.emit(
|
|
3579
|
-
{
|
|
3590
|
+
{file: 'test', function: 'test'},
|
|
3580
3591
|
StatsAnalyzerEventNames.MEDIA_QUALITY,
|
|
3581
|
-
{
|
|
3592
|
+
{data: fakeData}
|
|
3582
3593
|
);
|
|
3583
3594
|
|
|
3584
3595
|
assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
|
|
@@ -3587,7 +3598,9 @@ describe('plugin-meetings', () => {
|
|
|
3587
3598
|
meetingId: meeting.id,
|
|
3588
3599
|
},
|
|
3589
3600
|
payload: {
|
|
3590
|
-
intervals: [
|
|
3601
|
+
intervals: [
|
|
3602
|
+
sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1)),
|
|
3603
|
+
],
|
|
3591
3604
|
},
|
|
3592
3605
|
});
|
|
3593
3606
|
});
|
|
@@ -3624,7 +3637,7 @@ describe('plugin-meetings', () => {
|
|
|
3624
3637
|
|
|
3625
3638
|
meeting.roap.doTurnDiscovery = sinon.stub().resolves({
|
|
3626
3639
|
turnServerInfo: {
|
|
3627
|
-
|
|
3640
|
+
urls: [FAKE_TURN_URL],
|
|
3628
3641
|
username: FAKE_TURN_USER,
|
|
3629
3642
|
password: FAKE_TURN_PASSWORD,
|
|
3630
3643
|
},
|
|
@@ -3650,7 +3663,7 @@ describe('plugin-meetings', () => {
|
|
|
3650
3663
|
meeting.id,
|
|
3651
3664
|
sinon.match({
|
|
3652
3665
|
turnServerInfo: {
|
|
3653
|
-
|
|
3666
|
+
urls: [FAKE_TURN_URL],
|
|
3654
3667
|
username: FAKE_TURN_USER,
|
|
3655
3668
|
password: FAKE_TURN_PASSWORD,
|
|
3656
3669
|
},
|
|
@@ -3842,7 +3855,6 @@ describe('plugin-meetings', () => {
|
|
|
3842
3855
|
});
|
|
3843
3856
|
|
|
3844
3857
|
describe('when in a multistream meeting', () => {
|
|
3845
|
-
|
|
3846
3858
|
beforeEach(() => {
|
|
3847
3859
|
meeting.isMultistream = true;
|
|
3848
3860
|
});
|
|
@@ -3853,7 +3865,7 @@ describe('plugin-meetings', () => {
|
|
|
3853
3865
|
await brbResult;
|
|
3854
3866
|
assert.exists(brbResult.then);
|
|
3855
3867
|
assert.calledOnce(meeting.brbState.enable);
|
|
3856
|
-
})
|
|
3868
|
+
});
|
|
3857
3869
|
|
|
3858
3870
|
it('should disable #beRightBack and return a promise', async () => {
|
|
3859
3871
|
const brbResult = meeting.beRightBack(false);
|
|
@@ -3861,7 +3873,7 @@ describe('plugin-meetings', () => {
|
|
|
3861
3873
|
await brbResult;
|
|
3862
3874
|
assert.exists(brbResult.then);
|
|
3863
3875
|
assert.calledOnce(meeting.brbState.enable);
|
|
3864
|
-
})
|
|
3876
|
+
});
|
|
3865
3877
|
|
|
3866
3878
|
it('should throw an error and reject the promise if setBrb fails', async () => {
|
|
3867
3879
|
const error = new Error('setBrb failed');
|
|
@@ -3874,7 +3886,7 @@ describe('plugin-meetings', () => {
|
|
|
3874
3886
|
assert.equal(err.message, 'setBrb failed');
|
|
3875
3887
|
assert.isRejected((Promise.reject()));
|
|
3876
3888
|
}
|
|
3877
|
-
})
|
|
3889
|
+
});
|
|
3878
3890
|
});
|
|
3879
3891
|
});
|
|
3880
3892
|
|
|
@@ -3928,7 +3940,7 @@ describe('plugin-meetings', () => {
|
|
|
3928
3940
|
.resolves({id: 'fake clientMediaPreferences'});
|
|
3929
3941
|
meeting.roap.doTurnDiscovery = sinon.stub().resolves({
|
|
3930
3942
|
turnServerInfo: {
|
|
3931
|
-
|
|
3943
|
+
urls: ['turns:turn-server-url1:443?transport=tcp', 'turns:turn-server-url2:443?transport=tcp'],
|
|
3932
3944
|
username: 'turn user',
|
|
3933
3945
|
password: 'turn password',
|
|
3934
3946
|
},
|
|
@@ -3946,12 +3958,7 @@ describe('plugin-meetings', () => {
|
|
|
3946
3958
|
expectedMediaConnectionConfig = {
|
|
3947
3959
|
iceServers: [
|
|
3948
3960
|
{
|
|
3949
|
-
urls: 'turn:turn-server-
|
|
3950
|
-
username: 'turn user',
|
|
3951
|
-
credential: 'turn password',
|
|
3952
|
-
},
|
|
3953
|
-
{
|
|
3954
|
-
urls: 'turns:turn-server-url:443?transport=tcp',
|
|
3961
|
+
urls: ['turns:turn-server-url1:443?transport=tcp', 'turns:turn-server-url2:443?transport=tcp'],
|
|
3955
3962
|
username: 'turn user',
|
|
3956
3963
|
credential: 'turn password',
|
|
3957
3964
|
},
|
|
@@ -4006,7 +4013,7 @@ describe('plugin-meetings', () => {
|
|
|
4006
4013
|
initiateOffer: sinon.stub().resolves({}),
|
|
4007
4014
|
update: sinon.stub().resolves({}),
|
|
4008
4015
|
on: sinon.stub(),
|
|
4009
|
-
roapMessageReceived: sinon.stub()
|
|
4016
|
+
roapMessageReceived: sinon.stub(),
|
|
4010
4017
|
};
|
|
4011
4018
|
|
|
4012
4019
|
fakeMultistreamRoapMediaConnection = {
|
|
@@ -4035,7 +4042,7 @@ describe('plugin-meetings', () => {
|
|
|
4035
4042
|
|
|
4036
4043
|
locusMediaRequestStub = sinon
|
|
4037
4044
|
.stub(WebexPlugin.prototype, 'request')
|
|
4038
|
-
.resolves({body: {locus: {fullState: {}}}});
|
|
4045
|
+
.resolves({body: {locus: {fullState: {}}}, upload: sinon.match.instanceOf(EventEmitter), download: sinon.match.instanceOf(EventEmitter)});
|
|
4039
4046
|
|
|
4040
4047
|
// setup some things and mocks so that the call to join() works
|
|
4041
4048
|
// (we need to call join() because it creates the LocusMediaRequest instance
|
|
@@ -4144,6 +4151,8 @@ describe('plugin-meetings', () => {
|
|
|
4144
4151
|
id: 'fake clientMediaPreferences',
|
|
4145
4152
|
},
|
|
4146
4153
|
},
|
|
4154
|
+
upload: sinon.match.instanceOf(EventEmitter),
|
|
4155
|
+
download: sinon.match.instanceOf(EventEmitter),
|
|
4147
4156
|
});
|
|
4148
4157
|
};
|
|
4149
4158
|
|
|
@@ -4171,6 +4180,8 @@ describe('plugin-meetings', () => {
|
|
|
4171
4180
|
},
|
|
4172
4181
|
],
|
|
4173
4182
|
},
|
|
4183
|
+
upload: sinon.match.instanceOf(EventEmitter),
|
|
4184
|
+
download: sinon.match.instanceOf(EventEmitter),
|
|
4174
4185
|
});
|
|
4175
4186
|
};
|
|
4176
4187
|
|
|
@@ -4195,6 +4206,8 @@ describe('plugin-meetings', () => {
|
|
|
4195
4206
|
respOnlySdp: true,
|
|
4196
4207
|
usingResource: null,
|
|
4197
4208
|
},
|
|
4209
|
+
upload: sinon.match.instanceOf(EventEmitter),
|
|
4210
|
+
download: sinon.match.instanceOf(EventEmitter),
|
|
4198
4211
|
});
|
|
4199
4212
|
};
|
|
4200
4213
|
|
|
@@ -5213,7 +5226,7 @@ describe('plugin-meetings', () => {
|
|
|
5213
5226
|
// and check that when we fallback to transcoded we still do another TURN discovery
|
|
5214
5227
|
await runCheck(
|
|
5215
5228
|
{
|
|
5216
|
-
|
|
5229
|
+
urls: ['turns:turn-server-url1:443?transport=tcp', 'turns:turn-server-url2:443?transport=tcp'],
|
|
5217
5230
|
username: 'turn user',
|
|
5218
5231
|
password: 'turn password',
|
|
5219
5232
|
},
|
|
@@ -5227,7 +5240,7 @@ describe('plugin-meetings', () => {
|
|
|
5227
5240
|
// but doing it just for completeness
|
|
5228
5241
|
await runCheck(
|
|
5229
5242
|
{
|
|
5230
|
-
|
|
5243
|
+
urls: ['turns:turn-server-url1:443?transport=tcp', 'turns:turn-server-url2:443?transport=tcp'],
|
|
5231
5244
|
username: 'turn user',
|
|
5232
5245
|
password: 'turn password',
|
|
5233
5246
|
},
|
|
@@ -6337,7 +6350,10 @@ describe('plugin-meetings', () => {
|
|
|
6337
6350
|
.throws(new MeetingInfoV2JoinForbiddenError(403003, FAKE_MEETING_INFO)),
|
|
6338
6351
|
};
|
|
6339
6352
|
|
|
6340
|
-
await assert.isRejected(
|
|
6353
|
+
await assert.isRejected(
|
|
6354
|
+
meeting.fetchMeetingInfo({sendCAevents: true}),
|
|
6355
|
+
JoinForbiddenError
|
|
6356
|
+
);
|
|
6341
6357
|
|
|
6342
6358
|
assert.calledWith(
|
|
6343
6359
|
meeting.attrs.meetingInfoProvider.fetchMeetingInfo,
|
|
@@ -6353,10 +6369,7 @@ describe('plugin-meetings', () => {
|
|
|
6353
6369
|
|
|
6354
6370
|
assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
|
|
6355
6371
|
assert.equal(meeting.meetingInfoFailureCode, 403003);
|
|
6356
|
-
assert.equal(
|
|
6357
|
-
meeting.meetingInfoFailureReason,
|
|
6358
|
-
MEETING_INFO_FAILURE_REASON.NOT_REACH_JBH
|
|
6359
|
-
);
|
|
6372
|
+
assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.NOT_REACH_JBH);
|
|
6360
6373
|
assert.equal(meeting.requiredCaptcha, null);
|
|
6361
6374
|
});
|
|
6362
6375
|
|
|
@@ -6733,15 +6746,10 @@ describe('plugin-meetings', () => {
|
|
|
6733
6746
|
meeting.attrs.meetingInfoProvider = {
|
|
6734
6747
|
fetchMeetingInfo: sinon
|
|
6735
6748
|
.stub()
|
|
6736
|
-
.throws(
|
|
6737
|
-
new MeetingInfoV2JoinWebinarError(403021, FAKE_MEETING_INFO, 'a message')
|
|
6738
|
-
),
|
|
6749
|
+
.throws(new MeetingInfoV2JoinWebinarError(403021, FAKE_MEETING_INFO, 'a message')),
|
|
6739
6750
|
};
|
|
6740
6751
|
|
|
6741
|
-
await assert.isRejected(
|
|
6742
|
-
meeting.fetchMeetingInfo({sendCAevents: true}),
|
|
6743
|
-
JoinWebinarError
|
|
6744
|
-
);
|
|
6752
|
+
await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinWebinarError);
|
|
6745
6753
|
|
|
6746
6754
|
assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
|
|
6747
6755
|
assert.equal(
|
|
@@ -6756,15 +6764,10 @@ describe('plugin-meetings', () => {
|
|
|
6756
6764
|
meeting.attrs.meetingInfoProvider = {
|
|
6757
6765
|
fetchMeetingInfo: sinon
|
|
6758
6766
|
.stub()
|
|
6759
|
-
.throws(
|
|
6760
|
-
new MeetingInfoV2JoinWebinarError(403026, FAKE_MEETING_INFO, 'a message')
|
|
6761
|
-
),
|
|
6767
|
+
.throws(new MeetingInfoV2JoinWebinarError(403026, FAKE_MEETING_INFO, 'a message')),
|
|
6762
6768
|
};
|
|
6763
6769
|
|
|
6764
|
-
await assert.isRejected(
|
|
6765
|
-
meeting.fetchMeetingInfo({sendCAevents: true}),
|
|
6766
|
-
JoinWebinarError
|
|
6767
|
-
);
|
|
6770
|
+
await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinWebinarError);
|
|
6768
6771
|
|
|
6769
6772
|
assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
|
|
6770
6773
|
assert.equal(
|
|
@@ -6779,15 +6782,10 @@ describe('plugin-meetings', () => {
|
|
|
6779
6782
|
meeting.attrs.meetingInfoProvider = {
|
|
6780
6783
|
fetchMeetingInfo: sinon
|
|
6781
6784
|
.stub()
|
|
6782
|
-
.throws(
|
|
6783
|
-
new MeetingInfoV2JoinWebinarError(403037, FAKE_MEETING_INFO, 'a message')
|
|
6784
|
-
),
|
|
6785
|
+
.throws(new MeetingInfoV2JoinWebinarError(403037, FAKE_MEETING_INFO, 'a message')),
|
|
6785
6786
|
};
|
|
6786
6787
|
|
|
6787
|
-
await assert.isRejected(
|
|
6788
|
-
meeting.fetchMeetingInfo({sendCAevents: true}),
|
|
6789
|
-
JoinWebinarError
|
|
6790
|
-
);
|
|
6788
|
+
await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinWebinarError);
|
|
6791
6789
|
|
|
6792
6790
|
assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
|
|
6793
6791
|
assert.equal(
|
|
@@ -7524,6 +7522,27 @@ describe('plugin-meetings', () => {
|
|
|
7524
7522
|
});
|
|
7525
7523
|
});
|
|
7526
7524
|
|
|
7525
|
+
describe('#setIsoLocalClientMeetingJoinTime', () => {
|
|
7526
|
+
it('should fallback to system clock ISO string when given an undefined value', () => {
|
|
7527
|
+
const currentSystemTime = new Date().toISOString();
|
|
7528
|
+
meeting.isoLocalClientMeetingJoinTime = undefined;
|
|
7529
|
+
assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
|
|
7530
|
+
});
|
|
7531
|
+
|
|
7532
|
+
it('should fallback to system clock ISO string when given an invalid value', () => {
|
|
7533
|
+
const currentSystemTime = new Date().toISOString();
|
|
7534
|
+
meeting.isoLocalClientMeetingJoinTime = 'invalid-date';
|
|
7535
|
+
assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
|
|
7536
|
+
});
|
|
7537
|
+
|
|
7538
|
+
it('should set the isoLocalClientMeetingJoinTime correctly for a valid date string', () => {
|
|
7539
|
+
const validDateString = 'Tue, 01 Apr 2025 13:00:36 GMT';
|
|
7540
|
+
const expectedISOString = new Date(validDateString).toISOString();
|
|
7541
|
+
meeting.isoLocalClientMeetingJoinTime = validDateString;
|
|
7542
|
+
assert.equal(meeting.isoLocalClientMeetingJoinTime, expectedISOString);
|
|
7543
|
+
});
|
|
7544
|
+
});
|
|
7545
|
+
|
|
7527
7546
|
describe('#updateCallStateForMetrics', () => {
|
|
7528
7547
|
it('should update the callState, overriding existing values', () => {
|
|
7529
7548
|
assert.deepEqual(meeting.callStateForMetrics, {correlationId, sessionCorrelationId: ''});
|
|
@@ -8590,13 +8609,19 @@ describe('plugin-meetings', () => {
|
|
|
8590
8609
|
const fakeErrorMessage = 'test error';
|
|
8591
8610
|
const fakeRootCauseName = 'root cause name';
|
|
8592
8611
|
const fakeErrorName = 'test error name';
|
|
8612
|
+
let clock;
|
|
8593
8613
|
|
|
8594
8614
|
beforeEach(() => {
|
|
8615
|
+
clock = sinon.useFakeTimers();
|
|
8595
8616
|
meeting.setupMediaConnectionListeners();
|
|
8596
8617
|
webex.internal.newMetrics.submitClientEvent.resetHistory();
|
|
8597
8618
|
Metrics.sendBehavioralMetric.resetHistory();
|
|
8598
8619
|
});
|
|
8599
8620
|
|
|
8621
|
+
afterEach(() => {
|
|
8622
|
+
clock.restore();
|
|
8623
|
+
});
|
|
8624
|
+
|
|
8600
8625
|
const checkMetricSent = (event, error) => {
|
|
8601
8626
|
assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
|
|
8602
8627
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
@@ -8665,6 +8690,13 @@ describe('plugin-meetings', () => {
|
|
|
8665
8690
|
});
|
|
8666
8691
|
|
|
8667
8692
|
it('should send metrics for SdpAnswerHandlingError error', () => {
|
|
8693
|
+
meeting.sdpResponseTimer = '1234';
|
|
8694
|
+
meeting.deferSDPAnswer = {
|
|
8695
|
+
reject: sinon.stub(),
|
|
8696
|
+
};
|
|
8697
|
+
|
|
8698
|
+
const clearTimeoutSpy = sinon.spy(clock, 'clearTimeout');
|
|
8699
|
+
|
|
8668
8700
|
const fakeError = new Errors.SdpAnswerHandlingError(fakeErrorMessage, {
|
|
8669
8701
|
name: fakeErrorName,
|
|
8670
8702
|
cause: {name: fakeRootCauseName},
|
|
@@ -8679,6 +8711,8 @@ describe('plugin-meetings', () => {
|
|
|
8679
8711
|
fakeErrorMessage,
|
|
8680
8712
|
fakeRootCauseName
|
|
8681
8713
|
);
|
|
8714
|
+
assert.calledOnce(meeting.deferSDPAnswer.reject);
|
|
8715
|
+
assert.calledOnce(clearTimeoutSpy);
|
|
8682
8716
|
});
|
|
8683
8717
|
|
|
8684
8718
|
it('should send metrics for SdpError error', () => {
|
|
@@ -9223,22 +9257,22 @@ describe('plugin-meetings', () => {
|
|
|
9223
9257
|
const assertBrb = (enabled) => {
|
|
9224
9258
|
meeting.brbState = createBrbState(meeting, false);
|
|
9225
9259
|
meeting.locusInfo.emit(
|
|
9226
|
-
{
|
|
9260
|
+
{function: 'test', file: 'test'},
|
|
9227
9261
|
LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
|
|
9228
|
-
{
|
|
9229
|
-
)
|
|
9262
|
+
{brb: {enabled}}
|
|
9263
|
+
);
|
|
9230
9264
|
assert.calledWithExactly(
|
|
9231
9265
|
TriggerProxy.trigger,
|
|
9232
9266
|
meeting,
|
|
9233
9267
|
{file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
|
|
9234
9268
|
EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
|
|
9235
|
-
{
|
|
9269
|
+
{payload: {brb: {enabled}}}
|
|
9236
9270
|
);
|
|
9237
|
-
}
|
|
9271
|
+
};
|
|
9238
9272
|
|
|
9239
9273
|
assertBrb(true);
|
|
9240
9274
|
assertBrb(false);
|
|
9241
|
-
})
|
|
9275
|
+
});
|
|
9242
9276
|
|
|
9243
9277
|
it('listens to the interpretation changed event', () => {
|
|
9244
9278
|
meeting.simultaneousInterpretation.updateSelfInterpretation = sinon.stub();
|
|
@@ -9922,6 +9956,22 @@ describe('plugin-meetings', () => {
|
|
|
9922
9956
|
});
|
|
9923
9957
|
});
|
|
9924
9958
|
|
|
9959
|
+
describe('#emailInput', () => {
|
|
9960
|
+
it('should set the email input', () => {
|
|
9961
|
+
assert.notOk(meeting.emailInput);
|
|
9962
|
+
meeting.emailInput = 'current';
|
|
9963
|
+
assert.equal(meeting.emailInput, 'current');
|
|
9964
|
+
});
|
|
9965
|
+
});
|
|
9966
|
+
|
|
9967
|
+
describe('#userNameInput', () => {
|
|
9968
|
+
it('should set the user name input', () => {
|
|
9969
|
+
assert.notOk(meeting.userNameInput);
|
|
9970
|
+
meeting.userNameInput = 'current';
|
|
9971
|
+
assert.equal(meeting.userNameInput, 'current');
|
|
9972
|
+
});
|
|
9973
|
+
});
|
|
9974
|
+
|
|
9925
9975
|
describe('#setPermissionTokenPayload', () => {
|
|
9926
9976
|
let now;
|
|
9927
9977
|
let clock;
|
|
@@ -11326,18 +11376,21 @@ describe('plugin-meetings', () => {
|
|
|
11326
11376
|
);
|
|
11327
11377
|
});
|
|
11328
11378
|
|
|
11329
|
-
|
|
11330
11379
|
it('connect ps data channel if ps started in webinar', async () => {
|
|
11331
11380
|
meeting.joinedWith = {state: 'JOINED'};
|
|
11332
|
-
meeting.locusInfo = {
|
|
11381
|
+
meeting.locusInfo = {
|
|
11382
|
+
url: 'a url',
|
|
11383
|
+
info: {
|
|
11384
|
+
datachannelUrl: 'a datachannel url',
|
|
11385
|
+
practiceSessionDatachannelUrl: 'a ps datachannel url',
|
|
11386
|
+
},
|
|
11387
|
+
};
|
|
11333
11388
|
meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(true);
|
|
11334
11389
|
await meeting.updateLLMConnection();
|
|
11335
11390
|
|
|
11336
11391
|
assert.notCalled(webex.internal.llm.disconnectLLM);
|
|
11337
11392
|
assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a ps datachannel url');
|
|
11338
|
-
|
|
11339
11393
|
});
|
|
11340
|
-
|
|
11341
11394
|
});
|
|
11342
11395
|
|
|
11343
11396
|
describe('#setLocus', () => {
|
|
@@ -11755,24 +11808,29 @@ describe('plugin-meetings', () => {
|
|
|
11755
11808
|
|
|
11756
11809
|
activeSharingId.whiteboard = beneficiaryId;
|
|
11757
11810
|
|
|
11758
|
-
eventTrigger.share.push(
|
|
11759
|
-
|
|
11760
|
-
|
|
11761
|
-
|
|
11762
|
-
|
|
11763
|
-
|
|
11764
|
-
|
|
11765
|
-
|
|
11766
|
-
|
|
11767
|
-
|
|
11768
|
-
|
|
11769
|
-
|
|
11770
|
-
|
|
11771
|
-
|
|
11772
|
-
|
|
11773
|
-
|
|
11774
|
-
|
|
11811
|
+
eventTrigger.share.push(
|
|
11812
|
+
meeting.webinar.selfIsAttendee
|
|
11813
|
+
? {
|
|
11814
|
+
eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
|
|
11815
|
+
functionName: 'remoteShare',
|
|
11816
|
+
eventPayload: {
|
|
11817
|
+
memberId: null,
|
|
11818
|
+
url,
|
|
11819
|
+
shareInstanceId,
|
|
11820
|
+
annotationInfo: undefined,
|
|
11821
|
+
resourceType: undefined,
|
|
11822
|
+
},
|
|
11823
|
+
}
|
|
11824
|
+
: {
|
|
11825
|
+
eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
|
|
11826
|
+
functionName: 'startWhiteboardShare',
|
|
11827
|
+
eventPayload: {resourceUrl, memberId: beneficiaryId},
|
|
11828
|
+
}
|
|
11829
|
+
);
|
|
11775
11830
|
|
|
11831
|
+
shareStatus = meeting.webinar.selfIsAttendee
|
|
11832
|
+
? SHARE_STATUS.REMOTE_SHARE_ACTIVE
|
|
11833
|
+
: SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
|
|
11776
11834
|
}
|
|
11777
11835
|
|
|
11778
11836
|
if (eventTrigger.member) {
|
|
@@ -11804,24 +11862,29 @@ describe('plugin-meetings', () => {
|
|
|
11804
11862
|
newPayload.current.content.disposition = FLOOR_ACTION.ACCEPTED;
|
|
11805
11863
|
newPayload.current.content.beneficiaryId = otherBeneficiaryId;
|
|
11806
11864
|
|
|
11807
|
-
eventTrigger.share.push(
|
|
11808
|
-
|
|
11809
|
-
|
|
11810
|
-
|
|
11811
|
-
|
|
11812
|
-
|
|
11813
|
-
|
|
11814
|
-
|
|
11815
|
-
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
11819
|
-
|
|
11820
|
-
|
|
11821
|
-
|
|
11822
|
-
|
|
11823
|
-
|
|
11865
|
+
eventTrigger.share.push(
|
|
11866
|
+
meeting.webinar.selfIsAttendee
|
|
11867
|
+
? {
|
|
11868
|
+
eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
|
|
11869
|
+
functionName: 'remoteShare',
|
|
11870
|
+
eventPayload: {
|
|
11871
|
+
memberId: null,
|
|
11872
|
+
url,
|
|
11873
|
+
shareInstanceId,
|
|
11874
|
+
annotationInfo: undefined,
|
|
11875
|
+
resourceType: undefined,
|
|
11876
|
+
},
|
|
11877
|
+
}
|
|
11878
|
+
: {
|
|
11879
|
+
eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
|
|
11880
|
+
functionName: 'startWhiteboardShare',
|
|
11881
|
+
eventPayload: {resourceUrl, memberId: beneficiaryId},
|
|
11882
|
+
}
|
|
11883
|
+
);
|
|
11824
11884
|
|
|
11885
|
+
shareStatus = meeting.webinar.selfIsAttendee
|
|
11886
|
+
? SHARE_STATUS.REMOTE_SHARE_ACTIVE
|
|
11887
|
+
: SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
|
|
11825
11888
|
} else {
|
|
11826
11889
|
eventTrigger.share.push({
|
|
11827
11890
|
eventName: EVENT_TRIGGERS.MEETING_STOPPED_SHARING_WHITEBOARD,
|
|
@@ -11951,24 +12014,24 @@ describe('plugin-meetings', () => {
|
|
|
11951
12014
|
describe('Whiteboard Share - Webinar Attendee', () => {
|
|
11952
12015
|
it('Scenario #1: Whiteboard sharing as a webinar attendee', () => {
|
|
11953
12016
|
// Set the webinar attendee flag
|
|
11954
|
-
meeting.webinar = {
|
|
12017
|
+
meeting.webinar = {selfIsAttendee: true};
|
|
11955
12018
|
meeting.locusInfo.info.isWebinar = true;
|
|
11956
12019
|
|
|
11957
12020
|
// Step 1: Start sharing whiteboard A
|
|
11958
12021
|
const data1 = generateData(
|
|
11959
|
-
blankPayload,
|
|
11960
|
-
true,
|
|
11961
|
-
false,
|
|
11962
|
-
USER_IDS.REMOTE_A,
|
|
12022
|
+
blankPayload, // Initial payload
|
|
12023
|
+
true, // isGranting: Granting share
|
|
12024
|
+
false, // isContent: Whiteboard (not content)
|
|
12025
|
+
USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
|
|
11963
12026
|
RESOURCE_URLS.WHITEBOARD_A // Resource URL: Whiteboard A
|
|
11964
12027
|
);
|
|
11965
12028
|
|
|
11966
12029
|
// Step 2: Stop sharing whiteboard A
|
|
11967
12030
|
const data2 = generateData(
|
|
11968
|
-
data1.payload,
|
|
11969
|
-
false,
|
|
11970
|
-
false,
|
|
11971
|
-
USER_IDS.REMOTE_A
|
|
12031
|
+
data1.payload, // Updated payload from Step 1
|
|
12032
|
+
false, // isGranting: Stopping share
|
|
12033
|
+
false, // isContent: Whiteboard
|
|
12034
|
+
USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
|
|
11972
12035
|
);
|
|
11973
12036
|
|
|
11974
12037
|
// Validate the payload changes and status updates
|
|
@@ -11979,7 +12042,6 @@ describe('plugin-meetings', () => {
|
|
|
11979
12042
|
});
|
|
11980
12043
|
});
|
|
11981
12044
|
|
|
11982
|
-
|
|
11983
12045
|
describe('Whiteboard A --> Whiteboard B', () => {
|
|
11984
12046
|
it('Scenario #1: you share both whiteboards', () => {
|
|
11985
12047
|
const data1 = generateData(
|
|
@@ -12632,6 +12694,31 @@ describe('plugin-meetings', () => {
|
|
|
12632
12694
|
});
|
|
12633
12695
|
});
|
|
12634
12696
|
});
|
|
12697
|
+
|
|
12698
|
+
describe('handleShareVideoStreamMuteStateChange', () => {
|
|
12699
|
+
it('should emit MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE event with correct fields', () => {
|
|
12700
|
+
meeting.isMultistream = true;
|
|
12701
|
+
meeting.statsAnalyzer = {shareVideoEncoderImplementation: 'OpenH264'};
|
|
12702
|
+
meeting.mediaProperties.shareVideoStream = {
|
|
12703
|
+
getSettings: sinon.stub().returns({displaySurface: 'monitor', frameRate: 30}),
|
|
12704
|
+
};
|
|
12705
|
+
|
|
12706
|
+
meeting.handleShareVideoStreamMuteStateChange(true);
|
|
12707
|
+
|
|
12708
|
+
assert.calledOnceWithExactly(
|
|
12709
|
+
Metrics.sendBehavioralMetric,
|
|
12710
|
+
BEHAVIORAL_METRICS.MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE,
|
|
12711
|
+
{
|
|
12712
|
+
correlationId: meeting.correlationId,
|
|
12713
|
+
muted: true,
|
|
12714
|
+
encoderImplementation: 'OpenH264',
|
|
12715
|
+
displaySurface: 'monitor',
|
|
12716
|
+
isMultistream: true,
|
|
12717
|
+
frameRate: 30,
|
|
12718
|
+
}
|
|
12719
|
+
);
|
|
12720
|
+
});
|
|
12721
|
+
});
|
|
12635
12722
|
});
|
|
12636
12723
|
|
|
12637
12724
|
describe('#startKeepAlive', () => {
|
|
@@ -13290,7 +13377,7 @@ describe('plugin-meetings', () => {
|
|
|
13290
13377
|
await meeting.roapMessageReceived(fakeMessage);
|
|
13291
13378
|
|
|
13292
13379
|
assert.fail('Expected MultistreamNotSupportedError to be thrown');
|
|
13293
|
-
} catch(e) {
|
|
13380
|
+
} catch (e) {
|
|
13294
13381
|
assert.isTrue(e instanceof MultistreamNotSupportedError);
|
|
13295
13382
|
}
|
|
13296
13383
|
|