@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.
- package/AGENTS.md +9 -0
- package/dist/aiEnableRequest/index.js +15 -2
- package/dist/aiEnableRequest/index.js.map +1 -1
- package/dist/breakouts/breakout.js +8 -3
- package/dist/breakouts/breakout.js.map +1 -1
- package/dist/breakouts/index.js +26 -2
- package/dist/breakouts/index.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/constants.js +6 -3
- package/dist/constants.js.map +1 -1
- package/dist/controls-options-manager/constants.js +11 -1
- package/dist/controls-options-manager/constants.js.map +1 -1
- package/dist/controls-options-manager/index.js +38 -24
- package/dist/controls-options-manager/index.js.map +1 -1
- package/dist/controls-options-manager/util.js +91 -0
- package/dist/controls-options-manager/util.js.map +1 -1
- package/dist/hashTree/constants.js +10 -1
- package/dist/hashTree/constants.js.map +1 -1
- package/dist/hashTree/hashTreeParser.js +716 -370
- package/dist/hashTree/hashTreeParser.js.map +1 -1
- package/dist/hashTree/utils.js +22 -0
- package/dist/hashTree/utils.js.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors/locusRetry.js +23 -8
- package/dist/interceptors/locusRetry.js.map +1 -1
- package/dist/interpretation/index.js +10 -1
- package/dist/interpretation/index.js.map +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/locus-info/controlsUtils.js +4 -1
- package/dist/locus-info/controlsUtils.js.map +1 -1
- package/dist/locus-info/index.js +289 -87
- package/dist/locus-info/index.js.map +1 -1
- package/dist/locus-info/types.js +19 -0
- package/dist/locus-info/types.js.map +1 -1
- package/dist/media/index.js +3 -1
- package/dist/media/index.js.map +1 -1
- package/dist/media/properties.js +1 -0
- package/dist/media/properties.js.map +1 -1
- package/dist/meeting/in-meeting-actions.js +3 -1
- package/dist/meeting/in-meeting-actions.js.map +1 -1
- package/dist/meeting/index.js +907 -535
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/util.js +19 -2
- package/dist/meeting/util.js.map +1 -1
- package/dist/meetings/index.js +231 -78
- package/dist/meetings/index.js.map +1 -1
- package/dist/meetings/meetings.types.js +6 -1
- package/dist/meetings/meetings.types.js.map +1 -1
- package/dist/meetings/request.js +39 -0
- package/dist/meetings/request.js.map +1 -1
- package/dist/meetings/util.js +79 -5
- package/dist/meetings/util.js.map +1 -1
- package/dist/member/index.js +10 -0
- package/dist/member/index.js.map +1 -1
- package/dist/member/types.js.map +1 -1
- package/dist/member/util.js +3 -0
- package/dist/member/util.js.map +1 -1
- package/dist/metrics/constants.js +4 -1
- package/dist/metrics/constants.js.map +1 -1
- package/dist/multistream/codec/constants.js +63 -0
- package/dist/multistream/codec/constants.js.map +1 -0
- package/dist/multistream/mediaRequestManager.js +62 -15
- package/dist/multistream/mediaRequestManager.js.map +1 -1
- package/dist/multistream/receiveSlot.js +9 -0
- package/dist/multistream/receiveSlot.js.map +1 -1
- package/dist/reactions/reactions.type.js.map +1 -1
- package/dist/recording-controller/index.js +1 -3
- package/dist/recording-controller/index.js.map +1 -1
- package/dist/types/config.d.ts +2 -0
- package/dist/types/constants.d.ts +2 -0
- package/dist/types/controls-options-manager/constants.d.ts +6 -1
- package/dist/types/controls-options-manager/index.d.ts +10 -0
- package/dist/types/hashTree/constants.d.ts +1 -0
- package/dist/types/hashTree/hashTreeParser.d.ts +92 -16
- package/dist/types/hashTree/utils.d.ts +11 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/interceptors/locusRetry.d.ts +4 -4
- package/dist/types/locus-info/index.d.ts +46 -6
- package/dist/types/locus-info/types.d.ts +21 -1
- package/dist/types/media/properties.d.ts +1 -0
- package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
- package/dist/types/meeting/index.d.ts +87 -3
- package/dist/types/meeting/util.d.ts +8 -0
- package/dist/types/meetings/index.d.ts +30 -2
- package/dist/types/meetings/meetings.types.d.ts +15 -0
- package/dist/types/meetings/request.d.ts +14 -0
- package/dist/types/member/index.d.ts +1 -0
- package/dist/types/member/types.d.ts +1 -0
- package/dist/types/member/util.d.ts +1 -0
- package/dist/types/metrics/constants.d.ts +3 -0
- package/dist/types/multistream/codec/constants.d.ts +7 -0
- package/dist/types/multistream/mediaRequestManager.d.ts +22 -5
- package/dist/types/reactions/reactions.type.d.ts +3 -0
- package/dist/webinar/index.js +361 -235
- package/dist/webinar/index.js.map +1 -1
- package/package.json +22 -22
- package/src/aiEnableRequest/index.ts +16 -0
- package/src/breakouts/breakout.ts +3 -1
- package/src/breakouts/index.ts +31 -0
- package/src/config.ts +2 -0
- package/src/constants.ts +5 -1
- package/src/controls-options-manager/constants.ts +14 -1
- package/src/controls-options-manager/index.ts +47 -24
- package/src/controls-options-manager/util.ts +81 -1
- package/src/hashTree/constants.ts +9 -0
- package/src/hashTree/hashTreeParser.ts +429 -183
- package/src/hashTree/utils.ts +17 -0
- package/src/index.ts +5 -0
- package/src/interceptors/locusRetry.ts +25 -4
- package/src/interpretation/index.ts +25 -8
- package/src/locus-info/controlsUtils.ts +3 -1
- package/src/locus-info/index.ts +291 -97
- package/src/locus-info/types.ts +25 -1
- package/src/media/index.ts +3 -0
- package/src/media/properties.ts +1 -0
- package/src/meeting/in-meeting-actions.ts +4 -0
- package/src/meeting/index.ts +388 -33
- package/src/meeting/util.ts +20 -2
- package/src/meetings/index.ts +134 -44
- package/src/meetings/meetings.types.ts +19 -0
- package/src/meetings/request.ts +43 -0
- package/src/meetings/util.ts +97 -1
- package/src/member/index.ts +10 -0
- package/src/member/types.ts +1 -0
- package/src/member/util.ts +3 -0
- package/src/metrics/constants.ts +3 -0
- package/src/multistream/codec/constants.ts +58 -0
- package/src/multistream/mediaRequestManager.ts +119 -28
- package/src/multistream/receiveSlot.ts +18 -0
- package/src/reactions/reactions.type.ts +3 -0
- package/src/recording-controller/index.ts +1 -2
- package/src/webinar/index.ts +162 -21
- package/test/unit/spec/aiEnableRequest/index.ts +86 -0
- package/test/unit/spec/breakouts/breakout.ts +9 -3
- package/test/unit/spec/breakouts/index.ts +49 -0
- package/test/unit/spec/controls-options-manager/index.js +140 -29
- package/test/unit/spec/controls-options-manager/util.js +165 -0
- package/test/unit/spec/hashTree/hashTreeParser.ts +1508 -149
- package/test/unit/spec/hashTree/utils.ts +88 -1
- package/test/unit/spec/interceptors/locusRetry.ts +205 -4
- package/test/unit/spec/interpretation/index.ts +26 -4
- package/test/unit/spec/locus-info/controlsUtils.js +172 -57
- package/test/unit/spec/locus-info/index.js +475 -81
- package/test/unit/spec/media/index.ts +31 -0
- package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
- package/test/unit/spec/meeting/index.js +1131 -49
- package/test/unit/spec/meeting/muteState.js +3 -0
- package/test/unit/spec/meeting/utils.js +33 -0
- package/test/unit/spec/meetings/index.js +360 -10
- package/test/unit/spec/meetings/request.js +141 -0
- package/test/unit/spec/meetings/utils.js +189 -0
- package/test/unit/spec/member/index.js +7 -0
- package/test/unit/spec/member/util.js +24 -0
- package/test/unit/spec/multistream/mediaRequestManager.ts +501 -37
- package/test/unit/spec/recording-controller/index.js +9 -8
- 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
|
|
84
|
+
type AudioMediaRequestManagerOptions = {
|
|
75
85
|
degradationPreferences: DegradationPreferences;
|
|
76
|
-
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(
|
|
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 ||
|
|
139
|
-
mr.codecInfo.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 ||
|
|
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 ||
|
|
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
|
-
|
|
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 */
|
|
@@ -261,8 +261,7 @@ export default class RecordingController {
|
|
|
261
261
|
|
|
262
262
|
LoggerProxy.logger.log(`RecordingController:index#recordingControls --> ${record}`);
|
|
263
263
|
|
|
264
|
-
|
|
265
|
-
return this.request.request({
|
|
264
|
+
return this.request.locusDeltaRequest({
|
|
266
265
|
uri: `${this.locusUrl}/${CONTROLS}`,
|
|
267
266
|
body: {
|
|
268
267
|
record,
|
package/src/webinar/index.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
151
|
-
this.
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
})
|
|
279
|
-
|
|
280
|
-
|
|
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.
|
|
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(
|
|
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
|
|