@webex/plugin-meetings 3.12.0-next.6 → 3.12.0-next.60

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 (158) hide show
  1. package/AGENTS.md +9 -0
  2. package/dist/aiEnableRequest/index.js +15 -2
  3. package/dist/aiEnableRequest/index.js.map +1 -1
  4. package/dist/breakouts/breakout.js +8 -3
  5. package/dist/breakouts/breakout.js.map +1 -1
  6. package/dist/breakouts/index.js +26 -2
  7. package/dist/breakouts/index.js.map +1 -1
  8. package/dist/config.js +2 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/constants.js +6 -3
  11. package/dist/constants.js.map +1 -1
  12. package/dist/controls-options-manager/constants.js +11 -1
  13. package/dist/controls-options-manager/constants.js.map +1 -1
  14. package/dist/controls-options-manager/index.js +38 -24
  15. package/dist/controls-options-manager/index.js.map +1 -1
  16. package/dist/controls-options-manager/util.js +91 -0
  17. package/dist/controls-options-manager/util.js.map +1 -1
  18. package/dist/hashTree/constants.js +10 -1
  19. package/dist/hashTree/constants.js.map +1 -1
  20. package/dist/hashTree/hashTreeParser.js +716 -370
  21. package/dist/hashTree/hashTreeParser.js.map +1 -1
  22. package/dist/hashTree/utils.js +22 -0
  23. package/dist/hashTree/utils.js.map +1 -1
  24. package/dist/index.js +7 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/interceptors/locusRetry.js +23 -8
  27. package/dist/interceptors/locusRetry.js.map +1 -1
  28. package/dist/interpretation/index.js +10 -1
  29. package/dist/interpretation/index.js.map +1 -1
  30. package/dist/interpretation/siLanguage.js +1 -1
  31. package/dist/locus-info/controlsUtils.js +4 -1
  32. package/dist/locus-info/controlsUtils.js.map +1 -1
  33. package/dist/locus-info/index.js +289 -87
  34. package/dist/locus-info/index.js.map +1 -1
  35. package/dist/locus-info/types.js +19 -0
  36. package/dist/locus-info/types.js.map +1 -1
  37. package/dist/media/index.js +3 -1
  38. package/dist/media/index.js.map +1 -1
  39. package/dist/media/properties.js +1 -0
  40. package/dist/media/properties.js.map +1 -1
  41. package/dist/meeting/in-meeting-actions.js +3 -1
  42. package/dist/meeting/in-meeting-actions.js.map +1 -1
  43. package/dist/meeting/index.js +907 -535
  44. package/dist/meeting/index.js.map +1 -1
  45. package/dist/meeting/util.js +19 -2
  46. package/dist/meeting/util.js.map +1 -1
  47. package/dist/meetings/index.js +231 -78
  48. package/dist/meetings/index.js.map +1 -1
  49. package/dist/meetings/meetings.types.js +6 -1
  50. package/dist/meetings/meetings.types.js.map +1 -1
  51. package/dist/meetings/request.js +39 -0
  52. package/dist/meetings/request.js.map +1 -1
  53. package/dist/meetings/util.js +79 -5
  54. package/dist/meetings/util.js.map +1 -1
  55. package/dist/member/index.js +10 -0
  56. package/dist/member/index.js.map +1 -1
  57. package/dist/member/types.js.map +1 -1
  58. package/dist/member/util.js +3 -0
  59. package/dist/member/util.js.map +1 -1
  60. package/dist/metrics/constants.js +4 -1
  61. package/dist/metrics/constants.js.map +1 -1
  62. package/dist/multistream/codec/constants.js +63 -0
  63. package/dist/multistream/codec/constants.js.map +1 -0
  64. package/dist/multistream/mediaRequestManager.js +62 -15
  65. package/dist/multistream/mediaRequestManager.js.map +1 -1
  66. package/dist/multistream/receiveSlot.js +9 -0
  67. package/dist/multistream/receiveSlot.js.map +1 -1
  68. package/dist/reactions/reactions.type.js.map +1 -1
  69. package/dist/recording-controller/index.js +1 -3
  70. package/dist/recording-controller/index.js.map +1 -1
  71. package/dist/types/config.d.ts +2 -0
  72. package/dist/types/constants.d.ts +2 -0
  73. package/dist/types/controls-options-manager/constants.d.ts +6 -1
  74. package/dist/types/controls-options-manager/index.d.ts +10 -0
  75. package/dist/types/hashTree/constants.d.ts +1 -0
  76. package/dist/types/hashTree/hashTreeParser.d.ts +92 -16
  77. package/dist/types/hashTree/utils.d.ts +11 -0
  78. package/dist/types/index.d.ts +2 -0
  79. package/dist/types/interceptors/locusRetry.d.ts +4 -4
  80. package/dist/types/locus-info/index.d.ts +46 -6
  81. package/dist/types/locus-info/types.d.ts +21 -1
  82. package/dist/types/media/properties.d.ts +1 -0
  83. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  84. package/dist/types/meeting/index.d.ts +87 -3
  85. package/dist/types/meeting/util.d.ts +8 -0
  86. package/dist/types/meetings/index.d.ts +30 -2
  87. package/dist/types/meetings/meetings.types.d.ts +15 -0
  88. package/dist/types/meetings/request.d.ts +14 -0
  89. package/dist/types/member/index.d.ts +1 -0
  90. package/dist/types/member/types.d.ts +1 -0
  91. package/dist/types/member/util.d.ts +1 -0
  92. package/dist/types/metrics/constants.d.ts +3 -0
  93. package/dist/types/multistream/codec/constants.d.ts +7 -0
  94. package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
  95. package/dist/types/reactions/reactions.type.d.ts +3 -0
  96. package/dist/webinar/index.js +361 -235
  97. package/dist/webinar/index.js.map +1 -1
  98. package/package.json +22 -22
  99. package/src/aiEnableRequest/index.ts +16 -0
  100. package/src/breakouts/breakout.ts +3 -1
  101. package/src/breakouts/index.ts +31 -0
  102. package/src/config.ts +2 -0
  103. package/src/constants.ts +5 -1
  104. package/src/controls-options-manager/constants.ts +14 -1
  105. package/src/controls-options-manager/index.ts +47 -24
  106. package/src/controls-options-manager/util.ts +81 -1
  107. package/src/hashTree/constants.ts +9 -0
  108. package/src/hashTree/hashTreeParser.ts +429 -183
  109. package/src/hashTree/utils.ts +17 -0
  110. package/src/index.ts +5 -0
  111. package/src/interceptors/locusRetry.ts +25 -4
  112. package/src/interpretation/index.ts +25 -8
  113. package/src/locus-info/controlsUtils.ts +3 -1
  114. package/src/locus-info/index.ts +291 -97
  115. package/src/locus-info/types.ts +25 -1
  116. package/src/media/index.ts +3 -0
  117. package/src/media/properties.ts +1 -0
  118. package/src/meeting/in-meeting-actions.ts +4 -0
  119. package/src/meeting/index.ts +388 -33
  120. package/src/meeting/util.ts +20 -2
  121. package/src/meetings/index.ts +134 -44
  122. package/src/meetings/meetings.types.ts +19 -0
  123. package/src/meetings/request.ts +43 -0
  124. package/src/meetings/util.ts +97 -1
  125. package/src/member/index.ts +10 -0
  126. package/src/member/types.ts +1 -0
  127. package/src/member/util.ts +3 -0
  128. package/src/metrics/constants.ts +3 -0
  129. package/src/multistream/codec/constants.ts +58 -0
  130. package/src/multistream/mediaRequestManager.ts +119 -28
  131. package/src/multistream/receiveSlot.ts +18 -0
  132. package/src/reactions/reactions.type.ts +3 -0
  133. package/src/recording-controller/index.ts +1 -2
  134. package/src/webinar/index.ts +162 -21
  135. package/test/unit/spec/aiEnableRequest/index.ts +86 -0
  136. package/test/unit/spec/breakouts/breakout.ts +9 -3
  137. package/test/unit/spec/breakouts/index.ts +49 -0
  138. package/test/unit/spec/controls-options-manager/index.js +140 -29
  139. package/test/unit/spec/controls-options-manager/util.js +165 -0
  140. package/test/unit/spec/hashTree/hashTreeParser.ts +1508 -149
  141. package/test/unit/spec/hashTree/utils.ts +88 -1
  142. package/test/unit/spec/interceptors/locusRetry.ts +205 -4
  143. package/test/unit/spec/interpretation/index.ts +26 -4
  144. package/test/unit/spec/locus-info/controlsUtils.js +172 -57
  145. package/test/unit/spec/locus-info/index.js +475 -81
  146. package/test/unit/spec/media/index.ts +31 -0
  147. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  148. package/test/unit/spec/meeting/index.js +1131 -49
  149. package/test/unit/spec/meeting/muteState.js +3 -0
  150. package/test/unit/spec/meeting/utils.js +33 -0
  151. package/test/unit/spec/meetings/index.js +360 -10
  152. package/test/unit/spec/meetings/request.js +141 -0
  153. package/test/unit/spec/meetings/utils.js +189 -0
  154. package/test/unit/spec/member/index.js +7 -0
  155. package/test/unit/spec/member/util.js +24 -0
  156. package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
  157. package/test/unit/spec/recording-controller/index.js +9 -8
  158. package/test/unit/spec/webinar/index.ts +141 -16
@@ -0,0 +1,58 @@
1
+ import {AV1EncodingParams, SupportedResolution} from '@webex/internal-media-core';
2
+
3
+ export const AV1_CODEC_PARAMETERS: Record<SupportedResolution, AV1EncodingParams> = {
4
+ '90p': {
5
+ levelIdx: 0,
6
+ tier: 0,
7
+ maxWidth: 160,
8
+ maxHeight: 90,
9
+ maxPicSize: 160 * 90,
10
+ maxDecodeRate: 5_529_600,
11
+ },
12
+ '180p': {
13
+ levelIdx: 0,
14
+ tier: 0,
15
+ maxWidth: 320,
16
+ maxHeight: 180,
17
+ maxPicSize: 320 * 180,
18
+ maxDecodeRate: 5_529_600,
19
+ },
20
+ '360p': {
21
+ levelIdx: 1,
22
+ tier: 0,
23
+ maxWidth: 640,
24
+ maxHeight: 360,
25
+ maxPicSize: 640 * 360,
26
+ maxDecodeRate: 10_454_400,
27
+ },
28
+ '540p': {
29
+ levelIdx: 4,
30
+ tier: 0,
31
+ maxWidth: 960,
32
+ maxHeight: 540,
33
+ maxPicSize: 960 * 540,
34
+ maxDecodeRate: 24_969_600,
35
+ },
36
+ '720p': {
37
+ levelIdx: 5,
38
+ tier: 0,
39
+ maxWidth: 1280,
40
+ maxHeight: 720,
41
+ maxPicSize: 1280 * 720,
42
+ maxDecodeRate: 39_938_400,
43
+ },
44
+ '1080p': {
45
+ levelIdx: 8,
46
+ tier: 0,
47
+ maxWidth: 1920,
48
+ maxHeight: 1080,
49
+ maxPicSize: 1920 * 1080,
50
+ maxDecodeRate: 77_856_768,
51
+ },
52
+ };
53
+
54
+ export const H264_CODEC_PARAMETERS = {
55
+ maxFs: 8192,
56
+ maxFps: 3000,
57
+ maxMbps: 245760,
58
+ };
@@ -9,6 +9,11 @@ import {
9
9
  getRecommendedMaxBitrateForFrameSize,
10
10
  RecommendedOpusBitrates,
11
11
  NamedMediaGroup,
12
+ AV1Codec,
13
+ SupportedResolution,
14
+ AV1EncodingParams,
15
+ MediaType,
16
+ MediaCodecMimeType,
12
17
  } from '@webex/internal-media-core';
13
18
  import {cloneDeepWith, debounce} from 'lodash';
14
19
 
@@ -16,6 +21,7 @@ import LoggerProxy from '../common/logs/logger-proxy';
16
21
 
17
22
  import {ReceiveSlot, ReceiveSlotEvents} from './receiveSlot';
18
23
  import {MAX_FS_VALUES} from './remoteMedia';
24
+ import {AV1_CODEC_PARAMETERS, H264_CODEC_PARAMETERS} from './codec/constants';
19
25
 
20
26
  export interface ActiveSpeakerPolicyInfo {
21
27
  policy: 'active-speaker';
@@ -54,34 +60,49 @@ export interface MediaRequest {
54
60
 
55
61
  export type MediaRequestId = string;
56
62
 
57
- const CODEC_DEFAULTS = {
58
- h264: {
59
- maxFs: 8192,
60
- maxFps: 3000,
61
- maxMbps: 245760,
62
- },
63
- };
64
-
65
63
  const DEBOUNCED_SOURCE_UPDATE_TIME = 1000;
66
64
 
65
+ const RESOLUTION_BUCKETS: Array<[SupportedResolution, number]> = [
66
+ ['90p', MAX_FS_VALUES['90p']],
67
+ ['180p', MAX_FS_VALUES['180p']],
68
+ ['360p', MAX_FS_VALUES['360p']],
69
+ ['540p', MAX_FS_VALUES['540p']],
70
+ ['720p', MAX_FS_VALUES['720p']],
71
+ ];
72
+
67
73
  type DegradationPreferences = {
68
74
  maxMacroblocksLimit: number;
69
75
  };
70
76
 
71
77
  type SendMediaRequestsCallback = (streamRequests: StreamRequest[]) => void;
78
+ type GetIngressPayloadTypeCallback = (
79
+ mediaType: MediaType,
80
+ codecMimeType: MediaCodecMimeType
81
+ ) => number | undefined;
72
82
  type Kind = 'audio' | 'video';
73
83
 
74
- type Options = {
84
+ type AudioMediaRequestManagerOptions = {
75
85
  degradationPreferences: DegradationPreferences;
76
- kind: Kind;
86
+ kind: 'audio';
77
87
  trimRequestsToNumOfSources: boolean; // if enabled, AS speaker requests will be trimmed based on the calls to setNumCurrentSources()
78
88
  };
79
89
 
90
+ type VideoMediaRequestManagerOptions = {
91
+ degradationPreferences: DegradationPreferences;
92
+ kind: 'video';
93
+ trimRequestsToNumOfSources: boolean;
94
+ enableAv1?: boolean;
95
+ };
96
+
97
+ type Options = AudioMediaRequestManagerOptions | VideoMediaRequestManagerOptions;
98
+
80
99
  type ClientRequestsMap = {[key: MediaRequestId]: MediaRequest};
81
100
 
82
101
  export class MediaRequestManager {
83
102
  private sendMediaRequestsCallback: SendMediaRequestsCallback;
84
103
 
104
+ private getIngressPayloadTypeCallback: GetIngressPayloadTypeCallback;
105
+
85
106
  private kind: Kind;
86
107
 
87
108
  private counter: number;
@@ -95,11 +116,17 @@ export class MediaRequestManager {
95
116
  private debouncedSourceUpdateListener: () => void;
96
117
 
97
118
  private trimRequestsToNumOfSources: boolean;
119
+ private enableAv1: boolean;
98
120
  private numTotalSources: number;
99
121
  private numLiveSources: number;
100
122
 
101
- constructor(sendMediaRequestsCallback: SendMediaRequestsCallback, options: Options) {
123
+ constructor(
124
+ sendMediaRequestsCallback: SendMediaRequestsCallback,
125
+ getIngressPayloadTypeCallback: GetIngressPayloadTypeCallback,
126
+ options: Options
127
+ ) {
102
128
  this.sendMediaRequestsCallback = sendMediaRequestsCallback;
129
+ this.getIngressPayloadTypeCallback = getIngressPayloadTypeCallback;
103
130
  this.counter = 0;
104
131
  this.numLiveSources = 0;
105
132
  this.numTotalSources = 0;
@@ -107,6 +134,7 @@ export class MediaRequestManager {
107
134
  this.degradationPreferences = options.degradationPreferences;
108
135
  this.kind = options.kind;
109
136
  this.trimRequestsToNumOfSources = options.trimRequestsToNumOfSources;
137
+ this.enableAv1 = options.kind === 'video' && !!options.enableAv1;
110
138
  this.sourceUpdateListener = this.commit.bind(this);
111
139
  this.debouncedSourceUpdateListener = debounce(
112
140
  this.sourceUpdateListener,
@@ -135,8 +163,8 @@ export class MediaRequestManager {
135
163
  Object.values(clientRequests).forEach((mr) => {
136
164
  if (mr.codecInfo) {
137
165
  mr.codecInfo.maxFs = Math.min(
138
- mr.preferredMaxFs || CODEC_DEFAULTS.h264.maxFs,
139
- mr.codecInfo.maxFs || CODEC_DEFAULTS.h264.maxFs,
166
+ mr.preferredMaxFs || H264_CODEC_PARAMETERS.maxFs,
167
+ mr.codecInfo.maxFs || H264_CODEC_PARAMETERS.maxFs,
140
168
  maxFsLimits[i]
141
169
  );
142
170
  // we only consider sources with "live" state
@@ -176,7 +204,7 @@ export class MediaRequestManager {
176
204
  }
177
205
 
178
206
  return getRecommendedMaxBitrateForFrameSize(
179
- mediaRequest.codecInfo.maxFs || CODEC_DEFAULTS.h264.maxFs
207
+ mediaRequest.codecInfo.maxFs || H264_CODEC_PARAMETERS.maxFs
180
208
  );
181
209
  }
182
210
 
@@ -192,12 +220,80 @@ export class MediaRequestManager {
192
220
  // eslint-disable-next-line class-methods-use-this
193
221
  private getH264MaxMbps(mediaRequest: MediaRequest): number {
194
222
  // fallback for maxFps (not needed for maxFs, since there is a fallback already in getDegradedClientRequests)
195
- const maxFps = mediaRequest.codecInfo.maxFps || CODEC_DEFAULTS.h264.maxFps;
223
+ const maxFps = mediaRequest.codecInfo.maxFps || H264_CODEC_PARAMETERS.maxFps;
196
224
 
197
225
  // divided by 100 since maxFps is 3000 (for 30 frames per seconds)
198
226
  return (mediaRequest.codecInfo.maxFs * maxFps) / 100;
199
227
  }
200
228
 
229
+ /**
230
+ * Returns the AV1 encoding parameters for a media request
231
+ * @param mediaRequest - The media request to get the AV1 encoding parameters for
232
+ * @returns {AV1EncodingParams} The AV1 encoding parameters
233
+ */
234
+ // eslint-disable-next-line class-methods-use-this
235
+ private getAv1EncodingParams(mediaRequest: MediaRequest): AV1EncodingParams {
236
+ const frameSize = mediaRequest.codecInfo.maxFs || H264_CODEC_PARAMETERS.maxFs;
237
+ const resolution = RESOLUTION_BUCKETS.find(([, maxFs]) => frameSize <= maxFs)?.[0] ?? '1080p';
238
+
239
+ return AV1_CODEC_PARAMETERS[resolution];
240
+ }
241
+
242
+ private buildH264CodecInfo(mr: MediaRequest): WcmeCodecInfo | undefined {
243
+ if (!mr.codecInfo) {
244
+ return undefined;
245
+ }
246
+
247
+ const h264PayloadType = this.getIngressPayloadTypeCallback(
248
+ mr.receiveSlots[0].mediaType,
249
+ MediaCodecMimeType.H264
250
+ );
251
+
252
+ if (h264PayloadType === undefined) {
253
+ return undefined;
254
+ }
255
+
256
+ return WcmeCodecInfo.fromH264(
257
+ h264PayloadType,
258
+ new H264Codec(
259
+ mr.codecInfo.maxFs,
260
+ mr.codecInfo.maxFps || H264_CODEC_PARAMETERS.maxFps,
261
+ this.getH264MaxMbps(mr),
262
+ mr.codecInfo.maxWidth,
263
+ mr.codecInfo.maxHeight
264
+ )
265
+ );
266
+ }
267
+
268
+ private buildAv1CodecInfo(mr: MediaRequest): WcmeCodecInfo | undefined {
269
+ if (!this.enableAv1 || !mr.codecInfo) {
270
+ return undefined;
271
+ }
272
+
273
+ const av1PayloadType = this.getIngressPayloadTypeCallback(
274
+ mr.receiveSlots[0].mediaType,
275
+ MediaCodecMimeType.AV1
276
+ );
277
+
278
+ if (av1PayloadType === undefined) {
279
+ return undefined;
280
+ }
281
+
282
+ const av1EncodingParams = this.getAv1EncodingParams(mr);
283
+
284
+ return WcmeCodecInfo.fromAv1(
285
+ av1PayloadType,
286
+ new AV1Codec(
287
+ av1EncodingParams.levelIdx,
288
+ av1EncodingParams.tier,
289
+ mr.codecInfo.maxWidth || av1EncodingParams.maxWidth,
290
+ mr.codecInfo.maxHeight || av1EncodingParams.maxHeight,
291
+ av1EncodingParams.maxPicSize,
292
+ av1EncodingParams.maxDecodeRate
293
+ )
294
+ );
295
+ }
296
+
201
297
  /** Modifies the passed in clientRequests and makes sure that in total they don't ask
202
298
  * for more streams than there are available.
203
299
  *
@@ -298,6 +394,12 @@ export class MediaRequestManager {
298
394
  // map all the client media requests to wcme stream requests
299
395
  Object.values(clientRequests).forEach((mr) => {
300
396
  if (mr.receiveSlots.length > 0) {
397
+ const codecInfos: WcmeCodecInfo[] = mr.codecInfo
398
+ ? [this.buildH264CodecInfo(mr), this.buildAv1CodecInfo(mr)].filter(
399
+ (info): info is WcmeCodecInfo => info !== undefined
400
+ )
401
+ : [];
402
+
301
403
  streamRequests.push(
302
404
  new StreamRequest(
303
405
  mr.policyInfo.policy === 'active-speaker'
@@ -314,25 +416,14 @@ export class MediaRequestManager {
314
416
  : new ReceiverSelectedInfo(mr.policyInfo.csi),
315
417
  mr.receiveSlots.map((receiveSlot) => receiveSlot.wcmeReceiveSlot),
316
418
  this.getMaxPayloadBitsPerSecond(mr),
317
- mr.codecInfo && [
318
- WcmeCodecInfo.fromH264(
319
- 0x80,
320
- new H264Codec(
321
- mr.codecInfo.maxFs,
322
- mr.codecInfo.maxFps || CODEC_DEFAULTS.h264.maxFps,
323
- this.getH264MaxMbps(mr),
324
- mr.codecInfo.maxWidth,
325
- mr.codecInfo.maxHeight
326
- )
327
- ),
328
- ]
419
+ codecInfos
329
420
  )
330
421
  );
331
422
  }
332
423
  });
333
424
 
334
425
  this.sendMediaRequestsCallback(streamRequests);
335
- LoggerProxy.logger.info(`multistream:sendRequests --> media requests sent. `);
426
+ LoggerProxy.logger.info(`multistream:sendRequests --> media requests sent.`);
336
427
  }
337
428
 
338
429
  public addRequest(mediaRequest: MediaRequest, commit = true): MediaRequestId {
@@ -134,6 +134,24 @@ export class ReceiveSlot extends EventsScope {
134
134
  });
135
135
  }
136
136
  );
137
+
138
+ this.mcReceiveSlot.on(WcmeReceiveSlotEvents.MediaStarted, () => {
139
+ LoggerProxy.logger.log(
140
+ `ReceiveSlot#setupEventListeners --> media started on receive slot ${this.id}, mediaType=${this.mediaType}`
141
+ );
142
+ });
143
+
144
+ this.mcReceiveSlot.on(WcmeReceiveSlotEvents.MediaStopped, () => {
145
+ LoggerProxy.logger.log(
146
+ `ReceiveSlot#setupEventListeners --> media stopped on receive slot ${this.id}, mediaType=${this.mediaType}`
147
+ );
148
+ });
149
+
150
+ this.mcReceiveSlot.on(WcmeReceiveSlotEvents.MediaEnded, () => {
151
+ LoggerProxy.logger.log(
152
+ `ReceiveSlot#setupEventListeners --> media ended on receive slot ${this.id}, mediaType=${this.mediaType}`
153
+ );
154
+ });
137
155
  }
138
156
 
139
157
  /** Tries to find the member id for this receive slot if it hasn't got one */
@@ -60,4 +60,7 @@ type RelayEventData = {
60
60
 
61
61
  export type RelayEvent = {
62
62
  data: RelayEventData;
63
+ headers?: {
64
+ route?: string;
65
+ };
63
66
  };
@@ -261,8 +261,7 @@ export default class RecordingController {
261
261
 
262
262
  LoggerProxy.logger.log(`RecordingController:index#recordingControls --> ${record}`);
263
263
 
264
- // @ts-ignore
265
- return this.request.request({
264
+ return this.request.locusDeltaRequest({
266
265
  uri: `${this.locusUrl}/${CONTROLS}`,
267
266
  body: {
268
267
  record,
@@ -18,6 +18,7 @@ import {
18
18
 
19
19
  import WebinarCollection from './collection';
20
20
  import LoggerProxy from '../common/logs/logger-proxy';
21
+ import MeetingUtil from '../meeting/util';
21
22
  import {sanitizeParams} from './utils';
22
23
 
23
24
  /**
@@ -98,13 +99,49 @@ const Webinar = WebexPlugin.extend({
98
99
  return {isPromoted, isDemoted};
99
100
  },
100
101
 
102
+ /**
103
+ * Resolves the meeting associated with this webinar instance, guarded against the
104
+ * meetingId pointer drifting onto an unrelated transient meeting (e.g. an inbound
105
+ * 1:1 call) that may exist in the meeting collection. Returns the meeting only when
106
+ * its locusUrl matches this webinar's tracked locusUrl. Returns undefined (with a
107
+ * warning) when the meeting cannot be resolved or when the webinar's locusUrl has
108
+ * not been initialized yet — callers must treat this as "no owned meeting" rather
109
+ * than fall through to an unvalidated lookup.
110
+ * @returns {object|undefined}
111
+ */
112
+ getValidatedWebinarMeeting() {
113
+ const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
114
+
115
+ if (!meeting) {
116
+ return undefined;
117
+ }
118
+
119
+ if (!this.locusUrl) {
120
+ LoggerProxy.logger.warn(
121
+ `Webinar:index#getValidatedWebinarMeeting --> skipping; webinar locusUrl is not yet initialized for meetingId ${this.meetingId}`
122
+ );
123
+
124
+ return undefined;
125
+ }
126
+
127
+ if (meeting.locusUrl !== this.locusUrl) {
128
+ LoggerProxy.logger.warn(
129
+ `Webinar:index#getValidatedWebinarMeeting --> skipping; meeting ${this.meetingId} locusUrl ${meeting.locusUrl} does not match webinar locusUrl ${this.locusUrl}`
130
+ );
131
+
132
+ return undefined;
133
+ }
134
+
135
+ return meeting;
136
+ },
137
+
101
138
  /**
102
139
  * should join practice session data channel or not
103
140
  * @param {Object} {isPromoted: boolean, isDemoted: boolean}} Role transition states
104
141
  * @returns {void}
105
142
  */
106
143
  updateStatusByRole({isPromoted, isDemoted}) {
107
- const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
144
+ const meeting = this.getValidatedWebinarMeeting();
108
145
 
109
146
  if (
110
147
  (isDemoted && meeting?.shareStatus === SHARE_STATUS.WHITEBOARD_SHARE_ACTIVE) ||
@@ -128,6 +165,9 @@ const Webinar = WebexPlugin.extend({
128
165
 
129
166
  /**
130
167
  * Disconnects the practice session data channel and removes its relay listener.
168
+ * The listener reference removed here is the exact callback captured at subscribe
169
+ * time (see updatePSDataChannel) so that cleanup is correct even if the underlying
170
+ * meeting can no longer be resolved (e.g. locusUrl mismatch).
131
171
  * @returns {Promise<void>}
132
172
  */
133
173
  async cleanupPSDataChannel() {
@@ -137,8 +177,6 @@ const Webinar = WebexPlugin.extend({
137
177
  this._pendingOnlineListener = null;
138
178
  }
139
179
 
140
- const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
141
-
142
180
  // @ts-ignore - Fix type
143
181
  await this.webex.internal.llm.disconnectLLM(
144
182
  {
@@ -147,11 +185,65 @@ const Webinar = WebexPlugin.extend({
147
185
  },
148
186
  LLM_PRACTICE_SESSION
149
187
  );
150
- // @ts-ignore - Fix type
151
- this.webex.internal.llm.off(
152
- `event:relay.event:${LLM_PRACTICE_SESSION}`,
153
- meeting?.processRelayEvent
188
+
189
+ if (this._practiceSessionRelayListener) {
190
+ // @ts-ignore - Fix type
191
+ this.webex.internal.llm.off(
192
+ `event:relay.event:${LLM_PRACTICE_SESSION}`,
193
+ this._practiceSessionRelayListener
194
+ );
195
+ this._practiceSessionRelayListener = null;
196
+ }
197
+ },
198
+
199
+ /**
200
+ * Ensures practice-session token exists before registering the practice LLM channel.
201
+ * Caller is responsible for passing a meeting that has already been resolved via
202
+ * getValidatedWebinarMeeting() — this method does not re-validate ownership.
203
+ * @param {object} meeting
204
+ * @returns {Promise<string|undefined>}
205
+ */
206
+ async ensurePracticeSessionDatachannelToken(meeting) {
207
+ // @ts-ignore
208
+ const isDataChannelTokenEnabled = await this.webex.internal.llm.isDataChannelTokenEnabled();
209
+
210
+ if (!isDataChannelTokenEnabled) {
211
+ return undefined;
212
+ }
213
+
214
+ // @ts-ignore
215
+ const cachedToken = this.webex.internal.llm.getDatachannelToken(
216
+ DataChannelTokenType.PracticeSession
154
217
  );
218
+
219
+ if (cachedToken) {
220
+ return cachedToken;
221
+ }
222
+
223
+ try {
224
+ const refreshResponse = await meeting.refreshDataChannelToken();
225
+ const {datachannelToken, dataChannelTokenType} = refreshResponse?.body ?? {};
226
+
227
+ if (!datachannelToken) {
228
+ return undefined;
229
+ }
230
+
231
+ // @ts-ignore
232
+ this.webex.internal.llm.setDatachannelToken(
233
+ datachannelToken,
234
+ dataChannelTokenType || DataChannelTokenType.PracticeSession
235
+ );
236
+
237
+ return datachannelToken;
238
+ } catch (error) {
239
+ LoggerProxy.logger.warn(
240
+ `Webinar:index#ensurePracticeSessionDatachannelToken --> failed to proactively refresh practice-session token: ${
241
+ error?.message || String(error)
242
+ }`
243
+ );
244
+
245
+ return undefined;
246
+ }
155
247
  },
156
248
 
157
249
  /**
@@ -160,7 +252,10 @@ const Webinar = WebexPlugin.extend({
160
252
  * @returns {Promise}
161
253
  */
162
254
  async updatePSDataChannel() {
163
- const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
255
+ this._updatePSDataChannelSequence = (this._updatePSDataChannelSequence || 0) + 1;
256
+ const invocationSequence = this._updatePSDataChannelSequence;
257
+
258
+ const meeting = this.getValidatedWebinarMeeting();
164
259
  const isPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
165
260
 
166
261
  if (!isPracticeSession) {
@@ -174,7 +269,7 @@ const Webinar = WebexPlugin.extend({
174
269
  meeting?.locusInfo || {};
175
270
 
176
271
  // @ts-ignore
177
- const practiceSessionDatachannelToken = this.webex.internal.llm.getDatachannelToken(
272
+ let practiceSessionDatachannelToken = this.webex.internal.llm.getDatachannelToken(
178
273
  DataChannelTokenType.PracticeSession
179
274
  );
180
275
 
@@ -229,6 +324,29 @@ const Webinar = WebexPlugin.extend({
229
324
  this._pendingOnlineListener = null;
230
325
  }
231
326
 
327
+ const refreshedPracticeSessionToken = await this.ensurePracticeSessionDatachannelToken(meeting);
328
+
329
+ const latestPracticeSessionDatachannelUrl = get(
330
+ meeting,
331
+ 'locusInfo.info.practiceSessionDatachannelUrl'
332
+ );
333
+ const isStillPracticeSession = meeting?.isJoined() && this.isJoinPracticeSessionDataChannel();
334
+
335
+ // Skip stale invocations after async refresh to avoid reconnecting a session
336
+ // that was already updated/cleaned by a newer state transition.
337
+ if (
338
+ invocationSequence !== this._updatePSDataChannelSequence ||
339
+ !isStillPracticeSession ||
340
+ !latestPracticeSessionDatachannelUrl ||
341
+ latestPracticeSessionDatachannelUrl !== practiceSessionDatachannelUrl
342
+ ) {
343
+ return undefined;
344
+ }
345
+
346
+ if (refreshedPracticeSessionToken) {
347
+ practiceSessionDatachannelToken = refreshedPracticeSessionToken;
348
+ }
349
+
232
350
  // @ts-ignore - Fix type
233
351
  return this.webex.internal.llm
234
352
  .registerAndConnect(
@@ -238,15 +356,21 @@ const Webinar = WebexPlugin.extend({
238
356
  LLM_PRACTICE_SESSION
239
357
  )
240
358
  .then((registerAndConnectResult) => {
241
- // @ts-ignore - Fix type
242
- this.webex.internal.llm.off(
243
- `event:relay.event:${LLM_PRACTICE_SESSION}`,
244
- meeting?.processRelayEvent
245
- );
359
+ // Track the exact listener reference so cleanupPSDataChannel can
360
+ // unsubscribe deterministically, even if the meeting can no longer
361
+ // be resolved at cleanup time.
362
+ if (this._practiceSessionRelayListener) {
363
+ // @ts-ignore - Fix type
364
+ this.webex.internal.llm.off(
365
+ `event:relay.event:${LLM_PRACTICE_SESSION}`,
366
+ this._practiceSessionRelayListener
367
+ );
368
+ }
369
+ this._practiceSessionRelayListener = meeting?.processRelayEvent;
246
370
  // @ts-ignore - Fix type
247
371
  this.webex.internal.llm.on(
248
372
  `event:relay.event:${LLM_PRACTICE_SESSION}`,
249
- meeting?.processRelayEvent
373
+ this._practiceSessionRelayListener
250
374
  );
251
375
  // @ts-ignore - Fix type
252
376
  this.webex.internal.voicea?.announce?.();
@@ -267,6 +391,8 @@ const Webinar = WebexPlugin.extend({
267
391
  * @returns {Promise}
268
392
  */
269
393
  setPracticeSessionState(enabled) {
394
+ const meeting = this.getValidatedWebinarMeeting();
395
+
270
396
  return this.request({
271
397
  method: HTTP_VERBS.PATCH,
272
398
  uri: `${this.locusUrl}/controls`,
@@ -275,10 +401,16 @@ const Webinar = WebexPlugin.extend({
275
401
  enabled,
276
402
  },
277
403
  },
278
- }).catch((error) => {
279
- LoggerProxy.logger.error('Meeting:webinar#setPracticeSessionState failed', error);
280
- throw error;
281
- });
404
+ })
405
+ .then((response) => {
406
+ MeetingUtil.updateLocusFromApiResponse(meeting, response);
407
+
408
+ return response;
409
+ })
410
+ .catch((error) => {
411
+ LoggerProxy.logger.error('Meeting:webinar#setPracticeSessionState failed', error);
412
+ throw error;
413
+ });
282
414
  },
283
415
 
284
416
  /**
@@ -458,7 +590,14 @@ const Webinar = WebexPlugin.extend({
458
590
  * @returns {Promise}
459
591
  */
460
592
  async searchLargeScaleWebinarAttendees(payload) {
461
- const meeting = this.webex.meetings.getMeetingByType(_ID_, this.meetingId);
593
+ const meeting = this.getValidatedWebinarMeeting();
594
+ if (!meeting) {
595
+ LoggerProxy.logger.error(
596
+ 'Meeting:webinar5k#searchLargeScaleWebinarAttendees failed --> webinar meeting could not be validated'
597
+ );
598
+ throw new Error('Meeting:webinar5k#Webinar meeting is not resolvable for the current locus');
599
+ }
600
+
462
601
  const rawParams = {
463
602
  search_text: payload?.queryString,
464
603
  limit: payload?.limit ?? DEFAULT_LARGE_SCALE_WEBINAR_ATTENDEE_SEARCH_LIMIT,
@@ -466,7 +605,9 @@ const Webinar = WebexPlugin.extend({
466
605
  };
467
606
  const attendeeSearchUrl = meeting?.locusInfo?.links?.resources?.attendeeSearch?.url;
468
607
  if (!attendeeSearchUrl) {
469
- LoggerProxy.logger.error(`Meeting:webinar5k#searchLargeScaleWebinarAttendees failed`);
608
+ LoggerProxy.logger.error(
609
+ 'Meeting:webinar5k#searchLargeScaleWebinarAttendees failed --> attendee search url unavailable'
610
+ );
470
611
  throw new Error('Meeting:webinar5k#Attendee search url is not available');
471
612
  }
472
613