@webex/plugin-meetings 3.7.0-next.9 → 3.7.0-wxcc.1

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