@livekit/track-processors 0.5.3 → 0.5.4
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 +379 -220
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +379 -220
- package/dist/index.mjs.map +1 -1
- package/dist/src/webgl/index.d.ts +0 -2
- 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/vertexShader.d.ts +1 -0
- package/dist/src/webgl/utils.d.ts +23 -0
- package/package.json +1 -1
- package/src/index.ts +9 -11
- package/src/webgl/index.ts +119 -339
- package/src/webgl/shader-programs/blurShader.ts +140 -0
- package/src/webgl/shader-programs/boxBlurShader.ts +98 -0
- package/src/webgl/shader-programs/compositeShader.ts +102 -0
- package/src/webgl/shader-programs/vertexShader.ts +11 -0
- package/src/webgl/utils.ts +110 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { glsl } from '../utils';
|
|
2
|
+
import { vertexShaderSource } from './vertexShader';
|
|
3
|
+
|
|
4
|
+
export const boxBlurFragmentShader = glsl`#version 300 es
|
|
5
|
+
precision mediump float;
|
|
6
|
+
|
|
7
|
+
in vec2 texCoords;
|
|
8
|
+
|
|
9
|
+
uniform sampler2D u_texture;
|
|
10
|
+
uniform vec2 u_texelSize; // 1.0 / texture size
|
|
11
|
+
uniform vec2 u_direction; // (1.0, 0.0) for horizontal, (0.0, 1.0) for vertical
|
|
12
|
+
uniform float u_radius; // blur radius in texels
|
|
13
|
+
|
|
14
|
+
out vec4 fragColor;
|
|
15
|
+
|
|
16
|
+
void main() {
|
|
17
|
+
vec3 sum = vec3(0.0);
|
|
18
|
+
float count = 0.0;
|
|
19
|
+
|
|
20
|
+
// Limit radius to avoid excessive loop cost
|
|
21
|
+
const int MAX_RADIUS = 16;
|
|
22
|
+
int radius = int(min(float(MAX_RADIUS), u_radius));
|
|
23
|
+
|
|
24
|
+
for (int i = -MAX_RADIUS; i <= MAX_RADIUS; ++i) {
|
|
25
|
+
if (abs(i) > radius) continue;
|
|
26
|
+
|
|
27
|
+
vec2 offset = u_direction * u_texelSize * float(i);
|
|
28
|
+
sum += texture(u_texture, texCoords + offset).rgb;
|
|
29
|
+
count += 1.0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fragColor = vec4(sum / count, 1.0);
|
|
33
|
+
}
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create the box blur shader program
|
|
38
|
+
*/
|
|
39
|
+
export function createBoxBlurProgram(gl: WebGL2RenderingContext) {
|
|
40
|
+
// Create vertex shader
|
|
41
|
+
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
42
|
+
if (!vertexShader) {
|
|
43
|
+
throw Error('cannot create vertex shader');
|
|
44
|
+
}
|
|
45
|
+
gl.shaderSource(vertexShader, vertexShaderSource());
|
|
46
|
+
gl.compileShader(vertexShader);
|
|
47
|
+
|
|
48
|
+
// Check vertex shader compilation
|
|
49
|
+
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
|
|
50
|
+
const info = gl.getShaderInfoLog(vertexShader);
|
|
51
|
+
throw Error(`Failed to compile vertex shader: ${info}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Create fragment shader
|
|
55
|
+
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
|
56
|
+
if (!fragmentShader) {
|
|
57
|
+
throw Error('cannot create fragment shader');
|
|
58
|
+
}
|
|
59
|
+
gl.shaderSource(fragmentShader, boxBlurFragmentShader);
|
|
60
|
+
gl.compileShader(fragmentShader);
|
|
61
|
+
|
|
62
|
+
// Check fragment shader compilation
|
|
63
|
+
if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
|
|
64
|
+
const info = gl.getShaderInfoLog(fragmentShader);
|
|
65
|
+
throw Error(`Failed to compile box blur shader: ${info}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create the program
|
|
69
|
+
const program = gl.createProgram();
|
|
70
|
+
if (!program) {
|
|
71
|
+
throw Error('cannot create box blur program');
|
|
72
|
+
}
|
|
73
|
+
gl.attachShader(program, vertexShader);
|
|
74
|
+
gl.attachShader(program, fragmentShader);
|
|
75
|
+
gl.linkProgram(program);
|
|
76
|
+
|
|
77
|
+
// Check program link status
|
|
78
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
79
|
+
const info = gl.getProgramInfoLog(program);
|
|
80
|
+
throw Error(`Failed to link box blur program: ${info}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Get attribute and uniform locations
|
|
84
|
+
const uniforms = {
|
|
85
|
+
position: gl.getAttribLocation(program, 'position'),
|
|
86
|
+
texture: gl.getUniformLocation(program, 'u_texture'),
|
|
87
|
+
texelSize: gl.getUniformLocation(program, 'u_texelSize'),
|
|
88
|
+
direction: gl.getUniformLocation(program, 'u_direction'),
|
|
89
|
+
radius: gl.getUniformLocation(program, 'u_radius'),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
program,
|
|
94
|
+
vertexShader,
|
|
95
|
+
fragmentShader,
|
|
96
|
+
uniforms,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { glsl } from '../utils';
|
|
2
|
+
import { vertexShaderSource } from './vertexShader';
|
|
3
|
+
|
|
4
|
+
// Fragment shader source for compositing
|
|
5
|
+
export const compositeFragmentShader = glsl`#version 300 es
|
|
6
|
+
precision highp float;
|
|
7
|
+
in vec2 texCoords;
|
|
8
|
+
uniform sampler2D background;
|
|
9
|
+
uniform sampler2D frame;
|
|
10
|
+
uniform sampler2D mask;
|
|
11
|
+
out vec4 fragColor;
|
|
12
|
+
|
|
13
|
+
void main() {
|
|
14
|
+
|
|
15
|
+
vec4 frameTex = texture(frame, texCoords);
|
|
16
|
+
vec4 bgTex = texture(background, texCoords);
|
|
17
|
+
|
|
18
|
+
float maskVal = texture(mask, texCoords).r;
|
|
19
|
+
|
|
20
|
+
// Compute screen-space gradient to detect edge sharpness
|
|
21
|
+
float grad = length(vec2(dFdx(maskVal), dFdy(maskVal)));
|
|
22
|
+
|
|
23
|
+
float edgeSoftness = 2.0; // higher = softer
|
|
24
|
+
|
|
25
|
+
// Create a smooth edge around binary transition
|
|
26
|
+
float smoothAlpha = smoothstep(0.5 - grad * edgeSoftness, 0.5 + grad * edgeSoftness, maskVal);
|
|
27
|
+
|
|
28
|
+
// Optional: preserve frame alpha, or override as fully opaque
|
|
29
|
+
vec4 blended = mix(bgTex, vec4(frameTex.rgb, 1.0), 1.0 - smoothAlpha);
|
|
30
|
+
|
|
31
|
+
fragColor = blended;
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create the composite shader program
|
|
38
|
+
*/
|
|
39
|
+
export function createCompositeProgram(gl: WebGL2RenderingContext) {
|
|
40
|
+
// Create vertex shader
|
|
41
|
+
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
42
|
+
if (!vertexShader) {
|
|
43
|
+
throw Error('cannot create vertex shader');
|
|
44
|
+
}
|
|
45
|
+
gl.shaderSource(vertexShader, vertexShaderSource());
|
|
46
|
+
gl.compileShader(vertexShader);
|
|
47
|
+
|
|
48
|
+
// Check vertex shader compilation
|
|
49
|
+
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
|
|
50
|
+
const info = gl.getShaderInfoLog(vertexShader);
|
|
51
|
+
throw Error(`Failed to compile vertex shader: ${info}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Create fragment shader
|
|
55
|
+
const compositeShader = gl.createShader(gl.FRAGMENT_SHADER);
|
|
56
|
+
if (!compositeShader) {
|
|
57
|
+
throw Error('cannot create fragment shader');
|
|
58
|
+
}
|
|
59
|
+
gl.shaderSource(compositeShader, compositeFragmentShader);
|
|
60
|
+
gl.compileShader(compositeShader);
|
|
61
|
+
|
|
62
|
+
// Check fragment shader compilation
|
|
63
|
+
if (!gl.getShaderParameter(compositeShader, gl.COMPILE_STATUS)) {
|
|
64
|
+
const info = gl.getShaderInfoLog(compositeShader);
|
|
65
|
+
throw Error(`Failed to compile composite shader: ${info}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create the program
|
|
69
|
+
const compositeProgram = gl.createProgram();
|
|
70
|
+
if (!compositeProgram) {
|
|
71
|
+
throw Error('cannot create composite program');
|
|
72
|
+
}
|
|
73
|
+
gl.attachShader(compositeProgram, vertexShader);
|
|
74
|
+
gl.attachShader(compositeProgram, compositeShader);
|
|
75
|
+
gl.linkProgram(compositeProgram);
|
|
76
|
+
|
|
77
|
+
// Check program link status
|
|
78
|
+
if (!gl.getProgramParameter(compositeProgram, gl.LINK_STATUS)) {
|
|
79
|
+
const info = gl.getProgramInfoLog(compositeProgram);
|
|
80
|
+
throw Error(`Failed to link composite program: ${info}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Get attribute and uniform locations
|
|
84
|
+
const attribLocations = {
|
|
85
|
+
position: gl.getAttribLocation(compositeProgram, 'position'),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const uniformLocations = {
|
|
89
|
+
mask: gl.getUniformLocation(compositeProgram, 'mask')!,
|
|
90
|
+
frame: gl.getUniformLocation(compositeProgram, 'frame')!,
|
|
91
|
+
background: gl.getUniformLocation(compositeProgram, 'background')!,
|
|
92
|
+
stepWidth: gl.getUniformLocation(compositeProgram, 'u_stepWidth')!,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
program: compositeProgram,
|
|
97
|
+
vertexShader,
|
|
98
|
+
fragmentShader: compositeShader,
|
|
99
|
+
attribLocations,
|
|
100
|
+
uniformLocations,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Vertex shader source
|
|
2
|
+
export const vertexShaderSource = (flipY: boolean = true) => `#version 300 es
|
|
3
|
+
in vec2 position;
|
|
4
|
+
out vec2 texCoords;
|
|
5
|
+
|
|
6
|
+
void main() {
|
|
7
|
+
texCoords = (position + 1.0) / 2.0;
|
|
8
|
+
texCoords.y = ${flipY ? '1.0 - texCoords.y' : 'texCoords.y'};
|
|
9
|
+
gl_Position = vec4(position, 0, 1.0);
|
|
10
|
+
}
|
|
11
|
+
`;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialize a WebGL texture
|
|
3
|
+
*/
|
|
4
|
+
export function initTexture(gl: WebGL2RenderingContext, texIndex: number) {
|
|
5
|
+
const texRef = gl.TEXTURE0 + texIndex;
|
|
6
|
+
gl.activeTexture(texRef);
|
|
7
|
+
const texture = gl.createTexture();
|
|
8
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
9
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
10
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
11
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
12
|
+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
|
13
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
14
|
+
|
|
15
|
+
return texture;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create a WebGL framebuffer with the given texture as color attachment
|
|
20
|
+
*/
|
|
21
|
+
export function createFramebuffer(
|
|
22
|
+
gl: WebGL2RenderingContext,
|
|
23
|
+
texture: WebGLTexture,
|
|
24
|
+
width: number,
|
|
25
|
+
height: number,
|
|
26
|
+
) {
|
|
27
|
+
const framebuffer = gl.createFramebuffer();
|
|
28
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
|
|
29
|
+
|
|
30
|
+
// Set the texture as the color attachment
|
|
31
|
+
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
|
32
|
+
|
|
33
|
+
// Ensure texture dimensions match the provided width and height
|
|
34
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
35
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
|
36
|
+
|
|
37
|
+
// Check if framebuffer is complete
|
|
38
|
+
const status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
|
39
|
+
if (status !== gl.FRAMEBUFFER_COMPLETE) {
|
|
40
|
+
throw new Error('Framebuffer not complete');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
|
44
|
+
return framebuffer;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a vertex buffer for a full-screen quad
|
|
49
|
+
*/
|
|
50
|
+
export function createVertexBuffer(gl: WebGL2RenderingContext): WebGLBuffer | null {
|
|
51
|
+
const vertexBuffer = gl.createBuffer();
|
|
52
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
|
|
53
|
+
gl.bufferData(
|
|
54
|
+
gl.ARRAY_BUFFER,
|
|
55
|
+
new Float32Array([-1, -1, -1, 1, 1, 1, -1, -1, 1, 1, 1, -1]),
|
|
56
|
+
gl.STATIC_DRAW,
|
|
57
|
+
);
|
|
58
|
+
return vertexBuffer;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resizes and crops an image to cover a target canvas while maintaining aspect ratio
|
|
63
|
+
* @param image The source image
|
|
64
|
+
* @param targetWidth The target width
|
|
65
|
+
* @param targetHeight The target height
|
|
66
|
+
* @returns A cropped and resized ImageBitmap
|
|
67
|
+
*/
|
|
68
|
+
export async function resizeImageToCover(
|
|
69
|
+
image: ImageBitmap,
|
|
70
|
+
targetWidth: number,
|
|
71
|
+
targetHeight: number,
|
|
72
|
+
): Promise<ImageBitmap> {
|
|
73
|
+
// Calculate dimensions and crop for "cover" mode
|
|
74
|
+
const imgAspect = image.width / image.height;
|
|
75
|
+
const targetAspect = targetWidth / targetHeight;
|
|
76
|
+
|
|
77
|
+
let sx = 0;
|
|
78
|
+
let sy = 0;
|
|
79
|
+
let sWidth = image.width;
|
|
80
|
+
let sHeight = image.height;
|
|
81
|
+
|
|
82
|
+
// For cover mode, we need to crop some parts of the image
|
|
83
|
+
// to ensure it covers the canvas while maintaining aspect ratio
|
|
84
|
+
if (imgAspect > targetAspect) {
|
|
85
|
+
// Image is wider than target - crop the sides
|
|
86
|
+
sWidth = Math.round(image.height * targetAspect);
|
|
87
|
+
sx = Math.round((image.width - sWidth) / 2); // Center the crop horizontally
|
|
88
|
+
} else if (imgAspect < targetAspect) {
|
|
89
|
+
// Image is taller than target - crop the top/bottom
|
|
90
|
+
sHeight = Math.round(image.width / targetAspect);
|
|
91
|
+
sy = Math.round((image.height - sHeight) / 2); // Center the crop vertically
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Create a new ImageBitmap with the cropped portion
|
|
95
|
+
return createImageBitmap(image, sx, sy, sWidth, sHeight, {
|
|
96
|
+
resizeWidth: targetWidth,
|
|
97
|
+
resizeHeight: targetHeight,
|
|
98
|
+
resizeQuality: 'medium',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const emptyImageData = new ImageData(2, 2);
|
|
103
|
+
emptyImageData.data[0] = 0;
|
|
104
|
+
emptyImageData.data[1] = 0;
|
|
105
|
+
emptyImageData.data[2] = 0;
|
|
106
|
+
emptyImageData.data[3] = 0;
|
|
107
|
+
|
|
108
|
+
const glsl = (source: any) => source;
|
|
109
|
+
|
|
110
|
+
export { emptyImageData, glsl };
|