@livekit/track-processors 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +59 -50
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +58 -49
- 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 +5 -18
- package/src/transformers/BackgroundTransformer.ts +35 -36
- package/src/utils.ts +22 -0
- package/dist/src/transformers/MediaPipeHolisticTrackerTransformer.d.ts +0 -19
- package/src/transformers/MediaPipeHolisticTrackerTransformer.ts +0 -65
package/dist/index.js
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
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/
|
|
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 };
|
|
13
|
+
}
|
|
14
|
+
await sleep(50);
|
|
15
|
+
}
|
|
16
|
+
return { width: void 0, height: void 0 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/ProcessorWrapper.ts
|
|
2
20
|
var ProcessorWrapper = class {
|
|
3
21
|
static get isSupported() {
|
|
4
22
|
return typeof MediaStreamTrackGenerator !== "undefined" && typeof MediaStreamTrackProcessor !== "undefined";
|
|
@@ -8,35 +26,22 @@ var ProcessorWrapper = class {
|
|
|
8
26
|
this.transformer = transformer;
|
|
9
27
|
}
|
|
10
28
|
async setup(opts) {
|
|
11
|
-
var _a, _b, _c, _d;
|
|
12
29
|
this.source = opts.track;
|
|
13
|
-
const
|
|
14
|
-
await this.source.applyConstraints({
|
|
15
|
-
...origConstraints,
|
|
16
|
-
// @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
|
|
17
|
-
// this leads to a shift of the underlying video as the frame itself is being rendered with the coded size
|
|
18
|
-
// but image segmentation is based on the display dimensions (-> the cropped version)
|
|
19
|
-
// in order to prevent this, we force the resize mode to "none"
|
|
20
|
-
resizeMode: "none"
|
|
21
|
-
});
|
|
22
|
-
this.sourceSettings = this.source.getSettings();
|
|
30
|
+
const { width, height } = await waitForTrackResolution(this.source);
|
|
23
31
|
this.sourceDummy = opts.element;
|
|
24
32
|
if (!(this.sourceDummy instanceof HTMLVideoElement)) {
|
|
25
33
|
throw TypeError("Currently only video transformers are supported");
|
|
26
34
|
}
|
|
27
35
|
if (this.sourceDummy instanceof HTMLVideoElement) {
|
|
28
|
-
this.sourceDummy.height =
|
|
29
|
-
this.sourceDummy.width =
|
|
36
|
+
this.sourceDummy.height = height != null ? height : 300;
|
|
37
|
+
this.sourceDummy.width = width != null ? width : 300;
|
|
30
38
|
}
|
|
31
39
|
this.processor = new MediaStreamTrackProcessor({ track: this.source });
|
|
32
40
|
this.trackGenerator = new MediaStreamTrackGenerator({
|
|
33
41
|
kind: "video",
|
|
34
42
|
signalTarget: this.source
|
|
35
43
|
});
|
|
36
|
-
this.canvas = new OffscreenCanvas(
|
|
37
|
-
(_c = this.sourceSettings.width) != null ? _c : 300,
|
|
38
|
-
(_d = this.sourceSettings.height) != null ? _d : 300
|
|
39
|
-
);
|
|
44
|
+
this.canvas = new OffscreenCanvas(width != null ? width : 300, height != null ? height : 300);
|
|
40
45
|
}
|
|
41
46
|
async init(opts) {
|
|
42
47
|
await this.setup(opts);
|
|
@@ -74,8 +79,7 @@ var _tasksvision = require('@mediapipe/tasks-vision'); var vision = _interopRequ
|
|
|
74
79
|
|
|
75
80
|
// package.json
|
|
76
81
|
var dependencies = {
|
|
77
|
-
"@mediapipe/
|
|
78
|
-
"@mediapipe/tasks-vision": "0.10.9"
|
|
82
|
+
"@mediapipe/tasks-vision": "0.10.21"
|
|
79
83
|
};
|
|
80
84
|
|
|
81
85
|
// src/transformers/VideoTransformer.ts
|
|
@@ -209,15 +213,17 @@ var BackgroundProcessor = class extends VideoTransformer {
|
|
|
209
213
|
var _a;
|
|
210
214
|
if (!this.canvas || !this.ctx || !this.segmentationResults || !this.inputVideo)
|
|
211
215
|
return;
|
|
212
|
-
if ((_a = this.segmentationResults) == null ? void 0 : _a.categoryMask) {
|
|
213
|
-
this.ctx.filter = "blur(10px)";
|
|
216
|
+
if (((_a = this.segmentationResults) == null ? void 0 : _a.categoryMask) && this.segmentationResults.categoryMask.width > 0) {
|
|
214
217
|
this.ctx.globalCompositeOperation = "copy";
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
218
|
+
this.ctx.putImageData(
|
|
219
|
+
maskToImageData(
|
|
220
|
+
this.segmentationResults.categoryMask,
|
|
221
|
+
this.segmentationResults.categoryMask.width,
|
|
222
|
+
this.segmentationResults.categoryMask.height
|
|
223
|
+
),
|
|
224
|
+
0,
|
|
225
|
+
0
|
|
219
226
|
);
|
|
220
|
-
this.ctx.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);
|
|
221
227
|
this.ctx.filter = "none";
|
|
222
228
|
this.ctx.globalCompositeOperation = "source-in";
|
|
223
229
|
if (this.backgroundImage) {
|
|
@@ -241,40 +247,43 @@ var BackgroundProcessor = class extends VideoTransformer {
|
|
|
241
247
|
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
242
248
|
}
|
|
243
249
|
async blurBackground(frame) {
|
|
244
|
-
var _a, _b;
|
|
250
|
+
var _a, _b, _c;
|
|
245
251
|
if (!this.ctx || !this.canvas || !((_b = (_a = this.segmentationResults) == null ? void 0 : _a.categoryMask) == null ? void 0 : _b.canvas) || !this.inputVideo) {
|
|
246
252
|
return;
|
|
247
253
|
}
|
|
248
254
|
this.ctx.save();
|
|
249
255
|
this.ctx.globalCompositeOperation = "copy";
|
|
250
|
-
|
|
251
|
-
this.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
256
|
+
if (((_c = this.segmentationResults) == null ? void 0 : _c.categoryMask) && this.segmentationResults.categoryMask.width > 0) {
|
|
257
|
+
this.ctx.putImageData(
|
|
258
|
+
maskToImageData(
|
|
259
|
+
this.segmentationResults.categoryMask,
|
|
260
|
+
this.segmentationResults.categoryMask.width,
|
|
261
|
+
this.segmentationResults.categoryMask.height
|
|
262
|
+
),
|
|
263
|
+
0,
|
|
264
|
+
0
|
|
265
|
+
);
|
|
266
|
+
this.ctx.filter = "none";
|
|
267
|
+
this.ctx.globalCompositeOperation = "source-out";
|
|
268
|
+
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
269
|
+
this.ctx.globalCompositeOperation = "destination-over";
|
|
270
|
+
this.ctx.filter = `blur(${this.blurRadius}px)`;
|
|
271
|
+
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
272
|
+
this.ctx.restore();
|
|
273
|
+
}
|
|
265
274
|
}
|
|
266
275
|
};
|
|
267
|
-
function
|
|
276
|
+
function maskToImageData(mask, videoWidth, videoHeight) {
|
|
268
277
|
const dataArray = new Uint8ClampedArray(videoWidth * videoHeight * 4);
|
|
269
278
|
const result = mask.getAsUint8Array();
|
|
270
279
|
for (let i = 0; i < result.length; i += 1) {
|
|
271
|
-
|
|
272
|
-
dataArray[
|
|
273
|
-
dataArray[
|
|
274
|
-
dataArray[
|
|
280
|
+
const offset = i * 4;
|
|
281
|
+
dataArray[offset] = result[i];
|
|
282
|
+
dataArray[offset + 1] = result[i];
|
|
283
|
+
dataArray[offset + 2] = result[i];
|
|
284
|
+
dataArray[offset + 3] = result[i];
|
|
275
285
|
}
|
|
276
|
-
|
|
277
|
-
return createImageBitmap(dataNew);
|
|
286
|
+
return new ImageData(dataArray, videoWidth, videoHeight);
|
|
278
287
|
}
|
|
279
288
|
|
|
280
289
|
// src/index.ts
|
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 }\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\n this.sourceSettings = this.source.getSettings();\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 = this.sourceSettings.height ?? 300;\n this.sourceDummy.width = this.sourceSettings.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(\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 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) {\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.3\",\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\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\n this.ctx.putImageData(\n maskToImageData(\n this.segmentationResults.categoryMask,\n this.segmentationResults.categoryMask.width,\n this.segmentationResults.categoryMask.height,\n ),\n 0,\n 0,\n );\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 }\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 this.ctx.putImageData(\n maskToImageData(\n this.segmentationResults.categoryMask,\n this.segmentationResults.categoryMask.width,\n this.segmentationResults.categoryMask.height,\n ),\n 0,\n 0,\n );\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 maskToImageData(mask: vision.MPMask, videoWidth: number, videoHeight: number): ImageData {\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 const offset = i * 4;\n dataArray[offset] = result[i];\n dataArray[offset + 1] = result[i];\n dataArray[offset + 2] = result[i];\n dataArray[offset + 3] = result[i];\n }\n return new ImageData(dataArray, videoWidth, videoHeight);\n}\n","{\n \"name\": \"@livekit/track-processors\",\n \"version\": \"0.4.1\",\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,3 +1,21 @@
|
|
|
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 };
|
|
13
|
+
}
|
|
14
|
+
await sleep(50);
|
|
15
|
+
}
|
|
16
|
+
return { width: void 0, height: void 0 };
|
|
17
|
+
}
|
|
18
|
+
|
|
1
19
|
// src/ProcessorWrapper.ts
|
|
2
20
|
var ProcessorWrapper = class {
|
|
3
21
|
static get isSupported() {
|
|
@@ -8,35 +26,22 @@ var ProcessorWrapper = class {
|
|
|
8
26
|
this.transformer = transformer;
|
|
9
27
|
}
|
|
10
28
|
async setup(opts) {
|
|
11
|
-
var _a, _b, _c, _d;
|
|
12
29
|
this.source = opts.track;
|
|
13
|
-
const
|
|
14
|
-
await this.source.applyConstraints({
|
|
15
|
-
...origConstraints,
|
|
16
|
-
// @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
|
|
17
|
-
// this leads to a shift of the underlying video as the frame itself is being rendered with the coded size
|
|
18
|
-
// but image segmentation is based on the display dimensions (-> the cropped version)
|
|
19
|
-
// in order to prevent this, we force the resize mode to "none"
|
|
20
|
-
resizeMode: "none"
|
|
21
|
-
});
|
|
22
|
-
this.sourceSettings = this.source.getSettings();
|
|
30
|
+
const { width, height } = await waitForTrackResolution(this.source);
|
|
23
31
|
this.sourceDummy = opts.element;
|
|
24
32
|
if (!(this.sourceDummy instanceof HTMLVideoElement)) {
|
|
25
33
|
throw TypeError("Currently only video transformers are supported");
|
|
26
34
|
}
|
|
27
35
|
if (this.sourceDummy instanceof HTMLVideoElement) {
|
|
28
|
-
this.sourceDummy.height =
|
|
29
|
-
this.sourceDummy.width =
|
|
36
|
+
this.sourceDummy.height = height != null ? height : 300;
|
|
37
|
+
this.sourceDummy.width = width != null ? width : 300;
|
|
30
38
|
}
|
|
31
39
|
this.processor = new MediaStreamTrackProcessor({ track: this.source });
|
|
32
40
|
this.trackGenerator = new MediaStreamTrackGenerator({
|
|
33
41
|
kind: "video",
|
|
34
42
|
signalTarget: this.source
|
|
35
43
|
});
|
|
36
|
-
this.canvas = new OffscreenCanvas(
|
|
37
|
-
(_c = this.sourceSettings.width) != null ? _c : 300,
|
|
38
|
-
(_d = this.sourceSettings.height) != null ? _d : 300
|
|
39
|
-
);
|
|
44
|
+
this.canvas = new OffscreenCanvas(width != null ? width : 300, height != null ? height : 300);
|
|
40
45
|
}
|
|
41
46
|
async init(opts) {
|
|
42
47
|
await this.setup(opts);
|
|
@@ -74,8 +79,7 @@ import * as vision from "@mediapipe/tasks-vision";
|
|
|
74
79
|
|
|
75
80
|
// package.json
|
|
76
81
|
var dependencies = {
|
|
77
|
-
"@mediapipe/
|
|
78
|
-
"@mediapipe/tasks-vision": "0.10.9"
|
|
82
|
+
"@mediapipe/tasks-vision": "0.10.21"
|
|
79
83
|
};
|
|
80
84
|
|
|
81
85
|
// src/transformers/VideoTransformer.ts
|
|
@@ -209,15 +213,17 @@ var BackgroundProcessor = class extends VideoTransformer {
|
|
|
209
213
|
var _a;
|
|
210
214
|
if (!this.canvas || !this.ctx || !this.segmentationResults || !this.inputVideo)
|
|
211
215
|
return;
|
|
212
|
-
if ((_a = this.segmentationResults) == null ? void 0 : _a.categoryMask) {
|
|
213
|
-
this.ctx.filter = "blur(10px)";
|
|
216
|
+
if (((_a = this.segmentationResults) == null ? void 0 : _a.categoryMask) && this.segmentationResults.categoryMask.width > 0) {
|
|
214
217
|
this.ctx.globalCompositeOperation = "copy";
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
218
|
+
this.ctx.putImageData(
|
|
219
|
+
maskToImageData(
|
|
220
|
+
this.segmentationResults.categoryMask,
|
|
221
|
+
this.segmentationResults.categoryMask.width,
|
|
222
|
+
this.segmentationResults.categoryMask.height
|
|
223
|
+
),
|
|
224
|
+
0,
|
|
225
|
+
0
|
|
219
226
|
);
|
|
220
|
-
this.ctx.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);
|
|
221
227
|
this.ctx.filter = "none";
|
|
222
228
|
this.ctx.globalCompositeOperation = "source-in";
|
|
223
229
|
if (this.backgroundImage) {
|
|
@@ -241,40 +247,43 @@ var BackgroundProcessor = class extends VideoTransformer {
|
|
|
241
247
|
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
242
248
|
}
|
|
243
249
|
async blurBackground(frame) {
|
|
244
|
-
var _a, _b;
|
|
250
|
+
var _a, _b, _c;
|
|
245
251
|
if (!this.ctx || !this.canvas || !((_b = (_a = this.segmentationResults) == null ? void 0 : _a.categoryMask) == null ? void 0 : _b.canvas) || !this.inputVideo) {
|
|
246
252
|
return;
|
|
247
253
|
}
|
|
248
254
|
this.ctx.save();
|
|
249
255
|
this.ctx.globalCompositeOperation = "copy";
|
|
250
|
-
|
|
251
|
-
this.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
256
|
+
if (((_c = this.segmentationResults) == null ? void 0 : _c.categoryMask) && this.segmentationResults.categoryMask.width > 0) {
|
|
257
|
+
this.ctx.putImageData(
|
|
258
|
+
maskToImageData(
|
|
259
|
+
this.segmentationResults.categoryMask,
|
|
260
|
+
this.segmentationResults.categoryMask.width,
|
|
261
|
+
this.segmentationResults.categoryMask.height
|
|
262
|
+
),
|
|
263
|
+
0,
|
|
264
|
+
0
|
|
265
|
+
);
|
|
266
|
+
this.ctx.filter = "none";
|
|
267
|
+
this.ctx.globalCompositeOperation = "source-out";
|
|
268
|
+
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
269
|
+
this.ctx.globalCompositeOperation = "destination-over";
|
|
270
|
+
this.ctx.filter = `blur(${this.blurRadius}px)`;
|
|
271
|
+
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
272
|
+
this.ctx.restore();
|
|
273
|
+
}
|
|
265
274
|
}
|
|
266
275
|
};
|
|
267
|
-
function
|
|
276
|
+
function maskToImageData(mask, videoWidth, videoHeight) {
|
|
268
277
|
const dataArray = new Uint8ClampedArray(videoWidth * videoHeight * 4);
|
|
269
278
|
const result = mask.getAsUint8Array();
|
|
270
279
|
for (let i = 0; i < result.length; i += 1) {
|
|
271
|
-
|
|
272
|
-
dataArray[
|
|
273
|
-
dataArray[
|
|
274
|
-
dataArray[
|
|
280
|
+
const offset = i * 4;
|
|
281
|
+
dataArray[offset] = result[i];
|
|
282
|
+
dataArray[offset + 1] = result[i];
|
|
283
|
+
dataArray[offset + 2] = result[i];
|
|
284
|
+
dataArray[offset + 3] = result[i];
|
|
275
285
|
}
|
|
276
|
-
|
|
277
|
-
return createImageBitmap(dataNew);
|
|
286
|
+
return new ImageData(dataArray, videoWidth, videoHeight);
|
|
278
287
|
}
|
|
279
288
|
|
|
280
289
|
// src/index.ts
|
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 }\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\n this.sourceSettings = this.source.getSettings();\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 = this.sourceSettings.height ?? 300;\n this.sourceDummy.width = this.sourceSettings.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(\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 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) {\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.3\",\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\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;AAAA,EACrB;AAAA,EAEA,MAAc,MAAM,MAAoC;AApC1D;AAqCI,SAAK,SAAS,KAAK;AACnB,UAAM,kBAAkB,KAAK,OAAO,eAAe;AACnD,UAAM,KAAK,OAAO,iBAAiB;AAAA,MACjC,GAAG;AAAA;AAAA;AAAA;AAAA;AAAA,MAKH,YAAY;AAAA,IACd,CAAC;AAED,SAAK,iBAAiB,KAAK,OAAO,YAAY;AAC9C,SAAK,cAAc,KAAK;AAExB,QAAI,EAAE,KAAK,uBAAuB,mBAAmB;AACnD,YAAM,UAAU,iDAAiD;AAAA,IACnE;AAEA,QAAI,KAAK,uBAAuB,kBAAkB;AAChD,WAAK,YAAY,UAAS,UAAK,eAAe,WAApB,YAA8B;AACxD,WAAK,YAAY,SAAQ,UAAK,eAAe,UAApB,YAA6B;AAAA,IACxD;AAGA,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,UAAM,iBAAiB,KAAK,UAAU;AAEtC,UAAM,KAAK,YAAY,KAAK;AAAA,MAC1B,cAAc,KAAK;AAAA,MACnB,cAAc,KAAK;AAAA,IACrB,CAAC;AAED,UAAM,cAAc,eAAe,YAAY,KAAK,YAAa,WAAY;AAE7E,gBACG,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;AA9GlB;AA+GI,UAAM,KAAK,YAAY,QAAQ;AAC/B,eAAK,mBAAL,mBAAqB;AAAA,EACvB;AACF;;;AClHA,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;AAEA,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;;;AFtCA,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,QACV,GAAG,KAAK,QAAQ;AAAA,MAClB;AAAA,MACA,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;AAAA,QAAM,CAAC,QACvD,QAAQ,MAAM,oDAAoD,GAAG;AAAA,MACvE;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,UAAU;AAlElB;AAmEI,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;AArF/F;AAsFI,QAAI;AACF,UAAI,EAAE,iBAAiB,aAAa;AAClC,gBAAQ,MAAM,gCAAgC;AAC9C;AAAA,MACF;AACA,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,qCAAO;AAAA,IACT;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;AAhIjD;AAiII,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;AAtK1C;AAuKI,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;;;AG5MO,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\n this.ctx.putImageData(\n maskToImageData(\n this.segmentationResults.categoryMask,\n this.segmentationResults.categoryMask.width,\n this.segmentationResults.categoryMask.height,\n ),\n 0,\n 0,\n );\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 }\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 this.ctx.putImageData(\n maskToImageData(\n this.segmentationResults.categoryMask,\n this.segmentationResults.categoryMask.width,\n this.segmentationResults.categoryMask.height,\n ),\n 0,\n 0,\n );\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 maskToImageData(mask: vision.MPMask, videoWidth: number, videoHeight: number): ImageData {\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 const offset = i * 4;\n dataArray[offset] = result[i];\n dataArray[offset + 1] = result[i];\n dataArray[offset + 2] = result[i];\n dataArray[offset + 3] = result[i];\n }\n return new ImageData(dataArray, videoWidth, videoHeight);\n}\n","{\n \"name\": \"@livekit/track-processors\",\n \"version\": \"0.4.1\",\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.1",
|
|
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>;
|
|
@@ -36,17 +35,8 @@ export default class ProcessorWrapper<TransformerOptions extends Record<string,
|
|
|
36
35
|
|
|
37
36
|
private async setup(opts: ProcessorOptions<Track.Kind>) {
|
|
38
37
|
this.source = opts.track as MediaStreamVideoTrack;
|
|
39
|
-
const origConstraints = this.source.getConstraints();
|
|
40
|
-
await this.source.applyConstraints({
|
|
41
|
-
...origConstraints,
|
|
42
|
-
// @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
|
|
43
|
-
// this leads to a shift of the underlying video as the frame itself is being rendered with the coded size
|
|
44
|
-
// but image segmentation is based on the display dimensions (-> the cropped version)
|
|
45
|
-
// in order to prevent this, we force the resize mode to "none"
|
|
46
|
-
resizeMode: 'none',
|
|
47
|
-
});
|
|
48
38
|
|
|
49
|
-
|
|
39
|
+
const { width, height } = await waitForTrackResolution(this.source);
|
|
50
40
|
this.sourceDummy = opts.element;
|
|
51
41
|
|
|
52
42
|
if (!(this.sourceDummy instanceof HTMLVideoElement)) {
|
|
@@ -54,8 +44,8 @@ export default class ProcessorWrapper<TransformerOptions extends Record<string,
|
|
|
54
44
|
}
|
|
55
45
|
|
|
56
46
|
if (this.sourceDummy instanceof HTMLVideoElement) {
|
|
57
|
-
this.sourceDummy.height =
|
|
58
|
-
this.sourceDummy.width =
|
|
47
|
+
this.sourceDummy.height = height ?? 300;
|
|
48
|
+
this.sourceDummy.width = width ?? 300;
|
|
59
49
|
}
|
|
60
50
|
|
|
61
51
|
// TODO explore if we can do all the processing work in a webworker
|
|
@@ -66,10 +56,7 @@ export default class ProcessorWrapper<TransformerOptions extends Record<string,
|
|
|
66
56
|
signalTarget: this.source,
|
|
67
57
|
});
|
|
68
58
|
|
|
69
|
-
this.canvas = new OffscreenCanvas(
|
|
70
|
-
this.sourceSettings.width ?? 300,
|
|
71
|
-
this.sourceSettings.height ?? 300,
|
|
72
|
-
);
|
|
59
|
+
this.canvas = new OffscreenCanvas(width ?? 300, height ?? 300);
|
|
73
60
|
}
|
|
74
61
|
|
|
75
62
|
async init(opts: ProcessorOptions<Track.Kind>) {
|
|
@@ -130,15 +130,18 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
|
|
|
130
130
|
if (!this.canvas || !this.ctx || !this.segmentationResults || !this.inputVideo) return;
|
|
131
131
|
// this.ctx.save();
|
|
132
132
|
// this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
133
|
-
if (this.segmentationResults?.categoryMask) {
|
|
134
|
-
this.ctx.filter = 'blur(10px)';
|
|
133
|
+
if (this.segmentationResults?.categoryMask && this.segmentationResults.categoryMask.width > 0) {
|
|
135
134
|
this.ctx.globalCompositeOperation = 'copy';
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
|
|
136
|
+
this.ctx.putImageData(
|
|
137
|
+
maskToImageData(
|
|
138
|
+
this.segmentationResults.categoryMask,
|
|
139
|
+
this.segmentationResults.categoryMask.width,
|
|
140
|
+
this.segmentationResults.categoryMask.height,
|
|
141
|
+
),
|
|
142
|
+
0,
|
|
143
|
+
0,
|
|
140
144
|
);
|
|
141
|
-
this.ctx.drawImage(bitmap, 0, 0, this.canvas.width, this.canvas.height);
|
|
142
145
|
this.ctx.filter = 'none';
|
|
143
146
|
this.ctx.globalCompositeOperation = 'source-in';
|
|
144
147
|
if (this.backgroundImage) {
|
|
@@ -161,7 +164,6 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
|
|
|
161
164
|
this.ctx.globalCompositeOperation = 'destination-over';
|
|
162
165
|
}
|
|
163
166
|
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
164
|
-
// this.ctx.restore();
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
async blurBackground(frame: VideoFrame) {
|
|
@@ -177,39 +179,36 @@ export default class BackgroundProcessor extends VideoTransformer<BackgroundOpti
|
|
|
177
179
|
this.ctx.save();
|
|
178
180
|
this.ctx.globalCompositeOperation = 'copy';
|
|
179
181
|
|
|
180
|
-
|
|
181
|
-
this.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
182
|
+
if (this.segmentationResults?.categoryMask && this.segmentationResults.categoryMask.width > 0) {
|
|
183
|
+
this.ctx.putImageData(
|
|
184
|
+
maskToImageData(
|
|
185
|
+
this.segmentationResults.categoryMask,
|
|
186
|
+
this.segmentationResults.categoryMask.width,
|
|
187
|
+
this.segmentationResults.categoryMask.height,
|
|
188
|
+
),
|
|
189
|
+
0,
|
|
190
|
+
0,
|
|
191
|
+
);
|
|
192
|
+
this.ctx.filter = 'none';
|
|
193
|
+
this.ctx.globalCompositeOperation = 'source-out';
|
|
194
|
+
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
195
|
+
this.ctx.globalCompositeOperation = 'destination-over';
|
|
196
|
+
this.ctx.filter = `blur(${this.blurRadius}px)`;
|
|
197
|
+
this.ctx.drawImage(frame, 0, 0, this.canvas.width, this.canvas.height);
|
|
198
|
+
this.ctx.restore();
|
|
199
|
+
}
|
|
196
200
|
}
|
|
197
201
|
}
|
|
198
202
|
|
|
199
|
-
function
|
|
200
|
-
mask: vision.MPMask,
|
|
201
|
-
videoWidth: number,
|
|
202
|
-
videoHeight: number,
|
|
203
|
-
): Promise<ImageBitmap> {
|
|
203
|
+
function maskToImageData(mask: vision.MPMask, videoWidth: number, videoHeight: number): ImageData {
|
|
204
204
|
const dataArray: Uint8ClampedArray = new Uint8ClampedArray(videoWidth * videoHeight * 4);
|
|
205
205
|
const result = mask.getAsUint8Array();
|
|
206
206
|
for (let i = 0; i < result.length; i += 1) {
|
|
207
|
-
|
|
208
|
-
dataArray[
|
|
209
|
-
dataArray[
|
|
210
|
-
dataArray[
|
|
207
|
+
const offset = i * 4;
|
|
208
|
+
dataArray[offset] = result[i];
|
|
209
|
+
dataArray[offset + 1] = result[i];
|
|
210
|
+
dataArray[offset + 2] = result[i];
|
|
211
|
+
dataArray[offset + 3] = result[i];
|
|
211
212
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return createImageBitmap(dataNew);
|
|
213
|
+
return new ImageData(dataArray, videoWidth, videoHeight);
|
|
215
214
|
}
|
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
|
-
}
|