@livekit/track-processors 0.6.0 → 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.
@@ -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 */
@@ -36,7 +38,7 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
36
38
 
37
39
  segmentationResults: vision.ImageSegmenterResult | undefined;
38
40
 
39
- backgroundImage: ImageBitmap | null = null;
41
+ backgroundImageAndPath: { imageData: ImageBitmap, path: string } | null = null;
40
42
 
41
43
  options: BackgroundOptions;
42
44
 
@@ -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;
@@ -75,45 +79,57 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
75
79
  });
76
80
 
77
81
  // Skip loading the image here if update already loaded the image below
78
- if (this.options?.imagePath && !this.backgroundImage) {
79
- await this.loadBackground(this.options.imagePath).catch((err) =>
80
- console.error('Error while loading processor background image: ', err),
82
+ if (this.options?.imagePath) {
83
+ await this.loadAndSetBackground(this.options.imagePath).catch((err) =>
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
- this.backgroundImage = null;
92
- this.isFirstFrame = true;
96
+ this.backgroundImageAndPath = null;
97
+
98
+ if (!options?.willProcessorRestart) {
99
+ this.isFirstFrame = true;
100
+ }
93
101
  }
94
102
 
95
- async loadBackground(path: string) {
96
- const img = new Image();
103
+ async loadAndSetBackground(path: string) {
104
+ if (!this.backgroundImageAndPath || this.backgroundImageAndPath?.path !== path) {
105
+ const img = new Image();
97
106
 
98
- await new Promise((resolve, reject) => {
99
- img.crossOrigin = 'Anonymous';
100
- img.onload = () => resolve(img);
101
- img.onerror = (err) => reject(err);
102
- img.src = path;
103
- });
104
- const imageData = await createImageBitmap(img);
105
- this.gl?.setBackgroundImage(imageData);
107
+ await new Promise((resolve, reject) => {
108
+ img.crossOrigin = 'Anonymous';
109
+ img.onload = () => resolve(img);
110
+ img.onerror = (err) => reject(err);
111
+ img.src = path;
112
+ });
113
+ const imageData = await createImageBitmap(img);
114
+ this.backgroundImageAndPath = { imageData, path };
115
+ }
116
+ this.gl?.setBackgroundImage(this.backgroundImageAndPath.imageData);
106
117
  }
107
118
 
108
119
  async transform(frame: VideoFrame, controller: TransformStreamDefaultController<VideoFrame>) {
109
120
  let enqueuedFrame = false;
110
121
  try {
111
122
  if (!(frame instanceof VideoFrame) || frame.codedWidth === 0 || frame.codedHeight === 0) {
112
- console.debug('empty frame detected, ignoring');
123
+ this.log.debug('empty frame detected, ignoring');
113
124
  return;
114
125
  }
115
126
 
116
- if (this.isDisabled) {
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) {
117
133
  controller.enqueue(frame);
118
134
  enqueuedFrame = true;
119
135
  return;
@@ -187,7 +203,7 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
187
203
  }
188
204
  await segmentationPromise;
189
205
  } catch (e) {
190
- console.error('Error while processing frame: ', e);
206
+ this.log.error('Error while processing frame: ', e);
191
207
  } finally {
192
208
  if (!enqueuedFrame) {
193
209
  frame.close();
@@ -200,10 +216,11 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
200
216
 
201
217
  this.gl?.setBlurRadius(opts.blurRadius ?? null);
202
218
  if (opts.imagePath) {
203
- await this.loadBackground(opts.imagePath);
219
+ await this.loadAndSetBackground(opts.imagePath);
204
220
  } else {
205
221
  this.gl?.setBackgroundImage(null);
206
222
  }
223
+ this.gl?.setBackgroundDisabled(opts.backgroundDisabled ?? false);
207
224
  }
208
225
 
209
226
  private async drawFrame(frame: VideoFrame) {
@@ -15,7 +15,7 @@ export default abstract class VideoTransformer<Options extends Record<string, un
15
15
 
16
16
  gl?: ReturnType<typeof setupWebGL>;
17
17
 
18
- protected isDisabled?: Boolean = false;
18
+ protected isDisabled?: boolean = false;
19
19
 
20
20
  async init({
21
21
  outputCanvas,
@@ -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
- T extends TrackTransformerInitOptions,
39
+ InitOpts extends TrackTransformerInitOptions,
38
40
  DataType extends VideoFrame | AudioData,
41
+ DestroyOpts extends TrackTransformerDestroyOptions = TrackTransformerDestroyOptions,
39
42
  > {
40
- init: (options: T) => void;
41
- destroy: () => void;
42
- restart: (options: T) => void;
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
  }
@@ -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
- console.error('Failed to create WebGL context');
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 = getEmptyImageData();
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 = getEmptyImageData();
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
- console.error(
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
- 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);
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 = getEmptyImageData();
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
- float maskVal = texture(mask, texCoords).r;
17
+ if (disableBackground) {
18
+ fragColor = frameTex;
19
+ } else {
20
+ vec4 bgTex = texture(background, texCoords);
19
21
 
20
- // Compute screen-space gradient to detect edge sharpness
21
- float grad = length(vec2(dFdx(maskVal), dFdy(maskVal)));
22
+ float maskVal = texture(mask, texCoords).r;
22
23
 
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);
24
+ // Compute screen-space gradient to detect edge sharpness
25
+ float grad = length(vec2(dFdx(maskVal), dFdy(maskVal)));
27
26
 
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;
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
 
@@ -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
- console.error('Shader compile failed:', gl.getShaderInfoLog(shader));
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
- console.error('Program link failed:', gl.getProgramInfoLog(program));
48
+ log.error('Program link failed:', gl.getProgramInfoLog(program));
45
49
  throw new Error('Program link failed');
46
50
  }
47
51
  return program;