@livekit/track-processors 0.4.1 → 0.5.1

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.
@@ -5,6 +5,12 @@ import { VideoTransformerInitOptions } from './types';
5
5
 
6
6
  export type SegmenterOptions = Partial<vision.ImageSegmenterOptions['baseOptions']>;
7
7
 
8
+ export interface FrameProcessingStats {
9
+ processingTimeMs: number;
10
+ segmentationTimeMs: number;
11
+ filterTimeMs: number;
12
+ }
13
+
8
14
  export type BackgroundOptions = {
9
15
  blurRadius?: number;
10
16
  imagePath?: string;
@@ -12,11 +18,18 @@ export type BackgroundOptions = {
12
18
  segmenterOptions?: SegmenterOptions;
13
19
  /** cannot be updated through the `update` method, needs a restart */
14
20
  assetPaths?: { tasksVisionFileSet?: string; modelAssetPath?: string };
21
+ /** called when a new frame is processed */
22
+ onFrameProcessed?: (stats: FrameProcessingStats) => void;
15
23
  };
16
24
 
17
25
  export default class BackgroundProcessor extends VideoTransformer<BackgroundOptions> {
18
26
  static get isSupported() {
19
- return typeof OffscreenCanvas !== 'undefined';
27
+ return (
28
+ typeof OffscreenCanvas !== 'undefined' &&
29
+ typeof VideoFrame !== 'undefined' &&
30
+ typeof createImageBitmap !== 'undefined' &&
31
+ !!document.createElement('canvas').getContext('webgl2')
32
+ );
20
33
  }
21
34
 
22
35
  imageSegmenter?: vision.ImageSegmenter;
@@ -25,8 +38,6 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
25
38
 
26
39
  backgroundImage: ImageBitmap | null = null;
27
40
 
28
- blurRadius?: number;
29
-
30
41
  options: BackgroundOptions;
31
42
 
32
43
  constructor(opts: BackgroundOptions) {
@@ -36,6 +47,8 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
36
47
  }
37
48
 
38
49
  async init({ outputCanvas, inputElement: inputVideo }: VideoTransformerInitOptions) {
50
+ // Initialize WebGL with appropriate options based on our current state
51
+
39
52
  await super.init({ outputCanvas, inputElement: inputVideo });
40
53
 
41
54
  const fileSet = await vision.FilesetResolver.forVisionTasks(
@@ -51,6 +64,7 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
51
64
  delegate: 'GPU',
52
65
  ...this.options.segmenterOptions,
53
66
  },
67
+ canvas: this.canvas,
54
68
  runningMode: 'VIDEO',
55
69
  outputCategoryMask: true,
56
70
  outputConfidenceMasks: false,
@@ -62,6 +76,9 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
62
76
  console.error('Error while loading processor background image: ', err),
63
77
  );
64
78
  }
79
+ if (this.options.blurRadius) {
80
+ this.gl?.setBlurRadius(this.options.blurRadius);
81
+ }
65
82
  }
66
83
 
67
84
  async destroy() {
@@ -80,15 +97,16 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
80
97
  img.src = path;
81
98
  });
82
99
  const imageData = await createImageBitmap(img);
83
- this.backgroundImage = imageData;
100
+ this.gl?.setBackgroundImage(imageData);
84
101
  }
85
102
 
86
103
  async transform(frame: VideoFrame, controller: TransformStreamDefaultController<VideoFrame>) {
87
104
  try {
88
- if (!(frame instanceof VideoFrame)) {
105
+ if (!(frame instanceof VideoFrame) || frame.codedWidth === 0 || frame.codedHeight === 0) {
89
106
  console.debug('empty frame detected, ignoring');
90
107
  return;
91
108
  }
109
+
92
110
  if (this.isDisabled) {
93
111
  controller.enqueue(frame);
94
112
  return;
@@ -96,119 +114,53 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
96
114
  if (!this.canvas) {
97
115
  throw TypeError('Canvas needs to be initialized first');
98
116
  }
117
+ this.canvas.width = frame.displayWidth;
118
+ this.canvas.height = frame.displayHeight;
99
119
  let startTimeMs = performance.now();
100
- this.imageSegmenter?.segmentForVideo(
101
- this.inputVideo!,
102
- startTimeMs,
103
- (result) => (this.segmentationResults = result),
104
- );
105
120
 
106
- if (this.blurRadius) {
107
- await this.blurBackground(frame);
108
- } else {
109
- await this.drawVirtualBackground(frame);
110
- }
111
- const newFrame = new VideoFrame(this.canvas, {
112
- timestamp: frame.timestamp || Date.now(),
121
+ this.imageSegmenter?.segmentForVideo(frame, startTimeMs, (result) => {
122
+ const segmentationTimeMs = performance.now() - startTimeMs;
123
+ this.segmentationResults = result;
124
+ this.drawFrame(frame);
125
+ if (this.canvas && this.canvas.width > 0 && this.canvas.height > 0) {
126
+ const newFrame = new VideoFrame(this.canvas, {
127
+ timestamp: frame.timestamp || Date.now(),
128
+ });
129
+ const filterTimeMs = performance.now() - startTimeMs - segmentationTimeMs;
130
+ const stats: FrameProcessingStats = {
131
+ processingTimeMs: performance.now() - startTimeMs,
132
+ segmentationTimeMs,
133
+ filterTimeMs,
134
+ };
135
+ this.options.onFrameProcessed?.(stats);
136
+
137
+ controller.enqueue(newFrame);
138
+ } else {
139
+ controller.enqueue(frame);
140
+ }
141
+ frame.close();
113
142
  });
114
- controller.enqueue(newFrame);
115
- } finally {
143
+ } catch (e) {
144
+ console.error('Error while processing frame: ', e);
116
145
  frame?.close();
117
146
  }
118
147
  }
119
148
 
120
149
  async update(opts: BackgroundOptions) {
121
- this.options = opts;
150
+ this.options = { ...this.options, ...opts };
122
151
  if (opts.blurRadius) {
123
- this.blurRadius = opts.blurRadius;
152
+ this.gl?.setBlurRadius(opts.blurRadius);
124
153
  } else if (opts.imagePath) {
125
154
  await this.loadBackground(opts.imagePath);
126
155
  }
127
156
  }
128
157
 
129
- async drawVirtualBackground(frame: VideoFrame) {
130
- if (!this.canvas || !this.ctx || !this.segmentationResults || !this.inputVideo) return;
131
- // this.ctx.save();
132
- // this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
133
- if (this.segmentationResults?.categoryMask && this.segmentationResults.categoryMask.width > 0) {
134
- this.ctx.globalCompositeOperation = 'copy';
135
-
136
- this.ctx.putImageData(
137
- maskToImageData(
138
- this.segmentationResults.categoryMask,
139
- this.segmentationResults.categoryMask.width,
140
- this.segmentationResults.categoryMask.height,
141
- ),
142
- 0,
143
- 0,
144
- );
145
- this.ctx.filter = 'none';
146
- this.ctx.globalCompositeOperation = 'source-in';
147
- if (this.backgroundImage) {
148
- this.ctx.drawImage(
149
- this.backgroundImage,
150
- 0,
151
- 0,
152
- this.backgroundImage.width,
153
- this.backgroundImage.height,
154
- 0,
155
- 0,
156
- this.canvas.width,
157
- this.canvas.height,
158
- );
159
- } else {
160
- this.ctx.fillStyle = '#00FF00';
161
- this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
162
- }
163
-
164
- this.ctx.globalCompositeOperation = 'destination-over';
165
- }
166
- this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
167
- }
158
+ async drawFrame(frame: VideoFrame) {
159
+ if (!this.canvas || !this.gl || !this.segmentationResults || !this.inputVideo) return;
168
160
 
169
- async blurBackground(frame: VideoFrame) {
170
- if (
171
- !this.ctx ||
172
- !this.canvas ||
173
- !this.segmentationResults?.categoryMask?.canvas ||
174
- !this.inputVideo
175
- ) {
176
- return;
161
+ const mask = this.segmentationResults.categoryMask;
162
+ if (mask) {
163
+ this.gl.render(frame, mask);
177
164
  }
178
-
179
- this.ctx.save();
180
- this.ctx.globalCompositeOperation = 'copy';
181
-
182
- if (this.segmentationResults?.categoryMask && this.segmentationResults.categoryMask.width > 0) {
183
- this.ctx.putImageData(
184
- maskToImageData(
185
- this.segmentationResults.categoryMask,
186
- this.segmentationResults.categoryMask.width,
187
- this.segmentationResults.categoryMask.height,
188
- ),
189
- 0,
190
- 0,
191
- );
192
- this.ctx.filter = 'none';
193
- this.ctx.globalCompositeOperation = 'source-out';
194
- this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
195
- this.ctx.globalCompositeOperation = 'destination-over';
196
- this.ctx.filter = `blur(${this.blurRadius}px)`;
197
- this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
198
- this.ctx.restore();
199
- }
200
- }
201
- }
202
-
203
- function maskToImageData(mask: vision.MPMask, videoWidth: number, videoHeight: number): ImageData {
204
- const dataArray: Uint8ClampedArray = new Uint8ClampedArray(videoWidth * videoHeight * 4);
205
- const result = mask.getAsUint8Array();
206
- for (let i = 0; i < result.length; i += 1) {
207
- const offset = i * 4;
208
- dataArray[offset] = result[i];
209
- dataArray[offset + 1] = result[i];
210
- dataArray[offset + 2] = result[i];
211
- dataArray[offset + 3] = result[i];
212
165
  }
213
- return new ImageData(dataArray, videoWidth, videoHeight);
214
166
  }
@@ -1,3 +1,4 @@
1
+ import { setupWebGL } from '../webgl/index';
1
2
  import { VideoTrackTransformer, VideoTransformerInitOptions } from './types';
2
3
 
3
4
  export default abstract class VideoTransformer<Options extends Record<string, unknown>>
@@ -7,10 +8,12 @@ export default abstract class VideoTransformer<Options extends Record<string, un
7
8
 
8
9
  canvas?: OffscreenCanvas;
9
10
 
10
- ctx?: OffscreenCanvasRenderingContext2D;
11
+ // ctx?: OffscreenCanvasRenderingContext2D;
11
12
 
12
13
  inputVideo?: HTMLVideoElement;
13
14
 
15
+ gl?: ReturnType<typeof setupWebGL>;
16
+
14
17
  protected isDisabled?: Boolean = false;
15
18
 
16
19
  async init({
@@ -26,7 +29,10 @@ export default abstract class VideoTransformer<Options extends Record<string, un
26
29
  });
27
30
  this.canvas = outputCanvas || null;
28
31
  if (outputCanvas) {
29
- this.ctx = this.canvas?.getContext('2d') || undefined;
32
+ // this.ctx = this.canvas?.getContext('2d') || undefined;
33
+ this.gl = setupWebGL(
34
+ this.canvas || new OffscreenCanvas(inputVideo.videoWidth, inputVideo.videoHeight),
35
+ );
30
36
  }
31
37
  this.inputVideo = inputVideo;
32
38
  this.isDisabled = false;
@@ -34,7 +40,10 @@ export default abstract class VideoTransformer<Options extends Record<string, un
34
40
 
35
41
  async restart({ outputCanvas, inputElement: inputVideo }: VideoTransformerInitOptions) {
36
42
  this.canvas = outputCanvas || null;
37
- this.ctx = this.canvas.getContext('2d') || undefined;
43
+ this.gl?.cleanup();
44
+ this.gl = setupWebGL(
45
+ this.canvas || new OffscreenCanvas(inputVideo.videoWidth, inputVideo.videoHeight),
46
+ );
38
47
 
39
48
  this.inputVideo = inputVideo;
40
49
  this.isDisabled = false;
@@ -43,7 +52,8 @@ export default abstract class VideoTransformer<Options extends Record<string, un
43
52
  async destroy() {
44
53
  this.isDisabled = true;
45
54
  this.canvas = undefined;
46
- this.ctx = undefined;
55
+ this.gl?.cleanup();
56
+ this.gl = undefined;
47
57
  }
48
58
 
49
59
  abstract transform(
package/src/utils.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/naming-convention */
1
2
  export const supportsProcessor = typeof MediaStreamTrackGenerator !== 'undefined';
2
3
  export const supportsOffscreenCanvas = typeof OffscreenCanvas !== 'undefined';
3
4