@webex/plugin-meetings 3.7.0-next.2 → 3.7.0-next.21
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/common/errors/{webinar-registration-error.js → join-webinar-error.js} +12 -12
- package/dist/common/errors/join-webinar-error.js.map +1 -0
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/constants.js +29 -6
- package/dist/constants.js.map +1 -1
- package/dist/index.js +8 -15
- package/dist/index.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/in-meeting-actions.js +11 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +115 -150
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/util.js +3 -8
- package/dist/meeting/util.js.map +1 -1
- package/dist/meeting-info/meeting-info-v2.js +29 -17
- package/dist/meeting-info/meeting-info-v2.js.map +1 -1
- package/dist/meetings/index.js +6 -3
- package/dist/meetings/index.js.map +1 -1
- package/dist/members/util.js +4 -2
- package/dist/members/util.js.map +1 -1
- package/dist/metrics/constants.js +3 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/remoteMedia.js +30 -15
- package/dist/multistream/remoteMedia.js.map +1 -1
- package/dist/reachability/clusterReachability.js +12 -11
- package/dist/reachability/clusterReachability.js.map +1 -1
- package/dist/recording-controller/enums.js +8 -4
- package/dist/recording-controller/enums.js.map +1 -1
- package/dist/recording-controller/index.js +18 -9
- package/dist/recording-controller/index.js.map +1 -1
- package/dist/recording-controller/util.js +13 -9
- package/dist/recording-controller/util.js.map +1 -1
- package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
- package/dist/types/constants.d.ts +21 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/locus-info/index.d.ts +2 -1
- package/dist/types/meeting/in-meeting-actions.d.ts +10 -0
- package/dist/types/meeting/index.d.ts +1 -10
- package/dist/types/meeting/util.d.ts +1 -1
- package/dist/types/meeting-info/meeting-info-v2.d.ts +4 -4
- package/dist/types/meetings/index.d.ts +3 -0
- package/dist/types/members/util.d.ts +2 -0
- package/dist/types/metrics/constants.d.ts +3 -1
- package/dist/types/recording-controller/enums.d.ts +5 -2
- package/dist/types/recording-controller/index.d.ts +1 -0
- package/dist/types/recording-controller/util.d.ts +2 -1
- package/dist/webinar/index.js +390 -7
- package/dist/webinar/index.js.map +1 -1
- package/package.json +22 -22
- package/src/common/errors/join-webinar-error.ts +24 -0
- package/src/config.ts +1 -1
- package/src/constants.ts +26 -3
- package/src/index.ts +2 -3
- package/src/locus-info/index.ts +4 -2
- package/src/meeting/in-meeting-actions.ts +21 -0
- package/src/meeting/index.ts +86 -54
- package/src/meeting/util.ts +3 -9
- package/src/meeting-info/meeting-info-v2.ts +23 -11
- package/src/meetings/index.ts +8 -2
- package/src/members/util.ts +1 -0
- package/src/metrics/constants.ts +3 -1
- package/src/multistream/remoteMedia.ts +28 -15
- package/src/reachability/clusterReachability.ts +4 -1
- package/src/recording-controller/enums.ts +5 -2
- package/src/recording-controller/index.ts +17 -4
- package/src/recording-controller/util.ts +20 -5
- package/src/webinar/index.ts +235 -9
- package/test/unit/spec/locus-info/index.js +129 -0
- package/test/unit/spec/meeting/in-meeting-actions.ts +13 -1
- package/test/unit/spec/meeting/index.js +179 -81
- package/test/unit/spec/meeting/utils.js +11 -19
- package/test/unit/spec/meeting-info/meetinginfov2.js +9 -4
- package/test/unit/spec/meetings/index.js +9 -5
- package/test/unit/spec/members/utils.js +95 -0
- package/test/unit/spec/multistream/remoteMedia.ts +11 -7
- package/test/unit/spec/reachability/clusterReachability.ts +7 -0
- package/test/unit/spec/recording-controller/index.js +61 -5
- package/test/unit/spec/recording-controller/util.js +39 -3
- package/test/unit/spec/webinar/index.ts +504 -0
- package/dist/common/errors/webinar-registration-error.js.map +0 -1
- package/src/common/errors/webinar-registration-error.ts +0 -27
|
@@ -19,6 +19,14 @@ export type RemoteVideoResolution =
|
|
|
19
19
|
| 'large' // 1080p or less
|
|
20
20
|
| 'best'; // highest possible resolution
|
|
21
21
|
|
|
22
|
+
const MAX_FS_VALUES = {
|
|
23
|
+
'90p': 60,
|
|
24
|
+
'180p': 240,
|
|
25
|
+
'360p': 920,
|
|
26
|
+
'720p': 3600,
|
|
27
|
+
'1080p': 8192,
|
|
28
|
+
};
|
|
29
|
+
|
|
22
30
|
/**
|
|
23
31
|
* Converts pane size into h264 maxFs
|
|
24
32
|
* @param {PaneSize} paneSize
|
|
@@ -29,28 +37,28 @@ export function getMaxFs(paneSize: RemoteVideoResolution): number {
|
|
|
29
37
|
|
|
30
38
|
switch (paneSize) {
|
|
31
39
|
case 'thumbnail':
|
|
32
|
-
maxFs =
|
|
40
|
+
maxFs = MAX_FS_VALUES['90p'];
|
|
33
41
|
break;
|
|
34
42
|
case 'very small':
|
|
35
|
-
maxFs =
|
|
43
|
+
maxFs = MAX_FS_VALUES['180p'];
|
|
36
44
|
break;
|
|
37
45
|
case 'small':
|
|
38
|
-
maxFs =
|
|
46
|
+
maxFs = MAX_FS_VALUES['360p'];
|
|
39
47
|
break;
|
|
40
48
|
case 'medium':
|
|
41
|
-
maxFs =
|
|
49
|
+
maxFs = MAX_FS_VALUES['720p'];
|
|
42
50
|
break;
|
|
43
51
|
case 'large':
|
|
44
|
-
maxFs =
|
|
52
|
+
maxFs = MAX_FS_VALUES['1080p'];
|
|
45
53
|
break;
|
|
46
54
|
case 'best':
|
|
47
|
-
maxFs =
|
|
55
|
+
maxFs = MAX_FS_VALUES['1080p']; // for now 'best' is 1080p, so same as 'large'
|
|
48
56
|
break;
|
|
49
57
|
default:
|
|
50
58
|
LoggerProxy.logger.warn(
|
|
51
59
|
`RemoteMedia#getMaxFs --> unsupported paneSize: ${paneSize}, using "medium" instead`
|
|
52
60
|
);
|
|
53
|
-
maxFs =
|
|
61
|
+
maxFs = MAX_FS_VALUES['720p'];
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
return maxFs;
|
|
@@ -117,16 +125,21 @@ export class RemoteMedia extends EventsScope {
|
|
|
117
125
|
return;
|
|
118
126
|
}
|
|
119
127
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
// we switch to the next resolution level when the height is 10% more than the current resolution height
|
|
129
|
+
// except for 1080p - we switch to it immediately when the height is more than 720p
|
|
130
|
+
const threshold = 1.1;
|
|
131
|
+
const getThresholdHeight = (h: number) => Math.round(h * threshold);
|
|
132
|
+
|
|
133
|
+
if (height < getThresholdHeight(90)) {
|
|
134
|
+
fs = MAX_FS_VALUES['90p'];
|
|
135
|
+
} else if (height < getThresholdHeight(180)) {
|
|
136
|
+
fs = MAX_FS_VALUES['180p'];
|
|
137
|
+
} else if (height < getThresholdHeight(360)) {
|
|
138
|
+
fs = MAX_FS_VALUES['360p'];
|
|
126
139
|
} else if (height <= 720) {
|
|
127
|
-
fs =
|
|
140
|
+
fs = MAX_FS_VALUES['720p'];
|
|
128
141
|
} else {
|
|
129
|
-
fs =
|
|
142
|
+
fs = MAX_FS_VALUES['1080p'];
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
this.receiveSlot?.setMaxFs(fs);
|
|
@@ -357,11 +357,14 @@ export class ClusterReachability extends EventsScope {
|
|
|
357
357
|
|
|
358
358
|
this.startTimestamp = performance.now();
|
|
359
359
|
|
|
360
|
+
// Set up the state change listeners before triggering the ICE gathering
|
|
361
|
+
const gatherIceCandidatePromise = this.gatherIceCandidates();
|
|
362
|
+
|
|
360
363
|
// not awaiting the next call on purpose, because we're not sending the offer anywhere and there won't be any answer
|
|
361
364
|
// we just need to make this call to trigger the ICE gathering process
|
|
362
365
|
this.pc.setLocalDescription(offer);
|
|
363
366
|
|
|
364
|
-
await
|
|
367
|
+
await gatherIceCandidatePromise;
|
|
365
368
|
} catch (error) {
|
|
366
369
|
LoggerProxy.logger.warn(`Reachability:ClusterReachability#start --> Error: `, error);
|
|
367
370
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import PermissionError from '../common/errors/permission';
|
|
2
|
+
import LoggerProxy from '../common/logs/logger-proxy';
|
|
2
3
|
import {CONTROLS, HTTP_VERBS, SELF_POLICY} from '../constants';
|
|
3
4
|
import MeetingRequest from '../meeting/request';
|
|
4
|
-
import RecordingAction from './enums';
|
|
5
|
+
import {RecordingAction, RecordingType} from './enums';
|
|
5
6
|
import Util from './util';
|
|
6
|
-
import LoggerProxy from '../common/logs/logger-proxy';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @description Recording manages the recording functionality of the meeting object, there should only be one instantation of recording per meeting
|
|
@@ -228,11 +228,12 @@ export default class RecordingController {
|
|
|
228
228
|
|
|
229
229
|
/**
|
|
230
230
|
* @param {RecordingAction} action
|
|
231
|
+
* @param {RecordingType} recordingType
|
|
231
232
|
* @private
|
|
232
233
|
* @memberof RecordingController
|
|
233
234
|
* @returns {Promise}
|
|
234
235
|
*/
|
|
235
|
-
private recordingService(action: RecordingAction): Promise<any> {
|
|
236
|
+
private recordingService(action: RecordingAction, recordingType: RecordingType): Promise<any> {
|
|
236
237
|
// @ts-ignore
|
|
237
238
|
return this.request.request({
|
|
238
239
|
body: {
|
|
@@ -242,6 +243,7 @@ export default class RecordingController {
|
|
|
242
243
|
recording: {
|
|
243
244
|
action: action.toLowerCase(),
|
|
244
245
|
},
|
|
246
|
+
recordingType,
|
|
245
247
|
},
|
|
246
248
|
uri: `${this.serviceUrl}/loci/${this.locusId}/recording`,
|
|
247
249
|
method: HTTP_VERBS.PUT,
|
|
@@ -276,14 +278,25 @@ export default class RecordingController {
|
|
|
276
278
|
* @returns {Promise}
|
|
277
279
|
*/
|
|
278
280
|
private recordingFacade(action: RecordingAction): Promise<any> {
|
|
281
|
+
const isPremiseRecordingEnabled = Util.isPremiseRecordingEnabled(
|
|
282
|
+
this.displayHints,
|
|
283
|
+
this.selfUserPolicies
|
|
284
|
+
);
|
|
279
285
|
LoggerProxy.logger.log(
|
|
280
286
|
`RecordingController:index#recordingFacade --> recording action [${action}]`
|
|
281
287
|
);
|
|
282
288
|
|
|
289
|
+
let recordingType: RecordingType;
|
|
290
|
+
if (isPremiseRecordingEnabled) {
|
|
291
|
+
recordingType = RecordingType.Premise;
|
|
292
|
+
} else {
|
|
293
|
+
recordingType = RecordingType.Cloud;
|
|
294
|
+
}
|
|
295
|
+
|
|
283
296
|
// assumes action is proper cased (i.e., Example)
|
|
284
297
|
if (Util?.[`canUser${action}`](this.displayHints, this.selfUserPolicies)) {
|
|
285
298
|
if (this.serviceUrl) {
|
|
286
|
-
return this.recordingService(action);
|
|
299
|
+
return this.recordingService(action, recordingType);
|
|
287
300
|
}
|
|
288
301
|
|
|
289
302
|
return this.recordingControls(action);
|
|
@@ -1,33 +1,47 @@
|
|
|
1
1
|
import {DISPLAY_HINTS, SELF_POLICY} from '../constants';
|
|
2
|
-
import RecordingAction from './enums';
|
|
2
|
+
import {RecordingAction} from './enums';
|
|
3
3
|
import MeetingUtil from '../meeting/util';
|
|
4
4
|
|
|
5
5
|
const canUserStart = (
|
|
6
6
|
displayHints: Array<string>,
|
|
7
7
|
userPolicies: Record<SELF_POLICY, boolean>
|
|
8
8
|
): boolean =>
|
|
9
|
-
displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_START)
|
|
9
|
+
(displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_START) ||
|
|
10
|
+
displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_START)) &&
|
|
10
11
|
MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies);
|
|
11
12
|
|
|
12
13
|
const canUserPause = (
|
|
13
14
|
displayHints: Array<string>,
|
|
14
15
|
userPolicies: Record<SELF_POLICY, boolean>
|
|
15
16
|
): boolean =>
|
|
16
|
-
displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_PAUSE)
|
|
17
|
+
(displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_PAUSE) ||
|
|
18
|
+
displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_PAUSE)) &&
|
|
17
19
|
MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies);
|
|
18
20
|
|
|
19
21
|
const canUserResume = (
|
|
20
22
|
displayHints: Array<string>,
|
|
21
23
|
userPolicies: Record<SELF_POLICY, boolean>
|
|
22
24
|
): boolean =>
|
|
23
|
-
displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_RESUME)
|
|
25
|
+
(displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_RESUME) ||
|
|
26
|
+
displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_RESUME)) &&
|
|
24
27
|
MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies);
|
|
25
28
|
|
|
26
29
|
const canUserStop = (
|
|
27
30
|
displayHints: Array<string>,
|
|
28
31
|
userPolicies: Record<SELF_POLICY, boolean>
|
|
29
32
|
): boolean =>
|
|
30
|
-
displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_STOP)
|
|
33
|
+
(displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_STOP) ||
|
|
34
|
+
displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_STOP)) &&
|
|
35
|
+
MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies);
|
|
36
|
+
|
|
37
|
+
const isPremiseRecordingEnabled = (
|
|
38
|
+
displayHints: Array<string>,
|
|
39
|
+
userPolicies: Record<SELF_POLICY, boolean>
|
|
40
|
+
): boolean =>
|
|
41
|
+
(displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_START) ||
|
|
42
|
+
displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_PAUSE) ||
|
|
43
|
+
displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_STOP) ||
|
|
44
|
+
displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_RESUME)) &&
|
|
31
45
|
MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies);
|
|
32
46
|
|
|
33
47
|
const extractLocusId = (url: string) => {
|
|
@@ -70,6 +84,7 @@ export default {
|
|
|
70
84
|
canUserPause,
|
|
71
85
|
canUserResume,
|
|
72
86
|
canUserStop,
|
|
87
|
+
isPremiseRecordingEnabled,
|
|
73
88
|
deriveRecordingStates,
|
|
74
89
|
extractLocusId,
|
|
75
90
|
};
|
package/src/webinar/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* Copyright (c) 2015-2023 Cisco Systems, Inc. See LICENSE file.
|
|
3
3
|
*/
|
|
4
|
-
import {WebexPlugin} from '@webex/webex-core';
|
|
4
|
+
import {WebexPlugin, config} from '@webex/webex-core';
|
|
5
|
+
import uuid from 'uuid';
|
|
5
6
|
import {get} from 'lodash';
|
|
6
|
-
import {MEETINGS, SELF_ROLES} from '../constants';
|
|
7
|
+
import {_ID_, HEADERS, HTTP_VERBS, MEETINGS, SELF_ROLES, SHARE_STATUS} from '../constants';
|
|
7
8
|
|
|
8
9
|
import WebinarCollection from './collection';
|
|
10
|
+
import LoggerProxy from '../common/logs/logger-proxy';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* @class Webinar
|
|
@@ -22,6 +24,8 @@ const Webinar = WebexPlugin.extend({
|
|
|
22
24
|
canManageWebcast: 'boolean', // appears the ability to manage webcast
|
|
23
25
|
selfIsPanelist: 'boolean', // self is panelist
|
|
24
26
|
selfIsAttendee: 'boolean', // self is attendee
|
|
27
|
+
practiceSessionEnabled: 'boolean', // practice session enabled
|
|
28
|
+
meetingId: 'string',
|
|
25
29
|
},
|
|
26
30
|
|
|
27
31
|
/**
|
|
@@ -59,18 +63,240 @@ const Webinar = WebexPlugin.extend({
|
|
|
59
63
|
* @returns {{isPromoted: boolean, isDemoted: boolean}} Role transition states
|
|
60
64
|
*/
|
|
61
65
|
updateRoleChanged(payload) {
|
|
66
|
+
const oldRoles = get(payload, 'oldRoles', []);
|
|
67
|
+
const newRoles = get(payload, 'newRoles', []);
|
|
68
|
+
|
|
62
69
|
const isPromoted =
|
|
63
|
-
|
|
64
|
-
get(payload, 'newRoles', []).includes(SELF_ROLES.PANELIST);
|
|
70
|
+
oldRoles.includes(SELF_ROLES.ATTENDEE) && newRoles.includes(SELF_ROLES.PANELIST);
|
|
65
71
|
const isDemoted =
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
this.set('selfIsPanelist',
|
|
69
|
-
this.set('selfIsAttendee',
|
|
70
|
-
this.updateCanManageWebcast(
|
|
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
|
|
74
|
+
this.set('selfIsPanelist', newRoles.includes(SELF_ROLES.PANELIST));
|
|
75
|
+
this.set('selfIsAttendee', newRoles.includes(SELF_ROLES.ATTENDEE));
|
|
76
|
+
this.updateCanManageWebcast(newRoles.includes(SELF_ROLES.MODERATOR));
|
|
77
|
+
this.updateStatusByRole({isPromoted, isDemoted});
|
|
71
78
|
|
|
72
79
|
return {isPromoted, isDemoted};
|
|
73
80
|
},
|
|
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
|
+
|
|
113
|
+
/**
|
|
114
|
+
* start or stop practice session for webinar
|
|
115
|
+
* @param {boolean} enabled
|
|
116
|
+
* @returns {Promise}
|
|
117
|
+
*/
|
|
118
|
+
setPracticeSessionState(enabled) {
|
|
119
|
+
return this.request({
|
|
120
|
+
method: HTTP_VERBS.PATCH,
|
|
121
|
+
uri: `${this.locusUrl}/controls`,
|
|
122
|
+
body: {
|
|
123
|
+
practiceSession: {
|
|
124
|
+
enabled,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
}).catch((error) => {
|
|
128
|
+
LoggerProxy.logger.error('Meeting:webinar#setPracticeSessionState failed', error);
|
|
129
|
+
throw error;
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* update practice session status
|
|
135
|
+
* @param {object} payload
|
|
136
|
+
* @returns {void}
|
|
137
|
+
*/
|
|
138
|
+
updatePracticeSessionStatus(payload) {
|
|
139
|
+
this.set('practiceSessionEnabled', payload.enabled);
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* start webcast mode for webinar
|
|
144
|
+
* @param {object} meeting
|
|
145
|
+
* @param {object} layout
|
|
146
|
+
* @returns {Promise}
|
|
147
|
+
*/
|
|
148
|
+
async startWebcast(meeting, layout) {
|
|
149
|
+
if (!meeting) {
|
|
150
|
+
LoggerProxy.logger.error(
|
|
151
|
+
`Meeting:webinar#startWebcast failed --> meeting parameter : ${meeting}`
|
|
152
|
+
);
|
|
153
|
+
throw new Error('Meeting parameter does not meet expectations');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return this.request({
|
|
157
|
+
method: HTTP_VERBS.PUT,
|
|
158
|
+
uri: `${this.webcastInstanceUrl}/streaming`,
|
|
159
|
+
headers: {
|
|
160
|
+
authorization: await this.webex.credentials.getUserToken(),
|
|
161
|
+
trackingId: `${config.trackingIdPrefix}_${uuid.v4().toString()}`,
|
|
162
|
+
[HEADERS.CONTENT_TYPE]: HEADERS.CONTENT_TYPE_VALUE.APPLICATION_JSON,
|
|
163
|
+
},
|
|
164
|
+
body: {
|
|
165
|
+
action: 'start',
|
|
166
|
+
meetingInfo: {
|
|
167
|
+
locusId: meeting.locusId,
|
|
168
|
+
correlationId: meeting.correlationId,
|
|
169
|
+
},
|
|
170
|
+
layout,
|
|
171
|
+
},
|
|
172
|
+
}).catch((error) => {
|
|
173
|
+
LoggerProxy.logger.error('Meeting:webinar#startWebcast failed', error);
|
|
174
|
+
throw error;
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* stop webcast mode for webinar
|
|
180
|
+
* @returns {Promise}
|
|
181
|
+
*/
|
|
182
|
+
async stopWebcast() {
|
|
183
|
+
return this.request({
|
|
184
|
+
method: HTTP_VERBS.PUT,
|
|
185
|
+
uri: `${this.webcastInstanceUrl}/streaming`,
|
|
186
|
+
headers: {
|
|
187
|
+
authorization: await this.webex.credentials.getUserToken(),
|
|
188
|
+
trackingId: `${config.trackingIdPrefix}_${uuid.v4().toString()}`,
|
|
189
|
+
[HEADERS.CONTENT_TYPE]: HEADERS.CONTENT_TYPE_VALUE.APPLICATION_JSON,
|
|
190
|
+
},
|
|
191
|
+
body: {
|
|
192
|
+
action: 'stop',
|
|
193
|
+
},
|
|
194
|
+
}).catch((error) => {
|
|
195
|
+
LoggerProxy.logger.error('Meeting:webinar#stopWebcast failed', error);
|
|
196
|
+
throw error;
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* query webcast layout for webinar
|
|
202
|
+
* @returns {Promise}
|
|
203
|
+
*/
|
|
204
|
+
async queryWebcastLayout() {
|
|
205
|
+
return this.request({
|
|
206
|
+
method: HTTP_VERBS.GET,
|
|
207
|
+
uri: `${this.webcastInstanceUrl}/layout`,
|
|
208
|
+
headers: {
|
|
209
|
+
authorization: await this.webex.credentials.getUserToken(),
|
|
210
|
+
trackingId: `${config.trackingIdPrefix}_${uuid.v4().toString()}`,
|
|
211
|
+
},
|
|
212
|
+
}).catch((error) => {
|
|
213
|
+
LoggerProxy.logger.error('Meeting:webinar#queryWebcastLayout failed', error);
|
|
214
|
+
throw error;
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* update webcast layout for webinar
|
|
220
|
+
* @param {object} layout
|
|
221
|
+
* @returns {Promise}
|
|
222
|
+
*/
|
|
223
|
+
async updateWebcastLayout(layout) {
|
|
224
|
+
return this.request({
|
|
225
|
+
method: HTTP_VERBS.PUT,
|
|
226
|
+
uri: `${this.webcastInstanceUrl}/layout`,
|
|
227
|
+
headers: {
|
|
228
|
+
authorization: await this.webex.credentials.getUserToken(),
|
|
229
|
+
trackingId: `${config.trackingIdPrefix}_${uuid.v4().toString()}`,
|
|
230
|
+
[HEADERS.CONTENT_TYPE]: HEADERS.CONTENT_TYPE_VALUE.APPLICATION_JSON,
|
|
231
|
+
},
|
|
232
|
+
body: {
|
|
233
|
+
videoLayout: layout.videoLayout,
|
|
234
|
+
contentLayout: layout.contentLayout,
|
|
235
|
+
syncStageLayout: layout.syncStageLayout,
|
|
236
|
+
syncStageInMeeting: layout.syncStageInMeeting,
|
|
237
|
+
},
|
|
238
|
+
}).catch((error) => {
|
|
239
|
+
LoggerProxy.logger.error('Meeting:webinar#updateWebcastLayout failed', error);
|
|
240
|
+
throw error;
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* view all webcast attendees
|
|
246
|
+
* @param {string} queryString
|
|
247
|
+
* @returns {Promise}
|
|
248
|
+
*/
|
|
249
|
+
async viewAllWebcastAttendees() {
|
|
250
|
+
return this.request({
|
|
251
|
+
method: HTTP_VERBS.GET,
|
|
252
|
+
uri: `${this.webcastInstanceUrl}/attendees`,
|
|
253
|
+
headers: {
|
|
254
|
+
authorization: await this.webex.credentials.getUserToken(),
|
|
255
|
+
trackingId: `${config.trackingIdPrefix}_${uuid.v4().toString()}`,
|
|
256
|
+
},
|
|
257
|
+
}).catch((error) => {
|
|
258
|
+
LoggerProxy.logger.error('Meeting:webinar#viewAllWebcastAttendees failed', error);
|
|
259
|
+
throw error;
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* search webcast attendees by query string
|
|
265
|
+
* @param {string} queryString
|
|
266
|
+
* @returns {Promise}
|
|
267
|
+
*/
|
|
268
|
+
async searchWebcastAttendees(queryString = '') {
|
|
269
|
+
return this.request({
|
|
270
|
+
method: HTTP_VERBS.GET,
|
|
271
|
+
uri: `${this.webcastInstanceUrl}/attendees?keyword=${encodeURIComponent(queryString)}`,
|
|
272
|
+
headers: {
|
|
273
|
+
authorization: await this.webex.credentials.getUserToken(),
|
|
274
|
+
trackingId: `${config.trackingIdPrefix}_${uuid.v4().toString()}`,
|
|
275
|
+
},
|
|
276
|
+
}).catch((error) => {
|
|
277
|
+
LoggerProxy.logger.error('Meeting:webinar#searchWebcastAttendees failed', error);
|
|
278
|
+
throw error;
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* expel webcast attendee by participantId
|
|
284
|
+
* @param {string} participantId
|
|
285
|
+
* @returns {Promise}
|
|
286
|
+
*/
|
|
287
|
+
async expelWebcastAttendee(participantId) {
|
|
288
|
+
return this.request({
|
|
289
|
+
method: HTTP_VERBS.DELETE,
|
|
290
|
+
uri: `${this.webcastInstanceUrl}/attendees/${participantId}`,
|
|
291
|
+
headers: {
|
|
292
|
+
authorization: await this.webex.credentials.getUserToken(),
|
|
293
|
+
trackingId: `${config.trackingIdPrefix}_${uuid.v4().toString()}`,
|
|
294
|
+
},
|
|
295
|
+
}).catch((error) => {
|
|
296
|
+
LoggerProxy.logger.error('Meeting:webinar#expelWebcastAttendee failed', error);
|
|
297
|
+
throw error;
|
|
298
|
+
});
|
|
299
|
+
},
|
|
74
300
|
});
|
|
75
301
|
|
|
76
302
|
export default Webinar;
|
|
@@ -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
|
{
|
|
@@ -33,6 +33,7 @@ describe('plugin-meetings', () => {
|
|
|
33
33
|
canStartManualCaption: null,
|
|
34
34
|
canStopManualCaption: null,
|
|
35
35
|
isManualCaptionActive: null,
|
|
36
|
+
isPremiseRecordingEnabled: null,
|
|
36
37
|
isSaveTranscriptsEnabled: null,
|
|
37
38
|
isWebexAssistantActive: null,
|
|
38
39
|
canViewCaptionPanel: null,
|
|
@@ -88,6 +89,11 @@ describe('plugin-meetings', () => {
|
|
|
88
89
|
canShowStageView: null,
|
|
89
90
|
canEnableStageView: null,
|
|
90
91
|
canDisableStageView: null,
|
|
92
|
+
isPracticeSessionOn : null,
|
|
93
|
+
isPracticeSessionOff : null,
|
|
94
|
+
canStartPracticeSession: null,
|
|
95
|
+
canStopPracticeSession: null,
|
|
96
|
+
|
|
91
97
|
...expected,
|
|
92
98
|
};
|
|
93
99
|
|
|
@@ -126,6 +132,7 @@ describe('plugin-meetings', () => {
|
|
|
126
132
|
'canStartManualCaption',
|
|
127
133
|
'canStopManualCaption',
|
|
128
134
|
'isManualCaptionActive',
|
|
135
|
+
'isPremiseRecordingEnabled',
|
|
129
136
|
'isSaveTranscriptsEnabled',
|
|
130
137
|
'isWebexAssistantActive',
|
|
131
138
|
'canViewCaptionPanel',
|
|
@@ -181,7 +188,12 @@ describe('plugin-meetings', () => {
|
|
|
181
188
|
'canShowStageView',
|
|
182
189
|
'canEnableStageView',
|
|
183
190
|
'canDisableStageView',
|
|
184
|
-
|
|
191
|
+
'isPracticeSessionOn',
|
|
192
|
+
'isPracticeSessionOff',
|
|
193
|
+
'canStartPracticeSession',
|
|
194
|
+
'canStopPracticeSession',
|
|
195
|
+
|
|
196
|
+
].forEach((key) => {
|
|
185
197
|
it(`get and set for ${key} work as expected`, () => {
|
|
186
198
|
const inMeetingActions = new InMeetingActions();
|
|
187
199
|
|