@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -7
  3. package/dist/main/audio.d.ts +123 -0
  4. package/dist/main/audio.js +653 -0
  5. package/dist/main/benchUtils.d.ts +12 -0
  6. package/dist/main/benchUtils.js +34 -0
  7. package/dist/main/generator.d.ts +4 -0
  8. package/dist/main/generator.js +4 -0
  9. package/dist/main/index.d.ts +17 -0
  10. package/dist/main/index.js +17 -0
  11. package/dist/main/math.d.ts +41 -0
  12. package/dist/main/math.js +57 -0
  13. package/dist/main/path.d.ts +102 -0
  14. package/dist/main/path.js +103 -0
  15. package/dist/main/process.d.ts +239 -0
  16. package/dist/main/process.js +364 -0
  17. package/dist/main/processor.d.ts +5 -0
  18. package/dist/main/processor.js +4 -0
  19. package/dist/main/transformer.d.ts +1 -0
  20. package/dist/main/transformer.js +4 -0
  21. package/dist/main/tsconfig.tsbuildinfo +1 -0
  22. package/dist/main/typeGuards.d.ts +5 -0
  23. package/dist/main/typeGuards.js +24 -0
  24. package/dist/main/types.d.ts +342 -0
  25. package/dist/main/types.js +1 -0
  26. package/dist/main/utils.d.ts +173 -0
  27. package/dist/main/utils.js +364 -0
  28. package/dist/main/video/canvasRenderUtils.d.ts +22 -0
  29. package/dist/main/video/canvasRenderUtils.js +198 -0
  30. package/dist/main/video/canvasTransform.d.ts +8 -0
  31. package/dist/main/video/canvasTransform.js +173 -0
  32. package/dist/main/video/constants.d.ts +10 -0
  33. package/dist/main/video/constants.js +12 -0
  34. package/dist/main/video/index.d.ts +9 -0
  35. package/dist/main/video/index.js +9 -0
  36. package/dist/main/video/load.d.ts +17 -0
  37. package/dist/main/video/load.js +47 -0
  38. package/dist/main/video/segmenters/index.d.ts +1 -0
  39. package/dist/main/video/segmenters/index.js +1 -0
  40. package/dist/main/video/segmenters/mediapipe.d.ts +13 -0
  41. package/dist/main/video/segmenters/mediapipe.js +85 -0
  42. package/dist/main/video/transformer.d.ts +10 -0
  43. package/dist/main/video/transformer.js +56 -0
  44. package/dist/main/video/typeGuards.d.ts +2 -0
  45. package/dist/main/video/typeGuards.js +13 -0
  46. package/dist/main/video/types.d.ts +98 -0
  47. package/dist/main/video/types.js +20 -0
  48. package/dist/main/video/utils.d.ts +102 -0
  49. package/dist/main/video/utils.js +476 -0
  50. package/dist/main/video/video.d.ts +19 -0
  51. package/dist/main/video/video.js +48 -0
  52. package/dist/main/video/videoStreamTrackProcessor.d.ts +14 -0
  53. package/dist/main/video/videoStreamTrackProcessor.js +82 -0
  54. package/dist/main/visual.d.ts +80 -0
  55. package/dist/main/visual.js +135 -0
  56. package/dist/main/workletNodes.d.ts +2 -0
  57. package/dist/main/workletNodes.js +3 -0
  58. package/dist/workers/index.d.ts +0 -0
  59. package/dist/workers/index.js +1 -0
  60. package/dist/workers/tsconfig.tsbuildinfo +1 -0
  61. package/dist/worklets/denoise.worklet.d.ts +1 -0
  62. package/dist/worklets/denoise.worklet.js +1 -2
  63. package/dist/worklets/tsconfig.tsbuildinfo +1 -0
  64. package/dist/worklets/types.d.ts +52 -0
  65. package/dist/worklets/types.js +0 -0
  66. package/package.json +10 -8
  67. package/dist/index.d.ts +0 -1129
  68. package/dist/index.mjs +0 -2441
  69. 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 {};