@pexip/media-processor 16.7.1 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +1 -7
- package/dist/main/audio.d.ts +123 -0
- package/dist/main/audio.js +653 -0
- package/dist/main/benchUtils.d.ts +12 -0
- package/dist/main/benchUtils.js +34 -0
- package/dist/main/generator.d.ts +4 -0
- package/dist/main/generator.js +4 -0
- package/dist/main/index.d.ts +17 -0
- package/dist/main/index.js +17 -0
- package/dist/main/math.d.ts +41 -0
- package/dist/main/math.js +57 -0
- package/dist/main/path.d.ts +102 -0
- package/dist/main/path.js +103 -0
- package/dist/main/process.d.ts +239 -0
- package/dist/main/process.js +364 -0
- package/dist/main/processor.d.ts +5 -0
- package/dist/main/processor.js +4 -0
- package/dist/main/transformer.d.ts +1 -0
- package/dist/main/transformer.js +4 -0
- package/dist/main/tsconfig.tsbuildinfo +1 -0
- package/dist/main/typeGuards.d.ts +5 -0
- package/dist/main/typeGuards.js +24 -0
- package/dist/main/types.d.ts +342 -0
- package/dist/main/types.js +1 -0
- package/dist/main/utils.d.ts +173 -0
- package/dist/main/utils.js +364 -0
- package/dist/main/video/canvasRenderUtils.d.ts +22 -0
- package/dist/main/video/canvasRenderUtils.js +198 -0
- package/dist/main/video/canvasTransform.d.ts +8 -0
- package/dist/main/video/canvasTransform.js +173 -0
- package/dist/main/video/constants.d.ts +10 -0
- package/dist/main/video/constants.js +12 -0
- package/dist/main/video/index.d.ts +9 -0
- package/dist/main/video/index.js +9 -0
- package/dist/main/video/load.d.ts +17 -0
- package/dist/main/video/load.js +47 -0
- package/dist/main/video/segmenters/index.d.ts +1 -0
- package/dist/main/video/segmenters/index.js +1 -0
- package/dist/main/video/segmenters/mediapipe.d.ts +13 -0
- package/dist/main/video/segmenters/mediapipe.js +85 -0
- package/dist/main/video/transformer.d.ts +10 -0
- package/dist/main/video/transformer.js +56 -0
- package/dist/main/video/typeGuards.d.ts +2 -0
- package/dist/main/video/typeGuards.js +13 -0
- package/dist/main/video/types.d.ts +98 -0
- package/dist/main/video/types.js +20 -0
- package/dist/main/video/utils.d.ts +102 -0
- package/dist/main/video/utils.js +476 -0
- package/dist/main/video/video.d.ts +19 -0
- package/dist/main/video/video.js +48 -0
- package/dist/main/video/videoStreamTrackProcessor.d.ts +14 -0
- package/dist/main/video/videoStreamTrackProcessor.js +82 -0
- package/dist/main/visual.d.ts +80 -0
- package/dist/main/visual.js +135 -0
- package/dist/main/workletNodes.d.ts +2 -0
- package/dist/main/workletNodes.js +3 -0
- package/dist/workers/index.d.ts +0 -0
- package/dist/workers/index.js +1 -0
- package/dist/workers/tsconfig.tsbuildinfo +1 -0
- package/dist/worklets/denoise.worklet.d.ts +1 -0
- package/dist/worklets/denoise.worklet.js +1 -2
- package/dist/worklets/tsconfig.tsbuildinfo +1 -0
- package/dist/worklets/types.d.ts +52 -0
- package/dist/worklets/types.js +0 -0
- package/package.json +10 -8
- package/dist/index.d.ts +0 -1129
- package/dist/index.mjs +0 -2441
- package/dist/worklets/denoise.worklet.js.map +0 -7
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/// <reference types="dom-webcodecs" />
|
|
2
|
+
import type { Tensor3D } from '@tensorflow/tfjs-core';
|
|
3
|
+
/**
|
|
4
|
+
* We support several ways to get image inputs.
|
|
5
|
+
*/
|
|
6
|
+
export type InputImage = HTMLVideoElement | HTMLImageElement | HTMLCanvasElement;
|
|
7
|
+
import type { Size, Rect, Frame, Canvas, CanvasContext, Callback } from '../types';
|
|
8
|
+
import { subscribeVisibilityChangeEvent } from '../utils';
|
|
9
|
+
import type { MaskUnderlyingType, Segmentation, Color, ImageType, ProcessInputType } from './types';
|
|
10
|
+
/**
|
|
11
|
+
* Draw the source onto the provided canvas according to the destination rect
|
|
12
|
+
*
|
|
13
|
+
* @param canvasContext - A buffer canvas used for drawing the frame
|
|
14
|
+
* @param source - Source to take the frame from
|
|
15
|
+
* @param destination - A destination rect to be draw on the canvas
|
|
16
|
+
*/
|
|
17
|
+
export declare const drawFrame: (canvasContext: CanvasContext, source: Frame, destination: Rect) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Taking a frame from the source and return the `ImageData`
|
|
20
|
+
*
|
|
21
|
+
* @param canvasContext - A buffer canvas used for drawing the frame
|
|
22
|
+
* @param source - Source to take the frame from
|
|
23
|
+
* @param destination - A destination rect to be draw on the canvas for getting the
|
|
24
|
+
* data
|
|
25
|
+
*/
|
|
26
|
+
export declare const takeFrame: (canvasContext: CanvasContext, source: Frame, destination: Rect) => ImageData;
|
|
27
|
+
/**
|
|
28
|
+
* Draw provided ImageData onto a canvas. This is just a wrapper of
|
|
29
|
+
* `putImageData` with both `dx` and `dy` set to 0
|
|
30
|
+
*
|
|
31
|
+
* @param canvasContext - The canvas context to draw on
|
|
32
|
+
* @param data - The data to draw
|
|
33
|
+
*/
|
|
34
|
+
export declare const drawImageData: (canvasContext: CanvasContext, data: ImageData) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Create a Canvas element with provided width and height
|
|
37
|
+
*
|
|
38
|
+
* @param width - canvas.width
|
|
39
|
+
* @param height - canvas.height
|
|
40
|
+
*/
|
|
41
|
+
export declare const createCanvas: (width: number, height: number) => HTMLCanvasElement;
|
|
42
|
+
/**
|
|
43
|
+
* Create an OffscreenCanvas with provided width and height. When
|
|
44
|
+
* OffscreenCanvas is not available, a Canvas element is returned.
|
|
45
|
+
*
|
|
46
|
+
* @param width - canvas.width
|
|
47
|
+
* @param height - canvas.height
|
|
48
|
+
*/
|
|
49
|
+
export declare const createOffscreenCanvas: (width: number, height: number) => HTMLCanvasElement | OffscreenCanvas;
|
|
50
|
+
/**
|
|
51
|
+
* Create a video element with provided width and height, and set it to muted
|
|
52
|
+
*
|
|
53
|
+
* @param width - video.width
|
|
54
|
+
* @param height - video.height
|
|
55
|
+
*/
|
|
56
|
+
export declare const createVideoElement: (width: number, height: number) => HTMLVideoElement;
|
|
57
|
+
export declare const setVideoElementSrc: (video: HTMLVideoElement, src: MediaProvider) => void;
|
|
58
|
+
export declare const toVideoElement: (input: MediaStream, width: number, height: number) => HTMLVideoElement;
|
|
59
|
+
export declare const playVideo: (video: HTMLVideoElement) => Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Load image with provided image element
|
|
62
|
+
*/
|
|
63
|
+
export declare const loadImage: (image: HTMLImageElement, src: string) => Promise<void>;
|
|
64
|
+
export declare const toNumber: (value: number | SVGAnimatedLength) => number;
|
|
65
|
+
export declare const getCanvasRenderingContext2D: (canvas: Canvas, options?: CanvasRenderingContext2DSettings) => CanvasRenderingContext2D;
|
|
66
|
+
export declare const getImageSize: (image: VideoFrame | CanvasImageSource | OffscreenCanvas | ImageData | SVGImageElement | Tensor3D) => Size;
|
|
67
|
+
export declare const toHTMLCanvasElementLossy: (image: ImageData | SVGImageElement | OffscreenCanvas | Tensor3D) => Promise<HTMLCanvasElement>;
|
|
68
|
+
export declare const toImageDataLossy: (image: CanvasImageSource | Tensor3D) => Promise<ImageData>;
|
|
69
|
+
export declare const toTensorLossy: (image: CanvasImageSource | ImageData) => Promise<Tensor3D>;
|
|
70
|
+
/**
|
|
71
|
+
* Construct a Segmentation object with tensorflow-models/shared utils
|
|
72
|
+
*/
|
|
73
|
+
export declare const toSegmentation: (data: CanvasImageSource | ImageData | Tensor3D, type: MaskUnderlyingType, maskValueToLabel: (value: number) => string) => Segmentation;
|
|
74
|
+
export declare const flipCanvasHorizontal: (canvas: Canvas) => void;
|
|
75
|
+
export declare const drawStroke: (bytes: Uint8ClampedArray, row: number, column: number, width: number, radius: number, color?: Color) => void;
|
|
76
|
+
export declare const isSegmentationBoundary: (data: Uint8ClampedArray, row: number, column: number, width: number, isForegroundId: boolean[], alphaCutoff: number, radius?: number) => boolean;
|
|
77
|
+
export declare const toBinaryMask: (segmentation: Segmentation | Segmentation[], foreground?: Color, background?: Color, drawContour?: boolean, foregroundThreshold?: number, foregroundMaskValues?: number[]) => Promise<ImageData | null>;
|
|
78
|
+
type Draw = (input: ImageType) => Promise<Canvas>;
|
|
79
|
+
export declare const createInputImageConvertor: (draw: Draw) => (input: ProcessInputType) => Promise<InputImage>;
|
|
80
|
+
export declare const ensure: <T>(prop: T, message?: string) => NonNullable<T>;
|
|
81
|
+
interface FrameCallbackRequestOptions {
|
|
82
|
+
/**
|
|
83
|
+
* Subscribe `visibilitychange` event from the DOM
|
|
84
|
+
* @see {@link subscribeVisibilityChangeEvent}
|
|
85
|
+
*/
|
|
86
|
+
subscribeVisibilityChange?: typeof subscribeVisibilityChangeEvent;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create a callback loop for video frame processing using
|
|
90
|
+
* `requestVideoFrameCallback` under-the-hood when available otherwise our
|
|
91
|
+
* fallback implementation based on `setTimeout`.
|
|
92
|
+
*
|
|
93
|
+
* @param callback - To be called by the loop
|
|
94
|
+
* @param frameRate - A fallback frame rate when we are not able to get the rate
|
|
95
|
+
* from API
|
|
96
|
+
*/
|
|
97
|
+
export declare const createFrameCallbackRequest: (callback: Callback<Promise<void>, [ProcessInputType]>, frameRate: number, { subscribeVisibilityChange, }?: FrameCallbackRequestOptions) => {
|
|
98
|
+
start: (input: ProcessInputType) => Promise<void>;
|
|
99
|
+
stop: () => void;
|
|
100
|
+
frameRate: number;
|
|
101
|
+
};
|
|
102
|
+
export {};
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { Tensor, browser } from '@tensorflow/tfjs-core/dist/base.js';
|
|
2
|
+
import { createAsyncCallbackLoop, subscribeVisibilityChangeEvent, } from '../utils';
|
|
3
|
+
/**
|
|
4
|
+
* Draw the source onto the provided canvas according to the destination rect
|
|
5
|
+
*
|
|
6
|
+
* @param canvasContext - A buffer canvas used for drawing the frame
|
|
7
|
+
* @param source - Source to take the frame from
|
|
8
|
+
* @param destination - A destination rect to be draw on the canvas
|
|
9
|
+
*/
|
|
10
|
+
export const drawFrame = (canvasContext, source, destination) => {
|
|
11
|
+
canvasContext.drawImage(source.source, 0, 0, source.width, source.height, destination.x, destination.y, destination.width, destination.height);
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Taking a frame from the source and return the `ImageData`
|
|
15
|
+
*
|
|
16
|
+
* @param canvasContext - A buffer canvas used for drawing the frame
|
|
17
|
+
* @param source - Source to take the frame from
|
|
18
|
+
* @param destination - A destination rect to be draw on the canvas for getting the
|
|
19
|
+
* data
|
|
20
|
+
*/
|
|
21
|
+
export const takeFrame = (canvasContext, source, destination) => {
|
|
22
|
+
drawFrame(canvasContext, source, destination);
|
|
23
|
+
return canvasContext.getImageData(0, 0, destination.width, destination.height);
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Draw provided ImageData onto a canvas. This is just a wrapper of
|
|
27
|
+
* `putImageData` with both `dx` and `dy` set to 0
|
|
28
|
+
*
|
|
29
|
+
* @param canvasContext - The canvas context to draw on
|
|
30
|
+
* @param data - The data to draw
|
|
31
|
+
*/
|
|
32
|
+
export const drawImageData = (canvasContext, data) => {
|
|
33
|
+
canvasContext.putImageData(data, 0, 0);
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Create a Canvas element with provided width and height
|
|
37
|
+
*
|
|
38
|
+
* @param width - canvas.width
|
|
39
|
+
* @param height - canvas.height
|
|
40
|
+
*/
|
|
41
|
+
export const createCanvas = (width, height) => {
|
|
42
|
+
const canvas = document.createElement('canvas');
|
|
43
|
+
canvas.width = width;
|
|
44
|
+
canvas.height = height;
|
|
45
|
+
return canvas;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Create an OffscreenCanvas with provided width and height. When
|
|
49
|
+
* OffscreenCanvas is not available, a Canvas element is returned.
|
|
50
|
+
*
|
|
51
|
+
* @param width - canvas.width
|
|
52
|
+
* @param height - canvas.height
|
|
53
|
+
*/
|
|
54
|
+
export const createOffscreenCanvas = (width, height) => {
|
|
55
|
+
try {
|
|
56
|
+
const offscreen = new OffscreenCanvas(width, height);
|
|
57
|
+
return offscreen;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return createCanvas(width, height);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Create a video element with provided width and height, and set it to muted
|
|
65
|
+
*
|
|
66
|
+
* @param width - video.width
|
|
67
|
+
* @param height - video.height
|
|
68
|
+
*/
|
|
69
|
+
export const createVideoElement = (width, height) => {
|
|
70
|
+
const video = document.createElement('video');
|
|
71
|
+
video.width = width;
|
|
72
|
+
video.height = height;
|
|
73
|
+
video.muted = true;
|
|
74
|
+
return video;
|
|
75
|
+
};
|
|
76
|
+
export const setVideoElementSrc = (video, src) => {
|
|
77
|
+
let url = '';
|
|
78
|
+
const revokeObjectURL = () => {
|
|
79
|
+
if (url) {
|
|
80
|
+
URL.revokeObjectURL(url);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
if (src instanceof MediaStream) {
|
|
84
|
+
src.getVideoTracks().forEach(track => {
|
|
85
|
+
track.addEventListener('ended', revokeObjectURL);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if ('MediaSource' in window && src instanceof MediaSource) {
|
|
89
|
+
src.addEventListener('sourceended', revokeObjectURL);
|
|
90
|
+
}
|
|
91
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/srcObject
|
|
92
|
+
try {
|
|
93
|
+
video.srcObject = src;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (error instanceof Error) {
|
|
97
|
+
if (error.name === 'TypeError') {
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
if (('MediaSource' in window && src instanceof MediaSource) ||
|
|
101
|
+
src instanceof Blob) {
|
|
102
|
+
url = URL.createObjectURL(src);
|
|
103
|
+
}
|
|
104
|
+
video.src = url;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
export const toVideoElement = (input, width, height) => {
|
|
109
|
+
const [settings = {}] = input
|
|
110
|
+
.getVideoTracks()
|
|
111
|
+
.map(track => track.getSettings());
|
|
112
|
+
const video = createVideoElement(settings.width ?? width, settings.height ?? height);
|
|
113
|
+
video.playsInline = true;
|
|
114
|
+
setVideoElementSrc(video, input);
|
|
115
|
+
return video;
|
|
116
|
+
};
|
|
117
|
+
var HTMLMediaElementReadyState;
|
|
118
|
+
(function (HTMLMediaElementReadyState) {
|
|
119
|
+
HTMLMediaElementReadyState[HTMLMediaElementReadyState["HaveNothing"] = 0] = "HaveNothing";
|
|
120
|
+
HTMLMediaElementReadyState[HTMLMediaElementReadyState["HaveMetaData"] = 1] = "HaveMetaData";
|
|
121
|
+
HTMLMediaElementReadyState[HTMLMediaElementReadyState["HaveCurrentData"] = 2] = "HaveCurrentData";
|
|
122
|
+
HTMLMediaElementReadyState[HTMLMediaElementReadyState["HaveFutureData"] = 3] = "HaveFutureData";
|
|
123
|
+
HTMLMediaElementReadyState[HTMLMediaElementReadyState["HaveEnoughData"] = 4] = "HaveEnoughData";
|
|
124
|
+
})(HTMLMediaElementReadyState || (HTMLMediaElementReadyState = {}));
|
|
125
|
+
export const playVideo = async (video) => {
|
|
126
|
+
if (video.autoplay) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (video.readyState < HTMLMediaElementReadyState.HaveCurrentData) {
|
|
130
|
+
// Not enough data, wait for the data and then play
|
|
131
|
+
await Promise.race([
|
|
132
|
+
new Promise(resolve => {
|
|
133
|
+
const waitForEvent = () => {
|
|
134
|
+
video.removeEventListener('loadeddata', waitForEvent);
|
|
135
|
+
resolve();
|
|
136
|
+
};
|
|
137
|
+
video.addEventListener('loadeddata', waitForEvent);
|
|
138
|
+
}),
|
|
139
|
+
// Use setTimeout to prevent a race to `loadeddata` event
|
|
140
|
+
new Promise(resolve => {
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
resolve();
|
|
143
|
+
}, 3000);
|
|
144
|
+
}),
|
|
145
|
+
]);
|
|
146
|
+
}
|
|
147
|
+
return await video.play();
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Load image with provided image element
|
|
151
|
+
*/
|
|
152
|
+
export const loadImage = async (image, src) => {
|
|
153
|
+
image.src = src;
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
image.onload = () => resolve();
|
|
156
|
+
image.onerror = reject;
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
export const toNumber = (value) => value instanceof SVGAnimatedLength ? value.baseVal.value : value;
|
|
160
|
+
export const getCanvasRenderingContext2D = (canvas, options) => {
|
|
161
|
+
const context = canvas.getContext('2d', options);
|
|
162
|
+
if (!context) {
|
|
163
|
+
throw new Error('Cannot get CanvasRenderingContext2D');
|
|
164
|
+
}
|
|
165
|
+
return context;
|
|
166
|
+
};
|
|
167
|
+
export const getImageSize = (image) => {
|
|
168
|
+
if (image instanceof Tensor) {
|
|
169
|
+
const [height = 0, width = 0] = image.shape.slice(0, 2);
|
|
170
|
+
return { height, width };
|
|
171
|
+
}
|
|
172
|
+
if ('VideoFrame' in window && image instanceof VideoFrame) {
|
|
173
|
+
return { height: image.displayHeight, width: image.displayWidth };
|
|
174
|
+
}
|
|
175
|
+
if ('offsetHeight' in image &&
|
|
176
|
+
image.offsetHeight !== 0 &&
|
|
177
|
+
'offsetWidth' in image &&
|
|
178
|
+
image.offsetWidth !== 0) {
|
|
179
|
+
return { height: image.offsetHeight, width: image.offsetWidth };
|
|
180
|
+
}
|
|
181
|
+
if ('height' in image &&
|
|
182
|
+
image.height !== 0 &&
|
|
183
|
+
'width' in image &&
|
|
184
|
+
image.width !== 0) {
|
|
185
|
+
return { height: toNumber(image.height), width: toNumber(image.width) };
|
|
186
|
+
}
|
|
187
|
+
throw new Error('Unknown input image');
|
|
188
|
+
};
|
|
189
|
+
export const toHTMLCanvasElementLossy = async (image) => {
|
|
190
|
+
const { width, height } = getImageSize(image);
|
|
191
|
+
const canvas = createCanvas(width, height);
|
|
192
|
+
if (image instanceof Tensor) {
|
|
193
|
+
await browser.toPixels(image, canvas);
|
|
194
|
+
return canvas;
|
|
195
|
+
}
|
|
196
|
+
const context = getCanvasRenderingContext2D(canvas);
|
|
197
|
+
if (image instanceof ImageData) {
|
|
198
|
+
context.putImageData(image, 0, 0);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
context.drawImage(image, 0, 0);
|
|
202
|
+
}
|
|
203
|
+
return canvas;
|
|
204
|
+
};
|
|
205
|
+
export const toImageDataLossy = async (image) => {
|
|
206
|
+
const { width, height } = getImageSize(image);
|
|
207
|
+
if (image instanceof Tensor) {
|
|
208
|
+
return new ImageData(await browser.toPixels(image), width, height);
|
|
209
|
+
}
|
|
210
|
+
const canvas = createOffscreenCanvas(width, height);
|
|
211
|
+
const context = getCanvasRenderingContext2D(canvas);
|
|
212
|
+
context.drawImage(image, 0, 0);
|
|
213
|
+
return context.getImageData(0, 0, canvas.width, canvas.height);
|
|
214
|
+
};
|
|
215
|
+
export const toTensorLossy = async (image) => {
|
|
216
|
+
const pixelsInput = image instanceof SVGImageElement || image instanceof OffscreenCanvas
|
|
217
|
+
? await toHTMLCanvasElementLossy(image)
|
|
218
|
+
: image;
|
|
219
|
+
return browser.fromPixels(pixelsInput, 4);
|
|
220
|
+
};
|
|
221
|
+
/**
|
|
222
|
+
* Construct a Segmentation object with tensorflow-models/shared utils
|
|
223
|
+
*/
|
|
224
|
+
export const toSegmentation = (data, type, maskValueToLabel) => {
|
|
225
|
+
const mask = {
|
|
226
|
+
toCanvasImageSource: async () => {
|
|
227
|
+
if (data instanceof HTMLCanvasElement) {
|
|
228
|
+
return data;
|
|
229
|
+
}
|
|
230
|
+
if (data instanceof HTMLVideoElement ||
|
|
231
|
+
data instanceof ImageBitmap ||
|
|
232
|
+
data instanceof HTMLImageElement) {
|
|
233
|
+
const canvas = createCanvas(data.width, data.height);
|
|
234
|
+
const context = canvas.getContext('2d');
|
|
235
|
+
context?.drawImage(data, 0, 0, data.width, data.height);
|
|
236
|
+
return canvas;
|
|
237
|
+
}
|
|
238
|
+
return await toHTMLCanvasElementLossy(data);
|
|
239
|
+
},
|
|
240
|
+
toImageData: async () => {
|
|
241
|
+
if (data instanceof ImageData) {
|
|
242
|
+
return data;
|
|
243
|
+
}
|
|
244
|
+
return await toImageDataLossy(data);
|
|
245
|
+
},
|
|
246
|
+
toTensor: async () => {
|
|
247
|
+
if (data instanceof Tensor) {
|
|
248
|
+
return data;
|
|
249
|
+
}
|
|
250
|
+
return await toTensorLossy(data);
|
|
251
|
+
},
|
|
252
|
+
getUnderlyingType: () => type,
|
|
253
|
+
};
|
|
254
|
+
return {
|
|
255
|
+
maskValueToLabel,
|
|
256
|
+
mask,
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
export const flipCanvasHorizontal = (canvas) => {
|
|
260
|
+
const ctx = getCanvasRenderingContext2D(canvas);
|
|
261
|
+
ctx.scale(-1, 1);
|
|
262
|
+
ctx.translate(-canvas.width, 0);
|
|
263
|
+
};
|
|
264
|
+
export const drawStroke = (bytes, row, column, width, radius, color = {
|
|
265
|
+
r: 0,
|
|
266
|
+
g: 255,
|
|
267
|
+
b: 255,
|
|
268
|
+
a: 255,
|
|
269
|
+
}) => {
|
|
270
|
+
for (let i = -radius; i <= radius; i++) {
|
|
271
|
+
for (let j = -radius; j <= radius; j++) {
|
|
272
|
+
if (i !== 0 && j !== 0) {
|
|
273
|
+
const n = (row + i) * width + (column + j);
|
|
274
|
+
bytes[4 * n + 0] = color.r;
|
|
275
|
+
bytes[4 * n + 1] = color.g;
|
|
276
|
+
bytes[4 * n + 2] = color.b;
|
|
277
|
+
bytes[4 * n + 3] = color.a;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
export const isSegmentationBoundary = (data, row, column, width, isForegroundId, alphaCutoff, radius = 1) => {
|
|
283
|
+
let numberBackgroundPixels = 0;
|
|
284
|
+
for (let i = -radius; i <= radius; i++) {
|
|
285
|
+
for (let j = -radius; j <= radius; j++) {
|
|
286
|
+
if (i !== 0 && j !== 0) {
|
|
287
|
+
const n = (row + i) * width + (column + j);
|
|
288
|
+
const foregroundColor = data[4 * n];
|
|
289
|
+
const alphaColor = data[4 * n + 3];
|
|
290
|
+
if ((foregroundColor !== undefined &&
|
|
291
|
+
!isForegroundId[foregroundColor]) ||
|
|
292
|
+
(alphaColor !== undefined && alphaColor < alphaCutoff)) {
|
|
293
|
+
numberBackgroundPixels += 1;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return numberBackgroundPixels > 0;
|
|
299
|
+
};
|
|
300
|
+
export const toBinaryMask = async (segmentation, foreground = {
|
|
301
|
+
r: 0,
|
|
302
|
+
g: 0,
|
|
303
|
+
b: 0,
|
|
304
|
+
a: 0,
|
|
305
|
+
}, background = {
|
|
306
|
+
r: 0,
|
|
307
|
+
g: 0,
|
|
308
|
+
b: 0,
|
|
309
|
+
a: 255,
|
|
310
|
+
}, drawContour = false, foregroundThreshold = 0.5, foregroundMaskValues = Array.from(Array(256).keys())) => {
|
|
311
|
+
const segmentations = !Array.isArray(segmentation)
|
|
312
|
+
? [segmentation]
|
|
313
|
+
: segmentation;
|
|
314
|
+
if (segmentations.length === 0) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
const masks = await Promise.all(segmentations.map(segmentation => segmentation.mask.toImageData()));
|
|
318
|
+
const [imageData] = masks;
|
|
319
|
+
if (!imageData) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const { width, height } = imageData;
|
|
323
|
+
const bytes = new Uint8ClampedArray(width * height * 4);
|
|
324
|
+
const alphaCutoff = Math.round(255 * foregroundThreshold);
|
|
325
|
+
const isForegroundId = new Array(256).fill(false);
|
|
326
|
+
foregroundMaskValues.forEach(id => (isForegroundId[id] = true));
|
|
327
|
+
for (let i = 0; i < height; i++) {
|
|
328
|
+
for (let j = 0; j < width; j++) {
|
|
329
|
+
const n = i * width + j;
|
|
330
|
+
bytes[4 * n + 0] = background.r;
|
|
331
|
+
bytes[4 * n + 1] = background.g;
|
|
332
|
+
bytes[4 * n + 2] = background.b;
|
|
333
|
+
bytes[4 * n + 3] = background.a;
|
|
334
|
+
for (const mask of masks) {
|
|
335
|
+
const maskForegroundColor = mask.data[4 * n];
|
|
336
|
+
const maskAlphaColor = mask.data[4 * n + 3];
|
|
337
|
+
if (maskForegroundColor !== undefined &&
|
|
338
|
+
isForegroundId[maskForegroundColor] &&
|
|
339
|
+
maskAlphaColor !== undefined &&
|
|
340
|
+
maskAlphaColor >= alphaCutoff) {
|
|
341
|
+
bytes[4 * n] = foreground.r;
|
|
342
|
+
bytes[4 * n + 1] = foreground.g;
|
|
343
|
+
bytes[4 * n + 2] = foreground.b;
|
|
344
|
+
bytes[4 * n + 3] = foreground.a;
|
|
345
|
+
if (drawContour &&
|
|
346
|
+
i - 1 >= 0 &&
|
|
347
|
+
i + 1 < height &&
|
|
348
|
+
j - 1 >= 0 &&
|
|
349
|
+
j + 1 < width &&
|
|
350
|
+
isSegmentationBoundary(mask.data, i, j, width, isForegroundId, alphaCutoff)) {
|
|
351
|
+
drawStroke(bytes, i, j, width, 1);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return new ImageData(bytes, width, height);
|
|
358
|
+
};
|
|
359
|
+
export const createInputImageConvertor = (draw) => async (input) => {
|
|
360
|
+
return (input instanceof ImageData || input instanceof ImageBitmap
|
|
361
|
+
? await draw(input)
|
|
362
|
+
: input);
|
|
363
|
+
};
|
|
364
|
+
export const ensure = (prop, message = 'Processor is not opened, please call open() method first') => {
|
|
365
|
+
if (!prop) {
|
|
366
|
+
throw new Error(message);
|
|
367
|
+
}
|
|
368
|
+
return prop;
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* Feature detection to detect if there is requestVideoFrameCallback API
|
|
372
|
+
* available as well as the input is an HTMLVideoElement
|
|
373
|
+
*
|
|
374
|
+
* @param input - An input to be verified for the feature
|
|
375
|
+
*/
|
|
376
|
+
const hasRequestVideoFrameCallback = (input) => typeof input === 'object' &&
|
|
377
|
+
input instanceof HTMLVideoElement &&
|
|
378
|
+
'requestVideoFrameCallback' in HTMLVideoElement.prototype;
|
|
379
|
+
/**
|
|
380
|
+
* Get the estimated video frame rate from HTMLVideoElement, otherwise returns
|
|
381
|
+
* a fallback rate provided by.
|
|
382
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/getVideoPlaybackQuality}
|
|
383
|
+
*
|
|
384
|
+
* @param input - The input to be verified if the `getVideoPlaybackQuality` API
|
|
385
|
+
* is available
|
|
386
|
+
* @param frameRate - A fallback rate to be used when the API is not available
|
|
387
|
+
*/
|
|
388
|
+
const getFrameRate = (input, frameRate) => {
|
|
389
|
+
if (input instanceof HTMLVideoElement) {
|
|
390
|
+
const quality = input.getVideoPlaybackQuality();
|
|
391
|
+
return (input.mozPresentedFrames ||
|
|
392
|
+
quality.totalVideoFrames - quality.droppedVideoFrames);
|
|
393
|
+
}
|
|
394
|
+
return frameRate;
|
|
395
|
+
};
|
|
396
|
+
/**
|
|
397
|
+
* Create a callback loop for video frame processing using
|
|
398
|
+
* `requestVideoFrameCallback` under-the-hood when available otherwise our
|
|
399
|
+
* fallback implementation based on `setTimeout`.
|
|
400
|
+
*
|
|
401
|
+
* @param callback - To be called by the loop
|
|
402
|
+
* @param frameRate - A fallback frame rate when we are not able to get the rate
|
|
403
|
+
* from API
|
|
404
|
+
*/
|
|
405
|
+
export const createFrameCallbackRequest = (callback, frameRate, { subscribeVisibilityChange = subscribeVisibilityChangeEvent, } = {}) => {
|
|
406
|
+
const props = { callbackId: 0, frameRate, started: false };
|
|
407
|
+
const getCallbackLoopRunner = (input) => {
|
|
408
|
+
if (!props.runner) {
|
|
409
|
+
const fallbackRequestCallback = async (input) => {
|
|
410
|
+
if (props.runner) {
|
|
411
|
+
props.runner.frameRate = getFrameRate(input, props.frameRate);
|
|
412
|
+
}
|
|
413
|
+
await callback(input);
|
|
414
|
+
};
|
|
415
|
+
props.runner = createAsyncCallbackLoop(fallbackRequestCallback, getFrameRate(input, props.frameRate));
|
|
416
|
+
}
|
|
417
|
+
return props.runner;
|
|
418
|
+
};
|
|
419
|
+
const cancelFrameCallback = () => {
|
|
420
|
+
if (hasRequestVideoFrameCallback(props.input) && props.callbackId) {
|
|
421
|
+
props.input.cancelVideoFrameCallback(props.callbackId);
|
|
422
|
+
props.callbackId = 0;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
return {
|
|
426
|
+
start: async (input) => {
|
|
427
|
+
props.input = input;
|
|
428
|
+
if (hasRequestVideoFrameCallback(input)) {
|
|
429
|
+
// Subscribe visibility changes and switch to fallback callback
|
|
430
|
+
// loop when the page is hidden since the
|
|
431
|
+
// `requestVideoFrameCallback` is paused when the page is hidden
|
|
432
|
+
props.unsubscribe = subscribeVisibilityChange(async (hidden) => {
|
|
433
|
+
if (props.started && props.input) {
|
|
434
|
+
if (hidden && props.callbackId) {
|
|
435
|
+
await getCallbackLoopRunner(props.input).start(props.input);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
getCallbackLoopRunner(props.input).stop();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
await new Promise(resolve => {
|
|
443
|
+
const wrap = async () => {
|
|
444
|
+
await callback(input);
|
|
445
|
+
if (!props.started) {
|
|
446
|
+
props.started = true;
|
|
447
|
+
}
|
|
448
|
+
resolve();
|
|
449
|
+
props.callbackId =
|
|
450
|
+
input.requestVideoFrameCallback(wrap);
|
|
451
|
+
};
|
|
452
|
+
props.callbackId = input.requestVideoFrameCallback(wrap);
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
await getCallbackLoopRunner(input).start(input);
|
|
457
|
+
props.started = true;
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
stop: () => {
|
|
461
|
+
cancelFrameCallback();
|
|
462
|
+
props.runner?.stop();
|
|
463
|
+
props.unsubscribe?.();
|
|
464
|
+
props.started = false;
|
|
465
|
+
},
|
|
466
|
+
get frameRate() {
|
|
467
|
+
return getFrameRate(props.input, frameRate);
|
|
468
|
+
},
|
|
469
|
+
set frameRate(value) {
|
|
470
|
+
props.frameRate = value;
|
|
471
|
+
if (props.runner) {
|
|
472
|
+
props.runner.frameRate = frameRate;
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Transform } from '../types';
|
|
2
|
+
import type { InputFrame } from './types';
|
|
3
|
+
import type { ProcessVideoTrack } from './videoStreamTrackProcessor';
|
|
4
|
+
export interface VideoProcessor {
|
|
5
|
+
open(): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Process the video and return a MediaStream
|
|
8
|
+
*/
|
|
9
|
+
process: (source: MediaStream) => Promise<MediaStream>;
|
|
10
|
+
/**
|
|
11
|
+
* Stop the process of video segmentation
|
|
12
|
+
*/
|
|
13
|
+
close(): void;
|
|
14
|
+
/**
|
|
15
|
+
* Destroy the segmentation wasm instance
|
|
16
|
+
*/
|
|
17
|
+
destroy: () => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare const createVideoProcessor: (transformers: Array<Transform<InputFrame, InputFrame>>, processTrack: ProcessVideoTrack) => VideoProcessor;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { stopStreamTracks } from '../utils';
|
|
2
|
+
import { AbortReason } from './constants';
|
|
3
|
+
export const createVideoProcessor = (transformers, processTrack) => {
|
|
4
|
+
const props = {
|
|
5
|
+
processing: false,
|
|
6
|
+
};
|
|
7
|
+
const open = async () => {
|
|
8
|
+
await Promise.all(transformers.map(transformer => transformer.init()));
|
|
9
|
+
};
|
|
10
|
+
const process = async (source) => {
|
|
11
|
+
const [track] = source.getVideoTracks();
|
|
12
|
+
if (!track) {
|
|
13
|
+
return source;
|
|
14
|
+
}
|
|
15
|
+
if (props.processing) {
|
|
16
|
+
throw new Error('Cannot process when it is already processing');
|
|
17
|
+
}
|
|
18
|
+
props.abortController = new AbortController();
|
|
19
|
+
const trackGenerated = await processTrack(track, transformers, {
|
|
20
|
+
signal: props.abortController.signal,
|
|
21
|
+
});
|
|
22
|
+
props.outputStream = new MediaStream([
|
|
23
|
+
trackGenerated,
|
|
24
|
+
...source.getAudioTracks().map(track => track.clone()),
|
|
25
|
+
]);
|
|
26
|
+
props.processing = true;
|
|
27
|
+
return props.outputStream;
|
|
28
|
+
};
|
|
29
|
+
const close = () => {
|
|
30
|
+
if (!props.processing) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
props.abortController?.abort(AbortReason.Close);
|
|
34
|
+
stopStreamTracks(props.outputStream);
|
|
35
|
+
props.outputStream = undefined;
|
|
36
|
+
props.processing = false;
|
|
37
|
+
};
|
|
38
|
+
const destroy = async () => {
|
|
39
|
+
close();
|
|
40
|
+
await Promise.all(transformers.map(transformer => transformer.destroy()));
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
open,
|
|
44
|
+
process,
|
|
45
|
+
close,
|
|
46
|
+
destroy,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { InputFrame } from './types';
|
|
2
|
+
type Track = MediaStreamVideoTrack | MediaStreamVideoTrackGenerator;
|
|
3
|
+
interface Options {
|
|
4
|
+
signal?: AbortSignal;
|
|
5
|
+
}
|
|
6
|
+
export type ProcessVideoTrack = (track: MediaStreamVideoTrack, transformers: Array<Transformer<InputFrame, InputFrame>>, options?: Options) => Promise<Track>;
|
|
7
|
+
export declare const createVideoTrackProcessor: () => ProcessVideoTrack;
|
|
8
|
+
interface FallbackOptions {
|
|
9
|
+
width?: number;
|
|
10
|
+
height?: number;
|
|
11
|
+
frameRate?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare const createVideoTrackProcessorWithFallback: ({ width, height, frameRate, }?: FallbackOptions) => ProcessVideoTrack;
|
|
14
|
+
export {};
|