@pexip/media-processor 16.7.1 → 17.1.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +1 -7
  3. package/dist/main/audio.d.ts +123 -0
  4. package/dist/main/audio.js +653 -0
  5. package/dist/main/benchUtils.d.ts +12 -0
  6. package/dist/main/benchUtils.js +34 -0
  7. package/dist/main/generator.d.ts +4 -0
  8. package/dist/main/generator.js +4 -0
  9. package/dist/main/index.d.ts +17 -0
  10. package/dist/main/index.js +17 -0
  11. package/dist/main/math.d.ts +41 -0
  12. package/dist/main/math.js +57 -0
  13. package/dist/main/path.d.ts +102 -0
  14. package/dist/main/path.js +103 -0
  15. package/dist/main/process.d.ts +239 -0
  16. package/dist/main/process.js +364 -0
  17. package/dist/main/processor.d.ts +5 -0
  18. package/dist/main/processor.js +4 -0
  19. package/dist/main/transformer.d.ts +1 -0
  20. package/dist/main/transformer.js +4 -0
  21. package/dist/main/tsconfig.tsbuildinfo +1 -0
  22. package/dist/main/typeGuards.d.ts +5 -0
  23. package/dist/main/typeGuards.js +24 -0
  24. package/dist/main/types.d.ts +342 -0
  25. package/dist/main/types.js +1 -0
  26. package/dist/main/utils.d.ts +173 -0
  27. package/dist/main/utils.js +364 -0
  28. package/dist/main/video/canvasRenderUtils.d.ts +22 -0
  29. package/dist/main/video/canvasRenderUtils.js +198 -0
  30. package/dist/main/video/canvasTransform.d.ts +8 -0
  31. package/dist/main/video/canvasTransform.js +173 -0
  32. package/dist/main/video/constants.d.ts +10 -0
  33. package/dist/main/video/constants.js +12 -0
  34. package/dist/main/video/index.d.ts +9 -0
  35. package/dist/main/video/index.js +9 -0
  36. package/dist/main/video/load.d.ts +17 -0
  37. package/dist/main/video/load.js +47 -0
  38. package/dist/main/video/segmenters/index.d.ts +1 -0
  39. package/dist/main/video/segmenters/index.js +1 -0
  40. package/dist/main/video/segmenters/mediapipe.d.ts +13 -0
  41. package/dist/main/video/segmenters/mediapipe.js +85 -0
  42. package/dist/main/video/transformer.d.ts +10 -0
  43. package/dist/main/video/transformer.js +56 -0
  44. package/dist/main/video/typeGuards.d.ts +2 -0
  45. package/dist/main/video/typeGuards.js +13 -0
  46. package/dist/main/video/types.d.ts +98 -0
  47. package/dist/main/video/types.js +20 -0
  48. package/dist/main/video/utils.d.ts +102 -0
  49. package/dist/main/video/utils.js +476 -0
  50. package/dist/main/video/video.d.ts +19 -0
  51. package/dist/main/video/video.js +48 -0
  52. package/dist/main/video/videoStreamTrackProcessor.d.ts +14 -0
  53. package/dist/main/video/videoStreamTrackProcessor.js +82 -0
  54. package/dist/main/visual.d.ts +80 -0
  55. package/dist/main/visual.js +135 -0
  56. package/dist/main/workletNodes.d.ts +2 -0
  57. package/dist/main/workletNodes.js +3 -0
  58. package/dist/workers/index.d.ts +0 -0
  59. package/dist/workers/index.js +1 -0
  60. package/dist/workers/tsconfig.tsbuildinfo +1 -0
  61. package/dist/worklets/denoise.worklet.d.ts +1 -0
  62. package/dist/worklets/denoise.worklet.js +1 -2
  63. package/dist/worklets/tsconfig.tsbuildinfo +1 -0
  64. package/dist/worklets/types.d.ts +52 -0
  65. package/dist/worklets/types.js +0 -0
  66. package/package.json +11 -9
  67. package/dist/index.d.ts +0 -1129
  68. package/dist/index.mjs +0 -2441
  69. package/dist/worklets/denoise.worklet.js.map +0 -7
@@ -0,0 +1,364 @@
1
+ export const hasCreateGain = (context) => typeof context.createGain !== 'undefined';
2
+ export const hasAudioWorkletNode = () => typeof AudioWorkletNode !== 'undefined';
3
+ export const hasAudioWorklet = () => typeof AudioWorklet !== 'undefined' && hasAudioWorkletNode();
4
+ const stopTrack = (track) => track.stop();
5
+ export const stopStreamTracks = (stream) => stream?.getTracks().forEach(stopTrack);
6
+ /**
7
+ * A function to create `MediaStreamAudioSourceNode` using constructor or factory
8
+ * function depends on the browser supports
9
+ *
10
+ * @param context - @see {@link AudioContext}
11
+ * @param options - @see {@link MediaStreamAudioSourceOptions}
12
+ *
13
+ * @internal
14
+ */
15
+ export const createMediaStreamAudioSourceNode = (context, options) => {
16
+ try {
17
+ const source = new MediaStreamAudioSourceNode(context, options);
18
+ return source;
19
+ }
20
+ catch {
21
+ return context.createMediaStreamSource(options.mediaStream);
22
+ }
23
+ };
24
+ /**
25
+ * A function to create `MediaStreamAudioSourceNode` using constructor or factory
26
+ * function depends on the browser supports
27
+ *
28
+ * @param context - @see {@link AudioContext}
29
+ * @param options - @see {@link MediaStreamAudioSourceOptions}
30
+ *
31
+ * @internal
32
+ */
33
+ export const createMediaElementSourceNode = (context, options) => {
34
+ try {
35
+ const source = new MediaElementAudioSourceNode(context, options);
36
+ return source;
37
+ }
38
+ catch {
39
+ return context.createMediaElementSource(options.mediaElement);
40
+ }
41
+ };
42
+ /**
43
+ * A function to set AudioNodeOptions accordingly
44
+ */
45
+ const setAudioNodeOptions = (node, options) => {
46
+ if (options?.channelCount) {
47
+ node.channelCount = options.channelCount;
48
+ }
49
+ if (options?.channelCountMode) {
50
+ node.channelCountMode = options.channelCountMode;
51
+ }
52
+ if (options?.channelInterpretation) {
53
+ node.channelInterpretation = options.channelInterpretation;
54
+ }
55
+ };
56
+ /**
57
+ * A function to create `AnalyserNode` using constructor or factory
58
+ * function depends on the browser supports
59
+ *
60
+ * @param audioContext - @see {@link AudioContext}
61
+ * @param options - @see {@link AnalyserOptions}
62
+ *
63
+ * @internal
64
+ */
65
+ export const createAnalyserNode = (audioContext, options) => {
66
+ try {
67
+ const analyser = new AnalyserNode(audioContext, options);
68
+ return analyser;
69
+ }
70
+ catch {
71
+ const analyser = audioContext.createAnalyser();
72
+ options?.fftSize && (analyser.fftSize = options.fftSize);
73
+ options?.maxDecibels && (analyser.maxDecibels = options.maxDecibels);
74
+ options?.minDecibels && (analyser.minDecibels = options.minDecibels);
75
+ options?.smoothingTimeConstant &&
76
+ (analyser.smoothingTimeConstant = options.smoothingTimeConstant);
77
+ setAudioNodeOptions(analyser, options);
78
+ return analyser;
79
+ }
80
+ };
81
+ /**
82
+ * A function to create `GainNode` using constructor or factory
83
+ * function depends on the browser supports
84
+ *
85
+ * @param context - @see {@link AudioContext}
86
+ * @param options - @see {@link GainOptions}
87
+ *
88
+ * @internal
89
+ */
90
+ export const createGainNode = (context, options) => {
91
+ try {
92
+ const volume = new GainNode(context, options);
93
+ return volume;
94
+ }
95
+ catch {
96
+ const volume = hasCreateGain(context)
97
+ ? context.createGain()
98
+ : context.createGainNode();
99
+ if (options?.gain) {
100
+ volume.gain.setValueAtTime(options.gain, context.currentTime);
101
+ }
102
+ setAudioNodeOptions(volume, options);
103
+ return volume;
104
+ }
105
+ };
106
+ /**
107
+ * A function to clone the Audio Track
108
+ *
109
+ * @param stream - Stream to be cloned
110
+ *
111
+ * @internal
112
+ */
113
+ export const createMediaStreamAudioClone = (stream) => {
114
+ try {
115
+ const mediaStream = new MediaStream(stream.getAudioTracks().map(track => track.clone()));
116
+ return mediaStream;
117
+ }
118
+ catch {
119
+ return stream.clone();
120
+ }
121
+ };
122
+ /**
123
+ * A function to create `MediaStreamAudioDestinationNode` using constructor or
124
+ * factory function depends on the browser supports
125
+ *
126
+ * @param context - @see {@link AudioContext}
127
+ * @param options - @see {@link AudioNodeOptions}
128
+ *
129
+ * @internal
130
+ */
131
+ export const createMediaStreamAudioDestinationNode = (context, options) => {
132
+ try {
133
+ const destination = new MediaStreamAudioDestinationNode(context, options);
134
+ return destination;
135
+ }
136
+ catch {
137
+ const destination = context.createMediaStreamDestination();
138
+ setAudioNodeOptions(destination, options);
139
+ return destination;
140
+ }
141
+ };
142
+ /**
143
+ * A function to create `DelayNode` using constructor or
144
+ * factory function depends on the browser supports
145
+ *
146
+ * @param context - @see {@link AudioContext}
147
+ * @param options - @see {@link DelayOptions}
148
+ *
149
+ * @internal
150
+ */
151
+ export const createDelayNode = (context, options) => {
152
+ try {
153
+ const delay = new DelayNode(context, options);
154
+ return delay;
155
+ }
156
+ catch {
157
+ const delay = context.createDelay(options?.maxDelayTime);
158
+ if (options?.delayTime !== undefined) {
159
+ delay.delayTime.setValueAtTime(options?.delayTime, context.currentTime);
160
+ }
161
+ setAudioNodeOptions(delay, options);
162
+ return delay;
163
+ }
164
+ };
165
+ /**
166
+ * A function to create `ChannelSplitterNode` using constructor or
167
+ * factory function depends on the browser supports
168
+ *
169
+ * @param context - @see {@link AudioContext}
170
+ * @param options - @see {@link ChannelSplitterOptions}
171
+ *
172
+ * @internal
173
+ */
174
+ export const createChannelSplitterNode = (context, options) => {
175
+ try {
176
+ const node = new ChannelSplitterNode(context, options);
177
+ return node;
178
+ }
179
+ catch {
180
+ const node = context.createChannelSplitter(options?.numberOfOutputs);
181
+ setAudioNodeOptions(node, options);
182
+ return node;
183
+ }
184
+ };
185
+ /**
186
+ * A function to create `ChannelMergerNode` using constructor or
187
+ * factory function depends on the browser supports
188
+ *
189
+ * @param context - @see {@link AudioContext}
190
+ * @param options - @see {@link ChannelSplitterOptions}
191
+ *
192
+ * @internal
193
+ */
194
+ export const createChannelMergerNode = (context, options) => {
195
+ try {
196
+ const node = new ChannelMergerNode(context, options);
197
+ return node;
198
+ }
199
+ catch {
200
+ const node = context.createChannelMerger(options?.numberOfInputs);
201
+ setAudioNodeOptions(node, options);
202
+ return node;
203
+ }
204
+ };
205
+ /**
206
+ * Map mute value to gain value
207
+ *
208
+ * ```
209
+ * `true` -> 0
210
+ * `false` -> 1
211
+ * ```
212
+ */
213
+ export const muteToGain = (mute) => (mute ? 0 : 1);
214
+ /**
215
+ * Calculate the timeout based on the provided data and returns a timeout in
216
+ * milliseconds with compensation added
217
+ *
218
+ * @param targetTime - The target timeout after the compensation
219
+ * @param startTime - The start time of the last execution
220
+ * @param endTime - The end time of the last execution
221
+ */
222
+ export const calculateNextTimeout = (targetTime, startTime, endTime) => Math.max(targetTime - Math.max(endTime - startTime, 0), 0);
223
+ /**
224
+ * Convert a callback to an async callback with delay added
225
+ *
226
+ * @param callback - The callback to be delayed
227
+ * @param options - The options to inject dependences
228
+ *
229
+ * @example
230
+ *
231
+ * ```typescript
232
+ * const getRandom = () => Math.random();
233
+ * const [delayGetRandom, cancelDelayGetRandom] = createDelayedCallback(getRandom);
234
+ *
235
+ * // Delay 500 ms to get the random number
236
+ * const random = await delayGetRandom(500);
237
+ * ```
238
+ */
239
+ export const createDelayedCallback = (callback, { setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, } = {}) => {
240
+ const props = {
241
+ timeoutID: 0,
242
+ };
243
+ const cancelTimeout = () => {
244
+ if (props.timeoutID) {
245
+ clearTimeout(props.timeoutID);
246
+ props.timeoutID = 0;
247
+ }
248
+ };
249
+ const delayedCallback = async (delayMs, ...params) => {
250
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- void
251
+ const resolved = await new Promise(resolve => {
252
+ cancelTimeout();
253
+ props.timeoutID = setTimeout(() => {
254
+ const result = callback(...params);
255
+ if (result instanceof Promise) {
256
+ result
257
+ .then(resolved => resolve(resolved))
258
+ .catch(e => {
259
+ throw e;
260
+ });
261
+ }
262
+ else {
263
+ resolve(result);
264
+ }
265
+ }, delayMs);
266
+ props.cancel = resolve;
267
+ });
268
+ return resolved;
269
+ };
270
+ const cancel = () => {
271
+ cancelTimeout();
272
+ props.cancel?.();
273
+ };
274
+ return [delayedCallback, cancel];
275
+ };
276
+ /**
277
+ * Convert the rate to milliseconds
278
+ */
279
+ const rateToMs = (rate) => Math.ceil(1000 / rate);
280
+ /**
281
+ * Create an async callback loop to be called recursively with delay based on
282
+ * the `frameRate`
283
+ *
284
+ * @param callback - The callback to be invoked
285
+ * @param frameRate - The rate to be expected to invoke the `callback`
286
+ */
287
+ export const createAsyncCallbackLoop = (callback, frameRate, { setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, now = () => performance.now(), } = {}) => {
288
+ const props = {
289
+ frameRate,
290
+ targetMs: rateToMs(frameRate),
291
+ prevCalledMs: 0,
292
+ timeoutID: 0,
293
+ stopped: false,
294
+ };
295
+ const [delayedCallback, cancel] = createDelayedCallback(callback, {
296
+ setTimeout,
297
+ clearTimeout,
298
+ });
299
+ const fork = async (...params) => {
300
+ if (props.stopped) {
301
+ return;
302
+ }
303
+ const currentMs = now();
304
+ const nextMs = calculateNextTimeout(props.targetMs, props.prevCalledMs, currentMs);
305
+ props.prevCalledMs = currentMs;
306
+ await delayedCallback(nextMs, ...params);
307
+ await fork(...params);
308
+ };
309
+ return {
310
+ start: async (...params) => {
311
+ props.prevCalledMs = now();
312
+ props.stopped = false;
313
+ await delayedCallback(0, ...params);
314
+ void fork(...params);
315
+ },
316
+ stop: () => {
317
+ props.stopped = true;
318
+ cancel();
319
+ },
320
+ get frameRate() {
321
+ return props.frameRate;
322
+ },
323
+ set frameRate(value) {
324
+ props.frameRate = value;
325
+ props.targetMs = rateToMs(value);
326
+ },
327
+ };
328
+ };
329
+ export const DEFAULT_THROTTLE_MS = 3000;
330
+ /**
331
+ * A function to limit the provided callback being called too frequently, and
332
+ * assuming the function is called repeatably, and NOT for general purpose.
333
+ *
334
+ * @param callback - the callback to be called under the specified time
335
+ * @param throttleMs - the specified time for throttling
336
+ * @param clock - how to get the current time
337
+ */
338
+ export const throttleProcess = (callback, throttleMs = DEFAULT_THROTTLE_MS, clock = performance) => {
339
+ let lastCall = 0;
340
+ return (...params) => {
341
+ const now = clock.now();
342
+ if (now - lastCall >= throttleMs) {
343
+ callback(...params);
344
+ lastCall = now;
345
+ }
346
+ };
347
+ };
348
+ /**
349
+ * Subscribe visibilitychange event
350
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event}
351
+ *
352
+ * @param callback - A callback to be called with `document.hidden` when the event is trigger
353
+ */
354
+ export const subscribeVisibilityChangeEvent = (callback) => {
355
+ const handleEvent = () => {
356
+ callback(document.hidden).catch(error => {
357
+ throw error;
358
+ });
359
+ };
360
+ document.addEventListener('visibilitychange', handleEvent);
361
+ return () => {
362
+ document.removeEventListener('visibilitychange', handleEvent);
363
+ };
364
+ };
@@ -0,0 +1,22 @@
1
+ /// <reference types="dom-webcodecs" />
2
+ import type { Canvas } from '../types';
3
+ import type { ProcessInputType, Segmentation, ImageType } from './types';
4
+ interface InternalCanvases {
5
+ drawImageDataCanvas?: Canvas;
6
+ maskCanvas?: Canvas;
7
+ blurredMaskCanvas?: Canvas;
8
+ blurredCanvas?: Canvas;
9
+ backgroundImageCanvas?: Canvas;
10
+ inputCanvas?: Canvas;
11
+ }
12
+ export declare const createCanvasRenderUtils: (processingWidth: number, processingHeight: number) => {
13
+ evaluateInput: (inputImage: ProcessInputType | VideoFrame) => Promise<HTMLCanvasElement>;
14
+ renderImageToCanvas: (image: ImageType, canvas: Canvas, dw?: number, dh?: number, options?: CanvasRenderingContext2DSettings) => Promise<void>;
15
+ drawBlurEffect: (canvas: Canvas, inputImage: ImageType, segmentations: Segmentation | Segmentation[], foregroundThreshold?: number, backgroundBlurAmount?: number, edgeBlurAmount?: number, flipHorizontal?: boolean) => Promise<void>;
16
+ drawOverlayEffect: (canvas: Canvas, inputImage: ProcessInputType, backgroundImage: CanvasImageSource | OffscreenCanvas, segmentations: Segmentation | Segmentation[], foregroundThreshold?: number, backgroundBlurAmount?: number, edgeBlurAmount?: number, flipHorizontal?: boolean) => Promise<void>;
17
+ loadBackgroundImage: (url: string) => Promise<Canvas>;
18
+ renderImageToOffScreenCanvas: (image: ImageType, canvasName: keyof InternalCanvases) => Promise<HTMLCanvasElement>;
19
+ renderImageDataToOffScreenCanvas: (image: ImageData, canvasName: keyof InternalCanvases) => Canvas;
20
+ drawAndBlurImageOnOffScreenCanvas: (image: ImageType, blurAmount: number, offscreenCanvasName: keyof InternalCanvases) => Promise<Canvas>;
21
+ };
22
+ export {};
@@ -0,0 +1,198 @@
1
+ import { Tensor, browser } from '@tensorflow/tfjs-core/dist/base.js';
2
+ import { fitDestinationSize } from '../process';
3
+ import { createOffscreenCanvas, getImageSize, getCanvasRenderingContext2D, toBinaryMask, flipCanvasHorizontal, loadImage, } from './utils';
4
+ export const createCanvasRenderUtils = (processingWidth, processingHeight) => {
5
+ const props = {};
6
+ const getImage = (imageName) => {
7
+ const image = props[imageName];
8
+ if (!image) {
9
+ const img = new Image();
10
+ props[imageName] = img;
11
+ return img;
12
+ }
13
+ return image;
14
+ };
15
+ const getCanvas = (canvasName) => {
16
+ const canvas = props[canvasName];
17
+ if (!canvas) {
18
+ const canvas = createOffscreenCanvas(processingWidth, processingHeight);
19
+ props[canvasName] = canvas;
20
+ return canvas;
21
+ }
22
+ return canvas;
23
+ };
24
+ const renderImageDataToOffScreenCanvas = (image, canvasName) => {
25
+ const canvas = getCanvas(canvasName);
26
+ const context = getCanvasRenderingContext2D(canvas);
27
+ context.putImageData(image, 0, 0);
28
+ return canvas;
29
+ };
30
+ /**
31
+ * Draw image on a 2D rendering context.
32
+ */
33
+ const drawImage = async (ctx, image, sx, sy, sw, sh, dx, dy, dw, dh) => {
34
+ if (image instanceof Tensor) {
35
+ const pixels = await browser.toPixels(image);
36
+ const { height, width } = getImageSize(image);
37
+ image = new ImageData(pixels, width, height);
38
+ }
39
+ const source = image instanceof ImageData
40
+ ? renderImageDataToOffScreenCanvas(image, 'drawImageDataCanvas')
41
+ : image;
42
+ if (sw === undefined || sh === undefined) {
43
+ ctx.drawImage(source, sx, sy);
44
+ }
45
+ else if (dx === undefined ||
46
+ dy === undefined ||
47
+ dw === undefined ||
48
+ dh === undefined) {
49
+ ctx.drawImage(source, sx, sy, sw, sh);
50
+ }
51
+ else {
52
+ ctx.drawImage(source, sx, sy, sw, sh, dx, dy, dw, dh);
53
+ }
54
+ };
55
+ const renderImageToCanvas = async (image, canvas, dw = processingWidth, dh = processingHeight, options = {}) => {
56
+ const { height, width } = getImageSize(image);
57
+ const rect = fitDestinationSize(width, height, dw, dh);
58
+ const ctx = getCanvasRenderingContext2D(canvas, options);
59
+ await drawImage(ctx, image, rect.x, rect.y, rect.width, rect.height);
60
+ };
61
+ const renderImageToOffScreenCanvas = async (image, canvasName) => {
62
+ const canvas = getCanvas(canvasName);
63
+ await renderImageToCanvas(image, canvas);
64
+ return canvas;
65
+ };
66
+ const drawWithCompositing = async (ctx, image, compositeOperation) => {
67
+ ctx.globalCompositeOperation = compositeOperation;
68
+ await drawImage(ctx, image, 0, 0);
69
+ };
70
+ // method copied from blur in https://codepen.io/zhaojun/pen/zZmRQe
71
+ const cpuBlur = async (canvas, image, blur) => {
72
+ const ctx = getCanvasRenderingContext2D(canvas);
73
+ let sum = 0;
74
+ const delta = 5;
75
+ const alphaLeft = 1 / (2 * Math.PI * delta * delta);
76
+ const step = blur < 3 ? 1 : 2;
77
+ for (let y = -blur; y <= blur; y += step) {
78
+ for (let x = -blur; x <= blur; x += step) {
79
+ const weight = alphaLeft *
80
+ Math.exp(-(x * x + y * y) / (2 * delta * delta));
81
+ sum += weight;
82
+ }
83
+ }
84
+ for (let y = -blur; y <= blur; y += step) {
85
+ for (let x = -blur; x <= blur; x += step) {
86
+ ctx.globalAlpha =
87
+ ((alphaLeft *
88
+ Math.exp(-(x * x + y * y) / (2 * delta * delta))) /
89
+ sum) *
90
+ blur;
91
+ await drawImage(ctx, image, x, y);
92
+ }
93
+ }
94
+ ctx.globalAlpha = 1;
95
+ };
96
+ const drawAndBlurImageOnCanvas = async (image, blurAmount, canvas) => {
97
+ const { height, width } = getImageSize(image);
98
+ const ctx = getCanvasRenderingContext2D(canvas);
99
+ ctx.clearRect(0, 0, width, height);
100
+ if (blurAmount <= 0) {
101
+ return drawImage(ctx, image, 0, 0, width, height);
102
+ }
103
+ ctx.save();
104
+ if ('filter' in ctx) {
105
+ // Avoid the transparent edge by Gaussian blur
106
+ await drawImage(ctx, image, 0, 0, width, height);
107
+ ctx.filter = `blur(${blurAmount}px)`;
108
+ await drawImage(ctx, image, 0, 0, width, height);
109
+ }
110
+ else {
111
+ // Safari doesn't support filter
112
+ // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter
113
+ await cpuBlur(canvas, image, blurAmount);
114
+ }
115
+ ctx.restore();
116
+ };
117
+ const drawAndBlurImageOnOffScreenCanvas = async (image, blurAmount, offscreenCanvasName) => {
118
+ const canvas = getCanvas(offscreenCanvasName);
119
+ if (blurAmount === 0) {
120
+ await renderImageToCanvas(image, canvas);
121
+ }
122
+ else {
123
+ await drawAndBlurImageOnCanvas(image, blurAmount, canvas);
124
+ }
125
+ return canvas;
126
+ };
127
+ const createPersonMask = async (segmentation, foregroundThreshold, edgeBlurAmount) => {
128
+ const backgroundMaskImage = await toBinaryMask(segmentation, { r: 0, g: 0, b: 0, a: 255 }, { r: 0, g: 0, b: 0, a: 0 }, false, foregroundThreshold);
129
+ if (!backgroundMaskImage) {
130
+ return getCanvas('maskCanvas');
131
+ }
132
+ const backgroundMask = renderImageDataToOffScreenCanvas(backgroundMaskImage, 'maskCanvas');
133
+ if (edgeBlurAmount === 0) {
134
+ return backgroundMask;
135
+ }
136
+ else {
137
+ return drawAndBlurImageOnOffScreenCanvas(backgroundMask, edgeBlurAmount, 'blurredMaskCanvas');
138
+ }
139
+ };
140
+ const loadImageElement = async (url, imageName) => {
141
+ const image = getImage(imageName);
142
+ await loadImage(image, url);
143
+ return image;
144
+ };
145
+ const loadAndDrawImageOnOffscreenCanvas = async (url, canvasName, imageName) => {
146
+ const image = await loadImageElement(url, imageName);
147
+ const canvas = getCanvas(canvasName);
148
+ const context = getCanvasRenderingContext2D(canvas);
149
+ const imageSize = getImageSize(image);
150
+ const rect = fitDestinationSize(imageSize.width, imageSize.height, processingWidth, processingHeight);
151
+ await drawImage(context, image, rect.x, rect.y, rect.width, rect.height);
152
+ return canvas;
153
+ };
154
+ const loadBackgroundImage = (url) => loadAndDrawImageOnOffscreenCanvas(url, 'backgroundImageCanvas', 'backgroundImage');
155
+ const drawBokehEffect = async (canvas, inputImage, backgroundImage, segmentations, foregroundThreshold = 0.5, backgroundBlurAmount = 3, edgeBlurAmount = 3, flipHorizontal = false) => {
156
+ const blurredImage = await drawAndBlurImageOnOffScreenCanvas(backgroundImage, backgroundBlurAmount, 'blurredCanvas');
157
+ const ctx = getCanvasRenderingContext2D(canvas);
158
+ if (Array.isArray(segmentations) && segmentations.length === 0) {
159
+ return drawImage(ctx, blurredImage, 0, 0);
160
+ }
161
+ const personMask = await createPersonMask(segmentations, foregroundThreshold, edgeBlurAmount);
162
+ ctx.save();
163
+ if (flipHorizontal) {
164
+ flipCanvasHorizontal(canvas);
165
+ }
166
+ // draw the original image on the final canvas
167
+ const { height, width } = getImageSize(inputImage);
168
+ await drawImage(ctx, inputImage, 0, 0, width, height);
169
+ // "destination-in" - "The existing canvas content is kept where both the
170
+ // new shape and existing canvas content overlap. Everything else is made
171
+ // transparent."
172
+ // crop what's not the person using the mask from the original image
173
+ await drawWithCompositing(ctx, personMask, 'destination-in');
174
+ // "destination-over" - "The existing canvas content is kept where both the
175
+ // new shape and existing canvas content overlap. Everything else is made
176
+ // transparent."
177
+ // draw the blurred background on top of the original image where it doesn't
178
+ // overlap.
179
+ await drawWithCompositing(ctx, blurredImage, 'destination-over');
180
+ ctx.restore();
181
+ };
182
+ const drawBlurEffect = (canvas, inputImage, segmentations, foregroundThreshold = 0.5, backgroundBlurAmount = 3, edgeBlurAmount = 3, flipHorizontal = false) => drawBokehEffect(canvas, inputImage, inputImage, segmentations, foregroundThreshold, backgroundBlurAmount, edgeBlurAmount, flipHorizontal);
183
+ const drawOverlayEffect = (canvas, inputImage, backgroundImage, segmentations, foregroundThreshold = 0.5, backgroundBlurAmount = 0, edgeBlurAmount = 3, flipHorizontal = false) => drawBokehEffect(canvas, inputImage, backgroundImage, segmentations, foregroundThreshold, backgroundBlurAmount, edgeBlurAmount, flipHorizontal);
184
+ const evaluateInput = async (inputImage) => {
185
+ const image = await renderImageToOffScreenCanvas(inputImage, 'inputCanvas');
186
+ return image;
187
+ };
188
+ return {
189
+ evaluateInput,
190
+ renderImageToCanvas,
191
+ drawBlurEffect,
192
+ drawOverlayEffect,
193
+ loadBackgroundImage,
194
+ renderImageToOffScreenCanvas,
195
+ renderImageDataToOffScreenCanvas,
196
+ drawAndBlurImageOnOffScreenCanvas,
197
+ };
198
+ };
@@ -0,0 +1,8 @@
1
+ import type { Segmenter, RenderParams, SegmentationTransform } from './types';
2
+ type Params = Omit<RenderParams, 'frameRate'>;
3
+ interface Options extends Omit<Params, 'backgroundImage'> {
4
+ selfManageSegmenter?: boolean;
5
+ bgImageUrl?: string;
6
+ }
7
+ export declare const createTransform: (segmenter: Segmenter, { width, height, foregroundThreshold, backgroundBlurAmount, edgeBlurAmount, flipHorizontal, effects, selfManageSegmenter, bgImageUrl, }?: Partial<Options>) => SegmentationTransform;
8
+ export {};