@webex/plugin-meetings 3.7.0-next.32 → 3.7.0-next.34
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/multistream-not-supported-error.js +53 -0
- package/dist/common/errors/multistream-not-supported-error.js.map +1 -0
- package/dist/constants.js +5 -0
- package/dist/constants.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/meeting/index.js +256 -166
- 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/meetings/util.js +1 -1
- package/dist/meetings/util.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/multistream-not-supported-error.d.ts +17 -0
- package/dist/types/constants.d.ts +5 -0
- package/dist/types/meeting/index.d.ts +11 -2
- package/dist/types/meeting/locusMediaRequest.d.ts +4 -0
- package/dist/webinar/index.js +1 -1
- package/package.json +21 -21
- package/src/annotation/index.ts +16 -0
- package/src/common/errors/multistream-not-supported-error.ts +30 -0
- package/src/constants.ts +5 -0
- package/src/meeting/index.ts +110 -27
- package/src/meeting/locusMediaRequest.ts +7 -0
- package/src/meetings/util.ts +2 -1
- package/src/roap/index.ts +10 -8
- package/test/unit/spec/annotation/index.ts +46 -1
- package/test/unit/spec/meeting/index.js +367 -21
- package/test/unit/spec/meetings/utils.js +10 -0
- package/test/unit/spec/roap/index.ts +47 -0
|
@@ -93,6 +93,7 @@ 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
97
|
import testUtils from '../../../utils/testUtils';
|
|
97
98
|
import {
|
|
98
99
|
MeetingInfoV2CaptchaError,
|
|
@@ -652,7 +653,7 @@ describe('plugin-meetings', () => {
|
|
|
652
653
|
const fakeTurnServerInfo = {id: 'fake turn info'};
|
|
653
654
|
const fakeJoinResult = {id: 'join result'};
|
|
654
655
|
|
|
655
|
-
const joinOptions = {correlationId: '12345'};
|
|
656
|
+
const joinOptions = {correlationId: '12345', enableMultistream: true};
|
|
656
657
|
const mediaOptions = {audioEnabled: true, allowMediaInLobby: true};
|
|
657
658
|
|
|
658
659
|
let generateTurnDiscoveryRequestMessageStub;
|
|
@@ -661,7 +662,10 @@ describe('plugin-meetings', () => {
|
|
|
661
662
|
let addMediaInternalStub;
|
|
662
663
|
|
|
663
664
|
beforeEach(() => {
|
|
664
|
-
meeting.join = sinon.stub().
|
|
665
|
+
meeting.join = sinon.stub().callsFake((joinOptions) => {
|
|
666
|
+
meeting.isMultistream = joinOptions.enableMultistream;
|
|
667
|
+
return Promise.resolve(fakeJoinResult)
|
|
668
|
+
});
|
|
665
669
|
addMediaInternalStub = sinon
|
|
666
670
|
.stub(meeting, 'addMediaInternal')
|
|
667
671
|
.returns(Promise.resolve(test4));
|
|
@@ -700,7 +704,7 @@ describe('plugin-meetings', () => {
|
|
|
700
704
|
mediaOptions
|
|
701
705
|
);
|
|
702
706
|
|
|
703
|
-
assert.deepEqual(result, {join: fakeJoinResult, media: test4});
|
|
707
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
|
|
704
708
|
|
|
705
709
|
// resets joinWithMediaRetryInfo
|
|
706
710
|
assert.deepEqual(meeting.joinWithMediaRetryInfo, {
|
|
@@ -733,7 +737,7 @@ describe('plugin-meetings', () => {
|
|
|
733
737
|
mediaOptions
|
|
734
738
|
);
|
|
735
739
|
|
|
736
|
-
assert.deepEqual(result, {join: fakeJoinResult, media: test4});
|
|
740
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
|
|
737
741
|
assert.equal(meeting.turnServerUsed, false);
|
|
738
742
|
});
|
|
739
743
|
|
|
@@ -768,7 +772,7 @@ describe('plugin-meetings', () => {
|
|
|
768
772
|
mediaOptions
|
|
769
773
|
);
|
|
770
774
|
|
|
771
|
-
assert.deepEqual(result, {join: fakeJoinResult, media: test4});
|
|
775
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
|
|
772
776
|
});
|
|
773
777
|
|
|
774
778
|
it('should reject if join() fails', async () => {
|
|
@@ -855,7 +859,8 @@ describe('plugin-meetings', () => {
|
|
|
855
859
|
}
|
|
856
860
|
);
|
|
857
861
|
|
|
858
|
-
|
|
862
|
+
// expect multistreamEnabled: false, because this test overrides the join meeting.join stub so it doesn't set the isMultistream flag
|
|
863
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: false});
|
|
859
864
|
|
|
860
865
|
// resets joinWithMediaRetryInfo
|
|
861
866
|
assert.deepEqual(meeting.joinWithMediaRetryInfo, {
|
|
@@ -944,7 +949,7 @@ describe('plugin-meetings', () => {
|
|
|
944
949
|
mediaOptions,
|
|
945
950
|
});
|
|
946
951
|
|
|
947
|
-
assert.deepEqual(result, {join: fakeJoinResult, media: test4});
|
|
952
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
|
|
948
953
|
|
|
949
954
|
assert.calledOnce(meeting.join);
|
|
950
955
|
assert.notCalled(leaveStub);
|
|
@@ -1038,6 +1043,7 @@ describe('plugin-meetings', () => {
|
|
|
1038
1043
|
getConnectionState: sinon.stub().returns(ConnectionState.Connected),
|
|
1039
1044
|
initiateOffer: sinon.stub().resolves({}),
|
|
1040
1045
|
on: sinon.stub(),
|
|
1046
|
+
createSendSlot: sinon.stub(),
|
|
1041
1047
|
};
|
|
1042
1048
|
|
|
1043
1049
|
/* Setup the stubs so that the first call to addMediaInternal() fails
|
|
@@ -1054,12 +1060,14 @@ describe('plugin-meetings', () => {
|
|
|
1054
1060
|
|
|
1055
1061
|
sinon.stub(meeting.roap, 'doTurnDiscovery').resolves({turnServerInfo: 'fake turn info'});
|
|
1056
1062
|
|
|
1063
|
+
// calling joinWithMedia() with enableMultistream=false, because this test uses real addMediaInternal() implementation
|
|
1064
|
+
// and it requires less stubs when it's without multistream
|
|
1057
1065
|
const result = await meeting.joinWithMedia({
|
|
1058
|
-
joinOptions,
|
|
1066
|
+
joinOptions: {...joinOptions, enableMultistream: false},
|
|
1059
1067
|
mediaOptions,
|
|
1060
1068
|
});
|
|
1061
1069
|
|
|
1062
|
-
assert.deepEqual(result, {join: fakeJoinResult, media: undefined});
|
|
1070
|
+
assert.deepEqual(result, {join: fakeJoinResult, media: undefined, multistreamEnabled: false});
|
|
1063
1071
|
|
|
1064
1072
|
assert.calledOnce(meeting.join);
|
|
1065
1073
|
|
|
@@ -1134,6 +1142,7 @@ describe('plugin-meetings', () => {
|
|
|
1134
1142
|
addMediaError.name = 'SdpOfferCreationError';
|
|
1135
1143
|
|
|
1136
1144
|
meeting.addMediaInternal.rejects(addMediaError);
|
|
1145
|
+
sinon.stub(meeting, 'leave').resolves();
|
|
1137
1146
|
|
|
1138
1147
|
await assert.isRejected(
|
|
1139
1148
|
meeting.joinWithMedia({
|
|
@@ -1162,6 +1171,7 @@ describe('plugin-meetings', () => {
|
|
|
1162
1171
|
type: addMediaError.name,
|
|
1163
1172
|
}
|
|
1164
1173
|
);
|
|
1174
|
+
assert.calledOnceWithExactly(meeting.leave, {resourceId: undefined, reason: 'joinWithMedia failure'})
|
|
1165
1175
|
});
|
|
1166
1176
|
});
|
|
1167
1177
|
|
|
@@ -1238,6 +1248,7 @@ describe('plugin-meetings', () => {
|
|
|
1238
1248
|
webex.internal.voicea.off = sinon.stub();
|
|
1239
1249
|
webex.internal.voicea.listenToEvents = sinon.stub();
|
|
1240
1250
|
webex.internal.voicea.turnOnCaptions = sinon.stub();
|
|
1251
|
+
webex.internal.voicea.deregisterEvents = sinon.stub();
|
|
1241
1252
|
});
|
|
1242
1253
|
|
|
1243
1254
|
it('should stop listening to voicea events and also trigger a stop event', () => {
|
|
@@ -1566,6 +1577,55 @@ describe('plugin-meetings', () => {
|
|
|
1566
1577
|
fakeProcessedReaction
|
|
1567
1578
|
);
|
|
1568
1579
|
});
|
|
1580
|
+
|
|
1581
|
+
it('should fail quietly if participantId does not exist in membersCollection', () => {
|
|
1582
|
+
LoggerProxy.logger.warn = sinon.stub();
|
|
1583
|
+
meeting.isReactionsSupported = sinon.stub().returns(true);
|
|
1584
|
+
meeting.config.receiveReactions = true;
|
|
1585
|
+
const fakeSendersName = 'Fake reactors name';
|
|
1586
|
+
const fakeReactionPayload = {
|
|
1587
|
+
type: 'fake_type',
|
|
1588
|
+
codepoints: 'fake_codepoints',
|
|
1589
|
+
shortcodes: 'fake_shortcodes',
|
|
1590
|
+
tone: {
|
|
1591
|
+
type: 'fake_tone_type',
|
|
1592
|
+
codepoints: 'fake_tone_codepoints',
|
|
1593
|
+
shortcodes: 'fake_tone_shortcodes',
|
|
1594
|
+
},
|
|
1595
|
+
};
|
|
1596
|
+
const fakeSenderPayload = {
|
|
1597
|
+
participantId: 'fake_participant_id',
|
|
1598
|
+
};
|
|
1599
|
+
const fakeProcessedReaction = {
|
|
1600
|
+
reaction: fakeReactionPayload,
|
|
1601
|
+
sender: {
|
|
1602
|
+
id: fakeSenderPayload.participantId,
|
|
1603
|
+
name: fakeSendersName,
|
|
1604
|
+
},
|
|
1605
|
+
};
|
|
1606
|
+
const fakeRelayEvent = {
|
|
1607
|
+
data: {
|
|
1608
|
+
relayType: REACTION_RELAY_TYPES.REACTION,
|
|
1609
|
+
reaction: fakeReactionPayload,
|
|
1610
|
+
sender: fakeSenderPayload,
|
|
1611
|
+
},
|
|
1612
|
+
};
|
|
1613
|
+
meeting.processRelayEvent(fakeRelayEvent);
|
|
1614
|
+
assert.calledWith(
|
|
1615
|
+
LoggerProxy.logger.warn,
|
|
1616
|
+
`Meeting:index#processRelayEvent --> Skipping handling of react for ${meeting.id}. participantId fake_participant_id does not exist in membersCollection.`
|
|
1617
|
+
);
|
|
1618
|
+
assert.neverCalledWith(
|
|
1619
|
+
TriggerProxy.trigger,
|
|
1620
|
+
sinon.match.instanceOf(Meeting),
|
|
1621
|
+
{
|
|
1622
|
+
file: 'meeting/index',
|
|
1623
|
+
function: 'join',
|
|
1624
|
+
},
|
|
1625
|
+
EVENT_TRIGGERS.MEETING_RECEIVE_REACTIONS,
|
|
1626
|
+
fakeProcessedReaction
|
|
1627
|
+
);
|
|
1628
|
+
});
|
|
1569
1629
|
});
|
|
1570
1630
|
|
|
1571
1631
|
describe('#handleLLMOnline', () => {
|
|
@@ -3957,6 +4017,7 @@ describe('plugin-meetings', () => {
|
|
|
3957
4017
|
initiateOffer: sinon.stub().resolves({}),
|
|
3958
4018
|
update: sinon.stub().resolves({}),
|
|
3959
4019
|
on: sinon.stub(),
|
|
4020
|
+
roapMessageReceived: sinon.stub()
|
|
3960
4021
|
};
|
|
3961
4022
|
|
|
3962
4023
|
fakeMultistreamRoapMediaConnection = {
|
|
@@ -4043,8 +4104,10 @@ describe('plugin-meetings', () => {
|
|
|
4043
4104
|
};
|
|
4044
4105
|
|
|
4045
4106
|
// simulates a Roap offer being generated by the RoapMediaConnection
|
|
4046
|
-
const simulateRoapOffer = async () => {
|
|
4047
|
-
|
|
4107
|
+
const simulateRoapOffer = async (stubWaitingForAnswer = true) => {
|
|
4108
|
+
if (stubWaitingForAnswer) {
|
|
4109
|
+
meeting.deferSDPAnswer = {resolve: sinon.stub()};
|
|
4110
|
+
}
|
|
4048
4111
|
const roapListener = getRoapListener();
|
|
4049
4112
|
|
|
4050
4113
|
await roapListener({roapMessage: roapOfferMessage});
|
|
@@ -4153,8 +4216,9 @@ describe('plugin-meetings', () => {
|
|
|
4153
4216
|
remoteQualityLevel,
|
|
4154
4217
|
expectedDebugId,
|
|
4155
4218
|
meetingId,
|
|
4219
|
+
expectMultistream = isMultistream,
|
|
4156
4220
|
}) => {
|
|
4157
|
-
if (
|
|
4221
|
+
if (expectMultistream) {
|
|
4158
4222
|
const {iceServers} = mediaConnectionConfig;
|
|
4159
4223
|
|
|
4160
4224
|
assert.calledOnceWithMatch(
|
|
@@ -4978,6 +5042,211 @@ describe('plugin-meetings', () => {
|
|
|
4978
5042
|
assert.notCalled(fakeRoapMediaConnection.update);
|
|
4979
5043
|
})
|
|
4980
5044
|
);
|
|
5045
|
+
|
|
5046
|
+
if (isMultistream) {
|
|
5047
|
+
describe('fallback from multistream to transcoded', () => {
|
|
5048
|
+
let multistreamEventListeners;
|
|
5049
|
+
let transcodedEventListeners;
|
|
5050
|
+
let mockStatsAnalyzerCtor;
|
|
5051
|
+
|
|
5052
|
+
const setupFakeRoapMediaConnection = (fakeRoapMediaConnection, eventListeners) => {
|
|
5053
|
+
fakeRoapMediaConnection.on.callsFake((eventName, cb) => {
|
|
5054
|
+
eventListeners[eventName] = cb;
|
|
5055
|
+
});
|
|
5056
|
+
fakeRoapMediaConnection.initiateOffer.callsFake(() => {
|
|
5057
|
+
// simulate offer being generated
|
|
5058
|
+
eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED]();
|
|
5059
|
+
|
|
5060
|
+
return Promise.resolve();
|
|
5061
|
+
});
|
|
5062
|
+
};
|
|
5063
|
+
|
|
5064
|
+
beforeEach(() => {
|
|
5065
|
+
multistreamEventListeners = {};
|
|
5066
|
+
transcodedEventListeners = {};
|
|
5067
|
+
|
|
5068
|
+
meeting.config.stats.enableStatsAnalyzer = true;
|
|
5069
|
+
|
|
5070
|
+
setupFakeRoapMediaConnection(fakeRoapMediaConnection, transcodedEventListeners);
|
|
5071
|
+
setupFakeRoapMediaConnection(
|
|
5072
|
+
fakeMultistreamRoapMediaConnection,
|
|
5073
|
+
multistreamEventListeners
|
|
5074
|
+
);
|
|
5075
|
+
|
|
5076
|
+
mockStatsAnalyzerCtor = sinon
|
|
5077
|
+
.stub(InternalMediaCoreModule, 'StatsAnalyzer')
|
|
5078
|
+
.callsFake(() => {
|
|
5079
|
+
return {on: sinon.stub(), stopAnalyzer: sinon.stub()};
|
|
5080
|
+
});
|
|
5081
|
+
|
|
5082
|
+
webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
|
|
5083
|
+
sinon.stub();
|
|
5084
|
+
|
|
5085
|
+
// setup the mock so that we get an SDP answer not from Homer
|
|
5086
|
+
locusMediaRequestStub.callsFake(() => {
|
|
5087
|
+
return Promise.resolve({
|
|
5088
|
+
body: {
|
|
5089
|
+
locus: {},
|
|
5090
|
+
mediaConnections: [
|
|
5091
|
+
{
|
|
5092
|
+
remoteSdp:
|
|
5093
|
+
'{"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"}',
|
|
5094
|
+
},
|
|
5095
|
+
],
|
|
5096
|
+
},
|
|
5097
|
+
});
|
|
5098
|
+
});
|
|
5099
|
+
|
|
5100
|
+
sinon.stub(meeting, 'closePeerConnections');
|
|
5101
|
+
sinon.stub(meeting.mediaProperties, 'unsetPeerConnection');
|
|
5102
|
+
sinon.stub(meeting.locusMediaRequest, 'downgradeFromMultistreamToTranscoded');
|
|
5103
|
+
});
|
|
5104
|
+
|
|
5105
|
+
const runCheck = async (turnServerInfo, forceTurnDiscovery) => {
|
|
5106
|
+
// we're calling addMediaInternal() with mic stream,
|
|
5107
|
+
// so that we also verify that audioMute, videoMute info is correctly sent to backend
|
|
5108
|
+
const addMediaPromise = meeting.addMediaInternal(
|
|
5109
|
+
() => '',
|
|
5110
|
+
turnServerInfo,
|
|
5111
|
+
forceTurnDiscovery,
|
|
5112
|
+
{
|
|
5113
|
+
localStreams: {microphone: fakeMicrophoneStream},
|
|
5114
|
+
}
|
|
5115
|
+
);
|
|
5116
|
+
await testUtils.flushPromises();
|
|
5117
|
+
await simulateRoapOffer(false);
|
|
5118
|
+
|
|
5119
|
+
// check MultistreamRoapMediaConnection was created correctly
|
|
5120
|
+
checkMediaConnectionCreated({
|
|
5121
|
+
expectMultistream: true,
|
|
5122
|
+
mediaConnectionConfig: expectedMediaConnectionConfig,
|
|
5123
|
+
localStreams: {
|
|
5124
|
+
audio: fakeMicrophoneStream,
|
|
5125
|
+
video: undefined,
|
|
5126
|
+
screenShareVideo: undefined,
|
|
5127
|
+
screenShareAudio: undefined,
|
|
5128
|
+
},
|
|
5129
|
+
direction: {
|
|
5130
|
+
audio: 'sendrecv',
|
|
5131
|
+
video: 'sendrecv',
|
|
5132
|
+
screenShare: 'recvonly',
|
|
5133
|
+
},
|
|
5134
|
+
remoteQualityLevel: 'HIGH',
|
|
5135
|
+
expectedDebugId,
|
|
5136
|
+
meetingId: meeting.id,
|
|
5137
|
+
});
|
|
5138
|
+
|
|
5139
|
+
// 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
|
|
5140
|
+
assert.calledOnceWithExactly(
|
|
5141
|
+
mockStatsAnalyzerCtor,
|
|
5142
|
+
sinon.match({
|
|
5143
|
+
isMultistream: true,
|
|
5144
|
+
})
|
|
5145
|
+
);
|
|
5146
|
+
const initialStatsAnalyzer = mockStatsAnalyzerCtor.returnValues[0];
|
|
5147
|
+
mockStatsAnalyzerCtor.resetHistory();
|
|
5148
|
+
|
|
5149
|
+
// TURN discovery was done (if needed)
|
|
5150
|
+
if (turnServerInfo) {
|
|
5151
|
+
assert.notCalled(meeting.roap.doTurnDiscovery);
|
|
5152
|
+
} else {
|
|
5153
|
+
assert.calledWith(meeting.roap.doTurnDiscovery, meeting, false, false);
|
|
5154
|
+
}
|
|
5155
|
+
|
|
5156
|
+
// and SDP offer was sent with the right audioMuted/videoMuted values
|
|
5157
|
+
checkSdpOfferSent({audioMuted: false, videoMuted: true});
|
|
5158
|
+
|
|
5159
|
+
await testUtils.flushPromises();
|
|
5160
|
+
|
|
5161
|
+
// at this point the meeting should have been downgraded to transcoded
|
|
5162
|
+
assert.equal(meeting.isMultistream, false);
|
|
5163
|
+
|
|
5164
|
+
// old stats analyzer stopped and new one created
|
|
5165
|
+
assert.calledOnce(initialStatsAnalyzer.stopAnalyzer);
|
|
5166
|
+
assert.calledOnceWithExactly(
|
|
5167
|
+
mockStatsAnalyzerCtor,
|
|
5168
|
+
sinon.match({
|
|
5169
|
+
isMultistream: false,
|
|
5170
|
+
})
|
|
5171
|
+
);
|
|
5172
|
+
|
|
5173
|
+
// and correct cleanup of other things should have been done
|
|
5174
|
+
assert.calledOnceWithExactly(meeting.closePeerConnections, false);
|
|
5175
|
+
assert.calledOnceWithExactly(meeting.mediaProperties.unsetPeerConnection);
|
|
5176
|
+
assert.calledOnceWithExactly(
|
|
5177
|
+
meeting.locusMediaRequest.downgradeFromMultistreamToTranscoded
|
|
5178
|
+
);
|
|
5179
|
+
|
|
5180
|
+
// new connection should have been created
|
|
5181
|
+
checkMediaConnectionCreated({
|
|
5182
|
+
expectMultistream: false,
|
|
5183
|
+
mediaConnectionConfig: expectedMediaConnectionConfig,
|
|
5184
|
+
localStreams: {
|
|
5185
|
+
audio: fakeMicrophoneStream,
|
|
5186
|
+
video: undefined,
|
|
5187
|
+
screenShareVideo: undefined,
|
|
5188
|
+
screenShareAudio: undefined,
|
|
5189
|
+
},
|
|
5190
|
+
direction: {
|
|
5191
|
+
audio: 'sendrecv',
|
|
5192
|
+
video: 'sendrecv',
|
|
5193
|
+
screenShare: 'recvonly',
|
|
5194
|
+
},
|
|
5195
|
+
remoteQualityLevel: 'HIGH',
|
|
5196
|
+
expectedDebugId,
|
|
5197
|
+
meetingId: meeting.id,
|
|
5198
|
+
});
|
|
5199
|
+
|
|
5200
|
+
// and new TURN discovery done (no matter if it was being done before or not)
|
|
5201
|
+
assert.calledWith(meeting.roap.doTurnDiscovery, meeting, true, true);
|
|
5202
|
+
|
|
5203
|
+
// simulate new offer
|
|
5204
|
+
await simulateRoapOffer(false);
|
|
5205
|
+
checkSdpOfferSent({audioMuted: false, videoMuted: true});
|
|
5206
|
+
|
|
5207
|
+
// overall there should have been 2 calls to locusMediaRequestStub, because 2 offers were sent
|
|
5208
|
+
assert.calledTwice(locusMediaRequestStub);
|
|
5209
|
+
|
|
5210
|
+
// simulate answer being processed correctly
|
|
5211
|
+
transcodedEventListeners[MediaConnectionEventNames.REMOTE_SDP_ANSWER_PROCESSED]();
|
|
5212
|
+
|
|
5213
|
+
// check that addMedia finally resolved
|
|
5214
|
+
await addMediaPromise;
|
|
5215
|
+
};
|
|
5216
|
+
|
|
5217
|
+
it('addMedia() falls back to transcoded if SDP answer is not from Homer', async () => {
|
|
5218
|
+
// call addMediaInternal like addMedia() does it
|
|
5219
|
+
await runCheck(undefined, false);
|
|
5220
|
+
});
|
|
5221
|
+
|
|
5222
|
+
it('addMediaInternal() correctly falls back to transcoded if SDP answer is not from Homer (joinWithMedia() case)', async () => {
|
|
5223
|
+
// call addMediaInternal the way joinWithMedia() does it - with TURN info already provided
|
|
5224
|
+
// and check that when we fallback to transcoded we still do another TURN discovery
|
|
5225
|
+
await runCheck(
|
|
5226
|
+
{
|
|
5227
|
+
url: 'turns:turn-server-url:443?transport=tcp',
|
|
5228
|
+
username: 'turn user',
|
|
5229
|
+
password: 'turn password',
|
|
5230
|
+
},
|
|
5231
|
+
false
|
|
5232
|
+
);
|
|
5233
|
+
});
|
|
5234
|
+
|
|
5235
|
+
it('addMediaInternal() correctly falls back to transcoded if SDP answer is not from Homer (joinWithMedia() retry case)', async () => {
|
|
5236
|
+
// call addMediaInternal the way joinWithMedia() does it when it does a retry - with TURN info already provided
|
|
5237
|
+
// but also with forceTurnDiscovery=true - this shouldn't affect the flow for fallback to transcoded in any way
|
|
5238
|
+
// but doing it just for completeness
|
|
5239
|
+
await runCheck(
|
|
5240
|
+
{
|
|
5241
|
+
url: 'turns:turn-server-url:443?transport=tcp',
|
|
5242
|
+
username: 'turn user',
|
|
5243
|
+
password: 'turn password',
|
|
5244
|
+
},
|
|
5245
|
+
true
|
|
5246
|
+
);
|
|
5247
|
+
});
|
|
5248
|
+
});
|
|
5249
|
+
}
|
|
4981
5250
|
})
|
|
4982
5251
|
);
|
|
4983
5252
|
|
|
@@ -5055,6 +5324,11 @@ describe('plugin-meetings', () => {
|
|
|
5055
5324
|
meeting.logger.error = sinon.stub().returns(true);
|
|
5056
5325
|
meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
|
|
5057
5326
|
webex.internal.voicea.off = sinon.stub().returns(true);
|
|
5327
|
+
meeting.stopTranscription = sinon.stub();
|
|
5328
|
+
meeting.transcription = {};
|
|
5329
|
+
|
|
5330
|
+
meeting.annotation.deregisterEvents = sinon.stub();
|
|
5331
|
+
webex.internal.llm.off = sinon.stub();
|
|
5058
5332
|
|
|
5059
5333
|
// A meeting needs to be joined to leave
|
|
5060
5334
|
meeting.meetingState = 'ACTIVE';
|
|
@@ -5075,6 +5349,9 @@ describe('plugin-meetings', () => {
|
|
|
5075
5349
|
assert.calledOnce(meeting.closePeerConnections);
|
|
5076
5350
|
assert.calledOnce(meeting.unsetRemoteStreams);
|
|
5077
5351
|
assert.calledOnce(meeting.unsetPeerConnections);
|
|
5352
|
+
assert.calledOnce(meeting.stopTranscription);
|
|
5353
|
+
assert.calledOnce(meeting.annotation.deregisterEvents);
|
|
5354
|
+
assert.calledWith(webex.internal.llm.off, 'event:relay.event', meeting.processRelayEvent);
|
|
5078
5355
|
});
|
|
5079
5356
|
|
|
5080
5357
|
it('should reset call diagnostic latencies correctly', async () => {
|
|
@@ -6957,6 +7234,9 @@ describe('plugin-meetings', () => {
|
|
|
6957
7234
|
meeting.transcription = {};
|
|
6958
7235
|
meeting.stopTranscription = sinon.stub();
|
|
6959
7236
|
|
|
7237
|
+
meeting.annotation.deregisterEvents = sinon.stub();
|
|
7238
|
+
webex.internal.llm.off = sinon.stub();
|
|
7239
|
+
|
|
6960
7240
|
// A meeting needs to be joined to end
|
|
6961
7241
|
meeting.meetingState = 'ACTIVE';
|
|
6962
7242
|
meeting.state = 'JOINED';
|
|
@@ -6977,6 +7257,9 @@ describe('plugin-meetings', () => {
|
|
|
6977
7257
|
assert.calledOnce(meeting?.unsetRemoteStreams);
|
|
6978
7258
|
assert.calledOnce(meeting?.unsetPeerConnections);
|
|
6979
7259
|
assert.calledOnce(meeting?.stopTranscription);
|
|
7260
|
+
|
|
7261
|
+
assert.called(meeting.annotation.deregisterEvents);
|
|
7262
|
+
assert.calledWith(webex.internal.llm.off, 'event:relay.event', meeting.processRelayEvent);
|
|
6980
7263
|
});
|
|
6981
7264
|
});
|
|
6982
7265
|
|
|
@@ -8552,8 +8835,7 @@ describe('plugin-meetings', () => {
|
|
|
8552
8835
|
assert.calledWith(meeting.roapMessageReceived, fakeAnswer);
|
|
8553
8836
|
});
|
|
8554
8837
|
|
|
8555
|
-
|
|
8556
|
-
const fakeError = new Error('fake error');
|
|
8838
|
+
const runOfferSendingFailureTest = async (fakeError, canProceed, expectedErrorCode) => {
|
|
8557
8839
|
const clock = sinon.useFakeTimers();
|
|
8558
8840
|
sinon.spy(clock, 'clearTimeout');
|
|
8559
8841
|
meeting.deferSDPAnswer = {reject: sinon.stub()};
|
|
@@ -8591,19 +8873,31 @@ describe('plugin-meetings', () => {
|
|
|
8591
8873
|
assert.equal(meeting.sdpResponseTimer, undefined);
|
|
8592
8874
|
|
|
8593
8875
|
assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
|
|
8594
|
-
clientErrorCode:
|
|
8876
|
+
clientErrorCode: expectedErrorCode,
|
|
8595
8877
|
});
|
|
8596
8878
|
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
|
|
8597
8879
|
name: 'client.media-engine.remote-sdp-received',
|
|
8598
8880
|
payload: {
|
|
8599
|
-
canProceed
|
|
8600
|
-
errors: [{errorCode:
|
|
8881
|
+
canProceed,
|
|
8882
|
+
errors: [{errorCode: expectedErrorCode, fatal: true}],
|
|
8601
8883
|
},
|
|
8602
8884
|
options: {
|
|
8603
8885
|
meetingId: meeting.id,
|
|
8604
8886
|
rawError: fakeError,
|
|
8605
8887
|
},
|
|
8606
8888
|
});
|
|
8889
|
+
};
|
|
8890
|
+
|
|
8891
|
+
it('handles OFFER message correctly when request fails', async () => {
|
|
8892
|
+
const fakeError = new Error('fake error');
|
|
8893
|
+
|
|
8894
|
+
await runOfferSendingFailureTest(fakeError, false, 2007);
|
|
8895
|
+
});
|
|
8896
|
+
|
|
8897
|
+
it('handles OFFER message correctly when we get a non-homer answer', async () => {
|
|
8898
|
+
const fakeError = new MultistreamNotSupportedError();
|
|
8899
|
+
|
|
8900
|
+
await runOfferSendingFailureTest(fakeError, true, 2012);
|
|
8607
8901
|
});
|
|
8608
8902
|
|
|
8609
8903
|
it('handles ANSWER message correctly', () => {
|
|
@@ -9703,14 +9997,39 @@ describe('plugin-meetings', () => {
|
|
|
9703
9997
|
it('should close the webrtc media connection, and return a promise', async () => {
|
|
9704
9998
|
const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
|
|
9705
9999
|
meeting.mediaProperties.webrtcMediaConnection = {close: sinon.stub()};
|
|
10000
|
+
|
|
10001
|
+
meeting.audio = {id: 'fakeAudioMuteState'};
|
|
10002
|
+
meeting.video = {id: 'fakeVideoMuteState'};
|
|
10003
|
+
|
|
9706
10004
|
const pcs = meeting.closePeerConnections();
|
|
9707
10005
|
|
|
9708
10006
|
assert.exists(pcs.then);
|
|
9709
10007
|
await pcs;
|
|
9710
10008
|
assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.close);
|
|
9711
10009
|
assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
|
|
10010
|
+
assert.equal(meeting.audio, null);
|
|
10011
|
+
assert.equal(meeting.video, null);
|
|
10012
|
+
});
|
|
10013
|
+
|
|
10014
|
+
it('should close the webrtc media connection, but keep audio and video props unchanged if called with resetMuteStates=false', async () => {
|
|
10015
|
+
const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
|
|
10016
|
+
meeting.mediaProperties.webrtcMediaConnection = {close: sinon.stub()};
|
|
10017
|
+
|
|
10018
|
+
const fakeAudio = {id: 'fakeAudioMuteState'};
|
|
10019
|
+
const fakeVideo = {id: 'fakeVideoMuteState'};
|
|
10020
|
+
|
|
10021
|
+
meeting.audio = fakeAudio;
|
|
10022
|
+
meeting.video = fakeVideo;
|
|
10023
|
+
|
|
10024
|
+
await meeting.closePeerConnections(false);
|
|
10025
|
+
|
|
10026
|
+
assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.close);
|
|
10027
|
+
assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
|
|
10028
|
+
assert.equal(meeting.audio, fakeAudio);
|
|
10029
|
+
assert.equal(meeting.video, fakeVideo);
|
|
9712
10030
|
});
|
|
9713
10031
|
});
|
|
10032
|
+
|
|
9714
10033
|
describe('#unsetPeerConnections', () => {
|
|
9715
10034
|
it('should unset the peer connections', () => {
|
|
9716
10035
|
meeting.mediaProperties.unsetPeerConnection = sinon.stub().returns(true);
|
|
@@ -12301,9 +12620,12 @@ describe('plugin-meetings', () => {
|
|
|
12301
12620
|
it('startKeepAlive starts the keep alive', async () => {
|
|
12302
12621
|
meeting.meetingRequest.keepAlive = sinon.stub().returns(Promise.resolve());
|
|
12303
12622
|
|
|
12623
|
+
const keepAliveUrl1 = 'keep.alive.url1';
|
|
12624
|
+
const keepAliveUrl2 = 'keep.alive.url2';
|
|
12625
|
+
|
|
12304
12626
|
assert.isNull(meeting.keepAliveTimerId);
|
|
12305
12627
|
meeting.joinedWith = {
|
|
12306
|
-
keepAliveUrl:
|
|
12628
|
+
keepAliveUrl: keepAliveUrl1,
|
|
12307
12629
|
keepAliveSecs: defaultKeepAliveSecs,
|
|
12308
12630
|
};
|
|
12309
12631
|
meeting.startKeepAlive();
|
|
@@ -12312,12 +12634,15 @@ describe('plugin-meetings', () => {
|
|
|
12312
12634
|
assert.notCalled(meeting.meetingRequest.keepAlive);
|
|
12313
12635
|
await progressTime(defaultExpectedInterval);
|
|
12314
12636
|
assert.calledOnceWithExactly(meeting.meetingRequest.keepAlive, {
|
|
12315
|
-
keepAliveUrl:
|
|
12637
|
+
keepAliveUrl: keepAliveUrl1,
|
|
12316
12638
|
});
|
|
12639
|
+
// joinedWith keep alive url might change (when we fallback from multistream to transcoded)
|
|
12640
|
+
meeting.joinedWith.keepAliveUrl = keepAliveUrl2;
|
|
12641
|
+
|
|
12317
12642
|
await progressTime(defaultExpectedInterval);
|
|
12318
12643
|
assert.calledTwice(meeting.meetingRequest.keepAlive);
|
|
12319
|
-
assert.
|
|
12320
|
-
keepAliveUrl:
|
|
12644
|
+
assert.calledWith(meeting.meetingRequest.keepAlive, {
|
|
12645
|
+
keepAliveUrl: keepAliveUrl2,
|
|
12321
12646
|
});
|
|
12322
12647
|
});
|
|
12323
12648
|
it('startKeepAlive handles existing keepAliveTimerId', async () => {
|
|
@@ -12915,5 +13240,26 @@ describe('plugin-meetings', () => {
|
|
|
12915
13240
|
assert.calledOnceWithExactly(getMediaServer, 'fake sdp');
|
|
12916
13241
|
assert.equal(meeting.mediaProperties.webrtcMediaConnection.mediaServer, 'homer');
|
|
12917
13242
|
});
|
|
13243
|
+
|
|
13244
|
+
it('throws MultistreamNotSupportedError if we get a non-homer SDP answer', async () => {
|
|
13245
|
+
const fakeMessage = {messageType: 'ANSWER', sdp: 'fake sdp'};
|
|
13246
|
+
|
|
13247
|
+
meeting.isMultistream = true;
|
|
13248
|
+
meeting.mediaProperties.webrtcMediaConnection = {
|
|
13249
|
+
roapMessageReceived: sinon.stub(),
|
|
13250
|
+
};
|
|
13251
|
+
|
|
13252
|
+
sinon.stub(MeetingsUtil, 'getMediaServer').returns('linus');
|
|
13253
|
+
|
|
13254
|
+
try {
|
|
13255
|
+
await meeting.roapMessageReceived(fakeMessage);
|
|
13256
|
+
|
|
13257
|
+
assert.fail('Expected MultistreamNotSupportedError to be thrown');
|
|
13258
|
+
} catch(e) {
|
|
13259
|
+
assert.isTrue(e instanceof MultistreamNotSupportedError);
|
|
13260
|
+
}
|
|
13261
|
+
|
|
13262
|
+
assert.notCalled(meeting.mediaProperties.webrtcMediaConnection.roapMessageReceived);
|
|
13263
|
+
});
|
|
12918
13264
|
});
|
|
12919
13265
|
});
|
|
@@ -290,4 +290,14 @@ describe('plugin-meetings', () => {
|
|
|
290
290
|
assert.equal(MeetingsUtil.isValidBreakoutLocus(newLocus), true);
|
|
291
291
|
});
|
|
292
292
|
});
|
|
293
|
+
|
|
294
|
+
describe('#getMediaServer', () => {
|
|
295
|
+
it('returns the contents of o-line lower cased', () => {
|
|
296
|
+
const sdp1 = 'v=0\r\no=homer 0 1 IN IP4 23.89.67.81\r\ns=-\r\nc=IN IP4 23.89.67.81\r\nb=TIAS:128000\r\nt=0 0\r\na=ice-lite\r\n'
|
|
297
|
+
assert.equal(MeetingsUtil.getMediaServer(sdp1), 'homer');
|
|
298
|
+
|
|
299
|
+
const sdp2 = 'v=0\r\no=HOMER 0 1 IN IP4 23.89.67.81\r\ns=-\r\nc=IN IP4 23.89.67.81\r\nb=TIAS:128000\r\nt=0 0\r\na=ice-lite\r\n'
|
|
300
|
+
assert.equal(MeetingsUtil.getMediaServer(sdp2), 'homer');
|
|
301
|
+
});
|
|
302
|
+
})
|
|
293
303
|
});
|
|
@@ -251,6 +251,53 @@ describe('Roap', () => {
|
|
|
251
251
|
);
|
|
252
252
|
});
|
|
253
253
|
|
|
254
|
+
it('handles the case when there is some other (not an answer) roap message type in the http response', async () => {
|
|
255
|
+
const roapError = {
|
|
256
|
+
seq: 1,
|
|
257
|
+
messageType: 'ERROR',
|
|
258
|
+
sdps: [],
|
|
259
|
+
errorType: 'error type',
|
|
260
|
+
errorCause: 'error cause',
|
|
261
|
+
headers: ['header1', 'header2'],
|
|
262
|
+
};
|
|
263
|
+
const fakeMediaConnections = [
|
|
264
|
+
{
|
|
265
|
+
remoteSdp: JSON.stringify({
|
|
266
|
+
roapMessage: roapError,
|
|
267
|
+
}),
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
sendRoapStub.resolves({
|
|
272
|
+
mediaConnections: fakeMediaConnections,
|
|
273
|
+
locus: fakeLocus,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const result = await roap.sendRoapMediaRequest({
|
|
277
|
+
meeting,
|
|
278
|
+
sdp: 'sdp',
|
|
279
|
+
reconnect: false,
|
|
280
|
+
seq: 1,
|
|
281
|
+
tieBreaker: 4294967294,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
assert.calledOnce(sendRoapStub);
|
|
285
|
+
assert.calledOnceWithExactly(meeting.updateMediaConnections, fakeMediaConnections);
|
|
286
|
+
assert.deepEqual(result, {
|
|
287
|
+
locus: fakeLocus,
|
|
288
|
+
roapAnswer: undefined,
|
|
289
|
+
});
|
|
290
|
+
assert.calledOnceWithExactly(
|
|
291
|
+
Metrics.sendBehavioralMetric,
|
|
292
|
+
BEHAVIORAL_METRICS.ROAP_HTTP_RESPONSE_MISSING,
|
|
293
|
+
{
|
|
294
|
+
correlationId: meeting.correlationId,
|
|
295
|
+
messageType: 'ANSWER',
|
|
296
|
+
isMultistream: meeting.isMultistream,
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
254
301
|
describe('does not crash when http response is missing things', () => {
|
|
255
302
|
[
|
|
256
303
|
{mediaConnections: undefined, title: 'mediaConnections are undefined'},
|