@webex/plugin-meetings 3.7.0-next.8 → 3.7.0-web-workers-keepalive.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 (107) 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/{webinar-registration-error.js → join-webinar-error.js} +12 -12
  6. package/dist/common/errors/join-webinar-error.js.map +1 -0
  7. package/dist/common/errors/multistream-not-supported-error.js +53 -0
  8. package/dist/common/errors/multistream-not-supported-error.js.map +1 -0
  9. package/dist/config.js +1 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +26 -5
  12. package/dist/constants.js.map +1 -1
  13. package/dist/index.js +16 -11
  14. package/dist/index.js.map +1 -1
  15. package/dist/interpretation/index.js +1 -1
  16. package/dist/interpretation/siLanguage.js +1 -1
  17. package/dist/locus-info/index.js +13 -2
  18. package/dist/locus-info/index.js.map +1 -1
  19. package/dist/locus-info/selfUtils.js +30 -17
  20. package/dist/locus-info/selfUtils.js.map +1 -1
  21. package/dist/meeting/index.js +903 -800
  22. package/dist/meeting/index.js.map +1 -1
  23. package/dist/meeting/locusMediaRequest.js +9 -0
  24. package/dist/meeting/locusMediaRequest.js.map +1 -1
  25. package/dist/meeting/request.js +30 -0
  26. package/dist/meeting/request.js.map +1 -1
  27. package/dist/meeting/request.type.js.map +1 -1
  28. package/dist/meeting/util.js +16 -16
  29. package/dist/meeting/util.js.map +1 -1
  30. package/dist/meeting-info/meeting-info-v2.js +29 -17
  31. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  32. package/dist/meetings/index.js +6 -3
  33. package/dist/meetings/index.js.map +1 -1
  34. package/dist/meetings/util.js +1 -1
  35. package/dist/meetings/util.js.map +1 -1
  36. package/dist/member/index.js +9 -0
  37. package/dist/member/index.js.map +1 -1
  38. package/dist/member/types.js.map +1 -1
  39. package/dist/member/util.js +39 -28
  40. package/dist/member/util.js.map +1 -1
  41. package/dist/metrics/constants.js +1 -1
  42. package/dist/metrics/constants.js.map +1 -1
  43. package/dist/multistream/remoteMedia.js +30 -15
  44. package/dist/multistream/remoteMedia.js.map +1 -1
  45. package/dist/multistream/sendSlotManager.js +24 -0
  46. package/dist/multistream/sendSlotManager.js.map +1 -1
  47. package/dist/roap/index.js +10 -8
  48. package/dist/roap/index.js.map +1 -1
  49. package/dist/types/annotation/index.d.ts +5 -0
  50. package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
  51. package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
  52. package/dist/types/constants.d.ts +20 -1
  53. package/dist/types/index.d.ts +3 -3
  54. package/dist/types/locus-info/index.d.ts +2 -1
  55. package/dist/types/meeting/index.d.ts +19 -12
  56. package/dist/types/meeting/locusMediaRequest.d.ts +4 -0
  57. package/dist/types/meeting/request.d.ts +12 -1
  58. package/dist/types/meeting/request.type.d.ts +6 -0
  59. package/dist/types/meeting/util.d.ts +1 -1
  60. package/dist/types/meeting-info/meeting-info-v2.d.ts +4 -4
  61. package/dist/types/meetings/index.d.ts +3 -0
  62. package/dist/types/member/index.d.ts +1 -0
  63. package/dist/types/member/types.d.ts +7 -0
  64. package/dist/types/metrics/constants.d.ts +1 -1
  65. package/dist/types/multistream/sendSlotManager.d.ts +8 -1
  66. package/dist/webinar/index.js +354 -3
  67. package/dist/webinar/index.js.map +1 -1
  68. package/package.json +23 -22
  69. package/src/annotation/index.ts +16 -0
  70. package/src/common/errors/join-webinar-error.ts +24 -0
  71. package/src/common/errors/multistream-not-supported-error.ts +30 -0
  72. package/src/config.ts +1 -1
  73. package/src/constants.ts +23 -3
  74. package/src/index.ts +5 -3
  75. package/src/locus-info/index.ts +17 -2
  76. package/src/locus-info/selfUtils.ts +19 -6
  77. package/src/meeting/index.ts +234 -80
  78. package/src/meeting/locusMediaRequest.ts +7 -0
  79. package/src/meeting/request.ts +26 -1
  80. package/src/meeting/request.type.ts +7 -0
  81. package/src/meeting/util.ts +8 -10
  82. package/src/meeting-info/meeting-info-v2.ts +23 -11
  83. package/src/meetings/index.ts +8 -2
  84. package/src/meetings/util.ts +2 -1
  85. package/src/member/index.ts +9 -0
  86. package/src/member/types.ts +8 -0
  87. package/src/member/util.ts +34 -24
  88. package/src/metrics/constants.ts +1 -1
  89. package/src/multistream/remoteMedia.ts +28 -15
  90. package/src/multistream/sendSlotManager.ts +31 -0
  91. package/src/roap/index.ts +10 -8
  92. package/src/webinar/index.ts +197 -3
  93. package/test/unit/spec/annotation/index.ts +46 -1
  94. package/test/unit/spec/locus-info/index.js +222 -0
  95. package/test/unit/spec/locus-info/selfConstant.js +7 -0
  96. package/test/unit/spec/locus-info/selfUtils.js +91 -1
  97. package/test/unit/spec/meeting/index.js +683 -103
  98. package/test/unit/spec/meeting/utils.js +22 -19
  99. package/test/unit/spec/meeting-info/meetinginfov2.js +9 -4
  100. package/test/unit/spec/meetings/index.js +9 -5
  101. package/test/unit/spec/meetings/utils.js +10 -0
  102. package/test/unit/spec/member/util.js +52 -11
  103. package/test/unit/spec/multistream/remoteMedia.ts +11 -7
  104. package/test/unit/spec/roap/index.ts +47 -0
  105. package/test/unit/spec/webinar/index.ts +457 -0
  106. package/dist/common/errors/webinar-registration-error.js.map +0 -1
  107. 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,
102
103
  } from '../../../../src/meeting-info/meeting-info-v2';
103
104
  import {
104
105
  DTLS_HANDSHAKE_FAILED_CLIENT_CODE,
@@ -652,7 +653,7 @@ describe('plugin-meetings', () => {
652
653
  const fakeTurnServerInfo = {id: 'fake turn info'};
653
654
  const fakeJoinResult = {id: 'join result'};
654
655
 
655
- const joinOptions = {correlationId: '12345'};
656
+ const joinOptions = {correlationId: '12345', enableMultistream: true};
656
657
  const mediaOptions = {audioEnabled: true, allowMediaInLobby: true};
657
658
 
658
659
  let generateTurnDiscoveryRequestMessageStub;
@@ -661,7 +662,10 @@ describe('plugin-meetings', () => {
661
662
  let addMediaInternalStub;
662
663
 
663
664
  beforeEach(() => {
664
- meeting.join = sinon.stub().returns(Promise.resolve(fakeJoinResult));
665
+ meeting.join = sinon.stub().callsFake((joinOptions) => {
666
+ meeting.isMultistream = joinOptions.enableMultistream;
667
+ return Promise.resolve(fakeJoinResult)
668
+ });
665
669
  addMediaInternalStub = sinon
666
670
  .stub(meeting, 'addMediaInternal')
667
671
  .returns(Promise.resolve(test4));
@@ -700,7 +704,7 @@ describe('plugin-meetings', () => {
700
704
  mediaOptions
701
705
  );
702
706
 
703
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
707
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
704
708
 
705
709
  // resets joinWithMediaRetryInfo
706
710
  assert.deepEqual(meeting.joinWithMediaRetryInfo, {
@@ -733,7 +737,7 @@ describe('plugin-meetings', () => {
733
737
  mediaOptions
734
738
  );
735
739
 
736
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
740
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
737
741
  assert.equal(meeting.turnServerUsed, false);
738
742
  });
739
743
 
@@ -768,7 +772,7 @@ describe('plugin-meetings', () => {
768
772
  mediaOptions
769
773
  );
770
774
 
771
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
775
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
772
776
  });
773
777
 
774
778
  it('should reject if join() fails', async () => {
@@ -855,7 +859,8 @@ describe('plugin-meetings', () => {
855
859
  }
856
860
  );
857
861
 
858
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
862
+ // expect multistreamEnabled: false, because this test overrides the join meeting.join stub so it doesn't set the isMultistream flag
863
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: false});
859
864
 
860
865
  // resets joinWithMediaRetryInfo
861
866
  assert.deepEqual(meeting.joinWithMediaRetryInfo, {
@@ -944,7 +949,7 @@ describe('plugin-meetings', () => {
944
949
  mediaOptions,
945
950
  });
946
951
 
947
- assert.deepEqual(result, {join: fakeJoinResult, media: test4});
952
+ assert.deepEqual(result, {join: fakeJoinResult, media: test4, multistreamEnabled: true});
948
953
 
949
954
  assert.calledOnce(meeting.join);
950
955
  assert.notCalled(leaveStub);
@@ -1038,6 +1043,7 @@ describe('plugin-meetings', () => {
1038
1043
  getConnectionState: sinon.stub().returns(ConnectionState.Connected),
1039
1044
  initiateOffer: sinon.stub().resolves({}),
1040
1045
  on: sinon.stub(),
1046
+ createSendSlot: sinon.stub(),
1041
1047
  };
1042
1048
 
1043
1049
  /* Setup the stubs so that the first call to addMediaInternal() fails
@@ -1054,12 +1060,14 @@ describe('plugin-meetings', () => {
1054
1060
 
1055
1061
  sinon.stub(meeting.roap, 'doTurnDiscovery').resolves({turnServerInfo: 'fake turn info'});
1056
1062
 
1063
+ // calling joinWithMedia() with enableMultistream=false, because this test uses real addMediaInternal() implementation
1064
+ // and it requires less stubs when it's without multistream
1057
1065
  const result = await meeting.joinWithMedia({
1058
- joinOptions,
1066
+ joinOptions: {...joinOptions, enableMultistream: false},
1059
1067
  mediaOptions,
1060
1068
  });
1061
1069
 
1062
- assert.deepEqual(result, {join: fakeJoinResult, media: undefined});
1070
+ assert.deepEqual(result, {join: fakeJoinResult, media: undefined, multistreamEnabled: false});
1063
1071
 
1064
1072
  assert.calledOnce(meeting.join);
1065
1073
 
@@ -1134,6 +1142,7 @@ describe('plugin-meetings', () => {
1134
1142
  addMediaError.name = 'SdpOfferCreationError';
1135
1143
 
1136
1144
  meeting.addMediaInternal.rejects(addMediaError);
1145
+ sinon.stub(meeting, 'leave').resolves();
1137
1146
 
1138
1147
  await assert.isRejected(
1139
1148
  meeting.joinWithMedia({
@@ -1162,6 +1171,7 @@ describe('plugin-meetings', () => {
1162
1171
  type: addMediaError.name,
1163
1172
  }
1164
1173
  );
1174
+ assert.calledOnceWithExactly(meeting.leave, {resourceId: undefined, reason: 'joinWithMedia failure'})
1165
1175
  });
1166
1176
  });
1167
1177
 
@@ -1238,6 +1248,7 @@ describe('plugin-meetings', () => {
1238
1248
  webex.internal.voicea.off = sinon.stub();
1239
1249
  webex.internal.voicea.listenToEvents = sinon.stub();
1240
1250
  webex.internal.voicea.turnOnCaptions = sinon.stub();
1251
+ webex.internal.voicea.deregisterEvents = sinon.stub();
1241
1252
  });
1242
1253
 
1243
1254
  it('should stop listening to voicea events and also trigger a stop event', () => {
@@ -1566,6 +1577,55 @@ describe('plugin-meetings', () => {
1566
1577
  fakeProcessedReaction
1567
1578
  );
1568
1579
  });
1580
+
1581
+ it('should fail quietly if participantId does not exist in membersCollection', () => {
1582
+ LoggerProxy.logger.warn = sinon.stub();
1583
+ meeting.isReactionsSupported = sinon.stub().returns(true);
1584
+ meeting.config.receiveReactions = true;
1585
+ const fakeSendersName = 'Fake reactors name';
1586
+ const fakeReactionPayload = {
1587
+ type: 'fake_type',
1588
+ codepoints: 'fake_codepoints',
1589
+ shortcodes: 'fake_shortcodes',
1590
+ tone: {
1591
+ type: 'fake_tone_type',
1592
+ codepoints: 'fake_tone_codepoints',
1593
+ shortcodes: 'fake_tone_shortcodes',
1594
+ },
1595
+ };
1596
+ const fakeSenderPayload = {
1597
+ participantId: 'fake_participant_id',
1598
+ };
1599
+ const fakeProcessedReaction = {
1600
+ reaction: fakeReactionPayload,
1601
+ sender: {
1602
+ id: fakeSenderPayload.participantId,
1603
+ name: fakeSendersName,
1604
+ },
1605
+ };
1606
+ const fakeRelayEvent = {
1607
+ data: {
1608
+ relayType: REACTION_RELAY_TYPES.REACTION,
1609
+ reaction: fakeReactionPayload,
1610
+ sender: fakeSenderPayload,
1611
+ },
1612
+ };
1613
+ meeting.processRelayEvent(fakeRelayEvent);
1614
+ assert.calledWith(
1615
+ LoggerProxy.logger.warn,
1616
+ `Meeting:index#processRelayEvent --> Skipping handling of react for ${meeting.id}. participantId fake_participant_id does not exist in membersCollection.`
1617
+ );
1618
+ assert.neverCalledWith(
1619
+ TriggerProxy.trigger,
1620
+ sinon.match.instanceOf(Meeting),
1621
+ {
1622
+ file: 'meeting/index',
1623
+ function: 'join',
1624
+ },
1625
+ EVENT_TRIGGERS.MEETING_RECEIVE_REACTIONS,
1626
+ fakeProcessedReaction
1627
+ );
1628
+ });
1569
1629
  });
1570
1630
 
1571
1631
  describe('#handleLLMOnline', () => {
@@ -1705,6 +1765,12 @@ describe('plugin-meetings', () => {
1705
1765
  sinon.assert.called(setCorrelationIdSpy);
1706
1766
  assert.equal(meeting.correlationId, '123');
1707
1767
  });
1768
+
1769
+ it('should not send client.call.initiated if told not to', async () => {
1770
+ await meeting.join({sendCallInitiated: false});
1771
+
1772
+ sinon.assert.notCalled(webex.internal.newMetrics.submitClientEvent);
1773
+ });
1708
1774
  });
1709
1775
 
1710
1776
  describe('failure', () => {
@@ -2492,9 +2558,11 @@ describe('plugin-meetings', () => {
2492
2558
  mediaSettings: {},
2493
2559
  });
2494
2560
 
2495
- const checkLogCounter = (delay, expectedCounter) => {
2561
+ const checkLogCounter = (delayInMinutes, expectedCounter) => {
2562
+ const delayInMilliseconds = delayInMinutes * 60 * 1000;
2563
+
2496
2564
  // first check that the counter is not increased just before the delay
2497
- clock.tick(delay - 50);
2565
+ clock.tick(delayInMilliseconds - 50);
2498
2566
  assert.equal(logUploadCounter, expectedCounter - 1);
2499
2567
 
2500
2568
  // and now check that it has reached expected value after the delay
@@ -2502,22 +2570,18 @@ describe('plugin-meetings', () => {
2502
2570
  assert.equal(logUploadCounter, expectedCounter);
2503
2571
  };
2504
2572
 
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);
2573
+ checkLogCounter(0.1, 1);
2574
+ checkLogCounter(15, 2);
2575
+ checkLogCounter(30, 3);
2576
+ checkLogCounter(60, 4);
2577
+ checkLogCounter(60, 5);
2515
2578
 
2516
- // simulate media connection being removed -> no more log uploads should happen
2579
+ // simulate media connection being removed -> 1 more upload should happen, but nothing more afterwards
2517
2580
  meeting.mediaProperties.webrtcMediaConnection = undefined;
2581
+ checkLogCounter(60, 6);
2518
2582
 
2519
- clock.tick(60000);
2520
- assert.equal(logUploadCounter, 11);
2583
+ clock.tick(120 * 1000 * 60);
2584
+ assert.equal(logUploadCounter, 6);
2521
2585
 
2522
2586
  clock.restore();
2523
2587
  });
@@ -3475,6 +3539,51 @@ describe('plugin-meetings', () => {
3475
3539
  });
3476
3540
  });
3477
3541
 
3542
+ it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
3543
+ let fakeMembersCollection = {
3544
+ members: {
3545
+ member1: { isInMeeting: true },
3546
+ member2: { isInMeeting: true },
3547
+ member3: { isInMeeting: false },
3548
+ },
3549
+ };
3550
+ sinon.stub(meeting, 'getMembers').returns({ membersCollection: fakeMembersCollection });
3551
+ const fakeData = { intervalMetadata: {}, networkType: 'wifi' };
3552
+
3553
+ statsAnalyzerStub.emit(
3554
+ { file: 'test', function: 'test' },
3555
+ StatsAnalyzerEventNames.MEDIA_QUALITY,
3556
+ { data: fakeData }
3557
+ );
3558
+
3559
+ assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
3560
+ name: 'client.mediaquality.event',
3561
+ options: {
3562
+ meetingId: meeting.id,
3563
+ },
3564
+ payload: {
3565
+ intervals: [sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2))],
3566
+ },
3567
+ });
3568
+ fakeMembersCollection.members.member2.isInMeeting = false;
3569
+
3570
+ statsAnalyzerStub.emit(
3571
+ { file: 'test', function: 'test' },
3572
+ StatsAnalyzerEventNames.MEDIA_QUALITY,
3573
+ { data: fakeData }
3574
+ );
3575
+
3576
+ assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
3577
+ name: 'client.mediaquality.event',
3578
+ options: {
3579
+ meetingId: meeting.id,
3580
+ },
3581
+ payload: {
3582
+ intervals: [sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1))],
3583
+ },
3584
+ });
3585
+ });
3586
+
3478
3587
  it('calls submitMQE correctly', async () => {
3479
3588
  const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'};
3480
3589
 
@@ -3552,14 +3661,6 @@ describe('plugin-meetings', () => {
3552
3661
  });
3553
3662
  });
3554
3663
 
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
3664
  describe('CA ice failures checks', () => {
3564
3665
  [
3565
3666
  {
@@ -3701,6 +3802,93 @@ describe('plugin-meetings', () => {
3701
3802
  });
3702
3803
  });
3703
3804
 
3805
+ describe(`#beRightBack`, () => {
3806
+ const fakeMultistreamRoapMediaConnection = {
3807
+ createSendSlot: sinon.stub().returns({
3808
+ setSourceStateOverride: sinon.stub().resolves(),
3809
+ clearSourceStateOverride: sinon.stub().resolves(),
3810
+ }),
3811
+ };
3812
+
3813
+ beforeEach(() => {
3814
+ meeting.meetingRequest.setBrb = sinon.stub().resolves({body: 'test'});
3815
+ meeting.mediaProperties.webrtcMediaConnection = {createSendSlot: sinon.stub()};
3816
+ meeting.sendSlotManager.createSlot(
3817
+ fakeMultistreamRoapMediaConnection,
3818
+ MediaType.VideoMain
3819
+ );
3820
+
3821
+ meeting.locusUrl = 'locus url';
3822
+ meeting.deviceUrl = 'device url';
3823
+ meeting.selfId = 'self id';
3824
+ });
3825
+
3826
+ afterEach(() => {
3827
+ sinon.restore();
3828
+ });
3829
+
3830
+ it('should have #beRightBack', () => {
3831
+ assert.exists(meeting.beRightBack);
3832
+ });
3833
+
3834
+ describe('when in a multistream meeting', () => {
3835
+
3836
+ beforeEach(() => {
3837
+ meeting.isMultistream = true;
3838
+ });
3839
+
3840
+ it('should enable #beRightBack and return a promise', async () => {
3841
+ const brbResult = meeting.beRightBack(true);
3842
+
3843
+ await brbResult;
3844
+ assert.exists(brbResult.then);
3845
+ assert.calledOnce(meeting.meetingRequest.setBrb);
3846
+ })
3847
+
3848
+ it('should disable #beRightBack and return a promise', async () => {
3849
+ const brbResult = meeting.beRightBack(false);
3850
+
3851
+ await brbResult;
3852
+ assert.exists(brbResult.then);
3853
+ assert.calledOnce(meeting.meetingRequest.setBrb);
3854
+ })
3855
+
3856
+ it('should throw an error and reject the promise if setBrb fails', async () => {
3857
+ const error = new Error('setBrb failed');
3858
+ meeting.meetingRequest.setBrb.rejects(error);
3859
+
3860
+ try {
3861
+ await meeting.beRightBack(true);
3862
+ } catch (err) {
3863
+ assert.instanceOf(err, Error);
3864
+ assert.equal(err.message, 'setBrb failed');
3865
+ assert.isRejected((Promise.reject()));
3866
+ }
3867
+ })
3868
+ });
3869
+
3870
+ describe('when in a transcoded meeting', () => {
3871
+
3872
+ beforeEach(() => {
3873
+ meeting.isMultistream = false;
3874
+ });
3875
+
3876
+ it('should ignore enabling #beRightBack', async () => {
3877
+ meeting.beRightBack(true);
3878
+
3879
+ assert.isRejected((Promise.reject()));
3880
+ assert.notCalled(meeting.meetingRequest.setBrb);
3881
+ })
3882
+
3883
+ it('should ignore disabling #beRightBack', async () => {
3884
+ meeting.beRightBack(false);
3885
+
3886
+ assert.isRejected((Promise.reject()));
3887
+ assert.notCalled(meeting.meetingRequest.setBrb);
3888
+ })
3889
+ });
3890
+ });
3891
+
3704
3892
  /* This set of tests are like semi-integration tests, they use real MuteState, Media, LocusMediaRequest and Roap classes.
3705
3893
  They mock the @webex/internal-media-core and sending of /media http requests to Locus.
3706
3894
  Their main purpose is to test that we send the right http requests to Locus and make right calls
@@ -3743,8 +3931,12 @@ describe('plugin-meetings', () => {
3743
3931
  meeting.setMercuryListener = sinon.stub();
3744
3932
  meeting.locusInfo.onFullLocus = sinon.stub();
3745
3933
  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'});
3934
+ meeting.webex.meetings.reachability.getReachabilityReportToAttachToRoap = sinon
3935
+ .stub()
3936
+ .resolves({id: 'fake reachability'});
3937
+ meeting.webex.meetings.reachability.getClientMediaPreferences = sinon
3938
+ .stub()
3939
+ .resolves({id: 'fake clientMediaPreferences'});
3748
3940
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3749
3941
  turnServerInfo: {
3750
3942
  url: 'turns:turn-server-url:443?transport=tcp',
@@ -3825,6 +4017,7 @@ describe('plugin-meetings', () => {
3825
4017
  initiateOffer: sinon.stub().resolves({}),
3826
4018
  update: sinon.stub().resolves({}),
3827
4019
  on: sinon.stub(),
4020
+ roapMessageReceived: sinon.stub()
3828
4021
  };
3829
4022
 
3830
4023
  fakeMultistreamRoapMediaConnection = {
@@ -3911,8 +4104,10 @@ describe('plugin-meetings', () => {
3911
4104
  };
3912
4105
 
3913
4106
  // simulates a Roap offer being generated by the RoapMediaConnection
3914
- const simulateRoapOffer = async () => {
3915
- meeting.deferSDPAnswer = {resolve: sinon.stub()};
4107
+ const simulateRoapOffer = async (stubWaitingForAnswer = true) => {
4108
+ if (stubWaitingForAnswer) {
4109
+ meeting.deferSDPAnswer = {resolve: sinon.stub()};
4110
+ }
3916
4111
  const roapListener = getRoapListener();
3917
4112
 
3918
4113
  await roapListener({roapMessage: roapOfferMessage});
@@ -3930,8 +4125,14 @@ describe('plugin-meetings', () => {
3930
4125
  const checkSdpOfferSent = ({audioMuted, videoMuted}) => {
3931
4126
  const {sdp, seq, tieBreaker} = roapOfferMessage;
3932
4127
 
3933
- assert.calledWith(meeting.webex.meetings.reachability.getClientMediaPreferences, meeting.isMultistream, 0);
3934
- assert.calledWith(meeting.webex.meetings.reachability.getReachabilityReportToAttachToRoap);
4128
+ assert.calledWith(
4129
+ meeting.webex.meetings.reachability.getClientMediaPreferences,
4130
+ meeting.isMultistream,
4131
+ 0
4132
+ );
4133
+ assert.calledWith(
4134
+ meeting.webex.meetings.reachability.getReachabilityReportToAttachToRoap
4135
+ );
3935
4136
 
3936
4137
  assert.calledWith(locusMediaRequestStub, {
3937
4138
  method: 'PUT',
@@ -4015,8 +4216,9 @@ describe('plugin-meetings', () => {
4015
4216
  remoteQualityLevel,
4016
4217
  expectedDebugId,
4017
4218
  meetingId,
4219
+ expectMultistream = isMultistream,
4018
4220
  }) => {
4019
- if (isMultistream) {
4221
+ if (expectMultistream) {
4020
4222
  const {iceServers} = mediaConnectionConfig;
4021
4223
 
4022
4224
  assert.calledOnceWithMatch(
@@ -4176,7 +4378,6 @@ describe('plugin-meetings', () => {
4176
4378
  });
4177
4379
 
4178
4380
  it('addMedia() works correctly when media is enabled with streams to publish', async () => {
4179
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4180
4381
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
4181
4382
  await simulateRoapOffer();
4182
4383
  await simulateRoapOk();
@@ -4207,12 +4408,9 @@ describe('plugin-meetings', () => {
4207
4408
 
4208
4409
  // and that these were the only /media requests that were sent
4209
4410
  assert.calledTwice(locusMediaRequestStub);
4210
-
4211
- assert.calledOnce(handleDeviceLoggingSpy);
4212
4411
  });
4213
4412
 
4214
4413
  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
4414
  fakeMicrophoneStream.userMuted = true;
4217
4415
 
4218
4416
  await meeting.addMedia({localStreams: {microphone: fakeMicrophoneStream}});
@@ -4244,7 +4442,6 @@ describe('plugin-meetings', () => {
4244
4442
 
4245
4443
  // and that these were the only /media requests that were sent
4246
4444
  assert.calledTwice(locusMediaRequestStub);
4247
- assert.calledOnce(handleDeviceLoggingSpy);
4248
4445
  });
4249
4446
 
4250
4447
  it('addMedia() works correctly when media is enabled with tracks to publish and track is ended', async () => {
@@ -4316,7 +4513,6 @@ describe('plugin-meetings', () => {
4316
4513
  });
4317
4514
 
4318
4515
  it('addMedia() works correctly when media is disabled with streams to publish', async () => {
4319
- const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging');
4320
4516
  await meeting.addMedia({
4321
4517
  localStreams: {microphone: fakeMicrophoneStream},
4322
4518
  audioEnabled: false,
@@ -4350,20 +4546,6 @@ describe('plugin-meetings', () => {
4350
4546
 
4351
4547
  // and that these were the only /media requests that were sent
4352
4548
  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
4549
  });
4368
4550
 
4369
4551
  it('addMedia() works correctly when media is disabled with no streams to publish', async () => {
@@ -4399,20 +4581,6 @@ describe('plugin-meetings', () => {
4399
4581
  assert.calledTwice(locusMediaRequestStub);
4400
4582
  });
4401
4583
 
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
4584
  it('addMedia() works correctly when video is disabled with no streams to publish', async () => {
4417
4585
  await meeting.addMedia({videoEnabled: false});
4418
4586
  await simulateRoapOffer();
@@ -4479,13 +4647,6 @@ describe('plugin-meetings', () => {
4479
4647
  assert.calledTwice(locusMediaRequestStub);
4480
4648
  });
4481
4649
 
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
4650
  describe('publishStreams()/unpublishStreams() calls', () => {
4490
4651
  [
4491
4652
  {mediaEnabled: true, expected: {direction: 'sendrecv', localMuteSentValue: false}},
@@ -4881,6 +5042,211 @@ describe('plugin-meetings', () => {
4881
5042
  assert.notCalled(fakeRoapMediaConnection.update);
4882
5043
  })
4883
5044
  );
5045
+
5046
+ if (isMultistream) {
5047
+ describe('fallback from multistream to transcoded', () => {
5048
+ let multistreamEventListeners;
5049
+ let transcodedEventListeners;
5050
+ let mockStatsAnalyzerCtor;
5051
+
5052
+ const setupFakeRoapMediaConnection = (fakeRoapMediaConnection, eventListeners) => {
5053
+ fakeRoapMediaConnection.on.callsFake((eventName, cb) => {
5054
+ eventListeners[eventName] = cb;
5055
+ });
5056
+ fakeRoapMediaConnection.initiateOffer.callsFake(() => {
5057
+ // simulate offer being generated
5058
+ eventListeners[MediaConnectionEventNames.LOCAL_SDP_OFFER_GENERATED]();
5059
+
5060
+ return Promise.resolve();
5061
+ });
5062
+ };
5063
+
5064
+ beforeEach(() => {
5065
+ multistreamEventListeners = {};
5066
+ transcodedEventListeners = {};
5067
+
5068
+ meeting.config.stats.enableStatsAnalyzer = true;
5069
+
5070
+ setupFakeRoapMediaConnection(fakeRoapMediaConnection, transcodedEventListeners);
5071
+ setupFakeRoapMediaConnection(
5072
+ fakeMultistreamRoapMediaConnection,
5073
+ multistreamEventListeners
5074
+ );
5075
+
5076
+ mockStatsAnalyzerCtor = sinon
5077
+ .stub(InternalMediaCoreModule, 'StatsAnalyzer')
5078
+ .callsFake(() => {
5079
+ return {on: sinon.stub(), stopAnalyzer: sinon.stub()};
5080
+ });
5081
+
5082
+ webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode =
5083
+ sinon.stub();
5084
+
5085
+ // setup the mock so that we get an SDP answer not from Homer
5086
+ locusMediaRequestStub.callsFake(() => {
5087
+ return Promise.resolve({
5088
+ body: {
5089
+ locus: {},
5090
+ mediaConnections: [
5091
+ {
5092
+ remoteSdp:
5093
+ '{"audioMuted":false,"videoMuted":true,"roapMessage":{"messageType":"ANSWER","version":"2","seq":1,"sdps":["v=0\\r\\no=linus 0 1 IN IP4 23.89.67.4\\r\\ns=-\\r\\nc=IN IP4 23.89.67.4\\r\\n"],"headers":["noOkInTransaction"]},"type":"SDP"}',
5094
+ },
5095
+ ],
5096
+ },
5097
+ });
5098
+ });
5099
+
5100
+ sinon.stub(meeting, 'closePeerConnections');
5101
+ sinon.stub(meeting.mediaProperties, 'unsetPeerConnection');
5102
+ sinon.stub(meeting.locusMediaRequest, 'downgradeFromMultistreamToTranscoded');
5103
+ });
5104
+
5105
+ const runCheck = async (turnServerInfo, forceTurnDiscovery) => {
5106
+ // we're calling addMediaInternal() with mic stream,
5107
+ // so that we also verify that audioMute, videoMute info is correctly sent to backend
5108
+ const addMediaPromise = meeting.addMediaInternal(
5109
+ () => '',
5110
+ turnServerInfo,
5111
+ forceTurnDiscovery,
5112
+ {
5113
+ localStreams: {microphone: fakeMicrophoneStream},
5114
+ }
5115
+ );
5116
+ await testUtils.flushPromises();
5117
+ await simulateRoapOffer(false);
5118
+
5119
+ // check MultistreamRoapMediaConnection was created correctly
5120
+ checkMediaConnectionCreated({
5121
+ expectMultistream: true,
5122
+ mediaConnectionConfig: expectedMediaConnectionConfig,
5123
+ localStreams: {
5124
+ audio: fakeMicrophoneStream,
5125
+ video: undefined,
5126
+ screenShareVideo: undefined,
5127
+ screenShareAudio: undefined,
5128
+ },
5129
+ direction: {
5130
+ audio: 'sendrecv',
5131
+ video: 'sendrecv',
5132
+ screenShare: 'recvonly',
5133
+ },
5134
+ remoteQualityLevel: 'HIGH',
5135
+ expectedDebugId,
5136
+ meetingId: meeting.id,
5137
+ });
5138
+
5139
+ // check that stats analyzer was created with the right config and store the reference to it so that we can later check that it was stopped
5140
+ assert.calledOnceWithExactly(
5141
+ mockStatsAnalyzerCtor,
5142
+ sinon.match({
5143
+ isMultistream: true,
5144
+ })
5145
+ );
5146
+ const initialStatsAnalyzer = mockStatsAnalyzerCtor.returnValues[0];
5147
+ mockStatsAnalyzerCtor.resetHistory();
5148
+
5149
+ // TURN discovery was done (if needed)
5150
+ if (turnServerInfo) {
5151
+ assert.notCalled(meeting.roap.doTurnDiscovery);
5152
+ } else {
5153
+ assert.calledWith(meeting.roap.doTurnDiscovery, meeting, false, false);
5154
+ }
5155
+
5156
+ // and SDP offer was sent with the right audioMuted/videoMuted values
5157
+ checkSdpOfferSent({audioMuted: false, videoMuted: true});
5158
+
5159
+ await testUtils.flushPromises();
5160
+
5161
+ // at this point the meeting should have been downgraded to transcoded
5162
+ assert.equal(meeting.isMultistream, false);
5163
+
5164
+ // old stats analyzer stopped and new one created
5165
+ assert.calledOnce(initialStatsAnalyzer.stopAnalyzer);
5166
+ assert.calledOnceWithExactly(
5167
+ mockStatsAnalyzerCtor,
5168
+ sinon.match({
5169
+ isMultistream: false,
5170
+ })
5171
+ );
5172
+
5173
+ // and correct cleanup of other things should have been done
5174
+ assert.calledOnceWithExactly(meeting.closePeerConnections, false);
5175
+ assert.calledOnceWithExactly(meeting.mediaProperties.unsetPeerConnection);
5176
+ assert.calledOnceWithExactly(
5177
+ meeting.locusMediaRequest.downgradeFromMultistreamToTranscoded
5178
+ );
5179
+
5180
+ // new connection should have been created
5181
+ checkMediaConnectionCreated({
5182
+ expectMultistream: false,
5183
+ mediaConnectionConfig: expectedMediaConnectionConfig,
5184
+ localStreams: {
5185
+ audio: fakeMicrophoneStream,
5186
+ video: undefined,
5187
+ screenShareVideo: undefined,
5188
+ screenShareAudio: undefined,
5189
+ },
5190
+ direction: {
5191
+ audio: 'sendrecv',
5192
+ video: 'sendrecv',
5193
+ screenShare: 'recvonly',
5194
+ },
5195
+ remoteQualityLevel: 'HIGH',
5196
+ expectedDebugId,
5197
+ meetingId: meeting.id,
5198
+ });
5199
+
5200
+ // and new TURN discovery done (no matter if it was being done before or not)
5201
+ assert.calledWith(meeting.roap.doTurnDiscovery, meeting, true, true);
5202
+
5203
+ // simulate new offer
5204
+ await simulateRoapOffer(false);
5205
+ checkSdpOfferSent({audioMuted: false, videoMuted: true});
5206
+
5207
+ // overall there should have been 2 calls to locusMediaRequestStub, because 2 offers were sent
5208
+ assert.calledTwice(locusMediaRequestStub);
5209
+
5210
+ // simulate answer being processed correctly
5211
+ transcodedEventListeners[MediaConnectionEventNames.REMOTE_SDP_ANSWER_PROCESSED]();
5212
+
5213
+ // check that addMedia finally resolved
5214
+ await addMediaPromise;
5215
+ };
5216
+
5217
+ it('addMedia() falls back to transcoded if SDP answer is not from Homer', async () => {
5218
+ // call addMediaInternal like addMedia() does it
5219
+ await runCheck(undefined, false);
5220
+ });
5221
+
5222
+ it('addMediaInternal() correctly falls back to transcoded if SDP answer is not from Homer (joinWithMedia() case)', async () => {
5223
+ // call addMediaInternal the way joinWithMedia() does it - with TURN info already provided
5224
+ // and check that when we fallback to transcoded we still do another TURN discovery
5225
+ await runCheck(
5226
+ {
5227
+ url: 'turns:turn-server-url:443?transport=tcp',
5228
+ username: 'turn user',
5229
+ password: 'turn password',
5230
+ },
5231
+ false
5232
+ );
5233
+ });
5234
+
5235
+ it('addMediaInternal() correctly falls back to transcoded if SDP answer is not from Homer (joinWithMedia() retry case)', async () => {
5236
+ // call addMediaInternal the way joinWithMedia() does it when it does a retry - with TURN info already provided
5237
+ // but also with forceTurnDiscovery=true - this shouldn't affect the flow for fallback to transcoded in any way
5238
+ // but doing it just for completeness
5239
+ await runCheck(
5240
+ {
5241
+ url: 'turns:turn-server-url:443?transport=tcp',
5242
+ username: 'turn user',
5243
+ password: 'turn password',
5244
+ },
5245
+ true
5246
+ );
5247
+ });
5248
+ });
5249
+ }
4884
5250
  })
4885
5251
  );
4886
5252
 
@@ -4958,6 +5324,11 @@ describe('plugin-meetings', () => {
4958
5324
  meeting.logger.error = sinon.stub().returns(true);
4959
5325
  meeting.updateLLMConnection = sinon.stub().returns(Promise.resolve());
4960
5326
  webex.internal.voicea.off = sinon.stub().returns(true);
5327
+ meeting.stopTranscription = sinon.stub();
5328
+ meeting.transcription = {};
5329
+
5330
+ meeting.annotation.deregisterEvents = sinon.stub();
5331
+ webex.internal.llm.off = sinon.stub();
4961
5332
 
4962
5333
  // A meeting needs to be joined to leave
4963
5334
  meeting.meetingState = 'ACTIVE';
@@ -4978,6 +5349,9 @@ describe('plugin-meetings', () => {
4978
5349
  assert.calledOnce(meeting.closePeerConnections);
4979
5350
  assert.calledOnce(meeting.unsetRemoteStreams);
4980
5351
  assert.calledOnce(meeting.unsetPeerConnections);
5352
+ assert.calledOnce(meeting.stopTranscription);
5353
+ assert.calledOnce(meeting.annotation.deregisterEvents);
5354
+ assert.calledWith(webex.internal.llm.off, 'event:relay.event', meeting.processRelayEvent);
4981
5355
  });
4982
5356
 
4983
5357
  it('should reset call diagnostic latencies correctly', async () => {
@@ -6332,29 +6706,74 @@ describe('plugin-meetings', () => {
6332
6706
  assert.equal(meeting.fetchMeetingInfoTimeoutId, undefined);
6333
6707
  });
6334
6708
 
6335
- it('handles meetingInfoProvider webinar need registration error', async () => {
6709
+ it('handles MeetingInfoV2JoinWebinarError webinar need registration', async () => {
6336
6710
  meeting.destination = FAKE_DESTINATION;
6337
6711
  meeting.destinationType = FAKE_TYPE;
6338
6712
  meeting.attrs.meetingInfoProvider = {
6339
6713
  fetchMeetingInfo: sinon
6340
6714
  .stub()
6341
6715
  .throws(
6342
- new MeetingInfoV2WebinarRegistrationError(403021, FAKE_MEETING_INFO, 'a message')
6716
+ new MeetingInfoV2JoinWebinarError(403021, FAKE_MEETING_INFO, 'a message')
6343
6717
  ),
6344
6718
  };
6345
6719
 
6346
6720
  await assert.isRejected(
6347
6721
  meeting.fetchMeetingInfo({sendCAevents: true}),
6348
- WebinarRegistrationError
6722
+ JoinWebinarError
6349
6723
  );
6350
6724
 
6351
6725
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6352
- assert.equal(meeting.meetingInfoFailureCode, 403021);
6353
6726
  assert.equal(
6354
6727
  meeting.meetingInfoFailureReason,
6355
6728
  MEETING_INFO_FAILURE_REASON.WEBINAR_REGISTRATION
6356
6729
  );
6357
6730
  });
6731
+
6732
+ it('handles MeetingInfoV2JoinWebinarError webinar need join with webcast', async () => {
6733
+ meeting.destination = FAKE_DESTINATION;
6734
+ meeting.destinationType = FAKE_TYPE;
6735
+ meeting.attrs.meetingInfoProvider = {
6736
+ fetchMeetingInfo: sinon
6737
+ .stub()
6738
+ .throws(
6739
+ new MeetingInfoV2JoinWebinarError(403026, FAKE_MEETING_INFO, 'a message')
6740
+ ),
6741
+ };
6742
+
6743
+ await assert.isRejected(
6744
+ meeting.fetchMeetingInfo({sendCAevents: true}),
6745
+ JoinWebinarError
6746
+ );
6747
+
6748
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6749
+ assert.equal(
6750
+ meeting.meetingInfoFailureReason,
6751
+ MEETING_INFO_FAILURE_REASON.NEED_JOIN_WITH_WEBCAST
6752
+ );
6753
+ });
6754
+
6755
+ it('handles MeetingInfoV2JoinWebinarError webinar need registrationId', async () => {
6756
+ meeting.destination = FAKE_DESTINATION;
6757
+ meeting.destinationType = FAKE_TYPE;
6758
+ meeting.attrs.meetingInfoProvider = {
6759
+ fetchMeetingInfo: sinon
6760
+ .stub()
6761
+ .throws(
6762
+ new MeetingInfoV2JoinWebinarError(403037, FAKE_MEETING_INFO, 'a message')
6763
+ ),
6764
+ };
6765
+
6766
+ await assert.isRejected(
6767
+ meeting.fetchMeetingInfo({sendCAevents: true}),
6768
+ JoinWebinarError
6769
+ );
6770
+
6771
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6772
+ assert.equal(
6773
+ meeting.meetingInfoFailureReason,
6774
+ MEETING_INFO_FAILURE_REASON.WEBINAR_NEED_REGISTRATIONID
6775
+ );
6776
+ });
6358
6777
  });
6359
6778
 
6360
6779
  describe('#refreshPermissionToken', () => {
@@ -6815,6 +7234,9 @@ describe('plugin-meetings', () => {
6815
7234
  meeting.transcription = {};
6816
7235
  meeting.stopTranscription = sinon.stub();
6817
7236
 
7237
+ meeting.annotation.deregisterEvents = sinon.stub();
7238
+ webex.internal.llm.off = sinon.stub();
7239
+
6818
7240
  // A meeting needs to be joined to end
6819
7241
  meeting.meetingState = 'ACTIVE';
6820
7242
  meeting.state = 'JOINED';
@@ -6835,6 +7257,9 @@ describe('plugin-meetings', () => {
6835
7257
  assert.calledOnce(meeting?.unsetRemoteStreams);
6836
7258
  assert.calledOnce(meeting?.unsetPeerConnections);
6837
7259
  assert.calledOnce(meeting?.stopTranscription);
7260
+
7261
+ assert.called(meeting.annotation.deregisterEvents);
7262
+ assert.calledWith(webex.internal.llm.off, 'event:relay.event', meeting.processRelayEvent);
6838
7263
  });
6839
7264
  });
6840
7265
 
@@ -7817,7 +8242,9 @@ describe('plugin-meetings', () => {
7817
8242
  });
7818
8243
 
7819
8244
  it('should collect ice candidates', () => {
7820
- eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({candidate: {candidate: 'candidate'}});
8245
+ eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]({
8246
+ candidate: {candidate: 'candidate'},
8247
+ });
7821
8248
 
7822
8249
  assert.equal(meeting.iceCandidatesCount, 1);
7823
8250
  });
@@ -8123,10 +8550,10 @@ describe('plugin-meetings', () => {
8123
8550
  meeting.statsAnalyzer.stopAnalyzer = sinon.stub().resolves();
8124
8551
  meeting.reconnectionManager = {
8125
8552
  reconnect: sinon.stub().resolves(),
8126
- resetReconnectionTimer: () => {}
8553
+ resetReconnectionTimer: () => {},
8127
8554
  };
8128
8555
  meeting.currentMediaStatus = {
8129
- video: true
8556
+ video: true,
8130
8557
  };
8131
8558
 
8132
8559
  await mockFailedEvent();
@@ -8408,8 +8835,7 @@ describe('plugin-meetings', () => {
8408
8835
  assert.calledWith(meeting.roapMessageReceived, fakeAnswer);
8409
8836
  });
8410
8837
 
8411
- it('handles OFFER message correctly when request fails', async () => {
8412
- const fakeError = new Error('fake error');
8838
+ const runOfferSendingFailureTest = async (fakeError, canProceed, expectedErrorCode) => {
8413
8839
  const clock = sinon.useFakeTimers();
8414
8840
  sinon.spy(clock, 'clearTimeout');
8415
8841
  meeting.deferSDPAnswer = {reject: sinon.stub()};
@@ -8447,19 +8873,31 @@ describe('plugin-meetings', () => {
8447
8873
  assert.equal(meeting.sdpResponseTimer, undefined);
8448
8874
 
8449
8875
  assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
8450
- clientErrorCode: 2007,
8876
+ clientErrorCode: expectedErrorCode,
8451
8877
  });
8452
8878
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
8453
8879
  name: 'client.media-engine.remote-sdp-received',
8454
8880
  payload: {
8455
- canProceed: false,
8456
- errors: [{errorCode: 2007, fatal: true}],
8881
+ canProceed,
8882
+ errors: [{errorCode: expectedErrorCode, fatal: true}],
8457
8883
  },
8458
8884
  options: {
8459
8885
  meetingId: meeting.id,
8460
8886
  rawError: fakeError,
8461
8887
  },
8462
8888
  });
8889
+ };
8890
+
8891
+ it('handles OFFER message correctly when request fails', async () => {
8892
+ const fakeError = new Error('fake error');
8893
+
8894
+ await runOfferSendingFailureTest(fakeError, false, 2007);
8895
+ });
8896
+
8897
+ it('handles OFFER message correctly when we get a non-homer answer', async () => {
8898
+ const fakeError = new MultistreamNotSupportedError();
8899
+
8900
+ await runOfferSendingFailureTest(fakeError, true, 2012);
8463
8901
  });
8464
8902
 
8465
8903
  it('handles ANSWER message correctly', () => {
@@ -8662,6 +9100,7 @@ describe('plugin-meetings', () => {
8662
9100
  });
8663
9101
  });
8664
9102
  });
9103
+
8665
9104
  describe('#setUpLocusInfoSelfListener', () => {
8666
9105
  it('listens to the self unadmitted guest event', (done) => {
8667
9106
  meeting.startKeepAlive = sinon.stub();
@@ -8756,6 +9195,26 @@ describe('plugin-meetings', () => {
8756
9195
  );
8757
9196
  });
8758
9197
 
9198
+ it('listens to the brb state changed event', () => {
9199
+ const assertBrb = (enabled) => {
9200
+ meeting.locusInfo.emit(
9201
+ { function: 'test', file: 'test' },
9202
+ LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
9203
+ { brb: { enabled } },
9204
+ )
9205
+ assert.calledWithExactly(
9206
+ TriggerProxy.trigger,
9207
+ meeting,
9208
+ {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
9209
+ EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
9210
+ { payload: { brb: { enabled } } },
9211
+ );
9212
+ }
9213
+
9214
+ assertBrb(true);
9215
+ assertBrb(false);
9216
+ })
9217
+
8759
9218
  it('listens to the interpretation changed event', () => {
8760
9219
  meeting.simultaneousInterpretation.updateSelfInterpretation = sinon.stub();
8761
9220
 
@@ -9054,7 +9513,7 @@ describe('plugin-meetings', () => {
9054
9513
  {state}
9055
9514
  );
9056
9515
 
9057
- assert.calledOnceWithExactly( meeting.webinar.updatePracticeSessionStatus, state);
9516
+ assert.calledOnceWithExactly(meeting.webinar.updatePracticeSessionStatus, state);
9058
9517
  assert.calledWith(
9059
9518
  TriggerProxy.trigger,
9060
9519
  meeting,
@@ -9538,14 +9997,39 @@ describe('plugin-meetings', () => {
9538
9997
  it('should close the webrtc media connection, and return a promise', async () => {
9539
9998
  const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
9540
9999
  meeting.mediaProperties.webrtcMediaConnection = {close: sinon.stub()};
10000
+
10001
+ meeting.audio = {id: 'fakeAudioMuteState'};
10002
+ meeting.video = {id: 'fakeVideoMuteState'};
10003
+
9541
10004
  const pcs = meeting.closePeerConnections();
9542
10005
 
9543
10006
  assert.exists(pcs.then);
9544
10007
  await pcs;
9545
10008
  assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.close);
9546
10009
  assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
10010
+ assert.equal(meeting.audio, null);
10011
+ assert.equal(meeting.video, null);
10012
+ });
10013
+
10014
+ it('should close the webrtc media connection, but keep audio and video props unchanged if called with resetMuteStates=false', async () => {
10015
+ const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
10016
+ meeting.mediaProperties.webrtcMediaConnection = {close: sinon.stub()};
10017
+
10018
+ const fakeAudio = {id: 'fakeAudioMuteState'};
10019
+ const fakeVideo = {id: 'fakeVideoMuteState'};
10020
+
10021
+ meeting.audio = fakeAudio;
10022
+ meeting.video = fakeVideo;
10023
+
10024
+ await meeting.closePeerConnections(false);
10025
+
10026
+ assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.close);
10027
+ assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
10028
+ assert.equal(meeting.audio, fakeAudio);
10029
+ assert.equal(meeting.video, fakeVideo);
9547
10030
  });
9548
10031
  });
10032
+
9549
10033
  describe('#unsetPeerConnections', () => {
9550
10034
  it('should unset the peer connections', () => {
9551
10035
  meeting.mediaProperties.unsetPeerConnection = sinon.stub().returns(true);
@@ -10674,6 +11158,7 @@ describe('plugin-meetings', () => {
10674
11158
  meeting.webex.internal.llm.on = sinon.stub();
10675
11159
  meeting.webex.internal.llm.off = sinon.stub();
10676
11160
  meeting.processRelayEvent = sinon.stub();
11161
+ meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(false);
10677
11162
  });
10678
11163
 
10679
11164
  it('does not connect if the call is not joined yet', async () => {
@@ -10805,6 +11290,19 @@ describe('plugin-meetings', () => {
10805
11290
  meeting.processRelayEvent
10806
11291
  );
10807
11292
  });
11293
+
11294
+
11295
+ it('connect ps data channel if ps started in webinar', async () => {
11296
+ meeting.joinedWith = {state: 'JOINED'};
11297
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url', practiceSessionDatachannelUrl: 'a ps datachannel url'}};
11298
+ meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(true);
11299
+ await meeting.updateLLMConnection();
11300
+
11301
+ assert.notCalled(webex.internal.llm.disconnectLLM);
11302
+ assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a ps datachannel url');
11303
+
11304
+ });
11305
+
10808
11306
  });
10809
11307
 
10810
11308
  describe('#setLocus', () => {
@@ -10996,6 +11494,7 @@ describe('plugin-meetings', () => {
10996
11494
  beforeEach(() => {
10997
11495
  meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
10998
11496
  meeting.deviceUrl = 'my-web-url';
11497
+ meeting.locusInfo.info = {isWebinar: false};
10999
11498
  });
11000
11499
 
11001
11500
  const USER_IDS = {
@@ -11221,13 +11720,24 @@ describe('plugin-meetings', () => {
11221
11720
 
11222
11721
  activeSharingId.whiteboard = beneficiaryId;
11223
11722
 
11224
- eventTrigger.share.push({
11723
+ eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
11724
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11725
+ functionName: 'remoteShare',
11726
+ eventPayload: {
11727
+ memberId: null,
11728
+ url,
11729
+ shareInstanceId,
11730
+ annotationInfo: undefined,
11731
+ resourceType: undefined,
11732
+ },
11733
+ } : {
11225
11734
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11226
11735
  functionName: 'startWhiteboardShare',
11227
11736
  eventPayload: {resourceUrl, memberId: beneficiaryId},
11228
11737
  });
11229
11738
 
11230
- shareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11739
+ shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11740
+
11231
11741
  }
11232
11742
 
11233
11743
  if (eventTrigger.member) {
@@ -11259,13 +11769,24 @@ describe('plugin-meetings', () => {
11259
11769
  newPayload.current.content.disposition = FLOOR_ACTION.ACCEPTED;
11260
11770
  newPayload.current.content.beneficiaryId = otherBeneficiaryId;
11261
11771
 
11262
- eventTrigger.share.push({
11772
+ eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
11773
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11774
+ functionName: 'remoteShare',
11775
+ eventPayload: {
11776
+ memberId: null,
11777
+ url,
11778
+ shareInstanceId,
11779
+ annotationInfo: undefined,
11780
+ resourceType: undefined,
11781
+ },
11782
+ } : {
11263
11783
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11264
11784
  functionName: 'startWhiteboardShare',
11265
11785
  eventPayload: {resourceUrl, memberId: beneficiaryId},
11266
11786
  });
11267
11787
 
11268
- shareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11788
+ shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11789
+
11269
11790
  } else {
11270
11791
  eventTrigger.share.push({
11271
11792
  eventName: EVENT_TRIGGERS.MEETING_STOPPED_SHARING_WHITEBOARD,
@@ -11392,6 +11913,38 @@ describe('plugin-meetings', () => {
11392
11913
  assert.exists(meeting.setUpLocusMediaSharesListener);
11393
11914
  });
11394
11915
 
11916
+ describe('Whiteboard Share - Webinar Attendee', () => {
11917
+ it('Scenario #1: Whiteboard sharing as a webinar attendee', () => {
11918
+ // Set the webinar attendee flag
11919
+ meeting.webinar = { selfIsAttendee: true };
11920
+ meeting.locusInfo.info.isWebinar = true;
11921
+
11922
+ // Step 1: Start sharing whiteboard A
11923
+ const data1 = generateData(
11924
+ blankPayload, // Initial payload
11925
+ true, // isGranting: Granting share
11926
+ false, // isContent: Whiteboard (not content)
11927
+ USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
11928
+ RESOURCE_URLS.WHITEBOARD_A // Resource URL: Whiteboard A
11929
+ );
11930
+
11931
+ // Step 2: Stop sharing whiteboard A
11932
+ const data2 = generateData(
11933
+ data1.payload, // Updated payload from Step 1
11934
+ false, // isGranting: Stopping share
11935
+ false, // isContent: Whiteboard
11936
+ USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
11937
+ );
11938
+
11939
+ // Validate the payload changes and status updates
11940
+ payloadTestHelper([data1]);
11941
+
11942
+ // Specific assertions for webinar attendee status
11943
+ assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
11944
+ });
11945
+ });
11946
+
11947
+
11395
11948
  describe('Whiteboard A --> Whiteboard B', () => {
11396
11949
  it('Scenario #1: you share both whiteboards', () => {
11397
11950
  const data1 = generateData(
@@ -12067,9 +12620,12 @@ describe('plugin-meetings', () => {
12067
12620
  it('startKeepAlive starts the keep alive', async () => {
12068
12621
  meeting.meetingRequest.keepAlive = sinon.stub().returns(Promise.resolve());
12069
12622
 
12623
+ const keepAliveUrl1 = 'keep.alive.url1';
12624
+ const keepAliveUrl2 = 'keep.alive.url2';
12625
+
12070
12626
  assert.isNull(meeting.keepAliveTimerId);
12071
12627
  meeting.joinedWith = {
12072
- keepAliveUrl: defaultKeepAliveUrl,
12628
+ keepAliveUrl: keepAliveUrl1,
12073
12629
  keepAliveSecs: defaultKeepAliveSecs,
12074
12630
  };
12075
12631
  meeting.startKeepAlive();
@@ -12078,12 +12634,15 @@ describe('plugin-meetings', () => {
12078
12634
  assert.notCalled(meeting.meetingRequest.keepAlive);
12079
12635
  await progressTime(defaultExpectedInterval);
12080
12636
  assert.calledOnceWithExactly(meeting.meetingRequest.keepAlive, {
12081
- keepAliveUrl: defaultKeepAliveUrl,
12637
+ keepAliveUrl: keepAliveUrl1,
12082
12638
  });
12639
+ // joinedWith keep alive url might change (when we fallback from multistream to transcoded)
12640
+ meeting.joinedWith.keepAliveUrl = keepAliveUrl2;
12641
+
12083
12642
  await progressTime(defaultExpectedInterval);
12084
12643
  assert.calledTwice(meeting.meetingRequest.keepAlive);
12085
- assert.alwaysCalledWithExactly(meeting.meetingRequest.keepAlive, {
12086
- keepAliveUrl: defaultKeepAliveUrl,
12644
+ assert.calledWith(meeting.meetingRequest.keepAlive, {
12645
+ keepAliveUrl: keepAliveUrl2,
12087
12646
  });
12088
12647
  });
12089
12648
  it('startKeepAlive handles existing keepAliveTimerId', async () => {
@@ -12681,5 +13240,26 @@ describe('plugin-meetings', () => {
12681
13240
  assert.calledOnceWithExactly(getMediaServer, 'fake sdp');
12682
13241
  assert.equal(meeting.mediaProperties.webrtcMediaConnection.mediaServer, 'homer');
12683
13242
  });
13243
+
13244
+ it('throws MultistreamNotSupportedError if we get a non-homer SDP answer', async () => {
13245
+ const fakeMessage = {messageType: 'ANSWER', sdp: 'fake sdp'};
13246
+
13247
+ meeting.isMultistream = true;
13248
+ meeting.mediaProperties.webrtcMediaConnection = {
13249
+ roapMessageReceived: sinon.stub(),
13250
+ };
13251
+
13252
+ sinon.stub(MeetingsUtil, 'getMediaServer').returns('linus');
13253
+
13254
+ try {
13255
+ await meeting.roapMessageReceived(fakeMessage);
13256
+
13257
+ assert.fail('Expected MultistreamNotSupportedError to be thrown');
13258
+ } catch(e) {
13259
+ assert.isTrue(e instanceof MultistreamNotSupportedError);
13260
+ }
13261
+
13262
+ assert.notCalled(meeting.mediaProperties.webrtcMediaConnection.roapMessageReceived);
13263
+ });
12684
13264
  });
12685
13265
  });