@pexip/media 17.2.0 → 17.4.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.
@@ -0,0 +1,349 @@
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
+ 'bgImageUrl',
10
+ 'edgeBlurAmount',
11
+ 'flipHorizontal',
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 'bgImageUrl': {
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 'backgroundBlurAmount': {
66
+ const [feature] = extracted[key];
67
+ if (feature !== undefined) {
68
+ const value = getValueFromConstrainNumber(feature);
69
+ if (props[key] !== value) {
70
+ props[key] = value;
71
+ return {
72
+ ...accm,
73
+ [key]: value,
74
+ };
75
+ }
76
+ }
77
+ return accm;
78
+ }
79
+ case 'flipHorizontal':
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 = async (transformer, features) => {
94
+ // bgImageUrl should be applied before `videoSegmentation` effects since
95
+ // backgroundImage is the prerequisite of the `overlay` effects
96
+ if (features.bgImageUrl) {
97
+ await transformer.loadBackgroundImage(features.bgImageUrl);
98
+ }
99
+ Object.keys(features).forEach(key => {
100
+ const k = key;
101
+ switch (k) {
102
+ case 'edgeBlurAmount':
103
+ case 'foregroundThreshold': {
104
+ const value = features[k];
105
+ if (value !== undefined) {
106
+ transformer[k] = value;
107
+ }
108
+ return;
109
+ }
110
+ case 'backgroundBlurAmount': {
111
+ const value = features[k];
112
+ if (value !== undefined) {
113
+ transformer[k] = getBlurKernelSize(value, transformer.height);
114
+ }
115
+ return;
116
+ }
117
+ case 'videoSegmentation': {
118
+ const value = features[k];
119
+ if (value && value !== transformer.effects) {
120
+ transformer.effects = value;
121
+ }
122
+ return;
123
+ }
124
+ case 'flipHorizontal': {
125
+ const value = features[k];
126
+ if (value !== undefined &&
127
+ value !== transformer.flipHorizontal) {
128
+ transformer.flipHorizontal = value;
129
+ }
130
+ return;
131
+ }
132
+ default: {
133
+ return;
134
+ }
135
+ }
136
+ });
137
+ };
138
+ const adjustResolution = async (media, features, processingSize) => {
139
+ if (features.videoSegmentation) {
140
+ const { video: [videoSettings], } = media.getSettings();
141
+ const constraints = updateFeatureProps(media.constraints?.video, {});
142
+ switch (features.videoSegmentation) {
143
+ case 'blur':
144
+ case 'overlay': {
145
+ if (videoSettings?.height !== processingSize.height) {
146
+ try {
147
+ await media.applyConstraints({
148
+ video: {
149
+ width: processingSize.width,
150
+ height: processingSize.height,
151
+ },
152
+ });
153
+ const { video: [postVideoSettings], } = media.getSettings();
154
+ if (postVideoSettings?.height !== processingSize.height) {
155
+ // Workaround Firefox 16:9 ratio https://bugzilla.mozilla.org/show_bug.cgi?id=1193640
156
+ await media.applyConstraints({
157
+ video: { height: 720 },
158
+ });
159
+ }
160
+ }
161
+ catch (error) {
162
+ // Workaround Firefox 16:9 ratio https://bugzilla.mozilla.org/show_bug.cgi?id=1193640
163
+ await media.applyConstraints({
164
+ video: { height: 720 },
165
+ });
166
+ }
167
+ }
168
+ break;
169
+ }
170
+ case 'none': {
171
+ if (constraints.height &&
172
+ constraints.height !== videoSettings?.height) {
173
+ await media.applyConstraints({
174
+ video: {
175
+ height: constraints.height,
176
+ },
177
+ });
178
+ }
179
+ break;
180
+ }
181
+ }
182
+ }
183
+ };
184
+ const getTrackProcessor = (shouldUseStreamTrackProcessor, ...params) => {
185
+ if (shouldUseStreamTrackProcessor &&
186
+ 'MediaStreamTrackProcessor' in window) {
187
+ return createVideoTrackProcessor();
188
+ }
189
+ return createVideoTrackProcessorWithFallback(...params);
190
+ };
191
+ export const createVideoStreamProcess = ({ trackProcessorAPI = () => 'stream', processingWidth, processingHeight, shouldEnable, frameRate,
192
+ //backgroundBlurAmount,
193
+ videoSegmentation,
194
+ //edgeBlurAmount,
195
+ foregroundThreshold, bgImageUrl, flipHorizontal, edgeBlurAmount, scope = 'media', ...options }) => {
196
+ const videoSegmentationModel = options.videoSegmentationModel ?? 'mediapipeSelfie';
197
+ const backgroundBlurAmount = options.backgroundBlurAmount &&
198
+ getBlurKernelSize(options.backgroundBlurAmount, processingHeight);
199
+ const transformer = options.transformer ??
200
+ createCanvasTransform(options.segmenters[videoSegmentationModel], {
201
+ width: processingWidth,
202
+ height: processingHeight,
203
+ effects: videoSegmentation,
204
+ foregroundThreshold,
205
+ backgroundBlurAmount,
206
+ edgeBlurAmount,
207
+ flipHorizontal,
208
+ });
209
+ const proxy = proxyWithLog(logger, scope);
210
+ const props = {
211
+ videoSegmentationModel,
212
+ segmenters: {
213
+ mediapipeSelfie: proxy(options.segmenters.mediapipeSelfie, 'Segmenter'),
214
+ },
215
+ transformer: proxy(transformer, 'Transformer'),
216
+ videoProcessor: () => proxy(createVideoProcessor([transformer], getTrackProcessor(trackProcessorAPI() === 'stream', {
217
+ width: processingWidth,
218
+ height: processingHeight,
219
+ frameRate,
220
+ })), 'VideoProcessor'),
221
+ videoSegmentation,
222
+ backgroundBlurAmount,
223
+ edgeBlurAmount,
224
+ foregroundThreshold,
225
+ frameRate,
226
+ bgImageUrl,
227
+ flipHorizontal,
228
+ hasInitialized: options.hasInitializedDeps ?? false,
229
+ };
230
+ return async (mediaP) => {
231
+ const media = await mediaP;
232
+ const features = updateFeatureProps(media.constraints?.video, props);
233
+ const shouldEnabled = shouldEnable();
234
+ if (!shouldEnabled || !media.stream?.getVideoTracks().length) {
235
+ logger.debug({ scope, features, shouldEnabled }, 'Video processing is skipped');
236
+ return media;
237
+ }
238
+ try {
239
+ if (!props.hasInitialized) {
240
+ await props.videoProcessor().open();
241
+ props.hasInitialized = true;
242
+ }
243
+ await applyFeatures(props.transformer, features);
244
+ await adjustResolution(media, features, {
245
+ width: processingWidth,
246
+ height: processingHeight,
247
+ });
248
+ if (features.videoSegmentationModel &&
249
+ features.videoSegmentationModel !==
250
+ props.transformer.segmenter.model) {
251
+ props.transformer.segmenter =
252
+ props.segmenters[features.videoSegmentationModel];
253
+ }
254
+ const stream = await props.videoProcessor().process(media.stream);
255
+ const release = async () => {
256
+ props.videoProcessor().close();
257
+ await media.release();
258
+ props.hasInitialized = false;
259
+ };
260
+ const muteAudio = (mute) => {
261
+ media.muteAudio(mute);
262
+ muteStreamTrack(stream)(mute, 'audio');
263
+ };
264
+ const muteVideo = (mute) => {
265
+ media.muteVideo(mute);
266
+ props.transformer.effects = mute
267
+ ? 'none'
268
+ : props.videoSegmentation ?? 'none';
269
+ muteStreamTrack(stream)(mute, 'video');
270
+ };
271
+ const applyConstraints = applyExtendedConstraints(media, async (constraints) => {
272
+ if (isEmpty(constraints.video)) {
273
+ return;
274
+ }
275
+ const features = updateFeatureProps(constraints.video, props);
276
+ logger.debug({ scope, constraints: constraints.video, features }, 'apply video constraints');
277
+ if (isEmpty(features)) {
278
+ return;
279
+ }
280
+ try {
281
+ await applyFeatures(props.transformer, features);
282
+ await adjustResolution(media, features, {
283
+ width: processingWidth,
284
+ height: processingHeight,
285
+ });
286
+ if (features.videoSegmentationModel &&
287
+ features.videoSegmentationModel !==
288
+ props.transformer.segmenter.model) {
289
+ props.transformer.segmenter =
290
+ props.segmenters[features.videoSegmentationModel];
291
+ }
292
+ }
293
+ catch (error) {
294
+ if (error instanceof Error) {
295
+ logger.error({
296
+ scope,
297
+ constraints: constraints.video,
298
+ features,
299
+ error,
300
+ }, 'failed to apply video constraints');
301
+ options.onError?.(error);
302
+ }
303
+ }
304
+ });
305
+ const prevGetSettings = media.getSettings;
306
+ return wrapToJSON(shallowCopy(media, {
307
+ stream,
308
+ muteAudio,
309
+ muteVideo,
310
+ applyConstraints,
311
+ release,
312
+ getSettings: () => {
313
+ const { audio, video } = prevGetSettings();
314
+ const contentHint = stream.getAudioTracks().at(0)
315
+ ?.contentHint ?? '';
316
+ const videoSettings = {
317
+ videoSegmentation: transformer.effects,
318
+ foregroundThreshold: transformer.foregroundThreshold,
319
+ // Background blur amount is a function of height
320
+ backgroundBlurAmount: props.backgroundBlurAmount,
321
+ edgeBlurAmount: transformer.edgeBlurAmount,
322
+ flipHorizontal: transformer.flipHorizontal,
323
+ bgImageUrl: transformer.backgroundImage && props.bgImageUrl,
324
+ videoSegmentationModel: transformer.segmenter.model,
325
+ pan: props.pan,
326
+ tilt: props.tilt,
327
+ zoom: props.zoom,
328
+ contentHint,
329
+ };
330
+ const settings = {
331
+ audio,
332
+ video: video.map(settings => ({
333
+ ...settings,
334
+ ...videoSettings,
335
+ })),
336
+ };
337
+ logger.debug({ scope, settings: videoSettings }, 'get video processor settings');
338
+ return settings;
339
+ },
340
+ }));
341
+ }
342
+ catch (e) {
343
+ if (e instanceof Error) {
344
+ options.onError?.(e);
345
+ }
346
+ return media;
347
+ }
348
+ };
349
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pexip/media",
3
- "version": "17.2.0",
3
+ "version": "17.4.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.mjs",
14
+ "module": "dist/index.js",
15
15
  "types": "dist/index.d.ts",
16
16
  "directories": {
17
17
  "lib": "lib",
@@ -32,16 +32,16 @@
32
32
  "typecheck": "yarn tsc --noEmit -p ."
33
33
  },
34
34
  "dependencies": {
35
- "@pexip/media-control": "17.2.0",
36
- "@pexip/media-processor": "17.2.0",
37
- "@pexip/signal": "16.6.0",
38
- "@pexip/utils": "16.8.0"
35
+ "@pexip/media-control": "17.4.0",
36
+ "@pexip/media-processor": "17.4.0",
37
+ "@pexip/signal": "16.7.0",
38
+ "@pexip/utils": "16.10.0"
39
39
  },
40
40
  "devDependencies": {
41
- "@jest/globals": "^29.5.0",
42
- "@pexip/bundler": "16.4.1",
43
- "prettier": "^3.0.3",
44
- "typescript": "~5.0.0"
41
+ "@jest/globals": "^29.7.0",
42
+ "@pexip/bundler": "16.6.0",
43
+ "prettier": "^3.2.5",
44
+ "typescript": "~5.3.0"
45
45
  },
46
46
  "publishConfig": {
47
47
  "access": "public",