@livekit/track-processors 0.6.1 → 0.7.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/README.md +31 -9
- package/dist/index.js +293 -83
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +292 -82
- package/dist/index.mjs.map +1 -1
- package/dist/src/ProcessorWrapper.d.ts +12 -7
- package/dist/src/index.d.ts +51 -5
- package/dist/src/logger.d.ts +29 -0
- package/dist/src/transformers/BackgroundTransformer.d.ts +4 -2
- package/dist/src/transformers/VideoTransformer.d.ts +1 -1
- package/dist/src/transformers/types.d.ts +11 -8
- package/dist/src/webgl/index.d.ts +1 -0
- package/dist/src/webgl/shader-programs/compositeShader.d.ts +1 -0
- package/package.json +3 -3
- package/src/ProcessorWrapper.ts +63 -23
- package/src/index.ts +206 -42
- package/src/logger.ts +74 -0
- package/src/transformers/BackgroundTransformer.ts +22 -8
- package/src/transformers/VideoTransformer.ts +1 -1
- package/src/transformers/types.ts +11 -8
- package/src/webgl/index.ts +24 -11
- package/src/webgl/shader-programs/compositeShader.ts +18 -13
- package/src/webgl/utils.ts +6 -2
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as vision from '@mediapipe/tasks-vision';
|
|
2
|
+
import { getLogger, LoggerNames } from '../logger';
|
|
2
3
|
import { dependencies } from '../../package.json';
|
|
3
4
|
import VideoTransformer from './VideoTransformer';
|
|
4
|
-
import { VideoTransformerInitOptions } from './types';
|
|
5
|
+
import { TrackTransformerDestroyOptions, VideoTransformerInitOptions } from './types';
|
|
5
6
|
|
|
6
7
|
export type SegmenterOptions = Partial<vision.ImageSegmenterOptions['baseOptions']>;
|
|
7
8
|
|
|
@@ -14,6 +15,7 @@ export interface FrameProcessingStats {
|
|
|
14
15
|
export type BackgroundOptions = {
|
|
15
16
|
blurRadius?: number;
|
|
16
17
|
imagePath?: string;
|
|
18
|
+
backgroundDisabled?: boolean;
|
|
17
19
|
/** cannot be updated through the `update` method, needs a restart */
|
|
18
20
|
segmenterOptions?: SegmenterOptions;
|
|
19
21
|
/** cannot be updated through the `update` method, needs a restart */
|
|
@@ -44,6 +46,8 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
|
|
|
44
46
|
|
|
45
47
|
isFirstFrame = true;
|
|
46
48
|
|
|
49
|
+
private log = getLogger(LoggerNames.ProcessorWrapper);
|
|
50
|
+
|
|
47
51
|
constructor(opts: BackgroundOptions) {
|
|
48
52
|
super();
|
|
49
53
|
this.options = opts;
|
|
@@ -77,19 +81,23 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
|
|
|
77
81
|
// Skip loading the image here if update already loaded the image below
|
|
78
82
|
if (this.options?.imagePath) {
|
|
79
83
|
await this.loadAndSetBackground(this.options.imagePath).catch((err) =>
|
|
80
|
-
|
|
84
|
+
this.log.error('Error while loading processor background image: ', err),
|
|
81
85
|
);
|
|
82
86
|
}
|
|
83
|
-
if (this.options.blurRadius) {
|
|
87
|
+
if (typeof this.options.blurRadius === 'number') {
|
|
84
88
|
this.gl?.setBlurRadius(this.options.blurRadius);
|
|
85
89
|
}
|
|
90
|
+
this.gl?.setBackgroundDisabled(this.options.backgroundDisabled ?? false);
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
async destroy() {
|
|
93
|
+
async destroy(options?: TrackTransformerDestroyOptions) {
|
|
89
94
|
await super.destroy();
|
|
90
95
|
await this.imageSegmenter?.close();
|
|
91
96
|
this.backgroundImageAndPath = null;
|
|
92
|
-
|
|
97
|
+
|
|
98
|
+
if (!options?.willProcessorRestart) {
|
|
99
|
+
this.isFirstFrame = true;
|
|
100
|
+
}
|
|
93
101
|
}
|
|
94
102
|
|
|
95
103
|
async loadAndSetBackground(path: string) {
|
|
@@ -112,11 +120,16 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
|
|
|
112
120
|
let enqueuedFrame = false;
|
|
113
121
|
try {
|
|
114
122
|
if (!(frame instanceof VideoFrame) || frame.codedWidth === 0 || frame.codedHeight === 0) {
|
|
115
|
-
|
|
123
|
+
this.log.debug('empty frame detected, ignoring');
|
|
116
124
|
return;
|
|
117
125
|
}
|
|
118
126
|
|
|
119
|
-
|
|
127
|
+
let skipProcessingFrame = this.isDisabled ?? this.options.backgroundDisabled ?? false;
|
|
128
|
+
if (typeof this.options.blurRadius !== 'number' && typeof this.options.imagePath !== 'string') {
|
|
129
|
+
skipProcessingFrame = true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (skipProcessingFrame) {
|
|
120
133
|
controller.enqueue(frame);
|
|
121
134
|
enqueuedFrame = true;
|
|
122
135
|
return;
|
|
@@ -190,7 +203,7 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
|
|
|
190
203
|
}
|
|
191
204
|
await segmentationPromise;
|
|
192
205
|
} catch (e) {
|
|
193
|
-
|
|
206
|
+
this.log.error('Error while processing frame: ', e);
|
|
194
207
|
} finally {
|
|
195
208
|
if (!enqueuedFrame) {
|
|
196
209
|
frame.close();
|
|
@@ -207,6 +220,7 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
|
|
|
207
220
|
} else {
|
|
208
221
|
this.gl?.setBackgroundImage(null);
|
|
209
222
|
}
|
|
223
|
+
this.gl?.setBackgroundDisabled(opts.backgroundDisabled ?? false);
|
|
210
224
|
}
|
|
211
225
|
|
|
212
226
|
private async drawFrame(frame: VideoFrame) {
|
|
@@ -10,9 +10,9 @@ export interface VideoTransformerInitOptions extends TrackTransformerInitOptions
|
|
|
10
10
|
export interface AudioTransformerInitOptions extends TrackTransformerInitOptions {}
|
|
11
11
|
|
|
12
12
|
export interface VideoTrackTransformer<Options extends Record<string, unknown>>
|
|
13
|
-
extends BaseTrackTransformer<VideoTransformerInitOptions, VideoFrame> {
|
|
13
|
+
extends BaseTrackTransformer<VideoTransformerInitOptions, VideoFrame, TrackTransformerDestroyOptions> {
|
|
14
14
|
init: (options: VideoTransformerInitOptions) => void;
|
|
15
|
-
destroy: () => void;
|
|
15
|
+
destroy: (options?: TrackTransformerDestroyOptions) => void;
|
|
16
16
|
restart: (options: VideoTransformerInitOptions) => void;
|
|
17
17
|
transform: (frame: VideoFrame, controller: TransformStreamDefaultController) => void;
|
|
18
18
|
transformer?: TransformStream;
|
|
@@ -20,26 +20,29 @@ export interface VideoTrackTransformer<Options extends Record<string, unknown>>
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export interface AudioTrackTransformer<Options extends Record<string, unknown>>
|
|
23
|
-
extends BaseTrackTransformer<AudioTransformerInitOptions, AudioData> {
|
|
23
|
+
extends BaseTrackTransformer<AudioTransformerInitOptions, AudioData, TrackTransformerDestroyOptions> {
|
|
24
24
|
init: (options: AudioTransformerInitOptions) => void;
|
|
25
|
-
destroy: () => void;
|
|
25
|
+
destroy: (options: TrackTransformerDestroyOptions) => void;
|
|
26
26
|
restart: (options: AudioTransformerInitOptions) => void;
|
|
27
27
|
transform: (frame: AudioData, controller: TransformStreamDefaultController) => void;
|
|
28
28
|
transformer?: TransformStream;
|
|
29
29
|
update: (options: Options) => void;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export type TrackTransformerDestroyOptions = { willProcessorRestart: boolean };
|
|
33
|
+
|
|
32
34
|
export type TrackTransformer<Options extends Record<string, unknown>> =
|
|
33
35
|
| VideoTrackTransformer<Options>
|
|
34
36
|
| AudioTrackTransformer<Options>;
|
|
35
37
|
|
|
36
38
|
export interface BaseTrackTransformer<
|
|
37
|
-
|
|
39
|
+
InitOpts extends TrackTransformerInitOptions,
|
|
38
40
|
DataType extends VideoFrame | AudioData,
|
|
41
|
+
DestroyOpts extends TrackTransformerDestroyOptions = TrackTransformerDestroyOptions,
|
|
39
42
|
> {
|
|
40
|
-
init: (options:
|
|
41
|
-
destroy: () => void;
|
|
42
|
-
restart: (options:
|
|
43
|
+
init: (options: InitOpts) => void;
|
|
44
|
+
destroy: (options: DestroyOpts) => void;
|
|
45
|
+
restart: (options: InitOpts) => void;
|
|
43
46
|
transform: (frame: DataType, controller: TransformStreamDefaultController) => void;
|
|
44
47
|
transformer?: TransformStream;
|
|
45
48
|
}
|
package/src/webgl/index.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* - downsample the video texture in background blur scenario before applying the (gaussian) blur for better performance
|
|
5
5
|
*
|
|
6
6
|
*/
|
|
7
|
+
import { getLogger, LoggerNames} from '../logger';
|
|
7
8
|
import { applyBlur, createBlurProgram } from './shader-programs/blurShader';
|
|
8
9
|
import { createBoxBlurProgram } from './shader-programs/boxBlurShader';
|
|
9
10
|
import { createCompositeProgram } from './shader-programs/compositeShader';
|
|
@@ -16,6 +17,8 @@ import {
|
|
|
16
17
|
resizeImageToCover,
|
|
17
18
|
} from './utils';
|
|
18
19
|
|
|
20
|
+
const log = getLogger(LoggerNames.WebGl);
|
|
21
|
+
|
|
19
22
|
export const setupWebGL = (canvas: OffscreenCanvas | HTMLCanvasElement) => {
|
|
20
23
|
const gl = canvas.getContext('webgl2', {
|
|
21
24
|
antialias: true,
|
|
@@ -27,7 +30,7 @@ export const setupWebGL = (canvas: OffscreenCanvas | HTMLCanvasElement) => {
|
|
|
27
30
|
const downsampleFactor = 4;
|
|
28
31
|
|
|
29
32
|
if (!gl) {
|
|
30
|
-
|
|
33
|
+
log.error('Failed to create WebGL context');
|
|
31
34
|
return undefined;
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -42,6 +45,7 @@ export const setupWebGL = (canvas: OffscreenCanvas | HTMLCanvasElement) => {
|
|
|
42
45
|
mask: maskTextureLocation,
|
|
43
46
|
frame: frameTextureLocation,
|
|
44
47
|
background: bgTextureLocation,
|
|
48
|
+
disableBackground: disableBackgroundLocation,
|
|
45
49
|
} = composite.uniformLocations;
|
|
46
50
|
|
|
47
51
|
// Create the blur program using the same vertex shader source
|
|
@@ -103,14 +107,17 @@ export const setupWebGL = (canvas: OffscreenCanvas | HTMLCanvasElement) => {
|
|
|
103
107
|
createFramebuffer(gl, finalMaskTextures[1], canvas.width, canvas.height),
|
|
104
108
|
];
|
|
105
109
|
|
|
110
|
+
let backgroundImageDisabled = false;
|
|
111
|
+
|
|
106
112
|
// Set up uniforms for the composite shader
|
|
107
113
|
gl.useProgram(compositeProgram);
|
|
114
|
+
gl.uniform1i(disableBackgroundLocation, backgroundImageDisabled ? 1 : 0);
|
|
108
115
|
gl.uniform1i(bgTextureLocation, 0);
|
|
109
116
|
gl.uniform1i(frameTextureLocation, 1);
|
|
110
117
|
gl.uniform1i(maskTextureLocation, 2);
|
|
111
118
|
|
|
112
119
|
// Store custom background image
|
|
113
|
-
let customBackgroundImage: ImageBitmap | ImageData =
|
|
120
|
+
let customBackgroundImage: ImageBitmap | ImageData | null = null;
|
|
114
121
|
|
|
115
122
|
function renderFrame(frame: VideoFrame) {
|
|
116
123
|
if (frame.codedWidth === 0 || finalMaskTextures.length === 0) {
|
|
@@ -149,7 +156,7 @@ export const setupWebGL = (canvas: OffscreenCanvas | HTMLCanvasElement) => {
|
|
|
149
156
|
bgBlurFrameBuffers,
|
|
150
157
|
bgBlurTextures,
|
|
151
158
|
);
|
|
152
|
-
} else {
|
|
159
|
+
} else if (customBackgroundImage) {
|
|
153
160
|
gl.activeTexture(gl.TEXTURE0);
|
|
154
161
|
gl.bindTexture(gl.TEXTURE_2D, bgTexture);
|
|
155
162
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, customBackgroundImage);
|
|
@@ -170,6 +177,7 @@ export const setupWebGL = (canvas: OffscreenCanvas | HTMLCanvasElement) => {
|
|
|
170
177
|
gl.activeTexture(gl.TEXTURE0);
|
|
171
178
|
gl.bindTexture(gl.TEXTURE_2D, backgroundTexture);
|
|
172
179
|
gl.uniform1i(bgTextureLocation, 0);
|
|
180
|
+
gl.uniform1i(disableBackgroundLocation, backgroundImageDisabled ? 1 : 0);
|
|
173
181
|
|
|
174
182
|
// Set frame texture
|
|
175
183
|
gl.activeTexture(gl.TEXTURE1);
|
|
@@ -189,9 +197,10 @@ export const setupWebGL = (canvas: OffscreenCanvas | HTMLCanvasElement) => {
|
|
|
189
197
|
*/
|
|
190
198
|
async function setBackgroundImage(image: ImageBitmap | null) {
|
|
191
199
|
// Clear existing background
|
|
192
|
-
customBackgroundImage =
|
|
200
|
+
customBackgroundImage = null;
|
|
193
201
|
|
|
194
202
|
if (image) {
|
|
203
|
+
customBackgroundImage = getEmptyImageData();
|
|
195
204
|
try {
|
|
196
205
|
// Resize and crop the image to cover the canvas
|
|
197
206
|
const croppedImage = await resizeImageToCover(image, canvas.width, canvas.height);
|
|
@@ -199,16 +208,16 @@ export const setupWebGL = (canvas: OffscreenCanvas | HTMLCanvasElement) => {
|
|
|
199
208
|
// Store the cropped and resized image
|
|
200
209
|
customBackgroundImage = croppedImage;
|
|
201
210
|
} catch (error) {
|
|
202
|
-
|
|
211
|
+
log.error(
|
|
203
212
|
'Error processing background image, falling back to black background:',
|
|
204
213
|
error,
|
|
205
214
|
);
|
|
206
215
|
}
|
|
207
|
-
}
|
|
208
216
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
217
|
+
gl.activeTexture(gl.TEXTURE0);
|
|
218
|
+
gl.bindTexture(gl.TEXTURE_2D, bgTexture);
|
|
219
|
+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, customBackgroundImage);
|
|
220
|
+
}
|
|
212
221
|
}
|
|
213
222
|
|
|
214
223
|
function setBlurRadius(radius: number | null) {
|
|
@@ -216,6 +225,10 @@ export const setupWebGL = (canvas: OffscreenCanvas | HTMLCanvasElement) => {
|
|
|
216
225
|
setBackgroundImage(null);
|
|
217
226
|
}
|
|
218
227
|
|
|
228
|
+
function setBackgroundDisabled(disabled: boolean) {
|
|
229
|
+
backgroundImageDisabled = disabled;
|
|
230
|
+
}
|
|
231
|
+
|
|
219
232
|
function updateMask(mask: WebGLTexture) {
|
|
220
233
|
// Use the existing applyBlur function to apply the first blur pass
|
|
221
234
|
// The second blur pass will be written to finalMaskTextures[writeMaskIndex]
|
|
@@ -282,12 +295,12 @@ export const setupWebGL = (canvas: OffscreenCanvas | HTMLCanvasElement) => {
|
|
|
282
295
|
if (customBackgroundImage instanceof ImageBitmap) {
|
|
283
296
|
customBackgroundImage.close();
|
|
284
297
|
}
|
|
285
|
-
customBackgroundImage =
|
|
298
|
+
customBackgroundImage = null;
|
|
286
299
|
}
|
|
287
300
|
bgBlurTextures = [];
|
|
288
301
|
bgBlurFrameBuffers = [];
|
|
289
302
|
finalMaskTextures = [];
|
|
290
303
|
}
|
|
291
304
|
|
|
292
|
-
return { renderFrame, updateMask, setBackgroundImage, setBlurRadius, cleanup };
|
|
305
|
+
return { renderFrame, updateMask, setBackgroundImage, setBlurRadius, setBackgroundDisabled, cleanup };
|
|
293
306
|
};
|
|
@@ -6,29 +6,33 @@ export const compositeFragmentShader = glsl`#version 300 es
|
|
|
6
6
|
precision mediump float;
|
|
7
7
|
in vec2 texCoords;
|
|
8
8
|
uniform sampler2D background;
|
|
9
|
+
uniform bool disableBackground;
|
|
9
10
|
uniform sampler2D frame;
|
|
10
11
|
uniform sampler2D mask;
|
|
11
12
|
out vec4 fragColor;
|
|
12
13
|
|
|
13
14
|
void main() {
|
|
14
|
-
|
|
15
15
|
vec4 frameTex = texture(frame, texCoords);
|
|
16
|
-
vec4 bgTex = texture(background, texCoords);
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
if (disableBackground) {
|
|
18
|
+
fragColor = frameTex;
|
|
19
|
+
} else {
|
|
20
|
+
vec4 bgTex = texture(background, texCoords);
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
float grad = length(vec2(dFdx(maskVal), dFdy(maskVal)));
|
|
22
|
+
float maskVal = texture(mask, texCoords).r;
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// Create a smooth edge around binary transition
|
|
26
|
-
float smoothAlpha = smoothstep(0.5 - grad * edgeSoftness, 0.5 + grad * edgeSoftness, maskVal);
|
|
24
|
+
// Compute screen-space gradient to detect edge sharpness
|
|
25
|
+
float grad = length(vec2(dFdx(maskVal), dFdy(maskVal)));
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
float edgeSoftness = 2.0; // higher = softer
|
|
28
|
+
|
|
29
|
+
// Create a smooth edge around binary transition
|
|
30
|
+
float smoothAlpha = smoothstep(0.5 - grad * edgeSoftness, 0.5 + grad * edgeSoftness, maskVal);
|
|
31
|
+
|
|
32
|
+
// Optional: preserve frame alpha, or override as fully opaque
|
|
33
|
+
vec4 blended = mix(bgTex, vec4(frameTex.rgb, 1.0), 1.0 - smoothAlpha);
|
|
34
|
+
fragColor = blended;
|
|
35
|
+
}
|
|
32
36
|
|
|
33
37
|
}
|
|
34
38
|
`;
|
|
@@ -51,6 +55,7 @@ export function createCompositeProgram(gl: WebGL2RenderingContext) {
|
|
|
51
55
|
mask: gl.getUniformLocation(compositeProgram, 'mask')!,
|
|
52
56
|
frame: gl.getUniformLocation(compositeProgram, 'frame')!,
|
|
53
57
|
background: gl.getUniformLocation(compositeProgram, 'background')!,
|
|
58
|
+
disableBackground: gl.getUniformLocation(compositeProgram, 'disableBackground')!,
|
|
54
59
|
stepWidth: gl.getUniformLocation(compositeProgram, 'u_stepWidth')!,
|
|
55
60
|
};
|
|
56
61
|
|
package/src/webgl/utils.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { getLogger, LoggerNames} from '../logger';
|
|
2
|
+
|
|
3
|
+
const log = getLogger(LoggerNames.WebGl);
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* Initialize a WebGL texture
|
|
3
7
|
*/
|
|
@@ -24,7 +28,7 @@ export function createShader(
|
|
|
24
28
|
gl.shaderSource(shader, source);
|
|
25
29
|
gl.compileShader(shader);
|
|
26
30
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
27
|
-
|
|
31
|
+
log.error('Shader compile failed:', gl.getShaderInfoLog(shader));
|
|
28
32
|
gl.deleteShader(shader);
|
|
29
33
|
throw new Error('Shader compile failed');
|
|
30
34
|
}
|
|
@@ -41,7 +45,7 @@ export function createProgram(
|
|
|
41
45
|
gl.attachShader(program, fs);
|
|
42
46
|
gl.linkProgram(program);
|
|
43
47
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
44
|
-
|
|
48
|
+
log.error('Program link failed:', gl.getProgramInfoLog(program));
|
|
45
49
|
throw new Error('Program link failed');
|
|
46
50
|
}
|
|
47
51
|
return program;
|