@pexip/media 17.3.0 → 18.0.0
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/CHANGELOG.md +56 -0
- package/dist/audioMixingProcessor.d.ts +18 -0
- package/dist/audioMixingProcessor.js +173 -0
- package/dist/audioProcessor.d.ts +86 -0
- package/dist/audioProcessor.js +310 -0
- package/dist/baseLogger.d.ts +37 -0
- package/dist/baseLogger.js +29 -0
- package/dist/displayMedia.d.ts +3 -0
- package/dist/displayMedia.js +31 -0
- package/dist/index.d.ts +11 -837
- package/dist/index.js +11 -0
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +44 -0
- package/dist/media.d.ts +8 -0
- package/dist/media.js +296 -0
- package/dist/previewController.d.ts +65 -0
- package/dist/previewController.js +372 -0
- package/dist/signals.d.ts +25 -0
- package/dist/signals.js +38 -0
- package/dist/status.d.ts +35 -0
- package/dist/status.js +199 -0
- package/dist/typeGuard.d.ts +9 -0
- package/dist/typeGuard.js +40 -0
- package/dist/types.d.ts +557 -0
- package/dist/types.js +278 -0
- package/dist/userMedia.d.ts +38 -0
- package/dist/userMedia.js +333 -0
- package/dist/utils.d.ts +108 -0
- package/dist/utils.js +361 -0
- package/dist/videoProcessor.d.ts +41 -0
- package/dist/videoProcessor.js +362 -0
- package/package.json +8 -8
- package/dist/index.mjs +0 -2902
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { createVideoProcessor, createCanvasTransform, createVideoTrackProcessor, createVideoTrackProcessorWithFallback, isRenderEffects, isSegmentationModel, } from '@pexip/media-processor';
|
|
2
|
+
import { muteStreamTrack, extractConstraintsWithKeys, getValueFromConstrainNumber, } from '@pexip/media-control';
|
|
3
|
+
import { isEmpty } from '@pexip/utils';
|
|
4
|
+
import { shallowCopy, wrapToJSON, applyExtendedConstraints, getBlurKernelSize, } from './utils';
|
|
5
|
+
import { logger, proxyWithLog } from './logger';
|
|
6
|
+
import { isVideoContentHint } from './typeGuard';
|
|
7
|
+
const FEATURE_KEYS = [
|
|
8
|
+
'backgroundBlurAmount',
|
|
9
|
+
'backgroundImageUrl',
|
|
10
|
+
'maskCombineRatio',
|
|
11
|
+
'edgeBlurAmount',
|
|
12
|
+
'foregroundThreshold',
|
|
13
|
+
'frameRate',
|
|
14
|
+
'videoSegmentation',
|
|
15
|
+
'videoSegmentationModel',
|
|
16
|
+
'width',
|
|
17
|
+
'height',
|
|
18
|
+
'pan',
|
|
19
|
+
'tilt',
|
|
20
|
+
'zoom',
|
|
21
|
+
'contentHint',
|
|
22
|
+
];
|
|
23
|
+
const getVideoConstraints = extractConstraintsWithKeys(FEATURE_KEYS);
|
|
24
|
+
export const updateFeatureProps = (constraints, props) => {
|
|
25
|
+
const extracted = getVideoConstraints(constraints);
|
|
26
|
+
return FEATURE_KEYS.reduce((accm, key) => {
|
|
27
|
+
switch (key) {
|
|
28
|
+
case 'contentHint': {
|
|
29
|
+
const [[feature] = []] = extracted[key];
|
|
30
|
+
if (isVideoContentHint(feature) && props[key] !== feature) {
|
|
31
|
+
props[key] = feature;
|
|
32
|
+
return { ...accm, [key]: feature };
|
|
33
|
+
}
|
|
34
|
+
return accm;
|
|
35
|
+
}
|
|
36
|
+
case 'videoSegmentation': {
|
|
37
|
+
const [[feature] = []] = extracted[key];
|
|
38
|
+
if (isRenderEffects(feature) && props[key] !== feature) {
|
|
39
|
+
props[key] = feature;
|
|
40
|
+
return { ...accm, [key]: feature };
|
|
41
|
+
}
|
|
42
|
+
return accm;
|
|
43
|
+
}
|
|
44
|
+
case 'videoSegmentationModel': {
|
|
45
|
+
const [[feature] = []] = extracted[key];
|
|
46
|
+
if (isSegmentationModel(feature) && props[key] !== feature) {
|
|
47
|
+
props[key] = feature;
|
|
48
|
+
return { ...accm, [key]: feature };
|
|
49
|
+
}
|
|
50
|
+
return accm;
|
|
51
|
+
}
|
|
52
|
+
case 'backgroundImageUrl': {
|
|
53
|
+
const [[feature] = []] = extracted[key];
|
|
54
|
+
if (feature) {
|
|
55
|
+
props[key] = feature;
|
|
56
|
+
return { ...accm, [key]: feature };
|
|
57
|
+
}
|
|
58
|
+
return accm;
|
|
59
|
+
}
|
|
60
|
+
case 'width':
|
|
61
|
+
case 'height':
|
|
62
|
+
case 'frameRate':
|
|
63
|
+
case 'foregroundThreshold':
|
|
64
|
+
case 'edgeBlurAmount':
|
|
65
|
+
case 'maskCombineRatio':
|
|
66
|
+
case 'backgroundBlurAmount': {
|
|
67
|
+
const [feature] = extracted[key];
|
|
68
|
+
if (feature !== undefined) {
|
|
69
|
+
const value = getValueFromConstrainNumber(feature);
|
|
70
|
+
if (props[key] !== value) {
|
|
71
|
+
props[key] = value;
|
|
72
|
+
return {
|
|
73
|
+
...accm,
|
|
74
|
+
[key]: value,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return accm;
|
|
79
|
+
}
|
|
80
|
+
case 'pan':
|
|
81
|
+
case 'tilt':
|
|
82
|
+
case 'zoom': {
|
|
83
|
+
const [feature] = extracted[key];
|
|
84
|
+
if (feature !== undefined && props[key] !== feature) {
|
|
85
|
+
props[key] = feature;
|
|
86
|
+
return { ...accm, [key]: feature };
|
|
87
|
+
}
|
|
88
|
+
return accm;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}, {});
|
|
92
|
+
};
|
|
93
|
+
const applyFeatures = (transformer, features) => {
|
|
94
|
+
Object.keys(features).forEach(key => {
|
|
95
|
+
const k = key;
|
|
96
|
+
switch (k) {
|
|
97
|
+
case 'edgeBlurAmount':
|
|
98
|
+
case 'maskCombineRatio':
|
|
99
|
+
case 'foregroundThreshold': {
|
|
100
|
+
const value = features[k];
|
|
101
|
+
if (value !== undefined) {
|
|
102
|
+
transformer[k] = value;
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
case 'backgroundBlurAmount': {
|
|
107
|
+
const value = features[k];
|
|
108
|
+
if (value !== undefined) {
|
|
109
|
+
transformer[k] = getBlurKernelSize(value, transformer.height);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
case 'videoSegmentation': {
|
|
114
|
+
const value = features[k];
|
|
115
|
+
if (value && value !== transformer.effects) {
|
|
116
|
+
transformer.effects = value;
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
case 'backgroundImageUrl': {
|
|
121
|
+
const value = features[k];
|
|
122
|
+
if (value && value !== transformer.backgroundImageUrl) {
|
|
123
|
+
transformer.backgroundImageUrl = value;
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
default: {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
const adjustResolution = async (media, features, processingSize) => {
|
|
134
|
+
if (features.videoSegmentation) {
|
|
135
|
+
const { video: [videoSettings], } = media.getSettings();
|
|
136
|
+
const constraints = updateFeatureProps(media.constraints?.video, {});
|
|
137
|
+
switch (features.videoSegmentation) {
|
|
138
|
+
case 'blur':
|
|
139
|
+
case 'overlay': {
|
|
140
|
+
if (videoSettings?.height !== processingSize.height) {
|
|
141
|
+
try {
|
|
142
|
+
await media.applyConstraints({
|
|
143
|
+
video: {
|
|
144
|
+
width: processingSize.width,
|
|
145
|
+
height: processingSize.height,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
const { video: [postVideoSettings], } = media.getSettings();
|
|
149
|
+
if (postVideoSettings?.height !== processingSize.height) {
|
|
150
|
+
// Workaround Firefox 16:9 ratio https://bugzilla.mozilla.org/show_bug.cgi?id=1193640
|
|
151
|
+
await media.applyConstraints({
|
|
152
|
+
video: { height: 720 },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
// Workaround Firefox 16:9 ratio https://bugzilla.mozilla.org/show_bug.cgi?id=1193640
|
|
158
|
+
await media.applyConstraints({
|
|
159
|
+
video: { height: 720 },
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case 'none': {
|
|
166
|
+
if (constraints.height &&
|
|
167
|
+
constraints.height !== videoSettings?.height) {
|
|
168
|
+
await media.applyConstraints({
|
|
169
|
+
video: {
|
|
170
|
+
height: constraints.height,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
const getTrackProcessor = (shouldUseStreamTrackProcessor, ...params) => {
|
|
180
|
+
if (shouldUseStreamTrackProcessor &&
|
|
181
|
+
'MediaStreamTrackProcessor' in window) {
|
|
182
|
+
return createVideoTrackProcessor();
|
|
183
|
+
}
|
|
184
|
+
return createVideoTrackProcessorWithFallback(...params);
|
|
185
|
+
};
|
|
186
|
+
export const createVideoStreamProcess = ({ trackProcessorAPI = () => 'stream', processingWidth, processingHeight, shouldEnable, frameRate,
|
|
187
|
+
//backgroundBlurAmount,
|
|
188
|
+
videoSegmentation,
|
|
189
|
+
//edgeBlurAmount,
|
|
190
|
+
foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, scope = 'media', ...options }) => {
|
|
191
|
+
const videoSegmentationModel = options.videoSegmentationModel ?? 'selfie';
|
|
192
|
+
const segmenter = options.segmenters[videoSegmentationModel];
|
|
193
|
+
if (!segmenter) {
|
|
194
|
+
throw new Error('Segmenter is undefined');
|
|
195
|
+
}
|
|
196
|
+
const backgroundBlurAmount = options.backgroundBlurAmount &&
|
|
197
|
+
getBlurKernelSize(options.backgroundBlurAmount, processingHeight);
|
|
198
|
+
const transformer = options.transformer ??
|
|
199
|
+
createCanvasTransform(segmenter, {
|
|
200
|
+
width: processingWidth,
|
|
201
|
+
height: processingHeight,
|
|
202
|
+
effects: videoSegmentation,
|
|
203
|
+
foregroundThreshold,
|
|
204
|
+
backgroundBlurAmount,
|
|
205
|
+
edgeBlurAmount,
|
|
206
|
+
backgroundImageUrl,
|
|
207
|
+
maskCombineRatio,
|
|
208
|
+
});
|
|
209
|
+
const proxy = proxyWithLog(logger, scope);
|
|
210
|
+
let _videoProcessor = undefined;
|
|
211
|
+
const props = {
|
|
212
|
+
videoSegmentationModel,
|
|
213
|
+
segmenters: {
|
|
214
|
+
selfie: options.segmenters.selfie &&
|
|
215
|
+
proxy(options.segmenters.selfie, 'Segmenter'),
|
|
216
|
+
deeplabV3: options.segmenters.deeplabV3 &&
|
|
217
|
+
proxy(options.segmenters.deeplabV3, 'Segmenter'),
|
|
218
|
+
},
|
|
219
|
+
transformer: proxy(transformer, 'Transformer'),
|
|
220
|
+
videoProcessor: () => {
|
|
221
|
+
if (!_videoProcessor) {
|
|
222
|
+
_videoProcessor = proxy(createVideoProcessor([transformer], getTrackProcessor(trackProcessorAPI() === 'stream', {
|
|
223
|
+
width: processingWidth,
|
|
224
|
+
height: processingHeight,
|
|
225
|
+
frameRate,
|
|
226
|
+
})), 'VideoProcessor');
|
|
227
|
+
}
|
|
228
|
+
return _videoProcessor;
|
|
229
|
+
},
|
|
230
|
+
videoSegmentation,
|
|
231
|
+
backgroundBlurAmount,
|
|
232
|
+
edgeBlurAmount,
|
|
233
|
+
foregroundThreshold,
|
|
234
|
+
frameRate,
|
|
235
|
+
backgroundImageUrl,
|
|
236
|
+
maskCombineRatio,
|
|
237
|
+
hasInitialized: options.hasInitializedDeps ?? false,
|
|
238
|
+
};
|
|
239
|
+
return async (mediaP) => {
|
|
240
|
+
const media = await mediaP;
|
|
241
|
+
const features = updateFeatureProps(media.constraints?.video, props);
|
|
242
|
+
const shouldEnabled = shouldEnable();
|
|
243
|
+
if (!shouldEnabled || !media.stream?.getVideoTracks().length) {
|
|
244
|
+
logger.debug({ scope, features, shouldEnabled }, 'Video processing is skipped');
|
|
245
|
+
return media;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
if (!props.hasInitialized) {
|
|
249
|
+
await props.videoProcessor().open();
|
|
250
|
+
props.hasInitialized = true;
|
|
251
|
+
}
|
|
252
|
+
applyFeatures(props.transformer, features);
|
|
253
|
+
await adjustResolution(media, features, {
|
|
254
|
+
width: processingWidth,
|
|
255
|
+
height: processingHeight,
|
|
256
|
+
});
|
|
257
|
+
const model = features.videoSegmentationModel &&
|
|
258
|
+
props.segmenters[features.videoSegmentationModel];
|
|
259
|
+
if (features.videoSegmentationModel &&
|
|
260
|
+
features.videoSegmentationModel !==
|
|
261
|
+
props.transformer.segmenter.modelAsset.modelName &&
|
|
262
|
+
model) {
|
|
263
|
+
props.transformer.segmenter = model;
|
|
264
|
+
}
|
|
265
|
+
const stream = await props.videoProcessor().process(media.stream);
|
|
266
|
+
const release = async () => {
|
|
267
|
+
props.videoProcessor().close();
|
|
268
|
+
await media.release();
|
|
269
|
+
_videoProcessor = undefined;
|
|
270
|
+
props.hasInitialized = false;
|
|
271
|
+
};
|
|
272
|
+
const muteAudio = (mute) => {
|
|
273
|
+
media.muteAudio(mute);
|
|
274
|
+
muteStreamTrack(stream)(mute, 'audio');
|
|
275
|
+
};
|
|
276
|
+
const muteVideo = (mute) => {
|
|
277
|
+
media.muteVideo(mute);
|
|
278
|
+
props.transformer.effects = mute
|
|
279
|
+
? 'none'
|
|
280
|
+
: props.videoSegmentation ?? 'none';
|
|
281
|
+
muteStreamTrack(stream)(mute, 'video');
|
|
282
|
+
};
|
|
283
|
+
const applyConstraints = applyExtendedConstraints(media, async (constraints) => {
|
|
284
|
+
if (isEmpty(constraints.video)) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const features = updateFeatureProps(constraints.video, props);
|
|
288
|
+
logger.debug({ scope, constraints: constraints.video, features }, 'apply video constraints');
|
|
289
|
+
if (isEmpty(features)) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
applyFeatures(props.transformer, features);
|
|
294
|
+
await adjustResolution(media, features, {
|
|
295
|
+
width: processingWidth,
|
|
296
|
+
height: processingHeight,
|
|
297
|
+
});
|
|
298
|
+
const model = features.videoSegmentationModel &&
|
|
299
|
+
props.segmenters[features.videoSegmentationModel];
|
|
300
|
+
if (model &&
|
|
301
|
+
features.videoSegmentationModel !==
|
|
302
|
+
props.transformer.segmenter.modelAsset.modelName) {
|
|
303
|
+
props.transformer.segmenter = model;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
if (error instanceof Error) {
|
|
308
|
+
logger.error({
|
|
309
|
+
scope,
|
|
310
|
+
constraints: constraints.video,
|
|
311
|
+
features,
|
|
312
|
+
error,
|
|
313
|
+
}, 'failed to apply video constraints');
|
|
314
|
+
options.onError?.(error);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
const prevGetSettings = media.getSettings;
|
|
319
|
+
return wrapToJSON(shallowCopy(media, {
|
|
320
|
+
stream,
|
|
321
|
+
muteAudio,
|
|
322
|
+
muteVideo,
|
|
323
|
+
applyConstraints,
|
|
324
|
+
release,
|
|
325
|
+
getSettings: () => {
|
|
326
|
+
const { audio, video } = prevGetSettings();
|
|
327
|
+
const contentHint = stream.getAudioTracks().at(0)
|
|
328
|
+
?.contentHint ?? '';
|
|
329
|
+
const videoSettings = {
|
|
330
|
+
videoSegmentation: transformer.effects,
|
|
331
|
+
foregroundThreshold: transformer.foregroundThreshold,
|
|
332
|
+
// Background blur amount is a function of height
|
|
333
|
+
backgroundBlurAmount: props.backgroundBlurAmount,
|
|
334
|
+
edgeBlurAmount: transformer.edgeBlurAmount,
|
|
335
|
+
maskCombineRatio: transformer.maskCombineRatio,
|
|
336
|
+
backgroundImageUrl: transformer.backgroundImageUrl,
|
|
337
|
+
videoSegmentationModel: transformer.segmenter.modelAsset.modelName,
|
|
338
|
+
pan: props.pan,
|
|
339
|
+
tilt: props.tilt,
|
|
340
|
+
zoom: props.zoom,
|
|
341
|
+
contentHint,
|
|
342
|
+
};
|
|
343
|
+
const settings = {
|
|
344
|
+
audio,
|
|
345
|
+
video: video.map(settings => ({
|
|
346
|
+
...settings,
|
|
347
|
+
...videoSettings,
|
|
348
|
+
})),
|
|
349
|
+
};
|
|
350
|
+
logger.debug({ scope, settings: videoSettings }, 'get video processor settings');
|
|
351
|
+
return settings;
|
|
352
|
+
},
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
if (e instanceof Error) {
|
|
357
|
+
options.onError?.(e);
|
|
358
|
+
}
|
|
359
|
+
return media;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pexip/media",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "18.0.0",
|
|
4
4
|
"description": "Home for media related stuff",
|
|
5
5
|
"homepage": "https://gitlab.com/pexip/zoo",
|
|
6
6
|
"bugs": "https://gitlab.com/pexip/zoo/issues",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"license": "Apache-2.0",
|
|
13
13
|
"author": "Terrence Lam <terrence@tlam.dev>",
|
|
14
|
-
"module": "dist/index.
|
|
14
|
+
"module": "dist/index.js",
|
|
15
15
|
"types": "dist/index.d.ts",
|
|
16
16
|
"directories": {
|
|
17
17
|
"lib": "lib",
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
"typecheck": "yarn tsc --noEmit -p ."
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@pexip/media-control": "
|
|
36
|
-
"@pexip/media-processor": "
|
|
37
|
-
"@pexip/signal": "16.
|
|
38
|
-
"@pexip/utils": "16.
|
|
35
|
+
"@pexip/media-control": "18.0.0",
|
|
36
|
+
"@pexip/media-processor": "18.0.0",
|
|
37
|
+
"@pexip/signal": "16.7.0",
|
|
38
|
+
"@pexip/utils": "16.11.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@jest/globals": "^29.7.0",
|
|
42
|
-
"@pexip/bundler": "16.
|
|
43
|
-
"prettier": "^3.
|
|
42
|
+
"@pexip/bundler": "16.6.1",
|
|
43
|
+
"prettier": "^3.2.5",
|
|
44
44
|
"typescript": "~5.3.0"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|