@livekit/track-processors 0.5.3 → 0.5.5

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.
@@ -1,305 +1,119 @@
1
- import { MPMask } from '@mediapipe/tasks-vision';
2
-
3
- // Define the blur fragment shader
4
- const blurFragmentShader = `
5
- precision highp float;
6
- varying vec2 texCoords;
7
- uniform sampler2D u_texture;
8
- uniform vec2 u_texelSize;
9
- uniform vec2 u_direction;
10
- uniform float u_radius;
11
-
12
- void main() {
13
- float sigma = u_radius;
14
- float twoSigmaSq = 2.0 * sigma * sigma;
15
- float totalWeight = 0.0;
16
- vec3 result = vec3(0.0);
17
- const int MAX_SAMPLES = 16;
18
- int radius = int(min(float(MAX_SAMPLES), ceil(u_radius)));
19
-
20
- for (int i = -MAX_SAMPLES; i <= MAX_SAMPLES; ++i) {
21
- float offset = float(i);
22
- if (abs(offset) > float(radius)) continue;
23
- float weight = exp(-(offset * offset) / twoSigmaSq);
24
- vec2 sampleCoord = texCoords + u_direction * u_texelSize * offset;
25
- result += texture2D(u_texture, sampleCoord).rgb * weight;
26
- totalWeight += weight;
27
- }
28
-
29
- gl_FragColor = vec4(result / totalWeight, 1.0);
30
- }
31
- `;
32
-
33
- const createShaderProgram = (gl: WebGL2RenderingContext) => {
34
- const vs = `
35
- attribute vec2 position;
36
- varying vec2 texCoords;
37
-
38
- void main() {
39
- texCoords = (position + 1.0) / 2.0;
40
- texCoords.y = 1.0 - texCoords.y;
41
- gl_Position = vec4(position, 0, 1.0);
42
- }
43
- `;
44
-
45
- const cS = `
46
- precision highp float;
47
- varying vec2 texCoords;
48
- uniform sampler2D background;
49
- uniform sampler2D frame;
50
- uniform sampler2D mask;
51
- void main() {
52
- vec4 maskTex = texture2D(mask, texCoords);
53
- vec4 frameTex = texture2D(frame, texCoords);
54
- vec4 bgTex = texture2D(background, texCoords);
55
-
56
-
57
- float a = maskTex.r;
58
-
59
- gl_FragColor = mix(bgTex, vec4(frameTex.rgb, 1.0), 1.0 - a);
60
-
61
- }
62
- `;
63
-
64
- const vertexShader = gl.createShader(gl.VERTEX_SHADER);
65
- if (!vertexShader) {
66
- throw Error('can not create vertex shader');
67
- }
68
- gl.shaderSource(vertexShader, vs);
69
- gl.compileShader(vertexShader);
70
-
71
- // Create our fragment shader
72
- const compositeShader = gl.createShader(gl.FRAGMENT_SHADER);
73
- if (!compositeShader) {
74
- throw Error('can not create fragment shader');
75
- }
76
- gl.shaderSource(compositeShader, cS);
77
- gl.compileShader(compositeShader);
78
-
79
- // Create the composite program
80
- const compositeProgram = gl.createProgram();
81
- if (!compositeProgram) {
82
- throw Error('can not create composite program');
83
- }
84
- gl.attachShader(compositeProgram, vertexShader);
85
- gl.attachShader(compositeProgram, compositeShader);
86
- gl.linkProgram(compositeProgram);
87
-
88
- let blurProgram = null;
89
- let blurVertexShader = null;
90
- let blurFrag = null;
91
- let blurUniforms = null;
92
-
93
- // Create blur shader if enabled
94
- blurFrag = gl.createShader(gl.FRAGMENT_SHADER);
95
- if (!blurFrag) {
96
- throw Error('can not create blur shader');
97
- }
98
- gl.shaderSource(blurFrag, blurFragmentShader);
99
- gl.compileShader(blurFrag);
100
-
101
- // Get compile status and log errors if any
102
- if (!gl.getShaderParameter(blurFrag, gl.COMPILE_STATUS)) {
103
- const info = gl.getShaderInfoLog(blurFrag);
104
- throw Error(`Failed to compile blur shader: ${info}`);
105
- }
106
-
107
- // Create blur program
108
- blurVertexShader = gl.createShader(gl.VERTEX_SHADER);
109
- if (!blurVertexShader) {
110
- throw Error('can not create blur vertex shader');
111
- }
112
- gl.shaderSource(blurVertexShader, vs);
113
- gl.compileShader(blurVertexShader);
114
-
115
- blurProgram = gl.createProgram();
116
- if (!blurProgram) {
117
- throw Error('can not create blur program');
118
- }
119
- gl.attachShader(blurProgram, blurVertexShader);
120
- gl.attachShader(blurProgram, blurFrag);
121
- gl.linkProgram(blurProgram);
122
-
123
- // Check blur program link status
124
- if (!gl.getProgramParameter(blurProgram, gl.LINK_STATUS)) {
125
- const info = gl.getProgramInfoLog(blurProgram);
126
- throw Error(`Failed to link blur program: ${info}`);
127
- }
128
-
129
- blurUniforms = {
130
- position: gl.getAttribLocation(blurProgram, 'position'),
131
- texture: gl.getUniformLocation(blurProgram, 'u_texture'),
132
- texelSize: gl.getUniformLocation(blurProgram, 'u_texelSize'),
133
- direction: gl.getUniformLocation(blurProgram, 'u_direction'),
134
- radius: gl.getUniformLocation(blurProgram, 'u_radius'),
135
- };
136
-
137
- return {
138
- vertexShader,
139
- compositeShader,
140
- blurShader: blurFrag,
141
- compositeProgram,
142
- blurProgram,
143
- attribLocations: {
144
- position: gl.getAttribLocation(compositeProgram, 'position'),
145
- },
146
- uniformLocations: {
147
- mask: gl.getUniformLocation(compositeProgram, 'mask')!,
148
- frame: gl.getUniformLocation(compositeProgram, 'frame')!,
149
- background: gl.getUniformLocation(compositeProgram, 'background')!,
150
- },
151
- blurUniforms,
152
- };
153
- };
154
-
155
- export function initTexture(gl: WebGL2RenderingContext, texIndex: number) {
156
- const texRef = gl.TEXTURE0 + texIndex;
157
- gl.activeTexture(texRef);
158
- const texture = gl.createTexture();
159
- gl.bindTexture(gl.TEXTURE_2D, texture);
160
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
161
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
162
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
163
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
164
- gl.bindTexture(gl.TEXTURE_2D, texture);
165
-
166
- return texture;
167
- }
168
-
169
- export function createFramebuffer(
170
- gl: WebGL2RenderingContext,
171
- texture: WebGLTexture,
172
- width: number,
173
- height: number,
174
- ) {
175
- const framebuffer = gl.createFramebuffer();
176
- gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
177
-
178
- // Set the texture as the color attachment
179
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
180
-
181
- // Ensure texture dimensions match the provided width and height
182
- gl.bindTexture(gl.TEXTURE_2D, texture);
183
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
184
-
185
- // Check if framebuffer is complete
186
- const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
187
- if (status !== gl.FRAMEBUFFER_COMPLETE) {
188
- throw new Error('Framebuffer not complete');
189
- }
190
-
191
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
192
- return framebuffer;
193
- }
194
-
195
- const createVertexBuffer = (gl: WebGL2RenderingContext) => {
196
- if (!gl) {
197
- return null;
198
- }
199
- const vertexBuffer = gl.createBuffer();
200
- gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
201
- gl.bufferData(
202
- gl.ARRAY_BUFFER,
203
- new Float32Array([-1, -1, -1, 1, 1, 1, -1, -1, 1, 1, 1, -1]),
204
- gl.STATIC_DRAW,
205
- );
206
- return vertexBuffer;
207
- };
1
+ /**
2
+ * WebGL setup for the mask processor
3
+ * potential improvements:
4
+ * - downsample the video texture in background blur scenario before applying the (gaussian) blur for better performance
5
+ *
6
+ */
7
+ import { applyBlur, createBlurProgram } from './shader-programs/blurShader';
8
+ import { createBoxBlurProgram } from './shader-programs/boxBlurShader';
9
+ import { createCompositeProgram } from './shader-programs/compositeShader';
10
+ import { applyDownsampling, createDownSampler } from './shader-programs/downSampler';
11
+ import {
12
+ createFramebuffer,
13
+ createVertexBuffer,
14
+ emptyImageData,
15
+ initTexture,
16
+ resizeImageToCover,
17
+ } from './utils';
208
18
 
209
19
  export const setupWebGL = (canvas: OffscreenCanvas) => {
210
- const gl = canvas.getContext('webgl2', { premultipliedAlpha: false }) as WebGL2RenderingContext;
20
+ const gl = canvas.getContext('webgl2', {
21
+ antialias: true,
22
+ premultipliedAlpha: true,
23
+ }) as WebGL2RenderingContext;
211
24
 
212
25
  let blurRadius: number | null = null;
26
+ let maskBlurRadius: number | null = 8;
27
+ const downsampleFactor = 4;
213
28
 
214
29
  if (!gl) {
30
+ console.error('Failed to create WebGL context');
215
31
  return undefined;
216
32
  }
217
33
 
218
34
  gl.enable(gl.BLEND);
219
35
  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
220
36
 
37
+ // Create the composite program
38
+ const composite = createCompositeProgram(gl);
39
+ const compositeProgram = composite.program;
40
+ const positionLocation = composite.attribLocations.position;
221
41
  const {
222
- compositeProgram,
223
- blurProgram,
224
- attribLocations: { position: positionLocation },
225
- uniformLocations: {
226
- mask: maskTextureLocation,
227
- frame: frameTextureLocation,
228
- background: bgTextureLocation,
229
- },
230
- blurUniforms,
231
- } = createShaderProgram(gl);
42
+ mask: maskTextureLocation,
43
+ frame: frameTextureLocation,
44
+ background: bgTextureLocation,
45
+ } = composite.uniformLocations;
46
+
47
+ // Create the blur program using the same vertex shader source
48
+ const blur = createBlurProgram(gl);
49
+ const blurProgram = blur.program;
50
+ const blurUniforms = blur.uniforms;
51
+
52
+ // Create the box blur program
53
+ const boxBlur = createBoxBlurProgram(gl);
54
+ const boxBlurProgram = boxBlur.program;
55
+ const boxBlurUniforms = boxBlur.uniforms;
232
56
 
233
57
  const bgTexture = initTexture(gl, 0);
234
58
  const frameTexture = initTexture(gl, 1);
235
59
  const vertexBuffer = createVertexBuffer(gl);
236
60
 
237
- // Create additional textures and framebuffers for processing
238
- let processTextures: WebGLTexture[] = [];
239
- let processFramebuffers: WebGLFramebuffer[] = [];
240
-
241
- // Create textures for processing (blur)
242
- processTextures.push(initTexture(gl, 3));
243
- processTextures.push(initTexture(gl, 4));
244
-
245
- // Create framebuffers for processing
246
- processFramebuffers.push(createFramebuffer(gl, processTextures[0], canvas.width, canvas.height));
247
- processFramebuffers.push(createFramebuffer(gl, processTextures[1], canvas.width, canvas.height));
248
-
249
- // Set up uniforms for the composite shader
250
- gl.useProgram(compositeProgram);
251
- gl.uniform1i(bgTextureLocation, 0);
252
- gl.uniform1i(frameTextureLocation, 1);
253
- gl.uniform1i(maskTextureLocation, 2);
254
-
255
- // Store custom background image
256
- let customBackgroundImage: ImageBitmap | null = null;
61
+ if (!vertexBuffer) {
62
+ throw new Error('Failed to create vertex buffer');
63
+ }
257
64
 
258
- function applyBlur(sourceTexture: WebGLTexture, width: number, height: number) {
259
- if (!blurRadius || !blurProgram || !blurUniforms) return bgTexture;
65
+ // Create additional textures and framebuffers for processing
66
+ let bgBlurTextures: WebGLTexture[] = [];
67
+ let bgBlurFrameBuffers: WebGLFramebuffer[] = [];
68
+ let blurredMaskTexture: WebGLTexture | null = null;
260
69
 
261
- gl.useProgram(blurProgram);
70
+ // For double buffering the final mask
71
+ let finalMaskTextures: WebGLTexture[] = [];
72
+ let readMaskIndex = 0; // Index for renderFrame to read from
73
+ let writeMaskIndex = 1; // Index for updateMask to write to
262
74
 
263
- // Set common attributes
264
- gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
265
- gl.vertexAttribPointer(blurUniforms.position, 2, gl.FLOAT, false, 0, 0);
266
- gl.enableVertexAttribArray(blurUniforms.position);
75
+ // Create textures for background processing (blur)
76
+ bgBlurTextures.push(initTexture(gl, 3)); // For blur pass 1
77
+ bgBlurTextures.push(initTexture(gl, 4)); // For blur pass 2
267
78
 
268
- const texelWidth = 1.0 / width;
269
- const texelHeight = 1.0 / height;
79
+ const bgBlurTextureWidth = Math.floor(canvas.width / downsampleFactor);
80
+ const bgBlurTextureHeight = Math.floor(canvas.height / downsampleFactor);
270
81
 
271
- // First pass - horizontal blur
272
- gl.bindFramebuffer(gl.FRAMEBUFFER, processFramebuffers[0]);
273
- gl.viewport(0, 0, width, height);
82
+ const downSampler = createDownSampler(gl, bgBlurTextureWidth, bgBlurTextureHeight);
274
83
 
275
- gl.activeTexture(gl.TEXTURE0);
276
- gl.bindTexture(gl.TEXTURE_2D, sourceTexture);
277
- gl.uniform1i(blurUniforms.texture, 0);
278
- gl.uniform2f(blurUniforms.texelSize, texelWidth, texelHeight);
279
- gl.uniform2f(blurUniforms.direction, 1.0, 0.0); // Horizontal
280
- gl.uniform1f(blurUniforms.radius, blurRadius);
84
+ // Create framebuffers for background processing
85
+ bgBlurFrameBuffers.push(
86
+ createFramebuffer(gl, bgBlurTextures[0], bgBlurTextureWidth, bgBlurTextureHeight),
87
+ );
88
+ bgBlurFrameBuffers.push(
89
+ createFramebuffer(gl, bgBlurTextures[1], bgBlurTextureWidth, bgBlurTextureHeight),
90
+ );
281
91
 
282
- gl.drawArrays(gl.TRIANGLES, 0, 6);
92
+ // Initialize texture for the first mask blur pass
93
+ const tempMaskTexture = initTexture(gl, 5);
94
+ const tempMaskFrameBuffer = createFramebuffer(gl, tempMaskTexture, canvas.width, canvas.height);
283
95
 
284
- // Second pass - vertical blur
285
- gl.bindFramebuffer(gl.FRAMEBUFFER, processFramebuffers[1]);
286
- gl.viewport(0, 0, width, height);
96
+ // Initialize two textures for double-buffering the final mask
97
+ finalMaskTextures.push(initTexture(gl, 6)); // For reading in renderFrame
98
+ finalMaskTextures.push(initTexture(gl, 7)); // For writing in updateMask
287
99
 
288
- gl.activeTexture(gl.TEXTURE0);
289
- gl.bindTexture(gl.TEXTURE_2D, processTextures[0]);
290
- gl.uniform1i(blurUniforms.texture, 0);
291
- gl.uniform2f(blurUniforms.direction, 0.0, 1.0); // Vertical
100
+ // Create framebuffers for the final mask textures
101
+ const finalMaskFrameBuffers = [
102
+ createFramebuffer(gl, finalMaskTextures[0], canvas.width, canvas.height),
103
+ createFramebuffer(gl, finalMaskTextures[1], canvas.width, canvas.height),
104
+ ];
292
105
 
293
- gl.drawArrays(gl.TRIANGLES, 0, 6);
294
-
295
- // Reset framebuffer
296
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
106
+ // Set up uniforms for the composite shader
107
+ gl.useProgram(compositeProgram);
108
+ gl.uniform1i(bgTextureLocation, 0);
109
+ gl.uniform1i(frameTextureLocation, 1);
110
+ gl.uniform1i(maskTextureLocation, 2);
297
111
 
298
- return processTextures[1];
299
- }
112
+ // Store custom background image
113
+ let customBackgroundImage: ImageBitmap | ImageData = emptyImageData;
300
114
 
301
- function render(frame: VideoFrame, mask: MPMask) {
302
- if (frame.codedWidth === 0 || mask.width === 0) {
115
+ function renderFrame(frame: VideoFrame) {
116
+ if (frame.codedWidth === 0 || finalMaskTextures.length === 0) {
303
117
  return;
304
118
  }
305
119
 
@@ -314,20 +128,34 @@ export const setupWebGL = (canvas: OffscreenCanvas) => {
314
128
  // Apply blur if enabled (and no custom background is set)
315
129
  let backgroundTexture = bgTexture;
316
130
 
317
- // If we have a custom background image, use that
318
- if (customBackgroundImage) {
131
+ if (blurRadius) {
132
+ const downSampledFrameTexture = applyDownsampling(
133
+ gl,
134
+ frameTexture,
135
+ downSampler,
136
+ vertexBuffer!,
137
+ bgBlurTextureWidth,
138
+ bgBlurTextureHeight,
139
+ );
140
+ backgroundTexture = applyBlur(
141
+ gl,
142
+ downSampledFrameTexture,
143
+ bgBlurTextureWidth,
144
+ bgBlurTextureHeight,
145
+ blurRadius,
146
+ blurProgram,
147
+ blurUniforms,
148
+ vertexBuffer!,
149
+ bgBlurFrameBuffers,
150
+ bgBlurTextures,
151
+ );
152
+ } else {
319
153
  gl.activeTexture(gl.TEXTURE0);
320
154
  gl.bindTexture(gl.TEXTURE_2D, bgTexture);
321
155
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, customBackgroundImage);
322
156
  backgroundTexture = bgTexture;
323
- } else if (blurRadius) {
324
- // Otherwise, if blur is enabled, apply blur effect to the frame
325
- backgroundTexture = applyBlur(frameTexture, width, height);
326
157
  }
327
158
 
328
- // Get the mask texture
329
- const maskTexture = mask.getAsWebGLTexture();
330
-
331
159
  // Render the final composite
332
160
  gl.viewport(0, 0, width, height);
333
161
  gl.clearColor(1.0, 1.0, 1.0, 1.0);
@@ -348,13 +176,11 @@ export const setupWebGL = (canvas: OffscreenCanvas) => {
348
176
  gl.bindTexture(gl.TEXTURE_2D, frameTexture);
349
177
  gl.uniform1i(frameTextureLocation, 1);
350
178
 
351
- // Set mask texture
179
+ // Set mask texture - always read from the current read index
352
180
  gl.activeTexture(gl.TEXTURE2);
353
- gl.bindTexture(gl.TEXTURE_2D, maskTexture);
181
+ gl.bindTexture(gl.TEXTURE_2D, finalMaskTextures[readMaskIndex]);
354
182
  gl.uniform1i(maskTextureLocation, 2);
355
183
  gl.drawArrays(gl.TRIANGLES, 0, 6);
356
-
357
- mask.close();
358
184
  }
359
185
 
360
186
  /**
@@ -363,96 +189,105 @@ export const setupWebGL = (canvas: OffscreenCanvas) => {
363
189
  */
364
190
  async function setBackgroundImage(image: ImageBitmap | null) {
365
191
  // Clear existing background
366
- customBackgroundImage = null;
192
+ customBackgroundImage = emptyImageData;
367
193
 
368
194
  if (image) {
369
195
  try {
370
- // Get current canvas dimensions
371
- const canvasWidth = canvas.width;
372
- const canvasHeight = canvas.height;
373
-
374
- // Calculate dimensions and crop for "cover" mode
375
- const imgAspect = image.width / image.height;
376
- const canvasAspect = canvasWidth / canvasHeight;
377
-
378
- let sx = 0;
379
- let sy = 0;
380
- let sWidth = image.width;
381
- let sHeight = image.height;
382
-
383
- // For cover mode, we need to crop some parts of the image
384
- // to ensure it covers the canvas while maintaining aspect ratio
385
- if (imgAspect > canvasAspect) {
386
- // Image is wider than canvas - crop the sides
387
- sWidth = Math.round(image.height * canvasAspect);
388
- sx = Math.round((image.width - sWidth) / 2); // Center the crop horizontally
389
- } else if (imgAspect < canvasAspect) {
390
- // Image is taller than canvas - crop the top/bottom
391
- sHeight = Math.round(image.width / canvasAspect);
392
- sy = Math.round((image.height - sHeight) / 2); // Center the crop vertically
393
- }
394
-
395
- // Create a new ImageBitmap with the cropped portion
396
- const croppedImage = await createImageBitmap(image, sx, sy, sWidth, sHeight, {
397
- resizeWidth: canvasWidth,
398
- resizeHeight: canvasHeight,
399
- resizeQuality: 'medium',
400
- });
196
+ // Resize and crop the image to cover the canvas
197
+ const croppedImage = await resizeImageToCover(image, canvas.width, canvas.height);
401
198
 
402
199
  // Store the cropped and resized image
403
200
  customBackgroundImage = croppedImage;
404
-
405
- // Load the image into the texture
406
- gl.activeTexture(gl.TEXTURE0);
407
- gl.bindTexture(gl.TEXTURE_2D, bgTexture);
408
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, croppedImage);
409
201
  } catch (error) {
410
- console.error('Error processing background image:', error);
411
- // Fallback to original image on error
412
- customBackgroundImage = image;
413
- gl.activeTexture(gl.TEXTURE0);
414
- gl.bindTexture(gl.TEXTURE_2D, bgTexture);
415
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
202
+ console.error(
203
+ 'Error processing background image, falling back to black background:',
204
+ error,
205
+ );
416
206
  }
417
- } else {
418
- // set the background texture to an empty 2x2 image
419
- const emptyImage = new ImageData(2, 2);
420
- emptyImage.data[0] = 0;
421
- emptyImage.data[1] = 0;
422
- emptyImage.data[2] = 0;
423
- emptyImage.data[3] = 0;
424
- gl.activeTexture(gl.TEXTURE0);
425
- gl.bindTexture(gl.TEXTURE_2D, bgTexture);
426
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, emptyImage);
427
207
  }
208
+
209
+ gl.activeTexture(gl.TEXTURE0);
210
+ gl.bindTexture(gl.TEXTURE_2D, bgTexture);
211
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, customBackgroundImage);
428
212
  }
429
213
 
430
214
  function setBlurRadius(radius: number | null) {
431
- blurRadius = radius;
215
+ blurRadius = radius ? Math.max(1, Math.floor(radius / downsampleFactor)) : null; // we are downsampling the blur texture, so decrease the radius here for better performance with a similar visual result
432
216
  setBackgroundImage(null);
433
217
  }
434
218
 
219
+ function updateMask(mask: WebGLTexture) {
220
+ // Use the existing applyBlur function to apply the first blur pass
221
+ // The second blur pass will be written to finalMaskTextures[writeMaskIndex]
222
+
223
+ // Create temporary arrays for the single blur operation
224
+ const tempFramebuffers = [tempMaskFrameBuffer, finalMaskFrameBuffers[writeMaskIndex]];
225
+
226
+ const tempTextures = [tempMaskTexture, finalMaskTextures[writeMaskIndex]];
227
+
228
+ // Apply the blur using the existing function
229
+ applyBlur(
230
+ gl,
231
+ mask,
232
+ canvas.width,
233
+ canvas.height,
234
+ maskBlurRadius || 1.0,
235
+ boxBlurProgram,
236
+ boxBlurUniforms,
237
+ vertexBuffer!,
238
+ tempFramebuffers,
239
+ tempTextures,
240
+ );
241
+
242
+ // Swap indices for the next frame
243
+ readMaskIndex = writeMaskIndex;
244
+ writeMaskIndex = 1 - writeMaskIndex;
245
+ }
246
+
435
247
  function cleanup() {
436
248
  gl.deleteProgram(compositeProgram);
437
249
  gl.deleteProgram(blurProgram);
250
+ gl.deleteProgram(boxBlurProgram);
438
251
  gl.deleteTexture(bgTexture);
439
252
  gl.deleteTexture(frameTexture);
440
- for (const texture of processTextures) {
253
+ gl.deleteTexture(tempMaskTexture);
254
+ gl.deleteFramebuffer(tempMaskFrameBuffer);
255
+
256
+ for (const texture of bgBlurTextures) {
257
+ gl.deleteTexture(texture);
258
+ }
259
+ for (const framebuffer of bgBlurFrameBuffers) {
260
+ gl.deleteFramebuffer(framebuffer);
261
+ }
262
+ for (const texture of finalMaskTextures) {
441
263
  gl.deleteTexture(texture);
442
264
  }
443
- for (const framebuffer of processFramebuffers) {
265
+ for (const framebuffer of finalMaskFrameBuffers) {
444
266
  gl.deleteFramebuffer(framebuffer);
445
267
  }
446
268
  gl.deleteBuffer(vertexBuffer);
447
269
 
270
+ if (blurredMaskTexture) {
271
+ gl.deleteTexture(blurredMaskTexture);
272
+ }
273
+
274
+ if (downSampler) {
275
+ gl.deleteTexture(downSampler.texture);
276
+ gl.deleteFramebuffer(downSampler.framebuffer);
277
+ gl.deleteProgram(downSampler.program);
278
+ }
279
+
448
280
  // Release any ImageBitmap resources
449
281
  if (customBackgroundImage) {
450
- customBackgroundImage.close();
451
- customBackgroundImage = null;
282
+ if (customBackgroundImage instanceof ImageBitmap) {
283
+ customBackgroundImage.close();
284
+ }
285
+ customBackgroundImage = emptyImageData;
452
286
  }
453
- processTextures = [];
454
- processFramebuffers = [];
287
+ bgBlurTextures = [];
288
+ bgBlurFrameBuffers = [];
289
+ finalMaskTextures = [];
455
290
  }
456
291
 
457
- return { render, setBackgroundImage, setBlurRadius, cleanup };
292
+ return { renderFrame, updateMask, setBackgroundImage, setBlurRadius, cleanup };
458
293
  };