@livekit/track-processors 0.3.2 → 0.4.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 +12 -2
- package/dist/index.js +56 -65
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +56 -65
- package/dist/index.mjs.map +1 -1
- package/dist/src/ProcessorWrapper.d.ts +0 -2
- package/dist/src/transformers/types.d.ts +0 -1
- package/dist/src/utils.d.ts +7 -0
- package/package.json +3 -3
- package/src/ProcessorWrapper.ts +15 -24
- package/src/transformers/BackgroundTransformer.ts +25 -19
- package/src/transformers/VideoTransformer.ts +1 -0
- package/src/utils.ts +22 -0
- package/dist/src/transformers/MediaPipeHolisticTrackerTransformer.d.ts +0 -19
- package/src/transformers/MediaPipeHolisticTrackerTransformer.ts +0 -65
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ npm add @livekit/track-processors
|
|
|
10
10
|
|
|
11
11
|
### Available processors
|
|
12
12
|
|
|
13
|
-
This package exposes `BackgroundBlur` and `VirtualBackground`
|
|
13
|
+
This package exposes the `BackgroundBlur` and `VirtualBackground` pre-prepared processor pipelines.
|
|
14
14
|
|
|
15
15
|
- `BackgroundBlur(blurRadius)`
|
|
16
16
|
- `VirtualBackground(imagePath)`
|
|
@@ -30,7 +30,7 @@ async function disableBackgroundBlur() {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
async updateBlurRadius(radius) {
|
|
33
|
-
return blur.updateTransformerOptions({blurRadius:
|
|
33
|
+
return blur.updateTransformerOptions({blurRadius: radius})
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
|
|
@@ -51,3 +51,13 @@ export const VirtualBackground = (imagePath: string) => {
|
|
|
51
51
|
### Available base transformers
|
|
52
52
|
|
|
53
53
|
- BackgroundTransformer (can blur background or use a virtual background);
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
## Running the sample app
|
|
57
|
+
|
|
58
|
+
This repository includes a small example app built on [Vite](https://vitejs.dev/). Run it with:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
npm install
|
|
62
|
+
npm run sample
|
|
63
|
+
```
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } }
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
14
|
-
if (__propIsEnum.call(b, prop))
|
|
15
|
-
__defNormalProp(a, prop, b[prop]);
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } }// src/utils.ts
|
|
2
|
+
async function sleep(time) {
|
|
3
|
+
return new Promise((resolve) => setTimeout(resolve, time));
|
|
4
|
+
}
|
|
5
|
+
async function waitForTrackResolution(track) {
|
|
6
|
+
const timeout = 500;
|
|
7
|
+
await sleep(10);
|
|
8
|
+
const started = Date.now();
|
|
9
|
+
while (Date.now() - started < timeout) {
|
|
10
|
+
const { width, height } = track.getSettings();
|
|
11
|
+
if (width && height) {
|
|
12
|
+
return { width, height };
|
|
16
13
|
}
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
14
|
+
await sleep(50);
|
|
15
|
+
}
|
|
16
|
+
return { width: void 0, height: void 0 };
|
|
17
|
+
}
|
|
20
18
|
|
|
21
19
|
// src/ProcessorWrapper.ts
|
|
22
20
|
var ProcessorWrapper = class {
|
|
@@ -26,50 +24,37 @@ var ProcessorWrapper = class {
|
|
|
26
24
|
constructor(transformer, name) {
|
|
27
25
|
this.name = name;
|
|
28
26
|
this.transformer = transformer;
|
|
29
|
-
this.transformer.restart;
|
|
30
27
|
}
|
|
31
28
|
async setup(opts) {
|
|
32
|
-
var _a, _b, _c, _d;
|
|
33
29
|
this.source = opts.track;
|
|
34
|
-
const
|
|
35
|
-
await this.source.applyConstraints(__spreadProps(__spreadValues({}, origConstraints), {
|
|
36
|
-
// @ts-expect-error when a mediastream track is resized and/or cropped, the `VideoFrame` will have a coded height/width of the original video size
|
|
37
|
-
// this leads to a shift of the underlying video as the frame itself is being rendered with the coded size
|
|
38
|
-
// but image segmentation is based on the display dimensions (-> the cropped version)
|
|
39
|
-
// in order to prevent this, we force the resize mode to "none"
|
|
40
|
-
resizeMode: "none"
|
|
41
|
-
}));
|
|
42
|
-
this.sourceSettings = this.source.getSettings();
|
|
30
|
+
const { width, height } = await waitForTrackResolution(this.source);
|
|
43
31
|
this.sourceDummy = opts.element;
|
|
44
|
-
if (this.sourceDummy instanceof HTMLVideoElement) {
|
|
45
|
-
this.sourceDummy.height = (_a = this.sourceSettings.height) != null ? _a : 300;
|
|
46
|
-
this.sourceDummy.width = (_b = this.sourceSettings.width) != null ? _b : 300;
|
|
47
|
-
}
|
|
48
32
|
if (!(this.sourceDummy instanceof HTMLVideoElement)) {
|
|
49
33
|
throw TypeError("Currently only video transformers are supported");
|
|
50
34
|
}
|
|
35
|
+
if (this.sourceDummy instanceof HTMLVideoElement) {
|
|
36
|
+
this.sourceDummy.height = height != null ? height : 300;
|
|
37
|
+
this.sourceDummy.width = width != null ? width : 300;
|
|
38
|
+
}
|
|
51
39
|
this.processor = new MediaStreamTrackProcessor({ track: this.source });
|
|
52
40
|
this.trackGenerator = new MediaStreamTrackGenerator({
|
|
53
41
|
kind: "video",
|
|
54
42
|
signalTarget: this.source
|
|
55
43
|
});
|
|
56
|
-
this.canvas = new OffscreenCanvas(
|
|
57
|
-
(_c = this.sourceSettings.width) != null ? _c : 300,
|
|
58
|
-
(_d = this.sourceSettings.height) != null ? _d : 300
|
|
59
|
-
);
|
|
44
|
+
this.canvas = new OffscreenCanvas(width != null ? width : 300, height != null ? height : 300);
|
|
60
45
|
}
|
|
61
46
|
async init(opts) {
|
|
62
47
|
await this.setup(opts);
|
|
63
48
|
if (!this.canvas || !this.processor || !this.trackGenerator) {
|
|
64
49
|
throw new TypeError("Expected both canvas and processor to be defined after setup");
|
|
65
50
|
}
|
|
66
|
-
|
|
51
|
+
const readableStream = this.processor.readable;
|
|
67
52
|
await this.transformer.init({
|
|
68
53
|
outputCanvas: this.canvas,
|
|
69
54
|
inputElement: this.sourceDummy
|
|
70
55
|
});
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
const pipedStream = readableStream.pipeThrough(this.transformer.transformer);
|
|
57
|
+
pipedStream.pipeTo(this.trackGenerator.writable).catch((e) => console.error("error when trying to pipe", e)).finally(() => this.destroy());
|
|
73
58
|
this.processedTrack = this.trackGenerator;
|
|
74
59
|
}
|
|
75
60
|
async restart(opts) {
|
|
@@ -94,8 +79,7 @@ var _tasksvision = require('@mediapipe/tasks-vision'); var vision = _interopRequ
|
|
|
94
79
|
|
|
95
80
|
// package.json
|
|
96
81
|
var dependencies = {
|
|
97
|
-
"@mediapipe/
|
|
98
|
-
"@mediapipe/tasks-vision": "0.10.9"
|
|
82
|
+
"@mediapipe/tasks-vision": "0.10.21"
|
|
99
83
|
};
|
|
100
84
|
|
|
101
85
|
// src/transformers/VideoTransformer.ts
|
|
@@ -152,16 +136,19 @@ var BackgroundProcessor = class extends VideoTransformer {
|
|
|
152
136
|
(_b = (_a = this.options.assetPaths) == null ? void 0 : _a.tasksVisionFileSet) != null ? _b : `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${dependencies["@mediapipe/tasks-vision"]}/wasm`
|
|
153
137
|
);
|
|
154
138
|
this.imageSegmenter = await vision.ImageSegmenter.createFromOptions(fileSet, {
|
|
155
|
-
baseOptions:
|
|
139
|
+
baseOptions: {
|
|
156
140
|
modelAssetPath: (_d = (_c = this.options.assetPaths) == null ? void 0 : _c.modelAssetPath) != null ? _d : "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite",
|
|
157
|
-
delegate: "GPU"
|
|
158
|
-
|
|
141
|
+
delegate: "GPU",
|
|
142
|
+
...this.options.segmenterOptions
|
|
143
|
+
},
|
|
159
144
|
runningMode: "VIDEO",
|
|
160
145
|
outputCategoryMask: true,
|
|
161
146
|
outputConfidenceMasks: false
|
|
162
147
|
});
|
|
163
148
|
if (((_e = this.options) == null ? void 0 : _e.imagePath) && !this.backgroundImage) {
|
|
164
|
-
await this.loadBackground(this.options.imagePath).catch(
|
|
149
|
+
await this.loadBackground(this.options.imagePath).catch(
|
|
150
|
+
(err) => console.error("Error while loading processor background image: ", err)
|
|
151
|
+
);
|
|
165
152
|
}
|
|
166
153
|
}
|
|
167
154
|
async destroy() {
|
|
@@ -184,6 +171,10 @@ var BackgroundProcessor = class extends VideoTransformer {
|
|
|
184
171
|
async transform(frame, controller) {
|
|
185
172
|
var _a;
|
|
186
173
|
try {
|
|
174
|
+
if (!(frame instanceof VideoFrame)) {
|
|
175
|
+
console.debug("empty frame detected, ignoring");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
187
178
|
if (this.isDisabled) {
|
|
188
179
|
controller.enqueue(frame);
|
|
189
180
|
return;
|
|
@@ -207,7 +198,7 @@ var BackgroundProcessor = class extends VideoTransformer {
|
|
|
207
198
|
});
|
|
208
199
|
controller.enqueue(newFrame);
|
|
209
200
|
} finally {
|
|
210
|
-
frame.close();
|
|
201
|
+
frame == null ? void 0 : frame.close();
|
|
211
202
|
}
|
|
212
203
|
}
|
|
213
204
|
async update(opts) {
|
|
@@ -222,8 +213,7 @@ var BackgroundProcessor = class extends VideoTransformer {
|
|
|
222
213
|
var _a;
|
|
223
214
|
if (!this.canvas || !this.ctx || !this.segmentationResults || !this.inputVideo)
|
|
224
215
|
return;
|
|
225
|
-
if ((_a = this.segmentationResults) == null ? void 0 : _a.categoryMask) {
|
|
226
|
-
this.ctx.filter = "blur(10px)";
|
|
216
|
+
if (((_a = this.segmentationResults) == null ? void 0 : _a.categoryMask) && this.segmentationResults.categoryMask.width > 0) {
|
|
227
217
|
this.ctx.globalCompositeOperation = "copy";
|
|
228
218
|
const bitmap = await maskToBitmap(
|
|
229
219
|
this.segmentationResults.categoryMask,
|
|
@@ -254,27 +244,28 @@ var BackgroundProcessor = class extends VideoTransformer {
|
|
|
254
244
|
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
255
245
|
}
|
|
256
246
|
async blurBackground(frame) {
|
|
257
|
-
var _a, _b;
|
|
247
|
+
var _a, _b, _c;
|
|
258
248
|
if (!this.ctx || !this.canvas || !((_b = (_a = this.segmentationResults) == null ? void 0 : _a.categoryMask) == null ? void 0 : _b.canvas) || !this.inputVideo) {
|
|
259
249
|
return;
|
|
260
250
|
}
|
|
261
251
|
this.ctx.save();
|
|
262
252
|
this.ctx.globalCompositeOperation = "copy";
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
253
|
+
if (((_c = this.segmentationResults) == null ? void 0 : _c.categoryMask) && this.segmentationResults.categoryMask.width > 0) {
|
|
254
|
+
const bitmap = await maskToBitmap(
|
|
255
|
+
this.segmentationResults.categoryMask,
|
|
256
|
+
this.segmentationResults.categoryMask.width,
|
|
257
|
+
this.segmentationResults.categoryMask.height
|
|
258
|
+
);
|
|
259
|
+
this.ctx.globalCompositeOperation = "copy";
|
|
260
|
+
this.ctx.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);
|
|
261
|
+
this.ctx.filter = "none";
|
|
262
|
+
this.ctx.globalCompositeOperation = "source-out";
|
|
263
|
+
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
264
|
+
this.ctx.globalCompositeOperation = "destination-over";
|
|
265
|
+
this.ctx.filter = `blur(${this.blurRadius}px)`;
|
|
266
|
+
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
267
|
+
this.ctx.restore();
|
|
268
|
+
}
|
|
278
269
|
}
|
|
279
270
|
};
|
|
280
271
|
function maskToBitmap(mask, videoWidth, videoHeight) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ProcessorWrapper.ts","../src/transformers/BackgroundTransformer.ts","../package.json","../src/transformers/VideoTransformer.ts","../src/index.ts"],"names":["BackgroundProcessor"],"mappings":"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","sourcesContent":["import type { ProcessorOptions, Track, TrackProcessor } from 'livekit-client';\nimport { TrackTransformer } from './transformers';\n\nexport default class ProcessorWrapper<TransformerOptions extends Record<string, unknown>>\n implements TrackProcessor<Track.Kind>\n{\n static get isSupported() {\n return (\n typeof MediaStreamTrackGenerator !== 'undefined' &&\n typeof MediaStreamTrackProcessor !== 'undefined'\n );\n }\n\n name: string;\n\n source?: MediaStreamVideoTrack;\n\n sourceSettings?: MediaTrackSettings;\n\n processor?: MediaStreamTrackProcessor<VideoFrame>;\n\n trackGenerator?: MediaStreamTrackGenerator<VideoFrame>;\n\n canvas?: OffscreenCanvas;\n\n sourceDummy?: HTMLMediaElement;\n\n processedTrack?: MediaStreamTrack;\n\n transformer: TrackTransformer<TransformerOptions>;\n\n constructor(transformer: TrackTransformer<TransformerOptions>, name: string) {\n this.name = name;\n this.transformer = transformer;\n this.transformer.restart;\n }\n\n private async setup(opts: ProcessorOptions<Track.Kind>) {\n this.source = opts.track as MediaStreamVideoTrack;\n const origConstraints = this.source.getConstraints();\n await this.source.applyConstraints({\n ...origConstraints,\n // @ts-expect-error when a mediastream track is resized and/or cropped, the `VideoFrame` will have a coded height/width of the original video size\n // this leads to a shift of the underlying video as the frame itself is being rendered with the coded size\n // but image segmentation is based on the display dimensions (-> the cropped version)\n // in order to prevent this, we force the resize mode to \"none\"\n resizeMode: 'none',\n });\n this.sourceSettings = this.source.getSettings();\n this.sourceDummy = opts.element;\n if (this.sourceDummy instanceof HTMLVideoElement) {\n this.sourceDummy.height = this.sourceSettings.height ?? 300;\n this.sourceDummy.width = this.sourceSettings.width ?? 300;\n }\n if (!(this.sourceDummy instanceof HTMLVideoElement)) {\n throw TypeError('Currently only video transformers are supported');\n }\n // TODO explore if we can do all the processing work in a webworker\n this.processor = new MediaStreamTrackProcessor({ track: this.source });\n\n this.trackGenerator = new MediaStreamTrackGenerator({\n kind: 'video',\n signalTarget: this.source,\n });\n\n this.canvas = new OffscreenCanvas(\n this.sourceSettings.width ?? 300,\n this.sourceSettings.height ?? 300,\n );\n }\n\n async init(opts: ProcessorOptions<Track.Kind>) {\n await this.setup(opts);\n if (!this.canvas || !this.processor || !this.trackGenerator) {\n throw new TypeError('Expected both canvas and processor to be defined after setup');\n }\n\n let readableStream = this.processor.readable;\n\n await this.transformer.init({\n outputCanvas: this.canvas,\n inputElement: this.sourceDummy as HTMLVideoElement,\n });\n readableStream = readableStream.pipeThrough(this.transformer!.transformer!);\n\n readableStream\n .pipeTo(this.trackGenerator.writable)\n .catch((e) => console.error('error when trying to pipe', e))\n .finally(() => this.destroy());\n this.processedTrack = this.trackGenerator as MediaStreamVideoTrack;\n }\n\n async restart(opts: ProcessorOptions<Track.Kind>) {\n await this.destroy();\n return this.init(opts);\n }\n\n async restartTransformer(...options: Parameters<(typeof this.transformer)['restart']>) {\n // @ts-ignore unclear why the restart method only accepts VideoTransformerInitOptions instead of either those or AudioTransformerInitOptions\n this.transformer.restart(options[0]);\n }\n\n async updateTransformerOptions(...options: Parameters<(typeof this.transformer)['update']>) {\n this.transformer.update(options[0]);\n }\n\n async destroy() {\n await this.transformer.destroy();\n this.trackGenerator?.stop();\n }\n}\n","import * as vision from '@mediapipe/tasks-vision';\nimport { dependencies } from '../../package.json';\nimport VideoTransformer from './VideoTransformer';\nimport { VideoTransformerInitOptions } from './types';\n\nexport type SegmenterOptions = Partial<vision.ImageSegmenterOptions['baseOptions']>;\n\nexport type BackgroundOptions = {\n blurRadius?: number;\n imagePath?: string;\n /** cannot be updated through the `update` method, needs a restart */\n segmenterOptions?: SegmenterOptions;\n /** cannot be updated through the `update` method, needs a restart */\n assetPaths?: { tasksVisionFileSet?: string; modelAssetPath?: string };\n};\n\nexport default class BackgroundProcessor extends VideoTransformer<BackgroundOptions> {\n static get isSupported() {\n return typeof OffscreenCanvas !== 'undefined';\n }\n\n imageSegmenter?: vision.ImageSegmenter;\n\n segmentationResults: vision.ImageSegmenterResult | undefined;\n\n backgroundImage: ImageBitmap | null = null;\n\n blurRadius?: number;\n\n options: BackgroundOptions;\n\n constructor(opts: BackgroundOptions) {\n super();\n this.options = opts;\n this.update(opts);\n }\n\n async init({ outputCanvas, inputElement: inputVideo }: VideoTransformerInitOptions) {\n await super.init({ outputCanvas, inputElement: inputVideo });\n\n const fileSet = await vision.FilesetResolver.forVisionTasks(\n this.options.assetPaths?.tasksVisionFileSet ??\n `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${dependencies['@mediapipe/tasks-vision']}/wasm`,\n );\n\n this.imageSegmenter = await vision.ImageSegmenter.createFromOptions(fileSet, {\n baseOptions: {\n modelAssetPath:\n this.options.assetPaths?.modelAssetPath ??\n 'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite',\n delegate: 'GPU',\n ...this.options.segmenterOptions,\n },\n runningMode: 'VIDEO',\n outputCategoryMask: true,\n outputConfidenceMasks: false,\n });\n\n // Skip loading the image here if update already loaded the image below\n if (this.options?.imagePath && !this.backgroundImage) {\n await this.loadBackground(this.options.imagePath).catch((err) => console.error(\"Error while loading processor background image: \", err));\n }\n }\n\n async destroy() {\n await super.destroy();\n await this.imageSegmenter?.close();\n this.backgroundImage = null;\n }\n\n async loadBackground(path: string) {\n const img = new Image();\n\n await new Promise((resolve, reject) => {\n img.crossOrigin = 'Anonymous';\n img.onload = () => resolve(img);\n img.onerror = (err) => reject(err);\n img.src = path;\n });\n const imageData = await createImageBitmap(img);\n this.backgroundImage = imageData;\n }\n\n async transform(frame: VideoFrame, controller: TransformStreamDefaultController<VideoFrame>) {\n try {\n if (this.isDisabled) {\n controller.enqueue(frame);\n return;\n }\n if (!this.canvas) {\n throw TypeError('Canvas needs to be initialized first');\n }\n let startTimeMs = performance.now();\n this.imageSegmenter?.segmentForVideo(\n this.inputVideo!,\n startTimeMs,\n (result) => (this.segmentationResults = result),\n );\n\n if (this.blurRadius) {\n await this.blurBackground(frame);\n } else {\n await this.drawVirtualBackground(frame);\n }\n const newFrame = new VideoFrame(this.canvas, {\n timestamp: frame.timestamp || Date.now(),\n });\n controller.enqueue(newFrame);\n } finally {\n frame.close();\n }\n }\n\n async update(opts: BackgroundOptions) {\n this.options = opts;\n if (opts.blurRadius) {\n this.blurRadius = opts.blurRadius;\n } else if (opts.imagePath) {\n await this.loadBackground(opts.imagePath);\n }\n }\n\n async drawVirtualBackground(frame: VideoFrame) {\n if (!this.canvas || !this.ctx || !this.segmentationResults || !this.inputVideo) return;\n // this.ctx.save();\n // this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n if (this.segmentationResults?.categoryMask) {\n this.ctx.filter = 'blur(10px)';\n this.ctx.globalCompositeOperation = 'copy';\n const bitmap = await maskToBitmap(\n this.segmentationResults.categoryMask,\n this.segmentationResults.categoryMask.width,\n this.segmentationResults.categoryMask.height,\n );\n this.ctx.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.filter = 'none';\n this.ctx.globalCompositeOperation = 'source-in';\n if (this.backgroundImage) {\n this.ctx.drawImage(\n this.backgroundImage,\n 0,\n 0,\n this.backgroundImage.width,\n this.backgroundImage.height,\n 0,\n 0,\n this.canvas.width,\n this.canvas.height,\n );\n } else {\n this.ctx.fillStyle = '#00FF00';\n this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);\n }\n\n this.ctx.globalCompositeOperation = 'destination-over';\n }\n this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);\n // this.ctx.restore();\n }\n\n async blurBackground(frame: VideoFrame) {\n if (\n !this.ctx ||\n !this.canvas ||\n !this.segmentationResults?.categoryMask?.canvas ||\n !this.inputVideo\n ) {\n return;\n }\n\n this.ctx.save();\n this.ctx.globalCompositeOperation = 'copy';\n\n const bitmap = await maskToBitmap(\n this.segmentationResults.categoryMask,\n this.segmentationResults.categoryMask.width,\n this.segmentationResults.categoryMask.height,\n );\n\n this.ctx.filter = 'blur(3px)';\n this.ctx.globalCompositeOperation = 'copy';\n this.ctx.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.filter = 'none';\n this.ctx.globalCompositeOperation = 'source-out';\n this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.globalCompositeOperation = 'destination-over';\n this.ctx.filter = `blur(${this.blurRadius}px)`;\n this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.restore();\n }\n}\n\nfunction maskToBitmap(\n mask: vision.MPMask,\n videoWidth: number,\n videoHeight: number,\n): Promise<ImageBitmap> {\n const dataArray: Uint8ClampedArray = new Uint8ClampedArray(videoWidth * videoHeight * 4);\n const result = mask.getAsUint8Array();\n for (let i = 0; i < result.length; i += 1) {\n dataArray[i * 4] = result[i];\n dataArray[i * 4 + 1] = result[i];\n dataArray[i * 4 + 2] = result[i];\n dataArray[i * 4 + 3] = result[i];\n }\n const dataNew = new ImageData(dataArray, videoWidth, videoHeight);\n\n return createImageBitmap(dataNew);\n}\n","{\n \"name\": \"@livekit/track-processors\",\n \"version\": \"0.3.2\",\n \"description\": \"LiveKit track processors\",\n \"main\": \"dist/index.js\",\n \"module\": \"dist/index.mjs\",\n \"source\": \"src/index.ts\",\n \"types\": \"dist/src/index.d.ts\",\n \"repository\": \"git@github.com:livekit/livekit-track-processors.git\",\n \"author\": \"Lukas Seiler\",\n \"license\": \"Apache-2.0\",\n \"scripts\": {\n \"build\": \"tsup --onSuccess \\\"tsc --declaration --emitDeclarationOnly\\\"\",\n \"build-sample\": \"cd example && vite build\",\n \"lint\": \"eslint src\",\n \"release\": \"pnpm build && changeset publish\",\n \"test\": \"jest\",\n \"sample\": \"vite serve example --port 8080 --open\"\n },\n \"files\": [\n \"dist\",\n \"src\"\n ],\n \"dependencies\": {\n \"@mediapipe/holistic\": \"0.5.1675471629\",\n \"@mediapipe/tasks-vision\": \"0.10.9\"\n },\n \"peerDependencies\": {\n \"livekit-client\": \"^1.12.0 || ^2.1.0\"\n },\n \"devDependencies\": {\n \"@changesets/cli\": \"^2.26.2\",\n \"@livekit/changesets-changelog-github\": \"^0.0.4\",\n \"@trivago/prettier-plugin-sort-imports\": \"^4.1.1\",\n \"@types/dom-mediacapture-transform\": \"^0.1.6\",\n \"@types/offscreencanvas\": \"^2019.7.0\",\n \"@typescript-eslint/eslint-plugin\": \"^5.62.0\",\n \"eslint\": \"8.39.0\",\n \"eslint-config-airbnb-typescript\": \"17.0.0\",\n \"eslint-config-prettier\": \"8.8.0\",\n \"eslint-plugin-ecmascript-compat\": \"^3.0.0\",\n \"eslint-plugin-import\": \"2.27.5\",\n \"prettier\": \"^2.8.8\",\n \"tsup\": \"^7.1.0\",\n \"typescript\": \"^5.0.4\",\n \"vite\": \"^4.3.8\"\n }\n}\n","import { VideoTrackTransformer, VideoTransformerInitOptions } from './types';\n\nexport default abstract class VideoTransformer<Options extends Record<string, unknown>>\n implements VideoTrackTransformer<Options>\n{\n transformer?: TransformStream;\n\n canvas?: OffscreenCanvas;\n\n ctx?: OffscreenCanvasRenderingContext2D;\n\n inputVideo?: HTMLVideoElement;\n\n protected isDisabled?: Boolean = false;\n\n async init({\n outputCanvas,\n inputElement: inputVideo,\n }: VideoTransformerInitOptions): Promise<void> {\n if (!(inputVideo instanceof HTMLVideoElement)) {\n throw TypeError('Video transformer needs a HTMLVideoElement as input');\n }\n this.transformer = new TransformStream({\n transform: (frame, controller) => this.transform(frame, controller),\n });\n this.canvas = outputCanvas || null;\n if (outputCanvas) {\n this.ctx = this.canvas?.getContext('2d') || undefined;\n }\n this.inputVideo = inputVideo;\n this.isDisabled = false;\n }\n\n async restart({ outputCanvas, inputElement: inputVideo }: VideoTransformerInitOptions) {\n this.canvas = outputCanvas || null;\n this.ctx = this.canvas.getContext('2d') || undefined;\n\n this.inputVideo = inputVideo;\n this.isDisabled = false;\n }\n\n async destroy() {\n this.isDisabled = true;\n this.canvas = undefined;\n this.ctx = undefined;\n }\n\n abstract transform(\n frame: VideoFrame,\n controller: TransformStreamDefaultController<VideoFrame>,\n ): void;\n\n abstract update(options: Options): void;\n}\n","import ProcessorWrapper from './ProcessorWrapper';\nimport BackgroundTransformer, {\n BackgroundOptions,\n SegmenterOptions,\n} from './transformers/BackgroundTransformer';\n\nexport * from './transformers/types';\nexport { default as VideoTransformer } from './transformers/VideoTransformer';\nexport { ProcessorWrapper, type BackgroundOptions, type SegmenterOptions, BackgroundTransformer };\n\nexport const BackgroundBlur = (blurRadius: number = 10, segmenterOptions?: SegmenterOptions) => {\n return BackgroundProcessor({ blurRadius, segmenterOptions }, 'background-blur');\n};\n\nexport const VirtualBackground = (imagePath: string, segmenterOptions?: SegmenterOptions) => {\n return BackgroundProcessor({ imagePath, segmenterOptions }, 'virtual-background');\n};\n\nexport const BackgroundProcessor = (options: BackgroundOptions, name = 'background-processor') => {\n const isProcessorSupported = ProcessorWrapper.isSupported && BackgroundTransformer.isSupported;\n if (!isProcessorSupported) {\n throw new Error('processor is not supported in this browser');\n }\n const processor = new ProcessorWrapper(new BackgroundTransformer(options), name);\n return processor;\n};\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/utils.ts","../src/ProcessorWrapper.ts","../src/transformers/BackgroundTransformer.ts","../package.json","../src/transformers/VideoTransformer.ts","../src/index.ts"],"names":["BackgroundProcessor"],"mappings":"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","sourcesContent":["export const supportsProcessor = typeof MediaStreamTrackGenerator !== 'undefined';\nexport const supportsOffscreenCanvas = typeof OffscreenCanvas !== 'undefined';\n\nasync function sleep(time: number) {\n return new Promise((resolve) => setTimeout(resolve, time));\n}\n\nexport async function waitForTrackResolution(track: MediaStreamTrack) {\n const timeout = 500;\n\n // browsers report wrong initial resolution on iOS.\n // when slightly delaying the call to .getSettings(), the correct resolution is being reported\n await sleep(10);\n\n const started = Date.now();\n while (Date.now() - started < timeout) {\n const { width, height } = track.getSettings();\n if (width && height) {\n return { width, height };\n }\n await sleep(50);\n }\n return { width: undefined, height: undefined };\n}\n","import type { ProcessorOptions, Track, TrackProcessor } from 'livekit-client';\nimport { TrackTransformer } from './transformers';\nimport { waitForTrackResolution } from './utils';\n\nexport default class ProcessorWrapper<TransformerOptions extends Record<string, unknown>>\n implements TrackProcessor<Track.Kind>\n{\n static get isSupported() {\n return (\n typeof MediaStreamTrackGenerator !== 'undefined' &&\n typeof MediaStreamTrackProcessor !== 'undefined'\n );\n }\n\n name: string;\n\n source?: MediaStreamVideoTrack;\n\n processor?: MediaStreamTrackProcessor<VideoFrame>;\n\n trackGenerator?: MediaStreamTrackGenerator<VideoFrame>;\n\n canvas?: OffscreenCanvas;\n\n sourceDummy?: HTMLMediaElement;\n\n processedTrack?: MediaStreamTrack;\n\n transformer: TrackTransformer<TransformerOptions>;\n\n constructor(transformer: TrackTransformer<TransformerOptions>, name: string) {\n this.name = name;\n this.transformer = transformer;\n }\n\n private async setup(opts: ProcessorOptions<Track.Kind>) {\n this.source = opts.track as MediaStreamVideoTrack;\n\n const { width, height } = await waitForTrackResolution(this.source);\n this.sourceDummy = opts.element;\n\n if (!(this.sourceDummy instanceof HTMLVideoElement)) {\n throw TypeError('Currently only video transformers are supported');\n }\n\n if (this.sourceDummy instanceof HTMLVideoElement) {\n this.sourceDummy.height = height ?? 300;\n this.sourceDummy.width = width ?? 300;\n }\n\n // TODO explore if we can do all the processing work in a webworker\n this.processor = new MediaStreamTrackProcessor({ track: this.source });\n\n this.trackGenerator = new MediaStreamTrackGenerator({\n kind: 'video',\n signalTarget: this.source,\n });\n\n this.canvas = new OffscreenCanvas(width ?? 300, height ?? 300);\n }\n\n async init(opts: ProcessorOptions<Track.Kind>) {\n await this.setup(opts);\n if (!this.canvas || !this.processor || !this.trackGenerator) {\n throw new TypeError('Expected both canvas and processor to be defined after setup');\n }\n\n const readableStream = this.processor.readable;\n\n await this.transformer.init({\n outputCanvas: this.canvas,\n inputElement: this.sourceDummy as HTMLVideoElement,\n });\n\n const pipedStream = readableStream.pipeThrough(this.transformer!.transformer!);\n\n pipedStream\n .pipeTo(this.trackGenerator.writable)\n .catch((e) => console.error('error when trying to pipe', e))\n .finally(() => this.destroy());\n this.processedTrack = this.trackGenerator as MediaStreamVideoTrack;\n }\n\n async restart(opts: ProcessorOptions<Track.Kind>) {\n await this.destroy();\n return this.init(opts);\n }\n\n async restartTransformer(...options: Parameters<(typeof this.transformer)['restart']>) {\n // @ts-ignore unclear why the restart method only accepts VideoTransformerInitOptions instead of either those or AudioTransformerInitOptions\n this.transformer.restart(options[0]);\n }\n\n async updateTransformerOptions(...options: Parameters<(typeof this.transformer)['update']>) {\n this.transformer.update(options[0]);\n }\n\n async destroy() {\n await this.transformer.destroy();\n this.trackGenerator?.stop();\n }\n}\n","import * as vision from '@mediapipe/tasks-vision';\nimport { dependencies } from '../../package.json';\nimport VideoTransformer from './VideoTransformer';\nimport { VideoTransformerInitOptions } from './types';\n\nexport type SegmenterOptions = Partial<vision.ImageSegmenterOptions['baseOptions']>;\n\nexport type BackgroundOptions = {\n blurRadius?: number;\n imagePath?: string;\n /** cannot be updated through the `update` method, needs a restart */\n segmenterOptions?: SegmenterOptions;\n /** cannot be updated through the `update` method, needs a restart */\n assetPaths?: { tasksVisionFileSet?: string; modelAssetPath?: string };\n};\n\nexport default class BackgroundProcessor extends VideoTransformer<BackgroundOptions> {\n static get isSupported() {\n return typeof OffscreenCanvas !== 'undefined';\n }\n\n imageSegmenter?: vision.ImageSegmenter;\n\n segmentationResults: vision.ImageSegmenterResult | undefined;\n\n backgroundImage: ImageBitmap | null = null;\n\n blurRadius?: number;\n\n options: BackgroundOptions;\n\n constructor(opts: BackgroundOptions) {\n super();\n this.options = opts;\n this.update(opts);\n }\n\n async init({ outputCanvas, inputElement: inputVideo }: VideoTransformerInitOptions) {\n await super.init({ outputCanvas, inputElement: inputVideo });\n\n const fileSet = await vision.FilesetResolver.forVisionTasks(\n this.options.assetPaths?.tasksVisionFileSet ??\n `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${dependencies['@mediapipe/tasks-vision']}/wasm`,\n );\n\n this.imageSegmenter = await vision.ImageSegmenter.createFromOptions(fileSet, {\n baseOptions: {\n modelAssetPath:\n this.options.assetPaths?.modelAssetPath ??\n 'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite',\n delegate: 'GPU',\n ...this.options.segmenterOptions,\n },\n runningMode: 'VIDEO',\n outputCategoryMask: true,\n outputConfidenceMasks: false,\n });\n\n // Skip loading the image here if update already loaded the image below\n if (this.options?.imagePath && !this.backgroundImage) {\n await this.loadBackground(this.options.imagePath).catch((err) =>\n console.error('Error while loading processor background image: ', err),\n );\n }\n }\n\n async destroy() {\n await super.destroy();\n await this.imageSegmenter?.close();\n this.backgroundImage = null;\n }\n\n async loadBackground(path: string) {\n const img = new Image();\n\n await new Promise((resolve, reject) => {\n img.crossOrigin = 'Anonymous';\n img.onload = () => resolve(img);\n img.onerror = (err) => reject(err);\n img.src = path;\n });\n const imageData = await createImageBitmap(img);\n this.backgroundImage = imageData;\n }\n\n async transform(frame: VideoFrame, controller: TransformStreamDefaultController<VideoFrame>) {\n try {\n if (!(frame instanceof VideoFrame)) {\n console.debug('empty frame detected, ignoring');\n return;\n }\n if (this.isDisabled) {\n controller.enqueue(frame);\n return;\n }\n if (!this.canvas) {\n throw TypeError('Canvas needs to be initialized first');\n }\n let startTimeMs = performance.now();\n this.imageSegmenter?.segmentForVideo(\n this.inputVideo!,\n startTimeMs,\n (result) => (this.segmentationResults = result),\n );\n\n if (this.blurRadius) {\n await this.blurBackground(frame);\n } else {\n await this.drawVirtualBackground(frame);\n }\n const newFrame = new VideoFrame(this.canvas, {\n timestamp: frame.timestamp || Date.now(),\n });\n controller.enqueue(newFrame);\n } finally {\n frame?.close();\n }\n }\n\n async update(opts: BackgroundOptions) {\n this.options = opts;\n if (opts.blurRadius) {\n this.blurRadius = opts.blurRadius;\n } else if (opts.imagePath) {\n await this.loadBackground(opts.imagePath);\n }\n }\n\n async drawVirtualBackground(frame: VideoFrame) {\n if (!this.canvas || !this.ctx || !this.segmentationResults || !this.inputVideo) return;\n // this.ctx.save();\n // this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n if (this.segmentationResults?.categoryMask && this.segmentationResults.categoryMask.width > 0) {\n this.ctx.globalCompositeOperation = 'copy';\n const bitmap = await maskToBitmap(\n this.segmentationResults.categoryMask,\n this.segmentationResults.categoryMask.width,\n this.segmentationResults.categoryMask.height,\n );\n this.ctx.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.filter = 'none';\n this.ctx.globalCompositeOperation = 'source-in';\n if (this.backgroundImage) {\n this.ctx.drawImage(\n this.backgroundImage,\n 0,\n 0,\n this.backgroundImage.width,\n this.backgroundImage.height,\n 0,\n 0,\n this.canvas.width,\n this.canvas.height,\n );\n } else {\n this.ctx.fillStyle = '#00FF00';\n this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);\n }\n\n this.ctx.globalCompositeOperation = 'destination-over';\n }\n this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);\n // this.ctx.restore();\n }\n\n async blurBackground(frame: VideoFrame) {\n if (\n !this.ctx ||\n !this.canvas ||\n !this.segmentationResults?.categoryMask?.canvas ||\n !this.inputVideo\n ) {\n return;\n }\n\n this.ctx.save();\n this.ctx.globalCompositeOperation = 'copy';\n\n if (this.segmentationResults?.categoryMask && this.segmentationResults.categoryMask.width > 0) {\n const bitmap = await maskToBitmap(\n this.segmentationResults.categoryMask,\n this.segmentationResults.categoryMask.width,\n this.segmentationResults.categoryMask.height,\n );\n\n this.ctx.globalCompositeOperation = 'copy';\n this.ctx.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.filter = 'none';\n this.ctx.globalCompositeOperation = 'source-out';\n this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.globalCompositeOperation = 'destination-over';\n this.ctx.filter = `blur(${this.blurRadius}px)`;\n this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.restore();\n }\n }\n}\n\nfunction maskToBitmap(\n mask: vision.MPMask,\n videoWidth: number,\n videoHeight: number,\n): Promise<ImageBitmap> {\n const dataArray: Uint8ClampedArray = new Uint8ClampedArray(videoWidth * videoHeight * 4);\n const result = mask.getAsUint8Array();\n for (let i = 0; i < result.length; i += 1) {\n dataArray[i * 4] = result[i];\n dataArray[i * 4 + 1] = result[i];\n dataArray[i * 4 + 2] = result[i];\n dataArray[i * 4 + 3] = result[i];\n }\n const dataNew = new ImageData(dataArray, videoWidth, videoHeight);\n\n return createImageBitmap(dataNew);\n}\n","{\n \"name\": \"@livekit/track-processors\",\n \"version\": \"0.4.0\",\n \"description\": \"LiveKit track processors\",\n \"main\": \"dist/index.js\",\n \"module\": \"dist/index.mjs\",\n \"source\": \"src/index.ts\",\n \"types\": \"dist/src/index.d.ts\",\n \"repository\": \"git@github.com:livekit/livekit-track-processors.git\",\n \"author\": \"Lukas Seiler\",\n \"license\": \"Apache-2.0\",\n \"scripts\": {\n \"build\": \"tsup --onSuccess \\\"tsc --declaration --emitDeclarationOnly\\\"\",\n \"build-sample\": \"cd example && vite build\",\n \"lint\": \"eslint src\",\n \"release\": \"pnpm build && changeset publish\",\n \"test\": \"jest\",\n \"sample\": \"vite serve example --port 8080 --open\"\n },\n \"files\": [\n \"dist\",\n \"src\"\n ],\n \"dependencies\": {\n \"@mediapipe/tasks-vision\": \"0.10.21\"\n },\n \"peerDependencies\": {\n \"livekit-client\": \"^1.12.0 || ^2.1.0\"\n },\n \"devDependencies\": {\n \"@changesets/cli\": \"^2.26.2\",\n \"@livekit/changesets-changelog-github\": \"^0.0.4\",\n \"@trivago/prettier-plugin-sort-imports\": \"^4.1.1\",\n \"@types/dom-mediacapture-transform\": \"^0.1.6\",\n \"@types/offscreencanvas\": \"^2019.7.0\",\n \"@typescript-eslint/eslint-plugin\": \"^5.62.0\",\n \"eslint\": \"8.39.0\",\n \"eslint-config-airbnb-typescript\": \"17.0.0\",\n \"eslint-config-prettier\": \"8.8.0\",\n \"eslint-plugin-ecmascript-compat\": \"^3.0.0\",\n \"eslint-plugin-import\": \"2.27.5\",\n \"prettier\": \"^2.8.8\",\n \"tsup\": \"^7.1.0\",\n \"typescript\": \"^5.0.4\",\n \"vite\": \"^4.3.8\"\n },\n \"packageManager\": \"pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531\"\n}\n","import { VideoTrackTransformer, VideoTransformerInitOptions } from './types';\n\nexport default abstract class VideoTransformer<Options extends Record<string, unknown>>\n implements VideoTrackTransformer<Options>\n{\n transformer?: TransformStream;\n\n canvas?: OffscreenCanvas;\n\n ctx?: OffscreenCanvasRenderingContext2D;\n\n inputVideo?: HTMLVideoElement;\n\n protected isDisabled?: Boolean = false;\n\n async init({\n outputCanvas,\n inputElement: inputVideo,\n }: VideoTransformerInitOptions): Promise<void> {\n if (!(inputVideo instanceof HTMLVideoElement)) {\n throw TypeError('Video transformer needs a HTMLVideoElement as input');\n }\n\n this.transformer = new TransformStream({\n transform: (frame, controller) => this.transform(frame, controller),\n });\n this.canvas = outputCanvas || null;\n if (outputCanvas) {\n this.ctx = this.canvas?.getContext('2d') || undefined;\n }\n this.inputVideo = inputVideo;\n this.isDisabled = false;\n }\n\n async restart({ outputCanvas, inputElement: inputVideo }: VideoTransformerInitOptions) {\n this.canvas = outputCanvas || null;\n this.ctx = this.canvas.getContext('2d') || undefined;\n\n this.inputVideo = inputVideo;\n this.isDisabled = false;\n }\n\n async destroy() {\n this.isDisabled = true;\n this.canvas = undefined;\n this.ctx = undefined;\n }\n\n abstract transform(\n frame: VideoFrame,\n controller: TransformStreamDefaultController<VideoFrame>,\n ): void;\n\n abstract update(options: Options): void;\n}\n","import ProcessorWrapper from './ProcessorWrapper';\nimport BackgroundTransformer, {\n BackgroundOptions,\n SegmenterOptions,\n} from './transformers/BackgroundTransformer';\n\nexport * from './transformers/types';\nexport { default as VideoTransformer } from './transformers/VideoTransformer';\nexport { ProcessorWrapper, type BackgroundOptions, type SegmenterOptions, BackgroundTransformer };\n\nexport const BackgroundBlur = (blurRadius: number = 10, segmenterOptions?: SegmenterOptions) => {\n return BackgroundProcessor({ blurRadius, segmenterOptions }, 'background-blur');\n};\n\nexport const VirtualBackground = (imagePath: string, segmenterOptions?: SegmenterOptions) => {\n return BackgroundProcessor({ imagePath, segmenterOptions }, 'virtual-background');\n};\n\nexport const BackgroundProcessor = (options: BackgroundOptions, name = 'background-processor') => {\n const isProcessorSupported = ProcessorWrapper.isSupported && BackgroundTransformer.isSupported;\n if (!isProcessorSupported) {\n throw new Error('processor is not supported in this browser');\n }\n const processor = new ProcessorWrapper(new BackgroundTransformer(options), name);\n return processor;\n};\n"]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
for (var prop of __getOwnPropSymbols(b)) {
|
|
14
|
-
if (__propIsEnum.call(b, prop))
|
|
15
|
-
__defNormalProp(a, prop, b[prop]);
|
|
1
|
+
// src/utils.ts
|
|
2
|
+
async function sleep(time) {
|
|
3
|
+
return new Promise((resolve) => setTimeout(resolve, time));
|
|
4
|
+
}
|
|
5
|
+
async function waitForTrackResolution(track) {
|
|
6
|
+
const timeout = 500;
|
|
7
|
+
await sleep(10);
|
|
8
|
+
const started = Date.now();
|
|
9
|
+
while (Date.now() - started < timeout) {
|
|
10
|
+
const { width, height } = track.getSettings();
|
|
11
|
+
if (width && height) {
|
|
12
|
+
return { width, height };
|
|
16
13
|
}
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
14
|
+
await sleep(50);
|
|
15
|
+
}
|
|
16
|
+
return { width: void 0, height: void 0 };
|
|
17
|
+
}
|
|
20
18
|
|
|
21
19
|
// src/ProcessorWrapper.ts
|
|
22
20
|
var ProcessorWrapper = class {
|
|
@@ -26,50 +24,37 @@ var ProcessorWrapper = class {
|
|
|
26
24
|
constructor(transformer, name) {
|
|
27
25
|
this.name = name;
|
|
28
26
|
this.transformer = transformer;
|
|
29
|
-
this.transformer.restart;
|
|
30
27
|
}
|
|
31
28
|
async setup(opts) {
|
|
32
|
-
var _a, _b, _c, _d;
|
|
33
29
|
this.source = opts.track;
|
|
34
|
-
const
|
|
35
|
-
await this.source.applyConstraints(__spreadProps(__spreadValues({}, origConstraints), {
|
|
36
|
-
// @ts-expect-error when a mediastream track is resized and/or cropped, the `VideoFrame` will have a coded height/width of the original video size
|
|
37
|
-
// this leads to a shift of the underlying video as the frame itself is being rendered with the coded size
|
|
38
|
-
// but image segmentation is based on the display dimensions (-> the cropped version)
|
|
39
|
-
// in order to prevent this, we force the resize mode to "none"
|
|
40
|
-
resizeMode: "none"
|
|
41
|
-
}));
|
|
42
|
-
this.sourceSettings = this.source.getSettings();
|
|
30
|
+
const { width, height } = await waitForTrackResolution(this.source);
|
|
43
31
|
this.sourceDummy = opts.element;
|
|
44
|
-
if (this.sourceDummy instanceof HTMLVideoElement) {
|
|
45
|
-
this.sourceDummy.height = (_a = this.sourceSettings.height) != null ? _a : 300;
|
|
46
|
-
this.sourceDummy.width = (_b = this.sourceSettings.width) != null ? _b : 300;
|
|
47
|
-
}
|
|
48
32
|
if (!(this.sourceDummy instanceof HTMLVideoElement)) {
|
|
49
33
|
throw TypeError("Currently only video transformers are supported");
|
|
50
34
|
}
|
|
35
|
+
if (this.sourceDummy instanceof HTMLVideoElement) {
|
|
36
|
+
this.sourceDummy.height = height != null ? height : 300;
|
|
37
|
+
this.sourceDummy.width = width != null ? width : 300;
|
|
38
|
+
}
|
|
51
39
|
this.processor = new MediaStreamTrackProcessor({ track: this.source });
|
|
52
40
|
this.trackGenerator = new MediaStreamTrackGenerator({
|
|
53
41
|
kind: "video",
|
|
54
42
|
signalTarget: this.source
|
|
55
43
|
});
|
|
56
|
-
this.canvas = new OffscreenCanvas(
|
|
57
|
-
(_c = this.sourceSettings.width) != null ? _c : 300,
|
|
58
|
-
(_d = this.sourceSettings.height) != null ? _d : 300
|
|
59
|
-
);
|
|
44
|
+
this.canvas = new OffscreenCanvas(width != null ? width : 300, height != null ? height : 300);
|
|
60
45
|
}
|
|
61
46
|
async init(opts) {
|
|
62
47
|
await this.setup(opts);
|
|
63
48
|
if (!this.canvas || !this.processor || !this.trackGenerator) {
|
|
64
49
|
throw new TypeError("Expected both canvas and processor to be defined after setup");
|
|
65
50
|
}
|
|
66
|
-
|
|
51
|
+
const readableStream = this.processor.readable;
|
|
67
52
|
await this.transformer.init({
|
|
68
53
|
outputCanvas: this.canvas,
|
|
69
54
|
inputElement: this.sourceDummy
|
|
70
55
|
});
|
|
71
|
-
|
|
72
|
-
|
|
56
|
+
const pipedStream = readableStream.pipeThrough(this.transformer.transformer);
|
|
57
|
+
pipedStream.pipeTo(this.trackGenerator.writable).catch((e) => console.error("error when trying to pipe", e)).finally(() => this.destroy());
|
|
73
58
|
this.processedTrack = this.trackGenerator;
|
|
74
59
|
}
|
|
75
60
|
async restart(opts) {
|
|
@@ -94,8 +79,7 @@ import * as vision from "@mediapipe/tasks-vision";
|
|
|
94
79
|
|
|
95
80
|
// package.json
|
|
96
81
|
var dependencies = {
|
|
97
|
-
"@mediapipe/
|
|
98
|
-
"@mediapipe/tasks-vision": "0.10.9"
|
|
82
|
+
"@mediapipe/tasks-vision": "0.10.21"
|
|
99
83
|
};
|
|
100
84
|
|
|
101
85
|
// src/transformers/VideoTransformer.ts
|
|
@@ -152,16 +136,19 @@ var BackgroundProcessor = class extends VideoTransformer {
|
|
|
152
136
|
(_b = (_a = this.options.assetPaths) == null ? void 0 : _a.tasksVisionFileSet) != null ? _b : `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${dependencies["@mediapipe/tasks-vision"]}/wasm`
|
|
153
137
|
);
|
|
154
138
|
this.imageSegmenter = await vision.ImageSegmenter.createFromOptions(fileSet, {
|
|
155
|
-
baseOptions:
|
|
139
|
+
baseOptions: {
|
|
156
140
|
modelAssetPath: (_d = (_c = this.options.assetPaths) == null ? void 0 : _c.modelAssetPath) != null ? _d : "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite",
|
|
157
|
-
delegate: "GPU"
|
|
158
|
-
|
|
141
|
+
delegate: "GPU",
|
|
142
|
+
...this.options.segmenterOptions
|
|
143
|
+
},
|
|
159
144
|
runningMode: "VIDEO",
|
|
160
145
|
outputCategoryMask: true,
|
|
161
146
|
outputConfidenceMasks: false
|
|
162
147
|
});
|
|
163
148
|
if (((_e = this.options) == null ? void 0 : _e.imagePath) && !this.backgroundImage) {
|
|
164
|
-
await this.loadBackground(this.options.imagePath).catch(
|
|
149
|
+
await this.loadBackground(this.options.imagePath).catch(
|
|
150
|
+
(err) => console.error("Error while loading processor background image: ", err)
|
|
151
|
+
);
|
|
165
152
|
}
|
|
166
153
|
}
|
|
167
154
|
async destroy() {
|
|
@@ -184,6 +171,10 @@ var BackgroundProcessor = class extends VideoTransformer {
|
|
|
184
171
|
async transform(frame, controller) {
|
|
185
172
|
var _a;
|
|
186
173
|
try {
|
|
174
|
+
if (!(frame instanceof VideoFrame)) {
|
|
175
|
+
console.debug("empty frame detected, ignoring");
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
187
178
|
if (this.isDisabled) {
|
|
188
179
|
controller.enqueue(frame);
|
|
189
180
|
return;
|
|
@@ -207,7 +198,7 @@ var BackgroundProcessor = class extends VideoTransformer {
|
|
|
207
198
|
});
|
|
208
199
|
controller.enqueue(newFrame);
|
|
209
200
|
} finally {
|
|
210
|
-
frame.close();
|
|
201
|
+
frame == null ? void 0 : frame.close();
|
|
211
202
|
}
|
|
212
203
|
}
|
|
213
204
|
async update(opts) {
|
|
@@ -222,8 +213,7 @@ var BackgroundProcessor = class extends VideoTransformer {
|
|
|
222
213
|
var _a;
|
|
223
214
|
if (!this.canvas || !this.ctx || !this.segmentationResults || !this.inputVideo)
|
|
224
215
|
return;
|
|
225
|
-
if ((_a = this.segmentationResults) == null ? void 0 : _a.categoryMask) {
|
|
226
|
-
this.ctx.filter = "blur(10px)";
|
|
216
|
+
if (((_a = this.segmentationResults) == null ? void 0 : _a.categoryMask) && this.segmentationResults.categoryMask.width > 0) {
|
|
227
217
|
this.ctx.globalCompositeOperation = "copy";
|
|
228
218
|
const bitmap = await maskToBitmap(
|
|
229
219
|
this.segmentationResults.categoryMask,
|
|
@@ -254,27 +244,28 @@ var BackgroundProcessor = class extends VideoTransformer {
|
|
|
254
244
|
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
255
245
|
}
|
|
256
246
|
async blurBackground(frame) {
|
|
257
|
-
var _a, _b;
|
|
247
|
+
var _a, _b, _c;
|
|
258
248
|
if (!this.ctx || !this.canvas || !((_b = (_a = this.segmentationResults) == null ? void 0 : _a.categoryMask) == null ? void 0 : _b.canvas) || !this.inputVideo) {
|
|
259
249
|
return;
|
|
260
250
|
}
|
|
261
251
|
this.ctx.save();
|
|
262
252
|
this.ctx.globalCompositeOperation = "copy";
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
253
|
+
if (((_c = this.segmentationResults) == null ? void 0 : _c.categoryMask) && this.segmentationResults.categoryMask.width > 0) {
|
|
254
|
+
const bitmap = await maskToBitmap(
|
|
255
|
+
this.segmentationResults.categoryMask,
|
|
256
|
+
this.segmentationResults.categoryMask.width,
|
|
257
|
+
this.segmentationResults.categoryMask.height
|
|
258
|
+
);
|
|
259
|
+
this.ctx.globalCompositeOperation = "copy";
|
|
260
|
+
this.ctx.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);
|
|
261
|
+
this.ctx.filter = "none";
|
|
262
|
+
this.ctx.globalCompositeOperation = "source-out";
|
|
263
|
+
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
264
|
+
this.ctx.globalCompositeOperation = "destination-over";
|
|
265
|
+
this.ctx.filter = `blur(${this.blurRadius}px)`;
|
|
266
|
+
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
267
|
+
this.ctx.restore();
|
|
268
|
+
}
|
|
278
269
|
}
|
|
279
270
|
};
|
|
280
271
|
function maskToBitmap(mask, videoWidth, videoHeight) {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/ProcessorWrapper.ts","../src/transformers/BackgroundTransformer.ts","../package.json","../src/transformers/VideoTransformer.ts","../src/index.ts"],"sourcesContent":["import type { ProcessorOptions, Track, TrackProcessor } from 'livekit-client';\nimport { TrackTransformer } from './transformers';\n\nexport default class ProcessorWrapper<TransformerOptions extends Record<string, unknown>>\n implements TrackProcessor<Track.Kind>\n{\n static get isSupported() {\n return (\n typeof MediaStreamTrackGenerator !== 'undefined' &&\n typeof MediaStreamTrackProcessor !== 'undefined'\n );\n }\n\n name: string;\n\n source?: MediaStreamVideoTrack;\n\n sourceSettings?: MediaTrackSettings;\n\n processor?: MediaStreamTrackProcessor<VideoFrame>;\n\n trackGenerator?: MediaStreamTrackGenerator<VideoFrame>;\n\n canvas?: OffscreenCanvas;\n\n sourceDummy?: HTMLMediaElement;\n\n processedTrack?: MediaStreamTrack;\n\n transformer: TrackTransformer<TransformerOptions>;\n\n constructor(transformer: TrackTransformer<TransformerOptions>, name: string) {\n this.name = name;\n this.transformer = transformer;\n this.transformer.restart;\n }\n\n private async setup(opts: ProcessorOptions<Track.Kind>) {\n this.source = opts.track as MediaStreamVideoTrack;\n const origConstraints = this.source.getConstraints();\n await this.source.applyConstraints({\n ...origConstraints,\n // @ts-expect-error when a mediastream track is resized and/or cropped, the `VideoFrame` will have a coded height/width of the original video size\n // this leads to a shift of the underlying video as the frame itself is being rendered with the coded size\n // but image segmentation is based on the display dimensions (-> the cropped version)\n // in order to prevent this, we force the resize mode to \"none\"\n resizeMode: 'none',\n });\n this.sourceSettings = this.source.getSettings();\n this.sourceDummy = opts.element;\n if (this.sourceDummy instanceof HTMLVideoElement) {\n this.sourceDummy.height = this.sourceSettings.height ?? 300;\n this.sourceDummy.width = this.sourceSettings.width ?? 300;\n }\n if (!(this.sourceDummy instanceof HTMLVideoElement)) {\n throw TypeError('Currently only video transformers are supported');\n }\n // TODO explore if we can do all the processing work in a webworker\n this.processor = new MediaStreamTrackProcessor({ track: this.source });\n\n this.trackGenerator = new MediaStreamTrackGenerator({\n kind: 'video',\n signalTarget: this.source,\n });\n\n this.canvas = new OffscreenCanvas(\n this.sourceSettings.width ?? 300,\n this.sourceSettings.height ?? 300,\n );\n }\n\n async init(opts: ProcessorOptions<Track.Kind>) {\n await this.setup(opts);\n if (!this.canvas || !this.processor || !this.trackGenerator) {\n throw new TypeError('Expected both canvas and processor to be defined after setup');\n }\n\n let readableStream = this.processor.readable;\n\n await this.transformer.init({\n outputCanvas: this.canvas,\n inputElement: this.sourceDummy as HTMLVideoElement,\n });\n readableStream = readableStream.pipeThrough(this.transformer!.transformer!);\n\n readableStream\n .pipeTo(this.trackGenerator.writable)\n .catch((e) => console.error('error when trying to pipe', e))\n .finally(() => this.destroy());\n this.processedTrack = this.trackGenerator as MediaStreamVideoTrack;\n }\n\n async restart(opts: ProcessorOptions<Track.Kind>) {\n await this.destroy();\n return this.init(opts);\n }\n\n async restartTransformer(...options: Parameters<(typeof this.transformer)['restart']>) {\n // @ts-ignore unclear why the restart method only accepts VideoTransformerInitOptions instead of either those or AudioTransformerInitOptions\n this.transformer.restart(options[0]);\n }\n\n async updateTransformerOptions(...options: Parameters<(typeof this.transformer)['update']>) {\n this.transformer.update(options[0]);\n }\n\n async destroy() {\n await this.transformer.destroy();\n this.trackGenerator?.stop();\n }\n}\n","import * as vision from '@mediapipe/tasks-vision';\nimport { dependencies } from '../../package.json';\nimport VideoTransformer from './VideoTransformer';\nimport { VideoTransformerInitOptions } from './types';\n\nexport type SegmenterOptions = Partial<vision.ImageSegmenterOptions['baseOptions']>;\n\nexport type BackgroundOptions = {\n blurRadius?: number;\n imagePath?: string;\n /** cannot be updated through the `update` method, needs a restart */\n segmenterOptions?: SegmenterOptions;\n /** cannot be updated through the `update` method, needs a restart */\n assetPaths?: { tasksVisionFileSet?: string; modelAssetPath?: string };\n};\n\nexport default class BackgroundProcessor extends VideoTransformer<BackgroundOptions> {\n static get isSupported() {\n return typeof OffscreenCanvas !== 'undefined';\n }\n\n imageSegmenter?: vision.ImageSegmenter;\n\n segmentationResults: vision.ImageSegmenterResult | undefined;\n\n backgroundImage: ImageBitmap | null = null;\n\n blurRadius?: number;\n\n options: BackgroundOptions;\n\n constructor(opts: BackgroundOptions) {\n super();\n this.options = opts;\n this.update(opts);\n }\n\n async init({ outputCanvas, inputElement: inputVideo }: VideoTransformerInitOptions) {\n await super.init({ outputCanvas, inputElement: inputVideo });\n\n const fileSet = await vision.FilesetResolver.forVisionTasks(\n this.options.assetPaths?.tasksVisionFileSet ??\n `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${dependencies['@mediapipe/tasks-vision']}/wasm`,\n );\n\n this.imageSegmenter = await vision.ImageSegmenter.createFromOptions(fileSet, {\n baseOptions: {\n modelAssetPath:\n this.options.assetPaths?.modelAssetPath ??\n 'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite',\n delegate: 'GPU',\n ...this.options.segmenterOptions,\n },\n runningMode: 'VIDEO',\n outputCategoryMask: true,\n outputConfidenceMasks: false,\n });\n\n // Skip loading the image here if update already loaded the image below\n if (this.options?.imagePath && !this.backgroundImage) {\n await this.loadBackground(this.options.imagePath).catch((err) => console.error(\"Error while loading processor background image: \", err));\n }\n }\n\n async destroy() {\n await super.destroy();\n await this.imageSegmenter?.close();\n this.backgroundImage = null;\n }\n\n async loadBackground(path: string) {\n const img = new Image();\n\n await new Promise((resolve, reject) => {\n img.crossOrigin = 'Anonymous';\n img.onload = () => resolve(img);\n img.onerror = (err) => reject(err);\n img.src = path;\n });\n const imageData = await createImageBitmap(img);\n this.backgroundImage = imageData;\n }\n\n async transform(frame: VideoFrame, controller: TransformStreamDefaultController<VideoFrame>) {\n try {\n if (this.isDisabled) {\n controller.enqueue(frame);\n return;\n }\n if (!this.canvas) {\n throw TypeError('Canvas needs to be initialized first');\n }\n let startTimeMs = performance.now();\n this.imageSegmenter?.segmentForVideo(\n this.inputVideo!,\n startTimeMs,\n (result) => (this.segmentationResults = result),\n );\n\n if (this.blurRadius) {\n await this.blurBackground(frame);\n } else {\n await this.drawVirtualBackground(frame);\n }\n const newFrame = new VideoFrame(this.canvas, {\n timestamp: frame.timestamp || Date.now(),\n });\n controller.enqueue(newFrame);\n } finally {\n frame.close();\n }\n }\n\n async update(opts: BackgroundOptions) {\n this.options = opts;\n if (opts.blurRadius) {\n this.blurRadius = opts.blurRadius;\n } else if (opts.imagePath) {\n await this.loadBackground(opts.imagePath);\n }\n }\n\n async drawVirtualBackground(frame: VideoFrame) {\n if (!this.canvas || !this.ctx || !this.segmentationResults || !this.inputVideo) return;\n // this.ctx.save();\n // this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n if (this.segmentationResults?.categoryMask) {\n this.ctx.filter = 'blur(10px)';\n this.ctx.globalCompositeOperation = 'copy';\n const bitmap = await maskToBitmap(\n this.segmentationResults.categoryMask,\n this.segmentationResults.categoryMask.width,\n this.segmentationResults.categoryMask.height,\n );\n this.ctx.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.filter = 'none';\n this.ctx.globalCompositeOperation = 'source-in';\n if (this.backgroundImage) {\n this.ctx.drawImage(\n this.backgroundImage,\n 0,\n 0,\n this.backgroundImage.width,\n this.backgroundImage.height,\n 0,\n 0,\n this.canvas.width,\n this.canvas.height,\n );\n } else {\n this.ctx.fillStyle = '#00FF00';\n this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);\n }\n\n this.ctx.globalCompositeOperation = 'destination-over';\n }\n this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);\n // this.ctx.restore();\n }\n\n async blurBackground(frame: VideoFrame) {\n if (\n !this.ctx ||\n !this.canvas ||\n !this.segmentationResults?.categoryMask?.canvas ||\n !this.inputVideo\n ) {\n return;\n }\n\n this.ctx.save();\n this.ctx.globalCompositeOperation = 'copy';\n\n const bitmap = await maskToBitmap(\n this.segmentationResults.categoryMask,\n this.segmentationResults.categoryMask.width,\n this.segmentationResults.categoryMask.height,\n );\n\n this.ctx.filter = 'blur(3px)';\n this.ctx.globalCompositeOperation = 'copy';\n this.ctx.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.filter = 'none';\n this.ctx.globalCompositeOperation = 'source-out';\n this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.globalCompositeOperation = 'destination-over';\n this.ctx.filter = `blur(${this.blurRadius}px)`;\n this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.restore();\n }\n}\n\nfunction maskToBitmap(\n mask: vision.MPMask,\n videoWidth: number,\n videoHeight: number,\n): Promise<ImageBitmap> {\n const dataArray: Uint8ClampedArray = new Uint8ClampedArray(videoWidth * videoHeight * 4);\n const result = mask.getAsUint8Array();\n for (let i = 0; i < result.length; i += 1) {\n dataArray[i * 4] = result[i];\n dataArray[i * 4 + 1] = result[i];\n dataArray[i * 4 + 2] = result[i];\n dataArray[i * 4 + 3] = result[i];\n }\n const dataNew = new ImageData(dataArray, videoWidth, videoHeight);\n\n return createImageBitmap(dataNew);\n}\n","{\n \"name\": \"@livekit/track-processors\",\n \"version\": \"0.3.2\",\n \"description\": \"LiveKit track processors\",\n \"main\": \"dist/index.js\",\n \"module\": \"dist/index.mjs\",\n \"source\": \"src/index.ts\",\n \"types\": \"dist/src/index.d.ts\",\n \"repository\": \"git@github.com:livekit/livekit-track-processors.git\",\n \"author\": \"Lukas Seiler\",\n \"license\": \"Apache-2.0\",\n \"scripts\": {\n \"build\": \"tsup --onSuccess \\\"tsc --declaration --emitDeclarationOnly\\\"\",\n \"build-sample\": \"cd example && vite build\",\n \"lint\": \"eslint src\",\n \"release\": \"pnpm build && changeset publish\",\n \"test\": \"jest\",\n \"sample\": \"vite serve example --port 8080 --open\"\n },\n \"files\": [\n \"dist\",\n \"src\"\n ],\n \"dependencies\": {\n \"@mediapipe/holistic\": \"0.5.1675471629\",\n \"@mediapipe/tasks-vision\": \"0.10.9\"\n },\n \"peerDependencies\": {\n \"livekit-client\": \"^1.12.0 || ^2.1.0\"\n },\n \"devDependencies\": {\n \"@changesets/cli\": \"^2.26.2\",\n \"@livekit/changesets-changelog-github\": \"^0.0.4\",\n \"@trivago/prettier-plugin-sort-imports\": \"^4.1.1\",\n \"@types/dom-mediacapture-transform\": \"^0.1.6\",\n \"@types/offscreencanvas\": \"^2019.7.0\",\n \"@typescript-eslint/eslint-plugin\": \"^5.62.0\",\n \"eslint\": \"8.39.0\",\n \"eslint-config-airbnb-typescript\": \"17.0.0\",\n \"eslint-config-prettier\": \"8.8.0\",\n \"eslint-plugin-ecmascript-compat\": \"^3.0.0\",\n \"eslint-plugin-import\": \"2.27.5\",\n \"prettier\": \"^2.8.8\",\n \"tsup\": \"^7.1.0\",\n \"typescript\": \"^5.0.4\",\n \"vite\": \"^4.3.8\"\n }\n}\n","import { VideoTrackTransformer, VideoTransformerInitOptions } from './types';\n\nexport default abstract class VideoTransformer<Options extends Record<string, unknown>>\n implements VideoTrackTransformer<Options>\n{\n transformer?: TransformStream;\n\n canvas?: OffscreenCanvas;\n\n ctx?: OffscreenCanvasRenderingContext2D;\n\n inputVideo?: HTMLVideoElement;\n\n protected isDisabled?: Boolean = false;\n\n async init({\n outputCanvas,\n inputElement: inputVideo,\n }: VideoTransformerInitOptions): Promise<void> {\n if (!(inputVideo instanceof HTMLVideoElement)) {\n throw TypeError('Video transformer needs a HTMLVideoElement as input');\n }\n this.transformer = new TransformStream({\n transform: (frame, controller) => this.transform(frame, controller),\n });\n this.canvas = outputCanvas || null;\n if (outputCanvas) {\n this.ctx = this.canvas?.getContext('2d') || undefined;\n }\n this.inputVideo = inputVideo;\n this.isDisabled = false;\n }\n\n async restart({ outputCanvas, inputElement: inputVideo }: VideoTransformerInitOptions) {\n this.canvas = outputCanvas || null;\n this.ctx = this.canvas.getContext('2d') || undefined;\n\n this.inputVideo = inputVideo;\n this.isDisabled = false;\n }\n\n async destroy() {\n this.isDisabled = true;\n this.canvas = undefined;\n this.ctx = undefined;\n }\n\n abstract transform(\n frame: VideoFrame,\n controller: TransformStreamDefaultController<VideoFrame>,\n ): void;\n\n abstract update(options: Options): void;\n}\n","import ProcessorWrapper from './ProcessorWrapper';\nimport BackgroundTransformer, {\n BackgroundOptions,\n SegmenterOptions,\n} from './transformers/BackgroundTransformer';\n\nexport * from './transformers/types';\nexport { default as VideoTransformer } from './transformers/VideoTransformer';\nexport { ProcessorWrapper, type BackgroundOptions, type SegmenterOptions, BackgroundTransformer };\n\nexport const BackgroundBlur = (blurRadius: number = 10, segmenterOptions?: SegmenterOptions) => {\n return BackgroundProcessor({ blurRadius, segmenterOptions }, 'background-blur');\n};\n\nexport const VirtualBackground = (imagePath: string, segmenterOptions?: SegmenterOptions) => {\n return BackgroundProcessor({ imagePath, segmenterOptions }, 'virtual-background');\n};\n\nexport const BackgroundProcessor = (options: BackgroundOptions, name = 'background-processor') => {\n const isProcessorSupported = ProcessorWrapper.isSupported && BackgroundTransformer.isSupported;\n if (!isProcessorSupported) {\n throw new Error('processor is not supported in this browser');\n }\n const processor = new ProcessorWrapper(new BackgroundTransformer(options), name);\n return processor;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAGA,IAAqB,mBAArB,MAEA;AAAA,EACE,WAAW,cAAc;AACvB,WACE,OAAO,8BAA8B,eACrC,OAAO,8BAA8B;AAAA,EAEzC;AAAA,EAoBA,YAAY,aAAmD,MAAc;AAC3E,SAAK,OAAO;AACZ,SAAK,cAAc;AACnB,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,MAAc,MAAM,MAAoC;AArC1D;AAsCI,SAAK,SAAS,KAAK;AACnB,UAAM,kBAAkB,KAAK,OAAO,eAAe;AACnD,UAAM,KAAK,OAAO,iBAAiB,iCAC9B,kBAD8B;AAAA;AAAA;AAAA;AAAA;AAAA,MAMjC,YAAY;AAAA,IACd,EAAC;AACD,SAAK,iBAAiB,KAAK,OAAO,YAAY;AAC9C,SAAK,cAAc,KAAK;AACxB,QAAI,KAAK,uBAAuB,kBAAkB;AAChD,WAAK,YAAY,UAAS,UAAK,eAAe,WAApB,YAA8B;AACxD,WAAK,YAAY,SAAQ,UAAK,eAAe,UAApB,YAA6B;AAAA,IACxD;AACA,QAAI,EAAE,KAAK,uBAAuB,mBAAmB;AACnD,YAAM,UAAU,iDAAiD;AAAA,IACnE;AAEA,SAAK,YAAY,IAAI,0BAA0B,EAAE,OAAO,KAAK,OAAO,CAAC;AAErE,SAAK,iBAAiB,IAAI,0BAA0B;AAAA,MAClD,MAAM;AAAA,MACN,cAAc,KAAK;AAAA,IACrB,CAAC;AAED,SAAK,SAAS,IAAI;AAAA,OAChB,UAAK,eAAe,UAApB,YAA6B;AAAA,OAC7B,UAAK,eAAe,WAApB,YAA8B;AAAA,IAChC;AAAA,EACF;AAAA,EAEA,MAAM,KAAK,MAAoC;AAC7C,UAAM,KAAK,MAAM,IAAI;AACrB,QAAI,CAAC,KAAK,UAAU,CAAC,KAAK,aAAa,CAAC,KAAK,gBAAgB;AAC3D,YAAM,IAAI,UAAU,8DAA8D;AAAA,IACpF;AAEA,QAAI,iBAAiB,KAAK,UAAU;AAEpC,UAAM,KAAK,YAAY,KAAK;AAAA,MAC1B,cAAc,KAAK;AAAA,MACnB,cAAc,KAAK;AAAA,IACrB,CAAC;AACD,qBAAiB,eAAe,YAAY,KAAK,YAAa,WAAY;AAE1E,mBACG,OAAO,KAAK,eAAe,QAAQ,EACnC,MAAM,CAAC,MAAM,QAAQ,MAAM,6BAA6B,CAAC,CAAC,EAC1D,QAAQ,MAAM,KAAK,QAAQ,CAAC;AAC/B,SAAK,iBAAiB,KAAK;AAAA,EAC7B;AAAA,EAEA,MAAM,QAAQ,MAAoC;AAChD,UAAM,KAAK,QAAQ;AACnB,WAAO,KAAK,KAAK,IAAI;AAAA,EACvB;AAAA,EAEA,MAAM,sBAAsB,SAA2D;AAErF,SAAK,YAAY,QAAQ,QAAQ,CAAC,CAAC;AAAA,EACrC;AAAA,EAEA,MAAM,4BAA4B,SAA0D;AAC1F,SAAK,YAAY,OAAO,QAAQ,CAAC,CAAC;AAAA,EACpC;AAAA,EAEA,MAAM,UAAU;AA1GlB;AA2GI,UAAM,KAAK,YAAY,QAAQ;AAC/B,eAAK,mBAAL,mBAAqB;AAAA,EACvB;AACF;;;AC9GA,YAAY,YAAY;;;ACuBtB,mBAAgB;AAAA,EACd,uBAAuB;AAAA,EACvB,2BAA2B;AAC7B;;;ACxBF,IAA8B,mBAA9B,MAEA;AAAA,EAFA;AAWE,SAAU,aAAuB;AAAA;AAAA,EAEjC,MAAM,KAAK;AAAA,IACT;AAAA,IACA,cAAc;AAAA,EAChB,GAA+C;AAlBjD;AAmBI,QAAI,EAAE,sBAAsB,mBAAmB;AAC7C,YAAM,UAAU,qDAAqD;AAAA,IACvE;AACA,SAAK,cAAc,IAAI,gBAAgB;AAAA,MACrC,WAAW,CAAC,OAAO,eAAe,KAAK,UAAU,OAAO,UAAU;AAAA,IACpE,CAAC;AACD,SAAK,SAAS,gBAAgB;AAC9B,QAAI,cAAc;AAChB,WAAK,QAAM,UAAK,WAAL,mBAAa,WAAW,UAAS;AAAA,IAC9C;AACA,SAAK,aAAa;AAClB,SAAK,aAAa;AAAA,EACpB;AAAA,EAEA,MAAM,QAAQ,EAAE,cAAc,cAAc,WAAW,GAAgC;AACrF,SAAK,SAAS,gBAAgB;AAC9B,SAAK,MAAM,KAAK,OAAO,WAAW,IAAI,KAAK;AAE3C,SAAK,aAAa;AAClB,SAAK,aAAa;AAAA,EACpB;AAAA,EAEA,MAAM,UAAU;AACd,SAAK,aAAa;AAClB,SAAK,SAAS;AACd,SAAK,MAAM;AAAA,EACb;AAQF;;;AFrCA,IAAqB,sBAArB,cAAiD,iBAAoC;AAAA,EAenF,YAAY,MAAyB;AACnC,UAAM;AAPR,2BAAsC;AAQpC,SAAK,UAAU;AACf,SAAK,OAAO,IAAI;AAAA,EAClB;AAAA,EAlBA,WAAW,cAAc;AACvB,WAAO,OAAO,oBAAoB;AAAA,EACpC;AAAA,EAkBA,MAAM,KAAK,EAAE,cAAc,cAAc,WAAW,GAAgC;AArCtF;AAsCI,UAAM,MAAM,KAAK,EAAE,cAAc,cAAc,WAAW,CAAC;AAE3D,UAAM,UAAU,MAAa,uBAAgB;AAAA,OAC3C,gBAAK,QAAQ,eAAb,mBAAyB,uBAAzB,YACE,wDAAwD,aAAa,yBAAyB,CAAC;AAAA,IACnG;AAEA,SAAK,iBAAiB,MAAa,sBAAe,kBAAkB,SAAS;AAAA,MAC3E,aAAa;AAAA,QACX,iBACE,gBAAK,QAAQ,eAAb,mBAAyB,mBAAzB,YACA;AAAA,QACF,UAAU;AAAA,SACP,KAAK,QAAQ;AAAA,MAElB,aAAa;AAAA,MACb,oBAAoB;AAAA,MACpB,uBAAuB;AAAA,IACzB,CAAC;AAGD,UAAI,UAAK,YAAL,mBAAc,cAAa,CAAC,KAAK,iBAAiB;AACpD,YAAM,KAAK,eAAe,KAAK,QAAQ,SAAS,EAAE,MAAM,CAAC,QAAQ,QAAQ,MAAM,oDAAoD,GAAG,CAAC;AAAA,IACzI;AAAA,EACF;AAAA,EAEA,MAAM,UAAU;AAhElB;AAiEI,UAAM,MAAM,QAAQ;AACpB,YAAM,UAAK,mBAAL,mBAAqB;AAC3B,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEA,MAAM,eAAe,MAAc;AACjC,UAAM,MAAM,IAAI,MAAM;AAEtB,UAAM,IAAI,QAAQ,CAAC,SAAS,WAAW;AACrC,UAAI,cAAc;AAClB,UAAI,SAAS,MAAM,QAAQ,GAAG;AAC9B,UAAI,UAAU,CAAC,QAAQ,OAAO,GAAG;AACjC,UAAI,MAAM;AAAA,IACZ,CAAC;AACD,UAAM,YAAY,MAAM,kBAAkB,GAAG;AAC7C,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEA,MAAM,UAAU,OAAmB,YAA0D;AAnF/F;AAoFI,QAAI;AACF,UAAI,KAAK,YAAY;AACnB,mBAAW,QAAQ,KAAK;AACxB;AAAA,MACF;AACA,UAAI,CAAC,KAAK,QAAQ;AAChB,cAAM,UAAU,sCAAsC;AAAA,MACxD;AACA,UAAI,cAAc,YAAY,IAAI;AAClC,iBAAK,mBAAL,mBAAqB;AAAA,QACnB,KAAK;AAAA,QACL;AAAA,QACA,CAAC,WAAY,KAAK,sBAAsB;AAAA;AAG1C,UAAI,KAAK,YAAY;AACnB,cAAM,KAAK,eAAe,KAAK;AAAA,MACjC,OAAO;AACL,cAAM,KAAK,sBAAsB,KAAK;AAAA,MACxC;AACA,YAAM,WAAW,IAAI,WAAW,KAAK,QAAQ;AAAA,QAC3C,WAAW,MAAM,aAAa,KAAK,IAAI;AAAA,MACzC,CAAC;AACD,iBAAW,QAAQ,QAAQ;AAAA,IAC7B,UAAE;AACA,YAAM,MAAM;AAAA,IACd;AAAA,EACF;AAAA,EAEA,MAAM,OAAO,MAAyB;AACpC,SAAK,UAAU;AACf,QAAI,KAAK,YAAY;AACnB,WAAK,aAAa,KAAK;AAAA,IACzB,WAAW,KAAK,WAAW;AACzB,YAAM,KAAK,eAAe,KAAK,SAAS;AAAA,IAC1C;AAAA,EACF;AAAA,EAEA,MAAM,sBAAsB,OAAmB;AA1HjD;AA2HI,QAAI,CAAC,KAAK,UAAU,CAAC,KAAK,OAAO,CAAC,KAAK,uBAAuB,CAAC,KAAK;AAAY;AAGhF,SAAI,UAAK,wBAAL,mBAA0B,cAAc;AAC1C,WAAK,IAAI,SAAS;AAClB,WAAK,IAAI,2BAA2B;AACpC,YAAM,SAAS,MAAM;AAAA,QACnB,KAAK,oBAAoB;AAAA,QACzB,KAAK,oBAAoB,aAAa;AAAA,QACtC,KAAK,oBAAoB,aAAa;AAAA,MACxC;AACA,WAAK,IAAI,UAAU,QAAQ,GAAG,GAAG,KAAK,OAAO,OAAO,KAAK,OAAO,MAAM;AACtE,WAAK,IAAI,SAAS;AAClB,WAAK,IAAI,2BAA2B;AACpC,UAAI,KAAK,iBAAiB;AACxB,aAAK,IAAI;AAAA,UACP,KAAK;AAAA,UACL;AAAA,UACA;AAAA,UACA,KAAK,gBAAgB;AAAA,UACrB,KAAK,gBAAgB;AAAA,UACrB;AAAA,UACA;AAAA,UACA,KAAK,OAAO;AAAA,UACZ,KAAK,OAAO;AAAA,QACd;AAAA,MACF,OAAO;AACL,aAAK,IAAI,YAAY;AACrB,aAAK,IAAI,SAAS,GAAG,GAAG,KAAK,OAAO,OAAO,KAAK,OAAO,MAAM;AAAA,MAC/D;AAEA,WAAK,IAAI,2BAA2B;AAAA,IACtC;AACA,SAAK,IAAI,UAAU,OAAO,GAAG,GAAG,KAAK,OAAO,OAAO,KAAK,OAAO,MAAM;AAAA,EAEvE;AAAA,EAEA,MAAM,eAAe,OAAmB;AAhK1C;AAiKI,QACE,CAAC,KAAK,OACN,CAAC,KAAK,UACN,GAAC,gBAAK,wBAAL,mBAA0B,iBAA1B,mBAAwC,WACzC,CAAC,KAAK,YACN;AACA;AAAA,IACF;AAEA,SAAK,IAAI,KAAK;AACd,SAAK,IAAI,2BAA2B;AAEpC,UAAM,SAAS,MAAM;AAAA,MACnB,KAAK,oBAAoB;AAAA,MACzB,KAAK,oBAAoB,aAAa;AAAA,MACtC,KAAK,oBAAoB,aAAa;AAAA,IACxC;AAEA,SAAK,IAAI,SAAS;AAClB,SAAK,IAAI,2BAA2B;AACpC,SAAK,IAAI,UAAU,QAAQ,GAAG,GAAG,KAAK,OAAO,OAAO,KAAK,OAAO,MAAM;AACtE,SAAK,IAAI,SAAS;AAClB,SAAK,IAAI,2BAA2B;AACpC,SAAK,IAAI,UAAU,OAAO,GAAG,GAAG,KAAK,OAAO,OAAO,KAAK,OAAO,MAAM;AACrE,SAAK,IAAI,2BAA2B;AACpC,SAAK,IAAI,SAAS,QAAQ,KAAK,UAAU;AACzC,SAAK,IAAI,UAAU,OAAO,GAAG,GAAG,KAAK,OAAO,OAAO,KAAK,OAAO,MAAM;AACrE,SAAK,IAAI,QAAQ;AAAA,EACnB;AACF;AAEA,SAAS,aACP,MACA,YACA,aACsB;AACtB,QAAM,YAA+B,IAAI,kBAAkB,aAAa,cAAc,CAAC;AACvF,QAAM,SAAS,KAAK,gBAAgB;AACpC,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,GAAG;AACzC,cAAU,IAAI,CAAC,IAAI,OAAO,CAAC;AAC3B,cAAU,IAAI,IAAI,CAAC,IAAI,OAAO,CAAC;AAC/B,cAAU,IAAI,IAAI,CAAC,IAAI,OAAO,CAAC;AAC/B,cAAU,IAAI,IAAI,CAAC,IAAI,OAAO,CAAC;AAAA,EACjC;AACA,QAAM,UAAU,IAAI,UAAU,WAAW,YAAY,WAAW;AAEhE,SAAO,kBAAkB,OAAO;AAClC;;;AGtMO,IAAM,iBAAiB,CAAC,aAAqB,IAAI,qBAAwC;AAC9F,SAAOA,qBAAoB,EAAE,YAAY,iBAAiB,GAAG,iBAAiB;AAChF;AAEO,IAAM,oBAAoB,CAAC,WAAmB,qBAAwC;AAC3F,SAAOA,qBAAoB,EAAE,WAAW,iBAAiB,GAAG,oBAAoB;AAClF;AAEO,IAAMA,uBAAsB,CAAC,SAA4B,OAAO,2BAA2B;AAChG,QAAM,uBAAuB,iBAAiB,eAAe,oBAAsB;AACnF,MAAI,CAAC,sBAAsB;AACzB,UAAM,IAAI,MAAM,4CAA4C;AAAA,EAC9D;AACA,QAAM,YAAY,IAAI,iBAAiB,IAAI,oBAAsB,OAAO,GAAG,IAAI;AAC/E,SAAO;AACT;","names":["BackgroundProcessor"]}
|
|
1
|
+
{"version":3,"sources":["../src/utils.ts","../src/ProcessorWrapper.ts","../src/transformers/BackgroundTransformer.ts","../package.json","../src/transformers/VideoTransformer.ts","../src/index.ts"],"sourcesContent":["export const supportsProcessor = typeof MediaStreamTrackGenerator !== 'undefined';\nexport const supportsOffscreenCanvas = typeof OffscreenCanvas !== 'undefined';\n\nasync function sleep(time: number) {\n return new Promise((resolve) => setTimeout(resolve, time));\n}\n\nexport async function waitForTrackResolution(track: MediaStreamTrack) {\n const timeout = 500;\n\n // browsers report wrong initial resolution on iOS.\n // when slightly delaying the call to .getSettings(), the correct resolution is being reported\n await sleep(10);\n\n const started = Date.now();\n while (Date.now() - started < timeout) {\n const { width, height } = track.getSettings();\n if (width && height) {\n return { width, height };\n }\n await sleep(50);\n }\n return { width: undefined, height: undefined };\n}\n","import type { ProcessorOptions, Track, TrackProcessor } from 'livekit-client';\nimport { TrackTransformer } from './transformers';\nimport { waitForTrackResolution } from './utils';\n\nexport default class ProcessorWrapper<TransformerOptions extends Record<string, unknown>>\n implements TrackProcessor<Track.Kind>\n{\n static get isSupported() {\n return (\n typeof MediaStreamTrackGenerator !== 'undefined' &&\n typeof MediaStreamTrackProcessor !== 'undefined'\n );\n }\n\n name: string;\n\n source?: MediaStreamVideoTrack;\n\n processor?: MediaStreamTrackProcessor<VideoFrame>;\n\n trackGenerator?: MediaStreamTrackGenerator<VideoFrame>;\n\n canvas?: OffscreenCanvas;\n\n sourceDummy?: HTMLMediaElement;\n\n processedTrack?: MediaStreamTrack;\n\n transformer: TrackTransformer<TransformerOptions>;\n\n constructor(transformer: TrackTransformer<TransformerOptions>, name: string) {\n this.name = name;\n this.transformer = transformer;\n }\n\n private async setup(opts: ProcessorOptions<Track.Kind>) {\n this.source = opts.track as MediaStreamVideoTrack;\n\n const { width, height } = await waitForTrackResolution(this.source);\n this.sourceDummy = opts.element;\n\n if (!(this.sourceDummy instanceof HTMLVideoElement)) {\n throw TypeError('Currently only video transformers are supported');\n }\n\n if (this.sourceDummy instanceof HTMLVideoElement) {\n this.sourceDummy.height = height ?? 300;\n this.sourceDummy.width = width ?? 300;\n }\n\n // TODO explore if we can do all the processing work in a webworker\n this.processor = new MediaStreamTrackProcessor({ track: this.source });\n\n this.trackGenerator = new MediaStreamTrackGenerator({\n kind: 'video',\n signalTarget: this.source,\n });\n\n this.canvas = new OffscreenCanvas(width ?? 300, height ?? 300);\n }\n\n async init(opts: ProcessorOptions<Track.Kind>) {\n await this.setup(opts);\n if (!this.canvas || !this.processor || !this.trackGenerator) {\n throw new TypeError('Expected both canvas and processor to be defined after setup');\n }\n\n const readableStream = this.processor.readable;\n\n await this.transformer.init({\n outputCanvas: this.canvas,\n inputElement: this.sourceDummy as HTMLVideoElement,\n });\n\n const pipedStream = readableStream.pipeThrough(this.transformer!.transformer!);\n\n pipedStream\n .pipeTo(this.trackGenerator.writable)\n .catch((e) => console.error('error when trying to pipe', e))\n .finally(() => this.destroy());\n this.processedTrack = this.trackGenerator as MediaStreamVideoTrack;\n }\n\n async restart(opts: ProcessorOptions<Track.Kind>) {\n await this.destroy();\n return this.init(opts);\n }\n\n async restartTransformer(...options: Parameters<(typeof this.transformer)['restart']>) {\n // @ts-ignore unclear why the restart method only accepts VideoTransformerInitOptions instead of either those or AudioTransformerInitOptions\n this.transformer.restart(options[0]);\n }\n\n async updateTransformerOptions(...options: Parameters<(typeof this.transformer)['update']>) {\n this.transformer.update(options[0]);\n }\n\n async destroy() {\n await this.transformer.destroy();\n this.trackGenerator?.stop();\n }\n}\n","import * as vision from '@mediapipe/tasks-vision';\nimport { dependencies } from '../../package.json';\nimport VideoTransformer from './VideoTransformer';\nimport { VideoTransformerInitOptions } from './types';\n\nexport type SegmenterOptions = Partial<vision.ImageSegmenterOptions['baseOptions']>;\n\nexport type BackgroundOptions = {\n blurRadius?: number;\n imagePath?: string;\n /** cannot be updated through the `update` method, needs a restart */\n segmenterOptions?: SegmenterOptions;\n /** cannot be updated through the `update` method, needs a restart */\n assetPaths?: { tasksVisionFileSet?: string; modelAssetPath?: string };\n};\n\nexport default class BackgroundProcessor extends VideoTransformer<BackgroundOptions> {\n static get isSupported() {\n return typeof OffscreenCanvas !== 'undefined';\n }\n\n imageSegmenter?: vision.ImageSegmenter;\n\n segmentationResults: vision.ImageSegmenterResult | undefined;\n\n backgroundImage: ImageBitmap | null = null;\n\n blurRadius?: number;\n\n options: BackgroundOptions;\n\n constructor(opts: BackgroundOptions) {\n super();\n this.options = opts;\n this.update(opts);\n }\n\n async init({ outputCanvas, inputElement: inputVideo }: VideoTransformerInitOptions) {\n await super.init({ outputCanvas, inputElement: inputVideo });\n\n const fileSet = await vision.FilesetResolver.forVisionTasks(\n this.options.assetPaths?.tasksVisionFileSet ??\n `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${dependencies['@mediapipe/tasks-vision']}/wasm`,\n );\n\n this.imageSegmenter = await vision.ImageSegmenter.createFromOptions(fileSet, {\n baseOptions: {\n modelAssetPath:\n this.options.assetPaths?.modelAssetPath ??\n 'https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite',\n delegate: 'GPU',\n ...this.options.segmenterOptions,\n },\n runningMode: 'VIDEO',\n outputCategoryMask: true,\n outputConfidenceMasks: false,\n });\n\n // Skip loading the image here if update already loaded the image below\n if (this.options?.imagePath && !this.backgroundImage) {\n await this.loadBackground(this.options.imagePath).catch((err) =>\n console.error('Error while loading processor background image: ', err),\n );\n }\n }\n\n async destroy() {\n await super.destroy();\n await this.imageSegmenter?.close();\n this.backgroundImage = null;\n }\n\n async loadBackground(path: string) {\n const img = new Image();\n\n await new Promise((resolve, reject) => {\n img.crossOrigin = 'Anonymous';\n img.onload = () => resolve(img);\n img.onerror = (err) => reject(err);\n img.src = path;\n });\n const imageData = await createImageBitmap(img);\n this.backgroundImage = imageData;\n }\n\n async transform(frame: VideoFrame, controller: TransformStreamDefaultController<VideoFrame>) {\n try {\n if (!(frame instanceof VideoFrame)) {\n console.debug('empty frame detected, ignoring');\n return;\n }\n if (this.isDisabled) {\n controller.enqueue(frame);\n return;\n }\n if (!this.canvas) {\n throw TypeError('Canvas needs to be initialized first');\n }\n let startTimeMs = performance.now();\n this.imageSegmenter?.segmentForVideo(\n this.inputVideo!,\n startTimeMs,\n (result) => (this.segmentationResults = result),\n );\n\n if (this.blurRadius) {\n await this.blurBackground(frame);\n } else {\n await this.drawVirtualBackground(frame);\n }\n const newFrame = new VideoFrame(this.canvas, {\n timestamp: frame.timestamp || Date.now(),\n });\n controller.enqueue(newFrame);\n } finally {\n frame?.close();\n }\n }\n\n async update(opts: BackgroundOptions) {\n this.options = opts;\n if (opts.blurRadius) {\n this.blurRadius = opts.blurRadius;\n } else if (opts.imagePath) {\n await this.loadBackground(opts.imagePath);\n }\n }\n\n async drawVirtualBackground(frame: VideoFrame) {\n if (!this.canvas || !this.ctx || !this.segmentationResults || !this.inputVideo) return;\n // this.ctx.save();\n // this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n if (this.segmentationResults?.categoryMask && this.segmentationResults.categoryMask.width > 0) {\n this.ctx.globalCompositeOperation = 'copy';\n const bitmap = await maskToBitmap(\n this.segmentationResults.categoryMask,\n this.segmentationResults.categoryMask.width,\n this.segmentationResults.categoryMask.height,\n );\n this.ctx.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.filter = 'none';\n this.ctx.globalCompositeOperation = 'source-in';\n if (this.backgroundImage) {\n this.ctx.drawImage(\n this.backgroundImage,\n 0,\n 0,\n this.backgroundImage.width,\n this.backgroundImage.height,\n 0,\n 0,\n this.canvas.width,\n this.canvas.height,\n );\n } else {\n this.ctx.fillStyle = '#00FF00';\n this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);\n }\n\n this.ctx.globalCompositeOperation = 'destination-over';\n }\n this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);\n // this.ctx.restore();\n }\n\n async blurBackground(frame: VideoFrame) {\n if (\n !this.ctx ||\n !this.canvas ||\n !this.segmentationResults?.categoryMask?.canvas ||\n !this.inputVideo\n ) {\n return;\n }\n\n this.ctx.save();\n this.ctx.globalCompositeOperation = 'copy';\n\n if (this.segmentationResults?.categoryMask && this.segmentationResults.categoryMask.width > 0) {\n const bitmap = await maskToBitmap(\n this.segmentationResults.categoryMask,\n this.segmentationResults.categoryMask.width,\n this.segmentationResults.categoryMask.height,\n );\n\n this.ctx.globalCompositeOperation = 'copy';\n this.ctx.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.filter = 'none';\n this.ctx.globalCompositeOperation = 'source-out';\n this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.globalCompositeOperation = 'destination-over';\n this.ctx.filter = `blur(${this.blurRadius}px)`;\n this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);\n this.ctx.restore();\n }\n }\n}\n\nfunction maskToBitmap(\n mask: vision.MPMask,\n videoWidth: number,\n videoHeight: number,\n): Promise<ImageBitmap> {\n const dataArray: Uint8ClampedArray = new Uint8ClampedArray(videoWidth * videoHeight * 4);\n const result = mask.getAsUint8Array();\n for (let i = 0; i < result.length; i += 1) {\n dataArray[i * 4] = result[i];\n dataArray[i * 4 + 1] = result[i];\n dataArray[i * 4 + 2] = result[i];\n dataArray[i * 4 + 3] = result[i];\n }\n const dataNew = new ImageData(dataArray, videoWidth, videoHeight);\n\n return createImageBitmap(dataNew);\n}\n","{\n \"name\": \"@livekit/track-processors\",\n \"version\": \"0.4.0\",\n \"description\": \"LiveKit track processors\",\n \"main\": \"dist/index.js\",\n \"module\": \"dist/index.mjs\",\n \"source\": \"src/index.ts\",\n \"types\": \"dist/src/index.d.ts\",\n \"repository\": \"git@github.com:livekit/livekit-track-processors.git\",\n \"author\": \"Lukas Seiler\",\n \"license\": \"Apache-2.0\",\n \"scripts\": {\n \"build\": \"tsup --onSuccess \\\"tsc --declaration --emitDeclarationOnly\\\"\",\n \"build-sample\": \"cd example && vite build\",\n \"lint\": \"eslint src\",\n \"release\": \"pnpm build && changeset publish\",\n \"test\": \"jest\",\n \"sample\": \"vite serve example --port 8080 --open\"\n },\n \"files\": [\n \"dist\",\n \"src\"\n ],\n \"dependencies\": {\n \"@mediapipe/tasks-vision\": \"0.10.21\"\n },\n \"peerDependencies\": {\n \"livekit-client\": \"^1.12.0 || ^2.1.0\"\n },\n \"devDependencies\": {\n \"@changesets/cli\": \"^2.26.2\",\n \"@livekit/changesets-changelog-github\": \"^0.0.4\",\n \"@trivago/prettier-plugin-sort-imports\": \"^4.1.1\",\n \"@types/dom-mediacapture-transform\": \"^0.1.6\",\n \"@types/offscreencanvas\": \"^2019.7.0\",\n \"@typescript-eslint/eslint-plugin\": \"^5.62.0\",\n \"eslint\": \"8.39.0\",\n \"eslint-config-airbnb-typescript\": \"17.0.0\",\n \"eslint-config-prettier\": \"8.8.0\",\n \"eslint-plugin-ecmascript-compat\": \"^3.0.0\",\n \"eslint-plugin-import\": \"2.27.5\",\n \"prettier\": \"^2.8.8\",\n \"tsup\": \"^7.1.0\",\n \"typescript\": \"^5.0.4\",\n \"vite\": \"^4.3.8\"\n },\n \"packageManager\": \"pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531\"\n}\n","import { VideoTrackTransformer, VideoTransformerInitOptions } from './types';\n\nexport default abstract class VideoTransformer<Options extends Record<string, unknown>>\n implements VideoTrackTransformer<Options>\n{\n transformer?: TransformStream;\n\n canvas?: OffscreenCanvas;\n\n ctx?: OffscreenCanvasRenderingContext2D;\n\n inputVideo?: HTMLVideoElement;\n\n protected isDisabled?: Boolean = false;\n\n async init({\n outputCanvas,\n inputElement: inputVideo,\n }: VideoTransformerInitOptions): Promise<void> {\n if (!(inputVideo instanceof HTMLVideoElement)) {\n throw TypeError('Video transformer needs a HTMLVideoElement as input');\n }\n\n this.transformer = new TransformStream({\n transform: (frame, controller) => this.transform(frame, controller),\n });\n this.canvas = outputCanvas || null;\n if (outputCanvas) {\n this.ctx = this.canvas?.getContext('2d') || undefined;\n }\n this.inputVideo = inputVideo;\n this.isDisabled = false;\n }\n\n async restart({ outputCanvas, inputElement: inputVideo }: VideoTransformerInitOptions) {\n this.canvas = outputCanvas || null;\n this.ctx = this.canvas.getContext('2d') || undefined;\n\n this.inputVideo = inputVideo;\n this.isDisabled = false;\n }\n\n async destroy() {\n this.isDisabled = true;\n this.canvas = undefined;\n this.ctx = undefined;\n }\n\n abstract transform(\n frame: VideoFrame,\n controller: TransformStreamDefaultController<VideoFrame>,\n ): void;\n\n abstract update(options: Options): void;\n}\n","import ProcessorWrapper from './ProcessorWrapper';\nimport BackgroundTransformer, {\n BackgroundOptions,\n SegmenterOptions,\n} from './transformers/BackgroundTransformer';\n\nexport * from './transformers/types';\nexport { default as VideoTransformer } from './transformers/VideoTransformer';\nexport { ProcessorWrapper, type BackgroundOptions, type SegmenterOptions, BackgroundTransformer };\n\nexport const BackgroundBlur = (blurRadius: number = 10, segmenterOptions?: SegmenterOptions) => {\n return BackgroundProcessor({ blurRadius, segmenterOptions }, 'background-blur');\n};\n\nexport const VirtualBackground = (imagePath: string, segmenterOptions?: SegmenterOptions) => {\n return BackgroundProcessor({ imagePath, segmenterOptions }, 'virtual-background');\n};\n\nexport const BackgroundProcessor = (options: BackgroundOptions, name = 'background-processor') => {\n const isProcessorSupported = ProcessorWrapper.isSupported && BackgroundTransformer.isSupported;\n if (!isProcessorSupported) {\n throw new Error('processor is not supported in this browser');\n }\n const processor = new ProcessorWrapper(new BackgroundTransformer(options), name);\n return processor;\n};\n"],"mappings":"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","names":["BackgroundProcessor"]}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
/// <reference types="dom-mediacapture-transform" />
|
|
2
1
|
import type { ProcessorOptions, Track, TrackProcessor } from 'livekit-client';
|
|
3
2
|
import { TrackTransformer } from './transformers';
|
|
4
3
|
export default class ProcessorWrapper<TransformerOptions extends Record<string, unknown>> implements TrackProcessor<Track.Kind> {
|
|
5
4
|
static get isSupported(): boolean;
|
|
6
5
|
name: string;
|
|
7
6
|
source?: MediaStreamVideoTrack;
|
|
8
|
-
sourceSettings?: MediaTrackSettings;
|
|
9
7
|
processor?: MediaStreamTrackProcessor<VideoFrame>;
|
|
10
8
|
trackGenerator?: MediaStreamTrackGenerator<VideoFrame>;
|
|
11
9
|
canvas?: OffscreenCanvas;
|
package/dist/src/utils.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
export declare const supportsProcessor: boolean;
|
|
2
2
|
export declare const supportsOffscreenCanvas: boolean;
|
|
3
|
+
export declare function waitForTrackResolution(track: MediaStreamTrack): Promise<{
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
} | {
|
|
7
|
+
width: undefined;
|
|
8
|
+
height: undefined;
|
|
9
|
+
}>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livekit/track-processors",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "LiveKit track processors",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
"src"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@mediapipe/
|
|
18
|
-
"@mediapipe/tasks-vision": "0.10.9"
|
|
17
|
+
"@mediapipe/tasks-vision": "0.10.21"
|
|
19
18
|
},
|
|
20
19
|
"peerDependencies": {
|
|
21
20
|
"livekit-client": "^1.12.0 || ^2.1.0"
|
|
@@ -37,6 +36,7 @@
|
|
|
37
36
|
"typescript": "^5.0.4",
|
|
38
37
|
"vite": "^4.3.8"
|
|
39
38
|
},
|
|
39
|
+
"packageManager": "pnpm@9.15.9+sha512.68046141893c66fad01c079231128e9afb89ef87e2691d69e4d40eee228988295fd4682181bae55b58418c3a253bde65a505ec7c5f9403ece5cc3cd37dcf2531",
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "tsup --onSuccess \"tsc --declaration --emitDeclarationOnly\"",
|
|
42
42
|
"build-sample": "cd example && vite build",
|
package/src/ProcessorWrapper.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ProcessorOptions, Track, TrackProcessor } from 'livekit-client';
|
|
2
2
|
import { TrackTransformer } from './transformers';
|
|
3
|
+
import { waitForTrackResolution } from './utils';
|
|
3
4
|
|
|
4
5
|
export default class ProcessorWrapper<TransformerOptions extends Record<string, unknown>>
|
|
5
6
|
implements TrackProcessor<Track.Kind>
|
|
@@ -15,8 +16,6 @@ export default class ProcessorWrapper<TransformerOptions extends Record<string,
|
|
|
15
16
|
|
|
16
17
|
source?: MediaStreamVideoTrack;
|
|
17
18
|
|
|
18
|
-
sourceSettings?: MediaTrackSettings;
|
|
19
|
-
|
|
20
19
|
processor?: MediaStreamTrackProcessor<VideoFrame>;
|
|
21
20
|
|
|
22
21
|
trackGenerator?: MediaStreamTrackGenerator<VideoFrame>;
|
|
@@ -32,29 +31,23 @@ export default class ProcessorWrapper<TransformerOptions extends Record<string,
|
|
|
32
31
|
constructor(transformer: TrackTransformer<TransformerOptions>, name: string) {
|
|
33
32
|
this.name = name;
|
|
34
33
|
this.transformer = transformer;
|
|
35
|
-
this.transformer.restart;
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
private async setup(opts: ProcessorOptions<Track.Kind>) {
|
|
39
37
|
this.source = opts.track as MediaStreamVideoTrack;
|
|
40
|
-
|
|
41
|
-
await this.source
|
|
42
|
-
...origConstraints,
|
|
43
|
-
// @ts-expect-error when a mediastream track is resized and/or cropped, the `VideoFrame` will have a coded height/width of the original video size
|
|
44
|
-
// this leads to a shift of the underlying video as the frame itself is being rendered with the coded size
|
|
45
|
-
// but image segmentation is based on the display dimensions (-> the cropped version)
|
|
46
|
-
// in order to prevent this, we force the resize mode to "none"
|
|
47
|
-
resizeMode: 'none',
|
|
48
|
-
});
|
|
49
|
-
this.sourceSettings = this.source.getSettings();
|
|
38
|
+
|
|
39
|
+
const { width, height } = await waitForTrackResolution(this.source);
|
|
50
40
|
this.sourceDummy = opts.element;
|
|
51
|
-
|
|
52
|
-
this.sourceDummy.height = this.sourceSettings.height ?? 300;
|
|
53
|
-
this.sourceDummy.width = this.sourceSettings.width ?? 300;
|
|
54
|
-
}
|
|
41
|
+
|
|
55
42
|
if (!(this.sourceDummy instanceof HTMLVideoElement)) {
|
|
56
43
|
throw TypeError('Currently only video transformers are supported');
|
|
57
44
|
}
|
|
45
|
+
|
|
46
|
+
if (this.sourceDummy instanceof HTMLVideoElement) {
|
|
47
|
+
this.sourceDummy.height = height ?? 300;
|
|
48
|
+
this.sourceDummy.width = width ?? 300;
|
|
49
|
+
}
|
|
50
|
+
|
|
58
51
|
// TODO explore if we can do all the processing work in a webworker
|
|
59
52
|
this.processor = new MediaStreamTrackProcessor({ track: this.source });
|
|
60
53
|
|
|
@@ -63,10 +56,7 @@ export default class ProcessorWrapper<TransformerOptions extends Record<string,
|
|
|
63
56
|
signalTarget: this.source,
|
|
64
57
|
});
|
|
65
58
|
|
|
66
|
-
this.canvas = new OffscreenCanvas(
|
|
67
|
-
this.sourceSettings.width ?? 300,
|
|
68
|
-
this.sourceSettings.height ?? 300,
|
|
69
|
-
);
|
|
59
|
+
this.canvas = new OffscreenCanvas(width ?? 300, height ?? 300);
|
|
70
60
|
}
|
|
71
61
|
|
|
72
62
|
async init(opts: ProcessorOptions<Track.Kind>) {
|
|
@@ -75,15 +65,16 @@ export default class ProcessorWrapper<TransformerOptions extends Record<string,
|
|
|
75
65
|
throw new TypeError('Expected both canvas and processor to be defined after setup');
|
|
76
66
|
}
|
|
77
67
|
|
|
78
|
-
|
|
68
|
+
const readableStream = this.processor.readable;
|
|
79
69
|
|
|
80
70
|
await this.transformer.init({
|
|
81
71
|
outputCanvas: this.canvas,
|
|
82
72
|
inputElement: this.sourceDummy as HTMLVideoElement,
|
|
83
73
|
});
|
|
84
|
-
readableStream = readableStream.pipeThrough(this.transformer!.transformer!);
|
|
85
74
|
|
|
86
|
-
readableStream
|
|
75
|
+
const pipedStream = readableStream.pipeThrough(this.transformer!.transformer!);
|
|
76
|
+
|
|
77
|
+
pipedStream
|
|
87
78
|
.pipeTo(this.trackGenerator.writable)
|
|
88
79
|
.catch((e) => console.error('error when trying to pipe', e))
|
|
89
80
|
.finally(() => this.destroy());
|
|
@@ -58,7 +58,9 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
|
|
|
58
58
|
|
|
59
59
|
// Skip loading the image here if update already loaded the image below
|
|
60
60
|
if (this.options?.imagePath && !this.backgroundImage) {
|
|
61
|
-
await this.loadBackground(this.options.imagePath).catch((err) =>
|
|
61
|
+
await this.loadBackground(this.options.imagePath).catch((err) =>
|
|
62
|
+
console.error('Error while loading processor background image: ', err),
|
|
63
|
+
);
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
@@ -83,6 +85,10 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
|
|
|
83
85
|
|
|
84
86
|
async transform(frame: VideoFrame, controller: TransformStreamDefaultController<VideoFrame>) {
|
|
85
87
|
try {
|
|
88
|
+
if (!(frame instanceof VideoFrame)) {
|
|
89
|
+
console.debug('empty frame detected, ignoring');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
86
92
|
if (this.isDisabled) {
|
|
87
93
|
controller.enqueue(frame);
|
|
88
94
|
return;
|
|
@@ -107,7 +113,7 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
|
|
|
107
113
|
});
|
|
108
114
|
controller.enqueue(newFrame);
|
|
109
115
|
} finally {
|
|
110
|
-
frame
|
|
116
|
+
frame?.close();
|
|
111
117
|
}
|
|
112
118
|
}
|
|
113
119
|
|
|
@@ -124,8 +130,7 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
|
|
|
124
130
|
if (!this.canvas || !this.ctx || !this.segmentationResults || !this.inputVideo) return;
|
|
125
131
|
// this.ctx.save();
|
|
126
132
|
// this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
127
|
-
if (this.segmentationResults?.categoryMask) {
|
|
128
|
-
this.ctx.filter = 'blur(10px)';
|
|
133
|
+
if (this.segmentationResults?.categoryMask && this.segmentationResults.categoryMask.width > 0) {
|
|
129
134
|
this.ctx.globalCompositeOperation = 'copy';
|
|
130
135
|
const bitmap = await maskToBitmap(
|
|
131
136
|
this.segmentationResults.categoryMask,
|
|
@@ -171,22 +176,23 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
|
|
|
171
176
|
this.ctx.save();
|
|
172
177
|
this.ctx.globalCompositeOperation = 'copy';
|
|
173
178
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
if (this.segmentationResults?.categoryMask && this.segmentationResults.categoryMask.width > 0) {
|
|
180
|
+
const bitmap = await maskToBitmap(
|
|
181
|
+
this.segmentationResults.categoryMask,
|
|
182
|
+
this.segmentationResults.categoryMask.width,
|
|
183
|
+
this.segmentationResults.categoryMask.height,
|
|
184
|
+
);
|
|
179
185
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
this.ctx.globalCompositeOperation = 'copy';
|
|
187
|
+
this.ctx.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);
|
|
188
|
+
this.ctx.filter = 'none';
|
|
189
|
+
this.ctx.globalCompositeOperation = 'source-out';
|
|
190
|
+
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
191
|
+
this.ctx.globalCompositeOperation = 'destination-over';
|
|
192
|
+
this.ctx.filter = `blur(${this.blurRadius}px)`;
|
|
193
|
+
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
194
|
+
this.ctx.restore();
|
|
195
|
+
}
|
|
190
196
|
}
|
|
191
197
|
}
|
|
192
198
|
|
|
@@ -20,6 +20,7 @@ export default abstract class VideoTransformer<Options extends Record<string, un
|
|
|
20
20
|
if (!(inputVideo instanceof HTMLVideoElement)) {
|
|
21
21
|
throw TypeError('Video transformer needs a HTMLVideoElement as input');
|
|
22
22
|
}
|
|
23
|
+
|
|
23
24
|
this.transformer = new TransformStream({
|
|
24
25
|
transform: (frame, controller) => this.transform(frame, controller),
|
|
25
26
|
});
|
package/src/utils.ts
CHANGED
|
@@ -1,2 +1,24 @@
|
|
|
1
1
|
export const supportsProcessor = typeof MediaStreamTrackGenerator !== 'undefined';
|
|
2
2
|
export const supportsOffscreenCanvas = typeof OffscreenCanvas !== 'undefined';
|
|
3
|
+
|
|
4
|
+
async function sleep(time: number) {
|
|
5
|
+
return new Promise((resolve) => setTimeout(resolve, time));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function waitForTrackResolution(track: MediaStreamTrack) {
|
|
9
|
+
const timeout = 500;
|
|
10
|
+
|
|
11
|
+
// browsers report wrong initial resolution on iOS.
|
|
12
|
+
// when slightly delaying the call to .getSettings(), the correct resolution is being reported
|
|
13
|
+
await sleep(10);
|
|
14
|
+
|
|
15
|
+
const started = Date.now();
|
|
16
|
+
while (Date.now() - started < timeout) {
|
|
17
|
+
const { width, height } = track.getSettings();
|
|
18
|
+
if (width && height) {
|
|
19
|
+
return { width, height };
|
|
20
|
+
}
|
|
21
|
+
await sleep(50);
|
|
22
|
+
}
|
|
23
|
+
return { width: undefined, height: undefined };
|
|
24
|
+
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Holistic, Options, Results } from '@mediapipe/holistic';
|
|
2
|
-
import VideoTransformer from './VideoTransformer';
|
|
3
|
-
import { VideoTransformerInitOptions } from './types';
|
|
4
|
-
export type MediaPipeHolisticTrackerTransformerOptions = {
|
|
5
|
-
holisticOptions?: Options;
|
|
6
|
-
callback?: (results: Results) => void;
|
|
7
|
-
};
|
|
8
|
-
export default class MediaPipeHolisticTrackerTransformer extends VideoTransformer<MediaPipeHolisticTrackerTransformerOptions> {
|
|
9
|
-
holistic?: Holistic;
|
|
10
|
-
holisticOptions: Options;
|
|
11
|
-
callback: (results: Results) => void;
|
|
12
|
-
static get isSupported(): boolean;
|
|
13
|
-
constructor({ holisticOptions, callback }: MediaPipeHolisticTrackerTransformerOptions);
|
|
14
|
-
init({ inputElement: inputVideo, outputCanvas, }: VideoTransformerInitOptions): Promise<void>;
|
|
15
|
-
destroy(): Promise<void>;
|
|
16
|
-
update(): Promise<void>;
|
|
17
|
-
transform(): Promise<void>;
|
|
18
|
-
sendFramesContinuouslyForTracking(videoEl: HTMLVideoElement): Promise<void>;
|
|
19
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { Holistic, Options, Results } from '@mediapipe/holistic';
|
|
2
|
-
import VideoTransformer from './VideoTransformer';
|
|
3
|
-
import { VideoTransformerInitOptions } from './types';
|
|
4
|
-
|
|
5
|
-
export type MediaPipeHolisticTrackerTransformerOptions = {
|
|
6
|
-
holisticOptions?: Options;
|
|
7
|
-
callback?: (results: Results) => void;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export default class MediaPipeHolisticTrackerTransformer extends VideoTransformer<MediaPipeHolisticTrackerTransformerOptions> {
|
|
11
|
-
holistic?: Holistic;
|
|
12
|
-
holisticOptions: Options;
|
|
13
|
-
callback: (results: Results) => void;
|
|
14
|
-
|
|
15
|
-
public static get isSupported(): boolean {
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
constructor({ holisticOptions, callback }: MediaPipeHolisticTrackerTransformerOptions) {
|
|
20
|
-
super();
|
|
21
|
-
this.callback = callback || (() => null);
|
|
22
|
-
this.holisticOptions = holisticOptions || {};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async init({
|
|
26
|
-
inputElement: inputVideo,
|
|
27
|
-
outputCanvas,
|
|
28
|
-
}: VideoTransformerInitOptions): Promise<void> {
|
|
29
|
-
super.init({ outputCanvas, inputElement: inputVideo });
|
|
30
|
-
|
|
31
|
-
this.holistic = new Holistic({
|
|
32
|
-
locateFile: (file) => `https://cdn.jsdelivr.net/npm/@mediapipe/holistic/${file}`,
|
|
33
|
-
});
|
|
34
|
-
this.holistic.setOptions(this.holisticOptions);
|
|
35
|
-
this.holistic.onResults((r) => {
|
|
36
|
-
this.callback(r);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
this.sendFramesContinuouslyForTracking(this.inputVideo!);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async destroy(): Promise<void> {
|
|
43
|
-
this.callback = () => null;
|
|
44
|
-
await super.destroy();
|
|
45
|
-
await this.holistic?.close();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async update() {}
|
|
49
|
-
|
|
50
|
-
async transform(): Promise<void> {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async sendFramesContinuouslyForTracking(videoEl: HTMLVideoElement): Promise<void> {
|
|
55
|
-
if (!this.isDisabled) {
|
|
56
|
-
if (videoEl.videoWidth > 0 && videoEl.videoHeight > 0) {
|
|
57
|
-
await this.holistic?.send({ image: videoEl });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
videoEl.requestVideoFrameCallback(() => {
|
|
61
|
-
this.sendFramesContinuouslyForTracking(videoEl);
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|