@webex/plugin-meetings 3.7.0-next.20 → 3.7.0-next.22

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.
@@ -1283,12 +1283,13 @@ export default class LocusInfo extends EventsScope {
1283
1283
  /**
1284
1284
  * handles when the locus.mediaShares is updated
1285
1285
  * @param {Object} mediaShares the locus.mediaShares property
1286
+ * @param {boolean} forceUpdate force to update the mediaShares
1286
1287
  * @returns {undefined}
1287
1288
  * @memberof LocusInfo
1288
1289
  * emits internal event locus_info_update_media_shares
1289
1290
  */
1290
- updateMediaShares(mediaShares: object) {
1291
- if (mediaShares && !isEqual(this.mediaShares, mediaShares)) {
1291
+ updateMediaShares(mediaShares: object, forceUpdate = false) {
1292
+ if (mediaShares && (!isEqual(this.mediaShares, mediaShares) || forceUpdate)) {
1292
1293
  const parsedMediaShares = MediaSharesUtils.getMediaShares(this.mediaShares, mediaShares);
1293
1294
 
1294
1295
  this.updateMeeting(parsedMediaShares.current);
@@ -1303,6 +1304,7 @@ export default class LocusInfo extends EventsScope {
1303
1304
  {
1304
1305
  current: parsedMediaShares.current,
1305
1306
  previous: parsedMediaShares.previous,
1307
+ forceUpdate,
1306
1308
  }
1307
1309
  );
1308
1310
  }
@@ -160,6 +160,7 @@ import PermissionError from '../common/errors/permission';
160
160
  import {LocusMediaRequest} from './locusMediaRequest';
161
161
  import {ConnectionStateHandler, ConnectionStateEvent} from './connectionStateHandler';
162
162
  import JoinWebinarError from '../common/errors/join-webinar-error';
163
+ import Member from '../member';
163
164
 
164
165
  // default callback so we don't call an undefined function, but in practice it should never be used
165
166
  const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL';
@@ -849,7 +850,7 @@ export default class Meeting extends StatelessWebexPlugin {
849
850
  * @memberof Meeting
850
851
  */
851
852
  // @ts-ignore
852
- this.webinar = new Webinar({}, {parent: this.webex});
853
+ this.webinar = new Webinar({meetingId: this.id}, {parent: this.webex});
853
854
  /**
854
855
  * helper class for managing receive slots (for multistream media connections)
855
856
  */
@@ -2740,6 +2741,7 @@ export default class Meeting extends StatelessWebexPlugin {
2740
2741
  this.triggerAnnotationInfoEvent(contentShare, previousContentShare);
2741
2742
 
2742
2743
  if (
2744
+ !payload.forceUpdate &&
2743
2745
  contentShare.beneficiaryId === previousContentShare?.beneficiaryId &&
2744
2746
  contentShare.disposition === previousContentShare?.disposition &&
2745
2747
  contentShare.deviceUrlSharing === previousContentShare.deviceUrlSharing &&
@@ -2786,7 +2788,11 @@ export default class Meeting extends StatelessWebexPlugin {
2786
2788
  // It does not matter who requested to share the whiteboard, everyone gets the same view
2787
2789
  else if (whiteboardShare.disposition === FLOOR_ACTION.GRANTED) {
2788
2790
  // WHITEBOARD - sharing whiteboard
2789
- newShareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
2791
+ // Webinar attendee should receive whiteboard as remote share
2792
+ newShareStatus =
2793
+ this.locusInfo?.info?.isWebinar && this.webinar?.selfIsAttendee
2794
+ ? SHARE_STATUS.REMOTE_SHARE_ACTIVE
2795
+ : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
2790
2796
  }
2791
2797
  // or if content share is either released or null and whiteboard share is either released or null, no one is sharing
2792
2798
  else if (
@@ -2801,6 +2807,7 @@ export default class Meeting extends StatelessWebexPlugin {
2801
2807
  LoggerProxy.logger.info(
2802
2808
  `Meeting:index#setUpLocusInfoMediaInactiveListener --> this.shareStatus=${this.shareStatus} newShareStatus=${newShareStatus}`
2803
2809
  );
2810
+
2804
2811
  if (newShareStatus !== this.shareStatus) {
2805
2812
  const oldShareStatus = this.shareStatus;
2806
2813
 
@@ -3058,7 +3065,20 @@ export default class Meeting extends StatelessWebexPlugin {
3058
3065
  */
3059
3066
  private setUpLocusResourcesListener() {
3060
3067
  this.locusInfo.on(LOCUSINFO.EVENTS.LINKS_RESOURCES, (payload) => {
3061
- this.webinar.updateWebcastUrl(payload);
3068
+ if (payload) {
3069
+ this.webinar.updateWebcastUrl(payload);
3070
+ Trigger.trigger(
3071
+ this,
3072
+ {
3073
+ file: 'meeting/index',
3074
+ function: 'setUpLocusInfoMeetingInfoListener',
3075
+ },
3076
+ EVENT_TRIGGERS.MEETING_RESOURCE_LINKS_UPDATE,
3077
+ {
3078
+ payload,
3079
+ }
3080
+ );
3081
+ }
3062
3082
  });
3063
3083
  }
3064
3084
 
@@ -3377,6 +3397,7 @@ export default class Meeting extends StatelessWebexPlugin {
3377
3397
  payload.newRoles?.includes(SELF_ROLES.MODERATOR)
3378
3398
  );
3379
3399
  this.webinar.updateRoleChanged(payload);
3400
+
3380
3401
  Trigger.trigger(
3381
3402
  this,
3382
3403
  {
@@ -5580,17 +5601,23 @@ export default class Meeting extends StatelessWebexPlugin {
5580
5601
  */
5581
5602
  async updateLLMConnection() {
5582
5603
  // @ts-ignore - Fix type
5583
- const {url, info: {datachannelUrl} = {}} = this.locusInfo;
5604
+ const {url, info: {datachannelUrl, practiceSessionDatachannelUrl} = {}} = this.locusInfo;
5584
5605
 
5585
5606
  const isJoined = this.isJoined();
5586
5607
 
5608
+ // webinar panelist should use new data channel in practice session
5609
+ const dataChannelUrl =
5610
+ this.webinar.isJoinPracticeSessionDataChannel() && practiceSessionDatachannelUrl
5611
+ ? practiceSessionDatachannelUrl
5612
+ : datachannelUrl;
5613
+
5587
5614
  // @ts-ignore - Fix type
5588
5615
  if (this.webex.internal.llm.isConnected()) {
5589
5616
  if (
5590
5617
  // @ts-ignore - Fix type
5591
5618
  url === this.webex.internal.llm.getLocusUrl() &&
5592
5619
  // @ts-ignore - Fix type
5593
- datachannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5620
+ dataChannelUrl === this.webex.internal.llm.getDatachannelUrl() &&
5594
5621
  isJoined
5595
5622
  ) {
5596
5623
  return undefined;
@@ -5607,7 +5634,7 @@ export default class Meeting extends StatelessWebexPlugin {
5607
5634
 
5608
5635
  // @ts-ignore - Fix type
5609
5636
  return this.webex.internal.llm
5610
- .registerAndConnect(url, datachannelUrl)
5637
+ .registerAndConnect(url, dataChannelUrl)
5611
5638
  .then((registerAndConnectResult) => {
5612
5639
  // @ts-ignore - Fix type
5613
5640
  this.webex.internal.llm.off('event:relay.event', this.processRelayEvent);
@@ -6446,6 +6473,14 @@ export default class Meeting extends StatelessWebexPlugin {
6446
6473
  this.webex.meetings.geoHintInfo?.clientAddress ||
6447
6474
  options.data.intervalMetadata.peerReflexiveIP ||
6448
6475
  MQA_STATS.DEFAULT_IP;
6476
+
6477
+ const {members} = this.getMembers().membersCollection;
6478
+
6479
+ // Count members that are in the meeting
6480
+ options.data.intervalMetadata.meetingUserCount = Object.values(members).filter(
6481
+ (member: Member) => member.isInMeeting
6482
+ ).length;
6483
+
6449
6484
  // @ts-ignore
6450
6485
  this.webex.internal.newMetrics.submitMQE({
6451
6486
  name: 'client.mediaquality.event',
@@ -441,6 +441,9 @@ const MeetingUtil = {
441
441
  displayHints.includes(DISPLAY_HINTS.LEAVE_END_MEETING),
442
442
 
443
443
  canManageBreakout: (displayHints) => displayHints.includes(DISPLAY_HINTS.BREAKOUT_MANAGEMENT),
444
+
445
+ canStartBreakout: (displayHints) => !displayHints.includes(DISPLAY_HINTS.DISABLE_BREAKOUT_START),
446
+
444
447
  canBroadcastMessageToBreakout: (displayHints, policies = {}) =>
445
448
  displayHints.includes(DISPLAY_HINTS.BROADCAST_MESSAGE_TO_BREAKOUT) &&
446
449
  !!policies[SELF_POLICY.SUPPORT_BROADCAST_MESSAGE],
@@ -4,7 +4,7 @@
4
4
  import {WebexPlugin, config} from '@webex/webex-core';
5
5
  import uuid from 'uuid';
6
6
  import {get} from 'lodash';
7
- import {HEADERS, HTTP_VERBS, MEETINGS, SELF_ROLES} from '../constants';
7
+ import {_ID_, HEADERS, HTTP_VERBS, MEETINGS, SELF_ROLES, SHARE_STATUS} from '../constants';
8
8
 
9
9
  import WebinarCollection from './collection';
10
10
  import LoggerProxy from '../common/logs/logger-proxy';
@@ -25,6 +25,7 @@ const Webinar = WebexPlugin.extend({
25
25
  selfIsPanelist: 'boolean', // self is panelist
26
26
  selfIsAttendee: 'boolean', // self is attendee
27
27
  practiceSessionEnabled: 'boolean', // practice session enabled
28
+ meetingId: 'string',
28
29
  },
29
30
 
30
31
  /**
@@ -68,14 +69,47 @@ const Webinar = WebexPlugin.extend({
68
69
  const isPromoted =
69
70
  oldRoles.includes(SELF_ROLES.ATTENDEE) && newRoles.includes(SELF_ROLES.PANELIST);
70
71
  const isDemoted =
71
- oldRoles.includes(SELF_ROLES.PANELIST) && newRoles.includes(SELF_ROLES.ATTENDEE);
72
+ (oldRoles.includes(SELF_ROLES.PANELIST) && newRoles.includes(SELF_ROLES.ATTENDEE)) ||
73
+ (!oldRoles.includes(SELF_ROLES.ATTENDEE) && newRoles.includes(SELF_ROLES.ATTENDEE)); // for attendee just join meeting case
72
74
  this.set('selfIsPanelist', newRoles.includes(SELF_ROLES.PANELIST));
73
75
  this.set('selfIsAttendee', newRoles.includes(SELF_ROLES.ATTENDEE));
74
76
  this.updateCanManageWebcast(newRoles.includes(SELF_ROLES.MODERATOR));
77
+ this.updateStatusByRole({isPromoted, isDemoted});
75
78
 
76
79
  return {isPromoted, isDemoted};
77
80
  },
78
81
 
82
+ /**
83
+ * should join practice session data channel or not
84
+ * @param {Object} {isPromoted: boolean, isDemoted: boolean}} Role transition states
85
+ * @returns {void}
86
+ */
87
+ updateStatusByRole({isPromoted, isDemoted}) {
88
+ const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
89
+
90
+ if (
91
+ (isDemoted && meeting?.shareStatus === SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE) ||
92
+ isPromoted
93
+ ) {
94
+ // attendees in webinar should subscribe streaming for whiteboard sharing
95
+ // while panelist still need subscribe native mode so trigger force update here
96
+ meeting?.locusInfo?.updateMediaShares(meeting?.locusInfo?.mediaShares, true);
97
+ }
98
+
99
+ if (this.practiceSessionEnabled) {
100
+ // may need change data channel in practice session
101
+ meeting?.updateLLMConnection();
102
+ }
103
+ },
104
+
105
+ /**
106
+ * should join practice session data channel or not
107
+ * @returns {boolean}
108
+ */
109
+ isJoinPracticeSessionDataChannel() {
110
+ return this.selfIsPanelist && this.practiceSessionEnabled;
111
+ },
112
+
79
113
  /**
80
114
  * start or stop practice session for webinar
81
115
  * @param {boolean} enabled
@@ -9,6 +9,7 @@ import LocusInfo from '@webex/plugin-meetings/src/locus-info';
9
9
  import SelfUtils from '@webex/plugin-meetings/src/locus-info/selfUtils';
10
10
  import InfoUtils from '@webex/plugin-meetings/src/locus-info/infoUtils';
11
11
  import EmbeddedAppsUtils from '@webex/plugin-meetings/src/locus-info/embeddedAppsUtils';
12
+ import MediaSharesUtils from '@webex/plugin-meetings/src/locus-info//mediaSharesUtils';
12
13
  import LocusDeltaParser from '@webex/plugin-meetings/src/locus-info/parser';
13
14
  import Metrics from '@webex/plugin-meetings/src/metrics';
14
15
 
@@ -1637,6 +1638,134 @@ describe('plugin-meetings', () => {
1637
1638
  });
1638
1639
  });
1639
1640
 
1641
+ describe('#updateMediaShares', () => {
1642
+ let getMediaSharesSpy;
1643
+
1644
+ beforeEach(() => {
1645
+ // Spy on MediaSharesUtils.getMediaShares
1646
+ getMediaSharesSpy = sinon.stub(MediaSharesUtils, 'getMediaShares');
1647
+
1648
+ // Stub the emitScoped method to monitor its calls
1649
+ sinon.stub(locusInfo, 'emitScoped');
1650
+ });
1651
+
1652
+ afterEach(() => {
1653
+ getMediaSharesSpy.restore();
1654
+ locusInfo.emitScoped.restore();
1655
+ });
1656
+
1657
+ it('should update media shares and emit LOCUS_INFO_UPDATE_MEDIA_SHARES when mediaShares change', () => {
1658
+ const initialMediaShares = { audio: true, video: false };
1659
+ const newMediaShares = { audio: false, video: true };
1660
+
1661
+ locusInfo.mediaShares = initialMediaShares;
1662
+ locusInfo.parsedLocus = { mediaShares: null };
1663
+
1664
+ const parsedMediaShares = {
1665
+ current: newMediaShares,
1666
+ previous: initialMediaShares,
1667
+ };
1668
+
1669
+ // Stub MediaSharesUtils.getMediaShares to return the expected parsedMediaShares
1670
+ getMediaSharesSpy.returns(parsedMediaShares);
1671
+
1672
+ // Call the function
1673
+ locusInfo.updateMediaShares(newMediaShares);
1674
+
1675
+ // Assert that MediaSharesUtils.getMediaShares was called with correct arguments
1676
+ assert.calledWith(getMediaSharesSpy, initialMediaShares, newMediaShares);
1677
+
1678
+ // Assert that updateMeeting was called with the parsed current media shares
1679
+ assert.deepEqual(locusInfo.parsedLocus.mediaShares, newMediaShares);
1680
+ assert.deepEqual(locusInfo.mediaShares, newMediaShares);
1681
+
1682
+ // Assert that emitScoped was called with the correct event
1683
+ assert.calledWith(
1684
+ locusInfo.emitScoped,
1685
+ {
1686
+ file: 'locus-info',
1687
+ function: 'updateMediaShares',
1688
+ },
1689
+ EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
1690
+ {
1691
+ current: newMediaShares,
1692
+ previous: initialMediaShares,
1693
+ forceUpdate: false,
1694
+ }
1695
+ );
1696
+ });
1697
+
1698
+ it('should force update media shares and emit LOCUS_INFO_UPDATE_MEDIA_SHARES even if shares are the same', () => {
1699
+ const initialMediaShares = { audio: true, video: false };
1700
+ locusInfo.mediaShares = initialMediaShares;
1701
+ locusInfo.parsedLocus = { mediaShares: null };
1702
+
1703
+ const parsedMediaShares = {
1704
+ current: initialMediaShares,
1705
+ previous: initialMediaShares,
1706
+ };
1707
+
1708
+ getMediaSharesSpy.returns(parsedMediaShares);
1709
+
1710
+ // Call the function with forceUpdate = true
1711
+ locusInfo.updateMediaShares(initialMediaShares, true);
1712
+
1713
+ // Assert that MediaSharesUtils.getMediaShares was called
1714
+ assert.calledWith(getMediaSharesSpy, initialMediaShares, initialMediaShares);
1715
+
1716
+ // Assert that emitScoped was called with the correct event
1717
+ assert.calledWith(
1718
+ locusInfo.emitScoped,
1719
+ {
1720
+ file: 'locus-info',
1721
+ function: 'updateMediaShares',
1722
+ },
1723
+ EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
1724
+ {
1725
+ current: initialMediaShares,
1726
+ previous: initialMediaShares,
1727
+ forceUpdate: true,
1728
+ }
1729
+ );
1730
+ });
1731
+
1732
+ it('should not emit LOCUS_INFO_UPDATE_MEDIA_SHARES if mediaShares do not change and forceUpdate is false', () => {
1733
+ const initialMediaShares = { audio: true, video: false };
1734
+ locusInfo.mediaShares = initialMediaShares;
1735
+
1736
+ // Call the function with the same mediaShares and forceUpdate = false
1737
+ locusInfo.updateMediaShares(initialMediaShares);
1738
+
1739
+ // Assert that MediaSharesUtils.getMediaShares was not called
1740
+ assert.notCalled(getMediaSharesSpy);
1741
+
1742
+ // Assert that emitScoped was not called
1743
+ assert.notCalled(locusInfo.emitScoped);
1744
+ });
1745
+
1746
+ it('should update internal state correctly when mediaShares are updated', () => {
1747
+ const initialMediaShares = { audio: true, video: false };
1748
+ const newMediaShares = { audio: false, video: true };
1749
+
1750
+ locusInfo.mediaShares = initialMediaShares;
1751
+ locusInfo.parsedLocus = { mediaShares: null };
1752
+
1753
+ const parsedMediaShares = {
1754
+ current: newMediaShares,
1755
+ previous: initialMediaShares,
1756
+ };
1757
+
1758
+ getMediaSharesSpy.returns(parsedMediaShares);
1759
+
1760
+ // Call the function
1761
+ locusInfo.updateMediaShares(newMediaShares);
1762
+
1763
+ // Assert that the internal state was updated correctly
1764
+ assert.deepEqual(locusInfo.parsedLocus.mediaShares, newMediaShares);
1765
+ assert.deepEqual(locusInfo.mediaShares, newMediaShares);
1766
+ });
1767
+ });
1768
+
1640
1769
  describe('#updateEmbeddedApps()', () => {
1641
1770
  const newEmbeddedApps = [
1642
1771
  {
@@ -3479,6 +3479,51 @@ describe('plugin-meetings', () => {
3479
3479
  });
3480
3480
  });
3481
3481
 
3482
+ it('counts the number of members that are in the meeting for MEDIA_QUALITY event', async () => {
3483
+ let fakeMembersCollection = {
3484
+ members: {
3485
+ member1: { isInMeeting: true },
3486
+ member2: { isInMeeting: true },
3487
+ member3: { isInMeeting: false },
3488
+ },
3489
+ };
3490
+ sinon.stub(meeting, 'getMembers').returns({ membersCollection: fakeMembersCollection });
3491
+ const fakeData = { intervalMetadata: {}, networkType: 'wifi' };
3492
+
3493
+ statsAnalyzerStub.emit(
3494
+ { file: 'test', function: 'test' },
3495
+ StatsAnalyzerEventNames.MEDIA_QUALITY,
3496
+ { data: fakeData }
3497
+ );
3498
+
3499
+ assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
3500
+ name: 'client.mediaquality.event',
3501
+ options: {
3502
+ meetingId: meeting.id,
3503
+ },
3504
+ payload: {
3505
+ intervals: [sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 2))],
3506
+ },
3507
+ });
3508
+ fakeMembersCollection.members.member2.isInMeeting = false;
3509
+
3510
+ statsAnalyzerStub.emit(
3511
+ { file: 'test', function: 'test' },
3512
+ StatsAnalyzerEventNames.MEDIA_QUALITY,
3513
+ { data: fakeData }
3514
+ );
3515
+
3516
+ assert.calledWithMatch(webex.internal.newMetrics.submitMQE, {
3517
+ name: 'client.mediaquality.event',
3518
+ options: {
3519
+ meetingId: meeting.id,
3520
+ },
3521
+ payload: {
3522
+ intervals: [sinon.match.has('intervalMetadata', sinon.match.has('meetingUserCount', 1))],
3523
+ },
3524
+ });
3525
+ });
3526
+
3482
3527
  it('calls submitMQE correctly', async () => {
3483
3528
  const fakeData = {intervalMetadata: {bla: 'bla'}, networkType: 'wifi'};
3484
3529
 
@@ -10686,6 +10731,7 @@ describe('plugin-meetings', () => {
10686
10731
  meeting.webex.internal.llm.on = sinon.stub();
10687
10732
  meeting.webex.internal.llm.off = sinon.stub();
10688
10733
  meeting.processRelayEvent = sinon.stub();
10734
+ meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(false);
10689
10735
  });
10690
10736
 
10691
10737
  it('does not connect if the call is not joined yet', async () => {
@@ -10817,6 +10863,19 @@ describe('plugin-meetings', () => {
10817
10863
  meeting.processRelayEvent
10818
10864
  );
10819
10865
  });
10866
+
10867
+
10868
+ it('connect ps data channel if ps started in webinar', async () => {
10869
+ meeting.joinedWith = {state: 'JOINED'};
10870
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url', practiceSessionDatachannelUrl: 'a ps datachannel url'}};
10871
+ meeting.webinar.isJoinPracticeSessionDataChannel = sinon.stub().returns(true);
10872
+ await meeting.updateLLMConnection();
10873
+
10874
+ assert.notCalled(webex.internal.llm.disconnectLLM);
10875
+ assert.calledWith(webex.internal.llm.registerAndConnect, 'a url', 'a ps datachannel url');
10876
+
10877
+ });
10878
+
10820
10879
  });
10821
10880
 
10822
10881
  describe('#setLocus', () => {
@@ -11008,6 +11067,7 @@ describe('plugin-meetings', () => {
11008
11067
  beforeEach(() => {
11009
11068
  meeting.selfId = '9528d952-e4de-46cf-8157-fd4823b98377';
11010
11069
  meeting.deviceUrl = 'my-web-url';
11070
+ meeting.locusInfo.info = {isWebinar: false};
11011
11071
  });
11012
11072
 
11013
11073
  const USER_IDS = {
@@ -11233,13 +11293,24 @@ describe('plugin-meetings', () => {
11233
11293
 
11234
11294
  activeSharingId.whiteboard = beneficiaryId;
11235
11295
 
11236
- eventTrigger.share.push({
11296
+ eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
11297
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11298
+ functionName: 'remoteShare',
11299
+ eventPayload: {
11300
+ memberId: null,
11301
+ url,
11302
+ shareInstanceId,
11303
+ annotationInfo: undefined,
11304
+ resourceType: undefined,
11305
+ },
11306
+ } : {
11237
11307
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11238
11308
  functionName: 'startWhiteboardShare',
11239
11309
  eventPayload: {resourceUrl, memberId: beneficiaryId},
11240
11310
  });
11241
11311
 
11242
- shareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11312
+ shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11313
+
11243
11314
  }
11244
11315
 
11245
11316
  if (eventTrigger.member) {
@@ -11271,13 +11342,24 @@ describe('plugin-meetings', () => {
11271
11342
  newPayload.current.content.disposition = FLOOR_ACTION.ACCEPTED;
11272
11343
  newPayload.current.content.beneficiaryId = otherBeneficiaryId;
11273
11344
 
11274
- eventTrigger.share.push({
11345
+ eventTrigger.share.push(meeting.webinar.selfIsAttendee ? {
11346
+ eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_REMOTE,
11347
+ functionName: 'remoteShare',
11348
+ eventPayload: {
11349
+ memberId: null,
11350
+ url,
11351
+ shareInstanceId,
11352
+ annotationInfo: undefined,
11353
+ resourceType: undefined,
11354
+ },
11355
+ } : {
11275
11356
  eventName: EVENT_TRIGGERS.MEETING_STARTED_SHARING_WHITEBOARD,
11276
11357
  functionName: 'startWhiteboardShare',
11277
11358
  eventPayload: {resourceUrl, memberId: beneficiaryId},
11278
11359
  });
11279
11360
 
11280
- shareStatus = SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11361
+ shareStatus = meeting.webinar.selfIsAttendee ? SHARE_STATUS.REMOTE_SHARE_ACTIVE : SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE;
11362
+
11281
11363
  } else {
11282
11364
  eventTrigger.share.push({
11283
11365
  eventName: EVENT_TRIGGERS.MEETING_STOPPED_SHARING_WHITEBOARD,
@@ -11404,6 +11486,38 @@ describe('plugin-meetings', () => {
11404
11486
  assert.exists(meeting.setUpLocusMediaSharesListener);
11405
11487
  });
11406
11488
 
11489
+ describe('Whiteboard Share - Webinar Attendee', () => {
11490
+ it('Scenario #1: Whiteboard sharing as a webinar attendee', () => {
11491
+ // Set the webinar attendee flag
11492
+ meeting.webinar = { selfIsAttendee: true };
11493
+ meeting.locusInfo.info.isWebinar = true;
11494
+
11495
+ // Step 1: Start sharing whiteboard A
11496
+ const data1 = generateData(
11497
+ blankPayload, // Initial payload
11498
+ true, // isGranting: Granting share
11499
+ false, // isContent: Whiteboard (not content)
11500
+ USER_IDS.REMOTE_A, // Beneficiary ID: Remote user A
11501
+ RESOURCE_URLS.WHITEBOARD_A // Resource URL: Whiteboard A
11502
+ );
11503
+
11504
+ // Step 2: Stop sharing whiteboard A
11505
+ const data2 = generateData(
11506
+ data1.payload, // Updated payload from Step 1
11507
+ false, // isGranting: Stopping share
11508
+ false, // isContent: Whiteboard
11509
+ USER_IDS.REMOTE_A // Beneficiary ID: Remote user A
11510
+ );
11511
+
11512
+ // Validate the payload changes and status updates
11513
+ payloadTestHelper([data1]);
11514
+
11515
+ // Specific assertions for webinar attendee status
11516
+ assert.equal(meeting.shareStatus, SHARE_STATUS.REMOTE_SHARE_ACTIVE);
11517
+ });
11518
+ });
11519
+
11520
+
11407
11521
  describe('Whiteboard A --> Whiteboard B', () => {
11408
11522
  it('Scenario #1: you share both whiteboards', () => {
11409
11523
  const data1 = generateData(
@@ -26,7 +26,7 @@ describe('plugin-meetings', () => {
26
26
  webex.meetings.reachability = {
27
27
  getReachabilityReportToAttachToRoap: sinon.stub().resolves({}),
28
28
  getClientMediaPreferences: sinon.stub().resolves({}),
29
- };
29
+ };
30
30
 
31
31
  const logger = {
32
32
  info: sandbox.stub(),
@@ -409,17 +409,17 @@ describe('plugin-meetings', () => {
409
409
  const FAKE_CLIENT_MEDIA_PREFERENCES = {
410
410
  id: 'fake client media preferences',
411
411
  };
412
-
412
+
413
413
  webex.meetings.reachability.getReachabilityReportToAttachToRoap.resolves(FAKE_REACHABILITY_REPORT);
414
414
  webex.meetings.reachability.getClientMediaPreferences.resolves(FAKE_CLIENT_MEDIA_PREFERENCES);
415
-
415
+
416
416
  sinon
417
417
  .stub(webex.internal.device.ipNetworkDetector, 'supportsIpV4')
418
418
  .get(() => true);
419
419
  sinon
420
420
  .stub(webex.internal.device.ipNetworkDetector, 'supportsIpV6')
421
421
  .get(() => true);
422
-
422
+
423
423
  await MeetingUtil.joinMeeting(meeting, {
424
424
  reachability: 'reachability',
425
425
  roapMessage: 'roapMessage',
@@ -760,6 +760,13 @@ describe('plugin-meetings', () => {
760
760
  });
761
761
  });
762
762
 
763
+ describe('canStartBreakout', () => {
764
+ it('works as expected', () => {
765
+ assert.deepEqual(MeetingUtil.canStartBreakout(['DISABLE_BREAKOUT_START']), false);
766
+ assert.deepEqual(MeetingUtil.canStartBreakout([]), true);
767
+ });
768
+ });
769
+
763
770
  describe('canBroadcastMessageToBreakout', () => {
764
771
  it('works as expected', () => {
765
772
  assert.deepEqual(