@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.
- package/README.md +9 -1
- package/dist/index.js +628 -124
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +628 -124
- package/dist/index.mjs.map +1 -1
- package/dist/src/ProcessorWrapper.d.ts +26 -1
- package/dist/src/index.d.ts +24 -6
- package/dist/src/transformers/BackgroundTransformer.d.ts +8 -3
- package/dist/src/transformers/VideoTransformer.d.ts +2 -1
- package/dist/src/webgl/index.d.ts +9 -0
- package/package.json +9 -9
- package/src/ProcessorWrapper.ts +271 -18
- package/src/index.ts +96 -10
- package/src/transformers/BackgroundTransformer.ts +55 -103
- package/src/transformers/VideoTransformer.ts +14 -4
- package/src/utils.ts +1 -0
- package/src/webgl/index.ts +458 -0
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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.
|
|
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
|
|
130
|
-
if (!this.canvas || !this.
|
|
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
|
-
|
|
170
|
-
if (
|
|
171
|
-
|
|
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.
|
|
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.
|
|
55
|
+
this.gl?.cleanup();
|
|
56
|
+
this.gl = undefined;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
59
|
abstract transform(
|
package/src/utils.ts
CHANGED