@livekit/track-processors 0.4.1 → 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/dist/index.mjs CHANGED
@@ -17,13 +17,29 @@ async function waitForTrackResolution(track) {
17
17
  }
18
18
 
19
19
  // src/ProcessorWrapper.ts
20
- var ProcessorWrapper = class {
21
- static get isSupported() {
22
- return typeof MediaStreamTrackGenerator !== "undefined" && typeof MediaStreamTrackProcessor !== "undefined";
23
- }
24
- constructor(transformer, name) {
20
+ var ProcessorWrapper = class _ProcessorWrapper {
21
+ constructor(transformer, name, options = {}) {
22
+ // For tracking whether we're using the stream API fallback
23
+ this.useStreamFallback = false;
24
+ this.processingEnabled = false;
25
+ var _a;
25
26
  this.name = name;
26
27
  this.transformer = transformer;
28
+ this.maxFps = (_a = options.maxFps) != null ? _a : 30;
29
+ }
30
+ /**
31
+ * Determines if the Processor is supported on the current browser
32
+ */
33
+ static get isSupported() {
34
+ const hasStreamProcessor = typeof MediaStreamTrackGenerator !== "undefined" && typeof MediaStreamTrackProcessor !== "undefined";
35
+ const hasFallbackSupport = typeof HTMLCanvasElement !== "undefined" && typeof VideoFrame !== "undefined" && "captureStream" in HTMLCanvasElement.prototype;
36
+ return hasStreamProcessor || hasFallbackSupport;
37
+ }
38
+ /**
39
+ * Determines if modern browser APIs are supported, which yield better performance
40
+ */
41
+ static get hasModernApiSupport() {
42
+ return typeof MediaStreamTrackGenerator !== "undefined" && typeof MediaStreamTrackProcessor !== "undefined";
27
43
  }
28
44
  async setup(opts) {
29
45
  this.source = opts.track;
@@ -36,41 +52,187 @@ var ProcessorWrapper = class {
36
52
  this.sourceDummy.height = height != null ? height : 300;
37
53
  this.sourceDummy.width = width != null ? width : 300;
38
54
  }
39
- this.processor = new MediaStreamTrackProcessor({ track: this.source });
40
- this.trackGenerator = new MediaStreamTrackGenerator({
41
- kind: "video",
42
- signalTarget: this.source
43
- });
44
- this.canvas = new OffscreenCanvas(width != null ? width : 300, height != null ? height : 300);
55
+ this.useStreamFallback = !_ProcessorWrapper.hasModernApiSupport;
56
+ if (this.useStreamFallback) {
57
+ const existingCanvas = document.querySelector(
58
+ 'canvas[data-livekit-processor="' + this.name + '"]'
59
+ );
60
+ if (existingCanvas) {
61
+ this.displayCanvas = existingCanvas;
62
+ this.displayCanvas.width = width != null ? width : 300;
63
+ this.displayCanvas.height = height != null ? height : 300;
64
+ } else {
65
+ this.displayCanvas = document.createElement("canvas");
66
+ this.displayCanvas.width = width != null ? width : 300;
67
+ this.displayCanvas.height = height != null ? height : 300;
68
+ this.displayCanvas.style.display = "none";
69
+ this.displayCanvas.dataset.livekitProcessor = this.name;
70
+ document.body.appendChild(this.displayCanvas);
71
+ }
72
+ this.renderContext = this.displayCanvas.getContext("2d");
73
+ this.capturedStream = this.displayCanvas.captureStream();
74
+ this.canvas = new OffscreenCanvas(width != null ? width : 300, height != null ? height : 300);
75
+ } else {
76
+ this.processor = new MediaStreamTrackProcessor({ track: this.source });
77
+ this.trackGenerator = new MediaStreamTrackGenerator({
78
+ kind: "video",
79
+ signalTarget: this.source
80
+ });
81
+ this.canvas = new OffscreenCanvas(width != null ? width : 300, height != null ? height : 300);
82
+ }
45
83
  }
46
84
  async init(opts) {
47
85
  await this.setup(opts);
48
- if (!this.canvas || !this.processor || !this.trackGenerator) {
49
- throw new TypeError("Expected both canvas and processor to be defined after setup");
86
+ if (!this.canvas) {
87
+ throw new TypeError("Expected canvas to be defined after setup");
50
88
  }
51
- const readableStream = this.processor.readable;
52
89
  await this.transformer.init({
53
90
  outputCanvas: this.canvas,
54
91
  inputElement: this.sourceDummy
55
92
  });
93
+ if (this.useStreamFallback) {
94
+ this.initFallbackPath();
95
+ } else {
96
+ this.initStreamProcessorPath();
97
+ }
98
+ }
99
+ initStreamProcessorPath() {
100
+ if (!this.processor || !this.trackGenerator) {
101
+ throw new TypeError(
102
+ "Expected processor and trackGenerator to be defined for stream processor path"
103
+ );
104
+ }
105
+ const readableStream = this.processor.readable;
56
106
  const pipedStream = readableStream.pipeThrough(this.transformer.transformer);
57
107
  pipedStream.pipeTo(this.trackGenerator.writable).catch((e) => console.error("error when trying to pipe", e)).finally(() => this.destroy());
58
108
  this.processedTrack = this.trackGenerator;
59
109
  }
110
+ initFallbackPath() {
111
+ if (!this.capturedStream || !this.source || !this.canvas || !this.renderContext) {
112
+ throw new TypeError("Missing required components for fallback implementation");
113
+ }
114
+ this.processedTrack = this.capturedStream.getVideoTracks()[0];
115
+ this.processingEnabled = true;
116
+ this.frameCallback = (frame) => {
117
+ if (!this.processingEnabled || !frame) {
118
+ frame.close();
119
+ return;
120
+ }
121
+ const controller = {
122
+ enqueue: (processedFrame) => {
123
+ if (this.renderContext && this.displayCanvas) {
124
+ this.renderContext.drawImage(
125
+ processedFrame,
126
+ 0,
127
+ 0,
128
+ this.displayCanvas.width,
129
+ this.displayCanvas.height
130
+ );
131
+ processedFrame.close();
132
+ }
133
+ }
134
+ };
135
+ try {
136
+ this.transformer.transform(frame, controller);
137
+ } catch (e) {
138
+ console.error("Error in transform:", e);
139
+ frame.close();
140
+ }
141
+ };
142
+ this.startRenderLoop();
143
+ }
144
+ startRenderLoop() {
145
+ if (!this.sourceDummy || !(this.sourceDummy instanceof HTMLVideoElement)) {
146
+ return;
147
+ }
148
+ let lastVideoTimestamp = -1;
149
+ let lastFrameTime = 0;
150
+ const videoElement = this.sourceDummy;
151
+ const minFrameInterval = 1e3 / this.maxFps;
152
+ let estimatedVideoFps = this.maxFps;
153
+ let frameTimeHistory = [];
154
+ let lastVideoTimeChange = 0;
155
+ let frameCount = 0;
156
+ let lastFpsLog = 0;
157
+ const renderLoop = () => {
158
+ if (!this.processingEnabled || !this.sourceDummy || !(this.sourceDummy instanceof HTMLVideoElement)) {
159
+ return;
160
+ }
161
+ const videoTime = videoElement.currentTime;
162
+ const now = performance.now();
163
+ const timeSinceLastFrame = now - lastFrameTime;
164
+ const hasNewFrame = videoTime !== lastVideoTimestamp;
165
+ if (hasNewFrame) {
166
+ if (lastVideoTimeChange > 0) {
167
+ const timeBetweenFrames = now - lastVideoTimeChange;
168
+ frameTimeHistory.push(timeBetweenFrames);
169
+ if (frameTimeHistory.length > 10) {
170
+ frameTimeHistory.shift();
171
+ }
172
+ if (frameTimeHistory.length > 2) {
173
+ const avgFrameTime = frameTimeHistory.reduce((sum, time) => sum + time, 0) / frameTimeHistory.length;
174
+ estimatedVideoFps = 1e3 / avgFrameTime;
175
+ const isDevelopment = typeof window !== "undefined" && window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
176
+ if (isDevelopment && now - lastFpsLog > 5e3) {
177
+ console.debug(
178
+ `[${this.name}] Estimated video FPS: ${estimatedVideoFps.toFixed(
179
+ 1
180
+ )}, Processing at: ${(frameCount / 5).toFixed(1)} FPS`
181
+ );
182
+ frameCount = 0;
183
+ lastFpsLog = now;
184
+ }
185
+ }
186
+ }
187
+ lastVideoTimeChange = now;
188
+ }
189
+ const timeThresholdMet = timeSinceLastFrame >= minFrameInterval;
190
+ if (hasNewFrame && timeThresholdMet) {
191
+ lastVideoTimestamp = videoTime;
192
+ lastFrameTime = now;
193
+ frameCount++;
194
+ try {
195
+ const frame = new VideoFrame(videoElement);
196
+ if (this.frameCallback) {
197
+ this.frameCallback(frame);
198
+ } else {
199
+ frame.close();
200
+ }
201
+ } catch (e) {
202
+ console.error("Error in render loop:", e);
203
+ }
204
+ }
205
+ this.animationFrameId = requestAnimationFrame(renderLoop);
206
+ };
207
+ this.animationFrameId = requestAnimationFrame(renderLoop);
208
+ }
60
209
  async restart(opts) {
61
210
  await this.destroy();
62
- return this.init(opts);
211
+ await this.init(opts);
63
212
  }
64
213
  async restartTransformer(...options) {
65
- this.transformer.restart(options[0]);
214
+ await this.transformer.restart(options[0]);
66
215
  }
67
216
  async updateTransformerOptions(...options) {
68
- this.transformer.update(options[0]);
217
+ await this.transformer.update(options[0]);
69
218
  }
70
219
  async destroy() {
71
- var _a;
220
+ var _a, _b, _c, _d;
221
+ if (this.useStreamFallback) {
222
+ this.processingEnabled = false;
223
+ if (this.animationFrameId) {
224
+ cancelAnimationFrame(this.animationFrameId);
225
+ this.animationFrameId = void 0;
226
+ }
227
+ if (this.displayCanvas && this.displayCanvas.parentNode) {
228
+ this.displayCanvas.parentNode.removeChild(this.displayCanvas);
229
+ }
230
+ (_a = this.capturedStream) == null ? void 0 : _a.getTracks().forEach((track) => track.stop());
231
+ } else {
232
+ await ((_c = (_b = this.processor) == null ? void 0 : _b.writableControl) == null ? void 0 : _c.close());
233
+ (_d = this.trackGenerator) == null ? void 0 : _d.stop();
234
+ }
72
235
  await this.transformer.destroy();
73
- (_a = this.trackGenerator) == null ? void 0 : _a.stop();
74
236
  }
75
237
  };
76
238
 
@@ -79,7 +241,348 @@ import * as vision from "@mediapipe/tasks-vision";
79
241
 
80
242
  // package.json
81
243
  var dependencies = {
82
- "@mediapipe/tasks-vision": "0.10.21"
244
+ "@mediapipe/tasks-vision": "^0.10.22-rc.20250304"
245
+ };
246
+
247
+ // src/webgl/index.ts
248
+ var blurFragmentShader = `
249
+ precision highp float;
250
+ varying vec2 texCoords;
251
+ uniform sampler2D u_texture;
252
+ uniform vec2 u_texelSize;
253
+ uniform vec2 u_direction;
254
+ uniform float u_radius;
255
+
256
+ void main() {
257
+ float sigma = u_radius;
258
+ float twoSigmaSq = 2.0 * sigma * sigma;
259
+ float totalWeight = 0.0;
260
+ vec3 result = vec3(0.0);
261
+ const int MAX_SAMPLES = 16;
262
+ int radius = int(min(float(MAX_SAMPLES), ceil(u_radius)));
263
+
264
+ for (int i = -MAX_SAMPLES; i <= MAX_SAMPLES; ++i) {
265
+ float offset = float(i);
266
+ if (abs(offset) > float(radius)) continue;
267
+ float weight = exp(-(offset * offset) / twoSigmaSq);
268
+ vec2 sampleCoord = texCoords + u_direction * u_texelSize * offset;
269
+ result += texture2D(u_texture, sampleCoord).rgb * weight;
270
+ totalWeight += weight;
271
+ }
272
+
273
+ gl_FragColor = vec4(result / totalWeight, 1.0);
274
+ }
275
+ `;
276
+ var createShaderProgram = (gl) => {
277
+ const vs = `
278
+ attribute vec2 position;
279
+ varying vec2 texCoords;
280
+
281
+ void main() {
282
+ texCoords = (position + 1.0) / 2.0;
283
+ texCoords.y = 1.0 - texCoords.y;
284
+ gl_Position = vec4(position, 0, 1.0);
285
+ }
286
+ `;
287
+ const cS = `
288
+ precision highp float;
289
+ varying vec2 texCoords;
290
+ uniform sampler2D background;
291
+ uniform sampler2D frame;
292
+ uniform sampler2D mask;
293
+ void main() {
294
+ vec4 maskTex = texture2D(mask, texCoords);
295
+ vec4 frameTex = texture2D(frame, texCoords);
296
+ vec4 bgTex = texture2D(background, texCoords);
297
+
298
+
299
+ float a = maskTex.r;
300
+
301
+ gl_FragColor = mix(bgTex, vec4(frameTex.rgb, 1.0), 1.0 - a);
302
+
303
+ }
304
+ `;
305
+ const vertexShader = gl.createShader(gl.VERTEX_SHADER);
306
+ if (!vertexShader) {
307
+ throw Error("can not create vertex shader");
308
+ }
309
+ gl.shaderSource(vertexShader, vs);
310
+ gl.compileShader(vertexShader);
311
+ const compositeShader = gl.createShader(gl.FRAGMENT_SHADER);
312
+ if (!compositeShader) {
313
+ throw Error("can not create fragment shader");
314
+ }
315
+ gl.shaderSource(compositeShader, cS);
316
+ gl.compileShader(compositeShader);
317
+ const compositeProgram = gl.createProgram();
318
+ if (!compositeProgram) {
319
+ throw Error("can not create composite program");
320
+ }
321
+ gl.attachShader(compositeProgram, vertexShader);
322
+ gl.attachShader(compositeProgram, compositeShader);
323
+ gl.linkProgram(compositeProgram);
324
+ let blurProgram = null;
325
+ let blurVertexShader = null;
326
+ let blurFrag = null;
327
+ let blurUniforms = null;
328
+ blurFrag = gl.createShader(gl.FRAGMENT_SHADER);
329
+ if (!blurFrag) {
330
+ throw Error("can not create blur shader");
331
+ }
332
+ gl.shaderSource(blurFrag, blurFragmentShader);
333
+ gl.compileShader(blurFrag);
334
+ if (!gl.getShaderParameter(blurFrag, gl.COMPILE_STATUS)) {
335
+ const info = gl.getShaderInfoLog(blurFrag);
336
+ throw Error(`Failed to compile blur shader: ${info}`);
337
+ }
338
+ blurVertexShader = gl.createShader(gl.VERTEX_SHADER);
339
+ if (!blurVertexShader) {
340
+ throw Error("can not create blur vertex shader");
341
+ }
342
+ gl.shaderSource(blurVertexShader, vs);
343
+ gl.compileShader(blurVertexShader);
344
+ blurProgram = gl.createProgram();
345
+ if (!blurProgram) {
346
+ throw Error("can not create blur program");
347
+ }
348
+ gl.attachShader(blurProgram, blurVertexShader);
349
+ gl.attachShader(blurProgram, blurFrag);
350
+ gl.linkProgram(blurProgram);
351
+ if (!gl.getProgramParameter(blurProgram, gl.LINK_STATUS)) {
352
+ const info = gl.getProgramInfoLog(blurProgram);
353
+ throw Error(`Failed to link blur program: ${info}`);
354
+ }
355
+ blurUniforms = {
356
+ position: gl.getAttribLocation(blurProgram, "position"),
357
+ texture: gl.getUniformLocation(blurProgram, "u_texture"),
358
+ texelSize: gl.getUniformLocation(blurProgram, "u_texelSize"),
359
+ direction: gl.getUniformLocation(blurProgram, "u_direction"),
360
+ radius: gl.getUniformLocation(blurProgram, "u_radius")
361
+ };
362
+ return {
363
+ vertexShader,
364
+ compositeShader,
365
+ blurShader: blurFrag,
366
+ compositeProgram,
367
+ blurProgram,
368
+ attribLocations: {
369
+ position: gl.getAttribLocation(compositeProgram, "position")
370
+ },
371
+ uniformLocations: {
372
+ mask: gl.getUniformLocation(compositeProgram, "mask"),
373
+ frame: gl.getUniformLocation(compositeProgram, "frame"),
374
+ background: gl.getUniformLocation(compositeProgram, "background")
375
+ },
376
+ blurUniforms
377
+ };
378
+ };
379
+ function initTexture(gl, texIndex) {
380
+ const texRef = gl.TEXTURE0 + texIndex;
381
+ gl.activeTexture(texRef);
382
+ const texture = gl.createTexture();
383
+ gl.bindTexture(gl.TEXTURE_2D, texture);
384
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
385
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
386
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
387
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
388
+ gl.bindTexture(gl.TEXTURE_2D, texture);
389
+ return texture;
390
+ }
391
+ function createFramebuffer(gl, texture, width, height) {
392
+ const framebuffer = gl.createFramebuffer();
393
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
394
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
395
+ gl.bindTexture(gl.TEXTURE_2D, texture);
396
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
397
+ const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
398
+ if (status !== gl.FRAMEBUFFER_COMPLETE) {
399
+ throw new Error("Framebuffer not complete");
400
+ }
401
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
402
+ return framebuffer;
403
+ }
404
+ var createVertexBuffer = (gl) => {
405
+ if (!gl) {
406
+ return null;
407
+ }
408
+ const vertexBuffer = gl.createBuffer();
409
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
410
+ gl.bufferData(
411
+ gl.ARRAY_BUFFER,
412
+ new Float32Array([-1, -1, -1, 1, 1, 1, -1, -1, 1, 1, 1, -1]),
413
+ gl.STATIC_DRAW
414
+ );
415
+ return vertexBuffer;
416
+ };
417
+ var setupWebGL = (canvas) => {
418
+ const gl = canvas.getContext("webgl2", { premultipliedAlpha: false });
419
+ let blurRadius = null;
420
+ if (!gl) {
421
+ return void 0;
422
+ }
423
+ gl.enable(gl.BLEND);
424
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
425
+ const {
426
+ compositeProgram,
427
+ blurProgram,
428
+ attribLocations: { position: positionLocation },
429
+ uniformLocations: {
430
+ mask: maskTextureLocation,
431
+ frame: frameTextureLocation,
432
+ background: bgTextureLocation
433
+ },
434
+ blurUniforms
435
+ } = createShaderProgram(gl);
436
+ const bgTexture = initTexture(gl, 0);
437
+ const frameTexture = initTexture(gl, 1);
438
+ const vertexBuffer = createVertexBuffer(gl);
439
+ let processTextures = [];
440
+ let processFramebuffers = [];
441
+ processTextures.push(initTexture(gl, 3));
442
+ processTextures.push(initTexture(gl, 4));
443
+ processFramebuffers.push(createFramebuffer(gl, processTextures[0], canvas.width, canvas.height));
444
+ processFramebuffers.push(createFramebuffer(gl, processTextures[1], canvas.width, canvas.height));
445
+ gl.useProgram(compositeProgram);
446
+ gl.uniform1i(bgTextureLocation, 0);
447
+ gl.uniform1i(frameTextureLocation, 1);
448
+ gl.uniform1i(maskTextureLocation, 2);
449
+ let customBackgroundImage = null;
450
+ function applyBlur(sourceTexture, width, height) {
451
+ if (!blurRadius || !blurProgram || !blurUniforms)
452
+ return bgTexture;
453
+ gl.useProgram(blurProgram);
454
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
455
+ gl.vertexAttribPointer(blurUniforms.position, 2, gl.FLOAT, false, 0, 0);
456
+ gl.enableVertexAttribArray(blurUniforms.position);
457
+ const texelWidth = 1 / width;
458
+ const texelHeight = 1 / height;
459
+ gl.bindFramebuffer(gl.FRAMEBUFFER, processFramebuffers[0]);
460
+ gl.viewport(0, 0, width, height);
461
+ gl.activeTexture(gl.TEXTURE0);
462
+ gl.bindTexture(gl.TEXTURE_2D, sourceTexture);
463
+ gl.uniform1i(blurUniforms.texture, 0);
464
+ gl.uniform2f(blurUniforms.texelSize, texelWidth, texelHeight);
465
+ gl.uniform2f(blurUniforms.direction, 1, 0);
466
+ gl.uniform1f(blurUniforms.radius, blurRadius);
467
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
468
+ gl.bindFramebuffer(gl.FRAMEBUFFER, processFramebuffers[1]);
469
+ gl.viewport(0, 0, width, height);
470
+ gl.activeTexture(gl.TEXTURE0);
471
+ gl.bindTexture(gl.TEXTURE_2D, processTextures[0]);
472
+ gl.uniform1i(blurUniforms.texture, 0);
473
+ gl.uniform2f(blurUniforms.direction, 0, 1);
474
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
475
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
476
+ return processTextures[1];
477
+ }
478
+ function render(frame, mask) {
479
+ if (frame.codedWidth === 0 || mask.width === 0) {
480
+ return;
481
+ }
482
+ const width = frame.displayWidth;
483
+ const height = frame.displayHeight;
484
+ gl.activeTexture(gl.TEXTURE1);
485
+ gl.bindTexture(gl.TEXTURE_2D, frameTexture);
486
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, frame);
487
+ let backgroundTexture = bgTexture;
488
+ if (customBackgroundImage) {
489
+ gl.activeTexture(gl.TEXTURE0);
490
+ gl.bindTexture(gl.TEXTURE_2D, bgTexture);
491
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, customBackgroundImage);
492
+ backgroundTexture = bgTexture;
493
+ } else if (blurRadius) {
494
+ backgroundTexture = applyBlur(frameTexture, width, height);
495
+ }
496
+ const maskTexture = mask.getAsWebGLTexture();
497
+ gl.viewport(0, 0, width, height);
498
+ gl.clearColor(1, 1, 1, 1);
499
+ gl.clear(gl.COLOR_BUFFER_BIT);
500
+ gl.useProgram(compositeProgram);
501
+ gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
502
+ gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
503
+ gl.enableVertexAttribArray(positionLocation);
504
+ gl.activeTexture(gl.TEXTURE0);
505
+ gl.bindTexture(gl.TEXTURE_2D, backgroundTexture);
506
+ gl.uniform1i(bgTextureLocation, 0);
507
+ gl.activeTexture(gl.TEXTURE1);
508
+ gl.bindTexture(gl.TEXTURE_2D, frameTexture);
509
+ gl.uniform1i(frameTextureLocation, 1);
510
+ gl.activeTexture(gl.TEXTURE2);
511
+ gl.bindTexture(gl.TEXTURE_2D, maskTexture);
512
+ gl.uniform1i(maskTextureLocation, 2);
513
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
514
+ mask.close();
515
+ }
516
+ async function setBackgroundImage(image) {
517
+ customBackgroundImage = null;
518
+ if (image) {
519
+ try {
520
+ const canvasWidth = canvas.width;
521
+ const canvasHeight = canvas.height;
522
+ const imgAspect = image.width / image.height;
523
+ const canvasAspect = canvasWidth / canvasHeight;
524
+ let sx = 0;
525
+ let sy = 0;
526
+ let sWidth = image.width;
527
+ let sHeight = image.height;
528
+ if (imgAspect > canvasAspect) {
529
+ sWidth = Math.round(image.height * canvasAspect);
530
+ sx = Math.round((image.width - sWidth) / 2);
531
+ } else if (imgAspect < canvasAspect) {
532
+ sHeight = Math.round(image.width / canvasAspect);
533
+ sy = Math.round((image.height - sHeight) / 2);
534
+ }
535
+ const croppedImage = await createImageBitmap(image, sx, sy, sWidth, sHeight, {
536
+ resizeWidth: canvasWidth,
537
+ resizeHeight: canvasHeight,
538
+ resizeQuality: "medium"
539
+ });
540
+ customBackgroundImage = croppedImage;
541
+ gl.activeTexture(gl.TEXTURE0);
542
+ gl.bindTexture(gl.TEXTURE_2D, bgTexture);
543
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, croppedImage);
544
+ } catch (error) {
545
+ console.error("Error processing background image:", error);
546
+ customBackgroundImage = image;
547
+ gl.activeTexture(gl.TEXTURE0);
548
+ gl.bindTexture(gl.TEXTURE_2D, bgTexture);
549
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
550
+ }
551
+ } else {
552
+ const emptyImage = new ImageData(2, 2);
553
+ emptyImage.data[0] = 0;
554
+ emptyImage.data[1] = 0;
555
+ emptyImage.data[2] = 0;
556
+ emptyImage.data[3] = 0;
557
+ gl.activeTexture(gl.TEXTURE0);
558
+ gl.bindTexture(gl.TEXTURE_2D, bgTexture);
559
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, emptyImage);
560
+ }
561
+ }
562
+ function setBlurRadius(radius) {
563
+ blurRadius = radius;
564
+ setBackgroundImage(null);
565
+ }
566
+ function cleanup() {
567
+ gl.deleteProgram(compositeProgram);
568
+ gl.deleteProgram(blurProgram);
569
+ gl.deleteTexture(bgTexture);
570
+ gl.deleteTexture(frameTexture);
571
+ for (const texture of processTextures) {
572
+ gl.deleteTexture(texture);
573
+ }
574
+ for (const framebuffer of processFramebuffers) {
575
+ gl.deleteFramebuffer(framebuffer);
576
+ }
577
+ gl.deleteBuffer(vertexBuffer);
578
+ if (customBackgroundImage) {
579
+ customBackgroundImage.close();
580
+ customBackgroundImage = null;
581
+ }
582
+ processTextures = [];
583
+ processFramebuffers = [];
584
+ }
585
+ return { render, setBackgroundImage, setBlurRadius, cleanup };
83
586
  };
84
587
 
85
588
  // src/transformers/VideoTransformer.ts
@@ -91,7 +594,6 @@ var VideoTransformer = class {
91
594
  outputCanvas,
92
595
  inputElement: inputVideo
93
596
  }) {
94
- var _a;
95
597
  if (!(inputVideo instanceof HTMLVideoElement)) {
96
598
  throw TypeError("Video transformer needs a HTMLVideoElement as input");
97
599
  }
@@ -100,21 +602,29 @@ var VideoTransformer = class {
100
602
  });
101
603
  this.canvas = outputCanvas || null;
102
604
  if (outputCanvas) {
103
- this.ctx = ((_a = this.canvas) == null ? void 0 : _a.getContext("2d")) || void 0;
605
+ this.gl = setupWebGL(
606
+ this.canvas || new OffscreenCanvas(inputVideo.videoWidth, inputVideo.videoHeight)
607
+ );
104
608
  }
105
609
  this.inputVideo = inputVideo;
106
610
  this.isDisabled = false;
107
611
  }
108
612
  async restart({ outputCanvas, inputElement: inputVideo }) {
613
+ var _a;
109
614
  this.canvas = outputCanvas || null;
110
- this.ctx = this.canvas.getContext("2d") || void 0;
615
+ (_a = this.gl) == null ? void 0 : _a.cleanup();
616
+ this.gl = setupWebGL(
617
+ this.canvas || new OffscreenCanvas(inputVideo.videoWidth, inputVideo.videoHeight)
618
+ );
111
619
  this.inputVideo = inputVideo;
112
620
  this.isDisabled = false;
113
621
  }
114
622
  async destroy() {
623
+ var _a;
115
624
  this.isDisabled = true;
116
625
  this.canvas = void 0;
117
- this.ctx = void 0;
626
+ (_a = this.gl) == null ? void 0 : _a.cleanup();
627
+ this.gl = void 0;
118
628
  }
119
629
  };
120
630
 
@@ -127,10 +637,10 @@ var BackgroundProcessor = class extends VideoTransformer {
127
637
  this.update(opts);
128
638
  }
129
639
  static get isSupported() {
130
- return typeof OffscreenCanvas !== "undefined";
640
+ return typeof OffscreenCanvas !== "undefined" && typeof VideoFrame !== "undefined" && typeof createImageBitmap !== "undefined" && !!document.createElement("canvas").getContext("webgl2");
131
641
  }
132
642
  async init({ outputCanvas, inputElement: inputVideo }) {
133
- var _a, _b, _c, _d, _e;
643
+ var _a, _b, _c, _d, _e, _f;
134
644
  await super.init({ outputCanvas, inputElement: inputVideo });
135
645
  const fileSet = await vision.FilesetResolver.forVisionTasks(
136
646
  (_b = (_a = this.options.assetPaths) == null ? void 0 : _a.tasksVisionFileSet) != null ? _b : `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${dependencies["@mediapipe/tasks-vision"]}/wasm`
@@ -141,6 +651,7 @@ var BackgroundProcessor = class extends VideoTransformer {
141
651
  delegate: "GPU",
142
652
  ...this.options.segmenterOptions
143
653
  },
654
+ canvas: this.canvas,
144
655
  runningMode: "VIDEO",
145
656
  outputCategoryMask: true,
146
657
  outputConfidenceMasks: false
@@ -150,6 +661,9 @@ var BackgroundProcessor = class extends VideoTransformer {
150
661
  (err) => console.error("Error while loading processor background image: ", err)
151
662
  );
152
663
  }
664
+ if (this.options.blurRadius) {
665
+ (_f = this.gl) == null ? void 0 : _f.setBlurRadius(this.options.blurRadius);
666
+ }
153
667
  }
154
668
  async destroy() {
155
669
  var _a;
@@ -158,6 +672,7 @@ var BackgroundProcessor = class extends VideoTransformer {
158
672
  this.backgroundImage = null;
159
673
  }
160
674
  async loadBackground(path) {
675
+ var _a;
161
676
  const img = new Image();
162
677
  await new Promise((resolve, reject) => {
163
678
  img.crossOrigin = "Anonymous";
@@ -166,12 +681,12 @@ var BackgroundProcessor = class extends VideoTransformer {
166
681
  img.src = path;
167
682
  });
168
683
  const imageData = await createImageBitmap(img);
169
- this.backgroundImage = imageData;
684
+ (_a = this.gl) == null ? void 0 : _a.setBackgroundImage(imageData);
170
685
  }
171
686
  async transform(frame, controller) {
172
687
  var _a;
173
688
  try {
174
- if (!(frame instanceof VideoFrame)) {
689
+ if (!(frame instanceof VideoFrame) || frame.codedWidth === 0 || frame.codedHeight === 0) {
175
690
  console.debug("empty frame detected, ignoring");
176
691
  return;
177
692
  }
@@ -182,123 +697,97 @@ var BackgroundProcessor = class extends VideoTransformer {
182
697
  if (!this.canvas) {
183
698
  throw TypeError("Canvas needs to be initialized first");
184
699
  }
700
+ this.canvas.width = frame.displayWidth;
701
+ this.canvas.height = frame.displayHeight;
185
702
  let startTimeMs = performance.now();
186
- (_a = this.imageSegmenter) == null ? void 0 : _a.segmentForVideo(
187
- this.inputVideo,
188
- startTimeMs,
189
- (result) => this.segmentationResults = result
190
- );
191
- if (this.blurRadius) {
192
- await this.blurBackground(frame);
193
- } else {
194
- await this.drawVirtualBackground(frame);
195
- }
196
- const newFrame = new VideoFrame(this.canvas, {
197
- timestamp: frame.timestamp || Date.now()
703
+ (_a = this.imageSegmenter) == null ? void 0 : _a.segmentForVideo(frame, startTimeMs, (result) => {
704
+ var _a2, _b;
705
+ const segmentationTimeMs = performance.now() - startTimeMs;
706
+ this.segmentationResults = result;
707
+ this.drawFrame(frame);
708
+ if (this.canvas && this.canvas.width > 0 && this.canvas.height > 0) {
709
+ const newFrame = new VideoFrame(this.canvas, {
710
+ timestamp: frame.timestamp || Date.now()
711
+ });
712
+ const filterTimeMs = performance.now() - startTimeMs - segmentationTimeMs;
713
+ const stats = {
714
+ processingTimeMs: performance.now() - startTimeMs,
715
+ segmentationTimeMs,
716
+ filterTimeMs
717
+ };
718
+ (_b = (_a2 = this.options).onFrameProcessed) == null ? void 0 : _b.call(_a2, stats);
719
+ controller.enqueue(newFrame);
720
+ } else {
721
+ controller.enqueue(frame);
722
+ }
723
+ frame.close();
198
724
  });
199
- controller.enqueue(newFrame);
200
- } finally {
725
+ } catch (e) {
726
+ console.error("Error while processing frame: ", e);
201
727
  frame == null ? void 0 : frame.close();
202
728
  }
203
729
  }
204
730
  async update(opts) {
205
- this.options = opts;
731
+ var _a;
732
+ this.options = { ...this.options, ...opts };
206
733
  if (opts.blurRadius) {
207
- this.blurRadius = opts.blurRadius;
734
+ (_a = this.gl) == null ? void 0 : _a.setBlurRadius(opts.blurRadius);
208
735
  } else if (opts.imagePath) {
209
736
  await this.loadBackground(opts.imagePath);
210
737
  }
211
738
  }
212
- async drawVirtualBackground(frame) {
213
- var _a;
214
- if (!this.canvas || !this.ctx || !this.segmentationResults || !this.inputVideo)
215
- return;
216
- if (((_a = this.segmentationResults) == null ? void 0 : _a.categoryMask) && this.segmentationResults.categoryMask.width > 0) {
217
- this.ctx.globalCompositeOperation = "copy";
218
- this.ctx.putImageData(
219
- maskToImageData(
220
- this.segmentationResults.categoryMask,
221
- this.segmentationResults.categoryMask.width,
222
- this.segmentationResults.categoryMask.height
223
- ),
224
- 0,
225
- 0
226
- );
227
- this.ctx.filter = "none";
228
- this.ctx.globalCompositeOperation = "source-in";
229
- if (this.backgroundImage) {
230
- this.ctx.drawImage(
231
- this.backgroundImage,
232
- 0,
233
- 0,
234
- this.backgroundImage.width,
235
- this.backgroundImage.height,
236
- 0,
237
- 0,
238
- this.canvas.width,
239
- this.canvas.height
240
- );
241
- } else {
242
- this.ctx.fillStyle = "#00FF00";
243
- this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
244
- }
245
- this.ctx.globalCompositeOperation = "destination-over";
246
- }
247
- this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
248
- }
249
- async blurBackground(frame) {
250
- var _a, _b, _c;
251
- if (!this.ctx || !this.canvas || !((_b = (_a = this.segmentationResults) == null ? void 0 : _a.categoryMask) == null ? void 0 : _b.canvas) || !this.inputVideo) {
739
+ async drawFrame(frame) {
740
+ if (!this.canvas || !this.gl || !this.segmentationResults || !this.inputVideo)
252
741
  return;
253
- }
254
- this.ctx.save();
255
- this.ctx.globalCompositeOperation = "copy";
256
- if (((_c = this.segmentationResults) == null ? void 0 : _c.categoryMask) && this.segmentationResults.categoryMask.width > 0) {
257
- this.ctx.putImageData(
258
- maskToImageData(
259
- this.segmentationResults.categoryMask,
260
- this.segmentationResults.categoryMask.width,
261
- this.segmentationResults.categoryMask.height
262
- ),
263
- 0,
264
- 0
265
- );
266
- this.ctx.filter = "none";
267
- this.ctx.globalCompositeOperation = "source-out";
268
- this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
269
- this.ctx.globalCompositeOperation = "destination-over";
270
- this.ctx.filter = `blur(${this.blurRadius}px)`;
271
- this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
272
- this.ctx.restore();
742
+ const mask = this.segmentationResults.categoryMask;
743
+ if (mask) {
744
+ this.gl.render(frame, mask);
273
745
  }
274
746
  }
275
747
  };
276
- function maskToImageData(mask, videoWidth, videoHeight) {
277
- const dataArray = new Uint8ClampedArray(videoWidth * videoHeight * 4);
278
- const result = mask.getAsUint8Array();
279
- for (let i = 0; i < result.length; i += 1) {
280
- const offset = i * 4;
281
- dataArray[offset] = result[i];
282
- dataArray[offset + 1] = result[i];
283
- dataArray[offset + 2] = result[i];
284
- dataArray[offset + 3] = result[i];
285
- }
286
- return new ImageData(dataArray, videoWidth, videoHeight);
287
- }
288
748
 
289
749
  // src/index.ts
290
- var BackgroundBlur = (blurRadius = 10, segmenterOptions) => {
291
- return BackgroundProcessor2({ blurRadius, segmenterOptions }, "background-blur");
750
+ var supportsBackgroundProcessors = () => BackgroundProcessor.isSupported && ProcessorWrapper.isSupported;
751
+ var supportsModernBackgroundProcessors = () => BackgroundProcessor.isSupported && ProcessorWrapper.hasModernApiSupport;
752
+ var BackgroundBlur = (blurRadius = 10, segmenterOptions, onFrameProcessed, processorOptions) => {
753
+ return BackgroundProcessor2(
754
+ {
755
+ blurRadius,
756
+ segmenterOptions,
757
+ onFrameProcessed,
758
+ ...processorOptions
759
+ },
760
+ "background-blur"
761
+ );
292
762
  };
293
- var VirtualBackground = (imagePath, segmenterOptions) => {
294
- return BackgroundProcessor2({ imagePath, segmenterOptions }, "virtual-background");
763
+ var VirtualBackground = (imagePath, segmenterOptions, onFrameProcessed, processorOptions) => {
764
+ return BackgroundProcessor2(
765
+ {
766
+ imagePath,
767
+ segmenterOptions,
768
+ onFrameProcessed,
769
+ ...processorOptions
770
+ },
771
+ "virtual-background"
772
+ );
295
773
  };
296
774
  var BackgroundProcessor2 = (options, name = "background-processor") => {
297
- const isProcessorSupported = ProcessorWrapper.isSupported && BackgroundProcessor.isSupported;
775
+ const isTransformerSupported = BackgroundProcessor.isSupported;
776
+ const isProcessorSupported = ProcessorWrapper.isSupported;
777
+ if (!isTransformerSupported) {
778
+ throw new Error("Background transformer is not supported in this browser");
779
+ }
298
780
  if (!isProcessorSupported) {
299
- throw new Error("processor is not supported in this browser");
781
+ throw new Error(
782
+ "Neither MediaStreamTrackProcessor nor canvas.captureStream() fallback is supported in this browser"
783
+ );
300
784
  }
301
- const processor = new ProcessorWrapper(new BackgroundProcessor(options), name);
785
+ const { blurRadius, imagePath, segmenterOptions, onFrameProcessed, ...processorOpts } = options;
786
+ const processor = new ProcessorWrapper(
787
+ new BackgroundProcessor({ blurRadius, imagePath, segmenterOptions, onFrameProcessed }),
788
+ name,
789
+ processorOpts
790
+ );
302
791
  return processor;
303
792
  };
304
793
  export {
@@ -307,6 +796,8 @@ export {
307
796
  BackgroundProcessor as BackgroundTransformer,
308
797
  ProcessorWrapper,
309
798
  VideoTransformer,
310
- VirtualBackground
799
+ VirtualBackground,
800
+ supportsBackgroundProcessors,
801
+ supportsModernBackgroundProcessors
311
802
  };
312
803
  //# sourceMappingURL=index.mjs.map