@pexip/media-processor 22.2.0 → 22.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +34 -0
  3. package/api-docs/README.mdx +37 -0
  4. package/api-docs/interfaces/ProcessorEvent.mdx +1 -0
  5. package/api-docs/interfaces/ProcessorOptions.mdx +10 -9
  6. package/api-docs/interfaces/ProcessorUpdateOptions.mdx +10 -9
  7. package/api-docs/interfaces/SegmenterOptions.mdx +7 -6
  8. package/api-docs/interfaces/TensorMetadata.mdx +11 -0
  9. package/api-docs/type-aliases/TensorLayout.mdx +3 -0
  10. package/api-docs/variables/getTensorMetadata.mdx +14 -0
  11. package/dist/common/backends/canvas2d/postprocessor.d.ts +30 -0
  12. package/dist/common/backends/canvas2d/postprocessor.js +88 -0
  13. package/dist/common/backends/canvas2d/preprocessor.d.ts +56 -0
  14. package/dist/common/backends/canvas2d/preprocessor.js +139 -0
  15. package/dist/common/backends/webgpu/renderer.js +1 -0
  16. package/dist/common/backends/webgpu/webgpuUtils.d.ts +35 -0
  17. package/dist/common/backends/webgpu/webgpuUtils.js +84 -2
  18. package/dist/common/inferencer.d.ts +12 -3
  19. package/dist/common/inferencer.js +60 -14
  20. package/dist/common/tsconfig.tsbuildinfo +1 -1
  21. package/dist/common/types/processor.d.ts +1 -0
  22. package/dist/common/types/segmentation.d.ts +8 -0
  23. package/dist/common/utils.d.ts +12 -0
  24. package/dist/common/utils.js +43 -0
  25. package/dist/main/tsconfig.tsbuildinfo +1 -1
  26. package/dist/main/video/segmenter.d.ts +4 -2
  27. package/dist/main/video/segmenter.js +36 -11
  28. package/dist/main/video/video.js +10 -0
  29. package/dist/workers/mediaWorker.js +23 -23
  30. package/dist/workers/tsconfig.tsbuildinfo +1 -1
  31. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 22.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2f3c6b7: Add pure CPU processing capability
8
+
9
+ ### Patch Changes
10
+
11
+ - b666166: Shares one WebGPU adapter and device across the renderers, and
12
+ destroys the device when the processor is destroyed. One of each was requested
13
+ for every renderer, so each rebuild of it, on a change of the processing
14
+ dimensions or the backend or on a recovery from a lost context, left the
15
+ memory of the pipelines and the shader modules of the previous one allocated.
16
+ Rebuilding a renderer no longer waits for a device either.
17
+ - 53af4ef: Relinquishes the WebGL context of the processing canvases when they
18
+ are replaced. A context is bound to its canvas for life, so restarting the
19
+ video processing repeatedly, e.g. by changing the effect, could exhaust the
20
+ contexts available to the page, at which point the browser drops the oldest
21
+ ones and the video feed stops.
22
+
3
23
  ## 22.2.0
4
24
 
5
25
  ### Minor Changes
package/README.md CHANGED
@@ -178,6 +178,40 @@ You can do it with chrome, [See here][profiling web audio].
178
178
  - No `fetch` API in the `AudioWorkletGlobalScope`
179
179
  - No `TextEncoder/Decoder` APIs in the `AudioWorkletGlobalScope`
180
180
 
181
+ ## CPU pipeline (LiteRT + Canvas2D)
182
+
183
+ By default the media processor uses MediaPipe with a WebGL or WebGPU delegate
184
+ for GPU-accelerated background segmentation. On devices without GPU support, or
185
+ when you explicitly want a pure-CPU path, you can switch to the LiteRT inference
186
+ pipeline.
187
+
188
+ Provide `litertjsCoreBasePath` instead of (or in addition to) `basePath` when
189
+ opening the processor. Its presence is the switch that selects the LiteRT
190
+ pipeline:
191
+
192
+ ```typescript
193
+ processor.open({
194
+ litertjsCoreBasePath: urls.litertjsCoreBasePath, // path to @litertjs/core/wasm/
195
+ imageSegmenterOptions: {
196
+ modelAsset: {
197
+ path: urls.selfieModelPath, // path to a .tflite selfie model
198
+ modelName: 'selfie',
199
+ },
200
+ },
201
+ });
202
+ ```
203
+
204
+ The `@litertjs/core` WASM files are bundled inside `@pexip/media-assets` under
205
+ `@litertjs/core/wasm/`. Point `litertjsCoreBasePath` at this directory relative
206
+ to your web root.
207
+
208
+ ### Supported models
209
+
210
+ | Pipeline | Models | Renderer |
211
+ | ------------------- | --------------------- | ------------- |
212
+ | MediaPipe (default) | `selfie`, `deeplabV3` | WebGL, WebGPU |
213
+ | LiteRT CPU | `selfie` | Canvas2D |
214
+
181
215
  ## References
182
216
 
183
217
  - [MDN - Web Audio API][web audio api]
@@ -178,6 +178,40 @@ You can do it with chrome, [See here][profiling web audio].
178
178
  - No `fetch` API in the `AudioWorkletGlobalScope`
179
179
  - No `TextEncoder/Decoder` APIs in the `AudioWorkletGlobalScope`
180
180
 
181
+ ## CPU pipeline (LiteRT + Canvas2D)
182
+
183
+ By default the media processor uses MediaPipe with a WebGL or WebGPU delegate
184
+ for GPU-accelerated background segmentation. On devices without GPU support, or
185
+ when you explicitly want a pure-CPU path, you can switch to the LiteRT inference
186
+ pipeline.
187
+
188
+ Provide `litertjsCoreBasePath` instead of (or in addition to) `basePath` when
189
+ opening the processor. Its presence is the switch that selects the LiteRT
190
+ pipeline:
191
+
192
+ ```typescript
193
+ processor.open({
194
+ litertjsCoreBasePath: urls.litertjsCoreBasePath, // path to @litertjs/core/wasm/
195
+ imageSegmenterOptions: {
196
+ modelAsset: {
197
+ path: urls.selfieModelPath, // path to a .tflite selfie model
198
+ modelName: 'selfie',
199
+ },
200
+ },
201
+ });
202
+ ```
203
+
204
+ The `@litertjs/core` WASM files are bundled inside `@pexip/media-assets` under
205
+ `@litertjs/core/wasm/`. Point `litertjsCoreBasePath` at this directory relative
206
+ to your web root.
207
+
208
+ ### Supported models
209
+
210
+ | Pipeline | Models | Renderer |
211
+ | ------------------- | --------------------- | ------------- |
212
+ | MediaPipe (default) | `selfie`, `deeplabV3` | WebGL, WebGPU |
213
+ | LiteRT CPU | `selfie` | Canvas2D |
214
+
181
215
  ## References
182
216
 
183
217
  - [MDN - Web Audio API][web audio api]
@@ -234,6 +268,7 @@ A library for media analysis using Web APIs.
234
268
  | [Stats](interfaces/Stats.mdx) | - |
235
269
  | [Weights](interfaces/Weights.mdx) | - |
236
270
  | [SelectionOptions](interfaces/SelectionOptions.mdx) | - |
271
+ | [TensorMetadata](interfaces/TensorMetadata.mdx) | - |
237
272
  | [Benchmark](interfaces/Benchmark.mdx) | - |
238
273
  | [Point](interfaces/Point.mdx) | Interface for Point consist of coordinates x and y |
239
274
  | [Size](interfaces/Size.mdx) | - |
@@ -284,6 +319,7 @@ A library for media analysis using Web APIs.
284
319
  | [IncludeMessageEventDataType](type-aliases/IncludeMessageEventDataType.mdx) | - |
285
320
  | [TupleOf](type-aliases/TupleOf.mdx) | From https://github.com/Microsoft/TypeScript/issues/26223#issuecomment-674500430 |
286
321
  | [OptionalKeys](type-aliases/OptionalKeys.mdx) | - |
322
+ | [TensorLayout](type-aliases/TensorLayout.mdx) | - |
287
323
  | [Canvas](type-aliases/Canvas.mdx) | - |
288
324
  | [Unsubscribe](type-aliases/Unsubscribe.mdx) | Unsubscribe the subscription |
289
325
  | [AudioBufferFloats](type-aliases/AudioBufferFloats.mdx) | Same as [AudioBuffer](https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer) Or the return from [AnalyserNode.getFloatFrequencyData()](https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/getFloatFrequencyData) |
@@ -372,6 +408,7 @@ A library for media analysis using Web APIs.
372
408
  | [cloneImageRecord](variables/cloneImageRecord.mdx) | - |
373
409
  | [compareImageRecords](variables/compareImageRecords.mdx) | Compare provided ImageRecord |
374
410
  | [getCanUseWebGL](variables/getCanUseWebGL.mdx) | - |
411
+ | [getTensorMetadata](variables/getTensorMetadata.mdx) | - |
375
412
  | [urls](variables/urls.mdx) | - |
376
413
  | [SILENT\_THRESHOLD](variables/SILENT_THRESHOLD.mdx) | Default silent threshold At least one LSB 16-bit data (compare is on absolute value). |
377
414
  | [MONO\_THRESHOLD](variables/MONO_THRESHOLD.mdx) | Default mono detection threshold Data must be identical within one LSB 16-bit to be identified as mono. |
@@ -6,3 +6,4 @@
6
6
  | <a id="error"></a> `error?` | `Error` |
7
7
  | <a id="message"></a> `message?` | `string` |
8
8
  | <a id="track"></a> `track?` | `MediaStreamTrack` |
9
+ | <a id="recoverable"></a> `recoverable?` | `boolean` |
@@ -4,12 +4,13 @@
4
4
 
5
5
  ## Properties
6
6
 
7
- | Property | Type | Inherited from |
8
- | ------ | ------ | ------ |
9
- | <a id="renderoptions"></a> `renderOptions?` | [`RendererOptions`](RendererOptions.mdx) | - |
10
- | <a id="backgroundimage"></a> `backgroundImage?` | [`ImageRecord`](ImageRecord.mdx) | - |
11
- | <a id="restart"></a> `restart?` | `boolean` | - |
12
- | <a id="basepath"></a> `basePath?` | `string` | [`SegmenterOptions`](SegmenterOptions.mdx).[`basePath`](SegmenterOptions.mdx#basepath) |
13
- | <a id="processingwidth"></a> `processingWidth?` | `number` | [`SegmenterOptions`](SegmenterOptions.mdx).[`processingWidth`](SegmenterOptions.mdx#processingwidth) |
14
- | <a id="processingheight"></a> `processingHeight?` | `number` | [`SegmenterOptions`](SegmenterOptions.mdx).[`processingHeight`](SegmenterOptions.mdx#processingheight) |
15
- | <a id="imagesegmenteroptions"></a> `imageSegmenterOptions?` | [`ImageSegmenterOptions`](ImageSegmenterOptions.mdx) | [`SegmenterOptions`](SegmenterOptions.mdx).[`imageSegmenterOptions`](SegmenterOptions.mdx#imagesegmenteroptions) |
7
+ | Property | Type | Description | Inherited from |
8
+ | ------ | ------ | ------ | ------ |
9
+ | <a id="renderoptions"></a> `renderOptions?` | [`RendererOptions`](RendererOptions.mdx) | - | - |
10
+ | <a id="backgroundimage"></a> `backgroundImage?` | [`ImageRecord`](ImageRecord.mdx) | - | - |
11
+ | <a id="restart"></a> `restart?` | `boolean` | - | - |
12
+ | <a id="basepath"></a> `basePath?` | `string` | MediaPipe fileset base URL. | [`SegmenterOptions`](SegmenterOptions.mdx).[`basePath`](SegmenterOptions.mdx#basepath) |
13
+ | <a id="litertjscorebasepath"></a> `litertjsCoreBasePath?` | `string` | LiteRT WASM base URL. Providing this selects the CPU inference pipeline (LiteRT + Canvas2D) instead of the default MediaPipe GPU pipeline. | [`SegmenterOptions`](SegmenterOptions.mdx).[`litertjsCoreBasePath`](SegmenterOptions.mdx#litertjscorebasepath) |
14
+ | <a id="processingwidth"></a> `processingWidth?` | `number` | - | [`SegmenterOptions`](SegmenterOptions.mdx).[`processingWidth`](SegmenterOptions.mdx#processingwidth) |
15
+ | <a id="processingheight"></a> `processingHeight?` | `number` | - | [`SegmenterOptions`](SegmenterOptions.mdx).[`processingHeight`](SegmenterOptions.mdx#processingheight) |
16
+ | <a id="imagesegmenteroptions"></a> `imageSegmenterOptions?` | [`ImageSegmenterOptions`](ImageSegmenterOptions.mdx) | - | [`SegmenterOptions`](SegmenterOptions.mdx).[`imageSegmenterOptions`](SegmenterOptions.mdx#imagesegmenteroptions) |
@@ -6,12 +6,13 @@
6
6
 
7
7
  ## Properties
8
8
 
9
- | Property | Type | Inherited from |
10
- | ------ | ------ | ------ |
11
- | <a id="backgroundimage"></a> `backgroundImage?` | [`ImageRecord`](ImageRecord.mdx) | [`ProcessorOptions`](ProcessorOptions.mdx).[`backgroundImage`](ProcessorOptions.mdx#backgroundimage) |
12
- | <a id="restart"></a> `restart?` | `boolean` | [`ProcessorOptions`](ProcessorOptions.mdx).[`restart`](ProcessorOptions.mdx#restart) |
13
- | <a id="basepath"></a> `basePath?` | `string` | [`SegmenterOptions`](SegmenterOptions.mdx).[`basePath`](SegmenterOptions.mdx#basepath) |
14
- | <a id="processingwidth"></a> `processingWidth?` | `number` | [`SegmenterOptions`](SegmenterOptions.mdx).[`processingWidth`](SegmenterOptions.mdx#processingwidth) |
15
- | <a id="processingheight"></a> `processingHeight?` | `number` | [`SegmenterOptions`](SegmenterOptions.mdx).[`processingHeight`](SegmenterOptions.mdx#processingheight) |
16
- | <a id="imagesegmenteroptions"></a> `imageSegmenterOptions?` | [`ImageSegmenterOptions`](ImageSegmenterOptions.mdx) | [`SegmenterOptions`](SegmenterOptions.mdx).[`imageSegmenterOptions`](SegmenterOptions.mdx#imagesegmenteroptions) |
17
- | <a id="renderoptions"></a> `renderOptions?` | `Partial`\<[`RendererOptions`](RendererOptions.mdx)\> | - |
9
+ | Property | Type | Description | Inherited from |
10
+ | ------ | ------ | ------ | ------ |
11
+ | <a id="backgroundimage"></a> `backgroundImage?` | [`ImageRecord`](ImageRecord.mdx) | - | [`ProcessorOptions`](ProcessorOptions.mdx).[`backgroundImage`](ProcessorOptions.mdx#backgroundimage) |
12
+ | <a id="restart"></a> `restart?` | `boolean` | - | [`ProcessorOptions`](ProcessorOptions.mdx).[`restart`](ProcessorOptions.mdx#restart) |
13
+ | <a id="basepath"></a> `basePath?` | `string` | MediaPipe fileset base URL. | [`SegmenterOptions`](SegmenterOptions.mdx).[`basePath`](SegmenterOptions.mdx#basepath) |
14
+ | <a id="litertjscorebasepath"></a> `litertjsCoreBasePath?` | `string` | LiteRT WASM base URL. Providing this selects the CPU inference pipeline (LiteRT + Canvas2D) instead of the default MediaPipe GPU pipeline. | [`SegmenterOptions`](SegmenterOptions.mdx).[`litertjsCoreBasePath`](SegmenterOptions.mdx#litertjscorebasepath) |
15
+ | <a id="processingwidth"></a> `processingWidth?` | `number` | - | [`SegmenterOptions`](SegmenterOptions.mdx).[`processingWidth`](SegmenterOptions.mdx#processingwidth) |
16
+ | <a id="processingheight"></a> `processingHeight?` | `number` | - | [`SegmenterOptions`](SegmenterOptions.mdx).[`processingHeight`](SegmenterOptions.mdx#processingheight) |
17
+ | <a id="imagesegmenteroptions"></a> `imageSegmenterOptions?` | [`ImageSegmenterOptions`](ImageSegmenterOptions.mdx) | - | [`SegmenterOptions`](SegmenterOptions.mdx).[`imageSegmenterOptions`](SegmenterOptions.mdx#imagesegmenteroptions) |
18
+ | <a id="renderoptions"></a> `renderOptions?` | `Partial`\<[`RendererOptions`](RendererOptions.mdx)\> | - | - |
@@ -4,9 +4,10 @@
4
4
 
5
5
  ## Properties
6
6
 
7
- | Property | Type |
8
- | ------ | ------ |
9
- | <a id="basepath"></a> `basePath?` | `string` |
10
- | <a id="processingwidth"></a> `processingWidth?` | `number` |
11
- | <a id="processingheight"></a> `processingHeight?` | `number` |
12
- | <a id="imagesegmenteroptions"></a> `imageSegmenterOptions?` | [`ImageSegmenterOptions`](ImageSegmenterOptions.mdx) |
7
+ | Property | Type | Description |
8
+ | ------ | ------ | ------ |
9
+ | <a id="basepath"></a> `basePath?` | `string` | MediaPipe fileset base URL. |
10
+ | <a id="litertjscorebasepath"></a> `litertjsCoreBasePath?` | `string` | LiteRT WASM base URL. Providing this selects the CPU inference pipeline (LiteRT + Canvas2D) instead of the default MediaPipe GPU pipeline. |
11
+ | <a id="processingwidth"></a> `processingWidth?` | `number` | - |
12
+ | <a id="processingheight"></a> `processingHeight?` | `number` | - |
13
+ | <a id="imagesegmenteroptions"></a> `imageSegmenterOptions?` | [`ImageSegmenterOptions`](ImageSegmenterOptions.mdx) | - |
@@ -0,0 +1,11 @@
1
+ ## Properties
2
+
3
+ | Property | Type |
4
+ | ------ | ------ |
5
+ | <a id="name"></a> `name` | `string` |
6
+ | <a id="shape"></a> `shape` | readonly `number[]` |
7
+ | <a id="width"></a> `width` | `number` |
8
+ | <a id="height"></a> `height` | `number` |
9
+ | <a id="layout"></a> `layout` | [`TensorLayout`](../type-aliases/TensorLayout.mdx) |
10
+ | <a id="dtype"></a> `dtype` | `"float32" \| "int32" \| "uint8"` |
11
+ | <a id="channels"></a> `channels` | `number` |
@@ -0,0 +1,3 @@
1
+ ```ts
2
+ type TensorLayout = "nhwc" | "nchw";
3
+ ```
@@ -0,0 +1,14 @@
1
+ ```ts
2
+ const getTensorMetadata: (details, supportedChannels?) => TensorMetadata;
3
+ ```
4
+
5
+ ## Parameters
6
+
7
+ | Parameter | Type |
8
+ | ------ | ------ |
9
+ | `details` | `TensorDetails` |
10
+ | `supportedChannels?` | readonly `number[]` |
11
+
12
+ ## Returns
13
+
14
+ [`TensorMetadata`](../interfaces/TensorMetadata.mdx)
@@ -0,0 +1,30 @@
1
+ import type { TypedArray, TensorDetails, Tensor } from '../../inferencer';
2
+ import type { TensorMetadata } from '../../utils';
3
+ /**
4
+ * Extract person confidence into an 8-bit mask.
5
+ *
6
+ * For two-channel outputs, background is category 0 and person is category 1.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * extractPersonConfidence(new Float32Array([0.1, 0.9]), mask, metadata);
11
+ * ```
12
+ */
13
+ export declare const extractPersonConfidence: (source: TypedArray, target: Uint8ClampedArray, metadata: TensorMetadata) => Uint8ClampedArray;
14
+ /**
15
+ * Create a reusable converter from LiteRT selfie output to a mask canvas.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * const output = createSelfieOutputProcessor(details, 256, 144);
20
+ * const maskCanvas = await output.process(inferenceOutputs);
21
+ * ```
22
+ */
23
+ export declare const createSelfieOutputProcessor: (output: TensorDetails, createCanvas?: (width: number, height: number) => OffscreenCanvas, createImageData?: (width: number, height: number) => ImageData) => {
24
+ metadata: TensorMetadata;
25
+ process: (inferenceOutputs: Record<string, Tensor>) => Promise<OffscreenCanvas>;
26
+ release: () => void;
27
+ readonly canvas: OffscreenCanvas | undefined;
28
+ readonly maskBuffer: Uint8ClampedArray<ArrayBufferLike> | undefined;
29
+ readonly imageData: ImageData | undefined;
30
+ };
@@ -0,0 +1,88 @@
1
+ import { getTensorMetadata } from '../../utils';
2
+ import { createOffscreenCanvas, getCanvasRenderingContext2D, } from '../../canvasRenderUtils';
3
+ /**
4
+ * Extract person confidence into an 8-bit mask.
5
+ *
6
+ * For two-channel outputs, background is category 0 and person is category 1.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * extractPersonConfidence(new Float32Array([0.1, 0.9]), mask, metadata);
11
+ * ```
12
+ */
13
+ export const extractPersonConfidence = (source, target, metadata) => {
14
+ const pixelCount = metadata.width * metadata.height;
15
+ if (target.length !== pixelCount ||
16
+ source.length !== pixelCount * metadata.channels) {
17
+ throw new Error('Output tensor and mask buffer sizes do not match');
18
+ }
19
+ const isFloat = source instanceof Float32Array;
20
+ for (let pixelIndex = 0; pixelIndex < pixelCount; pixelIndex++) {
21
+ const sourceIndex = metadata.layout === 'nhwc'
22
+ ? pixelIndex * metadata.channels + metadata.channels - 1
23
+ : (metadata.channels - 1) * pixelCount + pixelIndex;
24
+ const confidence = source[sourceIndex] ?? 0;
25
+ target[pixelIndex] = isFloat
26
+ ? Math.round(Math.min(1, Math.max(0, confidence)) * 255)
27
+ : confidence;
28
+ }
29
+ return target;
30
+ };
31
+ /**
32
+ * Create a reusable converter from LiteRT selfie output to a mask canvas.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * const output = createSelfieOutputProcessor(details, 256, 144);
37
+ * const maskCanvas = await output.process(inferenceOutputs);
38
+ * ```
39
+ */
40
+ export const createSelfieOutputProcessor = (output, createCanvas = (width, height) => createOffscreenCanvas(width, height), createImageData = (width, height) => new ImageData(width, height)) => {
41
+ const metadata = getTensorMetadata(output, [1, 2]);
42
+ let canvas = createCanvas(metadata.width, metadata.height);
43
+ let context = getCanvasRenderingContext2D(canvas);
44
+ let maskBuffer = new Uint8ClampedArray(metadata.width * metadata.height);
45
+ let imageData = createImageData(metadata.width, metadata.height);
46
+ let rgbaBuffer = imageData.data;
47
+ const process = async (inferenceOutputs) => {
48
+ if (!canvas || !context || !maskBuffer || !rgbaBuffer || !imageData) {
49
+ throw new Error('The output processor has been released');
50
+ }
51
+ const output = inferenceOutputs[metadata.name];
52
+ if (!output) {
53
+ throw new Error(`Missing model output: ${metadata.name}`);
54
+ }
55
+ const values = await output.data();
56
+ extractPersonConfidence(values, maskBuffer, metadata);
57
+ for (let pixelIndex = 0; pixelIndex < maskBuffer.length; pixelIndex++) {
58
+ rgbaBuffer[pixelIndex * 4 + 3] = maskBuffer[pixelIndex] ?? 0;
59
+ }
60
+ context.putImageData(imageData, 0, 0);
61
+ return canvas;
62
+ };
63
+ const release = () => {
64
+ context = undefined;
65
+ maskBuffer = undefined;
66
+ rgbaBuffer = undefined;
67
+ imageData = undefined;
68
+ if (canvas) {
69
+ canvas.width = 0;
70
+ canvas.height = 0;
71
+ }
72
+ canvas = undefined;
73
+ };
74
+ return {
75
+ metadata,
76
+ process,
77
+ release,
78
+ get canvas() {
79
+ return canvas;
80
+ },
81
+ get maskBuffer() {
82
+ return maskBuffer;
83
+ },
84
+ get imageData() {
85
+ return imageData;
86
+ },
87
+ };
88
+ };
@@ -0,0 +1,56 @@
1
+ import { Tensor, type TensorDetails } from '../../inferencer';
2
+ import { type TensorLayout } from '../../utils';
3
+ export { getTensorMetadata } from '../../utils';
4
+ /**
5
+ * Convert packed RGBA pixels to normalized model RGB input.
6
+ *
7
+ * Float inputs use the selfie model metadata normalization `(value - 0) / 255`.
8
+ * Uint8 inputs retain their original channel values.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * convertRgbaToRgbTensor(
13
+ * new Uint8Array([255, 128, 0, 255]),
14
+ * new Float32Array(3),
15
+ * 'nhwc',
16
+ * );
17
+ * ```
18
+ */
19
+ export declare const convertRgbaToRgbTensor: (rgba: Uint8Array, target: Float32Array | Uint8Array, layout: TensorLayout) => Float32Array | Uint8Array;
20
+ /**
21
+ * Calculate a centered crop that keeps model input and output masks aligned.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * getModelResizeCoordinates(1280, 720, 256, 256);
26
+ * ```
27
+ */
28
+ export declare const getModelResizeCoordinates: (sourceWidth: number, sourceHeight: number, modelWidth: number, modelHeight: number, processingWidth?: number, processingHeight?: number) => {
29
+ sx: number;
30
+ sy: number;
31
+ sw: number;
32
+ sh: number;
33
+ dx: number;
34
+ dy: number;
35
+ dw: number;
36
+ dh: number;
37
+ };
38
+ /**
39
+ * Create a reusable Canvas2D preprocessor for a LiteRT model input.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const preprocessor = createCanvas2dPreprocessor(inputDetails);
44
+ * const input = await preprocessor.process(videoFrame);
45
+ * ```
46
+ */
47
+ export declare const createCanvas2dPreprocessor: (details: TensorDetails, processingWidth?: number, processingHeight?: number) => {
48
+ metadata: import("../..").TensorMetadata;
49
+ process: (frame: VideoFrame) => Promise<{
50
+ [x: string]: Tensor;
51
+ }>;
52
+ release: () => void;
53
+ readonly canvas: OffscreenCanvas | undefined;
54
+ readonly pixelBuffer: Uint8Array<ArrayBufferLike> | undefined;
55
+ readonly tensorStorage: Float32Array<ArrayBuffer> | Uint8Array<ArrayBuffer>;
56
+ };
@@ -0,0 +1,139 @@
1
+ import { Tensor } from '../../inferencer';
2
+ import { createOffscreenCanvas, getCanvasRenderingContext2D, } from '../../canvasRenderUtils';
3
+ import { getImageSize } from '../../canvasRenderUtils';
4
+ import { resize, getTensorMetadata } from '../../utils';
5
+ export { getTensorMetadata } from '../../utils';
6
+ /**
7
+ * Convert packed RGBA pixels to normalized model RGB input.
8
+ *
9
+ * Float inputs use the selfie model metadata normalization `(value - 0) / 255`.
10
+ * Uint8 inputs retain their original channel values.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * convertRgbaToRgbTensor(
15
+ * new Uint8Array([255, 128, 0, 255]),
16
+ * new Float32Array(3),
17
+ * 'nhwc',
18
+ * );
19
+ * ```
20
+ */
21
+ export const convertRgbaToRgbTensor = (rgba, target, layout) => {
22
+ const pixelCount = rgba.length / 4;
23
+ if (!Number.isInteger(pixelCount) || target.length !== pixelCount * 3) {
24
+ throw new Error('RGBA and RGB tensor buffer sizes do not match');
25
+ }
26
+ const normalize = target instanceof Float32Array;
27
+ for (let pixelIndex = 0; pixelIndex < pixelCount; pixelIndex++) {
28
+ const rgbaOffset = pixelIndex * 4;
29
+ for (let channelIndex = 0; channelIndex < 3; channelIndex++) {
30
+ const targetIndex = layout === 'nhwc'
31
+ ? pixelIndex * 3 + channelIndex
32
+ : channelIndex * pixelCount + pixelIndex;
33
+ const value = rgba[rgbaOffset + channelIndex] ?? 0;
34
+ target[targetIndex] = normalize ? value / 255 : value;
35
+ }
36
+ }
37
+ return target;
38
+ };
39
+ /**
40
+ * Calculate a centered crop that keeps model input and output masks aligned.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * getModelResizeCoordinates(1280, 720, 256, 256);
45
+ * ```
46
+ */
47
+ export const getModelResizeCoordinates = (sourceWidth, sourceHeight, modelWidth, modelHeight, processingWidth = modelWidth, processingHeight = modelHeight) => {
48
+ if (sourceWidth <= 0 ||
49
+ sourceHeight <= 0 ||
50
+ modelWidth <= 0 ||
51
+ modelHeight <= 0 ||
52
+ processingWidth <= 0 ||
53
+ processingHeight <= 0) {
54
+ throw new Error('Source, processing, and model dimensions must be positive');
55
+ }
56
+ const sourceCrop = resize(sourceWidth, sourceHeight, processingWidth, processingHeight);
57
+ const scale = Math.min(modelWidth / processingWidth, modelHeight / processingHeight);
58
+ const destinationWidth = processingWidth * scale;
59
+ const destinationHeight = processingHeight * scale;
60
+ return {
61
+ sx: sourceCrop.sx,
62
+ sy: sourceCrop.sy,
63
+ sw: sourceCrop.sw,
64
+ sh: sourceCrop.sh,
65
+ dx: (modelWidth - destinationWidth) / 2,
66
+ dy: (modelHeight - destinationHeight) / 2,
67
+ dw: destinationWidth,
68
+ dh: destinationHeight,
69
+ };
70
+ };
71
+ /**
72
+ * Create a reusable Canvas2D preprocessor for a LiteRT model input.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * const preprocessor = createCanvas2dPreprocessor(inputDetails);
77
+ * const input = await preprocessor.process(videoFrame);
78
+ * ```
79
+ */
80
+ export const createCanvas2dPreprocessor = (details, processingWidth, processingHeight) => {
81
+ const metadata = getTensorMetadata(details);
82
+ let canvas = createOffscreenCanvas(metadata.width, metadata.height);
83
+ let context = getCanvasRenderingContext2D(canvas, { alpha: false });
84
+ let pixelBuffer = new Uint8Array(metadata.width * metadata.height * 4);
85
+ let tensorStorage = metadata.dtype === 'float32'
86
+ ? new Float32Array(metadata.width * metadata.height * 3)
87
+ : new Uint8Array(metadata.width * metadata.height * 3);
88
+ let tensor;
89
+ const process = async (frame) => {
90
+ if (!canvas || !context || !pixelBuffer) {
91
+ throw new Error('The preprocessor has been released');
92
+ }
93
+ const sourceSize = getImageSize(frame);
94
+ const coordinates = getModelResizeCoordinates(sourceSize.width, sourceSize.height, metadata.width, metadata.height, processingWidth ?? metadata.width, processingHeight ?? metadata.height);
95
+ context.clearRect(0, 0, metadata.width, metadata.height);
96
+ context.drawImage(frame, coordinates.sx, coordinates.sy, coordinates.sw, coordinates.sh, coordinates.dx, coordinates.dy, coordinates.dw, coordinates.dh);
97
+ const resizedFrame = new VideoFrame(canvas, {
98
+ // VideoFrame timestamp is in microseconds
99
+ // https://developer.mozilla.org/en-US/docs/Web/API/VideoFrame/timestamp
100
+ timestamp: frame.timestamp ?? performance.now() * 1000,
101
+ });
102
+ try {
103
+ await resizedFrame.copyTo(pixelBuffer, { format: 'RGBA' });
104
+ }
105
+ finally {
106
+ resizedFrame.close();
107
+ }
108
+ convertRgbaToRgbTensor(pixelBuffer, tensorStorage, metadata.layout);
109
+ tensor?.delete();
110
+ tensor = new Tensor(tensorStorage, Array.from(metadata.shape));
111
+ return { [metadata.name]: tensor };
112
+ };
113
+ const release = () => {
114
+ tensor?.delete();
115
+ tensor = undefined;
116
+ tensorStorage = new Uint8Array();
117
+ pixelBuffer = undefined;
118
+ context = undefined;
119
+ if (canvas) {
120
+ canvas.width = 0;
121
+ canvas.height = 0;
122
+ }
123
+ canvas = undefined;
124
+ };
125
+ return {
126
+ metadata,
127
+ process,
128
+ release,
129
+ get canvas() {
130
+ return canvas;
131
+ },
132
+ get pixelBuffer() {
133
+ return pixelBuffer;
134
+ },
135
+ get tensorStorage() {
136
+ return tensorStorage;
137
+ },
138
+ };
139
+ };
@@ -272,6 +272,7 @@ export const createRenderer = (adapter, renderEventHandlers, canvas = new Offscr
272
272
  };
273
273
  const release = () => {
274
274
  lazyProps.release();
275
+ props.device = undefined;
275
276
  props.deviceLost = false;
276
277
  props.prevMask2 = undefined;
277
278
  props.prevMask4 = undefined;
@@ -1,6 +1,41 @@
1
1
  export declare const hasR16FloatTextureStorage: (adapter: GPUAdapter) => boolean;
2
2
  export declare const hasF16: (adapter: GPUAdapter) => boolean;
3
+ export declare class GPUDeviceRequestInvalidatedError extends Error {
4
+ constructor();
5
+ }
6
+ /**
7
+ * Get the GPU adapter, requesting it only once.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const adapter = await getGPUAdapter();
12
+ * ```
13
+ */
14
+ export declare const getGPUAdapter: () => Promise<GPUAdapter | null>;
15
+ /**
16
+ * Get the GPU device, requesting it only once.
17
+ *
18
+ * A device owns the memory of the pipelines and the shader modules created from
19
+ * it, and neither of those can be freed on their own, so it is shared by every
20
+ * renderer instead of being requested per renderer.
21
+ *
22
+ * @param adapter - The adapter to request the device from
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const device = await getGPUDevice(adapter);
27
+ * ```
28
+ */
3
29
  export declare const getGPUDevice: (adapter: GPUAdapter) => Promise<GPUDevice>;
30
+ /**
31
+ * Destroy the shared GPU device.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * destroyGPUDevice();
36
+ * ```
37
+ */
38
+ export declare const destroyGPUDevice: () => void;
4
39
  /**
5
40
  * Check if provided format is supported
6
41
  *