@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.
- package/dist/index.js +474 -267
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +474 -267
- package/dist/index.mjs.map +1 -1
- package/dist/src/transformers/BackgroundTransformer.d.ts +3 -1
- package/dist/src/webgl/index.d.ts +2 -4
- package/dist/src/webgl/shader-programs/blurShader.d.ts +14 -0
- package/dist/src/webgl/shader-programs/boxBlurShader.d.ts +16 -0
- package/dist/src/webgl/shader-programs/compositeShader.d.ts +18 -0
- package/dist/src/webgl/shader-programs/downSampler.d.ts +12 -0
- package/dist/src/webgl/shader-programs/vertexShader.d.ts +1 -0
- package/dist/src/webgl/utils.d.ts +25 -0
- package/package.json +1 -1
- package/src/index.ts +9 -11
- package/src/transformers/BackgroundTransformer.ts +42 -28
- package/src/webgl/index.ts +180 -345
- package/src/webgl/shader-programs/blurShader.ts +108 -0
- package/src/webgl/shader-programs/boxBlurShader.ts +60 -0
- package/src/webgl/shader-programs/compositeShader.ts +64 -0
- package/src/webgl/shader-programs/downSampler.ts +94 -0
- package/src/webgl/shader-programs/vertexShader.ts +11 -0
- package/src/webgl/utils.ts +142 -0
package/src/webgl/index.ts
CHANGED
|
@@ -1,305 +1,119 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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', {
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
269
|
-
|
|
79
|
+
const bgBlurTextureWidth = Math.floor(canvas.width / downsampleFactor);
|
|
80
|
+
const bgBlurTextureHeight = Math.floor(canvas.height / downsampleFactor);
|
|
270
81
|
|
|
271
|
-
|
|
272
|
-
gl.bindFramebuffer(gl.FRAMEBUFFER, processFramebuffers[0]);
|
|
273
|
-
gl.viewport(0, 0, width, height);
|
|
82
|
+
const downSampler = createDownSampler(gl, bgBlurTextureWidth, bgBlurTextureHeight);
|
|
274
83
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
gl
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
gl
|
|
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
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
gl.
|
|
291
|
-
gl
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
299
|
-
|
|
112
|
+
// Store custom background image
|
|
113
|
+
let customBackgroundImage: ImageBitmap | ImageData = emptyImageData;
|
|
300
114
|
|
|
301
|
-
function
|
|
302
|
-
if (frame.codedWidth === 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
|
-
|
|
318
|
-
|
|
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,
|
|
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 =
|
|
192
|
+
customBackgroundImage = emptyImageData;
|
|
367
193
|
|
|
368
194
|
if (image) {
|
|
369
195
|
try {
|
|
370
|
-
//
|
|
371
|
-
const
|
|
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(
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
451
|
-
|
|
282
|
+
if (customBackgroundImage instanceof ImageBitmap) {
|
|
283
|
+
customBackgroundImage.close();
|
|
284
|
+
}
|
|
285
|
+
customBackgroundImage = emptyImageData;
|
|
452
286
|
}
|
|
453
|
-
|
|
454
|
-
|
|
287
|
+
bgBlurTextures = [];
|
|
288
|
+
bgBlurFrameBuffers = [];
|
|
289
|
+
finalMaskTextures = [];
|
|
455
290
|
}
|
|
456
291
|
|
|
457
|
-
return {
|
|
292
|
+
return { renderFrame, updateMask, setBackgroundImage, setBlurRadius, cleanup };
|
|
458
293
|
};
|