@webex/plugin-meetings 3.7.0-next.6 → 3.7.0-next.61

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 (138) 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/join-forbidden-error.js +52 -0
  6. package/dist/common/errors/join-forbidden-error.js.map +1 -0
  7. package/dist/common/errors/{webinar-registration-error.js → join-webinar-error.js} +12 -12
  8. package/dist/common/errors/join-webinar-error.js.map +1 -0
  9. package/dist/common/errors/multistream-not-supported-error.js +53 -0
  10. package/dist/common/errors/multistream-not-supported-error.js.map +1 -0
  11. package/dist/config.js +1 -1
  12. package/dist/config.js.map +1 -1
  13. package/dist/constants.js +46 -5
  14. package/dist/constants.js.map +1 -1
  15. package/dist/index.js +16 -11
  16. package/dist/index.js.map +1 -1
  17. package/dist/interpretation/index.js +1 -1
  18. package/dist/interpretation/siLanguage.js +1 -1
  19. package/dist/locus-info/index.js +14 -3
  20. package/dist/locus-info/index.js.map +1 -1
  21. package/dist/locus-info/selfUtils.js +35 -17
  22. package/dist/locus-info/selfUtils.js.map +1 -1
  23. package/dist/meeting/brbState.js +167 -0
  24. package/dist/meeting/brbState.js.map +1 -0
  25. package/dist/meeting/in-meeting-actions.js +2 -0
  26. package/dist/meeting/in-meeting-actions.js.map +1 -1
  27. package/dist/meeting/index.js +774 -649
  28. package/dist/meeting/index.js.map +1 -1
  29. package/dist/meeting/locusMediaRequest.js +9 -0
  30. package/dist/meeting/locusMediaRequest.js.map +1 -1
  31. package/dist/meeting/muteState.js +1 -6
  32. package/dist/meeting/muteState.js.map +1 -1
  33. package/dist/meeting/request.js +30 -0
  34. package/dist/meeting/request.js.map +1 -1
  35. package/dist/meeting/request.type.js.map +1 -1
  36. package/dist/meeting/util.js +16 -16
  37. package/dist/meeting/util.js.map +1 -1
  38. package/dist/meeting-info/meeting-info-v2.js +96 -33
  39. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  40. package/dist/meeting-info/utilv2.js +1 -1
  41. package/dist/meeting-info/utilv2.js.map +1 -1
  42. package/dist/meetings/index.js +107 -55
  43. package/dist/meetings/index.js.map +1 -1
  44. package/dist/meetings/meetings.types.js +2 -0
  45. package/dist/meetings/meetings.types.js.map +1 -1
  46. package/dist/meetings/util.js +1 -1
  47. package/dist/meetings/util.js.map +1 -1
  48. package/dist/member/index.js +9 -0
  49. package/dist/member/index.js.map +1 -1
  50. package/dist/member/types.js.map +1 -1
  51. package/dist/member/util.js +39 -28
  52. package/dist/member/util.js.map +1 -1
  53. package/dist/metrics/constants.js +3 -2
  54. package/dist/metrics/constants.js.map +1 -1
  55. package/dist/multistream/remoteMedia.js +30 -15
  56. package/dist/multistream/remoteMedia.js.map +1 -1
  57. package/dist/multistream/sendSlotManager.js +24 -0
  58. package/dist/multistream/sendSlotManager.js.map +1 -1
  59. package/dist/reachability/index.js +31 -3
  60. package/dist/reachability/index.js.map +1 -1
  61. package/dist/roap/index.js +10 -8
  62. package/dist/roap/index.js.map +1 -1
  63. package/dist/types/annotation/index.d.ts +5 -0
  64. package/dist/types/common/errors/join-forbidden-error.d.ts +15 -0
  65. package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
  66. package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
  67. package/dist/types/constants.d.ts +38 -1
  68. package/dist/types/index.d.ts +3 -3
  69. package/dist/types/locus-info/index.d.ts +2 -1
  70. package/dist/types/meeting/brbState.d.ts +54 -0
  71. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  72. package/dist/types/meeting/index.d.ts +21 -12
  73. package/dist/types/meeting/locusMediaRequest.d.ts +4 -0
  74. package/dist/types/meeting/request.d.ts +12 -1
  75. package/dist/types/meeting/request.type.d.ts +6 -0
  76. package/dist/types/meeting/util.d.ts +1 -1
  77. package/dist/types/meeting-info/meeting-info-v2.d.ts +27 -4
  78. package/dist/types/meetings/index.d.ts +19 -1
  79. package/dist/types/meetings/meetings.types.d.ts +8 -0
  80. package/dist/types/member/index.d.ts +1 -0
  81. package/dist/types/member/types.d.ts +7 -0
  82. package/dist/types/metrics/constants.d.ts +2 -1
  83. package/dist/types/multistream/sendSlotManager.d.ts +8 -1
  84. package/dist/types/reachability/index.d.ts +9 -1
  85. package/dist/webinar/index.js +354 -3
  86. package/dist/webinar/index.js.map +1 -1
  87. package/package.json +23 -22
  88. package/src/annotation/index.ts +16 -0
  89. package/src/common/errors/join-forbidden-error.ts +26 -0
  90. package/src/common/errors/join-webinar-error.ts +24 -0
  91. package/src/common/errors/multistream-not-supported-error.ts +30 -0
  92. package/src/config.ts +1 -1
  93. package/src/constants.ts +43 -3
  94. package/src/index.ts +5 -3
  95. package/src/locus-info/index.ts +20 -3
  96. package/src/locus-info/selfUtils.ts +24 -6
  97. package/src/meeting/brbState.ts +169 -0
  98. package/src/meeting/in-meeting-actions.ts +4 -0
  99. package/src/meeting/index.ts +256 -82
  100. package/src/meeting/locusMediaRequest.ts +7 -0
  101. package/src/meeting/muteState.ts +1 -6
  102. package/src/meeting/request.ts +26 -1
  103. package/src/meeting/request.type.ts +7 -0
  104. package/src/meeting/util.ts +8 -10
  105. package/src/meeting-info/meeting-info-v2.ts +74 -11
  106. package/src/meeting-info/utilv2.ts +3 -1
  107. package/src/meetings/index.ts +79 -20
  108. package/src/meetings/meetings.types.ts +10 -0
  109. package/src/meetings/util.ts +2 -1
  110. package/src/member/index.ts +9 -0
  111. package/src/member/types.ts +8 -0
  112. package/src/member/util.ts +34 -24
  113. package/src/metrics/constants.ts +2 -1
  114. package/src/multistream/remoteMedia.ts +28 -15
  115. package/src/multistream/sendSlotManager.ts +31 -0
  116. package/src/reachability/index.ts +29 -1
  117. package/src/roap/index.ts +10 -8
  118. package/src/webinar/index.ts +197 -3
  119. package/test/unit/spec/annotation/index.ts +46 -1
  120. package/test/unit/spec/locus-info/index.js +292 -60
  121. package/test/unit/spec/locus-info/selfConstant.js +7 -0
  122. package/test/unit/spec/locus-info/selfUtils.js +101 -1
  123. package/test/unit/spec/meeting/brbState.ts +114 -0
  124. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  125. package/test/unit/spec/meeting/index.js +733 -106
  126. package/test/unit/spec/meeting/muteState.js +0 -24
  127. package/test/unit/spec/meeting/utils.js +22 -19
  128. package/test/unit/spec/meeting-info/meetinginfov2.js +46 -4
  129. package/test/unit/spec/meeting-info/utilv2.js +17 -0
  130. package/test/unit/spec/meetings/index.js +159 -18
  131. package/test/unit/spec/meetings/utils.js +10 -0
  132. package/test/unit/spec/member/util.js +52 -11
  133. package/test/unit/spec/multistream/remoteMedia.ts +11 -7
  134. package/test/unit/spec/reachability/index.ts +120 -10
  135. package/test/unit/spec/roap/index.ts +47 -0
  136. package/test/unit/spec/webinar/index.ts +457 -0
  137. package/dist/common/errors/webinar-registration-error.js.map +0 -1
  138. 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 WebinarRegistrationError from '../../../../src/common/errors/webinar-registration-error';
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
- MeetingInfoV2WebinarRegistrationError,
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,8 @@ 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 { createBrbState } from '@webex/plugin-meetings/src/meeting/brbState';
118
+ import JoinForbiddenError from '../../../../src/common/errors/join-forbidden-error';
116
119
 
117
120
  describe('plugin-meetings', () => {
118
121
  const logger = {
@@ -244,6 +247,7 @@ describe('plugin-meetings', () => {
244
247
  isAnyPublicClusterReachable: sinon.stub().resolves(true),
245
248
  getReachabilityResults: sinon.stub().resolves(undefined),
246
249
  getReachabilityMetrics: sinon.stub().resolves({}),
250
+ stopReachability: sinon.stub(),
247
251
  };
248
252
  webex.internal.llm.on = sinon.stub();
249
253
  webex.internal.newMetrics.callDiagnosticLatencies = new CallDiagnosticLatencies(
@@ -652,7 +656,7 @@ describe('plugin-meetings', () => {
652
656
  const fakeTurnServerInfo = {id: 'fake turn info'};
653
657
  const fakeJoinResult = {id: 'join result'};
654
658
 
655
- const joinOptions = {correlationId: '12345'};
659
+ const joinOptions = {correlationId: '12345', enableMultistream: true};
656
660
  const mediaOptions = {audioEnabled: true, allowMediaInLobby: true};
657
661
 
658
662
  let generateTurnDiscoveryRequestMessageStub;
@@ -661,7 +665,10 @@ describe('plugin-meetings', () => {
661
665
  let addMediaInternalStub;
662
666
 
663
667
  beforeEach(() => {
664
- meeting.join = sinon.stub().returns(Promise.resolve(fakeJoinResult));
668
+ meeting.join = sinon.stub().callsFake((joinOptions) => {
669
+ meeting.isMultistream = joinOptions.enableMultistream;
670
+ return Promise.resolve(fakeJoinResult)
671
+ });
665
672
  addMediaInternalStub = sinon
666
673
  .stub(meeting, 'addMediaInternal')
667
674
  .returns(Promise.resolve(test4));
@@ -700,7 +707,7 @@ describe('plugin-meetings', () => {
700
707
  mediaOptions
701
708
  );
702
709
 
703
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
710
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
704
711
 
705
712
  // resets joinWithMediaRetryInfo
706
713
  assert.deepEqual(meeting.joinWithMediaRetryInfo, {
@@ -733,7 +740,7 @@ describe('plugin-meetings', () => {
733
740
  mediaOptions
734
741
  );
735
742
 
736
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
743
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
737
744
  assert.equal(meeting.turnServerUsed, false);
738
745
  });
739
746
 
@@ -768,7 +775,7 @@ describe('plugin-meetings', () => {
768
775
  mediaOptions
769
776
  );
770
777
 
771
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
778
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
772
779
  });
773
780
 
774
781
  it('should reject if join() fails', async () => {
@@ -855,7 +862,8 @@ describe('plugin-meetings', () => {
855
862
  }
856
863
  );
857
864
 
858
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
865
+ // expect multistreamEnabled: false, because this test overrides the join meeting.join stub so it doesn't set the isMultistream flag
866
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: false});
859
867
 
860
868
  // resets joinWithMediaRetryInfo
861
869
  assert.deepEqual(meeting.joinWithMediaRetryInfo, {
@@ -944,7 +952,7 @@ describe('plugin-meetings', () => {
944
952
  mediaOptions,
945
953
  });
946
954
 
947
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
955
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
948
956
 
949
957
  assert.calledOnce(meeting.join);
950
958
  assert.notCalled(leaveStub);
@@ -1038,6 +1046,7 @@ describe('plugin-meetings', () => {
1038
1046
  getConnectionState: sinon.stub().returns(ConnectionState.Connected),
1039
1047
  initiateOffer: sinon.stub().resolves({}),
1040
1048
  on: sinon.stub(),
1049
+ createSendSlot: sinon.stub(),
1041
1050
  };
1042
1051
 
1043
1052
  /* Setup the stubs so that the first call to addMediaInternal() fails
@@ -1054,12 +1063,14 @@ describe('plugin-meetings', () => {
1054
1063
 
1055
1064
  sinon.stub(meeting.roap, 'doTurnDiscovery').resolves({turnServerInfo: 'fake turn info'});
1056
1065
 
1066
+ // calling joinWithMedia() with enableMultistream=false, because this test uses real addMediaInternal() implementation
1067
+ // and it requires less stubs when it's without multistream
1057
1068
  const result = await meeting.joinWithMedia({
1058
- joinOptions,
1069
+ joinOptions: {...joinOptions, enableMultistream: false},
1059
1070
  mediaOptions,
1060
1071
  });
1061
1072
 
1062
- assert.deepEqual(result, {join: fakeJoinResult, media: undefined});
1073
+ assert.deepEqual(result, {join: fakeJoinResult, media: undefined, multistreamEnabled: false});
1063
1074
 
1064
1075
  assert.calledOnce(meeting.join);
1065
1076
 
@@ -1134,6 +1145,7 @@ describe('plugin-meetings', () => {
1134
1145
  addMediaError.name = 'SdpOfferCreationError';
1135
1146
 
1136
1147
  meeting.addMediaInternal.rejects(addMediaError);
1148
+ sinon.stub(meeting, 'leave').resolves();
1137
1149
 
1138
1150
  await assert.isRejected(
1139
1151
  meeting.joinWithMedia({
@@ -1162,6 +1174,7 @@ describe('plugin-meetings', () => {
1162
1174
  type: addMediaError.name,
1163
1175
  }
1164
1176
  );
1177
+ assert.calledOnceWithExactly(meeting.leave, {resourceId: undefined, reason: 'joinWithMedia failure'})
1165
1178
  });
1166
1179
  });
1167
1180
 
@@ -1238,6 +1251,7 @@ describe('plugin-meetings', () => {
1238
1251
  webex.internal.voicea.off = sinon.stub();
1239
1252
  webex.internal.voicea.listenToEvents = sinon.stub();
1240
1253
  webex.internal.voicea.turnOnCaptions = sinon.stub();
1254
+ webex.internal.voicea.deregisterEvents = sinon.stub();
1241
1255
  });
1242
1256
 
1243
1257
  it('should stop listening to voicea events and also trigger a stop event', () => {
@@ -1566,6 +1580,55 @@ describe('plugin-meetings', () => {
1566
1580
  fakeProcessedReaction
1567
1581
  );
1568
1582
  });
1583
+
1584
+ it('should fail quietly if participantId does not exist in membersCollection', () => {
1585
+ LoggerProxy.logger.warn = sinon.stub();
1586
+ meeting.isReactionsSupported = sinon.stub().returns(true);
1587
+ meeting.config.receiveReactions = true;
1588
+ const fakeSendersName = 'Fake reactors name';
1589
+ const fakeReactionPayload = {
1590
+ type: 'fake_type',
1591
+ codepoints: 'fake_codepoints',
1592
+ shortcodes: 'fake_shortcodes',
1593
+ tone: {
1594
+ type: 'fake_tone_type',
1595
+ codepoints: 'fake_tone_codepoints',
1596
+ shortcodes: 'fake_tone_shortcodes',
1597
+ },
1598
+ };
1599
+ const fakeSenderPayload = {
1600
+ participantId: 'fake_participant_id',
1601
+ };
1602
+ const fakeProcessedReaction = {
1603
+ reaction: fakeReactionPayload,
1604
+ sender: {
1605
+ id: fakeSenderPayload.participantId,
1606
+ name: fakeSendersName,
1607
+ },
1608
+ };
1609
+ const fakeRelayEvent = {
1610
+ data: {
1611
+ relayType: REACTION_RELAY_TYPES.REACTION,
1612
+ reaction: fakeReactionPayload,
1613
+ sender: fakeSenderPayload,
1614
+ },
1615
+ };
1616
+ meeting.processRelayEvent(fakeRelayEvent);
1617
+ assert.calledWith(
1618
+ LoggerProxy.logger.warn,
1619
+ `Meeting:index#processRelayEvent --> Skipping handling of react for ${meeting.id}. participantId fake_participant_id does not exist in membersCollection.`
1620
+ );
1621
+ assert.neverCalledWith(
1622
+ TriggerProxy.trigger,
1623
+ sinon.match.instanceOf(Meeting),
1624
+ {
1625
+ file: 'meeting/index',
1626
+ function: 'join',
1627
+ },
1628
+ EVENT_TRIGGERS.MEETING_RECEIVE_REACTIONS,
1629
+ fakeProcessedReaction
1630
+ );
1631
+ });
1569
1632
  });
1570
1633
 
1571
1634
  describe('#handleLLMOnline', () => {
@@ -1705,6 +1768,12 @@ describe('plugin-meetings', () => {
1705
1768
  sinon.assert.called(setCorrelationIdSpy);
1706
1769
  assert.equal(meeting.correlationId, '123');
1707
1770
  });
1771
+
1772
+ it('should not send client.call.initiated if told not to', async () => {
1773
+ await meeting.join({sendCallInitiated: false});
1774
+
1775
+ sinon.assert.notCalled(webex.internal.newMetrics.submitClientEvent);
1776
+ });
1708
1777
  });
1709
1778
 
1710
1779
  describe('failure', () => {
@@ -2028,6 +2097,7 @@ describe('plugin-meetings', () => {
2028
2097
  someReachabilityMetric1: 'some value1',
2029
2098
  someReachabilityMetric2: 'some value2',
2030
2099
  }),
2100
+ stopReachability: sinon.stub(),
2031
2101
  };
2032
2102
 
2033
2103
  const forceRtcMetricsSend = sinon.stub().resolves();
@@ -2447,6 +2517,7 @@ describe('plugin-meetings', () => {
2447
2517
  assert.calledOnce(meeting.setMercuryListener);
2448
2518
  assert.calledOnce(fakeMediaConnection.initiateOffer);
2449
2519
  assert.equal(meeting.allowMediaInLobby, allowMediaInLobby);
2520
+ assert.calledOnce(webex.meetings.reachability.stopReachability);
2450
2521
  };
2451
2522
 
2452
2523
  it('should attach the media and return promise', async () => {
@@ -2492,9 +2563,11 @@ describe('plugin-meetings', () => {
2492
2563
  mediaSettings: {},
2493
2564
  });
2494
2565
 
2495
- const checkLogCounter = (delay, expectedCounter) => {
2566
+ const checkLogCounter = (delayInMinutes, expectedCounter) => {
2567
+ const delayInMilliseconds = delayInMinutes * 60 * 1000;
2568
+
2496
2569
  // first check that the counter is not increased just before the delay
2497
- clock.tick(delay - 50);
2570
+ clock.tick(delayInMilliseconds - 50);
2498
2571
  assert.equal(logUploadCounter, expectedCounter - 1);
2499
2572
 
2500
2573
  // and now check that it has reached expected value after the delay
@@ -2502,22 +2575,18 @@ describe('plugin-meetings', () => {
2502
2575
  assert.equal(logUploadCounter, expectedCounter);
2503
2576
  };
2504
2577
 
2505
- checkLogCounter(100, 1);
2506
- checkLogCounter(1000, 2);
2507
- checkLogCounter(15000, 3);
2508
- checkLogCounter(15000, 4);
2509
- checkLogCounter(30000, 5);
2510
- checkLogCounter(30000, 6);
2511
- checkLogCounter(30000, 7);
2512
- checkLogCounter(60000, 8);
2513
- checkLogCounter(60000, 9);
2514
- checkLogCounter(60000, 10);
2578
+ checkLogCounter(0.1, 1);
2579
+ checkLogCounter(15, 2);
2580
+ checkLogCounter(30, 3);
2581
+ checkLogCounter(60, 4);
2582
+ checkLogCounter(60, 5);
2515
2583
 
2516
- // simulate media connection being removed -> no more log uploads should happen
2584
+ // simulate media connection being removed -> 1 more upload should happen, but nothing more afterwards
2517
2585
  meeting.mediaProperties.webrtcMediaConnection = undefined;
2586
+ checkLogCounter(60, 6);
2518
2587
 
2519
- clock.tick(60000);
2520
- assert.equal(logUploadCounter, 11);
2588
+ clock.tick(120 * 1000 * 60);
2589
+ assert.equal(logUploadCounter, 6);
2521
2590
 
2522
2591
  clock.restore();
2523
2592
  });
@@ -2644,6 +2713,7 @@ describe('plugin-meetings', () => {
2644
2713
  webex.meetings.reachability = {
2645
2714
  isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
2646
2715
  getReachabilityMetrics: sinon.stub().resolves(),
2716
+ stopReachability: sinon.stub(),
2647
2717
  };
2648
2718
  const MOCK_CLIENT_ERROR_CODE = 2004;
2649
2719
  const generateClientErrorCodeForIceFailureStub = sinon
@@ -2852,6 +2922,7 @@ describe('plugin-meetings', () => {
2852
2922
  .onCall(2)
2853
2923
  .resolves(false),
2854
2924
  getReachabilityMetrics: sinon.stub().resolves({}),
2925
+ stopReachability: sinon.stub(),
2855
2926
  };
2856
2927
  const getErrorPayloadForClientErrorCodeStub =
2857
2928
  (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
@@ -3146,6 +3217,7 @@ describe('plugin-meetings', () => {
3146
3217
  someReachabilityMetric1: 'some value1',
3147
3218
  someReachabilityMetric2: 'some value2',
3148
3219
  }),
3220
+ stopReachability: sinon.stub(),
3149
3221
  };
3150
3222
  meeting.iceCandidatesCount = 3;
3151
3223
  meeting.iceCandidateErrors.set('701_error', 3);
@@ -3475,6 +3547,51 @@ describe('plugin-meetings', () => {
3475
3547
  });
3476
3548
  });
3477
3549
 
3550
+ it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
3551
+ let fakeMembersCollection = {
3552
+ members: {
3553
+ member1: { isInMeeting: true },
3554
+ member2: { isInMeeting: true },
3555
+ member3: { isInMeeting: false },
3556
+ },
3557
+ };
3558
+ sinon.stub(meeting, 'getMembers').returns({ membersCollection: fakeMembersCollection });
3559
+ const fakeData = { intervalMetadata: {}, networkType: 'wifi' };
3560
+
3561
+ statsAnalyzerStub.emit(
3562
+ { file: 'test', function: 'test' },
3563
+ StatsAnalyzerEventNames.MEDIA_QUALITY,
3564
+ { data: fakeData }
3565
+ );
3566
+
3567
+ assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
3568
+ name: 'client.mediaquality.event',
3569
+ options: {
3570
+ meetingId: meeting.id,
3571
+ },
3572
+ payload: {
3573
+ intervals: [sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2))],
3574
+ },
3575
+ });
3576
+ fakeMembersCollection.members.member2.isInMeeting = false;
3577
+
3578
+ statsAnalyzerStub.emit(
3579
+ { file: 'test', function: 'test' },
3580
+ StatsAnalyzerEventNames.MEDIA_QUALITY,
3581
+ { data: fakeData }
3582
+ );
3583
+
3584
+ assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
3585
+ name: 'client.mediaquality.event',
3586
+ options: {
3587
+ meetingId: meeting.id,
3588
+ },
3589
+ payload: {
3590
+ intervals: [sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1))],
3591
+ },
3592
+ });
3593
+ });
3594
+
3478
3595
  it('calls submitMQE correctly', async () => {
3479
3596
  const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'};
3480
3597
 
@@ -3552,14 +3669,6 @@ describe('plugin-meetings', () => {
3552
3669
  });
3553
3670
  });
3554
3671
 
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
3672
  describe('CA ice failures checks', () => {
3564
3673
  [
3565
3674
  {
@@ -3613,6 +3722,7 @@ describe('plugin-meetings', () => {
3613
3722
 
3614
3723
  webex.meetings.reachability = {
3615
3724
  isWebexMediaBackendUnreachable: sinon.stub().resolves(unreachable || false),
3725
+ stopReachability: sinon.stub(),
3616
3726
  };
3617
3727
 
3618
3728
  const generateClientErrorCodeForIceFailureStub = sinon
@@ -3701,6 +3811,73 @@ describe('plugin-meetings', () => {
3701
3811
  });
3702
3812
  });
3703
3813
 
3814
+ describe(`#beRightBack`, () => {
3815
+ const fakeMultistreamRoapMediaConnection = {
3816
+ createSendSlot: sinon.stub().returns({
3817
+ setSourceStateOverride: sinon.stub().resolves(),
3818
+ clearSourceStateOverride: sinon.stub().resolves(),
3819
+ }),
3820
+ };
3821
+
3822
+ beforeEach(() => {
3823
+ meeting.mediaProperties.webrtcMediaConnection = {createSendSlot: sinon.stub()};
3824
+ meeting.sendSlotManager.createSlot(
3825
+ fakeMultistreamRoapMediaConnection,
3826
+ MediaType.VideoMain
3827
+ );
3828
+
3829
+ meeting.locusUrl = 'locus url';
3830
+ meeting.deviceUrl = 'device url';
3831
+ meeting.selfId = 'self id';
3832
+ meeting.brbState = createBrbState(meeting, false);
3833
+ meeting.brbState.enable = sinon.stub().resolves();
3834
+ });
3835
+
3836
+ afterEach(() => {
3837
+ sinon.restore();
3838
+ });
3839
+
3840
+ it('should have #beRightBack', () => {
3841
+ assert.exists(meeting.beRightBack);
3842
+ });
3843
+
3844
+ describe('when in a multistream meeting', () => {
3845
+
3846
+ beforeEach(() => {
3847
+ meeting.isMultistream = true;
3848
+ });
3849
+
3850
+ it('should enable #beRightBack and return a promise', async () => {
3851
+ const brbResult = meeting.beRightBack(true);
3852
+
3853
+ await brbResult;
3854
+ assert.exists(brbResult.then);
3855
+ assert.calledOnce(meeting.brbState.enable);
3856
+ })
3857
+
3858
+ it('should disable #beRightBack and return a promise', async () => {
3859
+ const brbResult = meeting.beRightBack(false);
3860
+
3861
+ await brbResult;
3862
+ assert.exists(brbResult.then);
3863
+ assert.calledOnce(meeting.brbState.enable);
3864
+ })
3865
+
3866
+ it('should throw an error and reject the promise if setBrb fails', async () => {
3867
+ const error = new Error('setBrb failed');
3868
+ meeting.brbState.enable.rejects(error);
3869
+
3870
+ try {
3871
+ await meeting.beRightBack(true);
3872
+ } catch (err) {
3873
+ assert.instanceOf(err, Error);
3874
+ assert.equal(err.message, 'setBrb failed');
3875
+ assert.isRejected((Promise.reject()));
3876
+ }
3877
+ })
3878
+ });
3879
+ });
3880
+
3704
3881
  /* This set of tests are like semi-integration tests, they use real MuteState, Media, LocusMediaRequest and Roap classes.
3705
3882
  They mock the @webex/internal-media-core and sending of /media http requests to Locus.
3706
3883
  Their main purpose is to test that we send the right http requests to Locus and make right calls
@@ -3743,8 +3920,12 @@ describe('plugin-meetings', () => {
3743
3920
  meeting.setMercuryListener = sinon.stub();
3744
3921
  meeting.locusInfo.onFullLocus = sinon.stub();
3745
3922
  meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
3746
- meeting.webex.meetings.reachability.getReachabilityReportToAttachToRoap = sinon.stub().resolves({id: 'fake reachability'});
3747
- meeting.webex.meetings.reachability.getClientMediaPreferences = sinon.stub().resolves({id: 'fake clientMediaPreferences'});
3923
+ meeting.webex.meetings.reachability.getReachabilityReportToAttachToRoap = sinon
3924
+ .stub()
3925
+ .resolves({id: 'fake reachability'});
3926
+ meeting.webex.meetings.reachability.getClientMediaPreferences = sinon
3927
+ .stub()
3928
+ .resolves({id: 'fake clientMediaPreferences'});
3748
3929
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3749
3930
  turnServerInfo: {
3750
3931
  url: 'turns:turn-server-url:443?transport=tcp',
@@ -3825,6 +4006,7 @@ describe('plugin-meetings', () => {
3825
4006
  initiateOffer: sinon.stub().resolves({}),
3826
4007
  update: sinon.stub().resolves({}),
3827
4008
  on: sinon.stub(),
4009
+ roapMessageReceived: sinon.stub()
3828
4010
  };
3829
4011
 
3830
4012
  fakeMultistreamRoapMediaConnection = {
@@ -3911,8 +4093,10 @@ describe('plugin-meetings', () => {
3911
4093
  };
3912
4094
 
3913
4095
  // simulates a Roap offer being generated by the RoapMediaConnection
3914
- const simulateRoapOffer = async () => {
3915
- meeting.deferSDPAnswer = {resolve: sinon.stub()};
4096
+ const simulateRoapOffer = async (stubWaitingForAnswer = true) => {
4097
+ if (stubWaitingForAnswer) {
4098
+ meeting.deferSDPAnswer = {resolve: sinon.stub()};
4099
+ }
3916
4100
  const roapListener = getRoapListener();
3917
4101
 
3918
4102
  await roapListener({roapMessage: roapOfferMessage});
@@ -3930,8 +4114,14 @@ describe('plugin-meetings', () => {
3930
4114
  const checkSdpOfferSent = ({audioMuted, videoMuted}) => {
3931
4115
  const {sdp, seq, tieBreaker} = roapOfferMessage;
3932
4116
 
3933
- assert.calledWith(meeting.webex.meetings.reachability.getClientMediaPreferences, meeting.isMultistream, 0);
3934
- assert.calledWith(meeting.webex.meetings.reachability.getReachabilityReportToAttachToRoap);
4117
+ assert.calledWith(
4118
+ meeting.webex.meetings.reachability.getClientMediaPreferences,
4119
+ meeting.isMultistream,
4120
+ 0
4121
+ );
4122
+ assert.calledWith(
4123
+ meeting.webex.meetings.reachability.getReachabilityReportToAttachToRoap
4124
+ );
3935
4125
 
3936
4126
  assert.calledWith(locusMediaRequestStub, {
3937
4127
  method: 'PUT',
@@ -4015,8 +4205,9 @@ describe('plugin-meetings', () => {
4015
4205
  remoteQualityLevel,
4016
4206
  expectedDebugId,
4017
4207
  meetingId,
4208
+ expectMultistream = isMultistream,
4018
4209
  }) => {
4019
- if (isMultistream) {
4210
+ if (expectMultistream) {
4020
4211
  const {iceServers} = mediaConnectionConfig;
4021
4212
 
4022
4213
  assert.calledOnceWithMatch(
@@ -4176,7 +4367,6 @@ describe('plugin-meetings', () => {
4176
4367
  });
4177
4368
 
4178
4369
  it('addMedia() works correctly when media is enabled with streams to publish', async () => {
4179
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4180
4370
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
4181
4371
  await simulateRoapOffer();
4182
4372
  await simulateRoapOk();
@@ -4207,12 +4397,9 @@ describe('plugin-meetings', () => {
4207
4397
 
4208
4398
  // and that these were the only /media requests that were sent
4209
4399
  assert.calledTwice(locusMediaRequestStub);
4210
-
4211
- assert.calledOnce(handleDeviceLoggingSpy);
4212
4400
  });
4213
4401
 
4214
4402
  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
4403
  fakeMicrophoneStream.userMuted = true;
4217
4404
 
4218
4405
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
@@ -4244,7 +4431,6 @@ describe('plugin-meetings', () => {
4244
4431
 
4245
4432
  // and that these were the only /media requests that were sent
4246
4433
  assert.calledTwice(locusMediaRequestStub);
4247
- assert.calledOnce(handleDeviceLoggingSpy);
4248
4434
  });
4249
4435
 
4250
4436
  it('addMedia() works correctly when media is enabled with tracks to publish and track is ended', async () => {
@@ -4316,7 +4502,6 @@ describe('plugin-meetings', () => {
4316
4502
  });
4317
4503
 
4318
4504
  it('addMedia() works correctly when media is disabled with streams to publish', async () => {
4319
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4320
4505
  await meeting.addMedia({
4321
4506
  localStreams: {microphone: fakeMicrophoneStream},
4322
4507
  audioEnabled: false,
@@ -4350,20 +4535,6 @@ describe('plugin-meetings', () => {
4350
4535
 
4351
4536
  // and that these were the only /media requests that were sent
4352
4537
  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
4538
  });
4368
4539
 
4369
4540
  it('addMedia() works correctly when media is disabled with no streams to publish', async () => {
@@ -4399,20 +4570,6 @@ describe('plugin-meetings', () => {
4399
4570
  assert.calledTwice(locusMediaRequestStub);
4400
4571
  });
4401
4572
 
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
4573
  it('addMedia() works correctly when video is disabled with no streams to publish', async () => {
4417
4574
  await meeting.addMedia({videoEnabled: false});
4418
4575
  await simulateRoapOffer();
@@ -4479,13 +4636,6 @@ describe('plugin-meetings', () => {
4479
4636
  assert.calledTwice(locusMediaRequestStub);
4480
4637
  });
4481
4638
 
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
4639
  describe('publishStreams()/unpublishStreams() calls', () => {
4490
4640
  [
4491
4641
  {mediaEnabled: true, expected: {direction: 'sendrecv', localMuteSentValue: false}},
@@ -4881,6 +5031,211 @@ describe('plugin-meetings', () => {
4881
5031
  assert.notCalled(fakeRoapMediaConnection.update);
4882
5032
  })
4883
5033
  );
5034
+
5035
+ if (isMultistream) {
5036
+ describe('fallback from multistream to transcoded', () => {
5037
+ let multistreamEventListeners;
5038
+ let transcodedEventListeners;
5039
+ let mockStatsAnalyzerCtor;
5040
+
5041
+ const setupFakeRoapMediaConnection = (fakeRoapMediaConnection, eventListeners) => {
5042
+ fakeRoapMediaConnection.on.callsFake((eventName, cb) => {
5043
+ eventListeners[eventName] = cb;
5044
+ });
5045
+ fakeRoapMediaConnection.initiateOffer.callsFake(() => {
5046
+ // simulate offer being generated
5047
+ eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED]();
5048
+
5049
+ return Promise.resolve();
5050
+ });
5051
+ };
5052
+
5053
+ beforeEach(() => {
5054
+ multistreamEventListeners = {};
5055
+ transcodedEventListeners = {};
5056
+
5057
+ meeting.config.stats.enableStatsAnalyzer = true;
5058
+
5059
+ setupFakeRoapMediaConnection(fakeRoapMediaConnection, transcodedEventListeners);
5060
+ setupFakeRoapMediaConnection(
5061
+ fakeMultistreamRoapMediaConnection,
5062
+ multistreamEventListeners
5063
+ );
5064
+
5065
+ mockStatsAnalyzerCtor = sinon
5066
+ .stub(InternalMediaCoreModule, 'StatsAnalyzer')
5067
+ .callsFake(() => {
5068
+ return {on: sinon.stub(), stopAnalyzer: sinon.stub()};
5069
+ });
5070
+
5071
+ webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
5072
+ sinon.stub();
5073
+
5074
+ // setup the mock so that we get an SDP answer not from Homer
5075
+ locusMediaRequestStub.callsFake(() => {
5076
+ return Promise.resolve({
5077
+ body: {
5078
+ locus: {},
5079
+ mediaConnections: [
5080
+ {
5081
+ remoteSdp:
5082
+ '{"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"}',
5083
+ },
5084
+ ],
5085
+ },
5086
+ });
5087
+ });
5088
+
5089
+ sinon.stub(meeting, 'closePeerConnections');
5090
+ sinon.stub(meeting.mediaProperties, 'unsetPeerConnection');
5091
+ sinon.stub(meeting.locusMediaRequest, 'downgradeFromMultistreamToTranscoded');
5092
+ });
5093
+
5094
+ const runCheck = async (turnServerInfo, forceTurnDiscovery) => {
5095
+ // we're calling addMediaInternal() with mic stream,
5096
+ // so that we also verify that audioMute, videoMute info is correctly sent to backend
5097
+ const addMediaPromise = meeting.addMediaInternal(
5098
+ () => '',
5099
+ turnServerInfo,
5100
+ forceTurnDiscovery,
5101
+ {
5102
+ localStreams: {microphone: fakeMicrophoneStream},
5103
+ }
5104
+ );
5105
+ await testUtils.flushPromises();
5106
+ await simulateRoapOffer(false);
5107
+
5108
+ // check MultistreamRoapMediaConnection was created correctly
5109
+ checkMediaConnectionCreated({
5110
+ expectMultistream: true,
5111
+ mediaConnectionConfig: expectedMediaConnectionConfig,
5112
+ localStreams: {
5113
+ audio: fakeMicrophoneStream,
5114
+ video: undefined,
5115
+ screenShareVideo: undefined,
5116
+ screenShareAudio: undefined,
5117
+ },
5118
+ direction: {
5119
+ audio: 'sendrecv',
5120
+ video: 'sendrecv',
5121
+ screenShare: 'recvonly',
5122
+ },
5123
+ remoteQualityLevel: 'HIGH',
5124
+ expectedDebugId,
5125
+ meetingId: meeting.id,
5126
+ });
5127
+
5128
+ // 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
5129
+ assert.calledOnceWithExactly(
5130
+ mockStatsAnalyzerCtor,
5131
+ sinon.match({
5132
+ isMultistream: true,
5133
+ })
5134
+ );
5135
+ const initialStatsAnalyzer = mockStatsAnalyzerCtor.returnValues[0];
5136
+ mockStatsAnalyzerCtor.resetHistory();
5137
+
5138
+ // TURN discovery was done (if needed)
5139
+ if (turnServerInfo) {
5140
+ assert.notCalled(meeting.roap.doTurnDiscovery);
5141
+ } else {
5142
+ assert.calledWith(meeting.roap.doTurnDiscovery, meeting, false, false);
5143
+ }
5144
+
5145
+ // and SDP offer was sent with the right audioMuted/videoMuted values
5146
+ checkSdpOfferSent({audioMuted: false, videoMuted: true});
5147
+
5148
+ await testUtils.flushPromises();
5149
+
5150
+ // at this point the meeting should have been downgraded to transcoded
5151
+ assert.equal(meeting.isMultistream, false);
5152
+
5153
+ // old stats analyzer stopped and new one created
5154
+ assert.calledOnce(initialStatsAnalyzer.stopAnalyzer);
5155
+ assert.calledOnceWithExactly(
5156
+ mockStatsAnalyzerCtor,
5157
+ sinon.match({
5158
+ isMultistream: false,
5159
+ })
5160
+ );
5161
+
5162
+ // and correct cleanup of other things should have been done
5163
+ assert.calledOnceWithExactly(meeting.closePeerConnections, false);
5164
+ assert.calledOnceWithExactly(meeting.mediaProperties.unsetPeerConnection);
5165
+ assert.calledOnceWithExactly(
5166
+ meeting.locusMediaRequest.downgradeFromMultistreamToTranscoded
5167
+ );
5168
+
5169
+ // new connection should have been created
5170
+ checkMediaConnectionCreated({
5171
+ expectMultistream: false,
5172
+ mediaConnectionConfig: expectedMediaConnectionConfig,
5173
+ localStreams: {
5174
+ audio: fakeMicrophoneStream,
5175
+ video: undefined,
5176
+ screenShareVideo: undefined,
5177
+ screenShareAudio: undefined,
5178
+ },
5179
+ direction: {
5180
+ audio: 'sendrecv',
5181
+ video: 'sendrecv',
5182
+ screenShare: 'recvonly',
5183
+ },
5184
+ remoteQualityLevel: 'HIGH',
5185
+ expectedDebugId,
5186
+ meetingId: meeting.id,
5187
+ });
5188
+
5189
+ // and new TURN discovery done (no matter if it was being done before or not)
5190
+ assert.calledWith(meeting.roap.doTurnDiscovery, meeting, true, true);
5191
+
5192
+ // simulate new offer
5193
+ await simulateRoapOffer(false);
5194
+ checkSdpOfferSent({audioMuted: false, videoMuted: true});
5195
+
5196
+ // overall there should have been 2 calls to locusMediaRequestStub, because 2 offers were sent
5197
+ assert.calledTwice(locusMediaRequestStub);
5198
+
5199
+ // simulate answer being processed correctly
5200
+ transcodedEventListeners[MediaConnectionEventNames.REMOTE_SDP_ANSWER_PROCESSED]();
5201
+
5202
+ // check that addMedia finally resolved
5203
+ await addMediaPromise;
5204
+ };
5205
+
5206
+ it('addMedia() falls back to transcoded if SDP answer is not from Homer', async () => {
5207
+ // call addMediaInternal like addMedia() does it
5208
+ await runCheck(undefined, false);
5209
+ });
5210
+
5211
+ it('addMediaInternal() correctly falls back to transcoded if SDP answer is not from Homer (joinWithMedia() case)', async () => {
5212
+ // call addMediaInternal the way joinWithMedia() does it - with TURN info already provided
5213
+ // and check that when we fallback to transcoded we still do another TURN discovery
5214
+ await runCheck(
5215
+ {
5216
+ url: 'turns:turn-server-url:443?transport=tcp',
5217
+ username: 'turn user',
5218
+ password: 'turn password',
5219
+ },
5220
+ false
5221
+ );
5222
+ });
5223
+
5224
+ it('addMediaInternal() correctly falls back to transcoded if SDP answer is not from Homer (joinWithMedia() retry case)', async () => {
5225
+ // call addMediaInternal the way joinWithMedia() does it when it does a retry - with TURN info already provided
5226
+ // but also with forceTurnDiscovery=true - this shouldn't affect the flow for fallback to transcoded in any way
5227
+ // but doing it just for completeness
5228
+ await runCheck(
5229
+ {
5230
+ url: 'turns:turn-server-url:443?transport=tcp',
5231
+ username: 'turn user',
5232
+ password: 'turn password',
5233
+ },
5234
+ true
5235
+ );
5236
+ });
5237
+ });
5238
+ }
4884
5239
  })
4885
5240
  );
4886
5241
 
@@ -4958,6 +5313,11 @@ describe('plugin-meetings', () => {
4958
5313
  meeting.logger.error = sinon.stub().returns(true);
4959
5314
  meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
4960
5315
  webex.internal.voicea.off = sinon.stub().returns(true);
5316
+ meeting.stopTranscription = sinon.stub();
5317
+ meeting.transcription = {};
5318
+
5319
+ meeting.annotation.deregisterEvents = sinon.stub();
5320
+ webex.internal.llm.off = sinon.stub();
4961
5321
 
4962
5322
  // A meeting needs to be joined to leave
4963
5323
  meeting.meetingState = 'ACTIVE';
@@ -4978,6 +5338,9 @@ describe('plugin-meetings', () => {
4978
5338
  assert.calledOnce(meeting.closePeerConnections);
4979
5339
  assert.calledOnce(meeting.unsetRemoteStreams);
4980
5340
  assert.calledOnce(meeting.unsetPeerConnections);
5341
+ assert.calledOnce(meeting.stopTranscription);
5342
+ assert.calledOnce(meeting.annotation.deregisterEvents);
5343
+ assert.calledWith(webex.internal.llm.off, 'event:relay.event', meeting.processRelayEvent);
4981
5344
  });
4982
5345
 
4983
5346
  it('should reset call diagnostic latencies correctly', async () => {
@@ -5965,6 +6328,38 @@ describe('plugin-meetings', () => {
5965
6328
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.REQUIRED);
5966
6329
  });
5967
6330
 
6331
+ it('handles meetingInfoProvider not reach JBH', async () => {
6332
+ meeting.destination = FAKE_DESTINATION;
6333
+ meeting.destinationType = FAKE_TYPE;
6334
+ meeting.attrs.meetingInfoProvider = {
6335
+ fetchMeetingInfo: sinon
6336
+ .stub()
6337
+ .throws(new MeetingInfoV2JoinForbiddenError(403003, FAKE_MEETING_INFO)),
6338
+ };
6339
+
6340
+ await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinForbiddenError);
6341
+
6342
+ assert.calledWith(
6343
+ meeting.attrs.meetingInfoProvider.fetchMeetingInfo,
6344
+ FAKE_DESTINATION,
6345
+ FAKE_TYPE,
6346
+ null,
6347
+ null,
6348
+ undefined,
6349
+ 'locus-id',
6350
+ {},
6351
+ {meetingId: meeting.id, sendCAevents: true}
6352
+ );
6353
+
6354
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6355
+ assert.equal(meeting.meetingInfoFailureCode, 403003);
6356
+ assert.equal(
6357
+ meeting.meetingInfoFailureReason,
6358
+ MEETING_INFO_FAILURE_REASON.NOT_REACH_JBH
6359
+ );
6360
+ assert.equal(meeting.requiredCaptcha, null);
6361
+ });
6362
+
5968
6363
  it('handles meetingInfoProvider policy error', async () => {
5969
6364
  meeting.destination = FAKE_DESTINATION;
5970
6365
  meeting.destinationType = FAKE_TYPE;
@@ -6332,29 +6727,74 @@ describe('plugin-meetings', () => {
6332
6727
  assert.equal(meeting.fetchMeetingInfoTimeoutId, undefined);
6333
6728
  });
6334
6729
 
6335
- it('handles meetingInfoProvider webinar need registration error', async () => {
6730
+ it('handles MeetingInfoV2JoinWebinarError webinar need registration', async () => {
6336
6731
  meeting.destination = FAKE_DESTINATION;
6337
6732
  meeting.destinationType = FAKE_TYPE;
6338
6733
  meeting.attrs.meetingInfoProvider = {
6339
6734
  fetchMeetingInfo: sinon
6340
6735
  .stub()
6341
6736
  .throws(
6342
- new MeetingInfoV2WebinarRegistrationError(403021, FAKE_MEETING_INFO, 'a message')
6737
+ new MeetingInfoV2JoinWebinarError(403021, FAKE_MEETING_INFO, 'a message')
6343
6738
  ),
6344
6739
  };
6345
6740
 
6346
6741
  await assert.isRejected(
6347
6742
  meeting.fetchMeetingInfo({sendCAevents: true}),
6348
- WebinarRegistrationError
6743
+ JoinWebinarError
6349
6744
  );
6350
6745
 
6351
6746
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6352
- assert.equal(meeting.meetingInfoFailureCode, 403021);
6353
6747
  assert.equal(
6354
6748
  meeting.meetingInfoFailureReason,
6355
6749
  MEETING_INFO_FAILURE_REASON.WEBINAR_REGISTRATION
6356
6750
  );
6357
6751
  });
6752
+
6753
+ it('handles MeetingInfoV2JoinWebinarError webinar need join with webcast', async () => {
6754
+ meeting.destination = FAKE_DESTINATION;
6755
+ meeting.destinationType = FAKE_TYPE;
6756
+ meeting.attrs.meetingInfoProvider = {
6757
+ fetchMeetingInfo: sinon
6758
+ .stub()
6759
+ .throws(
6760
+ new MeetingInfoV2JoinWebinarError(403026, FAKE_MEETING_INFO, 'a message')
6761
+ ),
6762
+ };
6763
+
6764
+ await assert.isRejected(
6765
+ meeting.fetchMeetingInfo({sendCAevents: true}),
6766
+ JoinWebinarError
6767
+ );
6768
+
6769
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6770
+ assert.equal(
6771
+ meeting.meetingInfoFailureReason,
6772
+ MEETING_INFO_FAILURE_REASON.NEED_JOIN_WITH_WEBCAST
6773
+ );
6774
+ });
6775
+
6776
+ it('handles MeetingInfoV2JoinWebinarError webinar need registrationId', async () => {
6777
+ meeting.destination = FAKE_DESTINATION;
6778
+ meeting.destinationType = FAKE_TYPE;
6779
+ meeting.attrs.meetingInfoProvider = {
6780
+ fetchMeetingInfo: sinon
6781
+ .stub()
6782
+ .throws(
6783
+ new MeetingInfoV2JoinWebinarError(403037, FAKE_MEETING_INFO, 'a message')
6784
+ ),
6785
+ };
6786
+
6787
+ await assert.isRejected(
6788
+ meeting.fetchMeetingInfo({sendCAevents: true}),
6789
+ JoinWebinarError
6790
+ );
6791
+
6792
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6793
+ assert.equal(
6794
+ meeting.meetingInfoFailureReason,
6795
+ MEETING_INFO_FAILURE_REASON.WEBINAR_NEED_REGISTRATIONID
6796
+ );
6797
+ });
6358
6798
  });
6359
6799
 
6360
6800
  describe('#refreshPermissionToken', () => {
@@ -6815,6 +7255,9 @@ describe('plugin-meetings', () => {
6815
7255
  meeting.transcription = {};
6816
7256
  meeting.stopTranscription = sinon.stub();
6817
7257
 
7258
+ meeting.annotation.deregisterEvents = sinon.stub();
7259
+ webex.internal.llm.off = sinon.stub();
7260
+
6818
7261
  // A meeting needs to be joined to end
6819
7262
  meeting.meetingState = 'ACTIVE';
6820
7263
  meeting.state = 'JOINED';
@@ -6835,6 +7278,9 @@ describe('plugin-meetings', () => {
6835
7278
  assert.calledOnce(meeting?.unsetRemoteStreams);
6836
7279
  assert.calledOnce(meeting?.unsetPeerConnections);
6837
7280
  assert.calledOnce(meeting?.stopTranscription);
7281
+
7282
+ assert.called(meeting.annotation.deregisterEvents);
7283
+ assert.calledWith(webex.internal.llm.off, 'event:relay.event', meeting.processRelayEvent);
6838
7284
  });
6839
7285
  });
6840
7286
 
@@ -7817,7 +8263,9 @@ describe('plugin-meetings', () => {
7817
8263
  });
7818
8264
 
7819
8265
  it('should collect ice candidates', () => {
7820
- eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: {candidate: 'candidate'}});
8266
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({
8267
+ candidate: {candidate: 'candidate'},
8268
+ });
7821
8269
 
7822
8270
  assert.equal(meeting.iceCandidatesCount, 1);
7823
8271
  });
@@ -8123,10 +8571,10 @@ describe('plugin-meetings', () => {
8123
8571
  meeting.statsAnalyzer.stopAnalyzer = sinon.stub().resolves();
8124
8572
  meeting.reconnectionManager = {
8125
8573
  reconnect: sinon.stub().resolves(),
8126
- resetReconnectionTimer: () => {}
8574
+ resetReconnectionTimer: () => {},
8127
8575
  };
8128
8576
  meeting.currentMediaStatus = {
8129
- video: true
8577
+ video: true,
8130
8578
  };
8131
8579
 
8132
8580
  await mockFailedEvent();
@@ -8408,8 +8856,7 @@ describe('plugin-meetings', () => {
8408
8856
  assert.calledWith(meeting.roapMessageReceived, fakeAnswer);
8409
8857
  });
8410
8858
 
8411
- it('handles OFFER message correctly when request fails', async () => {
8412
- const fakeError = new Error('fake error');
8859
+ const runOfferSendingFailureTest = async (fakeError, canProceed, expectedErrorCode) => {
8413
8860
  const clock = sinon.useFakeTimers();
8414
8861
  sinon.spy(clock, 'clearTimeout');
8415
8862
  meeting.deferSDPAnswer = {reject: sinon.stub()};
@@ -8447,19 +8894,31 @@ describe('plugin-meetings', () => {
8447
8894
  assert.equal(meeting.sdpResponseTimer, undefined);
8448
8895
 
8449
8896
  assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
8450
- clientErrorCode: 2007,
8897
+ clientErrorCode: expectedErrorCode,
8451
8898
  });
8452
8899
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
8453
8900
  name: 'client.media-engine.remote-sdp-received',
8454
8901
  payload: {
8455
- canProceed: false,
8456
- errors: [{errorCode: 2007, fatal: true}],
8902
+ canProceed,
8903
+ errors: [{errorCode: expectedErrorCode, fatal: true}],
8457
8904
  },
8458
8905
  options: {
8459
8906
  meetingId: meeting.id,
8460
8907
  rawError: fakeError,
8461
8908
  },
8462
8909
  });
8910
+ };
8911
+
8912
+ it('handles OFFER message correctly when request fails', async () => {
8913
+ const fakeError = new Error('fake error');
8914
+
8915
+ await runOfferSendingFailureTest(fakeError, false, 2007);
8916
+ });
8917
+
8918
+ it('handles OFFER message correctly when we get a non-homer answer', async () => {
8919
+ const fakeError = new MultistreamNotSupportedError();
8920
+
8921
+ await runOfferSendingFailureTest(fakeError, true, 2012);
8463
8922
  });
8464
8923
 
8465
8924
  it('handles ANSWER message correctly', () => {
@@ -8662,6 +9121,7 @@ describe('plugin-meetings', () => {
8662
9121
  });
8663
9122
  });
8664
9123
  });
9124
+
8665
9125
  describe('#setUpLocusInfoSelfListener', () => {
8666
9126
  it('listens to the self unadmitted guest event', (done) => {
8667
9127
  meeting.startKeepAlive = sinon.stub();
@@ -8756,6 +9216,27 @@ describe('plugin-meetings', () => {
8756
9216
  );
8757
9217
  });
8758
9218
 
9219
+ it('listens to the brb state changed event', () => {
9220
+ const assertBrb = (enabled) => {
9221
+ meeting.brbState = createBrbState(meeting, false);
9222
+ meeting.locusInfo.emit(
9223
+ { function: 'test', file: 'test' },
9224
+ LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
9225
+ { brb: { enabled } },
9226
+ )
9227
+ assert.calledWithExactly(
9228
+ TriggerProxy.trigger,
9229
+ meeting,
9230
+ {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
9231
+ EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
9232
+ { payload: { brb: { enabled } } },
9233
+ );
9234
+ }
9235
+
9236
+ assertBrb(true);
9237
+ assertBrb(false);
9238
+ })
9239
+
8759
9240
  it('listens to the interpretation changed event', () => {
8760
9241
  meeting.simultaneousInterpretation.updateSelfInterpretation = sinon.stub();
8761
9242
 
@@ -9054,7 +9535,7 @@ describe('plugin-meetings', () => {
9054
9535
  {state}
9055
9536
  );
9056
9537
 
9057
- assert.calledOnceWithExactly( meeting.webinar.updatePracticeSessionStatus, state);
9538
+ assert.calledOnceWithExactly(meeting.webinar.updatePracticeSessionStatus, state);
9058
9539
  assert.calledWith(
9059
9540
  TriggerProxy.trigger,
9060
9541
  meeting,
@@ -9537,15 +10018,44 @@ describe('plugin-meetings', () => {
9537
10018
  describe('#closePeerConnections', () => {
9538
10019
  it('should close the webrtc media connection, and return a promise', async () => {
9539
10020
  const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
9540
- meeting.mediaProperties.webrtcMediaConnection = {close: sinon.stub()};
10021
+ const fakeWebrtcMediaConnection = {close: sinon.stub()};
10022
+ meeting.mediaProperties.webrtcMediaConnection = fakeWebrtcMediaConnection;
10023
+
10024
+ meeting.audio = {id: 'fakeAudioMuteState'};
10025
+ meeting.video = {id: 'fakeVideoMuteState'};
10026
+
9541
10027
  const pcs = meeting.closePeerConnections();
9542
10028
 
9543
10029
  assert.exists(pcs.then);
9544
10030
  await pcs;
9545
- assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.close);
10031
+ assert.calledOnce(fakeWebrtcMediaConnection.close);
10032
+ assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
10033
+ assert.equal(meeting.audio, null);
10034
+ assert.equal(meeting.video, null);
10035
+ assert.equal(meeting.mediaProperties.webrtcMediaConnection, null);
10036
+ });
10037
+
10038
+ it('should close the webrtc media connection, but keep audio and video props unchanged if called with resetMuteStates=false', async () => {
10039
+ const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
10040
+ const fakeWebrtcMediaConnection = {close: sinon.stub()};
10041
+ meeting.mediaProperties.webrtcMediaConnection = fakeWebrtcMediaConnection;
10042
+
10043
+ const fakeAudio = {id: 'fakeAudioMuteState'};
10044
+ const fakeVideo = {id: 'fakeVideoMuteState'};
10045
+
10046
+ meeting.audio = fakeAudio;
10047
+ meeting.video = fakeVideo;
10048
+
10049
+ await meeting.closePeerConnections(false);
10050
+
10051
+ assert.calledOnce(fakeWebrtcMediaConnection.close);
9546
10052
  assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
10053
+ assert.equal(meeting.audio, fakeAudio);
10054
+ assert.equal(meeting.video, fakeVideo);
10055
+ assert.equal(meeting.mediaProperties.webrtcMediaConnection, null);
9547
10056
  });
9548
10057
  });
10058
+
9549
10059
  describe('#unsetPeerConnections', () => {
9550
10060
  it('should unset the peer connections', () => {
9551
10061
  meeting.mediaProperties.unsetPeerConnection = sinon.stub().returns(true);
@@ -10674,6 +11184,7 @@ describe('plugin-meetings', () => {
10674
11184
  meeting.webex.internal.llm.on = sinon.stub();
10675
11185
  meeting.webex.internal.llm.off = sinon.stub();
10676
11186
  meeting.processRelayEvent = sinon.stub();
11187
+ meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(false);
10677
11188
  });
10678
11189
 
10679
11190
  it('does not connect if the call is not joined yet', async () => {
@@ -10805,6 +11316,19 @@ describe('plugin-meetings', () => {
10805
11316
  meeting.processRelayEvent
10806
11317
  );
10807
11318
  });
11319
+
11320
+
11321
+ it('connect ps data channel if ps started in webinar', async () => {
11322
+ meeting.joinedWith = {state: 'JOINED'};
11323
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url', practiceSessionDatachannelUrl: 'a ps datachannel url'}};
11324
+ meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(true);
11325
+ await meeting.updateLLMConnection();
11326
+
11327
+ assert.notCalled(webex.internal.llm.disconnectLLM);
11328
+ assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a ps datachannel url');
11329
+
11330
+ });
11331
+
10808
11332
  });
10809
11333
 
10810
11334
  describe('#setLocus', () => {
@@ -10996,6 +11520,7 @@ describe('plugin-meetings', () => {
10996
11520
  beforeEach(() => {
10997
11521
  meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
10998
11522
  meeting.deviceUrl = 'my-web-url';
11523
+ meeting.locusInfo.info = {isWebinar: false};
10999
11524
  });
11000
11525
 
11001
11526
  const USER_IDS = {
@@ -11221,13 +11746,24 @@ describe('plugin-meetings', () => {
11221
11746
 
11222
11747
  activeSharingId.whiteboard = beneficiaryId;
11223
11748
 
11224
- eventTrigger.share.push({
11749
+ eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
11750
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11751
+ functionName: 'remoteShare',
11752
+ eventPayload: {
11753
+ memberId: null,
11754
+ url,
11755
+ shareInstanceId,
11756
+ annotationInfo: undefined,
11757
+ resourceType: undefined,
11758
+ },
11759
+ } : {
11225
11760
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11226
11761
  functionName: 'startWhiteboardShare',
11227
11762
  eventPayload: {resourceUrl, memberId: beneficiaryId},
11228
11763
  });
11229
11764
 
11230
- shareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11765
+ shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11766
+
11231
11767
  }
11232
11768
 
11233
11769
  if (eventTrigger.member) {
@@ -11259,13 +11795,24 @@ describe('plugin-meetings', () => {
11259
11795
  newPayload.current.content.disposition = FLOOR_ACTION.ACCEPTED;
11260
11796
  newPayload.current.content.beneficiaryId = otherBeneficiaryId;
11261
11797
 
11262
- eventTrigger.share.push({
11798
+ eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
11799
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11800
+ functionName: 'remoteShare',
11801
+ eventPayload: {
11802
+ memberId: null,
11803
+ url,
11804
+ shareInstanceId,
11805
+ annotationInfo: undefined,
11806
+ resourceType: undefined,
11807
+ },
11808
+ } : {
11263
11809
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11264
11810
  functionName: 'startWhiteboardShare',
11265
11811
  eventPayload: {resourceUrl, memberId: beneficiaryId},
11266
11812
  });
11267
11813
 
11268
- shareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11814
+ shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11815
+
11269
11816
  } else {
11270
11817
  eventTrigger.share.push({
11271
11818
  eventName: EVENT_TRIGGERS.MEETING_STOPPED_SHARING_WHITEBOARD,
@@ -11392,6 +11939,38 @@ describe('plugin-meetings', () => {
11392
11939
  assert.exists(meeting.setUpLocusMediaSharesListener);
11393
11940
  });
11394
11941
 
11942
+ describe('Whiteboard Share - Webinar Attendee', () => {
11943
+ it('Scenario #1: Whiteboard sharing as a webinar attendee', () => {
11944
+ // Set the webinar attendee flag
11945
+ meeting.webinar = { selfIsAttendee: true };
11946
+ meeting.locusInfo.info.isWebinar = true;
11947
+
11948
+ // Step 1: Start sharing whiteboard A
11949
+ const data1 = generateData(
11950
+ blankPayload, // Initial payload
11951
+ true, // isGranting: Granting share
11952
+ false, // isContent: Whiteboard (not content)
11953
+ USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
11954
+ RESOURCE_URLS.WHITEBOARD_A // Resource URL: Whiteboard A
11955
+ );
11956
+
11957
+ // Step 2: Stop sharing whiteboard A
11958
+ const data2 = generateData(
11959
+ data1.payload, // Updated payload from Step 1
11960
+ false, // isGranting: Stopping share
11961
+ false, // isContent: Whiteboard
11962
+ USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
11963
+ );
11964
+
11965
+ // Validate the payload changes and status updates
11966
+ payloadTestHelper([data1]);
11967
+
11968
+ // Specific assertions for webinar attendee status
11969
+ assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
11970
+ });
11971
+ });
11972
+
11973
+
11395
11974
  describe('Whiteboard A --> Whiteboard B', () => {
11396
11975
  it('Scenario #1: you share both whiteboards', () => {
11397
11976
  const data1 = generateData(
@@ -12067,9 +12646,12 @@ describe('plugin-meetings', () => {
12067
12646
  it('startKeepAlive starts the keep alive', async () => {
12068
12647
  meeting.meetingRequest.keepAlive = sinon.stub().returns(Promise.resolve());
12069
12648
 
12649
+ const keepAliveUrl1 = 'keep.alive.url1';
12650
+ const keepAliveUrl2 = 'keep.alive.url2';
12651
+
12070
12652
  assert.isNull(meeting.keepAliveTimerId);
12071
12653
  meeting.joinedWith = {
12072
- keepAliveUrl: defaultKeepAliveUrl,
12654
+ keepAliveUrl: keepAliveUrl1,
12073
12655
  keepAliveSecs: defaultKeepAliveSecs,
12074
12656
  };
12075
12657
  meeting.startKeepAlive();
@@ -12078,12 +12660,15 @@ describe('plugin-meetings', () => {
12078
12660
  assert.notCalled(meeting.meetingRequest.keepAlive);
12079
12661
  await progressTime(defaultExpectedInterval);
12080
12662
  assert.calledOnceWithExactly(meeting.meetingRequest.keepAlive, {
12081
- keepAliveUrl: defaultKeepAliveUrl,
12663
+ keepAliveUrl: keepAliveUrl1,
12082
12664
  });
12665
+ // joinedWith keep alive url might change (when we fallback from multistream to transcoded)
12666
+ meeting.joinedWith.keepAliveUrl = keepAliveUrl2;
12667
+
12083
12668
  await progressTime(defaultExpectedInterval);
12084
12669
  assert.calledTwice(meeting.meetingRequest.keepAlive);
12085
- assert.alwaysCalledWithExactly(meeting.meetingRequest.keepAlive, {
12086
- keepAliveUrl: defaultKeepAliveUrl,
12670
+ assert.calledWith(meeting.meetingRequest.keepAlive, {
12671
+ keepAliveUrl: keepAliveUrl2,
12087
12672
  });
12088
12673
  });
12089
12674
  it('startKeepAlive handles existing keepAliveTimerId', async () => {
@@ -12664,7 +13249,7 @@ describe('plugin-meetings', () => {
12664
13249
 
12665
13250
  describe('#roapMessageReceived', () => {
12666
13251
  it('calls roapMessageReceived on the webrtc media connection', () => {
12667
- const fakeMessage = {messageType: 'fake', sdp: 'fake sdp'};
13252
+ const fakeMessage = {messageType: 'ANSWER', sdp: 'fake sdp'};
12668
13253
 
12669
13254
  const getMediaServer = sinon.stub(MeetingsUtil, 'getMediaServer').returns('homer');
12670
13255
 
@@ -12681,5 +13266,47 @@ describe('plugin-meetings', () => {
12681
13266
  assert.calledOnceWithExactly(getMediaServer, 'fake sdp');
12682
13267
  assert.equal(meeting.mediaProperties.webrtcMediaConnection.mediaServer, 'homer');
12683
13268
  });
13269
+
13270
+ it('throws MultistreamNotSupportedError if we get a non-homer SDP answer', async () => {
13271
+ const fakeMessage = {messageType: 'ANSWER', sdp: 'fake sdp'};
13272
+
13273
+ meeting.isMultistream = true;
13274
+ meeting.mediaProperties.webrtcMediaConnection = {
13275
+ roapMessageReceived: sinon.stub(),
13276
+ };
13277
+
13278
+ sinon.stub(MeetingsUtil, 'getMediaServer').returns('linus');
13279
+
13280
+ try {
13281
+ await meeting.roapMessageReceived(fakeMessage);
13282
+
13283
+ assert.fail('Expected MultistreamNotSupportedError to be thrown');
13284
+ } catch(e) {
13285
+ assert.isTrue(e instanceof MultistreamNotSupportedError);
13286
+ }
13287
+
13288
+ assert.notCalled(meeting.mediaProperties.webrtcMediaConnection.roapMessageReceived);
13289
+ });
13290
+
13291
+ it('does not call getMediaServer for a roap message other than ANSWER', async () => {
13292
+ const fakeMessage = {messageType: 'ERROR', sdp: 'fake sdp'};
13293
+
13294
+ meeting.isMultistream = true;
13295
+ meeting.mediaProperties.webrtcMediaConnection = {
13296
+ roapMessageReceived: sinon.stub(),
13297
+ };
13298
+ meeting.mediaProperties.webrtcMediaConnection.mediaServer = 'linus';
13299
+
13300
+ const getMediaServerStub = sinon.stub(MeetingsUtil, 'getMediaServer').returns('something');
13301
+
13302
+ meeting.roapMessageReceived(fakeMessage);
13303
+
13304
+ assert.calledOnceWithExactly(
13305
+ meeting.mediaProperties.webrtcMediaConnection.roapMessageReceived,
13306
+ fakeMessage
13307
+ );
13308
+ assert.notCalled(getMediaServerStub);
13309
+ assert.equal(meeting.mediaProperties.webrtcMediaConnection.mediaServer, 'linus'); // check that it hasn't been overwritten
13310
+ });
12684
13311
  });
12685
13312
  });