@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.
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/index.js +5 -2
- package/dist/locus-info/index.js.map +1 -1
- package/dist/meeting/index.js +39 -20
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/util.js +3 -0
- package/dist/meeting/util.js.map +1 -1
- package/dist/types/constants.d.ts +2 -0
- package/dist/types/locus-info/index.d.ts +2 -1
- package/dist/types/meeting/util.d.ts +1 -0
- package/dist/webinar/index.js +36 -3
- package/dist/webinar/index.js.map +1 -1
- package/package.json +3 -3
- package/src/constants.ts +2 -0
- package/src/locus-info/index.ts +4 -2
- package/src/meeting/index.ts +41 -6
- package/src/meeting/util.ts +3 -0
- package/src/webinar/index.ts +36 -2
- package/test/unit/spec/locus-info/index.js +129 -0
- package/test/unit/spec/meeting/index.js +118 -4
- package/test/unit/spec/meeting/utils.js +11 -4
- package/test/unit/spec/webinar/index.ts +167 -26
package/src/locus-info/index.ts
CHANGED
|
@@ -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
|
}
|
package/src/meeting/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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',
|
package/src/meeting/util.ts
CHANGED
|
@@ -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],
|
package/src/webinar/index.ts
CHANGED
|
@@ -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(
|