@livekit/track-processors 0.4.1 → 0.5.1
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/README.md +9 -1
- package/dist/index.js +628 -124
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +628 -124
- package/dist/index.mjs.map +1 -1
- package/dist/src/ProcessorWrapper.d.ts +26 -1
- package/dist/src/index.d.ts +24 -6
- package/dist/src/transformers/BackgroundTransformer.d.ts +8 -3
- package/dist/src/transformers/VideoTransformer.d.ts +2 -1
- package/dist/src/webgl/index.d.ts +9 -0
- package/package.json +9 -9
- package/src/ProcessorWrapper.ts +271 -18
- package/src/index.ts +96 -10
- package/src/transformers/BackgroundTransformer.ts +55 -103
- package/src/transformers/VideoTransformer.ts +14 -4
- package/src/utils.ts +1 -0
- package/src/webgl/index.ts +458 -0
package/src/ProcessorWrapper.ts
CHANGED
|
@@ -2,10 +2,40 @@ import type { ProcessorOptions, Track, TrackProcessor } from 'livekit-client';
|
|
|
2
2
|
import { TrackTransformer } from './transformers';
|
|
3
3
|
import { waitForTrackResolution } from './utils';
|
|
4
4
|
|
|
5
|
+
export interface ProcessorWrapperOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Maximum frame rate for fallback canvas.captureStream implementation
|
|
8
|
+
* Default: 30
|
|
9
|
+
*/
|
|
10
|
+
maxFps?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
export default class ProcessorWrapper<TransformerOptions extends Record<string, unknown>>
|
|
6
14
|
implements TrackProcessor<Track.Kind>
|
|
7
15
|
{
|
|
16
|
+
/**
|
|
17
|
+
* Determines if the Processor is supported on the current browser
|
|
18
|
+
*/
|
|
8
19
|
static get isSupported() {
|
|
20
|
+
// Check for primary implementation support
|
|
21
|
+
const hasStreamProcessor =
|
|
22
|
+
typeof MediaStreamTrackGenerator !== 'undefined' &&
|
|
23
|
+
typeof MediaStreamTrackProcessor !== 'undefined';
|
|
24
|
+
|
|
25
|
+
// Check for fallback implementation support
|
|
26
|
+
const hasFallbackSupport =
|
|
27
|
+
typeof HTMLCanvasElement !== 'undefined' &&
|
|
28
|
+
typeof VideoFrame !== 'undefined' &&
|
|
29
|
+
'captureStream' in HTMLCanvasElement.prototype;
|
|
30
|
+
|
|
31
|
+
// We can work if either implementation is available
|
|
32
|
+
return hasStreamProcessor || hasFallbackSupport;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Determines if modern browser APIs are supported, which yield better performance
|
|
37
|
+
*/
|
|
38
|
+
static get hasModernApiSupport() {
|
|
9
39
|
return (
|
|
10
40
|
typeof MediaStreamTrackGenerator !== 'undefined' &&
|
|
11
41
|
typeof MediaStreamTrackProcessor !== 'undefined'
|
|
@@ -22,15 +52,39 @@ export default class ProcessorWrapper<TransformerOptions extends Record<string,
|
|
|
22
52
|
|
|
23
53
|
canvas?: OffscreenCanvas;
|
|
24
54
|
|
|
55
|
+
displayCanvas?: HTMLCanvasElement;
|
|
56
|
+
|
|
25
57
|
sourceDummy?: HTMLMediaElement;
|
|
26
58
|
|
|
27
59
|
processedTrack?: MediaStreamTrack;
|
|
28
60
|
|
|
29
61
|
transformer: TrackTransformer<TransformerOptions>;
|
|
30
62
|
|
|
31
|
-
|
|
63
|
+
// For tracking whether we're using the stream API fallback
|
|
64
|
+
private useStreamFallback = false;
|
|
65
|
+
|
|
66
|
+
// For fallback rendering with canvas.captureStream()
|
|
67
|
+
private capturedStream?: MediaStream;
|
|
68
|
+
|
|
69
|
+
private animationFrameId?: number;
|
|
70
|
+
|
|
71
|
+
private renderContext?: CanvasRenderingContext2D;
|
|
72
|
+
|
|
73
|
+
private frameCallback?: (frame: VideoFrame) => void;
|
|
74
|
+
|
|
75
|
+
private processingEnabled = false;
|
|
76
|
+
|
|
77
|
+
// FPS control for fallback implementation
|
|
78
|
+
private maxFps: number;
|
|
79
|
+
|
|
80
|
+
constructor(
|
|
81
|
+
transformer: TrackTransformer<TransformerOptions>,
|
|
82
|
+
name: string,
|
|
83
|
+
options: ProcessorWrapperOptions = {},
|
|
84
|
+
) {
|
|
32
85
|
this.name = name;
|
|
33
86
|
this.transformer = transformer;
|
|
87
|
+
this.maxFps = options.maxFps ?? 30;
|
|
34
88
|
}
|
|
35
89
|
|
|
36
90
|
private async setup(opts: ProcessorOptions<Track.Kind>) {
|
|
@@ -48,55 +102,254 @@ export default class ProcessorWrapper<TransformerOptions extends Record<string,
|
|
|
48
102
|
this.sourceDummy.width = width ?? 300;
|
|
49
103
|
}
|
|
50
104
|
|
|
51
|
-
|
|
52
|
-
this.processor = new MediaStreamTrackProcessor({ track: this.source });
|
|
105
|
+
this.useStreamFallback = !ProcessorWrapper.hasModernApiSupport;
|
|
53
106
|
|
|
54
|
-
this.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
107
|
+
if (this.useStreamFallback) {
|
|
108
|
+
// Create a visible canvas for the fallback implementation or use an existing one if provided
|
|
109
|
+
const existingCanvas = document.querySelector(
|
|
110
|
+
'canvas[data-livekit-processor="' + this.name + '"]',
|
|
111
|
+
) as HTMLCanvasElement;
|
|
58
112
|
|
|
59
|
-
|
|
113
|
+
if (existingCanvas) {
|
|
114
|
+
this.displayCanvas = existingCanvas;
|
|
115
|
+
this.displayCanvas.width = width ?? 300;
|
|
116
|
+
this.displayCanvas.height = height ?? 300;
|
|
117
|
+
} else {
|
|
118
|
+
this.displayCanvas = document.createElement('canvas');
|
|
119
|
+
this.displayCanvas.width = width ?? 300;
|
|
120
|
+
this.displayCanvas.height = height ?? 300;
|
|
121
|
+
this.displayCanvas.style.display = 'none';
|
|
122
|
+
this.displayCanvas.dataset.livekitProcessor = this.name;
|
|
123
|
+
document.body.appendChild(this.displayCanvas);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.renderContext = this.displayCanvas.getContext('2d')!;
|
|
127
|
+
this.capturedStream = this.displayCanvas.captureStream();
|
|
128
|
+
this.canvas = new OffscreenCanvas(width ?? 300, height ?? 300);
|
|
129
|
+
} else {
|
|
130
|
+
// Use MediaStreamTrackProcessor API
|
|
131
|
+
this.processor = new MediaStreamTrackProcessor({ track: this.source });
|
|
132
|
+
this.trackGenerator = new MediaStreamTrackGenerator({
|
|
133
|
+
kind: 'video',
|
|
134
|
+
signalTarget: this.source,
|
|
135
|
+
});
|
|
136
|
+
this.canvas = new OffscreenCanvas(width ?? 300, height ?? 300);
|
|
137
|
+
}
|
|
60
138
|
}
|
|
61
139
|
|
|
62
|
-
async init(opts: ProcessorOptions<Track.Kind>) {
|
|
140
|
+
async init(opts: ProcessorOptions<Track.Kind>): Promise<void> {
|
|
63
141
|
await this.setup(opts);
|
|
64
|
-
if (!this.canvas || !this.processor || !this.trackGenerator) {
|
|
65
|
-
throw new TypeError('Expected both canvas and processor to be defined after setup');
|
|
66
|
-
}
|
|
67
142
|
|
|
68
|
-
|
|
143
|
+
if (!this.canvas) {
|
|
144
|
+
throw new TypeError('Expected canvas to be defined after setup');
|
|
145
|
+
}
|
|
69
146
|
|
|
70
147
|
await this.transformer.init({
|
|
71
148
|
outputCanvas: this.canvas,
|
|
72
149
|
inputElement: this.sourceDummy as HTMLVideoElement,
|
|
73
150
|
});
|
|
74
151
|
|
|
152
|
+
if (this.useStreamFallback) {
|
|
153
|
+
this.initFallbackPath();
|
|
154
|
+
} else {
|
|
155
|
+
this.initStreamProcessorPath();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private initStreamProcessorPath() {
|
|
160
|
+
if (!this.processor || !this.trackGenerator) {
|
|
161
|
+
throw new TypeError(
|
|
162
|
+
'Expected processor and trackGenerator to be defined for stream processor path',
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const readableStream = this.processor.readable;
|
|
75
167
|
const pipedStream = readableStream.pipeThrough(this.transformer!.transformer!);
|
|
76
168
|
|
|
77
169
|
pipedStream
|
|
78
170
|
.pipeTo(this.trackGenerator.writable)
|
|
79
171
|
.catch((e) => console.error('error when trying to pipe', e))
|
|
80
172
|
.finally(() => this.destroy());
|
|
173
|
+
|
|
81
174
|
this.processedTrack = this.trackGenerator as MediaStreamVideoTrack;
|
|
82
175
|
}
|
|
83
176
|
|
|
84
|
-
|
|
177
|
+
private initFallbackPath() {
|
|
178
|
+
if (!this.capturedStream || !this.source || !this.canvas || !this.renderContext) {
|
|
179
|
+
throw new TypeError('Missing required components for fallback implementation');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.processedTrack = this.capturedStream.getVideoTracks()[0];
|
|
183
|
+
this.processingEnabled = true;
|
|
184
|
+
|
|
185
|
+
// Set up the frame callback for the transformer
|
|
186
|
+
this.frameCallback = (frame: VideoFrame) => {
|
|
187
|
+
if (!this.processingEnabled || !frame) {
|
|
188
|
+
frame.close();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const controller = {
|
|
193
|
+
enqueue: (processedFrame: VideoFrame) => {
|
|
194
|
+
if (this.renderContext && this.displayCanvas) {
|
|
195
|
+
// Draw the processed frame to the visible canvas
|
|
196
|
+
this.renderContext.drawImage(
|
|
197
|
+
processedFrame,
|
|
198
|
+
0,
|
|
199
|
+
0,
|
|
200
|
+
this.displayCanvas.width,
|
|
201
|
+
this.displayCanvas.height,
|
|
202
|
+
);
|
|
203
|
+
processedFrame.close();
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
} as TransformStreamDefaultController<VideoFrame>;
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// Pass the frame through our transformer
|
|
210
|
+
// @ts-ignore - The controller expects both VideoFrame & AudioData but we're only using VideoFrame
|
|
211
|
+
this.transformer.transform(frame, controller);
|
|
212
|
+
} catch (e) {
|
|
213
|
+
console.error('Error in transform:', e);
|
|
214
|
+
frame.close();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Start the rendering loop
|
|
219
|
+
this.startRenderLoop();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private startRenderLoop() {
|
|
223
|
+
if (!this.sourceDummy || !(this.sourceDummy instanceof HTMLVideoElement)) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Store the last processed timestamp to avoid duplicate processing
|
|
228
|
+
let lastVideoTimestamp = -1;
|
|
229
|
+
let lastFrameTime = 0;
|
|
230
|
+
const videoElement = this.sourceDummy as HTMLVideoElement;
|
|
231
|
+
const minFrameInterval = 1000 / this.maxFps; // Minimum time between frames
|
|
232
|
+
|
|
233
|
+
// Estimate the video's native frame rate
|
|
234
|
+
let estimatedVideoFps = this.maxFps;
|
|
235
|
+
let frameTimeHistory: number[] = [];
|
|
236
|
+
let lastVideoTimeChange = 0;
|
|
237
|
+
let frameCount = 0;
|
|
238
|
+
let lastFpsLog = 0;
|
|
239
|
+
|
|
240
|
+
const renderLoop = () => {
|
|
241
|
+
if (
|
|
242
|
+
!this.processingEnabled ||
|
|
243
|
+
!this.sourceDummy ||
|
|
244
|
+
!(this.sourceDummy instanceof HTMLVideoElement)
|
|
245
|
+
) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Only process a new frame if the video has actually updated
|
|
250
|
+
const videoTime = videoElement.currentTime;
|
|
251
|
+
const now = performance.now();
|
|
252
|
+
const timeSinceLastFrame = now - lastFrameTime;
|
|
253
|
+
|
|
254
|
+
// Detect if video has a new frame
|
|
255
|
+
const hasNewFrame = videoTime !== lastVideoTimestamp;
|
|
256
|
+
|
|
257
|
+
// Update frame rate estimation if we have a new frame
|
|
258
|
+
if (hasNewFrame) {
|
|
259
|
+
if (lastVideoTimeChange > 0) {
|
|
260
|
+
const timeBetweenFrames = now - lastVideoTimeChange;
|
|
261
|
+
frameTimeHistory.push(timeBetweenFrames);
|
|
262
|
+
|
|
263
|
+
// Keep a rolling window of the last 10 frame times
|
|
264
|
+
if (frameTimeHistory.length > 10) {
|
|
265
|
+
frameTimeHistory.shift();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Calculate average frame interval
|
|
269
|
+
if (frameTimeHistory.length > 2) {
|
|
270
|
+
const avgFrameTime =
|
|
271
|
+
frameTimeHistory.reduce((sum, time) => sum + time, 0) / frameTimeHistory.length;
|
|
272
|
+
estimatedVideoFps = 1000 / avgFrameTime;
|
|
273
|
+
|
|
274
|
+
// Log estimated FPS every 5 seconds in development environments
|
|
275
|
+
// Use a simpler check that works in browsers without process.env
|
|
276
|
+
const isDevelopment =
|
|
277
|
+
(typeof window !== 'undefined' && window.location.hostname === 'localhost') ||
|
|
278
|
+
window.location.hostname === '127.0.0.1';
|
|
279
|
+
|
|
280
|
+
if (isDevelopment && now - lastFpsLog > 5000) {
|
|
281
|
+
console.debug(
|
|
282
|
+
`[${this.name}] Estimated video FPS: ${estimatedVideoFps.toFixed(
|
|
283
|
+
1,
|
|
284
|
+
)}, Processing at: ${(frameCount / 5).toFixed(1)} FPS`,
|
|
285
|
+
);
|
|
286
|
+
frameCount = 0;
|
|
287
|
+
lastFpsLog = now;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
lastVideoTimeChange = now;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Determine if we should process this frame
|
|
295
|
+
// We'll process if:
|
|
296
|
+
// 1. The video has a new frame
|
|
297
|
+
// 2. Enough time has passed since last frame (respecting maxFps)
|
|
298
|
+
const timeThresholdMet = timeSinceLastFrame >= minFrameInterval;
|
|
299
|
+
|
|
300
|
+
if (hasNewFrame && timeThresholdMet) {
|
|
301
|
+
lastVideoTimestamp = videoTime;
|
|
302
|
+
lastFrameTime = now;
|
|
303
|
+
frameCount++;
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
// Create a VideoFrame from the video element
|
|
307
|
+
const frame = new VideoFrame(videoElement);
|
|
308
|
+
|
|
309
|
+
if (this.frameCallback) {
|
|
310
|
+
this.frameCallback(frame);
|
|
311
|
+
} else {
|
|
312
|
+
frame.close();
|
|
313
|
+
}
|
|
314
|
+
} catch (e) {
|
|
315
|
+
console.error('Error in render loop:', e);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
this.animationFrameId = requestAnimationFrame(renderLoop);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
this.animationFrameId = requestAnimationFrame(renderLoop);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async restart(opts: ProcessorOptions<Track.Kind>): Promise<void> {
|
|
85
325
|
await this.destroy();
|
|
86
|
-
|
|
326
|
+
await this.init(opts);
|
|
87
327
|
}
|
|
88
328
|
|
|
89
329
|
async restartTransformer(...options: Parameters<(typeof this.transformer)['restart']>) {
|
|
90
330
|
// @ts-ignore unclear why the restart method only accepts VideoTransformerInitOptions instead of either those or AudioTransformerInitOptions
|
|
91
|
-
this.transformer.restart(options[0]);
|
|
331
|
+
await this.transformer.restart(options[0]);
|
|
92
332
|
}
|
|
93
333
|
|
|
94
334
|
async updateTransformerOptions(...options: Parameters<(typeof this.transformer)['update']>) {
|
|
95
|
-
this.transformer.update(options[0]);
|
|
335
|
+
await this.transformer.update(options[0]);
|
|
96
336
|
}
|
|
97
337
|
|
|
98
338
|
async destroy() {
|
|
339
|
+
if (this.useStreamFallback) {
|
|
340
|
+
this.processingEnabled = false;
|
|
341
|
+
if (this.animationFrameId) {
|
|
342
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
343
|
+
this.animationFrameId = undefined;
|
|
344
|
+
}
|
|
345
|
+
if (this.displayCanvas && this.displayCanvas.parentNode) {
|
|
346
|
+
this.displayCanvas.parentNode.removeChild(this.displayCanvas);
|
|
347
|
+
}
|
|
348
|
+
this.capturedStream?.getTracks().forEach((track) => track.stop());
|
|
349
|
+
} else {
|
|
350
|
+
await this.processor?.writableControl?.close();
|
|
351
|
+
this.trackGenerator?.stop();
|
|
352
|
+
}
|
|
99
353
|
await this.transformer.destroy();
|
|
100
|
-
this.trackGenerator?.stop();
|
|
101
354
|
}
|
|
102
355
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,26 +1,112 @@
|
|
|
1
|
-
import ProcessorWrapper from './ProcessorWrapper';
|
|
1
|
+
import ProcessorWrapper, { ProcessorWrapperOptions } from './ProcessorWrapper';
|
|
2
2
|
import BackgroundTransformer, {
|
|
3
3
|
BackgroundOptions,
|
|
4
|
+
FrameProcessingStats,
|
|
4
5
|
SegmenterOptions,
|
|
5
6
|
} from './transformers/BackgroundTransformer';
|
|
6
7
|
|
|
7
8
|
export * from './transformers/types';
|
|
8
9
|
export { default as VideoTransformer } from './transformers/VideoTransformer';
|
|
9
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
ProcessorWrapper,
|
|
12
|
+
type BackgroundOptions,
|
|
13
|
+
type SegmenterOptions,
|
|
14
|
+
BackgroundTransformer,
|
|
15
|
+
type ProcessorWrapperOptions,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Determines if the current browser supports background processors
|
|
20
|
+
*/
|
|
21
|
+
export const supportsBackgroundProcessors = () =>
|
|
22
|
+
BackgroundTransformer.isSupported && ProcessorWrapper.isSupported;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Determines if the current browser supports modern background processors, which yield better performance
|
|
26
|
+
*/
|
|
27
|
+
export const supportsModernBackgroundProcessors = () =>
|
|
28
|
+
BackgroundTransformer.isSupported && ProcessorWrapper.hasModernApiSupport;
|
|
10
29
|
|
|
11
|
-
export
|
|
12
|
-
|
|
30
|
+
export interface BackgroundProcessorOptions extends ProcessorWrapperOptions {
|
|
31
|
+
blurRadius?: number;
|
|
32
|
+
imagePath?: string;
|
|
33
|
+
segmenterOptions?: SegmenterOptions;
|
|
34
|
+
assetPaths?: { tasksVisionFileSet?: string; modelAssetPath?: string };
|
|
35
|
+
onFrameProcessed?: (stats: FrameProcessingStats) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const BackgroundBlur = (
|
|
39
|
+
blurRadius: number = 10,
|
|
40
|
+
segmenterOptions?: SegmenterOptions,
|
|
41
|
+
onFrameProcessed?: (stats: FrameProcessingStats) => void,
|
|
42
|
+
processorOptions?: ProcessorWrapperOptions,
|
|
43
|
+
) => {
|
|
44
|
+
return BackgroundProcessor(
|
|
45
|
+
{
|
|
46
|
+
blurRadius,
|
|
47
|
+
segmenterOptions,
|
|
48
|
+
onFrameProcessed,
|
|
49
|
+
...processorOptions,
|
|
50
|
+
},
|
|
51
|
+
'background-blur',
|
|
52
|
+
);
|
|
13
53
|
};
|
|
14
54
|
|
|
15
|
-
export const VirtualBackground = (
|
|
16
|
-
|
|
55
|
+
export const VirtualBackground = (
|
|
56
|
+
imagePath: string,
|
|
57
|
+
segmenterOptions?: SegmenterOptions,
|
|
58
|
+
onFrameProcessed?: (stats: FrameProcessingStats) => void,
|
|
59
|
+
processorOptions?: ProcessorWrapperOptions,
|
|
60
|
+
) => {
|
|
61
|
+
return BackgroundProcessor(
|
|
62
|
+
{
|
|
63
|
+
imagePath,
|
|
64
|
+
segmenterOptions,
|
|
65
|
+
onFrameProcessed,
|
|
66
|
+
...processorOptions,
|
|
67
|
+
},
|
|
68
|
+
'virtual-background',
|
|
69
|
+
);
|
|
17
70
|
};
|
|
18
71
|
|
|
19
|
-
export const BackgroundProcessor = (
|
|
20
|
-
|
|
72
|
+
export const BackgroundProcessor = (
|
|
73
|
+
options: BackgroundProcessorOptions,
|
|
74
|
+
name = 'background-processor',
|
|
75
|
+
) => {
|
|
76
|
+
const isTransformerSupported = BackgroundTransformer.isSupported;
|
|
77
|
+
const isProcessorSupported = ProcessorWrapper.isSupported;
|
|
78
|
+
|
|
79
|
+
if (!isTransformerSupported) {
|
|
80
|
+
throw new Error('Background transformer is not supported in this browser');
|
|
81
|
+
}
|
|
82
|
+
|
|
21
83
|
if (!isProcessorSupported) {
|
|
22
|
-
throw new Error(
|
|
84
|
+
throw new Error(
|
|
85
|
+
'Neither MediaStreamTrackProcessor nor canvas.captureStream() fallback is supported in this browser',
|
|
86
|
+
);
|
|
23
87
|
}
|
|
24
|
-
|
|
88
|
+
|
|
89
|
+
// Extract transformer-specific options and processor options
|
|
90
|
+
const {
|
|
91
|
+
blurRadius,
|
|
92
|
+
imagePath,
|
|
93
|
+
segmenterOptions,
|
|
94
|
+
assetPaths,
|
|
95
|
+
onFrameProcessed,
|
|
96
|
+
...processorOpts
|
|
97
|
+
} = options;
|
|
98
|
+
|
|
99
|
+
const processor = new ProcessorWrapper(
|
|
100
|
+
new BackgroundTransformer({
|
|
101
|
+
blurRadius,
|
|
102
|
+
imagePath,
|
|
103
|
+
segmenterOptions,
|
|
104
|
+
assetPaths,
|
|
105
|
+
onFrameProcessed,
|
|
106
|
+
}),
|
|
107
|
+
name,
|
|
108
|
+
processorOpts,
|
|
109
|
+
);
|
|
110
|
+
|
|
25
111
|
return processor;
|
|
26
112
|
};
|