@webex/plugin-meetings 3.7.0-next.5 → 3.7.0-next.50

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 (134) hide show
  1. package/dist/annotation/index.js +17 -0
  2. package/dist/annotation/index.js.map +1 -1
  3. package/dist/breakouts/breakout.js +1 -1
  4. package/dist/breakouts/index.js +1 -1
  5. package/dist/common/errors/{webinar-registration-error.js → join-webinar-error.js} +12 -12
  6. package/dist/common/errors/join-webinar-error.js.map +1 -0
  7. package/dist/common/errors/multistream-not-supported-error.js +53 -0
  8. package/dist/common/errors/multistream-not-supported-error.js.map +1 -0
  9. package/dist/config.js +1 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +40 -5
  12. package/dist/constants.js.map +1 -1
  13. package/dist/index.js +16 -11
  14. package/dist/index.js.map +1 -1
  15. package/dist/interpretation/index.js +1 -1
  16. package/dist/interpretation/siLanguage.js +1 -1
  17. package/dist/locus-info/index.js +14 -3
  18. package/dist/locus-info/index.js.map +1 -1
  19. package/dist/locus-info/selfUtils.js +30 -17
  20. package/dist/locus-info/selfUtils.js.map +1 -1
  21. package/dist/meeting/in-meeting-actions.js +4 -0
  22. package/dist/meeting/in-meeting-actions.js.map +1 -1
  23. package/dist/meeting/index.js +944 -832
  24. package/dist/meeting/index.js.map +1 -1
  25. package/dist/meeting/locusMediaRequest.js +9 -0
  26. package/dist/meeting/locusMediaRequest.js.map +1 -1
  27. package/dist/meeting/request.js +30 -0
  28. package/dist/meeting/request.js.map +1 -1
  29. package/dist/meeting/request.type.js.map +1 -1
  30. package/dist/meeting/util.js +16 -16
  31. package/dist/meeting/util.js.map +1 -1
  32. package/dist/meeting-info/meeting-info-v2.js +29 -17
  33. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  34. package/dist/meeting-info/utilv2.js +1 -1
  35. package/dist/meeting-info/utilv2.js.map +1 -1
  36. package/dist/meetings/index.js +106 -55
  37. package/dist/meetings/index.js.map +1 -1
  38. package/dist/meetings/meetings.types.js +2 -0
  39. package/dist/meetings/meetings.types.js.map +1 -1
  40. package/dist/meetings/util.js +1 -1
  41. package/dist/meetings/util.js.map +1 -1
  42. package/dist/member/index.js +9 -0
  43. package/dist/member/index.js.map +1 -1
  44. package/dist/member/types.js.map +1 -1
  45. package/dist/member/util.js +39 -28
  46. package/dist/member/util.js.map +1 -1
  47. package/dist/metrics/constants.js +1 -1
  48. package/dist/metrics/constants.js.map +1 -1
  49. package/dist/multistream/remoteMedia.js +30 -15
  50. package/dist/multistream/remoteMedia.js.map +1 -1
  51. package/dist/multistream/sendSlotManager.js +24 -0
  52. package/dist/multistream/sendSlotManager.js.map +1 -1
  53. package/dist/recording-controller/enums.js +8 -4
  54. package/dist/recording-controller/enums.js.map +1 -1
  55. package/dist/recording-controller/index.js +18 -9
  56. package/dist/recording-controller/index.js.map +1 -1
  57. package/dist/recording-controller/util.js +13 -9
  58. package/dist/recording-controller/util.js.map +1 -1
  59. package/dist/roap/index.js +10 -8
  60. package/dist/roap/index.js.map +1 -1
  61. package/dist/types/annotation/index.d.ts +5 -0
  62. package/dist/types/common/errors/{webinar-registration-error.d.ts → join-webinar-error.d.ts} +2 -2
  63. package/dist/types/common/errors/multistream-not-supported-error.d.ts +17 -0
  64. package/dist/types/constants.d.ts +34 -1
  65. package/dist/types/index.d.ts +3 -3
  66. package/dist/types/locus-info/index.d.ts +2 -1
  67. package/dist/types/meeting/in-meeting-actions.d.ts +4 -0
  68. package/dist/types/meeting/index.d.ts +19 -12
  69. package/dist/types/meeting/locusMediaRequest.d.ts +4 -0
  70. package/dist/types/meeting/request.d.ts +12 -1
  71. package/dist/types/meeting/request.type.d.ts +6 -0
  72. package/dist/types/meeting/util.d.ts +1 -1
  73. package/dist/types/meeting-info/meeting-info-v2.d.ts +4 -4
  74. package/dist/types/meetings/index.d.ts +19 -1
  75. package/dist/types/meetings/meetings.types.d.ts +8 -0
  76. package/dist/types/member/index.d.ts +1 -0
  77. package/dist/types/member/types.d.ts +7 -0
  78. package/dist/types/metrics/constants.d.ts +1 -1
  79. package/dist/types/multistream/sendSlotManager.d.ts +8 -1
  80. package/dist/types/recording-controller/enums.d.ts +5 -2
  81. package/dist/types/recording-controller/index.d.ts +1 -0
  82. package/dist/types/recording-controller/util.d.ts +2 -1
  83. package/dist/webinar/index.js +354 -3
  84. package/dist/webinar/index.js.map +1 -1
  85. package/package.json +23 -22
  86. package/src/annotation/index.ts +16 -0
  87. package/src/common/errors/join-webinar-error.ts +24 -0
  88. package/src/common/errors/multistream-not-supported-error.ts +30 -0
  89. package/src/config.ts +1 -1
  90. package/src/constants.ts +39 -3
  91. package/src/index.ts +5 -3
  92. package/src/locus-info/index.ts +20 -3
  93. package/src/locus-info/selfUtils.ts +19 -6
  94. package/src/meeting/in-meeting-actions.ts +8 -0
  95. package/src/meeting/index.ts +246 -80
  96. package/src/meeting/locusMediaRequest.ts +7 -0
  97. package/src/meeting/request.ts +26 -1
  98. package/src/meeting/request.type.ts +7 -0
  99. package/src/meeting/util.ts +8 -10
  100. package/src/meeting-info/meeting-info-v2.ts +23 -11
  101. package/src/meeting-info/utilv2.ts +3 -1
  102. package/src/meetings/index.ts +77 -20
  103. package/src/meetings/meetings.types.ts +10 -0
  104. package/src/meetings/util.ts +2 -1
  105. package/src/member/index.ts +9 -0
  106. package/src/member/types.ts +8 -0
  107. package/src/member/util.ts +34 -24
  108. package/src/metrics/constants.ts +1 -1
  109. package/src/multistream/remoteMedia.ts +28 -15
  110. package/src/multistream/sendSlotManager.ts +31 -0
  111. package/src/recording-controller/enums.ts +5 -2
  112. package/src/recording-controller/index.ts +17 -4
  113. package/src/recording-controller/util.ts +20 -5
  114. package/src/roap/index.ts +10 -8
  115. package/src/webinar/index.ts +197 -3
  116. package/test/unit/spec/annotation/index.ts +46 -1
  117. package/test/unit/spec/locus-info/index.js +292 -60
  118. package/test/unit/spec/locus-info/selfConstant.js +7 -0
  119. package/test/unit/spec/locus-info/selfUtils.js +91 -1
  120. package/test/unit/spec/meeting/in-meeting-actions.ts +4 -0
  121. package/test/unit/spec/meeting/index.js +689 -105
  122. package/test/unit/spec/meeting/utils.js +22 -19
  123. package/test/unit/spec/meeting-info/meetinginfov2.js +9 -4
  124. package/test/unit/spec/meeting-info/utilv2.js +17 -0
  125. package/test/unit/spec/meetings/index.js +153 -18
  126. package/test/unit/spec/meetings/utils.js +10 -0
  127. package/test/unit/spec/member/util.js +52 -11
  128. package/test/unit/spec/multistream/remoteMedia.ts +11 -7
  129. package/test/unit/spec/recording-controller/index.js +61 -5
  130. package/test/unit/spec/recording-controller/util.js +39 -3
  131. package/test/unit/spec/roap/index.ts +47 -0
  132. package/test/unit/spec/webinar/index.ts +457 -0
  133. package/dist/common/errors/webinar-registration-error.js.map +0 -1
  134. 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 = 60;
40
+ maxFs = MAX_FS_VALUES['90p'];
33
41
  break;
34
42
  case 'very small':
35
- maxFs = 240;
43
+ maxFs = MAX_FS_VALUES['180p'];
36
44
  break;
37
45
  case 'small':
38
- maxFs = 920;
46
+ maxFs = MAX_FS_VALUES['360p'];
39
47
  break;
40
48
  case 'medium':
41
- maxFs = 3600;
49
+ maxFs = MAX_FS_VALUES['720p'];
42
50
  break;
43
51
  case 'large':
44
- maxFs = 8192;
52
+ maxFs = MAX_FS_VALUES['1080p'];
45
53
  break;
46
54
  case 'best':
47
- maxFs = 8192; // for now 'best' is 1080p, so same as 'large'
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 = 3600;
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
- if (height < 135) {
121
- fs = 60;
122
- } else if (height < 270) {
123
- fs = 240;
124
- } else if (height < 540) {
125
- fs = 920;
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 = 3600;
140
+ fs = MAX_FS_VALUES['720p'];
128
141
  } else {
129
- fs = 8192;
142
+ fs = MAX_FS_VALUES['1080p'];
130
143
  }
131
144
 
132
145
  this.receiveSlot?.setMaxFs(fs);
@@ -4,6 +4,7 @@ import {
4
4
  LocalStream,
5
5
  MultistreamRoapMediaConnection,
6
6
  NamedMediaGroup,
7
+ StreamState,
7
8
  } from '@webex/internal-media-core';
8
9
 
9
10
  export default class SendSlotManager {
@@ -83,6 +84,36 @@ export default class SendSlotManager {
83
84
  );
84
85
  }
85
86
 
87
+ /**
88
+ * Sets the source state override for the given media type.
89
+ * @param {MediaType} mediaType - The type of media (must be MediaType.VideoMain to apply source state changes).
90
+ * @param {StreamState | null} state - The state to set or null to clear the override value.
91
+ * @returns {void}
92
+ */
93
+ public setSourceStateOverride(mediaType: MediaType, state: StreamState | null) {
94
+ if (mediaType !== MediaType.VideoMain) {
95
+ throw new Error(
96
+ `sendSlotManager cannot set source state override which media type is ${mediaType}`
97
+ );
98
+ }
99
+
100
+ const slot = this.slots.get(mediaType);
101
+
102
+ if (!slot) {
103
+ throw new Error(`Slot for ${mediaType} does not exist`);
104
+ }
105
+
106
+ if (state) {
107
+ slot.setSourceStateOverride(state);
108
+ } else {
109
+ slot.clearSourceStateOverride();
110
+ }
111
+
112
+ this.LoggerProxy.logger.info(
113
+ `SendSlotsManager->setSourceStateOverride#set source state override for ${mediaType} to ${state}`
114
+ );
115
+ }
116
+
86
117
  /**
87
118
  * This method publishes the given stream to the sendSlot for the given mediaType
88
119
  * @param {MediaType} mediaType MediaType of the sendSlot to which a stream needs to be published (AUDIO_MAIN/VIDEO_MAIN/AUDIO_SLIDES/VIDEO_SLIDES)
@@ -1,8 +1,11 @@
1
- enum RecordingAction {
1
+ export enum RecordingAction {
2
2
  Start = 'Start',
3
3
  Stop = 'Stop',
4
4
  Pause = 'Pause',
5
5
  Resume = 'Resume',
6
6
  }
7
7
 
8
- export default RecordingAction;
8
+ export enum RecordingType {
9
+ Premise = 'premise',
10
+ Cloud = 'cloud',
11
+ }
@@ -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/roap/index.ts CHANGED
@@ -231,14 +231,16 @@ export default class Roap extends StatelessWebexPlugin {
231
231
  headers,
232
232
  } = remoteSdp.roapMessage;
233
233
 
234
- roapAnswer = {
235
- seq: answerSeq,
236
- messageType,
237
- sdp: sdps[0],
238
- errorType,
239
- errorCause,
240
- headers,
241
- };
234
+ if (messageType === ROAP.ROAP_TYPES.ANSWER) {
235
+ roapAnswer = {
236
+ seq: answerSeq,
237
+ messageType,
238
+ sdp: sdps[0],
239
+ errorType,
240
+ errorCause,
241
+ headers,
242
+ };
243
+ }
242
244
  }
243
245
  }
244
246
 
@@ -1,9 +1,10 @@
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 {HTTP_VERBS, 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';
9
10
  import LoggerProxy from '../common/logs/logger-proxy';
@@ -24,6 +25,7 @@ const Webinar = WebexPlugin.extend({
24
25
  selfIsPanelist: 'boolean', // self is panelist
25
26
  selfIsAttendee: 'boolean', // self is attendee
26
27
  practiceSessionEnabled: 'boolean', // practice session enabled
28
+ meetingId: 'string',
27
29
  },
28
30
 
29
31
  /**
@@ -67,14 +69,47 @@ const Webinar = WebexPlugin.extend({
67
69
  const isPromoted =
68
70
  oldRoles.includes(SELF_ROLES.ATTENDEE) && newRoles.includes(SELF_ROLES.PANELIST);
69
71
  const isDemoted =
70
- 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
71
74
  this.set('selfIsPanelist', newRoles.includes(SELF_ROLES.PANELIST));
72
75
  this.set('selfIsAttendee', newRoles.includes(SELF_ROLES.ATTENDEE));
73
76
  this.updateCanManageWebcast(newRoles.includes(SELF_ROLES.MODERATOR));
77
+ this.updateStatusByRole({isPromoted, isDemoted});
74
78
 
75
79
  return {isPromoted, isDemoted};
76
80
  },
77
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
+
78
113
  /**
79
114
  * start or stop practice session for webinar
80
115
  * @param {boolean} enabled
@@ -103,6 +138,165 @@ const Webinar = WebexPlugin.extend({
103
138
  updatePracticeSessionStatus(payload) {
104
139
  this.set('practiceSessionEnabled', payload.enabled);
105
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
+ },
106
300
  });
107
301
 
108
302
  export default Webinar;
@@ -413,6 +413,51 @@ describe('live-annotation', () => {
413
413
  });
414
414
  });
415
415
  });
416
- });
417
416
 
417
+ describe('#deregisterEvents', () => {
418
+ let llmOn;
419
+ let llmOff;
420
+ let mercuryOn;
421
+ let mercuryOff;
422
+
423
+ beforeEach(() => {
424
+ llmOn = sinon.spy(webex.internal.llm, 'on');
425
+ llmOff = sinon.spy(webex.internal.llm, 'off');
426
+ mercuryOn = sinon.spy(webex.internal.mercury, 'on');
427
+ mercuryOff = sinon.spy(webex.internal.mercury, 'off');
428
+ });
429
+
430
+ it('cleans up events', () => {
431
+ annotationService.locusUrlUpdate(locusUrl);
432
+ assert.calledWith(
433
+ mercuryOn,
434
+ 'event:locus.approval_request',
435
+ annotationService.eventCommandProcessor,
436
+ annotationService
437
+ );
438
+ assert.calledWith(
439
+ llmOn,
440
+ 'event:relay.event',
441
+ annotationService.eventDataProcessor,
442
+ annotationService
443
+ );
444
+ assert.match(annotationService.hasSubscribedToEvents, true);
445
+
446
+ annotationService.deregisterEvents();
447
+ assert.calledWith(llmOff, 'event:relay.event', annotationService.eventDataProcessor);
448
+ assert.calledWith(
449
+ mercuryOff,
450
+ 'event:locus.approval_request',
451
+ annotationService.eventCommandProcessor
452
+ );
453
+ assert.match(annotationService.hasSubscribedToEvents, false);
454
+ });
455
+
456
+ it('does not call llm off if events have not been registered', () => {
457
+ annotationService.deregisterEvents();
458
+ assert.notCalled(llmOff);
459
+ assert.notCalled(mercuryOff);
460
+ });
461
+ });
462
+ });
418
463
  });