@webex/plugin-meetings 3.8.0-next.4 → 3.8.0-next.41

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 (130) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/config.js +1 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/constants.js +14 -1
  6. package/dist/constants.js.map +1 -1
  7. package/dist/controls-options-manager/enums.js +2 -0
  8. package/dist/controls-options-manager/enums.js.map +1 -1
  9. package/dist/controls-options-manager/types.js.map +1 -1
  10. package/dist/controls-options-manager/util.js +52 -0
  11. package/dist/controls-options-manager/util.js.map +1 -1
  12. package/dist/interpretation/index.js +1 -1
  13. package/dist/interpretation/siLanguage.js +1 -1
  14. package/dist/locus-info/controlsUtils.js +28 -10
  15. package/dist/locus-info/controlsUtils.js.map +1 -1
  16. package/dist/locus-info/index.js +20 -1
  17. package/dist/locus-info/index.js.map +1 -1
  18. package/dist/media/index.js +3 -15
  19. package/dist/media/index.js.map +1 -1
  20. package/dist/meeting/in-meeting-actions.js +11 -1
  21. package/dist/meeting/in-meeting-actions.js.map +1 -1
  22. package/dist/meeting/index.js +443 -256
  23. package/dist/meeting/index.js.map +1 -1
  24. package/dist/meeting/locusMediaRequest.js +21 -22
  25. package/dist/meeting/locusMediaRequest.js.map +1 -1
  26. package/dist/meeting/muteState.js +0 -2
  27. package/dist/meeting/muteState.js.map +1 -1
  28. package/dist/meeting/request.js +30 -0
  29. package/dist/meeting/request.js.map +1 -1
  30. package/dist/meeting/request.type.js.map +1 -1
  31. package/dist/meeting/util.js +10 -2
  32. package/dist/meeting/util.js.map +1 -1
  33. package/dist/meeting-info/meeting-info-v2.js +359 -60
  34. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  35. package/dist/meetings/index.js +60 -1
  36. package/dist/meetings/index.js.map +1 -1
  37. package/dist/member/index.js +10 -0
  38. package/dist/member/index.js.map +1 -1
  39. package/dist/member/util.js +3 -0
  40. package/dist/member/util.js.map +1 -1
  41. package/dist/metrics/constants.js +9 -0
  42. package/dist/metrics/constants.js.map +1 -1
  43. package/dist/reachability/clusterReachability.js +52 -8
  44. package/dist/reachability/clusterReachability.js.map +1 -1
  45. package/dist/reachability/index.js +70 -45
  46. package/dist/reachability/index.js.map +1 -1
  47. package/dist/reachability/reachability.types.js +14 -0
  48. package/dist/reachability/reachability.types.js.map +1 -1
  49. package/dist/reachability/request.js +19 -3
  50. package/dist/reachability/request.js.map +1 -1
  51. package/dist/reconnection-manager/index.js +2 -2
  52. package/dist/reconnection-manager/index.js.map +1 -1
  53. package/dist/recording-controller/util.js +5 -5
  54. package/dist/recording-controller/util.js.map +1 -1
  55. package/dist/roap/index.js.map +1 -1
  56. package/dist/roap/turnDiscovery.js +45 -27
  57. package/dist/roap/turnDiscovery.js.map +1 -1
  58. package/dist/roap/types.js +17 -0
  59. package/dist/roap/types.js.map +1 -0
  60. package/dist/types/config.d.ts +1 -0
  61. package/dist/types/constants.d.ts +10 -0
  62. package/dist/types/controls-options-manager/enums.d.ts +3 -1
  63. package/dist/types/controls-options-manager/types.d.ts +7 -1
  64. package/dist/types/locus-info/index.d.ts +1 -0
  65. package/dist/types/meeting/in-meeting-actions.d.ts +10 -0
  66. package/dist/types/meeting/index.d.ts +47 -1
  67. package/dist/types/meeting/muteState.d.ts +0 -1
  68. package/dist/types/meeting/request.d.ts +12 -1
  69. package/dist/types/meeting/request.type.d.ts +6 -0
  70. package/dist/types/meeting/util.d.ts +2 -1
  71. package/dist/types/meeting-info/meeting-info-v2.d.ts +80 -0
  72. package/dist/types/meetings/index.d.ts +29 -0
  73. package/dist/types/member/index.d.ts +1 -0
  74. package/dist/types/metrics/constants.d.ts +9 -0
  75. package/dist/types/reachability/clusterReachability.d.ts +13 -1
  76. package/dist/types/reachability/index.d.ts +2 -1
  77. package/dist/types/reachability/reachability.types.d.ts +5 -0
  78. package/dist/types/roap/index.d.ts +3 -2
  79. package/dist/types/roap/turnDiscovery.d.ts +5 -17
  80. package/dist/types/roap/types.d.ts +16 -0
  81. package/dist/webinar/index.js +1 -1
  82. package/package.json +22 -22
  83. package/src/config.ts +1 -0
  84. package/src/constants.ts +17 -0
  85. package/src/controls-options-manager/enums.ts +2 -0
  86. package/src/controls-options-manager/types.ts +11 -1
  87. package/src/controls-options-manager/util.ts +62 -0
  88. package/src/locus-info/controlsUtils.ts +44 -14
  89. package/src/locus-info/index.ts +23 -1
  90. package/src/media/index.ts +5 -21
  91. package/src/meeting/in-meeting-actions.ts +20 -0
  92. package/src/meeting/index.ts +263 -69
  93. package/src/meeting/locusMediaRequest.ts +27 -22
  94. package/src/meeting/muteState.ts +0 -2
  95. package/src/meeting/request.ts +36 -1
  96. package/src/meeting/request.type.ts +7 -0
  97. package/src/meeting/util.ts +9 -2
  98. package/src/meeting-info/meeting-info-v2.ts +247 -6
  99. package/src/meetings/index.ts +72 -1
  100. package/src/member/index.ts +11 -0
  101. package/src/member/util.ts +3 -0
  102. package/src/metrics/constants.ts +9 -0
  103. package/src/reachability/clusterReachability.ts +47 -1
  104. package/src/reachability/index.ts +15 -0
  105. package/src/reachability/reachability.types.ts +6 -0
  106. package/src/reachability/request.ts +7 -0
  107. package/src/reconnection-manager/index.ts +2 -2
  108. package/src/recording-controller/util.ts +17 -13
  109. package/src/roap/index.ts +3 -7
  110. package/src/roap/turnDiscovery.ts +34 -39
  111. package/src/roap/types.ts +23 -0
  112. package/test/unit/spec/controls-options-manager/util.js +120 -0
  113. package/test/unit/spec/locus-info/controlsUtils.js +103 -9
  114. package/test/unit/spec/locus-info/index.js +28 -0
  115. package/test/unit/spec/media/index.ts +6 -16
  116. package/test/unit/spec/meeting/in-meeting-actions.ts +13 -4
  117. package/test/unit/spec/meeting/index.js +490 -130
  118. package/test/unit/spec/meeting/locusMediaRequest.ts +95 -87
  119. package/test/unit/spec/meeting/muteState.js +0 -2
  120. package/test/unit/spec/meeting/request.js +32 -1
  121. package/test/unit/spec/meeting/utils.js +115 -18
  122. package/test/unit/spec/meeting-info/meetinginfov2.js +443 -114
  123. package/test/unit/spec/meetings/index.js +78 -1
  124. package/test/unit/spec/member/index.js +7 -0
  125. package/test/unit/spec/member/util.js +24 -0
  126. package/test/unit/spec/reachability/clusterReachability.ts +47 -1
  127. package/test/unit/spec/reachability/index.ts +12 -0
  128. package/test/unit/spec/reachability/request.js +47 -2
  129. package/test/unit/spec/reconnection-manager/index.js +4 -4
  130. package/test/unit/spec/roap/turnDiscovery.ts +110 -28
@@ -1,6 +1,7 @@
1
1
  /*!
2
2
  * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
3
  */
4
+ import {v4 as uuidv4} from 'uuid';
4
5
  import 'jsdom-global/register';
5
6
  import {cloneDeep, forEach, isEqual, isUndefined} from 'lodash';
6
7
  import sinon from 'sinon';
@@ -93,13 +94,14 @@ import CaptchaError from '../../../../src/common/errors/captcha-error';
93
94
  import PermissionError from '../../../../src/common/errors/permission';
94
95
  import JoinWebinarError from '../../../../src/common/errors/join-webinar-error';
95
96
  import IntentToJoinError from '../../../../src/common/errors/intent-to-join';
96
- import MultistreamNotSupportedError from '../../../../src/common/errors/multistream-not-supported-error';;
97
+ import MultistreamNotSupportedError from '../../../../src/common/errors/multistream-not-supported-error';
97
98
  import testUtils from '../../../utils/testUtils';
98
99
  import {
99
100
  MeetingInfoV2CaptchaError,
100
101
  MeetingInfoV2PasswordError,
101
102
  MeetingInfoV2PolicyError,
102
- MeetingInfoV2JoinWebinarError, MeetingInfoV2JoinForbiddenError,
103
+ MeetingInfoV2JoinWebinarError,
104
+ MeetingInfoV2JoinForbiddenError,
103
105
  } from '../../../../src/meeting-info/meeting-info-v2';
104
106
  import {
105
107
  DTLS_HANDSHAKE_FAILED_CLIENT_CODE,
@@ -114,8 +116,9 @@ import {ERROR_DESCRIPTIONS} from '@webex/internal-plugin-metrics/src/call-diagno
114
116
  import MeetingCollection from '@webex/plugin-meetings/src/meetings/collection';
115
117
 
116
118
  import {EVENT_TRIGGERS as VOICEAEVENTS} from '@webex/internal-plugin-voicea';
117
- import { createBrbState } from '@webex/plugin-meetings/src/meeting/brbState';
118
- import JoinForbiddenError from '../../../../src/common/errors/join-forbidden-error';
119
+ import {createBrbState} from '@webex/plugin-meetings/src/meeting/brbState';
120
+ import JoinForbiddenError from '../../../../src/common/errors/join-forbidden-error';
121
+ import {EventEmitter} from 'stream';
119
122
 
120
123
  describe('plugin-meetings', () => {
121
124
  const logger = {
@@ -208,6 +211,8 @@ describe('plugin-meetings', () => {
208
211
  let membersSpy;
209
212
  let meetingRequestSpy;
210
213
  let correlationId;
214
+ let isoLocalClientMeetingJoinTime;
215
+ let uploadEvent;
211
216
 
212
217
  beforeEach(() => {
213
218
  webex = new MockWebex({
@@ -277,6 +282,8 @@ describe('plugin-meetings', () => {
277
282
  test4 = `test4-${uuid.v4()}`;
278
283
  testDestination = `testDestination-${uuid.v4()}`;
279
284
  correlationId = uuid.v4();
285
+ uploadEvent = new EventEmitter();
286
+ uploadEvent.addListener('progress', () => {});
280
287
 
281
288
  meeting = new Meeting(
282
289
  {
@@ -667,7 +674,7 @@ describe('plugin-meetings', () => {
667
674
  beforeEach(() => {
668
675
  meeting.join = sinon.stub().callsFake((joinOptions) => {
669
676
  meeting.isMultistream = joinOptions.enableMultistream;
670
- return Promise.resolve(fakeJoinResult)
677
+ return Promise.resolve(fakeJoinResult);
671
678
  });
672
679
  addMediaInternalStub = sinon
673
680
  .stub(meeting, 'addMediaInternal')
@@ -1070,7 +1077,11 @@ describe('plugin-meetings', () => {
1070
1077
  mediaOptions,
1071
1078
  });
1072
1079
 
1073
- assert.deepEqual(result, {join: fakeJoinResult, media: undefined, multistreamEnabled: false});
1080
+ assert.deepEqual(result, {
1081
+ join: fakeJoinResult,
1082
+ media: undefined,
1083
+ multistreamEnabled: false,
1084
+ });
1074
1085
 
1075
1086
  assert.calledOnce(meeting.join);
1076
1087
 
@@ -1174,7 +1185,10 @@ describe('plugin-meetings', () => {
1174
1185
  type: addMediaError.name,
1175
1186
  }
1176
1187
  );
1177
- assert.calledOnceWithExactly(meeting.leave, {resourceId: undefined, reason: 'joinWithMedia failure'})
1188
+ assert.calledOnceWithExactly(meeting.leave, {
1189
+ resourceId: undefined,
1190
+ reason: 'joinWithMedia failure',
1191
+ });
1178
1192
  });
1179
1193
  });
1180
1194
 
@@ -1680,10 +1694,6 @@ describe('plugin-meetings', () => {
1680
1694
  sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.resolve(joinMeetingResult));
1681
1695
  });
1682
1696
 
1683
- afterEach(() => {
1684
- assert.exists(meeting.isoLocalClientMeetingJoinTime);
1685
- });
1686
-
1687
1697
  it('should join the meeting and return promise', async () => {
1688
1698
  const join = meeting.join({pstnAudioType: 'dial-in'});
1689
1699
  meeting.config.enableAutomaticLLM = true;
@@ -2655,7 +2665,7 @@ describe('plugin-meetings', () => {
2655
2665
 
2656
2666
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
2657
2667
  turnServerInfo: {
2658
- url: FAKE_TURN_URL,
2668
+ urls: [FAKE_TURN_URL],
2659
2669
  username: FAKE_TURN_USER,
2660
2670
  password: FAKE_TURN_PASSWORD,
2661
2671
  },
@@ -2677,7 +2687,7 @@ describe('plugin-meetings', () => {
2677
2687
  meeting.id,
2678
2688
  sinon.match({
2679
2689
  turnServerInfo: {
2680
- url: FAKE_TURN_URL,
2690
+ urls: [FAKE_TURN_URL],
2681
2691
  username: FAKE_TURN_USER,
2682
2692
  password: FAKE_TURN_PASSWORD,
2683
2693
  },
@@ -2735,7 +2745,7 @@ describe('plugin-meetings', () => {
2735
2745
  .onSecondCall()
2736
2746
  .returns({
2737
2747
  turnServerInfo: {
2738
- url: FAKE_TURN_URL,
2748
+ urls: [FAKE_TURN_URL],
2739
2749
  username: FAKE_TURN_USER,
2740
2750
  password: FAKE_TURN_PASSWORD,
2741
2751
  },
@@ -2947,7 +2957,7 @@ describe('plugin-meetings', () => {
2947
2957
  .onSecondCall()
2948
2958
  .returns({
2949
2959
  turnServerInfo: {
2950
- url: FAKE_TURN_URL,
2960
+ urls: [FAKE_TURN_URL],
2951
2961
  username: FAKE_TURN_USER,
2952
2962
  password: FAKE_TURN_PASSWORD,
2953
2963
  },
@@ -3124,7 +3134,7 @@ describe('plugin-meetings', () => {
3124
3134
  .onSecondCall()
3125
3135
  .returns({
3126
3136
  turnServerInfo: {
3127
- url: FAKE_TURN_URL,
3137
+ urls: [FAKE_TURN_URL],
3128
3138
  username: FAKE_TURN_USER,
3129
3139
  password: FAKE_TURN_PASSWORD,
3130
3140
  },
@@ -3176,7 +3186,7 @@ describe('plugin-meetings', () => {
3176
3186
  .onSecondCall()
3177
3187
  .returns({
3178
3188
  turnServerInfo: {
3179
- url: FAKE_TURN_URL,
3189
+ urls: [FAKE_TURN_URL],
3180
3190
  username: FAKE_TURN_USER,
3181
3191
  password: FAKE_TURN_PASSWORD,
3182
3192
  },
@@ -3427,6 +3437,40 @@ describe('plugin-meetings', () => {
3427
3437
  });
3428
3438
  });
3429
3439
 
3440
+ it('LOCAL_MEDIA_STARTED triggers "meeting:media:local:start" event and does not send metric because we already have', async () => {
3441
+ meeting.shareCAEventSentStatus = {
3442
+ transmitStart: true,
3443
+ transmitStop: false,
3444
+ receiveStart: false,
3445
+ receiveStop: false,
3446
+ };
3447
+ statsAnalyzerStub.emit(
3448
+ {file: 'test', function: 'test'},
3449
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STARTED,
3450
+ {mediaType: 'share'}
3451
+ );
3452
+
3453
+ assert.calledWith(
3454
+ TriggerProxy.trigger,
3455
+ sinon.match.instanceOf(Meeting),
3456
+ {
3457
+ file: 'meeting/index',
3458
+ function: 'addMedia',
3459
+ },
3460
+ EVENT_TRIGGERS.MEETING_MEDIA_LOCAL_STARTED,
3461
+ {
3462
+ mediaType: 'share',
3463
+ }
3464
+ );
3465
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3466
+ name: 'client.media.tx.start',
3467
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3468
+ options: {
3469
+ meetingId: meeting.id,
3470
+ },
3471
+ });
3472
+ });
3473
+
3430
3474
  it('LOCAL_MEDIA_STOPPED triggers the right metrics', async () => {
3431
3475
  statsAnalyzerStub.emit(
3432
3476
  {file: 'test', function: 'test'},
@@ -3443,6 +3487,28 @@ describe('plugin-meetings', () => {
3443
3487
  });
3444
3488
  });
3445
3489
 
3490
+ it('LOCAL_MEDIA_STOPPED does not send metric because we already have', async () => {
3491
+ meeting.shareCAEventSentStatus = {
3492
+ transmitStart: false,
3493
+ transmitStop: true,
3494
+ receiveStart: false,
3495
+ receiveStop: false,
3496
+ };
3497
+ statsAnalyzerStub.emit(
3498
+ {file: 'test', function: 'test'},
3499
+ StatsAnalyzerEventNames.LOCAL_MEDIA_STOPPED,
3500
+ {mediaType: 'share'}
3501
+ );
3502
+
3503
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3504
+ name: 'client.media.tx.stop',
3505
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3506
+ options: {
3507
+ meetingId: meeting.id,
3508
+ },
3509
+ });
3510
+ });
3511
+
3446
3512
  it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and sends metrics', async () => {
3447
3513
  statsAnalyzerStub.emit(
3448
3514
  {file: 'test', function: 'test'},
@@ -3523,6 +3589,47 @@ describe('plugin-meetings', () => {
3523
3589
  });
3524
3590
  });
3525
3591
 
3592
+ it('REMOTE_MEDIA_STARTED triggers "meeting:media:remote:start" event and does not send metric because we already have', async () => {
3593
+ meeting.shareCAEventSentStatus = {
3594
+ transmitStart: false,
3595
+ transmitStop: false,
3596
+ receiveStart: true,
3597
+ receiveStop: false,
3598
+ };
3599
+ statsAnalyzerStub.emit(
3600
+ {file: 'test', function: 'test'},
3601
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STARTED,
3602
+ {mediaType: 'share'}
3603
+ );
3604
+
3605
+ assert.calledWith(
3606
+ TriggerProxy.trigger,
3607
+ sinon.match.instanceOf(Meeting),
3608
+ {
3609
+ file: 'meeting/index',
3610
+ function: 'addMedia',
3611
+ },
3612
+ EVENT_TRIGGERS.MEETING_MEDIA_REMOTE_STARTED,
3613
+ {
3614
+ mediaType: 'share',
3615
+ }
3616
+ );
3617
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3618
+ name: 'client.media.render.start',
3619
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3620
+ options: {
3621
+ meetingId: meeting.id,
3622
+ },
3623
+ });
3624
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3625
+ name: 'client.media.rx.start',
3626
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3627
+ options: {
3628
+ meetingId: meeting.id,
3629
+ },
3630
+ });
3631
+ });
3632
+
3526
3633
  it('REMOTE_MEDIA_STOPPED triggers the right metrics for share', async () => {
3527
3634
  statsAnalyzerStub.emit(
3528
3635
  {file: 'test', function: 'test'},
@@ -3547,21 +3654,49 @@ describe('plugin-meetings', () => {
3547
3654
  });
3548
3655
  });
3549
3656
 
3657
+ it('REMOTE_MEDIA_STOPPED does not send metric because we already have', async () => {
3658
+ meeting.shareCAEventSentStatus = {
3659
+ transmitStart: false,
3660
+ transmitStop: false,
3661
+ receiveStart: true,
3662
+ receiveStop: true,
3663
+ };
3664
+ statsAnalyzerStub.emit(
3665
+ {file: 'test', function: 'test'},
3666
+ StatsAnalyzerEventNames.REMOTE_MEDIA_STOPPED,
3667
+ {mediaType: 'share'}
3668
+ );
3669
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3670
+ name: 'client.media.render.stop',
3671
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3672
+ options: {
3673
+ meetingId: meeting.id,
3674
+ },
3675
+ });
3676
+ assert.neverCalledWith(webex.internal.newMetrics.submitClientEvent, {
3677
+ name: 'client.media.rx.stop',
3678
+ payload: {mediaType: 'share', shareInstanceId: meeting.remoteShareInstanceId},
3679
+ options: {
3680
+ meetingId: meeting.id,
3681
+ },
3682
+ });
3683
+ });
3684
+
3550
3685
  it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
3551
3686
  let fakeMembersCollection = {
3552
3687
  members: {
3553
- member1: { isInMeeting: true },
3554
- member2: { isInMeeting: true },
3555
- member3: { isInMeeting: false },
3688
+ member1: {isInMeeting: true},
3689
+ member2: {isInMeeting: true},
3690
+ member3: {isInMeeting: false},
3556
3691
  },
3557
3692
  };
3558
- sinon.stub(meeting, 'getMembers').returns({ membersCollection: fakeMembersCollection });
3559
- const fakeData = { intervalMetadata: {}, networkType: 'wifi' };
3693
+ sinon.stub(meeting, 'getMembers').returns({membersCollection: fakeMembersCollection});
3694
+ const fakeData = {intervalMetadata: {}, networkType: 'wifi'};
3560
3695
 
3561
3696
  statsAnalyzerStub.emit(
3562
- { file: 'test', function: 'test' },
3697
+ {file: 'test', function: 'test'},
3563
3698
  StatsAnalyzerEventNames.MEDIA_QUALITY,
3564
- { data: fakeData }
3699
+ {data: fakeData}
3565
3700
  );
3566
3701
 
3567
3702
  assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
@@ -3570,15 +3705,17 @@ describe('plugin-meetings', () => {
3570
3705
  meetingId: meeting.id,
3571
3706
  },
3572
3707
  payload: {
3573
- intervals: [sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2))],
3708
+ intervals: [
3709
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2)),
3710
+ ],
3574
3711
  },
3575
3712
  });
3576
3713
  fakeMembersCollection.members.member2.isInMeeting = false;
3577
3714
 
3578
3715
  statsAnalyzerStub.emit(
3579
- { file: 'test', function: 'test' },
3716
+ {file: 'test', function: 'test'},
3580
3717
  StatsAnalyzerEventNames.MEDIA_QUALITY,
3581
- { data: fakeData }
3718
+ {data: fakeData}
3582
3719
  );
3583
3720
 
3584
3721
  assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
@@ -3587,7 +3724,9 @@ describe('plugin-meetings', () => {
3587
3724
  meetingId: meeting.id,
3588
3725
  },
3589
3726
  payload: {
3590
- intervals: [sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1))],
3727
+ intervals: [
3728
+ sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1)),
3729
+ ],
3591
3730
  },
3592
3731
  });
3593
3732
  });
@@ -3624,7 +3763,7 @@ describe('plugin-meetings', () => {
3624
3763
 
3625
3764
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3626
3765
  turnServerInfo: {
3627
- url: FAKE_TURN_URL,
3766
+ urls: [FAKE_TURN_URL],
3628
3767
  username: FAKE_TURN_USER,
3629
3768
  password: FAKE_TURN_PASSWORD,
3630
3769
  },
@@ -3650,7 +3789,7 @@ describe('plugin-meetings', () => {
3650
3789
  meeting.id,
3651
3790
  sinon.match({
3652
3791
  turnServerInfo: {
3653
- url: FAKE_TURN_URL,
3792
+ urls: [FAKE_TURN_URL],
3654
3793
  username: FAKE_TURN_USER,
3655
3794
  password: FAKE_TURN_PASSWORD,
3656
3795
  },
@@ -3842,7 +3981,6 @@ describe('plugin-meetings', () => {
3842
3981
  });
3843
3982
 
3844
3983
  describe('when in a multistream meeting', () => {
3845
-
3846
3984
  beforeEach(() => {
3847
3985
  meeting.isMultistream = true;
3848
3986
  });
@@ -3853,7 +3991,7 @@ describe('plugin-meetings', () => {
3853
3991
  await brbResult;
3854
3992
  assert.exists(brbResult.then);
3855
3993
  assert.calledOnce(meeting.brbState.enable);
3856
- })
3994
+ });
3857
3995
 
3858
3996
  it('should disable #beRightBack and return a promise', async () => {
3859
3997
  const brbResult = meeting.beRightBack(false);
@@ -3861,7 +3999,7 @@ describe('plugin-meetings', () => {
3861
3999
  await brbResult;
3862
4000
  assert.exists(brbResult.then);
3863
4001
  assert.calledOnce(meeting.brbState.enable);
3864
- })
4002
+ });
3865
4003
 
3866
4004
  it('should throw an error and reject the promise if setBrb fails', async () => {
3867
4005
  const error = new Error('setBrb failed');
@@ -3872,9 +4010,29 @@ describe('plugin-meetings', () => {
3872
4010
  } catch (err) {
3873
4011
  assert.instanceOf(err, Error);
3874
4012
  assert.equal(err.message, 'setBrb failed');
3875
- assert.isRejected((Promise.reject()));
4013
+ assert.isRejected(Promise.reject());
3876
4014
  }
3877
- })
4015
+ });
4016
+
4017
+ it('updates remote mute state when brb is enabled', async () => {
4018
+ meeting.audio = {handleServerRemoteMuteUpdate: sinon.stub()};
4019
+
4020
+ await meeting.beRightBack(true);
4021
+
4022
+ sinon.assert.calledOnceWithExactly(
4023
+ meeting.audio.handleServerRemoteMuteUpdate,
4024
+ meeting,
4025
+ true
4026
+ );
4027
+ });
4028
+
4029
+ it('does not update remote mute state when brb is disabled', async () => {
4030
+ meeting.audio = {handleServerRemoteMuteUpdate: sinon.stub()};
4031
+
4032
+ await meeting.beRightBack(false);
4033
+
4034
+ assert.notCalled(meeting.audio.handleServerRemoteMuteUpdate);
4035
+ });
3878
4036
  });
3879
4037
  });
3880
4038
 
@@ -3928,7 +4086,10 @@ describe('plugin-meetings', () => {
3928
4086
  .resolves({id: 'fake clientMediaPreferences'});
3929
4087
  meeting.roap.doTurnDiscovery = sinon.stub().resolves({
3930
4088
  turnServerInfo: {
3931
- url: 'turns:turn-server-url:443?transport=tcp',
4089
+ urls: [
4090
+ 'turns:turn-server-url1:443?transport=tcp',
4091
+ 'turns:turn-server-url2:443?transport=tcp',
4092
+ ],
3932
4093
  username: 'turn user',
3933
4094
  password: 'turn password',
3934
4095
  },
@@ -3946,12 +4107,10 @@ describe('plugin-meetings', () => {
3946
4107
  expectedMediaConnectionConfig = {
3947
4108
  iceServers: [
3948
4109
  {
3949
- urls: 'turn:turn-server-url:5004?transport=tcp',
3950
- username: 'turn user',
3951
- credential: 'turn password',
3952
- },
3953
- {
3954
- urls: 'turns:turn-server-url:443?transport=tcp',
4110
+ urls: [
4111
+ 'turns:turn-server-url1:443?transport=tcp',
4112
+ 'turns:turn-server-url2:443?transport=tcp',
4113
+ ],
3955
4114
  username: 'turn user',
3956
4115
  credential: 'turn password',
3957
4116
  },
@@ -4006,7 +4165,7 @@ describe('plugin-meetings', () => {
4006
4165
  initiateOffer: sinon.stub().resolves({}),
4007
4166
  update: sinon.stub().resolves({}),
4008
4167
  on: sinon.stub(),
4009
- roapMessageReceived: sinon.stub()
4168
+ roapMessageReceived: sinon.stub(),
4010
4169
  };
4011
4170
 
4012
4171
  fakeMultistreamRoapMediaConnection = {
@@ -4033,9 +4192,11 @@ describe('plugin-meetings', () => {
4033
4192
  .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection')
4034
4193
  .returns(fakeMultistreamRoapMediaConnection);
4035
4194
 
4036
- locusMediaRequestStub = sinon
4037
- .stub(WebexPlugin.prototype, 'request')
4038
- .resolves({body: {locus: {fullState: {}}}});
4195
+ locusMediaRequestStub = sinon.stub(WebexPlugin.prototype, 'request').resolves({
4196
+ body: {locus: {fullState: {}}},
4197
+ upload: sinon.match.instanceOf(EventEmitter),
4198
+ download: sinon.match.instanceOf(EventEmitter),
4199
+ });
4039
4200
 
4040
4201
  // setup some things and mocks so that the call to join() works
4041
4202
  // (we need to call join() because it creates the LocusMediaRequest instance
@@ -4144,6 +4305,8 @@ describe('plugin-meetings', () => {
4144
4305
  id: 'fake clientMediaPreferences',
4145
4306
  },
4146
4307
  },
4308
+ upload: sinon.match.instanceOf(EventEmitter),
4309
+ download: sinon.match.instanceOf(EventEmitter),
4147
4310
  });
4148
4311
  };
4149
4312
 
@@ -4171,6 +4334,8 @@ describe('plugin-meetings', () => {
4171
4334
  },
4172
4335
  ],
4173
4336
  },
4337
+ upload: sinon.match.instanceOf(EventEmitter),
4338
+ download: sinon.match.instanceOf(EventEmitter),
4174
4339
  });
4175
4340
  };
4176
4341
 
@@ -4195,6 +4360,8 @@ describe('plugin-meetings', () => {
4195
4360
  respOnlySdp: true,
4196
4361
  usingResource: null,
4197
4362
  },
4363
+ upload: sinon.match.instanceOf(EventEmitter),
4364
+ download: sinon.match.instanceOf(EventEmitter),
4198
4365
  });
4199
4366
  };
4200
4367
 
@@ -5213,7 +5380,10 @@ describe('plugin-meetings', () => {
5213
5380
  // and check that when we fallback to transcoded we still do another TURN discovery
5214
5381
  await runCheck(
5215
5382
  {
5216
- url: 'turns:turn-server-url:443?transport=tcp',
5383
+ urls: [
5384
+ 'turns:turn-server-url1:443?transport=tcp',
5385
+ 'turns:turn-server-url2:443?transport=tcp',
5386
+ ],
5217
5387
  username: 'turn user',
5218
5388
  password: 'turn password',
5219
5389
  },
@@ -5227,7 +5397,10 @@ describe('plugin-meetings', () => {
5227
5397
  // but doing it just for completeness
5228
5398
  await runCheck(
5229
5399
  {
5230
- url: 'turns:turn-server-url:443?transport=tcp',
5400
+ urls: [
5401
+ 'turns:turn-server-url1:443?transport=tcp',
5402
+ 'turns:turn-server-url2:443?transport=tcp',
5403
+ ],
5231
5404
  username: 'turn user',
5232
5405
  password: 'turn password',
5233
5406
  },
@@ -6337,7 +6510,10 @@ describe('plugin-meetings', () => {
6337
6510
  .throws(new MeetingInfoV2JoinForbiddenError(403003, FAKE_MEETING_INFO)),
6338
6511
  };
6339
6512
 
6340
- await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinForbiddenError);
6513
+ await assert.isRejected(
6514
+ meeting.fetchMeetingInfo({sendCAevents: true}),
6515
+ JoinForbiddenError
6516
+ );
6341
6517
 
6342
6518
  assert.calledWith(
6343
6519
  meeting.attrs.meetingInfoProvider.fetchMeetingInfo,
@@ -6353,10 +6529,7 @@ describe('plugin-meetings', () => {
6353
6529
 
6354
6530
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6355
6531
  assert.equal(meeting.meetingInfoFailureCode, 403003);
6356
- assert.equal(
6357
- meeting.meetingInfoFailureReason,
6358
- MEETING_INFO_FAILURE_REASON.NOT_REACH_JBH
6359
- );
6532
+ assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.NOT_REACH_JBH);
6360
6533
  assert.equal(meeting.requiredCaptcha, null);
6361
6534
  });
6362
6535
 
@@ -6733,15 +6906,10 @@ describe('plugin-meetings', () => {
6733
6906
  meeting.attrs.meetingInfoProvider = {
6734
6907
  fetchMeetingInfo: sinon
6735
6908
  .stub()
6736
- .throws(
6737
- new MeetingInfoV2JoinWebinarError(403021, FAKE_MEETING_INFO, 'a message')
6738
- ),
6909
+ .throws(new MeetingInfoV2JoinWebinarError(403021, FAKE_MEETING_INFO, 'a message')),
6739
6910
  };
6740
6911
 
6741
- await assert.isRejected(
6742
- meeting.fetchMeetingInfo({sendCAevents: true}),
6743
- JoinWebinarError
6744
- );
6912
+ await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinWebinarError);
6745
6913
 
6746
6914
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6747
6915
  assert.equal(
@@ -6756,15 +6924,10 @@ describe('plugin-meetings', () => {
6756
6924
  meeting.attrs.meetingInfoProvider = {
6757
6925
  fetchMeetingInfo: sinon
6758
6926
  .stub()
6759
- .throws(
6760
- new MeetingInfoV2JoinWebinarError(403026, FAKE_MEETING_INFO, 'a message')
6761
- ),
6927
+ .throws(new MeetingInfoV2JoinWebinarError(403026, FAKE_MEETING_INFO, 'a message')),
6762
6928
  };
6763
6929
 
6764
- await assert.isRejected(
6765
- meeting.fetchMeetingInfo({sendCAevents: true}),
6766
- JoinWebinarError
6767
- );
6930
+ await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinWebinarError);
6768
6931
 
6769
6932
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6770
6933
  assert.equal(
@@ -6779,15 +6942,10 @@ describe('plugin-meetings', () => {
6779
6942
  meeting.attrs.meetingInfoProvider = {
6780
6943
  fetchMeetingInfo: sinon
6781
6944
  .stub()
6782
- .throws(
6783
- new MeetingInfoV2JoinWebinarError(403037, FAKE_MEETING_INFO, 'a message')
6784
- ),
6945
+ .throws(new MeetingInfoV2JoinWebinarError(403037, FAKE_MEETING_INFO, 'a message')),
6785
6946
  };
6786
6947
 
6787
- await assert.isRejected(
6788
- meeting.fetchMeetingInfo({sendCAevents: true}),
6789
- JoinWebinarError
6790
- );
6948
+ await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinWebinarError);
6791
6949
 
6792
6950
  assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6793
6951
  assert.equal(
@@ -7524,6 +7682,27 @@ describe('plugin-meetings', () => {
7524
7682
  });
7525
7683
  });
7526
7684
 
7685
+ describe('#setIsoLocalClientMeetingJoinTime', () => {
7686
+ it('should fallback to system clock ISO string when given an undefined value', () => {
7687
+ const currentSystemTime = new Date().toISOString();
7688
+ meeting.isoLocalClientMeetingJoinTime = undefined;
7689
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
7690
+ });
7691
+
7692
+ it('should fallback to system clock ISO string when given an invalid value', () => {
7693
+ const currentSystemTime = new Date().toISOString();
7694
+ meeting.isoLocalClientMeetingJoinTime = 'invalid-date';
7695
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, currentSystemTime);
7696
+ });
7697
+
7698
+ it('should set the isoLocalClientMeetingJoinTime correctly for a valid date string', () => {
7699
+ const validDateString = 'Tue, 01 Apr 2025 13:00:36 GMT';
7700
+ const expectedISOString = new Date(validDateString).toISOString();
7701
+ meeting.isoLocalClientMeetingJoinTime = validDateString;
7702
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, expectedISOString);
7703
+ });
7704
+ });
7705
+
7527
7706
  describe('#updateCallStateForMetrics', () => {
7528
7707
  it('should update the callState, overriding existing values', () => {
7529
7708
  assert.deepEqual(meeting.callStateForMetrics, {correlationId, sessionCorrelationId: ''});
@@ -7605,6 +7784,12 @@ describe('plugin-meetings', () => {
7605
7784
  meeting.audio = {handleLocalStreamChange: sinon.stub()};
7606
7785
  meeting.video = {handleLocalStreamChange: sinon.stub()};
7607
7786
  meeting.statsAnalyzer = {updateMediaStatus: sinon.stub()};
7787
+ meeting.shareCAEventSentStatus = {
7788
+ transmitStart: false,
7789
+ transmitStop: false,
7790
+ receiveStart: false,
7791
+ receiveStop: false,
7792
+ };
7608
7793
  fakeMultistreamRoapMediaConnection = {
7609
7794
  createSendSlot: () => {
7610
7795
  return {
@@ -7672,6 +7857,9 @@ describe('plugin-meetings', () => {
7672
7857
  });
7673
7858
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
7674
7859
 
7860
+ assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
7861
+ assert.equal(meeting.shareCAEventSentStatus.transmitStop, false);
7862
+
7675
7863
  assert.calledWith(meeting.statsAnalyzer.updateMediaStatus, {
7676
7864
  expected: {sendShare: true},
7677
7865
  });
@@ -7692,18 +7880,23 @@ describe('plugin-meetings', () => {
7692
7880
  assert.equal(meeting.mediaProperties.shareAudioStream, stream);
7693
7881
  assert.equal(meeting.mediaProperties.mediaDirection.sendShare, true);
7694
7882
 
7883
+ assert.equal(meeting.shareCAEventSentStatus.transmitStart, false);
7884
+ assert.equal(meeting.shareCAEventSentStatus.transmitStop, false);
7885
+
7695
7886
  assert.calledWith(meeting.statsAnalyzer.updateMediaStatus, {
7696
7887
  expected: {sendShare: true},
7697
7888
  });
7698
7889
  };
7699
7890
 
7700
7891
  it('requests screen share floor and publishes the screen share video stream', async () => {
7892
+ meeting.shareCAEventSentStatus.transmitStart = true;
7701
7893
  await meeting.publishStreams({screenShare: {video: videoShareStream}});
7702
7894
 
7703
7895
  checkScreenShareVideoPublished(videoShareStream);
7704
7896
  });
7705
7897
 
7706
7898
  it('requests screen share floor and publishes the screen share audio stream', async () => {
7899
+ meeting.shareCAEventSentStatus.transmitStart = true;
7707
7900
  await meeting.publishStreams({screenShare: {audio: audioShareStream}});
7708
7901
 
7709
7902
  checkScreenShareAudioPublished(audioShareStream);
@@ -8590,13 +8783,19 @@ describe('plugin-meetings', () => {
8590
8783
  const fakeErrorMessage = 'test error';
8591
8784
  const fakeRootCauseName = 'root cause name';
8592
8785
  const fakeErrorName = 'test error name';
8786
+ let clock;
8593
8787
 
8594
8788
  beforeEach(() => {
8789
+ clock = sinon.useFakeTimers();
8595
8790
  meeting.setupMediaConnectionListeners();
8596
8791
  webex.internal.newMetrics.submitClientEvent.resetHistory();
8597
8792
  Metrics.sendBehavioralMetric.resetHistory();
8598
8793
  });
8599
8794
 
8795
+ afterEach(() => {
8796
+ clock.restore();
8797
+ });
8798
+
8600
8799
  const checkMetricSent = (event, error) => {
8601
8800
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
8602
8801
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -8665,6 +8864,13 @@ describe('plugin-meetings', () => {
8665
8864
  });
8666
8865
 
8667
8866
  it('should send metrics for SdpAnswerHandlingError error', () => {
8867
+ meeting.sdpResponseTimer = '1234';
8868
+ meeting.deferSDPAnswer = {
8869
+ reject: sinon.stub(),
8870
+ };
8871
+
8872
+ const clearTimeoutSpy = sinon.spy(clock, 'clearTimeout');
8873
+
8668
8874
  const fakeError = new Errors.SdpAnswerHandlingError(fakeErrorMessage, {
8669
8875
  name: fakeErrorName,
8670
8876
  cause: {name: fakeRootCauseName},
@@ -8679,6 +8885,8 @@ describe('plugin-meetings', () => {
8679
8885
  fakeErrorMessage,
8680
8886
  fakeRootCauseName
8681
8887
  );
8888
+ assert.calledOnce(meeting.deferSDPAnswer.reject);
8889
+ assert.calledOnce(clearTimeoutSpy);
8682
8890
  });
8683
8891
 
8684
8892
  it('should send metrics for SdpError error', () => {
@@ -9223,22 +9431,22 @@ describe('plugin-meetings', () => {
9223
9431
  const assertBrb = (enabled) => {
9224
9432
  meeting.brbState = createBrbState(meeting, false);
9225
9433
  meeting.locusInfo.emit(
9226
- { function: 'test', file: 'test' },
9434
+ {function: 'test', file: 'test'},
9227
9435
  LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
9228
- { brb: { enabled } },
9229
- )
9436
+ {brb: {enabled}}
9437
+ );
9230
9438
  assert.calledWithExactly(
9231
9439
  TriggerProxy.trigger,
9232
9440
  meeting,
9233
9441
  {file: 'meeting/index', function: 'setUpLocusInfoSelfListener'},
9234
9442
  EVENT_TRIGGERS.MEETING_SELF_BRB_UPDATE,
9235
- { payload: { brb: { enabled } } },
9443
+ {payload: {brb: {enabled}}}
9236
9444
  );
9237
- }
9445
+ };
9238
9446
 
9239
9447
  assertBrb(true);
9240
9448
  assertBrb(false);
9241
- })
9449
+ });
9242
9450
 
9243
9451
  it('listens to the interpretation changed event', () => {
9244
9452
  meeting.simultaneousInterpretation.updateSelfInterpretation = sinon.stub();
@@ -9584,6 +9792,42 @@ describe('plugin-meetings', () => {
9584
9792
  );
9585
9793
  });
9586
9794
 
9795
+ it('listens to CONTROLS_ANNOTATION_CHANGED', async () => {
9796
+ const state = {example: 'value'};
9797
+
9798
+ await meeting.locusInfo.emitScoped(
9799
+ {function: 'test', file: 'test'},
9800
+ LOCUSINFO.EVENTS.CONTROLS_ANNOTATION_CHANGED,
9801
+ {state}
9802
+ );
9803
+
9804
+ assert.calledWith(
9805
+ TriggerProxy.trigger,
9806
+ meeting,
9807
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
9808
+ EVENT_TRIGGERS.MEETING_CONTROLS_ANNOTATION_UPDATED,
9809
+ {state}
9810
+ );
9811
+ });
9812
+
9813
+ it('listens to CONTROLS_REMOTE_DESKTOP_CONTROL_CHANGED', async () => {
9814
+ const state = {example: 'value'};
9815
+
9816
+ await meeting.locusInfo.emitScoped(
9817
+ {function: 'test', file: 'test'},
9818
+ LOCUSINFO.EVENTS.CONTROLS_REMOTE_DESKTOP_CONTROL_CHANGED,
9819
+ {state}
9820
+ );
9821
+
9822
+ assert.calledWith(
9823
+ TriggerProxy.trigger,
9824
+ meeting,
9825
+ {file: 'meeting/index', function: 'setupLocusControlsListener'},
9826
+ EVENT_TRIGGERS.MEETING_CONTROLS_REMOTE_DESKTOP_CONTROL_UPDATED,
9827
+ {state}
9828
+ );
9829
+ });
9830
+
9587
9831
  it('listens to the locus interpretation update event', () => {
9588
9832
  const interpretation = {
9589
9833
  siLanguages: [{languageCode: 20, languageName: 'en'}],
@@ -9922,6 +10166,22 @@ describe('plugin-meetings', () => {
9922
10166
  });
9923
10167
  });
9924
10168
 
10169
+ describe('#emailInput', () => {
10170
+ it('should set the email input', () => {
10171
+ assert.notOk(meeting.emailInput);
10172
+ meeting.emailInput = 'current';
10173
+ assert.equal(meeting.emailInput, 'current');
10174
+ });
10175
+ });
10176
+
10177
+ describe('#userNameInput', () => {
10178
+ it('should set the user name input', () => {
10179
+ assert.notOk(meeting.userNameInput);
10180
+ meeting.userNameInput = 'current';
10181
+ assert.equal(meeting.userNameInput, 'current');
10182
+ });
10183
+ });
10184
+
9925
10185
  describe('#setPermissionTokenPayload', () => {
9926
10186
  let now;
9927
10187
  let clock;
@@ -10463,6 +10723,7 @@ describe('plugin-meetings', () => {
10463
10723
  let canUserLowerSomeoneElsesHandSpy;
10464
10724
  let waitingForOthersToJoinSpy;
10465
10725
  let canSendReactionsSpy;
10726
+ let requiresPostMeetingDataConsentPromptSpy;
10466
10727
  let canUserRenameSelfAndObservedSpy;
10467
10728
  let canUserRenameOthersSpy;
10468
10729
  let canShareWhiteBoardSpy;
@@ -10490,6 +10751,10 @@ describe('plugin-meetings', () => {
10490
10751
  waitingForOthersToJoinSpy = sinon.spy(MeetingUtil, 'waitingForOthersToJoin');
10491
10752
  canSendReactionsSpy = sinon.spy(MeetingUtil, 'canSendReactions');
10492
10753
  canUserRenameSelfAndObservedSpy = sinon.spy(MeetingUtil, 'canUserRenameSelfAndObserved');
10754
+ requiresPostMeetingDataConsentPromptSpy = sinon.spy(
10755
+ MeetingUtil,
10756
+ 'requiresPostMeetingDataConsentPrompt'
10757
+ );
10493
10758
  canUserRenameOthersSpy = sinon.spy(MeetingUtil, 'canUserRenameOthers');
10494
10759
  canShareWhiteBoardSpy = sinon.spy(MeetingUtil, 'canShareWhiteBoard');
10495
10760
  });
@@ -10618,6 +10883,11 @@ describe('plugin-meetings', () => {
10618
10883
  requiredDisplayHints: [],
10619
10884
  requiredPolicies: [SELF_POLICY.SUPPORT_POLLING_AND_QA],
10620
10885
  },
10886
+ {
10887
+ actionName: 'canShareWhiteBoard',
10888
+ requiredDisplayHints: [DISPLAY_HINTS.SHARE_WHITEBOARD],
10889
+ requiredPolicies: [SELF_POLICY.SUPPORT_WHITEBOARD],
10890
+ },
10621
10891
  ],
10622
10892
  ({
10623
10893
  actionName,
@@ -11025,8 +11295,9 @@ describe('plugin-meetings', () => {
11025
11295
  assert.calledWith(waitingForOthersToJoinSpy, userDisplayHints);
11026
11296
  assert.calledWith(canSendReactionsSpy, null, userDisplayHints);
11027
11297
  assert.calledWith(canUserRenameSelfAndObservedSpy, userDisplayHints);
11298
+ assert.calledWith(requiresPostMeetingDataConsentPromptSpy, userDisplayHints);
11028
11299
  assert.calledWith(canUserRenameOthersSpy, userDisplayHints);
11029
- assert.calledWith(canShareWhiteBoardSpy, userDisplayHints);
11300
+ assert.calledWith(canShareWhiteBoardSpy, userDisplayHints, selfUserPolicies);
11030
11301
 
11031
11302
  assert.calledWith(ControlsOptionsUtil.hasHints, {
11032
11303
  requiredHints: [DISPLAY_HINTS.MUTE_ALL],
@@ -11120,6 +11391,22 @@ describe('plugin-meetings', () => {
11120
11391
  requiredPolicies: [SELF_POLICY.SUPPORT_VOIP],
11121
11392
  policies: selfUserPolicies,
11122
11393
  });
11394
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11395
+ requiredHints: [DISPLAY_HINTS.ENABLE_ANNOTATION_MEETING_OPTION],
11396
+ displayHints: userDisplayHints,
11397
+ });
11398
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11399
+ requiredHints: [DISPLAY_HINTS.DISABLE_ANNOTATION_MEETING_OPTION],
11400
+ displayHints: userDisplayHints,
11401
+ });
11402
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11403
+ requiredHints: [DISPLAY_HINTS.ENABLE_RDC_MEETING_OPTION],
11404
+ displayHints: userDisplayHints,
11405
+ });
11406
+ assert.calledWith(ControlsOptionsUtil.hasHints, {
11407
+ requiredHints: [DISPLAY_HINTS.DISABLE_RDC_MEETING_OPTION],
11408
+ displayHints: userDisplayHints,
11409
+ });
11123
11410
 
11124
11411
  assert.calledWith(
11125
11412
  TriggerProxy.trigger,
@@ -11326,18 +11613,21 @@ describe('plugin-meetings', () => {
11326
11613
  );
11327
11614
  });
11328
11615
 
11329
-
11330
11616
  it('connect ps data channel if ps started in webinar', async () => {
11331
11617
  meeting.joinedWith = {state: 'JOINED'};
11332
- meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url', practiceSessionDatachannelUrl: 'a ps datachannel url'}};
11618
+ meeting.locusInfo = {
11619
+ url: 'a url',
11620
+ info: {
11621
+ datachannelUrl: 'a datachannel url',
11622
+ practiceSessionDatachannelUrl: 'a ps datachannel url',
11623
+ },
11624
+ };
11333
11625
  meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(true);
11334
11626
  await meeting.updateLLMConnection();
11335
11627
 
11336
11628
  assert.notCalled(webex.internal.llm.disconnectLLM);
11337
11629
  assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a ps datachannel url');
11338
-
11339
11630
  });
11340
-
11341
11631
  });
11342
11632
 
11343
11633
  describe('#setLocus', () => {
@@ -11755,24 +12045,29 @@ describe('plugin-meetings', () => {
11755
12045
 
11756
12046
  activeSharingId.whiteboard = beneficiaryId;
11757
12047
 
11758
- eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
11759
- eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11760
- functionName: 'remoteShare',
11761
- eventPayload: {
11762
- memberId: null,
11763
- url,
11764
- shareInstanceId,
11765
- annotationInfo: undefined,
11766
- resourceType: undefined,
11767
- },
11768
- } : {
11769
- eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11770
- functionName: 'startWhiteboardShare',
11771
- eventPayload: {resourceUrl, memberId: beneficiaryId},
11772
- });
11773
-
11774
- shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
12048
+ eventTrigger.share.push(
12049
+ meeting.webinar.selfIsAttendee
12050
+ ? {
12051
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12052
+ functionName: 'remoteShare',
12053
+ eventPayload: {
12054
+ memberId: null,
12055
+ url,
12056
+ shareInstanceId,
12057
+ annotationInfo: undefined,
12058
+ resourceType: undefined,
12059
+ },
12060
+ }
12061
+ : {
12062
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
12063
+ functionName: 'startWhiteboardShare',
12064
+ eventPayload: {resourceUrl, memberId: beneficiaryId},
12065
+ }
12066
+ );
11775
12067
 
12068
+ shareStatus = meeting.webinar.selfIsAttendee
12069
+ ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
12070
+ : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11776
12071
  }
11777
12072
 
11778
12073
  if (eventTrigger.member) {
@@ -11804,24 +12099,29 @@ describe('plugin-meetings', () => {
11804
12099
  newPayload.current.content.disposition = FLOOR_ACTION.ACCEPTED;
11805
12100
  newPayload.current.content.beneficiaryId = otherBeneficiaryId;
11806
12101
 
11807
- eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
11808
- eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11809
- functionName: 'remoteShare',
11810
- eventPayload: {
11811
- memberId: null,
11812
- url,
11813
- shareInstanceId,
11814
- annotationInfo: undefined,
11815
- resourceType: undefined,
11816
- },
11817
- } : {
11818
- eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11819
- functionName: 'startWhiteboardShare',
11820
- eventPayload: {resourceUrl, memberId: beneficiaryId},
11821
- });
11822
-
11823
- shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
12102
+ eventTrigger.share.push(
12103
+ meeting.webinar.selfIsAttendee
12104
+ ? {
12105
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
12106
+ functionName: 'remoteShare',
12107
+ eventPayload: {
12108
+ memberId: null,
12109
+ url,
12110
+ shareInstanceId,
12111
+ annotationInfo: undefined,
12112
+ resourceType: undefined,
12113
+ },
12114
+ }
12115
+ : {
12116
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
12117
+ functionName: 'startWhiteboardShare',
12118
+ eventPayload: {resourceUrl, memberId: beneficiaryId},
12119
+ }
12120
+ );
11824
12121
 
12122
+ shareStatus = meeting.webinar.selfIsAttendee
12123
+ ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
12124
+ : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11825
12125
  } else {
11826
12126
  eventTrigger.share.push({
11827
12127
  eventName: EVENT_TRIGGERS.MEETING_STOPPED_SHARING_WHITEBOARD,
@@ -11951,24 +12251,26 @@ describe('plugin-meetings', () => {
11951
12251
  describe('Whiteboard Share - Webinar Attendee', () => {
11952
12252
  it('Scenario #1: Whiteboard sharing as a webinar attendee', () => {
11953
12253
  // Set the webinar attendee flag
11954
- meeting.webinar = { selfIsAttendee: true };
12254
+ meeting.webinar = {selfIsAttendee: true};
11955
12255
  meeting.locusInfo.info.isWebinar = true;
12256
+ meeting.shareCAEventSentStatus.receiveStart = true;
12257
+ meeting.shareCAEventSentStatus.receiveStop = true;
11956
12258
 
11957
12259
  // Step 1: Start sharing whiteboard A
11958
12260
  const data1 = generateData(
11959
- blankPayload, // Initial payload
11960
- true, // isGranting: Granting share
11961
- false, // isContent: Whiteboard (not content)
11962
- USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
12261
+ blankPayload, // Initial payload
12262
+ true, // isGranting: Granting share
12263
+ false, // isContent: Whiteboard (not content)
12264
+ USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
11963
12265
  RESOURCE_URLS.WHITEBOARD_A // Resource URL: Whiteboard A
11964
12266
  );
11965
12267
 
11966
12268
  // Step 2: Stop sharing whiteboard A
11967
12269
  const data2 = generateData(
11968
- data1.payload, // Updated payload from Step 1
11969
- false, // isGranting: Stopping share
11970
- false, // isContent: Whiteboard
11971
- USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
12270
+ data1.payload, // Updated payload from Step 1
12271
+ false, // isGranting: Stopping share
12272
+ false, // isContent: Whiteboard
12273
+ USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
11972
12274
  );
11973
12275
 
11974
12276
  // Validate the payload changes and status updates
@@ -11976,10 +12278,11 @@ describe('plugin-meetings', () => {
11976
12278
 
11977
12279
  // Specific assertions for webinar attendee status
11978
12280
  assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
12281
+ assert.equal(meeting.shareCAEventSentStatus.receiveStart, false);
12282
+ assert.equal(meeting.shareCAEventSentStatus.receiveStop, false);
11979
12283
  });
11980
12284
  });
11981
12285
 
11982
-
11983
12286
  describe('Whiteboard A --> Whiteboard B', () => {
11984
12287
  it('Scenario #1: you share both whiteboards', () => {
11985
12288
  const data1 = generateData(
@@ -12632,6 +12935,31 @@ describe('plugin-meetings', () => {
12632
12935
  });
12633
12936
  });
12634
12937
  });
12938
+
12939
+ describe('handleShareVideoStreamMuteStateChange', () => {
12940
+ it('should emit MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE event with correct fields', () => {
12941
+ meeting.isMultistream = true;
12942
+ meeting.statsAnalyzer = {shareVideoEncoderImplementation: 'OpenH264'};
12943
+ meeting.mediaProperties.shareVideoStream = {
12944
+ getSettings: sinon.stub().returns({displaySurface: 'monitor', frameRate: 30}),
12945
+ };
12946
+
12947
+ meeting.handleShareVideoStreamMuteStateChange(true);
12948
+
12949
+ assert.calledOnceWithExactly(
12950
+ Metrics.sendBehavioralMetric,
12951
+ BEHAVIORAL_METRICS.MEETING_SHARE_VIDEO_MUTE_STATE_CHANGE,
12952
+ {
12953
+ correlationId: meeting.correlationId,
12954
+ muted: true,
12955
+ encoderImplementation: 'OpenH264',
12956
+ displaySurface: 'monitor',
12957
+ isMultistream: true,
12958
+ frameRate: 30,
12959
+ }
12960
+ );
12961
+ });
12962
+ });
12635
12963
  });
12636
12964
 
12637
12965
  describe('#startKeepAlive', () => {
@@ -12799,6 +13127,38 @@ describe('plugin-meetings', () => {
12799
13127
  });
12800
13128
  });
12801
13129
 
13130
+ describe('#setPostMeetingDataConsent', () => {
13131
+ it('should have #setPostMeetingDataConsent', () => {
13132
+ assert.exists(meeting.setPostMeetingDataConsent);
13133
+ });
13134
+
13135
+ beforeEach(() => {
13136
+ meeting.meetingRequest.setPostMeetingDataConsent = sinon
13137
+ .stub()
13138
+ .returns(Promise.resolve());
13139
+ });
13140
+
13141
+ [true, false].forEach((accept) => {
13142
+ it(`should send consent with ${accept}`, async () => {
13143
+ const id = uuidv4();
13144
+ meeting.locusUrl = `https://locus-test.wbx2.com/locus/api/v1/loci/${accept}`;
13145
+ meeting.deviceUrl = `https://wdm-test.wbx2.com/wdm/api/v1/devices/${accept}`;
13146
+ meeting.members.selfId = id;
13147
+
13148
+ const consentPromise = meeting.setPostMeetingDataConsent(accept);
13149
+
13150
+ assert.exists(consentPromise.then);
13151
+ await consentPromise;
13152
+ assert.calledOnceWithExactly(meeting.meetingRequest.setPostMeetingDataConsent, {
13153
+ locusUrl: `https://locus-test.wbx2.com/locus/api/v1/loci/${accept}`,
13154
+ postMeetingDataConsent: accept,
13155
+ selfId: id,
13156
+ deviceUrl: `https://wdm-test.wbx2.com/wdm/api/v1/devices/${accept}`,
13157
+ });
13158
+ });
13159
+ });
13160
+ });
13161
+
12802
13162
  describe('#sendReaction', () => {
12803
13163
  it('should have #sendReaction', () => {
12804
13164
  assert.exists(meeting.sendReaction);
@@ -13290,7 +13650,7 @@ describe('plugin-meetings', () => {
13290
13650
  await meeting.roapMessageReceived(fakeMessage);
13291
13651
 
13292
13652
  assert.fail('Expected MultistreamNotSupportedError to be thrown');
13293
- } catch(e) {
13653
+ } catch (e) {
13294
13654
  assert.isTrue(e instanceof MultistreamNotSupportedError);
13295
13655
  }
13296
13656