@stream-io/video-filters-web 0.5.0 → 0.6.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 +16 -0
- package/dist/index.cjs.js +449 -127
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +449 -128
- package/dist/index.es.js.map +1 -1
- package/dist/src/BaseVideoProcessor.d.ts +38 -0
- package/dist/src/FullScreenBlur.d.ts +28 -0
- package/dist/src/FullScreenBlurRenderer.d.ts +42 -0
- package/dist/src/VirtualBackground.d.ts +15 -28
- package/index.ts +1 -0
- package/package.json +1 -1
- package/src/BaseVideoProcessor.ts +132 -0
- package/src/FallbackProcessor.ts +4 -14
- package/src/FullScreenBlur.ts +52 -0
- package/src/FullScreenBlurRenderer.ts +411 -0
- package/src/VirtualBackground.ts +83 -184
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple WebGL renderer for full-screen Gaussian blur.
|
|
3
|
+
* Uses a two-pass separable Gaussian blur (horizontal then vertical).
|
|
4
|
+
* Optimized for moderation use cases by blurring at reduced resolution (15% scale)
|
|
5
|
+
* and upscaling back to full resolution for output.
|
|
6
|
+
*/
|
|
7
|
+
export class FullScreenBlurRenderer {
|
|
8
|
+
readonly canvas: OffscreenCanvas;
|
|
9
|
+
readonly gl: WebGL2RenderingContext;
|
|
10
|
+
|
|
11
|
+
readonly blurProgramHandle: WebGLProgram;
|
|
12
|
+
readonly blurLocations: {
|
|
13
|
+
positionLocation: number;
|
|
14
|
+
texCoordLocation: number;
|
|
15
|
+
imageLocation: WebGLUniformLocation | null;
|
|
16
|
+
texelSizeLocation: WebGLUniformLocation | null;
|
|
17
|
+
directionLocation: WebGLUniformLocation | null;
|
|
18
|
+
weightsLocation: WebGLUniformLocation | null;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
readonly passthroughProgramHandle: WebGLProgram;
|
|
22
|
+
readonly passthroughLocations: {
|
|
23
|
+
positionLocation: number;
|
|
24
|
+
texCoordLocation: number;
|
|
25
|
+
imageLocation: WebGLUniformLocation | null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
readonly positionBuffer: WebGLBuffer | null;
|
|
29
|
+
readonly texCoordBuffer: WebGLBuffer | null;
|
|
30
|
+
|
|
31
|
+
readonly pingTexture: WebGLTexture | null;
|
|
32
|
+
readonly pongTexture: WebGLTexture | null;
|
|
33
|
+
readonly pingFbo: WebGLFramebuffer | null;
|
|
34
|
+
readonly pongFbo: WebGLFramebuffer | null;
|
|
35
|
+
|
|
36
|
+
private inputTexture: WebGLTexture | null = null;
|
|
37
|
+
private isRunning = false;
|
|
38
|
+
|
|
39
|
+
private targetWidth = 0;
|
|
40
|
+
private targetHeight = 0;
|
|
41
|
+
|
|
42
|
+
private weightCache = new Map<number, Float32Array>();
|
|
43
|
+
|
|
44
|
+
constructor(canvas: OffscreenCanvas) {
|
|
45
|
+
this.canvas = canvas;
|
|
46
|
+
|
|
47
|
+
const gl = canvas.getContext('webgl2', {
|
|
48
|
+
alpha: false,
|
|
49
|
+
antialias: false,
|
|
50
|
+
desynchronized: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!gl) throw new Error('WebGL2 not supported');
|
|
54
|
+
this.gl = gl;
|
|
55
|
+
|
|
56
|
+
const vertexShaderSource = `#version 300 es
|
|
57
|
+
precision highp float;
|
|
58
|
+
in vec2 a_position;
|
|
59
|
+
in vec2 a_texCoord;
|
|
60
|
+
out vec2 v_texCoord;
|
|
61
|
+
void main() {
|
|
62
|
+
v_texCoord = a_texCoord;
|
|
63
|
+
gl_Position = vec4(a_position, 0.0, 1.0);
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const fragmentShaderSource = `#version 300 es
|
|
68
|
+
precision highp float;
|
|
69
|
+
in vec2 v_texCoord;
|
|
70
|
+
out vec4 outColor;
|
|
71
|
+
uniform sampler2D u_image;
|
|
72
|
+
uniform vec2 u_texelSize;
|
|
73
|
+
uniform vec2 u_direction;
|
|
74
|
+
uniform float u_weights[25];
|
|
75
|
+
void main() {
|
|
76
|
+
vec4 color = vec4(0.0);
|
|
77
|
+
for (int i = -12; i <= 12; i++) {
|
|
78
|
+
float w = u_weights[i + 12];
|
|
79
|
+
if (w == 0.0) continue;
|
|
80
|
+
vec2 offset = float(i) * u_direction * u_texelSize;
|
|
81
|
+
color += w * texture(u_image, v_texCoord + offset);
|
|
82
|
+
}
|
|
83
|
+
outColor = color;
|
|
84
|
+
}
|
|
85
|
+
`;
|
|
86
|
+
|
|
87
|
+
this.blurProgramHandle = this.createAndLinkProgram(
|
|
88
|
+
vertexShaderSource,
|
|
89
|
+
fragmentShaderSource,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const passthroughFragmentShaderSource = `#version 300 es
|
|
93
|
+
precision highp float;
|
|
94
|
+
in vec2 v_texCoord;
|
|
95
|
+
out vec4 outColor;
|
|
96
|
+
uniform sampler2D u_image;
|
|
97
|
+
void main() {
|
|
98
|
+
outColor = texture(u_image, v_texCoord);
|
|
99
|
+
}
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
this.passthroughProgramHandle = this.createAndLinkProgram(
|
|
103
|
+
vertexShaderSource,
|
|
104
|
+
passthroughFragmentShaderSource,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const blurProgram = this.blurProgramHandle;
|
|
108
|
+
const passthroughProgram = this.passthroughProgramHandle;
|
|
109
|
+
|
|
110
|
+
this.blurLocations = {
|
|
111
|
+
positionLocation: gl.getAttribLocation(blurProgram, 'a_position'),
|
|
112
|
+
texCoordLocation: gl.getAttribLocation(blurProgram, 'a_texCoord'),
|
|
113
|
+
imageLocation: gl.getUniformLocation(blurProgram, 'u_image'),
|
|
114
|
+
texelSizeLocation: gl.getUniformLocation(blurProgram, 'u_texelSize'),
|
|
115
|
+
directionLocation: gl.getUniformLocation(blurProgram, 'u_direction'),
|
|
116
|
+
weightsLocation: gl.getUniformLocation(blurProgram, 'u_weights'),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
this.passthroughLocations = {
|
|
120
|
+
positionLocation: gl.getAttribLocation(passthroughProgram, 'a_position'),
|
|
121
|
+
texCoordLocation: gl.getAttribLocation(passthroughProgram, 'a_texCoord'),
|
|
122
|
+
imageLocation: gl.getUniformLocation(passthroughProgram, 'u_image'),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
this.positionBuffer = gl.createBuffer();
|
|
126
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
|
|
127
|
+
gl.bufferData(
|
|
128
|
+
gl.ARRAY_BUFFER,
|
|
129
|
+
new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
|
|
130
|
+
gl.STATIC_DRAW,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
this.texCoordBuffer = gl.createBuffer();
|
|
134
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
|
|
135
|
+
gl.bufferData(
|
|
136
|
+
gl.ARRAY_BUFFER,
|
|
137
|
+
new Float32Array([0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0]),
|
|
138
|
+
gl.STATIC_DRAW,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const createTexture2D = () => {
|
|
142
|
+
const tex = gl.createTexture();
|
|
143
|
+
if (!tex) throw new Error('Failed to create texture');
|
|
144
|
+
gl.bindTexture(gl.TEXTURE_2D, tex);
|
|
145
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
146
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
147
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
148
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
149
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
150
|
+
return tex;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const createFramebufferForTexture = (tex: WebGLTexture) => {
|
|
154
|
+
const fb = gl.createFramebuffer();
|
|
155
|
+
if (!fb) throw new Error('Failed to create framebuffer');
|
|
156
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
|
|
157
|
+
gl.framebufferTexture2D(
|
|
158
|
+
gl.FRAMEBUFFER,
|
|
159
|
+
gl.COLOR_ATTACHMENT0,
|
|
160
|
+
gl.TEXTURE_2D,
|
|
161
|
+
tex,
|
|
162
|
+
0,
|
|
163
|
+
);
|
|
164
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
165
|
+
return fb;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
this.pingTexture = createTexture2D();
|
|
169
|
+
this.pongTexture = createTexture2D();
|
|
170
|
+
this.pingFbo = createFramebufferForTexture(this.pingTexture);
|
|
171
|
+
this.pongFbo = createFramebufferForTexture(this.pongTexture);
|
|
172
|
+
|
|
173
|
+
this.inputTexture = createTexture2D();
|
|
174
|
+
|
|
175
|
+
gl.useProgram(blurProgram);
|
|
176
|
+
|
|
177
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
|
|
178
|
+
gl.enableVertexAttribArray(this.blurLocations.positionLocation);
|
|
179
|
+
gl.vertexAttribPointer(
|
|
180
|
+
this.blurLocations.positionLocation,
|
|
181
|
+
2,
|
|
182
|
+
gl.FLOAT,
|
|
183
|
+
false,
|
|
184
|
+
0,
|
|
185
|
+
0,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
|
|
189
|
+
gl.enableVertexAttribArray(this.blurLocations.texCoordLocation);
|
|
190
|
+
gl.vertexAttribPointer(
|
|
191
|
+
this.blurLocations.texCoordLocation,
|
|
192
|
+
2,
|
|
193
|
+
gl.FLOAT,
|
|
194
|
+
false,
|
|
195
|
+
0,
|
|
196
|
+
0,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (this.blurLocations.imageLocation) {
|
|
200
|
+
gl.uniform1i(this.blurLocations.imageLocation, 0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.isRunning = true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private createAndLinkProgram(
|
|
207
|
+
vsSource: string,
|
|
208
|
+
fsSource: string,
|
|
209
|
+
): WebGLProgram {
|
|
210
|
+
const gl = this.gl;
|
|
211
|
+
const vs = this.createShader(gl.VERTEX_SHADER, vsSource);
|
|
212
|
+
const fs = this.createShader(gl.FRAGMENT_SHADER, fsSource);
|
|
213
|
+
|
|
214
|
+
const prog = gl.createProgram();
|
|
215
|
+
if (!prog) throw new Error('Failed to create program');
|
|
216
|
+
gl.attachShader(prog, vs);
|
|
217
|
+
gl.attachShader(prog, fs);
|
|
218
|
+
gl.linkProgram(prog);
|
|
219
|
+
|
|
220
|
+
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
|
|
221
|
+
throw new Error('Shader link failed: ' + gl.getProgramInfoLog(prog));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
gl.deleteShader(vs);
|
|
225
|
+
gl.deleteShader(fs);
|
|
226
|
+
return prog;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private createShader(type: number, source: string): WebGLShader {
|
|
230
|
+
const gl = this.gl;
|
|
231
|
+
const shader = gl.createShader(type);
|
|
232
|
+
if (!shader) throw new Error('Failed to create shader');
|
|
233
|
+
gl.shaderSource(shader, source);
|
|
234
|
+
gl.compileShader(shader);
|
|
235
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
236
|
+
throw new Error('Shader compile failed: ' + gl.getShaderInfoLog(shader));
|
|
237
|
+
}
|
|
238
|
+
return shader;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private getGaussianWeights(radius: number): Float32Array {
|
|
242
|
+
const r = Math.max(0, Math.min(radius | 0, 12));
|
|
243
|
+
|
|
244
|
+
const cached = this.weightCache.get(r);
|
|
245
|
+
if (cached) return cached;
|
|
246
|
+
|
|
247
|
+
const weights = new Float32Array(25);
|
|
248
|
+
|
|
249
|
+
if (r === 0) {
|
|
250
|
+
weights[12] = 1.0;
|
|
251
|
+
this.weightCache.set(r, weights);
|
|
252
|
+
return weights;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const sigma = r * 0.6;
|
|
256
|
+
let sum = 0;
|
|
257
|
+
|
|
258
|
+
for (let i = -r; i <= r; i++) {
|
|
259
|
+
const w = Math.exp(-(i * i) / (2 * sigma * sigma));
|
|
260
|
+
weights[i + 12] = w;
|
|
261
|
+
sum += w;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (let i = -r; i <= r; i++) {
|
|
265
|
+
weights[i + 12] /= sum;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.weightCache.set(r, weights);
|
|
269
|
+
return weights;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public render(frame: VideoFrame, radius: number): void {
|
|
273
|
+
if (!this.isRunning) return;
|
|
274
|
+
|
|
275
|
+
const gl = this.gl;
|
|
276
|
+
|
|
277
|
+
const width = frame.displayWidth;
|
|
278
|
+
const height = frame.displayHeight;
|
|
279
|
+
|
|
280
|
+
if (!width || !height) return;
|
|
281
|
+
|
|
282
|
+
if (this.canvas.width !== width || this.canvas.height !== height) {
|
|
283
|
+
this.canvas.width = width;
|
|
284
|
+
this.canvas.height = height;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const scale = 0.15;
|
|
288
|
+
const scaledWidth = Math.max(1, Math.floor(width * scale));
|
|
289
|
+
const scaledHeight = Math.max(1, Math.floor(height * scale));
|
|
290
|
+
|
|
291
|
+
if (
|
|
292
|
+
scaledWidth !== this.targetWidth ||
|
|
293
|
+
scaledHeight !== this.targetHeight
|
|
294
|
+
) {
|
|
295
|
+
this.targetWidth = scaledWidth;
|
|
296
|
+
this.targetHeight = scaledHeight;
|
|
297
|
+
|
|
298
|
+
gl.bindTexture(gl.TEXTURE_2D, this.pingTexture);
|
|
299
|
+
gl.texImage2D(
|
|
300
|
+
gl.TEXTURE_2D,
|
|
301
|
+
0,
|
|
302
|
+
gl.RGBA,
|
|
303
|
+
scaledWidth,
|
|
304
|
+
scaledHeight,
|
|
305
|
+
0,
|
|
306
|
+
gl.RGBA,
|
|
307
|
+
gl.UNSIGNED_BYTE,
|
|
308
|
+
null,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
gl.bindTexture(gl.TEXTURE_2D, this.pongTexture);
|
|
312
|
+
gl.texImage2D(
|
|
313
|
+
gl.TEXTURE_2D,
|
|
314
|
+
0,
|
|
315
|
+
gl.RGBA,
|
|
316
|
+
scaledWidth,
|
|
317
|
+
scaledHeight,
|
|
318
|
+
0,
|
|
319
|
+
gl.RGBA,
|
|
320
|
+
gl.UNSIGNED_BYTE,
|
|
321
|
+
null,
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
328
|
+
gl.bindTexture(gl.TEXTURE_2D, this.inputTexture);
|
|
329
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, frame);
|
|
330
|
+
|
|
331
|
+
gl.useProgram(this.blurProgramHandle);
|
|
332
|
+
|
|
333
|
+
if (this.blurLocations.texelSizeLocation) {
|
|
334
|
+
gl.uniform2f(
|
|
335
|
+
this.blurLocations.texelSizeLocation,
|
|
336
|
+
1.0 / scaledWidth,
|
|
337
|
+
1.0 / scaledHeight,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const weights = this.getGaussianWeights(radius);
|
|
342
|
+
if (this.blurLocations.weightsLocation) {
|
|
343
|
+
gl.uniform1fv(this.blurLocations.weightsLocation, weights);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
gl.viewport(0, 0, scaledWidth, scaledHeight);
|
|
347
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.pingFbo);
|
|
348
|
+
gl.bindTexture(gl.TEXTURE_2D, this.inputTexture);
|
|
349
|
+
if (this.blurLocations.directionLocation) {
|
|
350
|
+
gl.uniform2f(this.blurLocations.directionLocation, 1.0, 0.0);
|
|
351
|
+
}
|
|
352
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
353
|
+
|
|
354
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, this.pongFbo);
|
|
355
|
+
gl.bindTexture(gl.TEXTURE_2D, this.pingTexture);
|
|
356
|
+
if (this.blurLocations.directionLocation) {
|
|
357
|
+
gl.uniform2f(this.blurLocations.directionLocation, 0.0, 1.0);
|
|
358
|
+
}
|
|
359
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
360
|
+
|
|
361
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
362
|
+
|
|
363
|
+
gl.viewport(0, 0, width, height);
|
|
364
|
+
gl.useProgram(this.passthroughProgramHandle);
|
|
365
|
+
|
|
366
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
|
|
367
|
+
gl.enableVertexAttribArray(this.passthroughLocations.positionLocation);
|
|
368
|
+
gl.vertexAttribPointer(
|
|
369
|
+
this.passthroughLocations.positionLocation,
|
|
370
|
+
2,
|
|
371
|
+
gl.FLOAT,
|
|
372
|
+
false,
|
|
373
|
+
0,
|
|
374
|
+
0,
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, this.texCoordBuffer);
|
|
378
|
+
gl.enableVertexAttribArray(this.passthroughLocations.texCoordLocation);
|
|
379
|
+
gl.vertexAttribPointer(
|
|
380
|
+
this.passthroughLocations.texCoordLocation,
|
|
381
|
+
2,
|
|
382
|
+
gl.FLOAT,
|
|
383
|
+
false,
|
|
384
|
+
0,
|
|
385
|
+
0,
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
gl.bindTexture(gl.TEXTURE_2D, this.pongTexture);
|
|
389
|
+
if (this.passthroughLocations.imageLocation) {
|
|
390
|
+
gl.uniform1i(this.passthroughLocations.imageLocation, 0);
|
|
391
|
+
}
|
|
392
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
public close(): void {
|
|
396
|
+
if (!this.isRunning) return;
|
|
397
|
+
this.isRunning = false;
|
|
398
|
+
const gl = this.gl;
|
|
399
|
+
|
|
400
|
+
if (this.pingFbo) gl.deleteFramebuffer(this.pingFbo);
|
|
401
|
+
if (this.pongFbo) gl.deleteFramebuffer(this.pongFbo);
|
|
402
|
+
if (this.pingTexture) gl.deleteTexture(this.pingTexture);
|
|
403
|
+
if (this.pongTexture) gl.deleteTexture(this.pongTexture);
|
|
404
|
+
if (this.inputTexture) gl.deleteTexture(this.inputTexture);
|
|
405
|
+
if (this.positionBuffer) gl.deleteBuffer(this.positionBuffer);
|
|
406
|
+
if (this.texCoordBuffer) gl.deleteBuffer(this.texCoordBuffer);
|
|
407
|
+
|
|
408
|
+
gl.deleteProgram(this.blurProgramHandle);
|
|
409
|
+
gl.deleteProgram(this.passthroughProgramHandle);
|
|
410
|
+
}
|
|
411
|
+
}
|