@webex/plugin-meetings 3.7.0-next.9 → 3.7.0-wxcc.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/annotation/index.js +17 -0
- package/dist/annotation/index.js.map +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/common/errors/join-forbidden-error.js +52 -0
- package/dist/common/errors/join-forbidden-error.js.map +1 -0
- package/dist/common/errors/{webinar-registration-error.js → join-webinar-error.js} +12 -12
- package/dist/common/errors/join-webinar-error.js.map +1 -0
- package/dist/common/errors/multistream-not-supported-error.js +53 -0
- package/dist/common/errors/multistream-not-supported-error.js.map +1 -0
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.js +46 -5
- package/dist/constants.js.map +1 -1
- package/dist/index.js +16 -11
- package/dist/index.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/index.js +14 -3
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/selfUtils.js +30 -17
- package/dist/locus-info/selfUtils.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +2 -0
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +960 -832
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/locusMediaRequest.js +9 -0
- package/dist/meeting/locusMediaRequest.js.map +1 -1
- package/dist/meeting/request.js +30 -0
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/request.type.js.map +1 -1
- package/dist/meeting/util.js +16 -16
- package/dist/meeting/util.js.map +1 -1
- package/dist/meeting-info/meeting-info-v2.js +96 -33
- package/dist/meeting-info/meeting-info-v2.js.map +1 -1
- package/dist/meeting-info/utilv2.js +1 -1
- package/dist/meeting-info/utilv2.js.map +1 -1
- package/dist/meetings/index.js +103 -54
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/meetings.types.js +2 -0
- package/dist/meetings/meetings.types.js.map +1 -1
- package/dist/meetings/util.js +1 -1
- package/dist/meetings/util.js.map +1 -1
- package/dist/member/index.js +9 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/types.js.map +1 -1
- package/dist/member/util.js +39 -28
- package/dist/member/util.js.map +1 -1
- package/dist/metrics/constants.js +3 -2
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/remoteMedia.js +30 -15
- package/dist/multistream/remoteMedia.js.map +1 -1
- package/dist/multistream/sendSlotManager.js +24 -0
- package/dist/multistream/sendSlotManager.js.map +1 -1
- package/dist/roap/index.js +10 -8
- package/dist/roap/index.js.map +1 -1
- package/dist/types/annotation/index.d.ts +5 -0
- package/dist/types/common/errors/join-forbidden-error.d.ts +15 -0
- package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
- package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
- package/dist/types/constants.d.ts +38 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/locus-info/index.d.ts +2 -1
- package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
- package/dist/types/meeting/index.d.ts +19 -12
- package/dist/types/meeting/locusMediaRequest.d.ts +4 -0
- package/dist/types/meeting/request.d.ts +12 -1
- package/dist/types/meeting/request.type.d.ts +6 -0
- package/dist/types/meeting/util.d.ts +1 -1
- package/dist/types/meeting-info/meeting-info-v2.d.ts +27 -4
- package/dist/types/meetings/index.d.ts +16 -1
- package/dist/types/meetings/meetings.types.d.ts +8 -0
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/member/types.d.ts +7 -0
- package/dist/types/metrics/constants.d.ts +2 -1
- package/dist/types/multistream/sendSlotManager.d.ts +8 -1
- package/dist/webinar/index.js +354 -3
- package/dist/webinar/index.js.map +1 -1
- package/package.json +23 -22
- package/src/annotation/index.ts +16 -0
- package/src/common/errors/join-forbidden-error.ts +26 -0
- package/src/common/errors/join-webinar-error.ts +24 -0
- package/src/common/errors/multistream-not-supported-error.ts +30 -0
- package/src/config.ts +1 -1
- package/src/constants.ts +43 -3
- package/src/index.ts +5 -3
- package/src/locus-info/index.ts +20 -3
- package/src/locus-info/selfUtils.ts +19 -6
- package/src/meeting/in-meeting-actions.ts +4 -0
- package/src/meeting/index.ts +259 -80
- package/src/meeting/locusMediaRequest.ts +7 -0
- package/src/meeting/request.ts +26 -1
- package/src/meeting/request.type.ts +7 -0
- package/src/meeting/util.ts +8 -10
- package/src/meeting-info/meeting-info-v2.ts +74 -11
- package/src/meeting-info/utilv2.ts +3 -1
- package/src/meetings/index.ts +73 -20
- package/src/meetings/meetings.types.ts +10 -0
- package/src/meetings/util.ts +2 -1
- package/src/member/index.ts +9 -0
- package/src/member/types.ts +8 -0
- package/src/member/util.ts +34 -24
- package/src/metrics/constants.ts +2 -1
- package/src/multistream/remoteMedia.ts +28 -15
- package/src/multistream/sendSlotManager.ts +31 -0
- package/src/roap/index.ts +10 -8
- package/src/webinar/index.ts +197 -3
- package/test/unit/spec/annotation/index.ts +46 -1
- package/test/unit/spec/locus-info/index.js +292 -60
- package/test/unit/spec/locus-info/selfConstant.js +7 -0
- package/test/unit/spec/locus-info/selfUtils.js +91 -1
- package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
- package/test/unit/spec/meeting/index.js +722 -105
- package/test/unit/spec/meeting/utils.js +22 -19
- package/test/unit/spec/meeting-info/meetinginfov2.js +46 -4
- package/test/unit/spec/meeting-info/utilv2.js +17 -0
- package/test/unit/spec/meetings/index.js +150 -13
- package/test/unit/spec/meetings/utils.js +10 -0
- package/test/unit/spec/member/util.js +52 -11
- package/test/unit/spec/multistream/remoteMedia.ts +11 -7
- package/test/unit/spec/roap/index.ts +47 -0
- package/test/unit/spec/webinar/index.ts +457 -0
- package/dist/common/errors/webinar-registration-error.js.map +0 -1
- package/src/common/errors/webinar-registration-error.ts +0 -27
|
@@ -91,14 +91,15 @@ import ParameterError from '../../../../src/common/errors/parameter';
|
|
|
91
91
|
import PasswordError from '../../../../src/common/errors/password-error';
|
|
92
92
|
import CaptchaError from '../../../../src/common/errors/captcha-error';
|
|
93
93
|
import PermissionError from '../../../../src/common/errors/permission';
|
|
94
|
-
import
|
|
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
97
|
import testUtils from '../../../utils/testUtils';
|
|
97
98
|
import {
|
|
98
99
|
MeetingInfoV2CaptchaError,
|
|
99
100
|
MeetingInfoV2PasswordError,
|
|
100
101
|
MeetingInfoV2PolicyError,
|
|
101
|
-
|
|
102
|
+
MeetingInfoV2JoinWebinarError, MeetingInfoV2JoinForbiddenError,
|
|
102
103
|
} from '../../../../src/meeting-info/meeting-info-v2';
|
|
103
104
|
import {
|
|
104
105
|
DTLS_HANDSHAKE_FAILED_CLIENT_CODE,
|
|
@@ -113,6 +114,7 @@ import {ERROR_DESCRIPTIONS} from '@webex/internal-plugin-metrics/src/call-diagno
|
|
|
113
114
|
import MeetingCollection from '@webex/plugin-meetings/src/meetings/collection';
|
|
114
115
|
|
|
115
116
|
import {EVENT_TRIGGERS as VOICEAEVENTS} from '@webex/internal-plugin-voicea';
|
|
117
|
+
import JoinForbiddenError from '../../../../src/common/errors/join-forbidden-error';
|
|
116
118
|
|
|
117
119
|
describe('plugin-meetings', () => {
|
|
118
120
|
const logger = {
|
|
@@ -652,7 +654,7 @@ describe('plugin-meetings', () => {
|
|
|
652
654
|
const fakeTurnServerInfo = {id: 'fake turn info'};
|
|
653
655
|
const fakeJoinResult = {id: 'join result'};
|
|
654
656
|
|
|
655
|
-
const joinOptions = {correlationId: '12345'};
|
|
657
|
+
const joinOptions = {correlationId: '12345', enableMultistream: true};
|
|
656
658
|
const mediaOptions = {audioEnabled: true, allowMediaInLobby: true};
|
|
657
659
|
|
|
658
660
|
let generateTurnDiscoveryRequestMessageStub;
|
|
@@ -661,7 +663,10 @@ describe('plugin-meetings', () => {
|
|
|
661
663
|
let addMediaInternalStub;
|
|
662
664
|
|
|
663
665
|
beforeEach(() => {
|
|
664
|
-
meeting.join = sinon.stub().
|
|
666
|
+
meeting.join = sinon.stub().callsFake((joinOptions) => {
|
|
667
|
+
meeting.isMultistream = joinOptions.enableMultistream;
|
|
668
|
+
return Promise.resolve(fakeJoinResult)
|
|
669
|
+
});
|
|
665
670
|
addMediaInternalStub = sinon
|
|
666
671
|
.stub(meeting, 'addMediaInternal')
|
|
667
672
|
.returns(Promise.resolve(test4));
|
|
@@ -700,7 +705,7 @@ describe('plugin-meetings', () => {
|
|
|
700
705
|
mediaOptions
|
|
701
706
|
);
|
|
702
707
|
|
|
703
|
-
assert.deepEqual(result, {join: fakeJoinResult, media: test4});
|
|
708
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
|
|
704
709
|
|
|
705
710
|
// resets joinWithMediaRetryInfo
|
|
706
711
|
assert.deepEqual(meeting.joinWithMediaRetryInfo, {
|
|
@@ -733,7 +738,7 @@ describe('plugin-meetings', () => {
|
|
|
733
738
|
mediaOptions
|
|
734
739
|
);
|
|
735
740
|
|
|
736
|
-
assert.deepEqual(result, {join: fakeJoinResult, media: test4});
|
|
741
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
|
|
737
742
|
assert.equal(meeting.turnServerUsed, false);
|
|
738
743
|
});
|
|
739
744
|
|
|
@@ -768,7 +773,7 @@ describe('plugin-meetings', () => {
|
|
|
768
773
|
mediaOptions
|
|
769
774
|
);
|
|
770
775
|
|
|
771
|
-
assert.deepEqual(result, {join: fakeJoinResult, media: test4});
|
|
776
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
|
|
772
777
|
});
|
|
773
778
|
|
|
774
779
|
it('should reject if join() fails', async () => {
|
|
@@ -855,7 +860,8 @@ describe('plugin-meetings', () => {
|
|
|
855
860
|
}
|
|
856
861
|
);
|
|
857
862
|
|
|
858
|
-
|
|
863
|
+
// expect multistreamEnabled: false, because this test overrides the join meeting.join stub so it doesn't set the isMultistream flag
|
|
864
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: false});
|
|
859
865
|
|
|
860
866
|
// resets joinWithMediaRetryInfo
|
|
861
867
|
assert.deepEqual(meeting.joinWithMediaRetryInfo, {
|
|
@@ -944,7 +950,7 @@ describe('plugin-meetings', () => {
|
|
|
944
950
|
mediaOptions,
|
|
945
951
|
});
|
|
946
952
|
|
|
947
|
-
assert.deepEqual(result, {join: fakeJoinResult, media: test4});
|
|
953
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
|
|
948
954
|
|
|
949
955
|
assert.calledOnce(meeting.join);
|
|
950
956
|
assert.notCalled(leaveStub);
|
|
@@ -1038,6 +1044,7 @@ describe('plugin-meetings', () => {
|
|
|
1038
1044
|
getConnectionState: sinon.stub().returns(ConnectionState.Connected),
|
|
1039
1045
|
initiateOffer: sinon.stub().resolves({}),
|
|
1040
1046
|
on: sinon.stub(),
|
|
1047
|
+
createSendSlot: sinon.stub(),
|
|
1041
1048
|
};
|
|
1042
1049
|
|
|
1043
1050
|
/* Setup the stubs so that the first call to addMediaInternal() fails
|
|
@@ -1054,12 +1061,14 @@ describe('plugin-meetings', () => {
|
|
|
1054
1061
|
|
|
1055
1062
|
sinon.stub(meeting.roap, 'doTurnDiscovery').resolves({turnServerInfo: 'fake turn info'});
|
|
1056
1063
|
|
|
1064
|
+
// calling joinWithMedia() with enableMultistream=false, because this test uses real addMediaInternal() implementation
|
|
1065
|
+
// and it requires less stubs when it's without multistream
|
|
1057
1066
|
const result = await meeting.joinWithMedia({
|
|
1058
|
-
joinOptions,
|
|
1067
|
+
joinOptions: {...joinOptions, enableMultistream: false},
|
|
1059
1068
|
mediaOptions,
|
|
1060
1069
|
});
|
|
1061
1070
|
|
|
1062
|
-
assert.deepEqual(result, {join: fakeJoinResult, media: undefined});
|
|
1071
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: undefined, multistreamEnabled: false});
|
|
1063
1072
|
|
|
1064
1073
|
assert.calledOnce(meeting.join);
|
|
1065
1074
|
|
|
@@ -1134,6 +1143,7 @@ describe('plugin-meetings', () => {
|
|
|
1134
1143
|
addMediaError.name = 'SdpOfferCreationError';
|
|
1135
1144
|
|
|
1136
1145
|
meeting.addMediaInternal.rejects(addMediaError);
|
|
1146
|
+
sinon.stub(meeting, 'leave').resolves();
|
|
1137
1147
|
|
|
1138
1148
|
await assert.isRejected(
|
|
1139
1149
|
meeting.joinWithMedia({
|
|
@@ -1162,6 +1172,7 @@ describe('plugin-meetings', () => {
|
|
|
1162
1172
|
type: addMediaError.name,
|
|
1163
1173
|
}
|
|
1164
1174
|
);
|
|
1175
|
+
assert.calledOnceWithExactly(meeting.leave, {resourceId: undefined, reason: 'joinWithMedia failure'})
|
|
1165
1176
|
});
|
|
1166
1177
|
});
|
|
1167
1178
|
|
|
@@ -1238,6 +1249,7 @@ describe('plugin-meetings', () => {
|
|
|
1238
1249
|
webex.internal.voicea.off = sinon.stub();
|
|
1239
1250
|
webex.internal.voicea.listenToEvents = sinon.stub();
|
|
1240
1251
|
webex.internal.voicea.turnOnCaptions = sinon.stub();
|
|
1252
|
+
webex.internal.voicea.deregisterEvents = sinon.stub();
|
|
1241
1253
|
});
|
|
1242
1254
|
|
|
1243
1255
|
it('should stop listening to voicea events and also trigger a stop event', () => {
|
|
@@ -1566,6 +1578,55 @@ describe('plugin-meetings', () => {
|
|
|
1566
1578
|
fakeProcessedReaction
|
|
1567
1579
|
);
|
|
1568
1580
|
});
|
|
1581
|
+
|
|
1582
|
+
it('should fail quietly if participantId does not exist in membersCollection', () => {
|
|
1583
|
+
LoggerProxy.logger.warn = sinon.stub();
|
|
1584
|
+
meeting.isReactionsSupported = sinon.stub().returns(true);
|
|
1585
|
+
meeting.config.receiveReactions = true;
|
|
1586
|
+
const fakeSendersName = 'Fake reactors name';
|
|
1587
|
+
const fakeReactionPayload = {
|
|
1588
|
+
type: 'fake_type',
|
|
1589
|
+
codepoints: 'fake_codepoints',
|
|
1590
|
+
shortcodes: 'fake_shortcodes',
|
|
1591
|
+
tone: {
|
|
1592
|
+
type: 'fake_tone_type',
|
|
1593
|
+
codepoints: 'fake_tone_codepoints',
|
|
1594
|
+
shortcodes: 'fake_tone_shortcodes',
|
|
1595
|
+
},
|
|
1596
|
+
};
|
|
1597
|
+
const fakeSenderPayload = {
|
|
1598
|
+
participantId: 'fake_participant_id',
|
|
1599
|
+
};
|
|
1600
|
+
const fakeProcessedReaction = {
|
|
1601
|
+
reaction: fakeReactionPayload,
|
|
1602
|
+
sender: {
|
|
1603
|
+
id: fakeSenderPayload.participantId,
|
|
1604
|
+
name: fakeSendersName,
|
|
1605
|
+
},
|
|
1606
|
+
};
|
|
1607
|
+
const fakeRelayEvent = {
|
|
1608
|
+
data: {
|
|
1609
|
+
relayType: REACTION_RELAY_TYPES.REACTION,
|
|
1610
|
+
reaction: fakeReactionPayload,
|
|
1611
|
+
sender: fakeSenderPayload,
|
|
1612
|
+
},
|
|
1613
|
+
};
|
|
1614
|
+
meeting.processRelayEvent(fakeRelayEvent);
|
|
1615
|
+
assert.calledWith(
|
|
1616
|
+
LoggerProxy.logger.warn,
|
|
1617
|
+
`Meeting:index#processRelayEvent --> Skipping handling of react for ${meeting.id}. participantId fake_participant_id does not exist in membersCollection.`
|
|
1618
|
+
);
|
|
1619
|
+
assert.neverCalledWith(
|
|
1620
|
+
TriggerProxy.trigger,
|
|
1621
|
+
sinon.match.instanceOf(Meeting),
|
|
1622
|
+
{
|
|
1623
|
+
file: 'meeting/index',
|
|
1624
|
+
function: 'join',
|
|
1625
|
+
},
|
|
1626
|
+
EVENT_TRIGGERS.MEETING_RECEIVE_REACTIONS,
|
|
1627
|
+
fakeProcessedReaction
|
|
1628
|
+
);
|
|
1629
|
+
});
|
|
1569
1630
|
});
|
|
1570
1631
|
|
|
1571
1632
|
describe('#handleLLMOnline', () => {
|
|
@@ -1705,6 +1766,12 @@ describe('plugin-meetings', () => {
|
|
|
1705
1766
|
sinon.assert.called(setCorrelationIdSpy);
|
|
1706
1767
|
assert.equal(meeting.correlationId, '123');
|
|
1707
1768
|
});
|
|
1769
|
+
|
|
1770
|
+
it('should not send client.call.initiated if told not to', async () => {
|
|
1771
|
+
await meeting.join({sendCallInitiated: false});
|
|
1772
|
+
|
|
1773
|
+
sinon.assert.notCalled(webex.internal.newMetrics.submitClientEvent);
|
|
1774
|
+
});
|
|
1708
1775
|
});
|
|
1709
1776
|
|
|
1710
1777
|
describe('failure', () => {
|
|
@@ -2492,9 +2559,11 @@ describe('plugin-meetings', () => {
|
|
|
2492
2559
|
mediaSettings: {},
|
|
2493
2560
|
});
|
|
2494
2561
|
|
|
2495
|
-
const checkLogCounter = (
|
|
2562
|
+
const checkLogCounter = (delayInMinutes, expectedCounter) => {
|
|
2563
|
+
const delayInMilliseconds = delayInMinutes * 60 * 1000;
|
|
2564
|
+
|
|
2496
2565
|
// first check that the counter is not increased just before the delay
|
|
2497
|
-
clock.tick(
|
|
2566
|
+
clock.tick(delayInMilliseconds - 50);
|
|
2498
2567
|
assert.equal(logUploadCounter, expectedCounter - 1);
|
|
2499
2568
|
|
|
2500
2569
|
// and now check that it has reached expected value after the delay
|
|
@@ -2502,22 +2571,18 @@ describe('plugin-meetings', () => {
|
|
|
2502
2571
|
assert.equal(logUploadCounter, expectedCounter);
|
|
2503
2572
|
};
|
|
2504
2573
|
|
|
2505
|
-
checkLogCounter(
|
|
2506
|
-
checkLogCounter(
|
|
2507
|
-
checkLogCounter(
|
|
2508
|
-
checkLogCounter(
|
|
2509
|
-
checkLogCounter(
|
|
2510
|
-
checkLogCounter(30000, 6);
|
|
2511
|
-
checkLogCounter(30000, 7);
|
|
2512
|
-
checkLogCounter(60000, 8);
|
|
2513
|
-
checkLogCounter(60000, 9);
|
|
2514
|
-
checkLogCounter(60000, 10);
|
|
2574
|
+
checkLogCounter(0.1, 1);
|
|
2575
|
+
checkLogCounter(15, 2);
|
|
2576
|
+
checkLogCounter(30, 3);
|
|
2577
|
+
checkLogCounter(60, 4);
|
|
2578
|
+
checkLogCounter(60, 5);
|
|
2515
2579
|
|
|
2516
|
-
// simulate media connection being removed ->
|
|
2580
|
+
// simulate media connection being removed -> 1 more upload should happen, but nothing more afterwards
|
|
2517
2581
|
meeting.mediaProperties.webrtcMediaConnection = undefined;
|
|
2582
|
+
checkLogCounter(60, 6);
|
|
2518
2583
|
|
|
2519
|
-
clock.tick(
|
|
2520
|
-
assert.equal(logUploadCounter,
|
|
2584
|
+
clock.tick(120 * 1000 * 60);
|
|
2585
|
+
assert.equal(logUploadCounter, 6);
|
|
2521
2586
|
|
|
2522
2587
|
clock.restore();
|
|
2523
2588
|
});
|
|
@@ -3475,6 +3540,51 @@ describe('plugin-meetings', () => {
|
|
|
3475
3540
|
});
|
|
3476
3541
|
});
|
|
3477
3542
|
|
|
3543
|
+
it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
|
|
3544
|
+
let fakeMembersCollection = {
|
|
3545
|
+
members: {
|
|
3546
|
+
member1: { isInMeeting: true },
|
|
3547
|
+
member2: { isInMeeting: true },
|
|
3548
|
+
member3: { isInMeeting: false },
|
|
3549
|
+
},
|
|
3550
|
+
};
|
|
3551
|
+
sinon.stub(meeting, 'getMembers').returns({ membersCollection: fakeMembersCollection });
|
|
3552
|
+
const fakeData = { intervalMetadata: {}, networkType: 'wifi' };
|
|
3553
|
+
|
|
3554
|
+
statsAnalyzerStub.emit(
|
|
3555
|
+
{ file: 'test', function: 'test' },
|
|
3556
|
+
StatsAnalyzerEventNames.MEDIA_QUALITY,
|
|
3557
|
+
{ data: fakeData }
|
|
3558
|
+
);
|
|
3559
|
+
|
|
3560
|
+
assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
|
|
3561
|
+
name: 'client.mediaquality.event',
|
|
3562
|
+
options: {
|
|
3563
|
+
meetingId: meeting.id,
|
|
3564
|
+
},
|
|
3565
|
+
payload: {
|
|
3566
|
+
intervals: [sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2))],
|
|
3567
|
+
},
|
|
3568
|
+
});
|
|
3569
|
+
fakeMembersCollection.members.member2.isInMeeting = false;
|
|
3570
|
+
|
|
3571
|
+
statsAnalyzerStub.emit(
|
|
3572
|
+
{ file: 'test', function: 'test' },
|
|
3573
|
+
StatsAnalyzerEventNames.MEDIA_QUALITY,
|
|
3574
|
+
{ data: fakeData }
|
|
3575
|
+
);
|
|
3576
|
+
|
|
3577
|
+
assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
|
|
3578
|
+
name: 'client.mediaquality.event',
|
|
3579
|
+
options: {
|
|
3580
|
+
meetingId: meeting.id,
|
|
3581
|
+
},
|
|
3582
|
+
payload: {
|
|
3583
|
+
intervals: [sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1))],
|
|
3584
|
+
},
|
|
3585
|
+
});
|
|
3586
|
+
});
|
|
3587
|
+
|
|
3478
3588
|
it('calls submitMQE correctly', async () => {
|
|
3479
3589
|
const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'};
|
|
3480
3590
|
|
|
@@ -3552,14 +3662,6 @@ describe('plugin-meetings', () => {
|
|
|
3552
3662
|
});
|
|
3553
3663
|
});
|
|
3554
3664
|
|
|
3555
|
-
it('succeeds even if getDevices() throws', async () => {
|
|
3556
|
-
meeting.meetingState = 'ACTIVE';
|
|
3557
|
-
|
|
3558
|
-
sinon.stub(InternalMediaCoreModule, 'getDevices').rejects(new Error('fake error'));
|
|
3559
|
-
|
|
3560
|
-
await meeting.addMedia();
|
|
3561
|
-
});
|
|
3562
|
-
|
|
3563
3665
|
describe('CA ice failures checks', () => {
|
|
3564
3666
|
[
|
|
3565
3667
|
{
|
|
@@ -3701,6 +3803,93 @@ describe('plugin-meetings', () => {
|
|
|
3701
3803
|
});
|
|
3702
3804
|
});
|
|
3703
3805
|
|
|
3806
|
+
describe(`#beRightBack`, () => {
|
|
3807
|
+
const fakeMultistreamRoapMediaConnection = {
|
|
3808
|
+
createSendSlot: sinon.stub().returns({
|
|
3809
|
+
setSourceStateOverride: sinon.stub().resolves(),
|
|
3810
|
+
clearSourceStateOverride: sinon.stub().resolves(),
|
|
3811
|
+
}),
|
|
3812
|
+
};
|
|
3813
|
+
|
|
3814
|
+
beforeEach(() => {
|
|
3815
|
+
meeting.meetingRequest.setBrb = sinon.stub().resolves({body: 'test'});
|
|
3816
|
+
meeting.mediaProperties.webrtcMediaConnection = {createSendSlot: sinon.stub()};
|
|
3817
|
+
meeting.sendSlotManager.createSlot(
|
|
3818
|
+
fakeMultistreamRoapMediaConnection,
|
|
3819
|
+
MediaType.VideoMain
|
|
3820
|
+
);
|
|
3821
|
+
|
|
3822
|
+
meeting.locusUrl = 'locus url';
|
|
3823
|
+
meeting.deviceUrl = 'device url';
|
|
3824
|
+
meeting.selfId = 'self id';
|
|
3825
|
+
});
|
|
3826
|
+
|
|
3827
|
+
afterEach(() => {
|
|
3828
|
+
sinon.restore();
|
|
3829
|
+
});
|
|
3830
|
+
|
|
3831
|
+
it('should have #beRightBack', () => {
|
|
3832
|
+
assert.exists(meeting.beRightBack);
|
|
3833
|
+
});
|
|
3834
|
+
|
|
3835
|
+
describe('when in a multistream meeting', () => {
|
|
3836
|
+
|
|
3837
|
+
beforeEach(() => {
|
|
3838
|
+
meeting.isMultistream = true;
|
|
3839
|
+
});
|
|
3840
|
+
|
|
3841
|
+
it('should enable #beRightBack and return a promise', async () => {
|
|
3842
|
+
const brbResult = meeting.beRightBack(true);
|
|
3843
|
+
|
|
3844
|
+
await brbResult;
|
|
3845
|
+
assert.exists(brbResult.then);
|
|
3846
|
+
assert.calledOnce(meeting.meetingRequest.setBrb);
|
|
3847
|
+
})
|
|
3848
|
+
|
|
3849
|
+
it('should disable #beRightBack and return a promise', async () => {
|
|
3850
|
+
const brbResult = meeting.beRightBack(false);
|
|
3851
|
+
|
|
3852
|
+
await brbResult;
|
|
3853
|
+
assert.exists(brbResult.then);
|
|
3854
|
+
assert.calledOnce(meeting.meetingRequest.setBrb);
|
|
3855
|
+
})
|
|
3856
|
+
|
|
3857
|
+
it('should throw an error and reject the promise if setBrb fails', async () => {
|
|
3858
|
+
const error = new Error('setBrb failed');
|
|
3859
|
+
meeting.meetingRequest.setBrb.rejects(error);
|
|
3860
|
+
|
|
3861
|
+
try {
|
|
3862
|
+
await meeting.beRightBack(true);
|
|
3863
|
+
} catch (err) {
|
|
3864
|
+
assert.instanceOf(err, Error);
|
|
3865
|
+
assert.equal(err.message, 'setBrb failed');
|
|
3866
|
+
assert.isRejected((Promise.reject()));
|
|
3867
|
+
}
|
|
3868
|
+
})
|
|
3869
|
+
});
|
|
3870
|
+
|
|
3871
|
+
describe('when in a transcoded meeting', () => {
|
|
3872
|
+
|
|
3873
|
+
beforeEach(() => {
|
|
3874
|
+
meeting.isMultistream = false;
|
|
3875
|
+
});
|
|
3876
|
+
|
|
3877
|
+
it('should ignore enabling #beRightBack', async () => {
|
|
3878
|
+
meeting.beRightBack(true);
|
|
3879
|
+
|
|
3880
|
+
assert.isRejected((Promise.reject()));
|
|
3881
|
+
assert.notCalled(meeting.meetingRequest.setBrb);
|
|
3882
|
+
})
|
|
3883
|
+
|
|
3884
|
+
it('should ignore disabling #beRightBack', async () => {
|
|
3885
|
+
meeting.beRightBack(false);
|
|
3886
|
+
|
|
3887
|
+
assert.isRejected((Promise.reject()));
|
|
3888
|
+
assert.notCalled(meeting.meetingRequest.setBrb);
|
|
3889
|
+
})
|
|
3890
|
+
});
|
|
3891
|
+
});
|
|
3892
|
+
|
|
3704
3893
|
/* This set of tests are like semi-integration tests, they use real MuteState, Media, LocusMediaRequest and Roap classes.
|
|
3705
3894
|
They mock the @webex/internal-media-core and sending of /media http requests to Locus.
|
|
3706
3895
|
Their main purpose is to test that we send the right http requests to Locus and make right calls
|
|
@@ -3743,8 +3932,12 @@ describe('plugin-meetings', () => {
|
|
|
3743
3932
|
meeting.setMercuryListener = sinon.stub();
|
|
3744
3933
|
meeting.locusInfo.onFullLocus = sinon.stub();
|
|
3745
3934
|
meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
|
|
3746
|
-
meeting.webex.meetings.reachability.getReachabilityReportToAttachToRoap = sinon
|
|
3747
|
-
|
|
3935
|
+
meeting.webex.meetings.reachability.getReachabilityReportToAttachToRoap = sinon
|
|
3936
|
+
.stub()
|
|
3937
|
+
.resolves({id: 'fake reachability'});
|
|
3938
|
+
meeting.webex.meetings.reachability.getClientMediaPreferences = sinon
|
|
3939
|
+
.stub()
|
|
3940
|
+
.resolves({id: 'fake clientMediaPreferences'});
|
|
3748
3941
|
meeting.roap.doTurnDiscovery = sinon.stub().resolves({
|
|
3749
3942
|
turnServerInfo: {
|
|
3750
3943
|
url: 'turns:turn-server-url:443?transport=tcp',
|
|
@@ -3825,6 +4018,7 @@ describe('plugin-meetings', () => {
|
|
|
3825
4018
|
initiateOffer: sinon.stub().resolves({}),
|
|
3826
4019
|
update: sinon.stub().resolves({}),
|
|
3827
4020
|
on: sinon.stub(),
|
|
4021
|
+
roapMessageReceived: sinon.stub()
|
|
3828
4022
|
};
|
|
3829
4023
|
|
|
3830
4024
|
fakeMultistreamRoapMediaConnection = {
|
|
@@ -3911,8 +4105,10 @@ describe('plugin-meetings', () => {
|
|
|
3911
4105
|
};
|
|
3912
4106
|
|
|
3913
4107
|
// simulates a Roap offer being generated by the RoapMediaConnection
|
|
3914
|
-
const simulateRoapOffer = async () => {
|
|
3915
|
-
|
|
4108
|
+
const simulateRoapOffer = async (stubWaitingForAnswer = true) => {
|
|
4109
|
+
if (stubWaitingForAnswer) {
|
|
4110
|
+
meeting.deferSDPAnswer = {resolve: sinon.stub()};
|
|
4111
|
+
}
|
|
3916
4112
|
const roapListener = getRoapListener();
|
|
3917
4113
|
|
|
3918
4114
|
await roapListener({roapMessage: roapOfferMessage});
|
|
@@ -3930,8 +4126,14 @@ describe('plugin-meetings', () => {
|
|
|
3930
4126
|
const checkSdpOfferSent = ({audioMuted, videoMuted}) => {
|
|
3931
4127
|
const {sdp, seq, tieBreaker} = roapOfferMessage;
|
|
3932
4128
|
|
|
3933
|
-
assert.calledWith(
|
|
3934
|
-
|
|
4129
|
+
assert.calledWith(
|
|
4130
|
+
meeting.webex.meetings.reachability.getClientMediaPreferences,
|
|
4131
|
+
meeting.isMultistream,
|
|
4132
|
+
0
|
|
4133
|
+
);
|
|
4134
|
+
assert.calledWith(
|
|
4135
|
+
meeting.webex.meetings.reachability.getReachabilityReportToAttachToRoap
|
|
4136
|
+
);
|
|
3935
4137
|
|
|
3936
4138
|
assert.calledWith(locusMediaRequestStub, {
|
|
3937
4139
|
method: 'PUT',
|
|
@@ -4015,8 +4217,9 @@ describe('plugin-meetings', () => {
|
|
|
4015
4217
|
remoteQualityLevel,
|
|
4016
4218
|
expectedDebugId,
|
|
4017
4219
|
meetingId,
|
|
4220
|
+
expectMultistream = isMultistream,
|
|
4018
4221
|
}) => {
|
|
4019
|
-
if (
|
|
4222
|
+
if (expectMultistream) {
|
|
4020
4223
|
const {iceServers} = mediaConnectionConfig;
|
|
4021
4224
|
|
|
4022
4225
|
assert.calledOnceWithMatch(
|
|
@@ -4176,7 +4379,6 @@ describe('plugin-meetings', () => {
|
|
|
4176
4379
|
});
|
|
4177
4380
|
|
|
4178
4381
|
it('addMedia() works correctly when media is enabled with streams to publish', async () => {
|
|
4179
|
-
const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
|
|
4180
4382
|
await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
|
|
4181
4383
|
await simulateRoapOffer();
|
|
4182
4384
|
await simulateRoapOk();
|
|
@@ -4207,12 +4409,9 @@ describe('plugin-meetings', () => {
|
|
|
4207
4409
|
|
|
4208
4410
|
// and that these were the only /media requests that were sent
|
|
4209
4411
|
assert.calledTwice(locusMediaRequestStub);
|
|
4210
|
-
|
|
4211
|
-
assert.calledOnce(handleDeviceLoggingSpy);
|
|
4212
4412
|
});
|
|
4213
4413
|
|
|
4214
4414
|
it('addMedia() works correctly when media is enabled with streams to publish and stream is user muted', async () => {
|
|
4215
|
-
const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
|
|
4216
4415
|
fakeMicrophoneStream.userMuted = true;
|
|
4217
4416
|
|
|
4218
4417
|
await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
|
|
@@ -4244,7 +4443,6 @@ describe('plugin-meetings', () => {
|
|
|
4244
4443
|
|
|
4245
4444
|
// and that these were the only /media requests that were sent
|
|
4246
4445
|
assert.calledTwice(locusMediaRequestStub);
|
|
4247
|
-
assert.calledOnce(handleDeviceLoggingSpy);
|
|
4248
4446
|
});
|
|
4249
4447
|
|
|
4250
4448
|
it('addMedia() works correctly when media is enabled with tracks to publish and track is ended', async () => {
|
|
@@ -4316,7 +4514,6 @@ describe('plugin-meetings', () => {
|
|
|
4316
4514
|
});
|
|
4317
4515
|
|
|
4318
4516
|
it('addMedia() works correctly when media is disabled with streams to publish', async () => {
|
|
4319
|
-
const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
|
|
4320
4517
|
await meeting.addMedia({
|
|
4321
4518
|
localStreams: {microphone: fakeMicrophoneStream},
|
|
4322
4519
|
audioEnabled: false,
|
|
@@ -4350,20 +4547,6 @@ describe('plugin-meetings', () => {
|
|
|
4350
4547
|
|
|
4351
4548
|
// and that these were the only /media requests that were sent
|
|
4352
4549
|
assert.calledTwice(locusMediaRequestStub);
|
|
4353
|
-
assert.calledOnce(handleDeviceLoggingSpy);
|
|
4354
|
-
});
|
|
4355
|
-
|
|
4356
|
-
it('handleDeviceLogging not called when media is disabled', async () => {
|
|
4357
|
-
const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
|
|
4358
|
-
await meeting.addMedia({
|
|
4359
|
-
localStreams: {microphone: fakeMicrophoneStream},
|
|
4360
|
-
audioEnabled: false,
|
|
4361
|
-
videoEnabled: false,
|
|
4362
|
-
});
|
|
4363
|
-
await simulateRoapOffer();
|
|
4364
|
-
await simulateRoapOk();
|
|
4365
|
-
|
|
4366
|
-
assert.notCalled(handleDeviceLoggingSpy);
|
|
4367
4550
|
});
|
|
4368
4551
|
|
|
4369
4552
|
it('addMedia() works correctly when media is disabled with no streams to publish', async () => {
|
|
@@ -4399,20 +4582,6 @@ describe('plugin-meetings', () => {
|
|
|
4399
4582
|
assert.calledTwice(locusMediaRequestStub);
|
|
4400
4583
|
});
|
|
4401
4584
|
|
|
4402
|
-
it('addMedia() works correctly when media is disabled with no streams to publish', async () => {
|
|
4403
|
-
const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
|
|
4404
|
-
await meeting.addMedia({audioEnabled: false});
|
|
4405
|
-
//calling handleDeviceLogging with audioEnaled as true adn videoEnabled as false
|
|
4406
|
-
assert.calledWith(handleDeviceLoggingSpy, false, true);
|
|
4407
|
-
});
|
|
4408
|
-
|
|
4409
|
-
it('addMedia() works correctly when video is disabled with no streams to publish', async () => {
|
|
4410
|
-
const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
|
|
4411
|
-
await meeting.addMedia({videoEnabled: false});
|
|
4412
|
-
//calling handleDeviceLogging audioEnabled as true videoEnabled as false
|
|
4413
|
-
assert.calledWith(handleDeviceLoggingSpy, true, false);
|
|
4414
|
-
});
|
|
4415
|
-
|
|
4416
4585
|
it('addMedia() works correctly when video is disabled with no streams to publish', async () => {
|
|
4417
4586
|
await meeting.addMedia({videoEnabled: false});
|
|
4418
4587
|
await simulateRoapOffer();
|
|
@@ -4479,13 +4648,6 @@ describe('plugin-meetings', () => {
|
|
|
4479
4648
|
assert.calledTwice(locusMediaRequestStub);
|
|
4480
4649
|
});
|
|
4481
4650
|
|
|
4482
|
-
it('addMedia() works correctly when both shareAudio and shareVideo is disabled with no streams publish', async () => {
|
|
4483
|
-
const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
|
|
4484
|
-
await meeting.addMedia({shareAudioEnabled: false, shareVideoEnabled: false});
|
|
4485
|
-
//calling handleDeviceLogging with audioEnabled true and videoEnabled as true
|
|
4486
|
-
assert.calledWith(handleDeviceLoggingSpy, true, true);
|
|
4487
|
-
});
|
|
4488
|
-
|
|
4489
4651
|
describe('publishStreams()/unpublishStreams() calls', () => {
|
|
4490
4652
|
[
|
|
4491
4653
|
{mediaEnabled: true, expected: {direction: 'sendrecv', localMuteSentValue: false}},
|
|
@@ -4881,6 +5043,211 @@ describe('plugin-meetings', () => {
|
|
|
4881
5043
|
assert.notCalled(fakeRoapMediaConnection.update);
|
|
4882
5044
|
})
|
|
4883
5045
|
);
|
|
5046
|
+
|
|
5047
|
+
if (isMultistream) {
|
|
5048
|
+
describe('fallback from multistream to transcoded', () => {
|
|
5049
|
+
let multistreamEventListeners;
|
|
5050
|
+
let transcodedEventListeners;
|
|
5051
|
+
let mockStatsAnalyzerCtor;
|
|
5052
|
+
|
|
5053
|
+
const setupFakeRoapMediaConnection = (fakeRoapMediaConnection, eventListeners) => {
|
|
5054
|
+
fakeRoapMediaConnection.on.callsFake((eventName, cb) => {
|
|
5055
|
+
eventListeners[eventName] = cb;
|
|
5056
|
+
});
|
|
5057
|
+
fakeRoapMediaConnection.initiateOffer.callsFake(() => {
|
|
5058
|
+
// simulate offer being generated
|
|
5059
|
+
eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED]();
|
|
5060
|
+
|
|
5061
|
+
return Promise.resolve();
|
|
5062
|
+
});
|
|
5063
|
+
};
|
|
5064
|
+
|
|
5065
|
+
beforeEach(() => {
|
|
5066
|
+
multistreamEventListeners = {};
|
|
5067
|
+
transcodedEventListeners = {};
|
|
5068
|
+
|
|
5069
|
+
meeting.config.stats.enableStatsAnalyzer = true;
|
|
5070
|
+
|
|
5071
|
+
setupFakeRoapMediaConnection(fakeRoapMediaConnection, transcodedEventListeners);
|
|
5072
|
+
setupFakeRoapMediaConnection(
|
|
5073
|
+
fakeMultistreamRoapMediaConnection,
|
|
5074
|
+
multistreamEventListeners
|
|
5075
|
+
);
|
|
5076
|
+
|
|
5077
|
+
mockStatsAnalyzerCtor = sinon
|
|
5078
|
+
.stub(InternalMediaCoreModule, 'StatsAnalyzer')
|
|
5079
|
+
.callsFake(() => {
|
|
5080
|
+
return {on: sinon.stub(), stopAnalyzer: sinon.stub()};
|
|
5081
|
+
});
|
|
5082
|
+
|
|
5083
|
+
webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
|
|
5084
|
+
sinon.stub();
|
|
5085
|
+
|
|
5086
|
+
// setup the mock so that we get an SDP answer not from Homer
|
|
5087
|
+
locusMediaRequestStub.callsFake(() => {
|
|
5088
|
+
return Promise.resolve({
|
|
5089
|
+
body: {
|
|
5090
|
+
locus: {},
|
|
5091
|
+
mediaConnections: [
|
|
5092
|
+
{
|
|
5093
|
+
remoteSdp:
|
|
5094
|
+
'{"audioMuted":false,"videoMuted":true,"roapMessage":{"messageType":"ANSWER","version":"2","seq":1,"sdps":["v=0\\r\\no=linus 0 1 IN IP4 23.89.67.4\\r\\ns=-\\r\\nc=IN IP4 23.89.67.4\\r\\n"],"headers":["noOkInTransaction"]},"type":"SDP"}',
|
|
5095
|
+
},
|
|
5096
|
+
],
|
|
5097
|
+
},
|
|
5098
|
+
});
|
|
5099
|
+
});
|
|
5100
|
+
|
|
5101
|
+
sinon.stub(meeting, 'closePeerConnections');
|
|
5102
|
+
sinon.stub(meeting.mediaProperties, 'unsetPeerConnection');
|
|
5103
|
+
sinon.stub(meeting.locusMediaRequest, 'downgradeFromMultistreamToTranscoded');
|
|
5104
|
+
});
|
|
5105
|
+
|
|
5106
|
+
const runCheck = async (turnServerInfo, forceTurnDiscovery) => {
|
|
5107
|
+
// we're calling addMediaInternal() with mic stream,
|
|
5108
|
+
// so that we also verify that audioMute, videoMute info is correctly sent to backend
|
|
5109
|
+
const addMediaPromise = meeting.addMediaInternal(
|
|
5110
|
+
() => '',
|
|
5111
|
+
turnServerInfo,
|
|
5112
|
+
forceTurnDiscovery,
|
|
5113
|
+
{
|
|
5114
|
+
localStreams: {microphone: fakeMicrophoneStream},
|
|
5115
|
+
}
|
|
5116
|
+
);
|
|
5117
|
+
await testUtils.flushPromises();
|
|
5118
|
+
await simulateRoapOffer(false);
|
|
5119
|
+
|
|
5120
|
+
// check MultistreamRoapMediaConnection was created correctly
|
|
5121
|
+
checkMediaConnectionCreated({
|
|
5122
|
+
expectMultistream: true,
|
|
5123
|
+
mediaConnectionConfig: expectedMediaConnectionConfig,
|
|
5124
|
+
localStreams: {
|
|
5125
|
+
audio: fakeMicrophoneStream,
|
|
5126
|
+
video: undefined,
|
|
5127
|
+
screenShareVideo: undefined,
|
|
5128
|
+
screenShareAudio: undefined,
|
|
5129
|
+
},
|
|
5130
|
+
direction: {
|
|
5131
|
+
audio: 'sendrecv',
|
|
5132
|
+
video: 'sendrecv',
|
|
5133
|
+
screenShare: 'recvonly',
|
|
5134
|
+
},
|
|
5135
|
+
remoteQualityLevel: 'HIGH',
|
|
5136
|
+
expectedDebugId,
|
|
5137
|
+
meetingId: meeting.id,
|
|
5138
|
+
});
|
|
5139
|
+
|
|
5140
|
+
// check that stats analyzer was created with the right config and store the reference to it so that we can later check that it was stopped
|
|
5141
|
+
assert.calledOnceWithExactly(
|
|
5142
|
+
mockStatsAnalyzerCtor,
|
|
5143
|
+
sinon.match({
|
|
5144
|
+
isMultistream: true,
|
|
5145
|
+
})
|
|
5146
|
+
);
|
|
5147
|
+
const initialStatsAnalyzer = mockStatsAnalyzerCtor.returnValues[0];
|
|
5148
|
+
mockStatsAnalyzerCtor.resetHistory();
|
|
5149
|
+
|
|
5150
|
+
// TURN discovery was done (if needed)
|
|
5151
|
+
if (turnServerInfo) {
|
|
5152
|
+
assert.notCalled(meeting.roap.doTurnDiscovery);
|
|
5153
|
+
} else {
|
|
5154
|
+
assert.calledWith(meeting.roap.doTurnDiscovery, meeting, false, false);
|
|
5155
|
+
}
|
|
5156
|
+
|
|
5157
|
+
// and SDP offer was sent with the right audioMuted/videoMuted values
|
|
5158
|
+
checkSdpOfferSent({audioMuted: false, videoMuted: true});
|
|
5159
|
+
|
|
5160
|
+
await testUtils.flushPromises();
|
|
5161
|
+
|
|
5162
|
+
// at this point the meeting should have been downgraded to transcoded
|
|
5163
|
+
assert.equal(meeting.isMultistream, false);
|
|
5164
|
+
|
|
5165
|
+
// old stats analyzer stopped and new one created
|
|
5166
|
+
assert.calledOnce(initialStatsAnalyzer.stopAnalyzer);
|
|
5167
|
+
assert.calledOnceWithExactly(
|
|
5168
|
+
mockStatsAnalyzerCtor,
|
|
5169
|
+
sinon.match({
|
|
5170
|
+
isMultistream: false,
|
|
5171
|
+
})
|
|
5172
|
+
);
|
|
5173
|
+
|
|
5174
|
+
// and correct cleanup of other things should have been done
|
|
5175
|
+
assert.calledOnceWithExactly(meeting.closePeerConnections, false);
|
|
5176
|
+
assert.calledOnceWithExactly(meeting.mediaProperties.unsetPeerConnection);
|
|
5177
|
+
assert.calledOnceWithExactly(
|
|
5178
|
+
meeting.locusMediaRequest.downgradeFromMultistreamToTranscoded
|
|
5179
|
+
);
|
|
5180
|
+
|
|
5181
|
+
// new connection should have been created
|
|
5182
|
+
checkMediaConnectionCreated({
|
|
5183
|
+
expectMultistream: false,
|
|
5184
|
+
mediaConnectionConfig: expectedMediaConnectionConfig,
|
|
5185
|
+
localStreams: {
|
|
5186
|
+
audio: fakeMicrophoneStream,
|
|
5187
|
+
video: undefined,
|
|
5188
|
+
screenShareVideo: undefined,
|
|
5189
|
+
screenShareAudio: undefined,
|
|
5190
|
+
},
|
|
5191
|
+
direction: {
|
|
5192
|
+
audio: 'sendrecv',
|
|
5193
|
+
video: 'sendrecv',
|
|
5194
|
+
screenShare: 'recvonly',
|
|
5195
|
+
},
|
|
5196
|
+
remoteQualityLevel: 'HIGH',
|
|
5197
|
+
expectedDebugId,
|
|
5198
|
+
meetingId: meeting.id,
|
|
5199
|
+
});
|
|
5200
|
+
|
|
5201
|
+
// and new TURN discovery done (no matter if it was being done before or not)
|
|
5202
|
+
assert.calledWith(meeting.roap.doTurnDiscovery, meeting, true, true);
|
|
5203
|
+
|
|
5204
|
+
// simulate new offer
|
|
5205
|
+
await simulateRoapOffer(false);
|
|
5206
|
+
checkSdpOfferSent({audioMuted: false, videoMuted: true});
|
|
5207
|
+
|
|
5208
|
+
// overall there should have been 2 calls to locusMediaRequestStub, because 2 offers were sent
|
|
5209
|
+
assert.calledTwice(locusMediaRequestStub);
|
|
5210
|
+
|
|
5211
|
+
// simulate answer being processed correctly
|
|
5212
|
+
transcodedEventListeners[MediaConnectionEventNames.REMOTE_SDP_ANSWER_PROCESSED]();
|
|
5213
|
+
|
|
5214
|
+
// check that addMedia finally resolved
|
|
5215
|
+
await addMediaPromise;
|
|
5216
|
+
};
|
|
5217
|
+
|
|
5218
|
+
it('addMedia() falls back to transcoded if SDP answer is not from Homer', async () => {
|
|
5219
|
+
// call addMediaInternal like addMedia() does it
|
|
5220
|
+
await runCheck(undefined, false);
|
|
5221
|
+
});
|
|
5222
|
+
|
|
5223
|
+
it('addMediaInternal() correctly falls back to transcoded if SDP answer is not from Homer (joinWithMedia() case)', async () => {
|
|
5224
|
+
// call addMediaInternal the way joinWithMedia() does it - with TURN info already provided
|
|
5225
|
+
// and check that when we fallback to transcoded we still do another TURN discovery
|
|
5226
|
+
await runCheck(
|
|
5227
|
+
{
|
|
5228
|
+
url: 'turns:turn-server-url:443?transport=tcp',
|
|
5229
|
+
username: 'turn user',
|
|
5230
|
+
password: 'turn password',
|
|
5231
|
+
},
|
|
5232
|
+
false
|
|
5233
|
+
);
|
|
5234
|
+
});
|
|
5235
|
+
|
|
5236
|
+
it('addMediaInternal() correctly falls back to transcoded if SDP answer is not from Homer (joinWithMedia() retry case)', async () => {
|
|
5237
|
+
// call addMediaInternal the way joinWithMedia() does it when it does a retry - with TURN info already provided
|
|
5238
|
+
// but also with forceTurnDiscovery=true - this shouldn't affect the flow for fallback to transcoded in any way
|
|
5239
|
+
// but doing it just for completeness
|
|
5240
|
+
await runCheck(
|
|
5241
|
+
{
|
|
5242
|
+
url: 'turns:turn-server-url:443?transport=tcp',
|
|
5243
|
+
username: 'turn user',
|
|
5244
|
+
password: 'turn password',
|
|
5245
|
+
},
|
|
5246
|
+
true
|
|
5247
|
+
);
|
|
5248
|
+
});
|
|
5249
|
+
});
|
|
5250
|
+
}
|
|
4884
5251
|
})
|
|
4885
5252
|
);
|
|
4886
5253
|
|
|
@@ -4958,6 +5325,11 @@ describe('plugin-meetings', () => {
|
|
|
4958
5325
|
meeting.logger.error = sinon.stub().returns(true);
|
|
4959
5326
|
meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
|
|
4960
5327
|
webex.internal.voicea.off = sinon.stub().returns(true);
|
|
5328
|
+
meeting.stopTranscription = sinon.stub();
|
|
5329
|
+
meeting.transcription = {};
|
|
5330
|
+
|
|
5331
|
+
meeting.annotation.deregisterEvents = sinon.stub();
|
|
5332
|
+
webex.internal.llm.off = sinon.stub();
|
|
4961
5333
|
|
|
4962
5334
|
// A meeting needs to be joined to leave
|
|
4963
5335
|
meeting.meetingState = 'ACTIVE';
|
|
@@ -4978,6 +5350,9 @@ describe('plugin-meetings', () => {
|
|
|
4978
5350
|
assert.calledOnce(meeting.closePeerConnections);
|
|
4979
5351
|
assert.calledOnce(meeting.unsetRemoteStreams);
|
|
4980
5352
|
assert.calledOnce(meeting.unsetPeerConnections);
|
|
5353
|
+
assert.calledOnce(meeting.stopTranscription);
|
|
5354
|
+
assert.calledOnce(meeting.annotation.deregisterEvents);
|
|
5355
|
+
assert.calledWith(webex.internal.llm.off, 'event:relay.event', meeting.processRelayEvent);
|
|
4981
5356
|
});
|
|
4982
5357
|
|
|
4983
5358
|
it('should reset call diagnostic latencies correctly', async () => {
|
|
@@ -5965,6 +6340,38 @@ describe('plugin-meetings', () => {
|
|
|
5965
6340
|
assert.equal(meeting.passwordStatus, PASSWORD_STATUS.REQUIRED);
|
|
5966
6341
|
});
|
|
5967
6342
|
|
|
6343
|
+
it('handles meetingInfoProvider not reach JBH', async () => {
|
|
6344
|
+
meeting.destination = FAKE_DESTINATION;
|
|
6345
|
+
meeting.destinationType = FAKE_TYPE;
|
|
6346
|
+
meeting.attrs.meetingInfoProvider = {
|
|
6347
|
+
fetchMeetingInfo: sinon
|
|
6348
|
+
.stub()
|
|
6349
|
+
.throws(new MeetingInfoV2JoinForbiddenError(403003, FAKE_MEETING_INFO)),
|
|
6350
|
+
};
|
|
6351
|
+
|
|
6352
|
+
await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinForbiddenError);
|
|
6353
|
+
|
|
6354
|
+
assert.calledWith(
|
|
6355
|
+
meeting.attrs.meetingInfoProvider.fetchMeetingInfo,
|
|
6356
|
+
FAKE_DESTINATION,
|
|
6357
|
+
FAKE_TYPE,
|
|
6358
|
+
null,
|
|
6359
|
+
null,
|
|
6360
|
+
undefined,
|
|
6361
|
+
'locus-id',
|
|
6362
|
+
{},
|
|
6363
|
+
{meetingId: meeting.id, sendCAevents: true}
|
|
6364
|
+
);
|
|
6365
|
+
|
|
6366
|
+
assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
|
|
6367
|
+
assert.equal(meeting.meetingInfoFailureCode, 403003);
|
|
6368
|
+
assert.equal(
|
|
6369
|
+
meeting.meetingInfoFailureReason,
|
|
6370
|
+
MEETING_INFO_FAILURE_REASON.NOT_REACH_JBH
|
|
6371
|
+
);
|
|
6372
|
+
assert.equal(meeting.requiredCaptcha, null);
|
|
6373
|
+
});
|
|
6374
|
+
|
|
5968
6375
|
it('handles meetingInfoProvider policy error', async () => {
|
|
5969
6376
|
meeting.destination = FAKE_DESTINATION;
|
|
5970
6377
|
meeting.destinationType = FAKE_TYPE;
|
|
@@ -6332,29 +6739,74 @@ describe('plugin-meetings', () => {
|
|
|
6332
6739
|
assert.equal(meeting.fetchMeetingInfoTimeoutId, undefined);
|
|
6333
6740
|
});
|
|
6334
6741
|
|
|
6335
|
-
it('handles
|
|
6742
|
+
it('handles MeetingInfoV2JoinWebinarError webinar need registration', async () => {
|
|
6336
6743
|
meeting.destination = FAKE_DESTINATION;
|
|
6337
6744
|
meeting.destinationType = FAKE_TYPE;
|
|
6338
6745
|
meeting.attrs.meetingInfoProvider = {
|
|
6339
6746
|
fetchMeetingInfo: sinon
|
|
6340
6747
|
.stub()
|
|
6341
6748
|
.throws(
|
|
6342
|
-
new
|
|
6749
|
+
new MeetingInfoV2JoinWebinarError(403021, FAKE_MEETING_INFO, 'a message')
|
|
6343
6750
|
),
|
|
6344
6751
|
};
|
|
6345
6752
|
|
|
6346
6753
|
await assert.isRejected(
|
|
6347
6754
|
meeting.fetchMeetingInfo({sendCAevents: true}),
|
|
6348
|
-
|
|
6755
|
+
JoinWebinarError
|
|
6349
6756
|
);
|
|
6350
6757
|
|
|
6351
6758
|
assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
|
|
6352
|
-
assert.equal(meeting.meetingInfoFailureCode, 403021);
|
|
6353
6759
|
assert.equal(
|
|
6354
6760
|
meeting.meetingInfoFailureReason,
|
|
6355
6761
|
MEETING_INFO_FAILURE_REASON.WEBINAR_REGISTRATION
|
|
6356
6762
|
);
|
|
6357
6763
|
});
|
|
6764
|
+
|
|
6765
|
+
it('handles MeetingInfoV2JoinWebinarError webinar need join with webcast', async () => {
|
|
6766
|
+
meeting.destination = FAKE_DESTINATION;
|
|
6767
|
+
meeting.destinationType = FAKE_TYPE;
|
|
6768
|
+
meeting.attrs.meetingInfoProvider = {
|
|
6769
|
+
fetchMeetingInfo: sinon
|
|
6770
|
+
.stub()
|
|
6771
|
+
.throws(
|
|
6772
|
+
new MeetingInfoV2JoinWebinarError(403026, FAKE_MEETING_INFO, 'a message')
|
|
6773
|
+
),
|
|
6774
|
+
};
|
|
6775
|
+
|
|
6776
|
+
await assert.isRejected(
|
|
6777
|
+
meeting.fetchMeetingInfo({sendCAevents: true}),
|
|
6778
|
+
JoinWebinarError
|
|
6779
|
+
);
|
|
6780
|
+
|
|
6781
|
+
assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
|
|
6782
|
+
assert.equal(
|
|
6783
|
+
meeting.meetingInfoFailureReason,
|
|
6784
|
+
MEETING_INFO_FAILURE_REASON.NEED_JOIN_WITH_WEBCAST
|
|
6785
|
+
);
|
|
6786
|
+
});
|
|
6787
|
+
|
|
6788
|
+
it('handles MeetingInfoV2JoinWebinarError webinar need registrationId', async () => {
|
|
6789
|
+
meeting.destination = FAKE_DESTINATION;
|
|
6790
|
+
meeting.destinationType = FAKE_TYPE;
|
|
6791
|
+
meeting.attrs.meetingInfoProvider = {
|
|
6792
|
+
fetchMeetingInfo: sinon
|
|
6793
|
+
.stub()
|
|
6794
|
+
.throws(
|
|
6795
|
+
new MeetingInfoV2JoinWebinarError(403037, FAKE_MEETING_INFO, 'a message')
|
|
6796
|
+
),
|
|
6797
|
+
};
|
|
6798
|
+
|
|
6799
|
+
await assert.isRejected(
|
|
6800
|
+
meeting.fetchMeetingInfo({sendCAevents: true}),
|
|
6801
|
+
JoinWebinarError
|
|
6802
|
+
);
|
|
6803
|
+
|
|
6804
|
+
assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
|
|
6805
|
+
assert.equal(
|
|
6806
|
+
meeting.meetingInfoFailureReason,
|
|
6807
|
+
MEETING_INFO_FAILURE_REASON.WEBINAR_NEED_REGISTRATIONID
|
|
6808
|
+
);
|
|
6809
|
+
});
|
|
6358
6810
|
});
|
|
6359
6811
|
|
|
6360
6812
|
describe('#refreshPermissionToken', () => {
|
|
@@ -6815,6 +7267,9 @@ describe('plugin-meetings', () => {
|
|
|
6815
7267
|
meeting.transcription = {};
|
|
6816
7268
|
meeting.stopTranscription = sinon.stub();
|
|
6817
7269
|
|
|
7270
|
+
meeting.annotation.deregisterEvents = sinon.stub();
|
|
7271
|
+
webex.internal.llm.off = sinon.stub();
|
|
7272
|
+
|
|
6818
7273
|
// A meeting needs to be joined to end
|
|
6819
7274
|
meeting.meetingState = 'ACTIVE';
|
|
6820
7275
|
meeting.state = 'JOINED';
|
|
@@ -6835,6 +7290,9 @@ describe('plugin-meetings', () => {
|
|
|
6835
7290
|
assert.calledOnce(meeting?.unsetRemoteStreams);
|
|
6836
7291
|
assert.calledOnce(meeting?.unsetPeerConnections);
|
|
6837
7292
|
assert.calledOnce(meeting?.stopTranscription);
|
|
7293
|
+
|
|
7294
|
+
assert.called(meeting.annotation.deregisterEvents);
|
|
7295
|
+
assert.calledWith(webex.internal.llm.off, 'event:relay.event', meeting.processRelayEvent);
|
|
6838
7296
|
});
|
|
6839
7297
|
});
|
|
6840
7298
|
|
|
@@ -7817,7 +8275,9 @@ describe('plugin-meetings', () => {
|
|
|
7817
8275
|
});
|
|
7818
8276
|
|
|
7819
8277
|
it('should collect ice candidates', () => {
|
|
7820
|
-
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({
|
|
8278
|
+
eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({
|
|
8279
|
+
candidate: {candidate: 'candidate'},
|
|
8280
|
+
});
|
|
7821
8281
|
|
|
7822
8282
|
assert.equal(meeting.iceCandidatesCount, 1);
|
|
7823
8283
|
});
|
|
@@ -8123,10 +8583,10 @@ describe('plugin-meetings', () => {
|
|
|
8123
8583
|
meeting.statsAnalyzer.stopAnalyzer = sinon.stub().resolves();
|
|
8124
8584
|
meeting.reconnectionManager = {
|
|
8125
8585
|
reconnect: sinon.stub().resolves(),
|
|
8126
|
-
resetReconnectionTimer: () => {}
|
|
8586
|
+
resetReconnectionTimer: () => {},
|
|
8127
8587
|
};
|
|
8128
8588
|
meeting.currentMediaStatus = {
|
|
8129
|
-
video: true
|
|
8589
|
+
video: true,
|
|
8130
8590
|
};
|
|
8131
8591
|
|
|
8132
8592
|
await mockFailedEvent();
|
|
@@ -8408,8 +8868,7 @@ describe('plugin-meetings', () => {
|
|
|
8408
8868
|
assert.calledWith(meeting.roapMessageReceived, fakeAnswer);
|
|
8409
8869
|
});
|
|
8410
8870
|
|
|
8411
|
-
|
|
8412
|
-
const fakeError = new Error('fake error');
|
|
8871
|
+
const runOfferSendingFailureTest = async (fakeError, canProceed, expectedErrorCode) => {
|
|
8413
8872
|
const clock = sinon.useFakeTimers();
|
|
8414
8873
|
sinon.spy(clock, 'clearTimeout');
|
|
8415
8874
|
meeting.deferSDPAnswer = {reject: sinon.stub()};
|
|
@@ -8447,19 +8906,31 @@ describe('plugin-meetings', () => {
|
|
|
8447
8906
|
assert.equal(meeting.sdpResponseTimer, undefined);
|
|
8448
8907
|
|
|
8449
8908
|
assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
|
|
8450
|
-
clientErrorCode:
|
|
8909
|
+
clientErrorCode: expectedErrorCode,
|
|
8451
8910
|
});
|
|
8452
8911
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
8453
8912
|
name: 'client.media-engine.remote-sdp-received',
|
|
8454
8913
|
payload: {
|
|
8455
|
-
canProceed
|
|
8456
|
-
errors: [{errorCode:
|
|
8914
|
+
canProceed,
|
|
8915
|
+
errors: [{errorCode: expectedErrorCode, fatal: true}],
|
|
8457
8916
|
},
|
|
8458
8917
|
options: {
|
|
8459
8918
|
meetingId: meeting.id,
|
|
8460
8919
|
rawError: fakeError,
|
|
8461
8920
|
},
|
|
8462
8921
|
});
|
|
8922
|
+
};
|
|
8923
|
+
|
|
8924
|
+
it('handles OFFER message correctly when request fails', async () => {
|
|
8925
|
+
const fakeError = new Error('fake error');
|
|
8926
|
+
|
|
8927
|
+
await runOfferSendingFailureTest(fakeError, false, 2007);
|
|
8928
|
+
});
|
|
8929
|
+
|
|
8930
|
+
it('handles OFFER message correctly when we get a non-homer answer', async () => {
|
|
8931
|
+
const fakeError = new MultistreamNotSupportedError();
|
|
8932
|
+
|
|
8933
|
+
await runOfferSendingFailureTest(fakeError, true, 2012);
|
|
8463
8934
|
});
|
|
8464
8935
|
|
|
8465
8936
|
it('handles ANSWER message correctly', () => {
|
|
@@ -8662,6 +9133,7 @@ describe('plugin-meetings', () => {
|
|
|
8662
9133
|
});
|
|
8663
9134
|
});
|
|
8664
9135
|
});
|
|
9136
|
+
|
|
8665
9137
|
describe('#setUpLocusInfoSelfListener', () => {
|
|
8666
9138
|
it('listens to the self unadmitted guest event', (done) => {
|
|
8667
9139
|
meeting.startKeepAlive = sinon.stub();
|
|
@@ -8756,6 +9228,26 @@ describe('plugin-meetings', () => {
|
|
|
8756
9228
|
);
|
|
8757
9229
|
});
|
|
8758
9230
|
|
|
9231
|
+
it('listens to the brb state changed event', () => {
|
|
9232
|
+
const assertBrb = (enabled) => {
|
|
9233
|
+
meeting.locusInfo.emit(
|
|
9234
|
+
{ function: 'test', file: 'test' },
|
|
9235
|
+
LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
|
|
9236
|
+
{ brb: { enabled } },
|
|
9237
|
+
)
|
|
9238
|
+
assert.calledWithExactly(
|
|
9239
|
+
TriggerProxy.trigger,
|
|
9240
|
+
meeting,
|
|
9241
|
+
{file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
|
|
9242
|
+
EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
|
|
9243
|
+
{ payload: { brb: { enabled } } },
|
|
9244
|
+
);
|
|
9245
|
+
}
|
|
9246
|
+
|
|
9247
|
+
assertBrb(true);
|
|
9248
|
+
assertBrb(false);
|
|
9249
|
+
})
|
|
9250
|
+
|
|
8759
9251
|
it('listens to the interpretation changed event', () => {
|
|
8760
9252
|
meeting.simultaneousInterpretation.updateSelfInterpretation = sinon.stub();
|
|
8761
9253
|
|
|
@@ -9054,7 +9546,7 @@ describe('plugin-meetings', () => {
|
|
|
9054
9546
|
{state}
|
|
9055
9547
|
);
|
|
9056
9548
|
|
|
9057
|
-
assert.calledOnceWithExactly(
|
|
9549
|
+
assert.calledOnceWithExactly(meeting.webinar.updatePracticeSessionStatus, state);
|
|
9058
9550
|
assert.calledWith(
|
|
9059
9551
|
TriggerProxy.trigger,
|
|
9060
9552
|
meeting,
|
|
@@ -9537,15 +10029,44 @@ describe('plugin-meetings', () => {
|
|
|
9537
10029
|
describe('#closePeerConnections', () => {
|
|
9538
10030
|
it('should close the webrtc media connection, and return a promise', async () => {
|
|
9539
10031
|
const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
|
|
9540
|
-
|
|
10032
|
+
const fakeWebrtcMediaConnection = {close: sinon.stub()};
|
|
10033
|
+
meeting.mediaProperties.webrtcMediaConnection = fakeWebrtcMediaConnection;
|
|
10034
|
+
|
|
10035
|
+
meeting.audio = {id: 'fakeAudioMuteState'};
|
|
10036
|
+
meeting.video = {id: 'fakeVideoMuteState'};
|
|
10037
|
+
|
|
9541
10038
|
const pcs = meeting.closePeerConnections();
|
|
9542
10039
|
|
|
9543
10040
|
assert.exists(pcs.then);
|
|
9544
10041
|
await pcs;
|
|
9545
|
-
assert.calledOnce(
|
|
10042
|
+
assert.calledOnce(fakeWebrtcMediaConnection.close);
|
|
10043
|
+
assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
|
|
10044
|
+
assert.equal(meeting.audio, null);
|
|
10045
|
+
assert.equal(meeting.video, null);
|
|
10046
|
+
assert.equal(meeting.mediaProperties.webrtcMediaConnection, null);
|
|
10047
|
+
});
|
|
10048
|
+
|
|
10049
|
+
it('should close the webrtc media connection, but keep audio and video props unchanged if called with resetMuteStates=false', async () => {
|
|
10050
|
+
const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
|
|
10051
|
+
const fakeWebrtcMediaConnection = {close: sinon.stub()};
|
|
10052
|
+
meeting.mediaProperties.webrtcMediaConnection = fakeWebrtcMediaConnection;
|
|
10053
|
+
|
|
10054
|
+
const fakeAudio = {id: 'fakeAudioMuteState'};
|
|
10055
|
+
const fakeVideo = {id: 'fakeVideoMuteState'};
|
|
10056
|
+
|
|
10057
|
+
meeting.audio = fakeAudio;
|
|
10058
|
+
meeting.video = fakeVideo;
|
|
10059
|
+
|
|
10060
|
+
await meeting.closePeerConnections(false);
|
|
10061
|
+
|
|
10062
|
+
assert.calledOnce(fakeWebrtcMediaConnection.close);
|
|
9546
10063
|
assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
|
|
10064
|
+
assert.equal(meeting.audio, fakeAudio);
|
|
10065
|
+
assert.equal(meeting.video, fakeVideo);
|
|
10066
|
+
assert.equal(meeting.mediaProperties.webrtcMediaConnection, null);
|
|
9547
10067
|
});
|
|
9548
10068
|
});
|
|
10069
|
+
|
|
9549
10070
|
describe('#unsetPeerConnections', () => {
|
|
9550
10071
|
it('should unset the peer connections', () => {
|
|
9551
10072
|
meeting.mediaProperties.unsetPeerConnection = sinon.stub().returns(true);
|
|
@@ -10674,6 +11195,7 @@ describe('plugin-meetings', () => {
|
|
|
10674
11195
|
meeting.webex.internal.llm.on = sinon.stub();
|
|
10675
11196
|
meeting.webex.internal.llm.off = sinon.stub();
|
|
10676
11197
|
meeting.processRelayEvent = sinon.stub();
|
|
11198
|
+
meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(false);
|
|
10677
11199
|
});
|
|
10678
11200
|
|
|
10679
11201
|
it('does not connect if the call is not joined yet', async () => {
|
|
@@ -10805,6 +11327,19 @@ describe('plugin-meetings', () => {
|
|
|
10805
11327
|
meeting.processRelayEvent
|
|
10806
11328
|
);
|
|
10807
11329
|
});
|
|
11330
|
+
|
|
11331
|
+
|
|
11332
|
+
it('connect ps data channel if ps started in webinar', async () => {
|
|
11333
|
+
meeting.joinedWith = {state: 'JOINED'};
|
|
11334
|
+
meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url', practiceSessionDatachannelUrl: 'a ps datachannel url'}};
|
|
11335
|
+
meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(true);
|
|
11336
|
+
await meeting.updateLLMConnection();
|
|
11337
|
+
|
|
11338
|
+
assert.notCalled(webex.internal.llm.disconnectLLM);
|
|
11339
|
+
assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a ps datachannel url');
|
|
11340
|
+
|
|
11341
|
+
});
|
|
11342
|
+
|
|
10808
11343
|
});
|
|
10809
11344
|
|
|
10810
11345
|
describe('#setLocus', () => {
|
|
@@ -10996,6 +11531,7 @@ describe('plugin-meetings', () => {
|
|
|
10996
11531
|
beforeEach(() => {
|
|
10997
11532
|
meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
|
|
10998
11533
|
meeting.deviceUrl = 'my-web-url';
|
|
11534
|
+
meeting.locusInfo.info = {isWebinar: false};
|
|
10999
11535
|
});
|
|
11000
11536
|
|
|
11001
11537
|
const USER_IDS = {
|
|
@@ -11221,13 +11757,24 @@ describe('plugin-meetings', () => {
|
|
|
11221
11757
|
|
|
11222
11758
|
activeSharingId.whiteboard = beneficiaryId;
|
|
11223
11759
|
|
|
11224
|
-
eventTrigger.share.push({
|
|
11760
|
+
eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
|
|
11761
|
+
eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
|
|
11762
|
+
functionName: 'remoteShare',
|
|
11763
|
+
eventPayload: {
|
|
11764
|
+
memberId: null,
|
|
11765
|
+
url,
|
|
11766
|
+
shareInstanceId,
|
|
11767
|
+
annotationInfo: undefined,
|
|
11768
|
+
resourceType: undefined,
|
|
11769
|
+
},
|
|
11770
|
+
} : {
|
|
11225
11771
|
eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
|
|
11226
11772
|
functionName: 'startWhiteboardShare',
|
|
11227
11773
|
eventPayload: {resourceUrl, memberId: beneficiaryId},
|
|
11228
11774
|
});
|
|
11229
11775
|
|
|
11230
|
-
shareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
|
|
11776
|
+
shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
|
|
11777
|
+
|
|
11231
11778
|
}
|
|
11232
11779
|
|
|
11233
11780
|
if (eventTrigger.member) {
|
|
@@ -11259,13 +11806,24 @@ describe('plugin-meetings', () => {
|
|
|
11259
11806
|
newPayload.current.content.disposition = FLOOR_ACTION.ACCEPTED;
|
|
11260
11807
|
newPayload.current.content.beneficiaryId = otherBeneficiaryId;
|
|
11261
11808
|
|
|
11262
|
-
eventTrigger.share.push({
|
|
11809
|
+
eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
|
|
11810
|
+
eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
|
|
11811
|
+
functionName: 'remoteShare',
|
|
11812
|
+
eventPayload: {
|
|
11813
|
+
memberId: null,
|
|
11814
|
+
url,
|
|
11815
|
+
shareInstanceId,
|
|
11816
|
+
annotationInfo: undefined,
|
|
11817
|
+
resourceType: undefined,
|
|
11818
|
+
},
|
|
11819
|
+
} : {
|
|
11263
11820
|
eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
|
|
11264
11821
|
functionName: 'startWhiteboardShare',
|
|
11265
11822
|
eventPayload: {resourceUrl, memberId: beneficiaryId},
|
|
11266
11823
|
});
|
|
11267
11824
|
|
|
11268
|
-
shareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
|
|
11825
|
+
shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
|
|
11826
|
+
|
|
11269
11827
|
} else {
|
|
11270
11828
|
eventTrigger.share.push({
|
|
11271
11829
|
eventName: EVENT_TRIGGERS.MEETING_STOPPED_SHARING_WHITEBOARD,
|
|
@@ -11392,6 +11950,38 @@ describe('plugin-meetings', () => {
|
|
|
11392
11950
|
assert.exists(meeting.setUpLocusMediaSharesListener);
|
|
11393
11951
|
});
|
|
11394
11952
|
|
|
11953
|
+
describe('Whiteboard Share - Webinar Attendee', () => {
|
|
11954
|
+
it('Scenario #1: Whiteboard sharing as a webinar attendee', () => {
|
|
11955
|
+
// Set the webinar attendee flag
|
|
11956
|
+
meeting.webinar = { selfIsAttendee: true };
|
|
11957
|
+
meeting.locusInfo.info.isWebinar = true;
|
|
11958
|
+
|
|
11959
|
+
// Step 1: Start sharing whiteboard A
|
|
11960
|
+
const data1 = generateData(
|
|
11961
|
+
blankPayload, // Initial payload
|
|
11962
|
+
true, // isGranting: Granting share
|
|
11963
|
+
false, // isContent: Whiteboard (not content)
|
|
11964
|
+
USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
|
|
11965
|
+
RESOURCE_URLS.WHITEBOARD_A // Resource URL: Whiteboard A
|
|
11966
|
+
);
|
|
11967
|
+
|
|
11968
|
+
// Step 2: Stop sharing whiteboard A
|
|
11969
|
+
const data2 = generateData(
|
|
11970
|
+
data1.payload, // Updated payload from Step 1
|
|
11971
|
+
false, // isGranting: Stopping share
|
|
11972
|
+
false, // isContent: Whiteboard
|
|
11973
|
+
USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
|
|
11974
|
+
);
|
|
11975
|
+
|
|
11976
|
+
// Validate the payload changes and status updates
|
|
11977
|
+
payloadTestHelper([data1]);
|
|
11978
|
+
|
|
11979
|
+
// Specific assertions for webinar attendee status
|
|
11980
|
+
assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
|
|
11981
|
+
});
|
|
11982
|
+
});
|
|
11983
|
+
|
|
11984
|
+
|
|
11395
11985
|
describe('Whiteboard A --> Whiteboard B', () => {
|
|
11396
11986
|
it('Scenario #1: you share both whiteboards', () => {
|
|
11397
11987
|
const data1 = generateData(
|
|
@@ -12067,9 +12657,12 @@ describe('plugin-meetings', () => {
|
|
|
12067
12657
|
it('startKeepAlive starts the keep alive', async () => {
|
|
12068
12658
|
meeting.meetingRequest.keepAlive = sinon.stub().returns(Promise.resolve());
|
|
12069
12659
|
|
|
12660
|
+
const keepAliveUrl1 = 'keep.alive.url1';
|
|
12661
|
+
const keepAliveUrl2 = 'keep.alive.url2';
|
|
12662
|
+
|
|
12070
12663
|
assert.isNull(meeting.keepAliveTimerId);
|
|
12071
12664
|
meeting.joinedWith = {
|
|
12072
|
-
keepAliveUrl:
|
|
12665
|
+
keepAliveUrl: keepAliveUrl1,
|
|
12073
12666
|
keepAliveSecs: defaultKeepAliveSecs,
|
|
12074
12667
|
};
|
|
12075
12668
|
meeting.startKeepAlive();
|
|
@@ -12078,12 +12671,15 @@ describe('plugin-meetings', () => {
|
|
|
12078
12671
|
assert.notCalled(meeting.meetingRequest.keepAlive);
|
|
12079
12672
|
await progressTime(defaultExpectedInterval);
|
|
12080
12673
|
assert.calledOnceWithExactly(meeting.meetingRequest.keepAlive, {
|
|
12081
|
-
keepAliveUrl:
|
|
12674
|
+
keepAliveUrl: keepAliveUrl1,
|
|
12082
12675
|
});
|
|
12676
|
+
// joinedWith keep alive url might change (when we fallback from multistream to transcoded)
|
|
12677
|
+
meeting.joinedWith.keepAliveUrl = keepAliveUrl2;
|
|
12678
|
+
|
|
12083
12679
|
await progressTime(defaultExpectedInterval);
|
|
12084
12680
|
assert.calledTwice(meeting.meetingRequest.keepAlive);
|
|
12085
|
-
assert.
|
|
12086
|
-
keepAliveUrl:
|
|
12681
|
+
assert.calledWith(meeting.meetingRequest.keepAlive, {
|
|
12682
|
+
keepAliveUrl: keepAliveUrl2,
|
|
12087
12683
|
});
|
|
12088
12684
|
});
|
|
12089
12685
|
it('startKeepAlive handles existing keepAliveTimerId', async () => {
|
|
@@ -12681,5 +13277,26 @@ describe('plugin-meetings', () => {
|
|
|
12681
13277
|
assert.calledOnceWithExactly(getMediaServer, 'fake sdp');
|
|
12682
13278
|
assert.equal(meeting.mediaProperties.webrtcMediaConnection.mediaServer, 'homer');
|
|
12683
13279
|
});
|
|
13280
|
+
|
|
13281
|
+
it('throws MultistreamNotSupportedError if we get a non-homer SDP answer', async () => {
|
|
13282
|
+
const fakeMessage = {messageType: 'ANSWER', sdp: 'fake sdp'};
|
|
13283
|
+
|
|
13284
|
+
meeting.isMultistream = true;
|
|
13285
|
+
meeting.mediaProperties.webrtcMediaConnection = {
|
|
13286
|
+
roapMessageReceived: sinon.stub(),
|
|
13287
|
+
};
|
|
13288
|
+
|
|
13289
|
+
sinon.stub(MeetingsUtil, 'getMediaServer').returns('linus');
|
|
13290
|
+
|
|
13291
|
+
try {
|
|
13292
|
+
await meeting.roapMessageReceived(fakeMessage);
|
|
13293
|
+
|
|
13294
|
+
assert.fail('Expected MultistreamNotSupportedError to be thrown');
|
|
13295
|
+
} catch(e) {
|
|
13296
|
+
assert.isTrue(e instanceof MultistreamNotSupportedError);
|
|
13297
|
+
}
|
|
13298
|
+
|
|
13299
|
+
assert.notCalled(meeting.mediaProperties.webrtcMediaConnection.roapMessageReceived);
|
|
13300
|
+
});
|
|
12684
13301
|
});
|
|
12685
13302
|
});
|