@webex/plugin-meetings 3.7.0-next.3 → 3.7.0-next.31

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 (114) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/common/errors/{webinar-registration-error.js → join-webinar-error.js} +12 -12
  4. package/dist/common/errors/join-webinar-error.js.map +1 -0
  5. package/dist/config.js +1 -1
  6. package/dist/config.js.map +1 -1
  7. package/dist/constants.js +31 -6
  8. package/dist/constants.js.map +1 -1
  9. package/dist/index.js +8 -15
  10. package/dist/index.js.map +1 -1
  11. package/dist/interpretation/index.js +1 -1
  12. package/dist/interpretation/siLanguage.js +1 -1
  13. package/dist/locus-info/index.js +13 -2
  14. package/dist/locus-info/index.js.map +1 -1
  15. package/dist/locus-info/selfUtils.js +30 -17
  16. package/dist/locus-info/selfUtils.js.map +1 -1
  17. package/dist/meeting/in-meeting-actions.js +11 -1
  18. package/dist/meeting/in-meeting-actions.js.map +1 -1
  19. package/dist/meeting/index.js +810 -779
  20. package/dist/meeting/index.js.map +1 -1
  21. package/dist/meeting/request.js +30 -0
  22. package/dist/meeting/request.js.map +1 -1
  23. package/dist/meeting/request.type.js.map +1 -1
  24. package/dist/meeting/util.js +3 -8
  25. package/dist/meeting/util.js.map +1 -1
  26. package/dist/meeting-info/meeting-info-v2.js +29 -17
  27. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  28. package/dist/meetings/index.js +6 -3
  29. package/dist/meetings/index.js.map +1 -1
  30. package/dist/member/index.js +9 -0
  31. package/dist/member/index.js.map +1 -1
  32. package/dist/member/types.js.map +1 -1
  33. package/dist/member/util.js +39 -28
  34. package/dist/member/util.js.map +1 -1
  35. package/dist/members/util.js +4 -2
  36. package/dist/members/util.js.map +1 -1
  37. package/dist/metrics/constants.js +1 -1
  38. package/dist/metrics/constants.js.map +1 -1
  39. package/dist/multistream/remoteMedia.js +30 -15
  40. package/dist/multistream/remoteMedia.js.map +1 -1
  41. package/dist/multistream/sendSlotManager.js +24 -0
  42. package/dist/multistream/sendSlotManager.js.map +1 -1
  43. package/dist/reachability/clusterReachability.js +12 -11
  44. package/dist/reachability/clusterReachability.js.map +1 -1
  45. package/dist/recording-controller/enums.js +8 -4
  46. package/dist/recording-controller/enums.js.map +1 -1
  47. package/dist/recording-controller/index.js +18 -9
  48. package/dist/recording-controller/index.js.map +1 -1
  49. package/dist/recording-controller/util.js +13 -9
  50. package/dist/recording-controller/util.js.map +1 -1
  51. package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
  52. package/dist/types/constants.d.ts +23 -1
  53. package/dist/types/index.d.ts +3 -3
  54. package/dist/types/locus-info/index.d.ts +2 -1
  55. package/dist/types/meeting/in-meeting-actions.d.ts +10 -0
  56. package/dist/types/meeting/index.d.ts +9 -10
  57. package/dist/types/meeting/request.d.ts +12 -1
  58. package/dist/types/meeting/request.type.d.ts +6 -0
  59. package/dist/types/meeting/util.d.ts +1 -1
  60. package/dist/types/meeting-info/meeting-info-v2.d.ts +4 -4
  61. package/dist/types/meetings/index.d.ts +3 -0
  62. package/dist/types/member/index.d.ts +1 -0
  63. package/dist/types/member/types.d.ts +7 -0
  64. package/dist/types/members/util.d.ts +2 -0
  65. package/dist/types/metrics/constants.d.ts +1 -1
  66. package/dist/types/multistream/sendSlotManager.d.ts +8 -1
  67. package/dist/types/recording-controller/enums.d.ts +5 -2
  68. package/dist/types/recording-controller/index.d.ts +1 -0
  69. package/dist/types/recording-controller/util.d.ts +2 -1
  70. package/dist/webinar/index.js +390 -7
  71. package/dist/webinar/index.js.map +1 -1
  72. package/package.json +23 -22
  73. package/src/common/errors/join-webinar-error.ts +24 -0
  74. package/src/config.ts +1 -1
  75. package/src/constants.ts +28 -3
  76. package/src/index.ts +2 -3
  77. package/src/locus-info/index.ts +17 -2
  78. package/src/locus-info/selfUtils.ts +19 -6
  79. package/src/meeting/in-meeting-actions.ts +21 -0
  80. package/src/meeting/index.ts +147 -54
  81. package/src/meeting/request.ts +26 -1
  82. package/src/meeting/request.type.ts +7 -0
  83. package/src/meeting/util.ts +3 -9
  84. package/src/meeting-info/meeting-info-v2.ts +23 -11
  85. package/src/meetings/index.ts +8 -2
  86. package/src/member/index.ts +9 -0
  87. package/src/member/types.ts +8 -0
  88. package/src/member/util.ts +34 -24
  89. package/src/members/util.ts +1 -0
  90. package/src/metrics/constants.ts +1 -1
  91. package/src/multistream/remoteMedia.ts +28 -15
  92. package/src/multistream/sendSlotManager.ts +31 -0
  93. package/src/reachability/clusterReachability.ts +4 -1
  94. package/src/recording-controller/enums.ts +5 -2
  95. package/src/recording-controller/index.ts +17 -4
  96. package/src/recording-controller/util.ts +20 -5
  97. package/src/webinar/index.ts +235 -9
  98. package/test/unit/spec/locus-info/index.js +222 -0
  99. package/test/unit/spec/locus-info/selfConstant.js +7 -0
  100. package/test/unit/spec/locus-info/selfUtils.js +91 -1
  101. package/test/unit/spec/meeting/in-meeting-actions.ts +13 -1
  102. package/test/unit/spec/meeting/index.js +318 -81
  103. package/test/unit/spec/meeting/utils.js +11 -19
  104. package/test/unit/spec/meeting-info/meetinginfov2.js +9 -4
  105. package/test/unit/spec/meetings/index.js +9 -5
  106. package/test/unit/spec/member/util.js +52 -11
  107. package/test/unit/spec/members/utils.js +95 -0
  108. package/test/unit/spec/multistream/remoteMedia.ts +11 -7
  109. package/test/unit/spec/reachability/clusterReachability.ts +7 -0
  110. package/test/unit/spec/recording-controller/index.js +61 -5
  111. package/test/unit/spec/recording-controller/util.js +39 -3
  112. package/test/unit/spec/webinar/index.ts +504 -0
  113. package/dist/common/errors/webinar-registration-error.js.map +0 -1
  114. package/src/common/errors/webinar-registration-error.ts +0 -27
@@ -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
  };
@@ -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
- get(payload, 'oldRoles', []).includes(SELF_ROLES.ATTENDEE) &&
64
- get(payload, 'newRoles', []).includes(SELF_ROLES.PANELIST);
70
+ oldRoles.includes(SELF_ROLES.ATTENDEE) && newRoles.includes(SELF_ROLES.PANELIST);
65
71
  const isDemoted =
66
- get(payload, 'oldRoles', []).includes(SELF_ROLES.PANELIST) &&
67
- get(payload, 'newRoles', []).includes(SELF_ROLES.ATTENDEE);
68
- this.set('selfIsPanelist', get(payload, 'newRoles', []).includes(SELF_ROLES.PANELIST));
69
- this.set('selfIsAttendee', get(payload, 'newRoles', []).includes(SELF_ROLES.ATTENDEE));
70
- this.updateCanManageWebcast(get(payload, 'newRoles', []).includes(SELF_ROLES.MODERATOR));
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
 
@@ -793,6 +794,75 @@ describe('plugin-meetings', () => {
793
794
  });
794
795
 
795
796
  describe('#updateSelf', () => {
797
+ it('should trigger SELF_MEETING_BRB_CHANGED when brb state changed', () => {
798
+ locusInfo.self = undefined;
799
+
800
+ const assertBrb = (enabled) => {
801
+ const selfWithBrbChanged = cloneDeep(self);
802
+ selfWithBrbChanged.controls.brb = enabled;
803
+
804
+ locusInfo.emitScoped = sinon.stub();
805
+ locusInfo.updateSelf(selfWithBrbChanged, []);
806
+
807
+ assert.calledWith(
808
+ locusInfo.emitScoped,
809
+ {file: 'locus-info', function: 'updateSelf'},
810
+ LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
811
+ {brb: enabled}
812
+ );
813
+ };
814
+
815
+ assertBrb(true);
816
+ assertBrb(false);
817
+ });
818
+
819
+ it('should not trigger SELF_MEETING_BRB_CHANGED when brb state did not change', () => {
820
+ const assertBrbUnchanged = (value) => {
821
+ locusInfo.self = undefined;
822
+
823
+ const selfWithBrbChanged = cloneDeep(self);
824
+ selfWithBrbChanged.controls.brb = value;
825
+ locusInfo.self = selfWithBrbChanged;
826
+
827
+ locusInfo.emitScoped = sinon.stub();
828
+
829
+ const newSelf = cloneDeep(self);
830
+ newSelf.controls.brb = value;
831
+
832
+ locusInfo.updateSelf(newSelf, []);
833
+
834
+ assert.neverCalledWith(
835
+ locusInfo.emitScoped,
836
+ {file: 'locus-info', function: 'updateSelf'},
837
+ LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
838
+ {brb: value}
839
+ );
840
+ };
841
+
842
+ assertBrbUnchanged(true);
843
+ assertBrbUnchanged(false);
844
+ });
845
+
846
+ it('should not trigger SELF_MEETING_BRB_CHANGED when brb state is undefined', () => {
847
+ const selfWithBrbChanged = cloneDeep(self);
848
+ selfWithBrbChanged.controls.brb = false;
849
+ locusInfo.self = selfWithBrbChanged;
850
+
851
+ locusInfo.emitScoped = sinon.stub();
852
+
853
+ const newSelf = cloneDeep(self);
854
+ newSelf.controls.brb = undefined;
855
+
856
+ locusInfo.updateSelf(newSelf, []);
857
+
858
+ assert.neverCalledWith(
859
+ locusInfo.emitScoped,
860
+ {file: 'locus-info', function: 'updateSelf'},
861
+ LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
862
+ {brb: undefined}
863
+ );
864
+ });
865
+
796
866
  it('should trigger CONTROLS_MEETING_LAYOUT_UPDATED when the meeting layout controls change', () => {
797
867
  const layoutType = 'EXAMPLE TYPE';
798
868
 
@@ -1388,6 +1458,30 @@ describe('plugin-meetings', () => {
1388
1458
  }
1389
1459
  );
1390
1460
  });
1461
+
1462
+ it('should not trigger any events if controls is undefined', () => {
1463
+ locusInfo.self = self;
1464
+ locusInfo.emitScoped = sinon.stub();
1465
+ const newSelf = cloneDeep(self);
1466
+ newSelf.controls = undefined;
1467
+
1468
+ locusInfo.updateSelf(newSelf, []);
1469
+
1470
+ const eventsSet = new Set([
1471
+ LOCUSINFO.EVENTS.CONTROLS_MEETING_LAYOUT_UPDATED,
1472
+ LOCUSINFO.EVENTS.SELF_MEETING_BREAKOUTS_CHANGED,
1473
+ LOCUSINFO.EVENTS.SELF_MEETING_BRB_CHANGED,
1474
+ LOCUSINFO.EVENTS.SELF_MEETING_INTERPRETATION_CHANGED,
1475
+ LOCUSINFO.EVENTS.LOCAL_UNMUTE_REQUIRED,
1476
+ LOCUSINFO.EVENTS.SELF_REMOTE_MUTE_STATUS_UPDATED,
1477
+ ]);
1478
+
1479
+ // check all events that contain logic on controls existence
1480
+ locusInfo.emitScoped.getCalls().forEach((call) => {
1481
+ const eventName = call.args[1];
1482
+ assert.isFalse(eventsSet.has(eventName));
1483
+ });
1484
+ });
1391
1485
  });
1392
1486
 
1393
1487
  describe('#updateMeetingInfo', () => {
@@ -1637,6 +1731,134 @@ describe('plugin-meetings', () => {
1637
1731
  });
1638
1732
  });
1639
1733
 
1734
+ describe('#updateMediaShares', () => {
1735
+ let getMediaSharesSpy;
1736
+
1737
+ beforeEach(() => {
1738
+ // Spy on MediaSharesUtils.getMediaShares
1739
+ getMediaSharesSpy = sinon.stub(MediaSharesUtils, 'getMediaShares');
1740
+
1741
+ // Stub the emitScoped method to monitor its calls
1742
+ sinon.stub(locusInfo, 'emitScoped');
1743
+ });
1744
+
1745
+ afterEach(() => {
1746
+ getMediaSharesSpy.restore();
1747
+ locusInfo.emitScoped.restore();
1748
+ });
1749
+
1750
+ it('should update media shares and emit LOCUS_INFO_UPDATE_MEDIA_SHARES when mediaShares change', () => {
1751
+ const initialMediaShares = { audio: true, video: false };
1752
+ const newMediaShares = { audio: false, video: true };
1753
+
1754
+ locusInfo.mediaShares = initialMediaShares;
1755
+ locusInfo.parsedLocus = { mediaShares: null };
1756
+
1757
+ const parsedMediaShares = {
1758
+ current: newMediaShares,
1759
+ previous: initialMediaShares,
1760
+ };
1761
+
1762
+ // Stub MediaSharesUtils.getMediaShares to return the expected parsedMediaShares
1763
+ getMediaSharesSpy.returns(parsedMediaShares);
1764
+
1765
+ // Call the function
1766
+ locusInfo.updateMediaShares(newMediaShares);
1767
+
1768
+ // Assert that MediaSharesUtils.getMediaShares was called with correct arguments
1769
+ assert.calledWith(getMediaSharesSpy, initialMediaShares, newMediaShares);
1770
+
1771
+ // Assert that updateMeeting was called with the parsed current media shares
1772
+ assert.deepEqual(locusInfo.parsedLocus.mediaShares, newMediaShares);
1773
+ assert.deepEqual(locusInfo.mediaShares, newMediaShares);
1774
+
1775
+ // Assert that emitScoped was called with the correct event
1776
+ assert.calledWith(
1777
+ locusInfo.emitScoped,
1778
+ {
1779
+ file: 'locus-info',
1780
+ function: 'updateMediaShares',
1781
+ },
1782
+ EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
1783
+ {
1784
+ current: newMediaShares,
1785
+ previous: initialMediaShares,
1786
+ forceUpdate: false,
1787
+ }
1788
+ );
1789
+ });
1790
+
1791
+ it('should force update media shares and emit LOCUS_INFO_UPDATE_MEDIA_SHARES even if shares are the same', () => {
1792
+ const initialMediaShares = { audio: true, video: false };
1793
+ locusInfo.mediaShares = initialMediaShares;
1794
+ locusInfo.parsedLocus = { mediaShares: null };
1795
+
1796
+ const parsedMediaShares = {
1797
+ current: initialMediaShares,
1798
+ previous: initialMediaShares,
1799
+ };
1800
+
1801
+ getMediaSharesSpy.returns(parsedMediaShares);
1802
+
1803
+ // Call the function with forceUpdate = true
1804
+ locusInfo.updateMediaShares(initialMediaShares, true);
1805
+
1806
+ // Assert that MediaSharesUtils.getMediaShares was called
1807
+ assert.calledWith(getMediaSharesSpy, initialMediaShares, initialMediaShares);
1808
+
1809
+ // Assert that emitScoped was called with the correct event
1810
+ assert.calledWith(
1811
+ locusInfo.emitScoped,
1812
+ {
1813
+ file: 'locus-info',
1814
+ function: 'updateMediaShares',
1815
+ },
1816
+ EVENTS.LOCUS_INFO_UPDATE_MEDIA_SHARES,
1817
+ {
1818
+ current: initialMediaShares,
1819
+ previous: initialMediaShares,
1820
+ forceUpdate: true,
1821
+ }
1822
+ );
1823
+ });
1824
+
1825
+ it('should not emit LOCUS_INFO_UPDATE_MEDIA_SHARES if mediaShares do not change and forceUpdate is false', () => {
1826
+ const initialMediaShares = { audio: true, video: false };
1827
+ locusInfo.mediaShares = initialMediaShares;
1828
+
1829
+ // Call the function with the same mediaShares and forceUpdate = false
1830
+ locusInfo.updateMediaShares(initialMediaShares);
1831
+
1832
+ // Assert that MediaSharesUtils.getMediaShares was not called
1833
+ assert.notCalled(getMediaSharesSpy);
1834
+
1835
+ // Assert that emitScoped was not called
1836
+ assert.notCalled(locusInfo.emitScoped);
1837
+ });
1838
+
1839
+ it('should update internal state correctly when mediaShares are updated', () => {
1840
+ const initialMediaShares = { audio: true, video: false };
1841
+ const newMediaShares = { audio: false, video: true };
1842
+
1843
+ locusInfo.mediaShares = initialMediaShares;
1844
+ locusInfo.parsedLocus = { mediaShares: null };
1845
+
1846
+ const parsedMediaShares = {
1847
+ current: newMediaShares,
1848
+ previous: initialMediaShares,
1849
+ };
1850
+
1851
+ getMediaSharesSpy.returns(parsedMediaShares);
1852
+
1853
+ // Call the function
1854
+ locusInfo.updateMediaShares(newMediaShares);
1855
+
1856
+ // Assert that the internal state was updated correctly
1857
+ assert.deepEqual(locusInfo.parsedLocus.mediaShares, newMediaShares);
1858
+ assert.deepEqual(locusInfo.mediaShares, newMediaShares);
1859
+ });
1860
+ });
1861
+
1640
1862
  describe('#updateEmbeddedApps()', () => {
1641
1863
  const newEmbeddedApps = [
1642
1864
  {
@@ -304,6 +304,13 @@ export const selfWithInactivity = {
304
304
  localRecord: {
305
305
  recording: false,
306
306
  },
307
+ brb: {
308
+ enabled: true,
309
+ meta: {
310
+ lastModified: '2024-10-24T14:05:58.526Z',
311
+ modifiedBy: '70978427-8238-4ffc-9227-8baf4b80b831',
312
+ },
313
+ },
307
314
  layouts: [
308
315
  {
309
316
  type: 'activePresence',