@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.
@@ -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
- console.error('Error while loading processor background image: ', 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
96
  this.backgroundImageAndPath = null;
92
- this.isFirstFrame = true;
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
- console.debug('empty frame detected, ignoring');
123
+ this.log.debug('empty frame detected, ignoring');
116
124
  return;
117
125
  }
118
126
 
119
- 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) {
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
- console.error('Error while processing frame: ', e);
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) {
@@ -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;