@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.
- package/CHANGELOG.md +23 -0
- package/README.md +1 -7
- package/dist/main/audio.d.ts +123 -0
- package/dist/main/audio.js +653 -0
- package/dist/main/benchUtils.d.ts +12 -0
- package/dist/main/benchUtils.js +34 -0
- package/dist/main/generator.d.ts +4 -0
- package/dist/main/generator.js +4 -0
- package/dist/main/index.d.ts +17 -0
- package/dist/main/index.js +17 -0
- package/dist/main/math.d.ts +41 -0
- package/dist/main/math.js +57 -0
- package/dist/main/path.d.ts +102 -0
- package/dist/main/path.js +103 -0
- package/dist/main/process.d.ts +239 -0
- package/dist/main/process.js +364 -0
- package/dist/main/processor.d.ts +5 -0
- package/dist/main/processor.js +4 -0
- package/dist/main/transformer.d.ts +1 -0
- package/dist/main/transformer.js +4 -0
- package/dist/main/tsconfig.tsbuildinfo +1 -0
- package/dist/main/typeGuards.d.ts +5 -0
- package/dist/main/typeGuards.js +24 -0
- package/dist/main/types.d.ts +342 -0
- package/dist/main/types.js +1 -0
- package/dist/main/utils.d.ts +173 -0
- package/dist/main/utils.js +364 -0
- package/dist/main/video/canvasRenderUtils.d.ts +22 -0
- package/dist/main/video/canvasRenderUtils.js +198 -0
- package/dist/main/video/canvasTransform.d.ts +8 -0
- package/dist/main/video/canvasTransform.js +173 -0
- package/dist/main/video/constants.d.ts +10 -0
- package/dist/main/video/constants.js +12 -0
- package/dist/main/video/index.d.ts +9 -0
- package/dist/main/video/index.js +9 -0
- package/dist/main/video/load.d.ts +17 -0
- package/dist/main/video/load.js +47 -0
- package/dist/main/video/segmenters/index.d.ts +1 -0
- package/dist/main/video/segmenters/index.js +1 -0
- package/dist/main/video/segmenters/mediapipe.d.ts +13 -0
- package/dist/main/video/segmenters/mediapipe.js +85 -0
- package/dist/main/video/transformer.d.ts +10 -0
- package/dist/main/video/transformer.js +56 -0
- package/dist/main/video/typeGuards.d.ts +2 -0
- package/dist/main/video/typeGuards.js +13 -0
- package/dist/main/video/types.d.ts +98 -0
- package/dist/main/video/types.js +20 -0
- package/dist/main/video/utils.d.ts +102 -0
- package/dist/main/video/utils.js +476 -0
- package/dist/main/video/video.d.ts +19 -0
- package/dist/main/video/video.js +48 -0
- package/dist/main/video/videoStreamTrackProcessor.d.ts +14 -0
- package/dist/main/video/videoStreamTrackProcessor.js +82 -0
- package/dist/main/visual.d.ts +80 -0
- package/dist/main/visual.js +135 -0
- package/dist/main/workletNodes.d.ts +2 -0
- package/dist/main/workletNodes.js +3 -0
- package/dist/workers/index.d.ts +0 -0
- package/dist/workers/index.js +1 -0
- package/dist/workers/tsconfig.tsbuildinfo +1 -0
- package/dist/worklets/denoise.worklet.d.ts +1 -0
- package/dist/worklets/denoise.worklet.js +1 -2
- package/dist/worklets/tsconfig.tsbuildinfo +1 -0
- package/dist/worklets/types.d.ts +52 -0
- package/dist/worklets/types.js +0 -0
- package/package.json +11 -9
- package/dist/index.d.ts +0 -1129
- package/dist/index.mjs +0 -2441
- 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 {};
|