@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.
Files changed (36) hide show
  1. package/dist/annotation/index.js +17 -0
  2. package/dist/annotation/index.js.map +1 -1
  3. package/dist/breakouts/breakout.js +1 -1
  4. package/dist/breakouts/index.js +1 -1
  5. package/dist/common/errors/multistream-not-supported-error.js +53 -0
  6. package/dist/common/errors/multistream-not-supported-error.js.map +1 -0
  7. package/dist/constants.js +5 -0
  8. package/dist/constants.js.map +1 -1
  9. package/dist/interpretation/index.js +1 -1
  10. package/dist/interpretation/siLanguage.js +1 -1
  11. package/dist/meeting/index.js +256 -166
  12. package/dist/meeting/index.js.map +1 -1
  13. package/dist/meeting/locusMediaRequest.js +9 -0
  14. package/dist/meeting/locusMediaRequest.js.map +1 -1
  15. package/dist/meetings/util.js +1 -1
  16. package/dist/meetings/util.js.map +1 -1
  17. package/dist/roap/index.js +10 -8
  18. package/dist/roap/index.js.map +1 -1
  19. package/dist/types/annotation/index.d.ts +5 -0
  20. package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
  21. package/dist/types/constants.d.ts +5 -0
  22. package/dist/types/meeting/index.d.ts +11 -2
  23. package/dist/types/meeting/locusMediaRequest.d.ts +4 -0
  24. package/dist/webinar/index.js +1 -1
  25. package/package.json +21 -21
  26. package/src/annotation/index.ts +16 -0
  27. package/src/common/errors/multistream-not-supported-error.ts +30 -0
  28. package/src/constants.ts +5 -0
  29. package/src/meeting/index.ts +110 -27
  30. package/src/meeting/locusMediaRequest.ts +7 -0
  31. package/src/meetings/util.ts +2 -1
  32. package/src/roap/index.ts +10 -8
  33. package/test/unit/spec/annotation/index.ts +46 -1
  34. package/test/unit/spec/meeting/index.js +367 -21
  35. package/test/unit/spec/meetings/utils.js +10 -0
  36. 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().returns(Promise.resolve(fakeJoinResult));
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
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
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
- meeting.deferSDPAnswer = {resolve: sinon.stub()};
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 (isMultistream) {
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
- it('handles OFFER message correctly when request fails', async () => {
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: 2007,
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: false,
8600
- errors: [{errorCode: 2007, fatal: true}],
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: defaultKeepAliveUrl,
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: defaultKeepAliveUrl,
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.alwaysCalledWithExactly(meeting.meetingRequest.keepAlive, {
12320
- keepAliveUrl: defaultKeepAliveUrl,
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'},