@webex/plugin-meetings 3.7.0 → 3.8.0-next.10

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 (206) 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 +3 -1
  12. package/dist/config.js.map +1 -1
  13. package/dist/constants.js +69 -6
  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 +4 -4
  18. package/dist/interpretation/index.js.map +1 -1
  19. package/dist/interpretation/siLanguage.js +1 -1
  20. package/dist/locus-info/index.js +14 -3
  21. package/dist/locus-info/index.js.map +1 -1
  22. package/dist/locus-info/selfUtils.js +35 -17
  23. package/dist/locus-info/selfUtils.js.map +1 -1
  24. package/dist/media/MediaConnectionAwaiter.js +1 -0
  25. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  26. package/dist/media/properties.js +30 -16
  27. package/dist/media/properties.js.map +1 -1
  28. package/dist/meeting/brbState.js +167 -0
  29. package/dist/meeting/brbState.js.map +1 -0
  30. package/dist/meeting/in-meeting-actions.js +13 -1
  31. package/dist/meeting/in-meeting-actions.js.map +1 -1
  32. package/dist/meeting/index.js +1373 -1052
  33. package/dist/meeting/index.js.map +1 -1
  34. package/dist/meeting/locusMediaRequest.js +32 -11
  35. package/dist/meeting/locusMediaRequest.js.map +1 -1
  36. package/dist/meeting/muteState.js +1 -6
  37. package/dist/meeting/muteState.js.map +1 -1
  38. package/dist/meeting/request.js +51 -29
  39. package/dist/meeting/request.js.map +1 -1
  40. package/dist/meeting/request.type.js.map +1 -1
  41. package/dist/meeting/util.js +103 -67
  42. package/dist/meeting/util.js.map +1 -1
  43. package/dist/meeting-info/meeting-info-v2.js +115 -45
  44. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  45. package/dist/meeting-info/utilv2.js +6 -2
  46. package/dist/meeting-info/utilv2.js.map +1 -1
  47. package/dist/meetings/index.js +107 -55
  48. package/dist/meetings/index.js.map +1 -1
  49. package/dist/meetings/meetings.types.js +2 -0
  50. package/dist/meetings/meetings.types.js.map +1 -1
  51. package/dist/meetings/util.js +1 -1
  52. package/dist/meetings/util.js.map +1 -1
  53. package/dist/member/index.js +9 -0
  54. package/dist/member/index.js.map +1 -1
  55. package/dist/member/types.js.map +1 -1
  56. package/dist/member/util.js +39 -28
  57. package/dist/member/util.js.map +1 -1
  58. package/dist/members/util.js +4 -2
  59. package/dist/members/util.js.map +1 -1
  60. package/dist/metrics/constants.js +6 -1
  61. package/dist/metrics/constants.js.map +1 -1
  62. package/dist/multistream/remoteMedia.js +30 -15
  63. package/dist/multistream/remoteMedia.js.map +1 -1
  64. package/dist/multistream/remoteMediaManager.js +40 -8
  65. package/dist/multistream/remoteMediaManager.js.map +1 -1
  66. package/dist/multistream/sendSlotManager.js +24 -0
  67. package/dist/multistream/sendSlotManager.js.map +1 -1
  68. package/dist/reachability/clusterReachability.js +12 -15
  69. package/dist/reachability/clusterReachability.js.map +1 -1
  70. package/dist/reachability/index.js +471 -140
  71. package/dist/reachability/index.js.map +1 -1
  72. package/dist/{rtcMetrics/constants.js → reachability/reachability.types.js} +1 -5
  73. package/dist/reachability/reachability.types.js.map +1 -0
  74. package/dist/reachability/request.js +21 -8
  75. package/dist/reachability/request.js.map +1 -1
  76. package/dist/recording-controller/enums.js +8 -4
  77. package/dist/recording-controller/enums.js.map +1 -1
  78. package/dist/recording-controller/index.js +18 -9
  79. package/dist/recording-controller/index.js.map +1 -1
  80. package/dist/recording-controller/util.js +13 -9
  81. package/dist/recording-controller/util.js.map +1 -1
  82. package/dist/roap/index.js +15 -15
  83. package/dist/roap/index.js.map +1 -1
  84. package/dist/roap/request.js +45 -79
  85. package/dist/roap/request.js.map +1 -1
  86. package/dist/roap/turnDiscovery.js +3 -6
  87. package/dist/roap/turnDiscovery.js.map +1 -1
  88. package/dist/types/annotation/index.d.ts +5 -0
  89. package/dist/types/common/errors/join-forbidden-error.d.ts +15 -0
  90. package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
  91. package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
  92. package/dist/types/config.d.ts +2 -0
  93. package/dist/types/constants.d.ts +54 -1
  94. package/dist/types/index.d.ts +3 -3
  95. package/dist/types/locus-info/index.d.ts +2 -1
  96. package/dist/types/meeting/brbState.d.ts +54 -0
  97. package/dist/types/meeting/in-meeting-actions.d.ts +12 -0
  98. package/dist/types/meeting/index.d.ts +86 -14
  99. package/dist/types/meeting/locusMediaRequest.d.ts +6 -3
  100. package/dist/types/meeting/request.d.ts +14 -3
  101. package/dist/types/meeting/request.type.d.ts +6 -0
  102. package/dist/types/meeting/util.d.ts +3 -3
  103. package/dist/types/meeting-info/meeting-info-v2.d.ts +30 -5
  104. package/dist/types/meetings/index.d.ts +20 -2
  105. package/dist/types/meetings/meetings.types.d.ts +8 -0
  106. package/dist/types/member/index.d.ts +1 -0
  107. package/dist/types/member/types.d.ts +7 -0
  108. package/dist/types/members/util.d.ts +2 -0
  109. package/dist/types/metrics/constants.d.ts +6 -1
  110. package/dist/types/multistream/remoteMediaManager.d.ts +10 -1
  111. package/dist/types/multistream/sendSlotManager.d.ts +8 -1
  112. package/dist/types/reachability/clusterReachability.d.ts +1 -10
  113. package/dist/types/reachability/index.d.ts +83 -36
  114. package/dist/types/reachability/reachability.types.d.ts +64 -0
  115. package/dist/types/reachability/request.d.ts +5 -1
  116. package/dist/types/recording-controller/enums.d.ts +5 -2
  117. package/dist/types/recording-controller/index.d.ts +1 -0
  118. package/dist/types/recording-controller/util.d.ts +2 -1
  119. package/dist/types/roap/request.d.ts +1 -13
  120. package/dist/webinar/index.js +390 -7
  121. package/dist/webinar/index.js.map +1 -1
  122. package/package.json +23 -22
  123. package/src/annotation/index.ts +16 -0
  124. package/src/common/errors/join-forbidden-error.ts +26 -0
  125. package/src/common/errors/join-webinar-error.ts +24 -0
  126. package/src/common/errors/multistream-not-supported-error.ts +30 -0
  127. package/src/config.ts +2 -0
  128. package/src/constants.ts +62 -3
  129. package/src/index.ts +5 -3
  130. package/src/interpretation/index.ts +3 -3
  131. package/src/locus-info/index.ts +20 -3
  132. package/src/locus-info/selfUtils.ts +24 -6
  133. package/src/media/MediaConnectionAwaiter.ts +2 -0
  134. package/src/media/properties.ts +34 -13
  135. package/src/meeting/brbState.ts +169 -0
  136. package/src/meeting/in-meeting-actions.ts +25 -0
  137. package/src/meeting/index.ts +485 -88
  138. package/src/meeting/locusMediaRequest.ts +38 -12
  139. package/src/meeting/muteState.ts +1 -6
  140. package/src/meeting/request.ts +30 -12
  141. package/src/meeting/request.type.ts +7 -0
  142. package/src/meeting/util.ts +32 -13
  143. package/src/meeting-info/meeting-info-v2.ts +83 -12
  144. package/src/meeting-info/utilv2.ts +17 -3
  145. package/src/meetings/index.ts +79 -20
  146. package/src/meetings/meetings.types.ts +10 -0
  147. package/src/meetings/util.ts +2 -1
  148. package/src/member/index.ts +9 -0
  149. package/src/member/types.ts +8 -0
  150. package/src/member/util.ts +34 -24
  151. package/src/members/util.ts +1 -0
  152. package/src/metrics/constants.ts +6 -1
  153. package/src/multistream/remoteMedia.ts +28 -15
  154. package/src/multistream/remoteMediaManager.ts +32 -10
  155. package/src/multistream/sendSlotManager.ts +31 -0
  156. package/src/reachability/clusterReachability.ts +5 -15
  157. package/src/reachability/index.ts +315 -75
  158. package/src/reachability/reachability.types.ts +85 -0
  159. package/src/reachability/request.ts +55 -31
  160. package/src/recording-controller/enums.ts +5 -2
  161. package/src/recording-controller/index.ts +17 -4
  162. package/src/recording-controller/util.ts +28 -9
  163. package/src/roap/index.ts +14 -13
  164. package/src/roap/request.ts +30 -44
  165. package/src/roap/turnDiscovery.ts +2 -4
  166. package/src/webinar/index.ts +235 -9
  167. package/test/unit/spec/annotation/index.ts +46 -1
  168. package/test/unit/spec/interpretation/index.ts +39 -1
  169. package/test/unit/spec/locus-info/index.js +292 -60
  170. package/test/unit/spec/locus-info/selfConstant.js +7 -0
  171. package/test/unit/spec/locus-info/selfUtils.js +101 -1
  172. package/test/unit/spec/media/properties.ts +15 -0
  173. package/test/unit/spec/meeting/brbState.ts +114 -0
  174. package/test/unit/spec/meeting/in-meeting-actions.ts +15 -1
  175. package/test/unit/spec/meeting/index.js +908 -124
  176. package/test/unit/spec/meeting/locusMediaRequest.ts +111 -66
  177. package/test/unit/spec/meeting/muteState.js +0 -24
  178. package/test/unit/spec/meeting/request.js +3 -26
  179. package/test/unit/spec/meeting/utils.js +73 -28
  180. package/test/unit/spec/meeting-info/meetinginfov2.js +46 -4
  181. package/test/unit/spec/meeting-info/utilv2.js +26 -0
  182. package/test/unit/spec/meetings/index.js +172 -18
  183. package/test/unit/spec/meetings/utils.js +10 -0
  184. package/test/unit/spec/member/util.js +52 -11
  185. package/test/unit/spec/members/utils.js +95 -0
  186. package/test/unit/spec/multistream/remoteMedia.ts +11 -7
  187. package/test/unit/spec/multistream/remoteMediaManager.ts +397 -118
  188. package/test/unit/spec/reachability/clusterReachability.ts +7 -0
  189. package/test/unit/spec/reachability/index.ts +391 -9
  190. package/test/unit/spec/reachability/request.js +48 -12
  191. package/test/unit/spec/recording-controller/index.js +61 -5
  192. package/test/unit/spec/recording-controller/util.js +39 -3
  193. package/test/unit/spec/roap/index.ts +48 -1
  194. package/test/unit/spec/roap/request.ts +51 -109
  195. package/test/unit/spec/roap/turnDiscovery.ts +202 -147
  196. package/test/unit/spec/webinar/index.ts +509 -0
  197. package/dist/common/errors/webinar-registration-error.js.map +0 -1
  198. package/dist/networkQualityMonitor/index.js +0 -227
  199. package/dist/networkQualityMonitor/index.js.map +0 -1
  200. package/dist/rtcMetrics/constants.js.map +0 -1
  201. package/dist/rtcMetrics/index.js +0 -197
  202. package/dist/rtcMetrics/index.js.map +0 -1
  203. package/dist/types/networkQualityMonitor/index.d.ts +0 -70
  204. package/dist/types/rtcMetrics/constants.d.ts +0 -4
  205. package/dist/types/rtcMetrics/index.d.ts +0 -71
  206. package/src/common/errors/webinar-registration-error.ts +0 -27
@@ -91,14 +91,16 @@ 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,
103
+ MeetingInfoV2JoinForbiddenError,
102
104
  } from '../../../../src/meeting-info/meeting-info-v2';
103
105
  import {
104
106
  DTLS_HANDSHAKE_FAILED_CLIENT_CODE,
@@ -113,6 +115,9 @@ import {ERROR_DESCRIPTIONS} from '@webex/internal-plugin-metrics/src/call-diagno
113
115
  import MeetingCollection from '@webex/plugin-meetings/src/meetings/collection';
114
116
 
115
117
  import {EVENT_TRIGGERS as VOICEAEVENTS} from '@webex/internal-plugin-voicea';
118
+ import { createBrbState } from '@webex/plugin-meetings/src/meeting/brbState';
119
+ import JoinForbiddenError from '../../../../src/common/errors/join-forbidden-error';
120
+ import { EventEmitter } from 'stream';
116
121
 
117
122
  describe('plugin-meetings', () => {
118
123
  const logger = {
@@ -205,6 +210,7 @@ describe('plugin-meetings', () => {
205
210
  let membersSpy;
206
211
  let meetingRequestSpy;
207
212
  let correlationId;
213
+ let uploadEvent;
208
214
 
209
215
  beforeEach(() => {
210
216
  webex = new MockWebex({
@@ -244,6 +250,7 @@ describe('plugin-meetings', () => {
244
250
  isAnyPublicClusterReachable: sinon.stub().resolves(true),
245
251
  getReachabilityResults: sinon.stub().resolves(undefined),
246
252
  getReachabilityMetrics: sinon.stub().resolves({}),
253
+ stopReachability: sinon.stub(),
247
254
  };
248
255
  webex.internal.llm.on = sinon.stub();
249
256
  webex.internal.newMetrics.callDiagnosticLatencies = new CallDiagnosticLatencies(
@@ -273,6 +280,8 @@ describe('plugin-meetings', () => {
273
280
  test4 = `test4-${uuid.v4()}`;
274
281
  testDestination = `testDestination-${uuid.v4()}`;
275
282
  correlationId = uuid.v4();
283
+ uploadEvent = new EventEmitter();
284
+ uploadEvent.addListener('progress', () => {})
276
285
 
277
286
  meeting = new Meeting(
278
287
  {
@@ -649,11 +658,10 @@ describe('plugin-meetings', () => {
649
658
  });
650
659
 
651
660
  const fakeRoapMessage = {id: 'fake TURN discovery message'};
652
- const fakeReachabilityResults = {id: 'fake reachability'};
653
661
  const fakeTurnServerInfo = {id: 'fake turn info'};
654
662
  const fakeJoinResult = {id: 'join result'};
655
663
 
656
- const joinOptions = {correlationId: '12345'};
664
+ const joinOptions = {correlationId: '12345', enableMultistream: true};
657
665
  const mediaOptions = {audioEnabled: true, allowMediaInLobby: true};
658
666
 
659
667
  let generateTurnDiscoveryRequestMessageStub;
@@ -662,13 +670,14 @@ describe('plugin-meetings', () => {
662
670
  let addMediaInternalStub;
663
671
 
664
672
  beforeEach(() => {
665
- meeting.join = sinon.stub().returns(Promise.resolve(fakeJoinResult));
673
+ meeting.join = sinon.stub().callsFake((joinOptions) => {
674
+ meeting.isMultistream = joinOptions.enableMultistream;
675
+ return Promise.resolve(fakeJoinResult);
676
+ });
666
677
  addMediaInternalStub = sinon
667
678
  .stub(meeting, 'addMediaInternal')
668
679
  .returns(Promise.resolve(test4));
669
680
 
670
- webex.meetings.reachability.getReachabilityResults.resolves(fakeReachabilityResults);
671
-
672
681
  generateTurnDiscoveryRequestMessageStub = sinon
673
682
  .stub(meeting.roap, 'generateTurnDiscoveryRequestMessage')
674
683
  .resolves({roapMessage: fakeRoapMessage});
@@ -688,7 +697,6 @@ describe('plugin-meetings', () => {
688
697
  assert.calledOnceWithExactly(meeting.join, {
689
698
  ...joinOptions,
690
699
  roapMessage: fakeRoapMessage,
691
- reachability: fakeReachabilityResults,
692
700
  });
693
701
  assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true);
694
702
  assert.calledOnceWithExactly(
@@ -704,7 +712,7 @@ describe('plugin-meetings', () => {
704
712
  mediaOptions
705
713
  );
706
714
 
707
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
715
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
708
716
 
709
717
  // resets joinWithMediaRetryInfo
710
718
  assert.deepEqual(meeting.joinWithMediaRetryInfo, {
@@ -725,7 +733,6 @@ describe('plugin-meetings', () => {
725
733
  assert.calledOnceWithExactly(meeting.join, {
726
734
  ...joinOptions,
727
735
  roapMessage: undefined,
728
- reachability: fakeReachabilityResults,
729
736
  });
730
737
  assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true);
731
738
  assert.notCalled(handleTurnDiscoveryHttpResponseStub);
@@ -738,7 +745,7 @@ describe('plugin-meetings', () => {
738
745
  mediaOptions
739
746
  );
740
747
 
741
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
748
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
742
749
  assert.equal(meeting.turnServerUsed, false);
743
750
  });
744
751
 
@@ -757,7 +764,6 @@ describe('plugin-meetings', () => {
757
764
  assert.calledOnceWithExactly(meeting.join, {
758
765
  ...joinOptions,
759
766
  roapMessage: fakeRoapMessage,
760
- reachability: fakeReachabilityResults,
761
767
  });
762
768
  assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true);
763
769
  assert.calledOnceWithExactly(
@@ -774,7 +780,7 @@ describe('plugin-meetings', () => {
774
780
  mediaOptions
775
781
  );
776
782
 
777
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
783
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
778
784
  });
779
785
 
780
786
  it('should reject if join() fails', async () => {
@@ -861,7 +867,8 @@ describe('plugin-meetings', () => {
861
867
  }
862
868
  );
863
869
 
864
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
870
+ // expect multistreamEnabled: false, because this test overrides the join meeting.join stub so it doesn't set the isMultistream flag
871
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: false});
865
872
 
866
873
  // resets joinWithMediaRetryInfo
867
874
  assert.deepEqual(meeting.joinWithMediaRetryInfo, {
@@ -950,7 +957,7 @@ describe('plugin-meetings', () => {
950
957
  mediaOptions,
951
958
  });
952
959
 
953
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
960
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
954
961
 
955
962
  assert.calledOnce(meeting.join);
956
963
  assert.notCalled(leaveStub);
@@ -1044,6 +1051,7 @@ describe('plugin-meetings', () => {
1044
1051
  getConnectionState: sinon.stub().returns(ConnectionState.Connected),
1045
1052
  initiateOffer: sinon.stub().resolves({}),
1046
1053
  on: sinon.stub(),
1054
+ createSendSlot: sinon.stub(),
1047
1055
  };
1048
1056
 
1049
1057
  /* Setup the stubs so that the first call to addMediaInternal() fails
@@ -1060,12 +1068,18 @@ describe('plugin-meetings', () => {
1060
1068
 
1061
1069
  sinon.stub(meeting.roap, 'doTurnDiscovery').resolves({turnServerInfo: 'fake turn info'});
1062
1070
 
1071
+ // calling joinWithMedia() with enableMultistream=false, because this test uses real addMediaInternal() implementation
1072
+ // and it requires less stubs when it's without multistream
1063
1073
  const result = await meeting.joinWithMedia({
1064
- joinOptions,
1074
+ joinOptions: {...joinOptions, enableMultistream: false},
1065
1075
  mediaOptions,
1066
1076
  });
1067
1077
 
1068
- assert.deepEqual(result, {join: fakeJoinResult, media: undefined});
1078
+ assert.deepEqual(result, {
1079
+ join: fakeJoinResult,
1080
+ media: undefined,
1081
+ multistreamEnabled: false,
1082
+ });
1069
1083
 
1070
1084
  assert.calledOnce(meeting.join);
1071
1085
 
@@ -1140,6 +1154,7 @@ describe('plugin-meetings', () => {
1140
1154
  addMediaError.name = 'SdpOfferCreationError';
1141
1155
 
1142
1156
  meeting.addMediaInternal.rejects(addMediaError);
1157
+ sinon.stub(meeting, 'leave').resolves();
1143
1158
 
1144
1159
  await assert.isRejected(
1145
1160
  meeting.joinWithMedia({
@@ -1168,6 +1183,10 @@ describe('plugin-meetings', () => {
1168
1183
  type: addMediaError.name,
1169
1184
  }
1170
1185
  );
1186
+ assert.calledOnceWithExactly(meeting.leave, {
1187
+ resourceId: undefined,
1188
+ reason: 'joinWithMedia failure',
1189
+ });
1171
1190
  });
1172
1191
  });
1173
1192
 
@@ -1244,6 +1263,7 @@ describe('plugin-meetings', () => {
1244
1263
  webex.internal.voicea.off = sinon.stub();
1245
1264
  webex.internal.voicea.listenToEvents = sinon.stub();
1246
1265
  webex.internal.voicea.turnOnCaptions = sinon.stub();
1266
+ webex.internal.voicea.deregisterEvents = sinon.stub();
1247
1267
  });
1248
1268
 
1249
1269
  it('should stop listening to voicea events and also trigger a stop event', () => {
@@ -1572,6 +1592,55 @@ describe('plugin-meetings', () => {
1572
1592
  fakeProcessedReaction
1573
1593
  );
1574
1594
  });
1595
+
1596
+ it('should fail quietly if participantId does not exist in membersCollection', () => {
1597
+ LoggerProxy.logger.warn = sinon.stub();
1598
+ meeting.isReactionsSupported = sinon.stub().returns(true);
1599
+ meeting.config.receiveReactions = true;
1600
+ const fakeSendersName = 'Fake reactors name';
1601
+ const fakeReactionPayload = {
1602
+ type: 'fake_type',
1603
+ codepoints: 'fake_codepoints',
1604
+ shortcodes: 'fake_shortcodes',
1605
+ tone: {
1606
+ type: 'fake_tone_type',
1607
+ codepoints: 'fake_tone_codepoints',
1608
+ shortcodes: 'fake_tone_shortcodes',
1609
+ },
1610
+ };
1611
+ const fakeSenderPayload = {
1612
+ participantId: 'fake_participant_id',
1613
+ };
1614
+ const fakeProcessedReaction = {
1615
+ reaction: fakeReactionPayload,
1616
+ sender: {
1617
+ id: fakeSenderPayload.participantId,
1618
+ name: fakeSendersName,
1619
+ },
1620
+ };
1621
+ const fakeRelayEvent = {
1622
+ data: {
1623
+ relayType: REACTION_RELAY_TYPES.REACTION,
1624
+ reaction: fakeReactionPayload,
1625
+ sender: fakeSenderPayload,
1626
+ },
1627
+ };
1628
+ meeting.processRelayEvent(fakeRelayEvent);
1629
+ assert.calledWith(
1630
+ LoggerProxy.logger.warn,
1631
+ `Meeting:index#processRelayEvent --> Skipping handling of react for ${meeting.id}. participantId fake_participant_id does not exist in membersCollection.`
1632
+ );
1633
+ assert.neverCalledWith(
1634
+ TriggerProxy.trigger,
1635
+ sinon.match.instanceOf(Meeting),
1636
+ {
1637
+ file: 'meeting/index',
1638
+ function: 'join',
1639
+ },
1640
+ EVENT_TRIGGERS.MEETING_RECEIVE_REACTIONS,
1641
+ fakeProcessedReaction
1642
+ );
1643
+ });
1575
1644
  });
1576
1645
 
1577
1646
  describe('#handleLLMOnline', () => {
@@ -1711,6 +1780,12 @@ describe('plugin-meetings', () => {
1711
1780
  sinon.assert.called(setCorrelationIdSpy);
1712
1781
  assert.equal(meeting.correlationId, '123');
1713
1782
  });
1783
+
1784
+ it('should not send client.call.initiated if told not to', async () => {
1785
+ await meeting.join({sendCallInitiated: false});
1786
+
1787
+ sinon.assert.notCalled(webex.internal.newMetrics.submitClientEvent);
1788
+ });
1714
1789
  });
1715
1790
 
1716
1791
  describe('failure', () => {
@@ -2034,6 +2109,7 @@ describe('plugin-meetings', () => {
2034
2109
  someReachabilityMetric1: 'some value1',
2035
2110
  someReachabilityMetric2: 'some value2',
2036
2111
  }),
2112
+ stopReachability: sinon.stub(),
2037
2113
  };
2038
2114
 
2039
2115
  const forceRtcMetricsSend = sinon.stub().resolves();
@@ -2453,6 +2529,7 @@ describe('plugin-meetings', () => {
2453
2529
  assert.calledOnce(meeting.setMercuryListener);
2454
2530
  assert.calledOnce(fakeMediaConnection.initiateOffer);
2455
2531
  assert.equal(meeting.allowMediaInLobby, allowMediaInLobby);
2532
+ assert.calledOnce(webex.meetings.reachability.stopReachability);
2456
2533
  };
2457
2534
 
2458
2535
  it('should attach the media and return promise', async () => {
@@ -2471,6 +2548,61 @@ describe('plugin-meetings', () => {
2471
2548
  checkWorking();
2472
2549
  });
2473
2550
 
2551
+ it('should upload logs periodically', async () => {
2552
+ const clock = sinon.useFakeTimers();
2553
+
2554
+ meeting.roap.doTurnDiscovery = sinon
2555
+ .stub()
2556
+ .resolves({turnServerInfo: undefined, turnDiscoverySkippedReason: undefined});
2557
+
2558
+ let logUploadCounter = 0;
2559
+
2560
+ TriggerProxy.trigger.callsFake((meetingObject, options, event) => {
2561
+ if (
2562
+ meetingObject === meeting &&
2563
+ options.file === 'meeting/index' &&
2564
+ options.function === 'uploadLogs' &&
2565
+ event === 'REQUEST_UPLOAD_LOGS'
2566
+ ) {
2567
+ logUploadCounter += 1;
2568
+ }
2569
+ });
2570
+
2571
+ meeting.config.logUploadIntervalMultiplicationFactor = 1;
2572
+ meeting.meetingState = 'ACTIVE';
2573
+
2574
+ await meeting.addMedia({
2575
+ mediaSettings: {},
2576
+ });
2577
+
2578
+ const checkLogCounter = (delayInMinutes, expectedCounter) => {
2579
+ const delayInMilliseconds = delayInMinutes * 60 * 1000;
2580
+
2581
+ // first check that the counter is not increased just before the delay
2582
+ clock.tick(delayInMilliseconds - 50);
2583
+ assert.equal(logUploadCounter, expectedCounter - 1);
2584
+
2585
+ // and now check that it has reached expected value after the delay
2586
+ clock.tick(50);
2587
+ assert.equal(logUploadCounter, expectedCounter);
2588
+ };
2589
+
2590
+ checkLogCounter(0.1, 1);
2591
+ checkLogCounter(15, 2);
2592
+ checkLogCounter(30, 3);
2593
+ checkLogCounter(60, 4);
2594
+ checkLogCounter(60, 5);
2595
+
2596
+ // simulate media connection being removed -> 1 more upload should happen, but nothing more afterwards
2597
+ meeting.mediaProperties.webrtcMediaConnection = undefined;
2598
+ checkLogCounter(60, 6);
2599
+
2600
+ clock.tick(120 * 1000 * 60);
2601
+ assert.equal(logUploadCounter, 6);
2602
+
2603
+ clock.restore();
2604
+ });
2605
+
2474
2606
  it('should attach the media and return promise when in the lobby if allowMediaInLobby is set', async () => {
2475
2607
  meeting.roap.doTurnDiscovery = sinon
2476
2608
  .stub()
@@ -2593,6 +2725,7 @@ describe('plugin-meetings', () => {
2593
2725
  webex.meetings.reachability = {
2594
2726
  isWebexMediaBackendUnreachable: sinon.stub().resolves(false),
2595
2727
  getReachabilityMetrics: sinon.stub().resolves(),
2728
+ stopReachability: sinon.stub(),
2596
2729
  };
2597
2730
  const MOCK_CLIENT_ERROR_CODE = 2004;
2598
2731
  const generateClientErrorCodeForIceFailureStub = sinon
@@ -2801,6 +2934,7 @@ describe('plugin-meetings', () => {
2801
2934
  .onCall(2)
2802
2935
  .resolves(false),
2803
2936
  getReachabilityMetrics: sinon.stub().resolves({}),
2937
+ stopReachability: sinon.stub(),
2804
2938
  };
2805
2939
  const getErrorPayloadForClientErrorCodeStub =
2806
2940
  (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
@@ -3095,6 +3229,7 @@ describe('plugin-meetings', () => {
3095
3229
  someReachabilityMetric1: 'some value1',
3096
3230
  someReachabilityMetric2: 'some value2',
3097
3231
  }),
3232
+ stopReachability: sinon.stub(),
3098
3233
  };
3099
3234
  meeting.iceCandidatesCount = 3;
3100
3235
  meeting.iceCandidateErrors.set('701_error', 3);
@@ -3424,6 +3559,55 @@ describe('plugin-meetings', () => {
3424
3559
  });
3425
3560
  });
3426
3561
 
3562
+ it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
3563
+ let fakeMembersCollection = {
3564
+ members: {
3565
+ member1: {isInMeeting: true},
3566
+ member2: {isInMeeting: true},
3567
+ member3: {isInMeeting: false},
3568
+ },
3569
+ };
3570
+ sinon.stub(meeting, 'getMembers').returns({membersCollection: fakeMembersCollection});
3571
+ const fakeData = {intervalMetadata: {}, networkType: 'wifi'};
3572
+
3573
+ statsAnalyzerStub.emit(
3574
+ {file: 'test', function: 'test'},
3575
+ StatsAnalyzerEventNames.MEDIA_QUALITY,
3576
+ {data: fakeData}
3577
+ );
3578
+
3579
+ assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
3580
+ name: 'client.mediaquality.event',
3581
+ options: {
3582
+ meetingId: meeting.id,
3583
+ },
3584
+ payload: {
3585
+ intervals: [
3586
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2)),
3587
+ ],
3588
+ },
3589
+ });
3590
+ fakeMembersCollection.members.member2.isInMeeting = false;
3591
+
3592
+ statsAnalyzerStub.emit(
3593
+ {file: 'test', function: 'test'},
3594
+ StatsAnalyzerEventNames.MEDIA_QUALITY,
3595
+ {data: fakeData}
3596
+ );
3597
+
3598
+ assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
3599
+ name: 'client.mediaquality.event',
3600
+ options: {
3601
+ meetingId: meeting.id,
3602
+ },
3603
+ payload: {
3604
+ intervals: [
3605
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1)),
3606
+ ],
3607
+ },
3608
+ });
3609
+ });
3610
+
3427
3611
  it('calls submitMQE correctly', async () => {
3428
3612
  const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'};
3429
3613
 
@@ -3501,14 +3685,6 @@ describe('plugin-meetings', () => {
3501
3685
  });
3502
3686
  });
3503
3687
 
3504
- it('succeeds even if getDevices() throws', async () => {
3505
- meeting.meetingState = 'ACTIVE';
3506
-
3507
- sinon.stub(InternalMediaCoreModule, 'getDevices').rejects(new Error('fake error'));
3508
-
3509
- await meeting.addMedia();
3510
- });
3511
-
3512
3688
  describe('CA ice failures checks', () => {
3513
3689
  [
3514
3690
  {
@@ -3562,6 +3738,7 @@ describe('plugin-meetings', () => {
3562
3738
 
3563
3739
  webex.meetings.reachability = {
3564
3740
  isWebexMediaBackendUnreachable: sinon.stub().resolves(unreachable || false),
3741
+ stopReachability: sinon.stub(),
3565
3742
  };
3566
3743
 
3567
3744
  const generateClientErrorCodeForIceFailureStub = sinon
@@ -3650,6 +3827,72 @@ describe('plugin-meetings', () => {
3650
3827
  });
3651
3828
  });
3652
3829
 
3830
+ describe(`#beRightBack`, () => {
3831
+ const fakeMultistreamRoapMediaConnection = {
3832
+ createSendSlot: sinon.stub().returns({
3833
+ setSourceStateOverride: sinon.stub().resolves(),
3834
+ clearSourceStateOverride: sinon.stub().resolves(),
3835
+ }),
3836
+ };
3837
+
3838
+ beforeEach(() => {
3839
+ meeting.mediaProperties.webrtcMediaConnection = {createSendSlot: sinon.stub()};
3840
+ meeting.sendSlotManager.createSlot(
3841
+ fakeMultistreamRoapMediaConnection,
3842
+ MediaType.VideoMain
3843
+ );
3844
+
3845
+ meeting.locusUrl = 'locus url';
3846
+ meeting.deviceUrl = 'device url';
3847
+ meeting.selfId = 'self id';
3848
+ meeting.brbState = createBrbState(meeting, false);
3849
+ meeting.brbState.enable = sinon.stub().resolves();
3850
+ });
3851
+
3852
+ afterEach(() => {
3853
+ sinon.restore();
3854
+ });
3855
+
3856
+ it('should have #beRightBack', () => {
3857
+ assert.exists(meeting.beRightBack);
3858
+ });
3859
+
3860
+ describe('when in a multistream meeting', () => {
3861
+ beforeEach(() => {
3862
+ meeting.isMultistream = true;
3863
+ });
3864
+
3865
+ it('should enable #beRightBack and return a promise', async () => {
3866
+ const brbResult = meeting.beRightBack(true);
3867
+
3868
+ await brbResult;
3869
+ assert.exists(brbResult.then);
3870
+ assert.calledOnce(meeting.brbState.enable);
3871
+ });
3872
+
3873
+ it('should disable #beRightBack and return a promise', async () => {
3874
+ const brbResult = meeting.beRightBack(false);
3875
+
3876
+ await brbResult;
3877
+ assert.exists(brbResult.then);
3878
+ assert.calledOnce(meeting.brbState.enable);
3879
+ });
3880
+
3881
+ it('should throw an error and reject the promise if setBrb fails', async () => {
3882
+ const error = new Error('setBrb failed');
3883
+ meeting.brbState.enable.rejects(error);
3884
+
3885
+ try {
3886
+ await meeting.beRightBack(true);
3887
+ } catch (err) {
3888
+ assert.instanceOf(err, Error);
3889
+ assert.equal(err.message, 'setBrb failed');
3890
+ assert.isRejected((Promise.reject()));
3891
+ }
3892
+ });
3893
+ });
3894
+ });
3895
+
3653
3896
  /* This set of tests are like semi-integration tests, they use real MuteState, Media, LocusMediaRequest and Roap classes.
3654
3897
  They mock the @webex/internal-media-core and sending of /media http requests to Locus.
3655
3898
  Their main purpose is to test that we send the right http requests to Locus and make right calls
@@ -3692,6 +3935,12 @@ describe('plugin-meetings', () => {
3692
3935
  meeting.setMercuryListener = sinon.stub();
3693
3936
  meeting.locusInfo.onFullLocus = sinon.stub();
3694
3937
  meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
3938
+ meeting.webex.meetings.reachability.getReachabilityReportToAttachToRoap = sinon
3939
+ .stub()
3940
+ .resolves({id: 'fake reachability'});
3941
+ meeting.webex.meetings.reachability.getClientMediaPreferences = sinon
3942
+ .stub()
3943
+ .resolves({id: 'fake clientMediaPreferences'});
3695
3944
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3696
3945
  turnServerInfo: {
3697
3946
  url: 'turns:turn-server-url:443?transport=tcp',
@@ -3772,6 +4021,7 @@ describe('plugin-meetings', () => {
3772
4021
  initiateOffer: sinon.stub().resolves({}),
3773
4022
  update: sinon.stub().resolves({}),
3774
4023
  on: sinon.stub(),
4024
+ roapMessageReceived: sinon.stub(),
3775
4025
  };
3776
4026
 
3777
4027
  fakeMultistreamRoapMediaConnection = {
@@ -3800,7 +4050,7 @@ describe('plugin-meetings', () => {
3800
4050
 
3801
4051
  locusMediaRequestStub = sinon
3802
4052
  .stub(WebexPlugin.prototype, 'request')
3803
- .resolves({body: {locus: {fullState: {}}}});
4053
+ .resolves({body: {locus: {fullState: {}}}, upload: sinon.match.instanceOf(EventEmitter), download: sinon.match.instanceOf(EventEmitter)});
3804
4054
 
3805
4055
  // setup some things and mocks so that the call to join() works
3806
4056
  // (we need to call join() because it creates the LocusMediaRequest instance
@@ -3858,8 +4108,10 @@ describe('plugin-meetings', () => {
3858
4108
  };
3859
4109
 
3860
4110
  // simulates a Roap offer being generated by the RoapMediaConnection
3861
- const simulateRoapOffer = async () => {
3862
- meeting.deferSDPAnswer = {resolve: sinon.stub()};
4111
+ const simulateRoapOffer = async (stubWaitingForAnswer = true) => {
4112
+ if (stubWaitingForAnswer) {
4113
+ meeting.deferSDPAnswer = {resolve: sinon.stub()};
4114
+ }
3863
4115
  const roapListener = getRoapListener();
3864
4116
 
3865
4117
  await roapListener({roapMessage: roapOfferMessage});
@@ -3877,6 +4129,15 @@ describe('plugin-meetings', () => {
3877
4129
  const checkSdpOfferSent = ({audioMuted, videoMuted}) => {
3878
4130
  const {sdp, seq, tieBreaker} = roapOfferMessage;
3879
4131
 
4132
+ assert.calledWith(
4133
+ meeting.webex.meetings.reachability.getClientMediaPreferences,
4134
+ meeting.isMultistream,
4135
+ 0
4136
+ );
4137
+ assert.calledWith(
4138
+ meeting.webex.meetings.reachability.getReachabilityReportToAttachToRoap
4139
+ );
4140
+
3880
4141
  assert.calledWith(locusMediaRequestStub, {
3881
4142
  method: 'PUT',
3882
4143
  uri: `${meeting.selfUrl}/media`,
@@ -3890,16 +4151,16 @@ describe('plugin-meetings', () => {
3890
4151
  correlationId: meeting.correlationId,
3891
4152
  localMedias: [
3892
4153
  {
3893
- localSdp: `{"audioMuted":${audioMuted},"videoMuted":${videoMuted},"roapMessage":{"messageType":"OFFER","sdps":["${sdp}"],"version":"2","seq":"${seq}","tieBreaker":"${tieBreaker}","headers":["includeAnswerInHttpResponse","noOkInTransaction"]}}`,
4154
+ localSdp: `{"audioMuted":${audioMuted},"videoMuted":${videoMuted},"roapMessage":{"messageType":"OFFER","sdps":["${sdp}"],"version":"2","seq":"${seq}","tieBreaker":"${tieBreaker}","headers":["includeAnswerInHttpResponse","noOkInTransaction"]},"reachability":{"id":"fake reachability"}}`,
3894
4155
  mediaId: 'fake media id',
3895
4156
  },
3896
4157
  ],
3897
4158
  clientMediaPreferences: {
3898
- preferTranscoding: !meeting.isMultistream,
3899
- joinCookie: undefined,
3900
- ipver: 0,
4159
+ id: 'fake clientMediaPreferences',
3901
4160
  },
3902
4161
  },
4162
+ upload: sinon.match.instanceOf(EventEmitter),
4163
+ download: sinon.match.instanceOf(EventEmitter),
3903
4164
  });
3904
4165
  };
3905
4166
 
@@ -3918,17 +4179,17 @@ describe('plugin-meetings', () => {
3918
4179
  },
3919
4180
  correlationId: meeting.correlationId,
3920
4181
  clientMediaPreferences: {
3921
- preferTranscoding: !meeting.isMultistream,
3922
- ipver: undefined,
3923
- joinCookie: undefined,
4182
+ id: 'fake clientMediaPreferences',
3924
4183
  },
3925
4184
  localMedias: [
3926
4185
  {
3927
- localSdp: `{"audioMuted":${audioMuted},"videoMuted":${videoMuted},"roapMessage":{"messageType":"OK","version":"2","seq":"${seq}"}}`,
4186
+ localSdp: `{"audioMuted":${audioMuted},"videoMuted":${videoMuted},"roapMessage":{"messageType":"OK","version":"2","seq":"${seq}"},"reachability":{"id":"fake reachability"}}`,
3928
4187
  mediaId: 'fake media id',
3929
4188
  },
3930
4189
  ],
3931
4190
  },
4191
+ upload: sinon.match.instanceOf(EventEmitter),
4192
+ download: sinon.match.instanceOf(EventEmitter),
3932
4193
  });
3933
4194
  };
3934
4195
 
@@ -3950,13 +4211,11 @@ describe('plugin-meetings', () => {
3950
4211
  mediaId: 'fake media id',
3951
4212
  },
3952
4213
  ],
3953
- clientMediaPreferences: {
3954
- preferTranscoding: !meeting.isMultistream,
3955
- ipver: undefined,
3956
- },
3957
4214
  respOnlySdp: true,
3958
4215
  usingResource: null,
3959
4216
  },
4217
+ upload: sinon.match.instanceOf(EventEmitter),
4218
+ download: sinon.match.instanceOf(EventEmitter),
3960
4219
  });
3961
4220
  };
3962
4221
 
@@ -3967,8 +4226,9 @@ describe('plugin-meetings', () => {
3967
4226
  remoteQualityLevel,
3968
4227
  expectedDebugId,
3969
4228
  meetingId,
4229
+ expectMultistream = isMultistream,
3970
4230
  }) => {
3971
- if (isMultistream) {
4231
+ if (expectMultistream) {
3972
4232
  const {iceServers} = mediaConnectionConfig;
3973
4233
 
3974
4234
  assert.calledOnceWithMatch(
@@ -4128,7 +4388,6 @@ describe('plugin-meetings', () => {
4128
4388
  });
4129
4389
 
4130
4390
  it('addMedia() works correctly when media is enabled with streams to publish', async () => {
4131
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4132
4391
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
4133
4392
  await simulateRoapOffer();
4134
4393
  await simulateRoapOk();
@@ -4159,12 +4418,9 @@ describe('plugin-meetings', () => {
4159
4418
 
4160
4419
  // and that these were the only /media requests that were sent
4161
4420
  assert.calledTwice(locusMediaRequestStub);
4162
-
4163
- assert.calledOnce(handleDeviceLoggingSpy);
4164
4421
  });
4165
4422
 
4166
4423
  it('addMedia() works correctly when media is enabled with streams to publish and stream is user muted', async () => {
4167
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4168
4424
  fakeMicrophoneStream.userMuted = true;
4169
4425
 
4170
4426
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
@@ -4196,7 +4452,6 @@ describe('plugin-meetings', () => {
4196
4452
 
4197
4453
  // and that these were the only /media requests that were sent
4198
4454
  assert.calledTwice(locusMediaRequestStub);
4199
- assert.calledOnce(handleDeviceLoggingSpy);
4200
4455
  });
4201
4456
 
4202
4457
  it('addMedia() works correctly when media is enabled with tracks to publish and track is ended', async () => {
@@ -4268,7 +4523,6 @@ describe('plugin-meetings', () => {
4268
4523
  });
4269
4524
 
4270
4525
  it('addMedia() works correctly when media is disabled with streams to publish', async () => {
4271
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4272
4526
  await meeting.addMedia({
4273
4527
  localStreams: {microphone: fakeMicrophoneStream},
4274
4528
  audioEnabled: false,
@@ -4302,20 +4556,6 @@ describe('plugin-meetings', () => {
4302
4556
 
4303
4557
  // and that these were the only /media requests that were sent
4304
4558
  assert.calledTwice(locusMediaRequestStub);
4305
- assert.calledOnce(handleDeviceLoggingSpy);
4306
- });
4307
-
4308
- it('handleDeviceLogging not called when media is disabled', async () => {
4309
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4310
- await meeting.addMedia({
4311
- localStreams: {microphone: fakeMicrophoneStream},
4312
- audioEnabled: false,
4313
- videoEnabled: false,
4314
- });
4315
- await simulateRoapOffer();
4316
- await simulateRoapOk();
4317
-
4318
- assert.notCalled(handleDeviceLoggingSpy);
4319
4559
  });
4320
4560
 
4321
4561
  it('addMedia() works correctly when media is disabled with no streams to publish', async () => {
@@ -4351,20 +4591,6 @@ describe('plugin-meetings', () => {
4351
4591
  assert.calledTwice(locusMediaRequestStub);
4352
4592
  });
4353
4593
 
4354
- it('addMedia() works correctly when media is disabled with no streams to publish', async () => {
4355
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4356
- await meeting.addMedia({audioEnabled: false});
4357
- //calling handleDeviceLogging with audioEnaled as true adn videoEnabled as false
4358
- assert.calledWith(handleDeviceLoggingSpy, false, true);
4359
- });
4360
-
4361
- it('addMedia() works correctly when video is disabled with no streams to publish', async () => {
4362
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4363
- await meeting.addMedia({videoEnabled: false});
4364
- //calling handleDeviceLogging audioEnabled as true videoEnabled as false
4365
- assert.calledWith(handleDeviceLoggingSpy, true, false);
4366
- });
4367
-
4368
4594
  it('addMedia() works correctly when video is disabled with no streams to publish', async () => {
4369
4595
  await meeting.addMedia({videoEnabled: false});
4370
4596
  await simulateRoapOffer();
@@ -4431,13 +4657,6 @@ describe('plugin-meetings', () => {
4431
4657
  assert.calledTwice(locusMediaRequestStub);
4432
4658
  });
4433
4659
 
4434
- it('addMedia() works correctly when both shareAudio and shareVideo is disabled with no streams publish', async () => {
4435
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4436
- await meeting.addMedia({shareAudioEnabled: false, shareVideoEnabled: false});
4437
- //calling handleDeviceLogging with audioEnabled true and videoEnabled as true
4438
- assert.calledWith(handleDeviceLoggingSpy, true, true);
4439
- });
4440
-
4441
4660
  describe('publishStreams()/unpublishStreams() calls', () => {
4442
4661
  [
4443
4662
  {mediaEnabled: true, expected: {direction: 'sendrecv', localMuteSentValue: false}},
@@ -4833,6 +5052,211 @@ describe('plugin-meetings', () => {
4833
5052
  assert.notCalled(fakeRoapMediaConnection.update);
4834
5053
  })
4835
5054
  );
5055
+
5056
+ if (isMultistream) {
5057
+ describe('fallback from multistream to transcoded', () => {
5058
+ let multistreamEventListeners;
5059
+ let transcodedEventListeners;
5060
+ let mockStatsAnalyzerCtor;
5061
+
5062
+ const setupFakeRoapMediaConnection = (fakeRoapMediaConnection, eventListeners) => {
5063
+ fakeRoapMediaConnection.on.callsFake((eventName, cb) => {
5064
+ eventListeners[eventName] = cb;
5065
+ });
5066
+ fakeRoapMediaConnection.initiateOffer.callsFake(() => {
5067
+ // simulate offer being generated
5068
+ eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED]();
5069
+
5070
+ return Promise.resolve();
5071
+ });
5072
+ };
5073
+
5074
+ beforeEach(() => {
5075
+ multistreamEventListeners = {};
5076
+ transcodedEventListeners = {};
5077
+
5078
+ meeting.config.stats.enableStatsAnalyzer = true;
5079
+
5080
+ setupFakeRoapMediaConnection(fakeRoapMediaConnection, transcodedEventListeners);
5081
+ setupFakeRoapMediaConnection(
5082
+ fakeMultistreamRoapMediaConnection,
5083
+ multistreamEventListeners
5084
+ );
5085
+
5086
+ mockStatsAnalyzerCtor = sinon
5087
+ .stub(InternalMediaCoreModule, 'StatsAnalyzer')
5088
+ .callsFake(() => {
5089
+ return {on: sinon.stub(), stopAnalyzer: sinon.stub()};
5090
+ });
5091
+
5092
+ webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
5093
+ sinon.stub();
5094
+
5095
+ // setup the mock so that we get an SDP answer not from Homer
5096
+ locusMediaRequestStub.callsFake(() => {
5097
+ return Promise.resolve({
5098
+ body: {
5099
+ locus: {},
5100
+ mediaConnections: [
5101
+ {
5102
+ remoteSdp:
5103
+ '{"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"}',
5104
+ },
5105
+ ],
5106
+ },
5107
+ });
5108
+ });
5109
+
5110
+ sinon.stub(meeting, 'closePeerConnections');
5111
+ sinon.stub(meeting.mediaProperties, 'unsetPeerConnection');
5112
+ sinon.stub(meeting.locusMediaRequest, 'downgradeFromMultistreamToTranscoded');
5113
+ });
5114
+
5115
+ const runCheck = async (turnServerInfo, forceTurnDiscovery) => {
5116
+ // we're calling addMediaInternal() with mic stream,
5117
+ // so that we also verify that audioMute, videoMute info is correctly sent to backend
5118
+ const addMediaPromise = meeting.addMediaInternal(
5119
+ () => '',
5120
+ turnServerInfo,
5121
+ forceTurnDiscovery,
5122
+ {
5123
+ localStreams: {microphone: fakeMicrophoneStream},
5124
+ }
5125
+ );
5126
+ await testUtils.flushPromises();
5127
+ await simulateRoapOffer(false);
5128
+
5129
+ // check MultistreamRoapMediaConnection was created correctly
5130
+ checkMediaConnectionCreated({
5131
+ expectMultistream: true,
5132
+ mediaConnectionConfig: expectedMediaConnectionConfig,
5133
+ localStreams: {
5134
+ audio: fakeMicrophoneStream,
5135
+ video: undefined,
5136
+ screenShareVideo: undefined,
5137
+ screenShareAudio: undefined,
5138
+ },
5139
+ direction: {
5140
+ audio: 'sendrecv',
5141
+ video: 'sendrecv',
5142
+ screenShare: 'recvonly',
5143
+ },
5144
+ remoteQualityLevel: 'HIGH',
5145
+ expectedDebugId,
5146
+ meetingId: meeting.id,
5147
+ });
5148
+
5149
+ // 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
5150
+ assert.calledOnceWithExactly(
5151
+ mockStatsAnalyzerCtor,
5152
+ sinon.match({
5153
+ isMultistream: true,
5154
+ })
5155
+ );
5156
+ const initialStatsAnalyzer = mockStatsAnalyzerCtor.returnValues[0];
5157
+ mockStatsAnalyzerCtor.resetHistory();
5158
+
5159
+ // TURN discovery was done (if needed)
5160
+ if (turnServerInfo) {
5161
+ assert.notCalled(meeting.roap.doTurnDiscovery);
5162
+ } else {
5163
+ assert.calledWith(meeting.roap.doTurnDiscovery, meeting, false, false);
5164
+ }
5165
+
5166
+ // and SDP offer was sent with the right audioMuted/videoMuted values
5167
+ checkSdpOfferSent({audioMuted: false, videoMuted: true});
5168
+
5169
+ await testUtils.flushPromises();
5170
+
5171
+ // at this point the meeting should have been downgraded to transcoded
5172
+ assert.equal(meeting.isMultistream, false);
5173
+
5174
+ // old stats analyzer stopped and new one created
5175
+ assert.calledOnce(initialStatsAnalyzer.stopAnalyzer);
5176
+ assert.calledOnceWithExactly(
5177
+ mockStatsAnalyzerCtor,
5178
+ sinon.match({
5179
+ isMultistream: false,
5180
+ })
5181
+ );
5182
+
5183
+ // and correct cleanup of other things should have been done
5184
+ assert.calledOnceWithExactly(meeting.closePeerConnections, false);
5185
+ assert.calledOnceWithExactly(meeting.mediaProperties.unsetPeerConnection);
5186
+ assert.calledOnceWithExactly(
5187
+ meeting.locusMediaRequest.downgradeFromMultistreamToTranscoded
5188
+ );
5189
+
5190
+ // new connection should have been created
5191
+ checkMediaConnectionCreated({
5192
+ expectMultistream: false,
5193
+ mediaConnectionConfig: expectedMediaConnectionConfig,
5194
+ localStreams: {
5195
+ audio: fakeMicrophoneStream,
5196
+ video: undefined,
5197
+ screenShareVideo: undefined,
5198
+ screenShareAudio: undefined,
5199
+ },
5200
+ direction: {
5201
+ audio: 'sendrecv',
5202
+ video: 'sendrecv',
5203
+ screenShare: 'recvonly',
5204
+ },
5205
+ remoteQualityLevel: 'HIGH',
5206
+ expectedDebugId,
5207
+ meetingId: meeting.id,
5208
+ });
5209
+
5210
+ // and new TURN discovery done (no matter if it was being done before or not)
5211
+ assert.calledWith(meeting.roap.doTurnDiscovery, meeting, true, true);
5212
+
5213
+ // simulate new offer
5214
+ await simulateRoapOffer(false);
5215
+ checkSdpOfferSent({audioMuted: false, videoMuted: true});
5216
+
5217
+ // overall there should have been 2 calls to locusMediaRequestStub, because 2 offers were sent
5218
+ assert.calledTwice(locusMediaRequestStub);
5219
+
5220
+ // simulate answer being processed correctly
5221
+ transcodedEventListeners[MediaConnectionEventNames.REMOTE_SDP_ANSWER_PROCESSED]();
5222
+
5223
+ // check that addMedia finally resolved
5224
+ await addMediaPromise;
5225
+ };
5226
+
5227
+ it('addMedia() falls back to transcoded if SDP answer is not from Homer', async () => {
5228
+ // call addMediaInternal like addMedia() does it
5229
+ await runCheck(undefined, false);
5230
+ });
5231
+
5232
+ it('addMediaInternal() correctly falls back to transcoded if SDP answer is not from Homer (joinWithMedia() case)', async () => {
5233
+ // call addMediaInternal the way joinWithMedia() does it - with TURN info already provided
5234
+ // and check that when we fallback to transcoded we still do another TURN discovery
5235
+ await runCheck(
5236
+ {
5237
+ url: 'turns:turn-server-url:443?transport=tcp',
5238
+ username: 'turn user',
5239
+ password: 'turn password',
5240
+ },
5241
+ false
5242
+ );
5243
+ });
5244
+
5245
+ it('addMediaInternal() correctly falls back to transcoded if SDP answer is not from Homer (joinWithMedia() retry case)', async () => {
5246
+ // call addMediaInternal the way joinWithMedia() does it when it does a retry - with TURN info already provided
5247
+ // but also with forceTurnDiscovery=true - this shouldn't affect the flow for fallback to transcoded in any way
5248
+ // but doing it just for completeness
5249
+ await runCheck(
5250
+ {
5251
+ url: 'turns:turn-server-url:443?transport=tcp',
5252
+ username: 'turn user',
5253
+ password: 'turn password',
5254
+ },
5255
+ true
5256
+ );
5257
+ });
5258
+ });
5259
+ }
4836
5260
  })
4837
5261
  );
4838
5262
 
@@ -4910,6 +5334,11 @@ describe('plugin-meetings', () => {
4910
5334
  meeting.logger.error = sinon.stub().returns(true);
4911
5335
  meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
4912
5336
  webex.internal.voicea.off = sinon.stub().returns(true);
5337
+ meeting.stopTranscription = sinon.stub();
5338
+ meeting.transcription = {};
5339
+
5340
+ meeting.annotation.deregisterEvents = sinon.stub();
5341
+ webex.internal.llm.off = sinon.stub();
4913
5342
 
4914
5343
  // A meeting needs to be joined to leave
4915
5344
  meeting.meetingState = 'ACTIVE';
@@ -4930,6 +5359,9 @@ describe('plugin-meetings', () => {
4930
5359
  assert.calledOnce(meeting.closePeerConnections);
4931
5360
  assert.calledOnce(meeting.unsetRemoteStreams);
4932
5361
  assert.calledOnce(meeting.unsetPeerConnections);
5362
+ assert.calledOnce(meeting.stopTranscription);
5363
+ assert.calledOnce(meeting.annotation.deregisterEvents);
5364
+ assert.calledWith(webex.internal.llm.off, 'event:relay.event', meeting.processRelayEvent);
4933
5365
  });
4934
5366
 
4935
5367
  it('should reset call diagnostic latencies correctly', async () => {
@@ -5917,6 +6349,38 @@ describe('plugin-meetings', () => {
5917
6349
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.REQUIRED);
5918
6350
  });
5919
6351
 
6352
+ it('handles meetingInfoProvider not reach JBH', async () => {
6353
+ meeting.destination = FAKE_DESTINATION;
6354
+ meeting.destinationType = FAKE_TYPE;
6355
+ meeting.attrs.meetingInfoProvider = {
6356
+ fetchMeetingInfo: sinon
6357
+ .stub()
6358
+ .throws(new MeetingInfoV2JoinForbiddenError(403003, FAKE_MEETING_INFO)),
6359
+ };
6360
+
6361
+ await assert.isRejected(
6362
+ meeting.fetchMeetingInfo({sendCAevents: true}),
6363
+ JoinForbiddenError
6364
+ );
6365
+
6366
+ assert.calledWith(
6367
+ meeting.attrs.meetingInfoProvider.fetchMeetingInfo,
6368
+ FAKE_DESTINATION,
6369
+ FAKE_TYPE,
6370
+ null,
6371
+ null,
6372
+ undefined,
6373
+ 'locus-id',
6374
+ {},
6375
+ {meetingId: meeting.id, sendCAevents: true}
6376
+ );
6377
+
6378
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6379
+ assert.equal(meeting.meetingInfoFailureCode, 403003);
6380
+ assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.NOT_REACH_JBH);
6381
+ assert.equal(meeting.requiredCaptcha, null);
6382
+ });
6383
+
5920
6384
  it('handles meetingInfoProvider policy error', async () => {
5921
6385
  meeting.destination = FAKE_DESTINATION;
5922
6386
  meeting.destinationType = FAKE_TYPE;
@@ -6284,29 +6748,59 @@ describe('plugin-meetings', () => {
6284
6748
  assert.equal(meeting.fetchMeetingInfoTimeoutId, undefined);
6285
6749
  });
6286
6750
 
6287
- it('handles meetingInfoProvider webinar need registration error', async () => {
6751
+ it('handles MeetingInfoV2JoinWebinarError webinar need registration', async () => {
6288
6752
  meeting.destination = FAKE_DESTINATION;
6289
6753
  meeting.destinationType = FAKE_TYPE;
6290
6754
  meeting.attrs.meetingInfoProvider = {
6291
6755
  fetchMeetingInfo: sinon
6292
6756
  .stub()
6293
- .throws(
6294
- new MeetingInfoV2WebinarRegistrationError(403021, FAKE_MEETING_INFO, 'a message')
6295
- ),
6757
+ .throws(new MeetingInfoV2JoinWebinarError(403021, FAKE_MEETING_INFO, 'a message')),
6296
6758
  };
6297
6759
 
6298
- await assert.isRejected(
6299
- meeting.fetchMeetingInfo({sendCAevents: true}),
6300
- WebinarRegistrationError
6301
- );
6760
+ await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinWebinarError);
6302
6761
 
6303
6762
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6304
- assert.equal(meeting.meetingInfoFailureCode, 403021);
6305
6763
  assert.equal(
6306
6764
  meeting.meetingInfoFailureReason,
6307
6765
  MEETING_INFO_FAILURE_REASON.WEBINAR_REGISTRATION
6308
6766
  );
6309
6767
  });
6768
+
6769
+ it('handles MeetingInfoV2JoinWebinarError webinar need join with webcast', async () => {
6770
+ meeting.destination = FAKE_DESTINATION;
6771
+ meeting.destinationType = FAKE_TYPE;
6772
+ meeting.attrs.meetingInfoProvider = {
6773
+ fetchMeetingInfo: sinon
6774
+ .stub()
6775
+ .throws(new MeetingInfoV2JoinWebinarError(403026, FAKE_MEETING_INFO, 'a message')),
6776
+ };
6777
+
6778
+ await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinWebinarError);
6779
+
6780
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6781
+ assert.equal(
6782
+ meeting.meetingInfoFailureReason,
6783
+ MEETING_INFO_FAILURE_REASON.NEED_JOIN_WITH_WEBCAST
6784
+ );
6785
+ });
6786
+
6787
+ it('handles MeetingInfoV2JoinWebinarError webinar need registrationId', async () => {
6788
+ meeting.destination = FAKE_DESTINATION;
6789
+ meeting.destinationType = FAKE_TYPE;
6790
+ meeting.attrs.meetingInfoProvider = {
6791
+ fetchMeetingInfo: sinon
6792
+ .stub()
6793
+ .throws(new MeetingInfoV2JoinWebinarError(403037, FAKE_MEETING_INFO, 'a message')),
6794
+ };
6795
+
6796
+ await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinWebinarError);
6797
+
6798
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6799
+ assert.equal(
6800
+ meeting.meetingInfoFailureReason,
6801
+ MEETING_INFO_FAILURE_REASON.WEBINAR_NEED_REGISTRATION_ID
6802
+ );
6803
+ });
6310
6804
  });
6311
6805
 
6312
6806
  describe('#refreshPermissionToken', () => {
@@ -6364,7 +6858,8 @@ describe('plugin-meetings', () => {
6364
6858
  'fake-installed-org-id',
6365
6859
  'locus-id',
6366
6860
  {extraParam1: 'value1', permissionToken: FAKE_PERMISSION_TOKEN},
6367
- {meetingId: meeting.id, sendCAevents: true}
6861
+ {meetingId: meeting.id, sendCAevents: true},
6862
+ null
6368
6863
  );
6369
6864
  assert.deepEqual(meeting.meetingInfo, {
6370
6865
  ...FAKE_MEETING_INFO,
@@ -6409,7 +6904,8 @@ describe('plugin-meetings', () => {
6409
6904
  'fake-installed-org-id',
6410
6905
  'locus-id',
6411
6906
  {extraParam1: 'value1', permissionToken: FAKE_PERMISSION_TOKEN},
6412
- {meetingId: meeting.id, sendCAevents: true}
6907
+ {meetingId: meeting.id, sendCAevents: true},
6908
+ null
6413
6909
  );
6414
6910
  assert.deepEqual(meeting.meetingInfo, {
6415
6911
  ...FAKE_MEETING_INFO,
@@ -6463,7 +6959,8 @@ describe('plugin-meetings', () => {
6463
6959
  extraParam1: 'value1',
6464
6960
  permissionToken: FAKE_PERMISSION_TOKEN,
6465
6961
  },
6466
- {meetingId: meeting.id, sendCAevents: true}
6962
+ {meetingId: meeting.id, sendCAevents: true},
6963
+ null
6467
6964
  );
6468
6965
  assert.deepEqual(meeting.meetingInfo, {
6469
6966
  ...FAKE_MEETING_INFO,
@@ -6767,6 +7264,9 @@ describe('plugin-meetings', () => {
6767
7264
  meeting.transcription = {};
6768
7265
  meeting.stopTranscription = sinon.stub();
6769
7266
 
7267
+ meeting.annotation.deregisterEvents = sinon.stub();
7268
+ webex.internal.llm.off = sinon.stub();
7269
+
6770
7270
  // A meeting needs to be joined to end
6771
7271
  meeting.meetingState = 'ACTIVE';
6772
7272
  meeting.state = 'JOINED';
@@ -6787,6 +7287,9 @@ describe('plugin-meetings', () => {
6787
7287
  assert.calledOnce(meeting?.unsetRemoteStreams);
6788
7288
  assert.calledOnce(meeting?.unsetPeerConnections);
6789
7289
  assert.calledOnce(meeting?.stopTranscription);
7290
+
7291
+ assert.called(meeting.annotation.deregisterEvents);
7292
+ assert.calledWith(webex.internal.llm.off, 'event:relay.event', meeting.processRelayEvent);
6790
7293
  });
6791
7294
  });
6792
7295
 
@@ -7769,7 +8272,9 @@ describe('plugin-meetings', () => {
7769
8272
  });
7770
8273
 
7771
8274
  it('should collect ice candidates', () => {
7772
- eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: {candidate: 'candidate'}});
8275
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({
8276
+ candidate: {candidate: 'candidate'},
8277
+ });
7773
8278
 
7774
8279
  assert.equal(meeting.iceCandidatesCount, 1);
7775
8280
  });
@@ -8075,10 +8580,10 @@ describe('plugin-meetings', () => {
8075
8580
  meeting.statsAnalyzer.stopAnalyzer = sinon.stub().resolves();
8076
8581
  meeting.reconnectionManager = {
8077
8582
  reconnect: sinon.stub().resolves(),
8078
- resetReconnectionTimer: () => {}
8583
+ resetReconnectionTimer: () => {},
8079
8584
  };
8080
8585
  meeting.currentMediaStatus = {
8081
- video: true
8586
+ video: true,
8082
8587
  };
8083
8588
 
8084
8589
  await mockFailedEvent();
@@ -8360,8 +8865,7 @@ describe('plugin-meetings', () => {
8360
8865
  assert.calledWith(meeting.roapMessageReceived, fakeAnswer);
8361
8866
  });
8362
8867
 
8363
- it('handles OFFER message correctly when request fails', async () => {
8364
- const fakeError = new Error('fake error');
8868
+ const runOfferSendingFailureTest = async (fakeError, canProceed, expectedErrorCode) => {
8365
8869
  const clock = sinon.useFakeTimers();
8366
8870
  sinon.spy(clock, 'clearTimeout');
8367
8871
  meeting.deferSDPAnswer = {reject: sinon.stub()};
@@ -8399,19 +8903,31 @@ describe('plugin-meetings', () => {
8399
8903
  assert.equal(meeting.sdpResponseTimer, undefined);
8400
8904
 
8401
8905
  assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
8402
- clientErrorCode: 2007,
8906
+ clientErrorCode: expectedErrorCode,
8403
8907
  });
8404
8908
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
8405
8909
  name: 'client.media-engine.remote-sdp-received',
8406
8910
  payload: {
8407
- canProceed: false,
8408
- errors: [{errorCode: 2007, fatal: true}],
8911
+ canProceed,
8912
+ errors: [{errorCode: expectedErrorCode, fatal: true}],
8409
8913
  },
8410
8914
  options: {
8411
8915
  meetingId: meeting.id,
8412
8916
  rawError: fakeError,
8413
8917
  },
8414
8918
  });
8919
+ };
8920
+
8921
+ it('handles OFFER message correctly when request fails', async () => {
8922
+ const fakeError = new Error('fake error');
8923
+
8924
+ await runOfferSendingFailureTest(fakeError, false, 2007);
8925
+ });
8926
+
8927
+ it('handles OFFER message correctly when we get a non-homer answer', async () => {
8928
+ const fakeError = new MultistreamNotSupportedError();
8929
+
8930
+ await runOfferSendingFailureTest(fakeError, true, 2012);
8415
8931
  });
8416
8932
 
8417
8933
  it('handles ANSWER message correctly', () => {
@@ -8614,6 +9130,7 @@ describe('plugin-meetings', () => {
8614
9130
  });
8615
9131
  });
8616
9132
  });
9133
+
8617
9134
  describe('#setUpLocusInfoSelfListener', () => {
8618
9135
  it('listens to the self unadmitted guest event', (done) => {
8619
9136
  meeting.startKeepAlive = sinon.stub();
@@ -8629,6 +9146,13 @@ describe('plugin-meetings', () => {
8629
9146
  {payload: test1}
8630
9147
  );
8631
9148
  assert.calledOnce(meeting.updateLLMConnection);
9149
+ assert.calledOnceWithExactly(
9150
+ Metrics.sendBehavioralMetric,
9151
+ BEHAVIORAL_METRICS.GUEST_ENTERED_LOBBY,
9152
+ {
9153
+ correlation_id: meeting.correlationId,
9154
+ }
9155
+ );
8632
9156
  done();
8633
9157
  });
8634
9158
  it('listens to the self admitted guest event', (done) => {
@@ -8650,6 +9174,13 @@ describe('plugin-meetings', () => {
8650
9174
  assert.calledOnce(meeting.updateLLMConnection);
8651
9175
  assert.calledOnceWithExactly(meeting.rtcMetrics.sendNextMetrics);
8652
9176
 
9177
+ assert.calledOnceWithExactly(
9178
+ Metrics.sendBehavioralMetric,
9179
+ BEHAVIORAL_METRICS.GUEST_EXITED_LOBBY,
9180
+ {
9181
+ correlation_id: meeting.correlationId,
9182
+ }
9183
+ );
8653
9184
  done();
8654
9185
  });
8655
9186
 
@@ -8694,6 +9225,27 @@ describe('plugin-meetings', () => {
8694
9225
  );
8695
9226
  });
8696
9227
 
9228
+ it('listens to the brb state changed event', () => {
9229
+ const assertBrb = (enabled) => {
9230
+ meeting.brbState = createBrbState(meeting, false);
9231
+ meeting.locusInfo.emit(
9232
+ {function: 'test', file: 'test'},
9233
+ LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
9234
+ {brb: {enabled}}
9235
+ );
9236
+ assert.calledWithExactly(
9237
+ TriggerProxy.trigger,
9238
+ meeting,
9239
+ {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
9240
+ EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
9241
+ {payload: {brb: {enabled}}}
9242
+ );
9243
+ };
9244
+
9245
+ assertBrb(true);
9246
+ assertBrb(false);
9247
+ });
9248
+
8697
9249
  it('listens to the interpretation changed event', () => {
8698
9250
  meeting.simultaneousInterpretation.updateSelfInterpretation = sinon.stub();
8699
9251
 
@@ -8982,6 +9534,8 @@ describe('plugin-meetings', () => {
8982
9534
  });
8983
9535
 
8984
9536
  it('listens to MEETING_CONTROLS_PRACTICE_SESSION_STATUS_UPDATED', async () => {
9537
+ meeting.webinar.updatePracticeSessionStatus = sinon.stub();
9538
+
8985
9539
  const state = {example: 'value'};
8986
9540
 
8987
9541
  await meeting.locusInfo.emitScoped(
@@ -8990,6 +9544,7 @@ describe('plugin-meetings', () => {
8990
9544
  {state}
8991
9545
  );
8992
9546
 
9547
+ assert.calledOnceWithExactly(meeting.webinar.updatePracticeSessionStatus, state);
8993
9548
  assert.calledWith(
8994
9549
  TriggerProxy.trigger,
8995
9550
  meeting,
@@ -9373,6 +9928,22 @@ describe('plugin-meetings', () => {
9373
9928
  });
9374
9929
  });
9375
9930
 
9931
+ describe('#emailInput', () => {
9932
+ it('should set the email input', () => {
9933
+ assert.notOk(meeting.emailInput);
9934
+ meeting.emailInput = 'current';
9935
+ assert.equal(meeting.emailInput, 'current');
9936
+ });
9937
+ });
9938
+
9939
+ describe('#userNameInput', () => {
9940
+ it('should set the user name input', () => {
9941
+ assert.notOk(meeting.userNameInput);
9942
+ meeting.userNameInput = 'current';
9943
+ assert.equal(meeting.userNameInput, 'current');
9944
+ });
9945
+ });
9946
+
9376
9947
  describe('#setPermissionTokenPayload', () => {
9377
9948
  let now;
9378
9949
  let clock;
@@ -9472,15 +10043,44 @@ describe('plugin-meetings', () => {
9472
10043
  describe('#closePeerConnections', () => {
9473
10044
  it('should close the webrtc media connection, and return a promise', async () => {
9474
10045
  const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
9475
- meeting.mediaProperties.webrtcMediaConnection = {close: sinon.stub()};
10046
+ const fakeWebrtcMediaConnection = {close: sinon.stub()};
10047
+ meeting.mediaProperties.webrtcMediaConnection = fakeWebrtcMediaConnection;
10048
+
10049
+ meeting.audio = {id: 'fakeAudioMuteState'};
10050
+ meeting.video = {id: 'fakeVideoMuteState'};
10051
+
9476
10052
  const pcs = meeting.closePeerConnections();
9477
10053
 
9478
10054
  assert.exists(pcs.then);
9479
10055
  await pcs;
9480
- assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.close);
10056
+ assert.calledOnce(fakeWebrtcMediaConnection.close);
9481
10057
  assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
10058
+ assert.equal(meeting.audio, null);
10059
+ assert.equal(meeting.video, null);
10060
+ assert.equal(meeting.mediaProperties.webrtcMediaConnection, null);
10061
+ });
10062
+
10063
+ it('should close the webrtc media connection, but keep audio and video props unchanged if called with resetMuteStates=false', async () => {
10064
+ const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
10065
+ const fakeWebrtcMediaConnection = {close: sinon.stub()};
10066
+ meeting.mediaProperties.webrtcMediaConnection = fakeWebrtcMediaConnection;
10067
+
10068
+ const fakeAudio = {id: 'fakeAudioMuteState'};
10069
+ const fakeVideo = {id: 'fakeVideoMuteState'};
10070
+
10071
+ meeting.audio = fakeAudio;
10072
+ meeting.video = fakeVideo;
10073
+
10074
+ await meeting.closePeerConnections(false);
10075
+
10076
+ assert.calledOnce(fakeWebrtcMediaConnection.close);
10077
+ assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
10078
+ assert.equal(meeting.audio, fakeAudio);
10079
+ assert.equal(meeting.video, fakeVideo);
10080
+ assert.equal(meeting.mediaProperties.webrtcMediaConnection, null);
9482
10081
  });
9483
10082
  });
10083
+
9484
10084
  describe('#unsetPeerConnections', () => {
9485
10085
  it('should unset the peer connections', () => {
9486
10086
  meeting.mediaProperties.unsetPeerConnection = sinon.stub().returns(true);
@@ -10609,6 +11209,7 @@ describe('plugin-meetings', () => {
10609
11209
  meeting.webex.internal.llm.on = sinon.stub();
10610
11210
  meeting.webex.internal.llm.off = sinon.stub();
10611
11211
  meeting.processRelayEvent = sinon.stub();
11212
+ meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(false);
10612
11213
  });
10613
11214
 
10614
11215
  it('does not connect if the call is not joined yet', async () => {
@@ -10672,7 +11273,10 @@ describe('plugin-meetings', () => {
10672
11273
 
10673
11274
  const result = await meeting.updateLLMConnection();
10674
11275
 
10675
- assert.calledWith(webex.internal.llm.disconnectLLM);
11276
+ assert.calledWith(webex.internal.llm.disconnectLLM, {
11277
+ code: 3050,
11278
+ reason: 'done (permanent)',
11279
+ });
10676
11280
  assert.calledWith(
10677
11281
  webex.internal.llm.registerAndConnect,
10678
11282
  'a different url',
@@ -10702,7 +11306,10 @@ describe('plugin-meetings', () => {
10702
11306
 
10703
11307
  const result = await meeting.updateLLMConnection();
10704
11308
 
10705
- assert.calledWith(webex.internal.llm.disconnectLLM);
11309
+ assert.calledWith(webex.internal.llm.disconnectLLM, {
11310
+ code: 3050,
11311
+ reason: 'done (permanent)',
11312
+ });
10706
11313
  assert.calledWith(
10707
11314
  webex.internal.llm.registerAndConnect,
10708
11315
  'a url',
@@ -10731,7 +11338,7 @@ describe('plugin-meetings', () => {
10731
11338
 
10732
11339
  const result = await meeting.updateLLMConnection();
10733
11340
 
10734
- assert.calledWith(webex.internal.llm.disconnectLLM);
11341
+ assert.calledWith(webex.internal.llm.disconnectLLM, undefined);
10735
11342
  assert.notCalled(webex.internal.llm.registerAndConnect);
10736
11343
  assert.equal(result, undefined);
10737
11344
  assert.calledOnceWithExactly(
@@ -10740,6 +11347,22 @@ describe('plugin-meetings', () => {
10740
11347
  meeting.processRelayEvent
10741
11348
  );
10742
11349
  });
11350
+
11351
+ it('connect ps data channel if ps started in webinar', async () => {
11352
+ meeting.joinedWith = {state: 'JOINED'};
11353
+ meeting.locusInfo = {
11354
+ url: 'a url',
11355
+ info: {
11356
+ datachannelUrl: 'a datachannel url',
11357
+ practiceSessionDatachannelUrl: 'a ps datachannel url',
11358
+ },
11359
+ };
11360
+ meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(true);
11361
+ await meeting.updateLLMConnection();
11362
+
11363
+ assert.notCalled(webex.internal.llm.disconnectLLM);
11364
+ assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a ps datachannel url');
11365
+ });
10743
11366
  });
10744
11367
 
10745
11368
  describe('#setLocus', () => {
@@ -10931,6 +11554,7 @@ describe('plugin-meetings', () => {
10931
11554
  beforeEach(() => {
10932
11555
  meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
10933
11556
  meeting.deviceUrl = 'my-web-url';
11557
+ meeting.locusInfo.info = {isWebinar: false};
10934
11558
  });
10935
11559
 
10936
11560
  const USER_IDS = {
@@ -11156,13 +11780,29 @@ describe('plugin-meetings', () => {
11156
11780
 
11157
11781
  activeSharingId.whiteboard = beneficiaryId;
11158
11782
 
11159
- eventTrigger.share.push({
11160
- eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11161
- functionName: 'startWhiteboardShare',
11162
- eventPayload: {resourceUrl, memberId: beneficiaryId},
11163
- });
11783
+ eventTrigger.share.push(
11784
+ meeting.webinar.selfIsAttendee
11785
+ ? {
11786
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11787
+ functionName: 'remoteShare',
11788
+ eventPayload: {
11789
+ memberId: null,
11790
+ url,
11791
+ shareInstanceId,
11792
+ annotationInfo: undefined,
11793
+ resourceType: undefined,
11794
+ },
11795
+ }
11796
+ : {
11797
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11798
+ functionName: 'startWhiteboardShare',
11799
+ eventPayload: {resourceUrl, memberId: beneficiaryId},
11800
+ }
11801
+ );
11164
11802
 
11165
- shareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11803
+ shareStatus = meeting.webinar.selfIsAttendee
11804
+ ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
11805
+ : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11166
11806
  }
11167
11807
 
11168
11808
  if (eventTrigger.member) {
@@ -11194,13 +11834,29 @@ describe('plugin-meetings', () => {
11194
11834
  newPayload.current.content.disposition = FLOOR_ACTION.ACCEPTED;
11195
11835
  newPayload.current.content.beneficiaryId = otherBeneficiaryId;
11196
11836
 
11197
- eventTrigger.share.push({
11198
- eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11199
- functionName: 'startWhiteboardShare',
11200
- eventPayload: {resourceUrl, memberId: beneficiaryId},
11201
- });
11837
+ eventTrigger.share.push(
11838
+ meeting.webinar.selfIsAttendee
11839
+ ? {
11840
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11841
+ functionName: 'remoteShare',
11842
+ eventPayload: {
11843
+ memberId: null,
11844
+ url,
11845
+ shareInstanceId,
11846
+ annotationInfo: undefined,
11847
+ resourceType: undefined,
11848
+ },
11849
+ }
11850
+ : {
11851
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11852
+ functionName: 'startWhiteboardShare',
11853
+ eventPayload: {resourceUrl, memberId: beneficiaryId},
11854
+ }
11855
+ );
11202
11856
 
11203
- shareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11857
+ shareStatus = meeting.webinar.selfIsAttendee
11858
+ ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
11859
+ : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11204
11860
  } else {
11205
11861
  eventTrigger.share.push({
11206
11862
  eventName: EVENT_TRIGGERS.MEETING_STOPPED_SHARING_WHITEBOARD,
@@ -11327,6 +11983,37 @@ describe('plugin-meetings', () => {
11327
11983
  assert.exists(meeting.setUpLocusMediaSharesListener);
11328
11984
  });
11329
11985
 
11986
+ describe('Whiteboard Share - Webinar Attendee', () => {
11987
+ it('Scenario #1: Whiteboard sharing as a webinar attendee', () => {
11988
+ // Set the webinar attendee flag
11989
+ meeting.webinar = {selfIsAttendee: true};
11990
+ meeting.locusInfo.info.isWebinar = true;
11991
+
11992
+ // Step 1: Start sharing whiteboard A
11993
+ const data1 = generateData(
11994
+ blankPayload, // Initial payload
11995
+ true, // isGranting: Granting share
11996
+ false, // isContent: Whiteboard (not content)
11997
+ USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
11998
+ RESOURCE_URLS.WHITEBOARD_A // Resource URL: Whiteboard A
11999
+ );
12000
+
12001
+ // Step 2: Stop sharing whiteboard A
12002
+ const data2 = generateData(
12003
+ data1.payload, // Updated payload from Step 1
12004
+ false, // isGranting: Stopping share
12005
+ false, // isContent: Whiteboard
12006
+ USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
12007
+ );
12008
+
12009
+ // Validate the payload changes and status updates
12010
+ payloadTestHelper([data1]);
12011
+
12012
+ // Specific assertions for webinar attendee status
12013
+ assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
12014
+ });
12015
+ });
12016
+
11330
12017
  describe('Whiteboard A --> Whiteboard B', () => {
11331
12018
  it('Scenario #1: you share both whiteboards', () => {
11332
12019
  const data1 = generateData(
@@ -12002,9 +12689,12 @@ describe('plugin-meetings', () => {
12002
12689
  it('startKeepAlive starts the keep alive', async () => {
12003
12690
  meeting.meetingRequest.keepAlive = sinon.stub().returns(Promise.resolve());
12004
12691
 
12692
+ const keepAliveUrl1 = 'keep.alive.url1';
12693
+ const keepAliveUrl2 = 'keep.alive.url2';
12694
+
12005
12695
  assert.isNull(meeting.keepAliveTimerId);
12006
12696
  meeting.joinedWith = {
12007
- keepAliveUrl: defaultKeepAliveUrl,
12697
+ keepAliveUrl: keepAliveUrl1,
12008
12698
  keepAliveSecs: defaultKeepAliveSecs,
12009
12699
  };
12010
12700
  meeting.startKeepAlive();
@@ -12013,12 +12703,15 @@ describe('plugin-meetings', () => {
12013
12703
  assert.notCalled(meeting.meetingRequest.keepAlive);
12014
12704
  await progressTime(defaultExpectedInterval);
12015
12705
  assert.calledOnceWithExactly(meeting.meetingRequest.keepAlive, {
12016
- keepAliveUrl: defaultKeepAliveUrl,
12706
+ keepAliveUrl: keepAliveUrl1,
12017
12707
  });
12708
+ // joinedWith keep alive url might change (when we fallback from multistream to transcoded)
12709
+ meeting.joinedWith.keepAliveUrl = keepAliveUrl2;
12710
+
12018
12711
  await progressTime(defaultExpectedInterval);
12019
12712
  assert.calledTwice(meeting.meetingRequest.keepAlive);
12020
- assert.alwaysCalledWithExactly(meeting.meetingRequest.keepAlive, {
12021
- keepAliveUrl: defaultKeepAliveUrl,
12713
+ assert.calledWith(meeting.meetingRequest.keepAlive, {
12714
+ keepAliveUrl: keepAliveUrl2,
12022
12715
  });
12023
12716
  });
12024
12717
  it('startKeepAlive handles existing keepAliveTimerId', async () => {
@@ -12599,7 +13292,7 @@ describe('plugin-meetings', () => {
12599
13292
 
12600
13293
  describe('#roapMessageReceived', () => {
12601
13294
  it('calls roapMessageReceived on the webrtc media connection', () => {
12602
- const fakeMessage = {messageType: 'fake', sdp: 'fake sdp'};
13295
+ const fakeMessage = {messageType: 'ANSWER', sdp: 'fake sdp'};
12603
13296
 
12604
13297
  const getMediaServer = sinon.stub(MeetingsUtil, 'getMediaServer').returns('homer');
12605
13298
 
@@ -12616,5 +13309,96 @@ describe('plugin-meetings', () => {
12616
13309
  assert.calledOnceWithExactly(getMediaServer, 'fake sdp');
12617
13310
  assert.equal(meeting.mediaProperties.webrtcMediaConnection.mediaServer, 'homer');
12618
13311
  });
13312
+
13313
+ it('throws MultistreamNotSupportedError if we get a non-homer SDP answer', async () => {
13314
+ const fakeMessage = {messageType: 'ANSWER', sdp: 'fake sdp'};
13315
+
13316
+ meeting.isMultistream = true;
13317
+ meeting.mediaProperties.webrtcMediaConnection = {
13318
+ roapMessageReceived: sinon.stub(),
13319
+ };
13320
+
13321
+ sinon.stub(MeetingsUtil, 'getMediaServer').returns('linus');
13322
+
13323
+ try {
13324
+ await meeting.roapMessageReceived(fakeMessage);
13325
+
13326
+ assert.fail('Expected MultistreamNotSupportedError to be thrown');
13327
+ } catch (e) {
13328
+ assert.isTrue(e instanceof MultistreamNotSupportedError);
13329
+ }
13330
+
13331
+ assert.notCalled(meeting.mediaProperties.webrtcMediaConnection.roapMessageReceived);
13332
+ });
13333
+
13334
+ it('does not call getMediaServer for a roap message other than ANSWER', async () => {
13335
+ const fakeMessage = {messageType: 'ERROR', sdp: 'fake sdp'};
13336
+
13337
+ meeting.isMultistream = true;
13338
+ meeting.mediaProperties.webrtcMediaConnection = {
13339
+ roapMessageReceived: sinon.stub(),
13340
+ };
13341
+ meeting.mediaProperties.webrtcMediaConnection.mediaServer = 'linus';
13342
+
13343
+ const getMediaServerStub = sinon.stub(MeetingsUtil, 'getMediaServer').returns('something');
13344
+
13345
+ meeting.roapMessageReceived(fakeMessage);
13346
+
13347
+ assert.calledOnceWithExactly(
13348
+ meeting.mediaProperties.webrtcMediaConnection.roapMessageReceived,
13349
+ fakeMessage
13350
+ );
13351
+ assert.notCalled(getMediaServerStub);
13352
+ assert.equal(meeting.mediaProperties.webrtcMediaConnection.mediaServer, 'linus'); // check that it hasn't been overwritten
13353
+ });
13354
+ });
13355
+
13356
+ describe('#verifyRegistrationId', () => {
13357
+ it('calls fetchMeetingInfo() with the passed registrationId and captcha code', async () => {
13358
+ // simulate successful case
13359
+ meeting.fetchMeetingInfo = sinon.stub().resolves();
13360
+ const result = await meeting.verifyRegistrationId('registrationId', 'captcha id');
13361
+
13362
+ assert(Metrics.sendBehavioralMetric.calledOnce);
13363
+ assert.calledWith(
13364
+ Metrics.sendBehavioralMetric,
13365
+ BEHAVIORAL_METRICS.VERIFY_REGISTRATION_ID_SUCCESS
13366
+ );
13367
+ assert.equal(result.isRegistrationIdValid, true);
13368
+ assert.equal(result.requiredCaptcha, null);
13369
+ assert.equal(result.failureReason, MEETING_INFO_FAILURE_REASON.NONE);
13370
+ assert.calledWith(meeting.fetchMeetingInfo, {
13371
+ registrationId: 'registrationId',
13372
+ captchaCode: 'captcha id',
13373
+ sendCAevents: false,
13374
+ });
13375
+ });
13376
+ it('handles registrationIdError returned by fetchMeetingInfo', async () => {
13377
+ meeting.fetchMeetingInfo = sinon.stub().callsFake(() => {
13378
+ meeting.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_REGISTRATIONID;
13379
+
13380
+ return Promise.reject(new JoinWebinarError());
13381
+ });
13382
+ const result = await meeting.verifyRegistrationId('registrationId', 'captcha id');
13383
+
13384
+ assert.equal(result.isRegistrationIdValid, false);
13385
+ assert.equal(result.requiredCaptcha, null);
13386
+ assert.equal(result.failureReason, MEETING_INFO_FAILURE_REASON.WRONG_REGISTRATION_ID);
13387
+ });
13388
+ it('handles CaptchaError returned by fetchMeetingInfo', async () => {
13389
+ const FAKE_CAPTCHA = {captchaId: 'some catcha id...'};
13390
+
13391
+ meeting.fetchMeetingInfo = sinon.stub().callsFake(() => {
13392
+ meeting.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA;
13393
+ meeting.requiredCaptcha = FAKE_CAPTCHA;
13394
+
13395
+ return Promise.reject(new CaptchaError());
13396
+ });
13397
+ const result = await meeting.verifyRegistrationId('registrationId', 'captcha id');
13398
+
13399
+ assert.equal(result.isRegistrationIdValid, false);
13400
+ assert.deepEqual(result.requiredCaptcha, FAKE_CAPTCHA);
13401
+ assert.equal(result.failureReason, MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA);
13402
+ });
12619
13403
  });
12620
13404
  });