@stream-io/video-filters-web 0.3.0 → 0.5.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 +12 -0
- package/README.md +27 -24
- package/dist/index.cjs.js +1099 -6
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.es.js +1096 -7
- package/dist/index.es.js.map +1 -1
- package/dist/src/FallbackGenerator.d.ts +17 -0
- package/dist/src/FallbackProcessor.d.ts +38 -0
- package/dist/src/VirtualBackground.d.ts +42 -0
- package/dist/src/WebGLRenderer.d.ts +80 -0
- package/dist/src/compatibility.d.ts +5 -0
- package/dist/src/{createRenderer.d.ts → legacy/createRenderer.d.ts} +1 -2
- package/dist/src/{webgl2 → legacy/webgl2}/backgroundBlurStage.d.ts +1 -1
- package/dist/src/{webgl2 → legacy/webgl2}/webgl2Pipeline.d.ts +1 -1
- package/dist/src/mediapipe.d.ts +4 -0
- package/dist/src/types.d.ts +45 -0
- package/index.ts +6 -3
- package/mediapipe/models/selfie_segmenter.tflite +0 -0
- package/mediapipe/wasm/vision_wasm_internal.js +20 -0
- package/mediapipe/wasm/vision_wasm_internal.wasm +0 -0
- package/mediapipe/wasm/vision_wasm_nosimd_internal.js +20 -0
- package/mediapipe/wasm/vision_wasm_nosimd_internal.wasm +0 -0
- package/package.json +5 -2
- package/src/FallbackGenerator.ts +90 -0
- package/src/FallbackProcessor.ts +132 -0
- package/src/VirtualBackground.ts +276 -0
- package/src/WebGLRenderer.ts +1187 -0
- package/src/compatibility.ts +22 -0
- package/src/{createRenderer.ts → legacy/createRenderer.ts} +1 -2
- package/src/{tflite.ts → legacy/tflite.ts} +1 -1
- package/src/{webgl2 → legacy/webgl2}/backgroundBlurStage.ts +1 -1
- package/src/{webgl2 → legacy/webgl2}/webgl2Pipeline.ts +1 -1
- package/src/mediapipe.ts +25 -0
- package/src/types.ts +62 -0
- /package/dist/src/{helpers → legacy/helpers}/webglHelper.d.ts +0 -0
- /package/dist/src/{segmentation.d.ts → legacy/segmentation.d.ts} +0 -0
- /package/dist/src/{tflite.d.ts → legacy/tflite.d.ts} +0 -0
- /package/dist/src/{webgl2 → legacy/webgl2}/backgroundImageStage.d.ts +0 -0
- /package/dist/src/{webgl2 → legacy/webgl2}/jointBilateralFilterStage.d.ts +0 -0
- /package/dist/src/{webgl2 → legacy/webgl2}/resizingStage.d.ts +0 -0
- /package/dist/src/{webgl2 → legacy/webgl2}/softmaxStage.d.ts +0 -0
- /package/src/{helpers → legacy/helpers}/webglHelper.ts +0 -0
- /package/src/{segmentation.ts → legacy/segmentation.ts} +0 -0
- /package/src/{tflite-simd.js → legacy/tflite-simd.js} +0 -0
- /package/src/{webgl2 → legacy/webgl2}/backgroundImageStage.ts +0 -0
- /package/src/{webgl2 → legacy/webgl2}/jointBilateralFilterStage.ts +0 -0
- /package/src/{webgl2 → legacy/webgl2}/resizingStage.ts +0 -0
- /package/src/{webgl2 → legacy/webgl2}/softmaxStage.ts +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type representing a video track generator that can be either the native
|
|
3
|
+
* MediaStreamTrackGenerator or the fallback implementation.
|
|
4
|
+
*/
|
|
5
|
+
export interface MediaStreamTrackGenerator<T> extends MediaStreamTrack {
|
|
6
|
+
writable: WritableStream<T>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Configuration options for creating a track generator.
|
|
11
|
+
*/
|
|
12
|
+
export interface TrackGeneratorOptions {
|
|
13
|
+
readonly kind: 'video';
|
|
14
|
+
readonly signalTarget?: MediaStreamTrack;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Fallback video processor for browsers that do not support MediaStreamTrackGenerator.
|
|
19
|
+
*
|
|
20
|
+
* Produces a video MediaStreamTrack sourced from a canvas and exposes
|
|
21
|
+
* a WritableStream<VideoFrame> on track.writable for writing frames.
|
|
22
|
+
*/
|
|
23
|
+
class FallbackGenerator {
|
|
24
|
+
constructor({ kind, signalTarget }: TrackGeneratorOptions) {
|
|
25
|
+
if (kind !== 'video') {
|
|
26
|
+
throw new Error('Only video tracks are supported');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const canvas = document.createElement('canvas');
|
|
30
|
+
const ctx = canvas.getContext('2d', { desynchronized: true });
|
|
31
|
+
if (!ctx) {
|
|
32
|
+
throw new Error('Failed to get 2D context from canvas');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const mediaStream = canvas.captureStream();
|
|
36
|
+
const track =
|
|
37
|
+
mediaStream.getVideoTracks()[0] as MediaStreamTrackGenerator<VideoFrame>;
|
|
38
|
+
|
|
39
|
+
const height = signalTarget?.getSettings().height;
|
|
40
|
+
const width = signalTarget?.getSettings().width;
|
|
41
|
+
if (height && width) {
|
|
42
|
+
canvas.height = height;
|
|
43
|
+
canvas.width = width;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!track) {
|
|
47
|
+
throw new Error('Failed to create canvas track');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (signalTarget) {
|
|
51
|
+
signalTarget.addEventListener('ended', () => {
|
|
52
|
+
track.stop();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
track.writable = new WritableStream({
|
|
57
|
+
write: (frame: VideoFrame) => {
|
|
58
|
+
if (
|
|
59
|
+
canvas.width !== frame.displayWidth ||
|
|
60
|
+
canvas.height !== frame.displayHeight
|
|
61
|
+
) {
|
|
62
|
+
canvas.width = frame.displayWidth;
|
|
63
|
+
canvas.height = frame.displayHeight;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);
|
|
67
|
+
frame.close();
|
|
68
|
+
},
|
|
69
|
+
abort: () => {
|
|
70
|
+
track.stop();
|
|
71
|
+
},
|
|
72
|
+
close: () => {
|
|
73
|
+
track.stop();
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return track as MediaStreamTrackGenerator<VideoFrame>;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type VideoTrackGeneratorConstructor = new (
|
|
82
|
+
options: TrackGeneratorOptions,
|
|
83
|
+
) => MediaStreamTrackGenerator<VideoFrame>;
|
|
84
|
+
|
|
85
|
+
const TrackGenerator: VideoTrackGeneratorConstructor =
|
|
86
|
+
typeof MediaStreamTrackGenerator !== 'undefined'
|
|
87
|
+
? (MediaStreamTrackGenerator as unknown as VideoTrackGeneratorConstructor)
|
|
88
|
+
: (FallbackGenerator as unknown as VideoTrackGeneratorConstructor);
|
|
89
|
+
|
|
90
|
+
export { TrackGenerator };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { WorkerTimer } from '@stream-io/worker-timer';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Representing a video track processor that can be either the native
|
|
5
|
+
* MediaStreamTrackProcessor or the fallback implementation.
|
|
6
|
+
*/
|
|
7
|
+
export interface MediaStreamTrackProcessor<T> {
|
|
8
|
+
readonly readable: ReadableStream<T>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Configuration options for creating a track processor.
|
|
13
|
+
*/
|
|
14
|
+
export interface TrackProcessorOptions {
|
|
15
|
+
readonly track: MediaStreamTrack;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Fallback implementation for browsers without MediaStreamTrackGenerator.
|
|
20
|
+
*
|
|
21
|
+
* Produces a video MediaStreamTrack sourced from a canvas and exposes a
|
|
22
|
+
* WritableStream<VideoFrame> on track.writable. Written frames are drawn
|
|
23
|
+
* into the canvas and update the underlying track automatically.
|
|
24
|
+
*/
|
|
25
|
+
class FallbackProcessor implements MediaStreamTrackProcessor<VideoFrame> {
|
|
26
|
+
readonly readable: ReadableStream<VideoFrame>;
|
|
27
|
+
|
|
28
|
+
readonly workerTimer: WorkerTimer;
|
|
29
|
+
readonly video: HTMLVideoElement;
|
|
30
|
+
|
|
31
|
+
constructor({ track }: TrackProcessorOptions) {
|
|
32
|
+
if (!track) throw new Error('MediaStreamTrack is required');
|
|
33
|
+
if (track.kind !== 'video') {
|
|
34
|
+
throw new Error('MediaStreamTrack must be video');
|
|
35
|
+
}
|
|
36
|
+
let running = true;
|
|
37
|
+
|
|
38
|
+
this.video = document.createElement('video');
|
|
39
|
+
|
|
40
|
+
this.video.muted = true;
|
|
41
|
+
this.video.playsInline = true;
|
|
42
|
+
this.video.srcObject = new MediaStream([track]);
|
|
43
|
+
|
|
44
|
+
const canvas = new OffscreenCanvas(1, 1);
|
|
45
|
+
const ctx = canvas.getContext('2d');
|
|
46
|
+
|
|
47
|
+
if (!ctx) throw new Error('Failed to get 2D context from OffscreenCanvas');
|
|
48
|
+
|
|
49
|
+
let timestamp = 0;
|
|
50
|
+
const frameRate = track.getSettings().frameRate || 30;
|
|
51
|
+
let frameDuration = 1000 / frameRate;
|
|
52
|
+
let lastVideoTime = -1;
|
|
53
|
+
|
|
54
|
+
this.workerTimer = new WorkerTimer({ useWorker: true });
|
|
55
|
+
this.readable = new ReadableStream({
|
|
56
|
+
start: async () => {
|
|
57
|
+
await Promise.all([
|
|
58
|
+
this.video.play(),
|
|
59
|
+
new Promise((r) =>
|
|
60
|
+
this.video.addEventListener('loadeddata', r, { once: true }),
|
|
61
|
+
),
|
|
62
|
+
]);
|
|
63
|
+
frameDuration = 1000 / (track.getSettings().frameRate || 30);
|
|
64
|
+
timestamp = performance.now();
|
|
65
|
+
},
|
|
66
|
+
pull: async (controller) => {
|
|
67
|
+
if (!running) {
|
|
68
|
+
controller.close();
|
|
69
|
+
this.close();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const delta = performance.now() - timestamp;
|
|
73
|
+
if (delta <= frameDuration) {
|
|
74
|
+
await new Promise((r: (value?: unknown) => void) =>
|
|
75
|
+
this.workerTimer.setTimeout(r, frameDuration - delta),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
timestamp = performance.now();
|
|
79
|
+
|
|
80
|
+
const currentTime = this.video.currentTime;
|
|
81
|
+
const hasNewFrame = currentTime !== lastVideoTime;
|
|
82
|
+
|
|
83
|
+
if (!hasNewFrame) {
|
|
84
|
+
await new Promise((r: (value?: unknown) => void) =>
|
|
85
|
+
this.workerTimer.setTimeout(r, frameDuration),
|
|
86
|
+
);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
lastVideoTime = currentTime;
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
canvas.width !== this.video.videoWidth ||
|
|
94
|
+
canvas.height !== this.video.videoHeight
|
|
95
|
+
) {
|
|
96
|
+
canvas.width = this.video.videoWidth;
|
|
97
|
+
canvas.height = this.video.videoHeight;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ctx.drawImage(this.video, 0, 0);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const frame = new VideoFrame(canvas, { timestamp });
|
|
104
|
+
controller.enqueue(frame);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
running = false;
|
|
107
|
+
controller.error(err);
|
|
108
|
+
this.close();
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
cancel: () => {
|
|
112
|
+
running = false;
|
|
113
|
+
this.close();
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public close = () => {
|
|
119
|
+
this.video.pause();
|
|
120
|
+
this.video.srcObject = null;
|
|
121
|
+
this.video.src = '';
|
|
122
|
+
|
|
123
|
+
this.workerTimer.destroy();
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const TrackProcessor =
|
|
128
|
+
typeof MediaStreamTrackProcessor !== 'undefined'
|
|
129
|
+
? MediaStreamTrackProcessor
|
|
130
|
+
: FallbackProcessor;
|
|
131
|
+
|
|
132
|
+
export { TrackProcessor };
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BACKGROUND_BLUR_MAP,
|
|
3
|
+
BackgroundOptions,
|
|
4
|
+
SegmenterOptions,
|
|
5
|
+
VideoTrackProcessorHooks,
|
|
6
|
+
} from './types';
|
|
7
|
+
import { FilesetResolver, ImageSegmenter } from '@mediapipe/tasks-vision';
|
|
8
|
+
import { WebGLRenderer } from './WebGLRenderer';
|
|
9
|
+
import { packageName, version } from './version';
|
|
10
|
+
import { TrackGenerator, MediaStreamTrackGenerator } from './FallbackGenerator';
|
|
11
|
+
import { MediaStreamTrackProcessor, TrackProcessor } from './FallbackProcessor';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wraps a video MediaStreamTrack in a real-time processing pipeline.
|
|
15
|
+
* Incoming frames are processed through a transformer and re-emitted
|
|
16
|
+
* on a new MediaStreamVideoTrack for downstream consumption.
|
|
17
|
+
*/
|
|
18
|
+
export class VirtualBackground {
|
|
19
|
+
private readonly processor: MediaStreamTrackProcessor<VideoFrame>;
|
|
20
|
+
private readonly generator: MediaStreamTrackGenerator<VideoFrame>;
|
|
21
|
+
|
|
22
|
+
private canvas!: OffscreenCanvas;
|
|
23
|
+
private segmenter: ImageSegmenter | null = null;
|
|
24
|
+
private isSegmenterReady = false;
|
|
25
|
+
private webGlRenderer!: WebGLRenderer;
|
|
26
|
+
private abortController: AbortController;
|
|
27
|
+
|
|
28
|
+
private segmenterDelayTotal = 0;
|
|
29
|
+
private frames = 0;
|
|
30
|
+
private lastStatsTime = 0;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly track: MediaStreamVideoTrack,
|
|
34
|
+
private readonly options: BackgroundOptions = {},
|
|
35
|
+
private readonly hooks: VideoTrackProcessorHooks = {},
|
|
36
|
+
) {
|
|
37
|
+
this.processor = new TrackProcessor({ track });
|
|
38
|
+
this.generator = new TrackGenerator({
|
|
39
|
+
kind: 'video',
|
|
40
|
+
signalTarget: track,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
this.abortController = new AbortController();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public async start(): Promise<MediaStreamTrack> {
|
|
47
|
+
const { onError } = this.hooks;
|
|
48
|
+
|
|
49
|
+
const { readable } = this.processor;
|
|
50
|
+
const { writable } = this.generator;
|
|
51
|
+
|
|
52
|
+
const displayWidth = this.track.getSettings().width ?? 1280;
|
|
53
|
+
const displayHeight = this.track.getSettings().height ?? 720;
|
|
54
|
+
|
|
55
|
+
this.canvas = new OffscreenCanvas(displayWidth, displayHeight);
|
|
56
|
+
this.webGlRenderer = new WebGLRenderer(this.canvas);
|
|
57
|
+
|
|
58
|
+
await this.initializeSegmenter();
|
|
59
|
+
|
|
60
|
+
const opts = await this.initializeSegmenterOptions();
|
|
61
|
+
|
|
62
|
+
const transformStream = new TransformStream<VideoFrame, VideoFrame>({
|
|
63
|
+
transform: async (frame, controller) => {
|
|
64
|
+
try {
|
|
65
|
+
if (this.abortController.signal.aborted) {
|
|
66
|
+
return frame.close();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const processed = await this.transform(frame, opts);
|
|
70
|
+
controller.enqueue(processed);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
console.error('[virtual-background] error processing frame:', e);
|
|
73
|
+
this.hooks.onError?.(e);
|
|
74
|
+
|
|
75
|
+
if (!this.abortController.signal.aborted) {
|
|
76
|
+
controller.enqueue(frame);
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
frame.close();
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
flush: () => {
|
|
83
|
+
if (this.segmenter) {
|
|
84
|
+
this.segmenter.close();
|
|
85
|
+
this.segmenter = null;
|
|
86
|
+
}
|
|
87
|
+
this.isSegmenterReady = false;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const signal = this.abortController.signal;
|
|
92
|
+
|
|
93
|
+
readable
|
|
94
|
+
.pipeThrough(transformStream, { signal })
|
|
95
|
+
.pipeTo(writable)
|
|
96
|
+
.catch((e) => {
|
|
97
|
+
if (e.name !== 'AbortError') {
|
|
98
|
+
console.error('[virtual-background] Error processing track:', e);
|
|
99
|
+
onError?.(e);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return this.generator;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Loads and initializes the MediaPipe `ImageSegmenter`.
|
|
108
|
+
*/
|
|
109
|
+
private async initializeSegmenter() {
|
|
110
|
+
try {
|
|
111
|
+
const basePath =
|
|
112
|
+
this.options?.basePath ||
|
|
113
|
+
`https://unpkg.com/${packageName}@${version}/mediapipe`;
|
|
114
|
+
|
|
115
|
+
const defaultModelPath = `${basePath}/models/selfie_segmenter.tflite`;
|
|
116
|
+
|
|
117
|
+
const model = this.options?.modelPath || defaultModelPath;
|
|
118
|
+
|
|
119
|
+
const wasmPath = `${basePath}/wasm`;
|
|
120
|
+
|
|
121
|
+
const fileset = await FilesetResolver.forVisionTasks(wasmPath);
|
|
122
|
+
|
|
123
|
+
this.segmenter = await ImageSegmenter.createFromOptions(fileset, {
|
|
124
|
+
baseOptions: {
|
|
125
|
+
modelAssetPath: model,
|
|
126
|
+
delegate: 'GPU',
|
|
127
|
+
},
|
|
128
|
+
runningMode: 'VIDEO',
|
|
129
|
+
outputCategoryMask: true,
|
|
130
|
+
outputConfidenceMasks: true,
|
|
131
|
+
canvas: this.canvas,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
this.isSegmenterReady = true;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(
|
|
137
|
+
'[virtual-background] Failed to initialize MediaPipe segmenter:',
|
|
138
|
+
error,
|
|
139
|
+
);
|
|
140
|
+
this.isSegmenterReady = false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Processes a single video frame.
|
|
146
|
+
*
|
|
147
|
+
* Performs segmentation via MediaPipe and then composites the frame
|
|
148
|
+
* through the WebGL renderer to apply background effects.
|
|
149
|
+
*
|
|
150
|
+
* @param frame - The incoming frame from the processor.
|
|
151
|
+
* @param opts - The segmentation options to use.
|
|
152
|
+
*
|
|
153
|
+
* @returns A new `VideoFrame` containing the processed image.
|
|
154
|
+
*/
|
|
155
|
+
private async transform(
|
|
156
|
+
frame: VideoFrame,
|
|
157
|
+
opts: SegmenterOptions,
|
|
158
|
+
): Promise<VideoFrame> {
|
|
159
|
+
if (this.isSegmenterReady && this.segmenter) {
|
|
160
|
+
try {
|
|
161
|
+
const start = performance.now();
|
|
162
|
+
await new Promise<void>((resolve) => {
|
|
163
|
+
this.segmenter!.segmentForVideo(frame, frame.timestamp, (result) => {
|
|
164
|
+
const categoryMask = result.categoryMask!.getAsWebGLTexture();
|
|
165
|
+
const confidenceMask =
|
|
166
|
+
result.confidenceMasks![0].getAsWebGLTexture();
|
|
167
|
+
|
|
168
|
+
this.webGlRenderer.render(
|
|
169
|
+
frame,
|
|
170
|
+
opts,
|
|
171
|
+
categoryMask,
|
|
172
|
+
confidenceMask,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const now = performance.now();
|
|
176
|
+
this.segmenterDelayTotal += now - start;
|
|
177
|
+
this.frames++;
|
|
178
|
+
|
|
179
|
+
if (this.lastStatsTime === 0) {
|
|
180
|
+
this.lastStatsTime = now;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (now - this.lastStatsTime > 1000) {
|
|
184
|
+
const delay =
|
|
185
|
+
Math.round((this.segmenterDelayTotal / this.frames) * 100) /
|
|
186
|
+
100;
|
|
187
|
+
const fps = Math.round(
|
|
188
|
+
(1000 * this.frames) / (now - this.lastStatsTime),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
this.hooks.onStats?.({ delay, fps, timestamp: now });
|
|
192
|
+
|
|
193
|
+
this.lastStatsTime = now;
|
|
194
|
+
this.segmenterDelayTotal = 0;
|
|
195
|
+
this.frames = 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
resolve();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error('[virtual-background] Error during segmentation:', error);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return new VideoFrame(this.canvas, { timestamp: frame.timestamp });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private async loadBackground(url: string | undefined) {
|
|
210
|
+
if (!url) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const response = await fetch(url);
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
console.error(
|
|
217
|
+
`[virtual-background] Failed to fetch background source ${url} (status: ${response.status})`,
|
|
218
|
+
);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const blob = await response.blob();
|
|
222
|
+
|
|
223
|
+
const imageBitmap = await createImageBitmap(blob);
|
|
224
|
+
return { type: 'image', media: imageBitmap, url };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private async initializeSegmenterOptions(): Promise<SegmenterOptions> {
|
|
228
|
+
const isSelfieMode = this.options.modelPath
|
|
229
|
+
? this.options.modelPath?.includes('selfie_segmenter')
|
|
230
|
+
: true;
|
|
231
|
+
|
|
232
|
+
if (this.options.backgroundFilter === 'image') {
|
|
233
|
+
return {
|
|
234
|
+
backgroundSource: await this.loadBackground(
|
|
235
|
+
this.options.backgroundImage,
|
|
236
|
+
),
|
|
237
|
+
bgBlur: 0,
|
|
238
|
+
bgBlurRadius: 0,
|
|
239
|
+
isSelfieMode,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const blurLevel = this.options.backgroundBlurLevel;
|
|
244
|
+
if (typeof blurLevel === 'string') {
|
|
245
|
+
return {
|
|
246
|
+
...BACKGROUND_BLUR_MAP[blurLevel],
|
|
247
|
+
backgroundSource: undefined,
|
|
248
|
+
isSelfieMode,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const numeric = blurLevel ?? 5;
|
|
253
|
+
|
|
254
|
+
const bgBlur = Math.min(numeric * 3, 30);
|
|
255
|
+
const bgBlurRadius = Math.min(numeric, 10);
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
backgroundSource: undefined,
|
|
259
|
+
bgBlur,
|
|
260
|
+
bgBlurRadius,
|
|
261
|
+
isSelfieMode,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
public stop(): void {
|
|
266
|
+
this.abortController.abort();
|
|
267
|
+
this.webGlRenderer.close();
|
|
268
|
+
this.generator.stop();
|
|
269
|
+
|
|
270
|
+
if (this.segmenter) {
|
|
271
|
+
this.segmenter.close();
|
|
272
|
+
this.segmenter = null;
|
|
273
|
+
}
|
|
274
|
+
this.isSegmenterReady = false;
|
|
275
|
+
}
|
|
276
|
+
}
|