@pexip/media 19.1.0 → 20.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/dist/utils.js CHANGED
@@ -3,22 +3,22 @@ import { calculateMaxBlurPass } from '@pexip/media-processor';
3
3
  import { hasOwn, assert, isEmpty } from '@pexip/utils';
4
4
  import { internalSignals } from './signals';
5
5
  export const makeDeriveDeviceStatus = (constraints) => (audio, video, both) => {
6
- if (constraints.audio === undefined || constraints.audio) {
7
- if (constraints.video === undefined || constraints.video) {
6
+ if (constraints.audio) {
7
+ if (constraints.video) {
8
8
  return both;
9
9
  }
10
10
  return audio;
11
11
  }
12
- if (constraints.video === undefined || constraints.video) {
12
+ if (constraints.video) {
13
13
  return video;
14
14
  }
15
15
  return both;
16
16
  };
17
17
  export const createMediaTrack = (trackInit) => {
18
- assert(trackInit.track === undefined ||
18
+ const trackConsistency = trackInit.track === undefined ||
19
19
  (trackInit.kind === trackInit.input?.kind &&
20
- trackInit.kind === toMediaDeviceInputKind(trackInit.track),
21
- 'Inconsistent track kind'));
20
+ trackInit.kind === toMediaDeviceInputKind(trackInit.track));
21
+ assert(trackConsistency, `Inconsistent track kind: ${trackInit.input?.kind} ${trackInit.track && toMediaDeviceInputKind(trackInit.track)} vs ${trackInit.kind}`);
22
22
  const currentConstraints = typeof trackInit.constraints === 'boolean' ? {} : trackInit.constraints;
23
23
  // Find the original source track through the linked list
24
24
  let sourceMediaTrack = trackInit.previousMediaTrack;
@@ -52,7 +52,9 @@ export const createMediaTrack = (trackInit) => {
52
52
  return trackInit.track;
53
53
  },
54
54
  get input() {
55
- return trackInit.input;
55
+ return trackInit.track?.readyState !== 'live'
56
+ ? undefined
57
+ : trackInit.input;
56
58
  },
57
59
  get expectedInput() {
58
60
  return trackInit.expectedInput;
@@ -89,11 +91,12 @@ export const createMediaTrack = (trackInit) => {
89
91
  getConstraints() {
90
92
  return getConstraints();
91
93
  },
92
- clone() {
94
+ clone(signals) {
93
95
  return createMediaTrack({
94
96
  ...trackInit,
95
97
  constraints: getConstraints(),
96
98
  track: trackInit.track?.clone(),
99
+ signals,
97
100
  });
98
101
  },
99
102
  async applyConstraints(constraints) {
@@ -157,6 +160,9 @@ export const createMediaTrack = (trackInit) => {
157
160
  export const createTrackProcessingPipeline = (processors) => async (track) => {
158
161
  let lastTrack = track;
159
162
  for (const process of processors) {
163
+ if (lastTrack.track?.readyState !== 'live') {
164
+ return lastTrack;
165
+ }
160
166
  lastTrack = await process(lastTrack);
161
167
  }
162
168
  return lastTrack;
@@ -188,7 +194,7 @@ export const findExpectedInput = (devices, constraints, input, kind) => {
188
194
  * A utility function to check if the provided track is muted. There are
189
195
  * 2 factors to be considered: `MediaStreamTrack['muted']` and `MediaStreamTrack['enabled']`.
190
196
  *
191
- * ```
197
+ * ```md
192
198
  * | muted \ enabled | true | false |
193
199
  * |-----------------| ----- | ----- |
194
200
  * | true | true | true |
@@ -335,23 +341,23 @@ export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevic
335
341
  return props.originalConstraints;
336
342
  },
337
343
  isAudioInputUnavailable() {
338
- return (mediaInit.permission.audio === 'granted' &&
339
- props.devices.size('audioinput') === 0);
344
+ return !props.devices.anyAuthorizedDevice('audioinput');
340
345
  },
341
346
  isVideoInputUnavailable() {
342
- return (mediaInit.permission.video === 'granted' &&
343
- props.devices.size('videoinput') === 0);
347
+ return !props.devices.anyAuthorizedDevice('videoinput');
344
348
  },
345
349
  setOriginalConstraints(constraints) {
346
350
  props.originalConstraints = constraints;
347
351
  },
348
352
  requestedAudio() {
349
353
  return (Boolean(props.audioTrack?.track) ||
350
- Boolean(props.originalConstraints.audio));
354
+ (Boolean(props.originalConstraints.audio) &&
355
+ props.devices.size('audioinput') > 0));
351
356
  },
352
357
  requestedVideo() {
353
358
  return (Boolean(props.videoTrack?.track) ||
354
- Boolean(props.originalConstraints.video));
359
+ (Boolean(props.originalConstraints.video) &&
360
+ props.devices.size('videoinput') > 0));
355
361
  },
356
362
  muteAudio(mute) {
357
363
  if (!props.audioTrack) {
@@ -378,7 +384,9 @@ export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevic
378
384
  async release() {
379
385
  unsubscribe?.();
380
386
  unsubscribe = undefined;
381
- trackSubscriptions.forEach(unsubscribe => unsubscribe());
387
+ for (const unsubscribe of trackSubscriptions.values()) {
388
+ unsubscribe();
389
+ }
382
390
  trackSubscriptions.clear();
383
391
  await Promise.all([
384
392
  props.audioTrack?.release(),
@@ -389,12 +397,12 @@ export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevic
389
397
  audio: props.audioTrack?.getSettings(),
390
398
  video: props.videoTrack?.getSettings(),
391
399
  }),
392
- clone() {
400
+ clone(signals) {
393
401
  const tracks = [props.audioTrack, props.videoTrack].flatMap(track => {
394
402
  if (!track?.source) {
395
403
  return [];
396
404
  }
397
- const cloned = track.source.clone();
405
+ const cloned = track.source.clone(signals);
398
406
  // Restore the enabled state for all cloned track
399
407
  cloned.mute(false);
400
408
  return cloned;
@@ -517,6 +525,11 @@ export const VIDEO_SETTINGS_KEYS = [
517
525
  export const MIXING_SETTINGS_KEYS = [
518
526
  'mixWithAdditionalMedia',
519
527
  ];
528
+ export const PAN_TILT_ZOOM_SETTINGS_KEYS = [
529
+ 'pan',
530
+ 'tilt',
531
+ 'zoom',
532
+ ];
520
533
  export const hasSettingsChanged = (keysToLookFor) => {
521
534
  const cache = {};
522
535
  return (settingsA, settingsB) => {
@@ -545,6 +558,28 @@ export const hasSettingsChanged = (keysToLookFor) => {
545
558
  return cache.result;
546
559
  };
547
560
  };
561
+ export const isApplyingRenderEffect = (effect) => {
562
+ switch (effect) {
563
+ case 'blur':
564
+ case 'overlay':
565
+ return true;
566
+ default:
567
+ return false;
568
+ }
569
+ };
570
+ export const getSettingsFromKeys = (keys, settings) => {
571
+ if (!settings) {
572
+ return settings;
573
+ }
574
+ const result = {};
575
+ for (const key of keys) {
576
+ if (settings[key] !== undefined) {
577
+ // @ts-expect-error --- Type issue and need to be fixed when we have time
578
+ result[key] = settings[key];
579
+ }
580
+ }
581
+ return result;
582
+ };
548
583
  export const diffSettings = (keysToLookFor) => {
549
584
  const cache = {};
550
585
  return (settingsA, settingsB) => {
@@ -560,7 +595,16 @@ export const diffSettings = (keysToLookFor) => {
560
595
  return cache.diff;
561
596
  }
562
597
  if (settingsA === undefined || settingsB === undefined) {
563
- cache.diff = settingsB;
598
+ const settings = settingsA ?? settingsB;
599
+ for (const key of keysToLookFor) {
600
+ if (settings?.[key] !== undefined) {
601
+ if (cache.diff === undefined) {
602
+ cache.diff = {};
603
+ }
604
+ // @ts-expect-error --- Type issue and need to be fixed when we have time
605
+ cache.diff[key] = settings[key];
606
+ }
607
+ }
564
608
  return cache.diff;
565
609
  }
566
610
  for (const key of keysToLookFor) {
@@ -625,6 +669,7 @@ export const refineMediaConstraints = (state) => {
625
669
  switch (state.permission) {
626
670
  case 'denied':
627
671
  return false;
672
+ case 'prompt':
628
673
  case 'granted': {
629
674
  const requestNeeded = shouldRequestDevice({
630
675
  kind: state.kind,
@@ -642,7 +687,6 @@ export const refineMediaConstraints = (state) => {
642
687
  }
643
688
  return state.request;
644
689
  }
645
- case 'prompt':
646
690
  default:
647
691
  return state.request;
648
692
  }
@@ -650,25 +694,25 @@ export const refineMediaConstraints = (state) => {
650
694
  export const createMediaProcessor = ({ audioProcessors, videoProcessors, onProcessingError, }) => {
651
695
  const processAudioTrack = createTrackProcessingPipeline(audioProcessors);
652
696
  const processVideoTrack = createTrackProcessingPipeline(videoProcessors);
653
- return async (newTracks) => {
654
- const processTrack = async (track) => {
655
- try {
656
- switch (track.kind) {
657
- case 'audioinput':
658
- return await processAudioTrack(track);
659
- case 'videoinput':
660
- return await processVideoTrack(track);
661
- default:
662
- assert(false, `Unexpected track kind: ${track.kind}`);
663
- }
697
+ const processTrack = async (track) => {
698
+ try {
699
+ switch (track.kind) {
700
+ case 'audioinput':
701
+ return await processAudioTrack(track);
702
+ case 'videoinput':
703
+ return await processVideoTrack(track);
704
+ default:
705
+ assert(false, `Unexpected track kind: ${track.kind}`);
664
706
  }
665
- catch (error) {
666
- if (error instanceof Error) {
667
- onProcessingError?.(error, track);
668
- }
669
- return track;
707
+ }
708
+ catch (error) {
709
+ if (error instanceof Error) {
710
+ onProcessingError?.(error, track);
670
711
  }
671
- };
712
+ return track;
713
+ }
714
+ };
715
+ return async (newTracks) => {
672
716
  return await Promise.all(newTracks.map(track => processTrack(track)));
673
717
  };
674
718
  };
@@ -1,4 +1,4 @@
1
- import type { VideoProcessor, SegmentationTransform, SegmentationModel } from '@pexip/media-processor';
1
+ import type { VideoProcessor, SegmentationTransform, SegmentationModel, RenderBackend } from '@pexip/media-processor';
2
2
  import type { MediaDeviceRequest } from '@pexip/media-control';
3
3
  import type { TrackProcessor, VideoRenderParams, Segmenters, VideoStreamTrackProcessorAPIs, VideoContentHint } from './types';
4
4
  interface ProcessorDeps {
@@ -24,6 +24,8 @@ interface VideoStreamProcessOptions extends Partial<VideoRenderParams>, Omit<Pro
24
24
  width?: number;
25
25
  height?: number;
26
26
  label?: string;
27
+ dynamicProcessingDimensions?: () => boolean;
28
+ gpuAPI?: () => RenderBackend;
27
29
  }
28
30
  interface VideoStreamProcessProps extends Partial<VideoRenderParams>, Required<ProcessorDeps> {
29
31
  hasInitialized: boolean;
@@ -32,6 +34,6 @@ interface VideoStreamProcessProps extends Partial<VideoRenderParams>, Required<P
32
34
  declare const FEATURE_KEYS: readonly ["backgroundBlurAmount", "backgroundImageUrl", "maskCombineRatio", "edgeBlurAmount", "foregroundThreshold", "frameRate", "videoSegmentation", "videoSegmentationModel", "width", "height", "pan", "tilt", "zoom", "contentHint"];
33
35
  type FeaturePropKeys = (typeof FEATURE_KEYS)[number];
34
36
  type FeatureProps = Pick<Partial<VideoStreamProcessProps>, FeaturePropKeys>;
35
- export declare const updateFeatureProps: (constraints: MediaDeviceRequest['video'], props: FeatureProps) => FeatureProps;
36
- export declare const createVideoStreamProcess: ({ trackProcessorAPI, processingWidth, processingHeight, shouldEnable, frameRate, videoSegmentation, foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, label, ...options }: VideoStreamProcessOptions) => TrackProcessor;
37
+ export declare const updateFeatureProps: (constraints: MediaDeviceRequest["video"], props: FeatureProps) => FeatureProps;
38
+ export declare const createVideoStreamProcess: ({ trackProcessorAPI, processingWidth, processingHeight, shouldEnable, frameRate, videoSegmentation, foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, label, dynamicProcessingDimensions, gpuAPI, ...options }: VideoStreamProcessOptions) => TrackProcessor;
37
39
  export {};
@@ -30,7 +30,8 @@ export const updateFeatureProps = (constraints, props) => {
30
30
  const [[feature = ''] = []] = extracted[key];
31
31
  if (isVideoContentHint(feature) && props[key] !== feature) {
32
32
  props[key] = feature;
33
- return { ...accm, [key]: feature };
33
+ accm[key] = feature;
34
+ return accm;
34
35
  }
35
36
  return accm;
36
37
  }
@@ -38,7 +39,8 @@ export const updateFeatureProps = (constraints, props) => {
38
39
  const [[feature] = []] = extracted[key];
39
40
  if (isRenderEffects(feature) && props[key] !== feature) {
40
41
  props[key] = feature;
41
- return { ...accm, [key]: feature };
42
+ accm[key] = feature;
43
+ return accm;
42
44
  }
43
45
  return accm;
44
46
  }
@@ -46,7 +48,8 @@ export const updateFeatureProps = (constraints, props) => {
46
48
  const [[feature] = []] = extracted[key];
47
49
  if (isSegmentationModel(feature) && props[key] !== feature) {
48
50
  props[key] = feature;
49
- return { ...accm, [key]: feature };
51
+ accm[key] = feature;
52
+ return accm;
50
53
  }
51
54
  return accm;
52
55
  }
@@ -54,7 +57,8 @@ export const updateFeatureProps = (constraints, props) => {
54
57
  const [[feature] = []] = extracted[key];
55
58
  if (feature) {
56
59
  props[key] = feature;
57
- return { ...accm, [key]: feature };
60
+ accm[key] = feature;
61
+ return accm;
58
62
  }
59
63
  return accm;
60
64
  }
@@ -70,10 +74,8 @@ export const updateFeatureProps = (constraints, props) => {
70
74
  const value = getValueFromConstrainNumber(feature);
71
75
  if (props[key] !== value) {
72
76
  props[key] = value;
73
- return {
74
- ...accm,
75
- [key]: value,
76
- };
77
+ accm[key] = value;
78
+ return accm;
77
79
  }
78
80
  }
79
81
  return accm;
@@ -84,7 +86,8 @@ export const updateFeatureProps = (constraints, props) => {
84
86
  const [feature] = extracted[key];
85
87
  if (feature !== undefined && props[key] !== feature) {
86
88
  props[key] = feature;
87
- return { ...accm, [key]: feature };
89
+ accm[key] = feature;
90
+ return accm;
88
91
  }
89
92
  return accm;
90
93
  }
@@ -92,7 +95,7 @@ export const updateFeatureProps = (constraints, props) => {
92
95
  }, {});
93
96
  };
94
97
  const applyFeatures = (transformer, features) => {
95
- Object.keys(features).forEach(key => {
98
+ for (const key in features) {
96
99
  const k = key;
97
100
  switch (k) {
98
101
  case 'edgeBlurAmount':
@@ -102,66 +105,30 @@ const applyFeatures = (transformer, features) => {
102
105
  if (value !== undefined) {
103
106
  transformer[k] = value;
104
107
  }
105
- return;
108
+ break;
106
109
  }
107
110
  case 'backgroundBlurAmount': {
108
111
  const value = features[k];
109
112
  if (value !== undefined) {
110
113
  transformer[k] = getBlurKernelSize(value, transformer.height);
111
114
  }
112
- return;
115
+ break;
113
116
  }
114
117
  case 'videoSegmentation': {
115
118
  const value = features[k];
116
119
  if (value && value !== transformer.effects) {
117
120
  transformer.effects = value;
118
121
  }
119
- return;
122
+ break;
120
123
  }
121
124
  case 'backgroundImageUrl': {
122
125
  const value = features[k];
123
126
  if (value && value !== transformer.backgroundImageUrl) {
124
127
  transformer.backgroundImageUrl = value;
125
128
  }
126
- return;
127
- }
128
- default: {
129
- return;
130
- }
131
- }
132
- });
133
- };
134
- const adjustResolution = async (track, features, processingSize) => {
135
- if (features.videoSegmentation) {
136
- const videoSettings = track.getSettings();
137
- const constraints = updateFeatureProps(track.getConstraints(), {});
138
- switch (features.videoSegmentation) {
139
- case 'blur':
140
- case 'overlay': {
141
- if (videoSettings?.height !== processingSize.height) {
142
- try {
143
- await track.applyConstraints({
144
- width: processingSize.width,
145
- height: processingSize.height,
146
- });
147
- const postVideoSettings = track.getSettings();
148
- if (postVideoSettings?.height !== processingSize.height) {
149
- // Workaround Firefox 16:9 ratio https://bugzilla.mozilla.org/show_bug.cgi?id=1193640
150
- await track.applyConstraints({ height: 720 });
151
- }
152
- }
153
- catch (error) {
154
- // Workaround Firefox 16:9 ratio https://bugzilla.mozilla.org/show_bug.cgi?id=1193640
155
- await track.applyConstraints({ height: 720 });
156
- }
157
- }
158
129
  break;
159
130
  }
160
- case 'none': {
161
- if (constraints.height &&
162
- constraints.height !== videoSettings?.height) {
163
- await track.applyConstraints({ height: constraints.height });
164
- }
131
+ default: {
165
132
  break;
166
133
  }
167
134
  }
@@ -178,7 +145,8 @@ export const createVideoStreamProcess = ({ trackProcessorAPI = () => 'stream', p
178
145
  //backgroundBlurAmount,
179
146
  videoSegmentation,
180
147
  //edgeBlurAmount,
181
- foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, label = PROCESSOR_LABELS.VideoProcessor, ...options }) => {
148
+ foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, label = PROCESSOR_LABELS.VideoProcessor, dynamicProcessingDimensions = () => false, gpuAPI = () => 'webgl', // dynamically changing the config will not be picked up, not a priority for now
149
+ ...options }) => {
182
150
  const videoSegmentationModel = options.videoSegmentationModel ?? 'selfie';
183
151
  const segmenter = options.segmenters[videoSegmentationModel];
184
152
  if (!segmenter) {
@@ -235,14 +203,34 @@ foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, label
235
203
  return prevMediaTrack;
236
204
  }
237
205
  if (!props.hasInitialized) {
238
- await props.videoProcessor().open();
206
+ if (dynamicProcessingDimensions()) {
207
+ const { width, height } = prevMediaTrack.getSettings();
208
+ /**
209
+ * when the processor is closed, the segmenter is not killed, but the track is released.
210
+ * this means we can't listen on the track settings changes when we change quality
211
+ * we need to update instead of open if the segmenter is already running
212
+ */
213
+ if (props.transformer.segmenter.status === 'new') {
214
+ await props.videoProcessor().open({
215
+ processingWidth: width,
216
+ processingHeight: height,
217
+ });
218
+ props.transformer.backend = gpuAPI();
219
+ }
220
+ else {
221
+ props.transformer.update({
222
+ processingWidth: width,
223
+ processingHeight: height,
224
+ });
225
+ }
226
+ }
227
+ else {
228
+ await props.videoProcessor().open();
229
+ props.transformer.backend = gpuAPI();
230
+ }
239
231
  props.hasInitialized = true;
240
232
  }
241
233
  applyFeatures(props.transformer, features);
242
- await adjustResolution(prevMediaTrack, features, {
243
- width: processingWidth,
244
- height: processingHeight,
245
- });
246
234
  const model = features.videoSegmentationModel &&
247
235
  props.segmenters[features.videoSegmentationModel];
248
236
  if (features.videoSegmentationModel &&
@@ -292,10 +280,15 @@ foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, label
292
280
  return;
293
281
  }
294
282
  applyFeatures(props.transformer, features);
295
- await adjustResolution(prevMediaTrack, features, {
296
- width: processingWidth,
297
- height: processingHeight,
298
- });
283
+ if (dynamicProcessingDimensions()) {
284
+ const acceptedSettings = prevMediaTrack.source.getSettings();
285
+ if (acceptedSettings.width && acceptedSettings.height) {
286
+ props.transformer.update({
287
+ processingHeight: acceptedSettings.height,
288
+ processingWidth: acceptedSettings.width,
289
+ });
290
+ }
291
+ }
299
292
  const model = features.videoSegmentationModel &&
300
293
  props.segmenters[features.videoSegmentationModel];
301
294
  if (model &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pexip/media",
3
- "version": "19.1.0",
3
+ "version": "20.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",
@@ -14,6 +14,16 @@
14
14
  "author": "Terrence Lam <terrence@tlam.dev>",
15
15
  "module": "dist/index.js",
16
16
  "types": "dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./*": {
23
+ "types": "./dist/*.d.ts",
24
+ "import": "./dist/*.js"
25
+ }
26
+ },
17
27
  "directories": {
18
28
  "lib": "lib",
19
29
  "test": "__tests__"
@@ -23,30 +33,31 @@
23
33
  ],
24
34
  "scripts": {
25
35
  "build": "pexip-bundler build",
36
+ "build:sourcemap": "pexip-bundler build --sourcemap",
26
37
  "clean": "rm -fr dist",
27
38
  "dev": "pexip-bundler dev",
28
- "prepack": "yarn clean && yarn build --no-sourcemap",
39
+ "prepack": "yarn clean && yarn build",
29
40
  "test": "jest",
30
41
  "check-format": "prettier --ignore-path=../../.prettierignore --check .",
31
42
  "format": "prettier --ignore-path=../../.prettierignore --write .",
32
- "lint": "yarn run -T eslint --ext .js,.ts,.tsx --report-unused-disable-directives .",
43
+ "lint": "yarn run -T biome lint",
33
44
  "typecheck": "yarn tsc --noEmit -p ."
34
45
  },
35
46
  "dependencies": {
36
- "@pexip/media-control": "19.1.0",
37
- "@pexip/media-processor": "19.1.0",
38
- "@pexip/signal": "16.7.1",
39
- "@pexip/utils": "16.13.0"
47
+ "@pexip/media-control": "20.0.0",
48
+ "@pexip/media-processor": "20.0.0",
49
+ "@pexip/signal": "16.8.0",
50
+ "@pexip/utils": "17.0.0"
40
51
  },
41
52
  "devDependencies": {
42
53
  "@jest/globals": "^29.7.0",
43
- "@pexip/bundler": "17.1.0",
44
- "@swc/core": "^1.4.6",
45
- "@swc/jest": "^0.2.36",
54
+ "@pexip/bundler": "18.0.0",
55
+ "@swc/core": "^1.10.12",
56
+ "@swc/jest": "^0.2.37",
46
57
  "jest": "^29.5.12",
47
58
  "jest-junit": "^16.0.0",
48
59
  "prettier": "^3.2.5",
49
- "typescript": "~5.3.0"
60
+ "typescript": "~5.7.3"
50
61
  },
51
62
  "publishConfig": {
52
63
  "access": "public",