@pexip/media 18.5.0 → 19.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.
@@ -1,6 +1,6 @@
1
1
  import type { VideoProcessor, SegmentationTransform, SegmentationModel } from '@pexip/media-processor';
2
2
  import type { MediaDeviceRequest } from '@pexip/media-control';
3
- import type { Process, Media, VideoRenderParams, Segmenters, VideoStreamTrackProcessorAPIs, VideoContentHint } from './types';
3
+ import type { TrackProcessor, VideoRenderParams, Segmenters, VideoStreamTrackProcessorAPIs, VideoContentHint } from './types';
4
4
  interface ProcessorDeps {
5
5
  videoProcessor?: () => VideoProcessor;
6
6
  transformer?: SegmentationTransform;
@@ -18,16 +18,12 @@ interface VideoStreamProcessOptions extends Partial<VideoRenderParams>, Omit<Pro
18
18
  * Whether or to enable this processor
19
19
  */
20
20
  shouldEnable: () => boolean;
21
- /**
22
- * Callback when error occurs
23
- */
24
- onError?: (error: Error) => void;
25
21
  processingWidth: number;
26
22
  processingHeight: number;
27
23
  hasInitializedDeps?: boolean;
28
24
  width?: number;
29
25
  height?: number;
30
- scope?: string;
26
+ label?: string;
31
27
  }
32
28
  interface VideoStreamProcessProps extends Partial<VideoRenderParams>, Required<ProcessorDeps> {
33
29
  hasInitialized: boolean;
@@ -37,5 +33,5 @@ declare const FEATURE_KEYS: readonly ["backgroundBlurAmount", "backgroundImageUr
37
33
  type FeaturePropKeys = (typeof FEATURE_KEYS)[number];
38
34
  type FeatureProps = Pick<Partial<VideoStreamProcessProps>, FeaturePropKeys>;
39
35
  export declare const updateFeatureProps: (constraints: MediaDeviceRequest['video'], props: FeatureProps) => FeatureProps;
40
- export declare const createVideoStreamProcess: ({ trackProcessorAPI, processingWidth, processingHeight, shouldEnable, frameRate, videoSegmentation, foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, scope, ...options }: VideoStreamProcessOptions) => Process<Promise<Media>>;
36
+ export declare const createVideoStreamProcess: ({ trackProcessorAPI, processingWidth, processingHeight, shouldEnable, frameRate, videoSegmentation, foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, label, ...options }: VideoStreamProcessOptions) => TrackProcessor;
41
37
  export {};
@@ -1,9 +1,10 @@
1
1
  import { createVideoProcessor, createCanvasTransform, createVideoTrackProcessor, createVideoTrackProcessorWithFallback, isRenderEffects, isSegmentationModel, } from '@pexip/media-processor';
2
2
  import { muteStreamTrack, extractConstraintsWithKeys, getValueFromConstrainNumber, } from '@pexip/media-control';
3
- import { isEmpty } from '@pexip/utils';
4
- import { shallowCopy, wrapToJSON, applyExtendedConstraints, getBlurKernelSize, } from './utils';
3
+ import { isEmpty, assert } from '@pexip/utils';
4
+ import { createMediaTrack, getBlurKernelSize } from './utils';
5
5
  import { logger, proxyWithLog } from './logger';
6
6
  import { isVideoContentHint } from './typeGuard';
7
+ import { PROCESSOR_LABELS } from './constants';
7
8
  const FEATURE_KEYS = [
8
9
  'backgroundBlurAmount',
9
10
  'backgroundImageUrl',
@@ -26,7 +27,7 @@ export const updateFeatureProps = (constraints, props) => {
26
27
  return FEATURE_KEYS.reduce((accm, key) => {
27
28
  switch (key) {
28
29
  case 'contentHint': {
29
- const [[feature] = []] = extracted[key];
30
+ const [[feature = ''] = []] = extracted[key];
30
31
  if (isVideoContentHint(feature) && props[key] !== feature) {
31
32
  props[key] = feature;
32
33
  return { ...accm, [key]: feature };
@@ -130,34 +131,28 @@ const applyFeatures = (transformer, features) => {
130
131
  }
131
132
  });
132
133
  };
133
- const adjustResolution = async (media, features, processingSize) => {
134
+ const adjustResolution = async (track, features, processingSize) => {
134
135
  if (features.videoSegmentation) {
135
- const { video: [videoSettings], } = media.getSettings();
136
- const constraints = updateFeatureProps(media.constraints?.video, {});
136
+ const videoSettings = track.getSettings();
137
+ const constraints = updateFeatureProps(track.getConstraints(), {});
137
138
  switch (features.videoSegmentation) {
138
139
  case 'blur':
139
140
  case 'overlay': {
140
141
  if (videoSettings?.height !== processingSize.height) {
141
142
  try {
142
- await media.applyConstraints({
143
- video: {
144
- width: processingSize.width,
145
- height: processingSize.height,
146
- },
143
+ await track.applyConstraints({
144
+ width: processingSize.width,
145
+ height: processingSize.height,
147
146
  });
148
- const { video: [postVideoSettings], } = media.getSettings();
147
+ const postVideoSettings = track.getSettings();
149
148
  if (postVideoSettings?.height !== processingSize.height) {
150
149
  // Workaround Firefox 16:9 ratio https://bugzilla.mozilla.org/show_bug.cgi?id=1193640
151
- await media.applyConstraints({
152
- video: { height: 720 },
153
- });
150
+ await track.applyConstraints({ height: 720 });
154
151
  }
155
152
  }
156
153
  catch (error) {
157
154
  // Workaround Firefox 16:9 ratio https://bugzilla.mozilla.org/show_bug.cgi?id=1193640
158
- await media.applyConstraints({
159
- video: { height: 720 },
160
- });
155
+ await track.applyConstraints({ height: 720 });
161
156
  }
162
157
  }
163
158
  break;
@@ -165,11 +160,7 @@ const adjustResolution = async (media, features, processingSize) => {
165
160
  case 'none': {
166
161
  if (constraints.height &&
167
162
  constraints.height !== videoSettings?.height) {
168
- await media.applyConstraints({
169
- video: {
170
- height: constraints.height,
171
- },
172
- });
163
+ await track.applyConstraints({ height: constraints.height });
173
164
  }
174
165
  break;
175
166
  }
@@ -187,7 +178,7 @@ export const createVideoStreamProcess = ({ trackProcessorAPI = () => 'stream', p
187
178
  //backgroundBlurAmount,
188
179
  videoSegmentation,
189
180
  //edgeBlurAmount,
190
- foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, scope = 'media', ...options }) => {
181
+ foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, label = PROCESSOR_LABELS.VideoProcessor, ...options }) => {
191
182
  const videoSegmentationModel = options.videoSegmentationModel ?? 'selfie';
192
183
  const segmenter = options.segmenters[videoSegmentationModel];
193
184
  if (!segmenter) {
@@ -206,7 +197,7 @@ foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, scope
206
197
  backgroundImageUrl,
207
198
  maskCombineRatio,
208
199
  });
209
- const proxy = proxyWithLog(logger, scope);
200
+ const proxy = proxyWithLog(logger, label);
210
201
  let _videoProcessor = undefined;
211
202
  const props = {
212
203
  videoSegmentationModel,
@@ -236,127 +227,100 @@ foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, scope
236
227
  maskCombineRatio,
237
228
  hasInitialized: options.hasInitializedDeps ?? false,
238
229
  };
239
- return async (mediaP) => {
240
- const media = await mediaP;
241
- const features = updateFeatureProps(media.constraints?.video, props);
230
+ return async (prevMediaTrack) => {
231
+ const features = updateFeatureProps(prevMediaTrack.getConstraints(), props);
242
232
  const shouldEnabled = shouldEnable();
243
- if (!shouldEnabled || !media.stream?.getVideoTracks().length) {
244
- logger.debug({ scope, features, shouldEnabled }, 'Video processing is skipped');
245
- return media;
233
+ if (!shouldEnabled || !prevMediaTrack.track) {
234
+ logger.debug({ label, features, shouldEnabled }, 'Video processing is skipped');
235
+ return prevMediaTrack;
246
236
  }
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)) {
237
+ if (!props.hasInitialized) {
238
+ await props.videoProcessor().open();
239
+ props.hasInitialized = true;
240
+ }
241
+ applyFeatures(props.transformer, features);
242
+ await adjustResolution(prevMediaTrack, features, {
243
+ width: processingWidth,
244
+ height: processingHeight,
245
+ });
246
+ const model = features.videoSegmentationModel &&
247
+ props.segmenters[features.videoSegmentationModel];
248
+ if (features.videoSegmentationModel &&
249
+ features.videoSegmentationModel !==
250
+ props.transformer.segmenter.modelAsset.modelName &&
251
+ model) {
252
+ props.transformer.segmenter = model;
253
+ }
254
+ const stream = await props
255
+ .videoProcessor()
256
+ // FIXME: Change process to accept MediaStreamTrack instead of MediaStream
257
+ .process(new MediaStream([prevMediaTrack.track]));
258
+ const track = stream.getVideoTracks().at(0);
259
+ assert(track, 'Video track should be there');
260
+ // Inherit contentHint from previous track
261
+ track.contentHint = prevMediaTrack.track.contentHint;
262
+ const release = async () => {
263
+ props.videoProcessor().close();
264
+ await prevMediaTrack.release();
265
+ _videoProcessor = undefined;
266
+ props.hasInitialized = false;
267
+ };
268
+ const mute = (mute) => {
269
+ props.transformer.effects = mute
270
+ ? 'none'
271
+ : props.videoSegmentation ?? 'none';
272
+ prevMediaTrack.mute(mute);
273
+ muteStreamTrack(stream)(mute, 'video');
274
+ };
275
+ return createMediaTrack({
276
+ label,
277
+ kind: 'videoinput',
278
+ constraints: prevMediaTrack.getConstraints(),
279
+ previousMediaTrack: prevMediaTrack,
280
+ input: prevMediaTrack.input,
281
+ expectedInput: prevMediaTrack.expectedInput,
282
+ track,
283
+ mute,
284
+ release,
285
+ applyConstraints: async (constraints) => {
286
+ if (isEmpty(constraints)) {
285
287
  return;
286
288
  }
287
- const features = updateFeatureProps(constraints.video, props);
288
- logger.debug({ scope, constraints: constraints.video, features }, 'apply video constraints');
289
+ const features = updateFeatureProps(constraints, props);
290
+ logger.debug({ label, constraints: constraints, features }, 'apply video constraints');
289
291
  if (isEmpty(features)) {
290
292
  return;
291
293
  }
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
- }
294
+ applyFeatures(props.transformer, features);
295
+ await adjustResolution(prevMediaTrack, features, {
296
+ width: processingWidth,
297
+ height: processingHeight,
298
+ });
299
+ const model = features.videoSegmentationModel &&
300
+ props.segmenters[features.videoSegmentationModel];
301
+ if (model &&
302
+ features.videoSegmentationModel !==
303
+ props.transformer.segmenter.modelAsset.modelName) {
304
+ props.transformer.segmenter = model;
316
305
  }
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
- }
306
+ },
307
+ getSettings: () => {
308
+ const contentHint = track.contentHint ?? '';
309
+ return {
310
+ videoSegmentation: transformer.effects,
311
+ foregroundThreshold: transformer.foregroundThreshold,
312
+ // Background blur amount is a function of height
313
+ backgroundBlurAmount: props.backgroundBlurAmount,
314
+ edgeBlurAmount: transformer.edgeBlurAmount,
315
+ maskCombineRatio: transformer.maskCombineRatio,
316
+ backgroundImageUrl: transformer.backgroundImageUrl,
317
+ videoSegmentationModel: transformer.segmenter.modelAsset.modelName,
318
+ pan: props.pan,
319
+ tilt: props.tilt,
320
+ zoom: props.zoom,
321
+ contentHint,
322
+ };
323
+ },
324
+ });
361
325
  };
362
326
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pexip/media",
3
- "version": "18.5.0",
3
+ "version": "19.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",
@@ -33,13 +33,14 @@
33
33
  "typecheck": "yarn tsc --noEmit -p ."
34
34
  },
35
35
  "dependencies": {
36
- "@pexip/media-control": "18.5.0",
37
- "@pexip/media-processor": "18.5.0",
36
+ "@pexip/media-control": "19.0.0",
37
+ "@pexip/media-processor": "19.0.0",
38
38
  "@pexip/signal": "16.7.1",
39
- "@pexip/utils": "16.12.1"
39
+ "@pexip/utils": "16.13.0"
40
40
  },
41
41
  "devDependencies": {
42
- "@pexip/bundler": "17.0.0",
42
+ "@jest/globals": "^29.7.0",
43
+ "@pexip/bundler": "17.1.0",
43
44
  "@swc/core": "^1.4.6",
44
45
  "@swc/jest": "^0.2.36",
45
46
  "jest": "^29.5.12",