@pexip/media-processor 21.0.0 → 22.1.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 (170) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +17 -10
  3. package/api-docs/README.mdx +40 -19
  4. package/api-docs/functions/assertMediaStreamVideoTrack.mdx +13 -0
  5. package/api-docs/functions/avg.mdx +1 -1
  6. package/api-docs/functions/createAnalyzerGraphNode.mdx +3 -1
  7. package/api-docs/functions/createAnalyzerSubscribableGraphNode.mdx +3 -1
  8. package/api-docs/functions/createAsyncCallbackLoop.mdx +6 -6
  9. package/api-docs/functions/createAudioDestinationGraphNode.mdx +1 -1
  10. package/api-docs/functions/createAudioSignalDetector.mdx +4 -4
  11. package/api-docs/functions/createAudioStats.mdx +1 -1
  12. package/api-docs/functions/createChannelMergerGraphNode.mdx +1 -1
  13. package/api-docs/functions/createChannelSplitterGraphNode.mdx +1 -1
  14. package/api-docs/functions/createDelayGraphNode.mdx +1 -1
  15. package/api-docs/functions/createDenoiseWorkletGraphNode.mdx +2 -2
  16. package/api-docs/functions/createFrameCallbackRequest.mdx +10 -4
  17. package/api-docs/functions/createGainGraphNode.mdx +3 -1
  18. package/api-docs/functions/createMediaElementSourceGraphNode.mdx +1 -1
  19. package/api-docs/functions/createMediaStreamTrackInputProcessAdaptor.mdx +10 -0
  20. package/api-docs/functions/createReadWritableStreamFallbackInputProcessAdaptor.mdx +17 -0
  21. package/api-docs/functions/createReadWritableStreamInputProcessAdaptor.mdx +10 -0
  22. package/api-docs/functions/createSegmenter.mdx +1 -1
  23. package/api-docs/functions/createStreamDestinationGraphNode.mdx +1 -1
  24. package/api-docs/functions/createStreamSourceGraphNode.mdx +1 -1
  25. package/api-docs/functions/createVADetector.mdx +3 -3
  26. package/api-docs/functions/createVideoProcessor.mdx +3 -3
  27. package/api-docs/functions/createVoiceDetectorFromProbability.mdx +1 -1
  28. package/api-docs/functions/createVoiceDetectorFromTimeData.mdx +3 -1
  29. package/api-docs/functions/createWindowedStats.mdx +2 -2
  30. package/api-docs/functions/getBezierCurveControlPoints.mdx +3 -1
  31. package/api-docs/functions/getDefaultInputProcessAdaptor.mdx +17 -0
  32. package/api-docs/functions/isMediaStreamVideoTrack.mdx +13 -0
  33. package/api-docs/functions/labelComponents.mdx +3 -1
  34. package/api-docs/functions/loadScript.mdx +1 -1
  35. package/api-docs/functions/loadWasms.mdx +1 -1
  36. package/api-docs/functions/nearestPowerOfTwo.mdx +1 -1
  37. package/api-docs/functions/playVideo.mdx +7 -5
  38. package/api-docs/functions/processAverageVolume.mdx +1 -1
  39. package/api-docs/functions/rms.mdx +1 -1
  40. package/api-docs/functions/subscribeWorkletNode.mdx +1 -1
  41. package/api-docs/functions/sum.mdx +1 -1
  42. package/api-docs/interfaces/Analyzer.mdx +4 -0
  43. package/api-docs/interfaces/AnalyzerSubscribableOptions.mdx +5 -3
  44. package/api-docs/interfaces/AudioGraph.mdx +2 -2
  45. package/api-docs/interfaces/AudioNodeInit.mdx +10 -6
  46. package/api-docs/interfaces/AudioNodeProps.mdx +3 -3
  47. package/api-docs/interfaces/Benchmark.mdx +17 -3
  48. package/api-docs/interfaces/Clock.mdx +1 -1
  49. package/api-docs/interfaces/Denoise.mdx +2 -2
  50. package/api-docs/interfaces/FrameTransformStats.mdx +9 -0
  51. package/api-docs/interfaces/Gain.mdx +4 -0
  52. package/api-docs/interfaces/ImageSegmenterOptions.mdx +0 -2
  53. package/api-docs/interfaces/InputProcessAdaptor.mdx +32 -0
  54. package/api-docs/interfaces/MPMask.mdx +1 -1
  55. package/api-docs/interfaces/Process.mdx +12 -7
  56. package/api-docs/interfaces/ProcessEventMute.mdx +6 -0
  57. package/api-docs/interfaces/ProcessWorkerDebugMessage.mdx +1 -1
  58. package/api-docs/interfaces/ProcessWorkerEventClosed.mdx +5 -0
  59. package/api-docs/interfaces/ProcessWorkerEventOpened.mdx +7 -0
  60. package/api-docs/interfaces/ProcessWorkerEventStats.mdx +6 -0
  61. package/api-docs/interfaces/ProcessWorkerEventUpdated.mdx +7 -0
  62. package/api-docs/interfaces/ProcessorEvent.mdx +3 -2
  63. package/api-docs/interfaces/ProcessorOptions.mdx +0 -1
  64. package/api-docs/interfaces/ProcessorProcessOptions.mdx +3 -1
  65. package/api-docs/interfaces/ProcessorUpdateOptions.mdx +17 -0
  66. package/api-docs/interfaces/ReadWritableStream.mdx +12 -0
  67. package/api-docs/interfaces/RenderEventHandlers.mdx +3 -3
  68. package/api-docs/interfaces/Renderer.mdx +5 -3
  69. package/api-docs/interfaces/RendererOptions.mdx +5 -9
  70. package/api-docs/interfaces/Runner.mdx +3 -3
  71. package/api-docs/interfaces/Segmentation.mdx +1 -1
  72. package/api-docs/interfaces/Segmenter.mdx +115 -16
  73. package/api-docs/interfaces/SegmenterOptions.mdx +0 -1
  74. package/api-docs/interfaces/VideoFrameLike.mdx +1 -1
  75. package/api-docs/interfaces/VideoProcessor.mdx +52 -8
  76. package/api-docs/interfaces/WorkletMessagePortOptions.mdx +2 -2
  77. package/api-docs/type-aliases/AsyncCallback.mdx +1 -1
  78. package/api-docs/type-aliases/Callback.mdx +2 -2
  79. package/api-docs/type-aliases/ExtractMessageEventType.mdx +1 -1
  80. package/api-docs/type-aliases/IncludeMessageEventDataType.mdx +1 -1
  81. package/api-docs/type-aliases/Logger.mdx +12 -0
  82. package/api-docs/type-aliases/OptionalKeys.mdx +1 -1
  83. package/api-docs/type-aliases/ProcessEvents.mdx +1 -0
  84. package/api-docs/type-aliases/ProcessInputType.mdx +3 -1
  85. package/api-docs/type-aliases/ProcessWorkerEvents.mdx +7 -4
  86. package/api-docs/type-aliases/ProcessorWorkerEvents.mdx +1 -1
  87. package/api-docs/type-aliases/RunnerCreator.mdx +3 -3
  88. package/api-docs/type-aliases/Struct.mdx +9 -0
  89. package/api-docs/type-aliases/TupleOf.mdx +1 -1
  90. package/api-docs/variables/FRAME_RATE.mdx +1 -1
  91. package/api-docs/variables/ImageFetchError.mdx +3 -0
  92. package/api-docs/variables/MediaStreamTrackTransferable.mdx +5 -0
  93. package/api-docs/variables/RENDERER_OPTIONS_STRUCT.mdx +3 -0
  94. package/api-docs/variables/cloneImageRecord.mdx +21 -0
  95. package/api-docs/variables/compareImageRecords.mdx +18 -0
  96. package/api-docs/variables/createLazyProps.mdx +1 -1
  97. package/api-docs/variables/createObjectUpdater.mdx +2 -2
  98. package/api-docs/variables/createRemoteImageBitmap.mdx +5 -5
  99. package/api-docs/variables/handleWebGLContextLoss.mdx +1 -1
  100. package/api-docs/variables/isRenderOptionsKey.mdx +13 -0
  101. package/api-docs/variables/isValidGpuAPI.mdx +13 -0
  102. package/api-docs/variables/urls.mdx +2 -2
  103. package/dist/common/backends/webgl/renderer.js +2 -12
  104. package/dist/common/backends/webgl/texture.d.ts +1 -2
  105. package/dist/common/backends/webgl/texture.js +2 -2
  106. package/dist/common/backends/webgl/webglUtils.js +8 -4
  107. package/dist/common/backends/webgpu/renderer.js +2 -12
  108. package/dist/common/backends/webgpu/tentBlur.js +1 -1
  109. package/dist/common/backends/webgpu/texture.d.ts +1 -2
  110. package/dist/common/backends/webgpu/texture.js +6 -2
  111. package/dist/common/constants.d.ts +3 -0
  112. package/dist/common/constants.js +4 -0
  113. package/dist/common/index.d.ts +1 -0
  114. package/dist/common/index.js +1 -0
  115. package/dist/common/tsconfig.tsbuildinfo +1 -1
  116. package/dist/common/typeGuards.d.ts +2 -0
  117. package/dist/common/typeGuards.js +7 -0
  118. package/dist/common/types/media.d.ts +5 -1
  119. package/dist/common/types/messageEvents.d.ts +34 -4
  120. package/dist/common/types/processor.d.ts +7 -4
  121. package/dist/common/types/render.d.ts +5 -2
  122. package/dist/common/types/render.js +23 -1
  123. package/dist/common/types/segmentation.d.ts +0 -12
  124. package/dist/common/utils.d.ts +13 -2
  125. package/dist/common/utils.js +54 -10
  126. package/dist/main/audio.js +3 -3
  127. package/dist/main/benchUtils.d.ts +2 -2
  128. package/dist/main/benchUtils.js +4 -4
  129. package/dist/main/index.d.ts +1 -1
  130. package/dist/main/index.js +1 -1
  131. package/dist/main/tsconfig.tsbuildinfo +1 -1
  132. package/dist/main/types.d.ts +0 -6
  133. package/dist/main/utils.d.ts +4 -0
  134. package/dist/main/utils.js +31 -0
  135. package/dist/main/video/constants.d.ts +1 -4
  136. package/dist/main/video/constants.js +1 -5
  137. package/dist/main/video/index.d.ts +2 -3
  138. package/dist/main/video/index.js +2 -3
  139. package/dist/main/video/inputProcessAdaptor.d.ts +30 -0
  140. package/dist/main/video/inputProcessAdaptor.js +198 -0
  141. package/dist/main/video/segmenter.d.ts +42 -0
  142. package/dist/main/video/{segmenters/mediapipe.js → segmenter.js} +130 -75
  143. package/dist/main/video/typeGuards.d.ts +2 -0
  144. package/dist/main/video/typeGuards.js +8 -1
  145. package/dist/main/video/types.d.ts +37 -17
  146. package/dist/main/video/utils.d.ts +5 -4
  147. package/dist/main/video/utils.js +49 -44
  148. package/dist/main/video/video.d.ts +30 -8
  149. package/dist/main/video/video.js +338 -32
  150. package/dist/workers/mediaWorker.js +24 -24
  151. package/dist/workers/tsconfig.tsbuildinfo +1 -1
  152. package/dist/worklets/tsconfig.tsbuildinfo +1 -1
  153. package/package.json +8 -8
  154. package/api-docs/functions/createCanvasTransform.mdx +0 -14
  155. package/api-docs/functions/createVideoTrackProcessor.mdx +0 -9
  156. package/api-docs/functions/createVideoTrackProcessorWithFallback.mdx +0 -15
  157. package/api-docs/interfaces/Detector.mdx +0 -81
  158. package/api-docs/interfaces/SegmentationParams.mdx +0 -35
  159. package/api-docs/interfaces/Transform.mdx +0 -62
  160. package/api-docs/type-aliases/ProcessVideoTrack.mdx +0 -15
  161. package/api-docs/type-aliases/SegmentationTransform.mdx +0 -3
  162. package/dist/main/transformer.d.ts +0 -1
  163. package/dist/main/transformer.js +0 -4
  164. package/dist/main/video/canvasTransform.d.ts +0 -17
  165. package/dist/main/video/canvasTransform.js +0 -341
  166. package/dist/main/video/segmenters/mediapipe.d.ts +0 -22
  167. package/dist/main/video/transformer.d.ts +0 -9
  168. package/dist/main/video/transformer.js +0 -68
  169. package/dist/main/video/videoStreamTrackProcessor.d.ts +0 -20
  170. package/dist/main/video/videoStreamTrackProcessor.js +0 -86
@@ -1,9 +1,8 @@
1
1
  import type { ProcessStatus, RenderingEvents } from '../../common/constants';
2
- import type { ProcessInputType, VideoFrameLike } from '../../common/types/media';
2
+ import type { ReadWritableStream } from '../../common/types/media';
3
3
  import type { RendererOptions } from '../../common/types/render';
4
4
  import type { SegmentationModelAsset } from '../../common/types/segmentation';
5
5
  import type { ProcessorOptions } from '../../common/types/processor';
6
- import type { Transform } from '../types';
7
6
  export type Color = {
8
7
  r: number;
9
8
  g: number;
@@ -16,10 +15,10 @@ export interface AsyncAssets {
16
15
  tfjsBackendLoaded: boolean;
17
16
  glueLoaded: boolean;
18
17
  }
19
- export interface Process {
18
+ export interface Process<T> {
20
19
  status: ProcessStatus;
21
- open(): Promise<void>;
22
- close(): void;
20
+ open(): Promise<T>;
21
+ close(): Promise<void>;
23
22
  destroy(): Promise<void>;
24
23
  }
25
24
  export interface BackgroundImage {
@@ -28,21 +27,42 @@ export interface BackgroundImage {
28
27
  }
29
28
  type Unsubscribe = () => void;
30
29
  type Callback<T> = (args: T) => void;
31
- export interface Segmenter extends Process {
30
+ export interface Segmenter extends Process<Partial<RendererOptions>> {
32
31
  modelAsset: SegmentationModelAsset;
33
32
  backgroundImage?: BackgroundImage;
34
- open(options?: ProcessorOptions): Promise<void>;
35
- update(options: ProcessorOptions): Promise<void>;
36
- process(input: VideoFrameLike, options?: RendererOptions): Promise<VideoFrameLike | undefined>;
33
+ /**
34
+ * Initialize the Segmenter as well as the underlying worker.
35
+ * It is expected to be called once.
36
+ */
37
+ open(options?: ProcessorOptions): Promise<Partial<RendererOptions>>;
38
+ /**
39
+ * Update the processor options
40
+ */
41
+ update(options: ProcessorOptions): Promise<Partial<RendererOptions>>;
42
+ /**
43
+ * Mute/unmute the track in the worker
44
+ */
45
+ mute(muted: boolean): void;
46
+ /**
47
+ * Process provided input
48
+ */
49
+ process(input: MediaStreamTrack, options?: RendererOptions): Promise<MediaStreamTrack>;
50
+ process(input: ReadWritableStream<VideoFrame>, options?: RendererOptions): Promise<ReadWritableStream<VideoFrame> | undefined>;
51
+ process(input: MediaStreamTrack | ReadWritableStream<VideoFrame>, options?: RendererOptions): Promise<MediaStreamTrack | ReadWritableStream<VideoFrame> | undefined>;
52
+ /**
53
+ * Subscribe rendering events, @see RenderingEvents
54
+ */
37
55
  subscribe(event: RenderingEvents, callback: Callback<void>): Unsubscribe;
38
56
  }
39
- export interface Detector<T> extends Process {
40
- detect(input: ProcessInputType): Promise<T>;
57
+ export interface InputProcessAdaptor {
58
+ /**
59
+ * Adapt the provided input track based on browser capabilities.
60
+ */
61
+ adapt(track: MediaStreamTrack): MediaStreamTrack | ReadWritableStream<VideoFrame>;
62
+ /**
63
+ * The generated track from the `adapt` function, and only available
64
+ * after calling the `adapt` method otherwise `undefined`
65
+ */
66
+ outputTrack?: MediaStreamTrack;
41
67
  }
42
- export interface SegmentationParams extends RendererOptions {
43
- segmenter: Segmenter;
44
- width: number;
45
- height: number;
46
- }
47
- export type SegmentationTransform = Transform<InputFrame, InputFrame> & SegmentationParams & Pick<Segmenter, 'update'> & Omit<Process, 'open'>;
48
68
  export {};
@@ -58,13 +58,14 @@ export declare const toVideoElement: (input: MediaStream, width: number, height:
58
58
  export declare const playVideo: (video: HTMLVideoElement, { setTimeout, clearTimeout }?: {
59
59
  setTimeout?: (((handler: TimerHandler, timeout?: number, ...arguments: any[]) => number) & typeof globalThis.setTimeout) | undefined;
60
60
  clearTimeout?: (((id: number | undefined) => void) & typeof globalThis.clearTimeout) | undefined;
61
- }) => Promise<void>;
61
+ }) => Promise<[void, void]> | undefined;
62
62
  /**
63
63
  * Load image with provided image element
64
64
  */
65
65
  export declare const loadImage: (image: HTMLImageElement, src: string) => Promise<void>;
66
66
  export declare const toNumber: (value: number | SVGAnimatedLength) => number;
67
- export declare const getCanvasRenderingContext2D: (canvas: Canvas, options?: CanvasRenderingContext2DSettings) => CanvasRenderingContext2D;
67
+ export declare function getCanvasRenderingContext2D(canvas: HTMLCanvasElement, options?: CanvasRenderingContext2DSettings): CanvasRenderingContext2D;
68
+ export declare function getCanvasRenderingContext2D(canvas: OffscreenCanvas, options?: CanvasRenderingContext2DSettings): OffscreenCanvasRenderingContext2D;
68
69
  export declare const hasVideoFrameSupport: () => boolean;
69
70
  export declare const isVideoFrame: (t: unknown) => t is VideoFrame;
70
71
  export declare const flipCanvasHorizontal: (canvas: Canvas) => void;
@@ -89,8 +90,8 @@ interface FrameCallbackRequestOptions {
89
90
  * @param frameRate - A fallback frame rate when we are not able to get the rate
90
91
  * from API
91
92
  */
92
- export declare const createFrameCallbackRequest: (callback: Callback<Promise<void>, [ProcessInputType]>, frameRate: number, { subscribeVisibilityChange, }?: FrameCallbackRequestOptions) => {
93
- start: (input: ProcessInputType) => Promise<void>;
93
+ export declare const createFrameCallbackRequest: <T>(callback: Callback<Promise<void>, [T]>, frameRate: number, { subscribeVisibilityChange, }?: FrameCallbackRequestOptions) => {
94
+ start: (input: T) => Promise<void>;
94
95
  stop: () => void;
95
96
  frameRate: number;
96
97
  };
@@ -126,52 +126,57 @@ var HTMLMediaElementReadyState;
126
126
  HTMLMediaElementReadyState[HTMLMediaElementReadyState["HaveFutureData"] = 3] = "HaveFutureData";
127
127
  HTMLMediaElementReadyState[HTMLMediaElementReadyState["HaveEnoughData"] = 4] = "HaveEnoughData";
128
128
  })(HTMLMediaElementReadyState || (HTMLMediaElementReadyState = {}));
129
- export const playVideo = async (video, { setTimeout = window.setTimeout, clearTimeout = window.clearTimeout } = {}) => {
129
+ export const playVideo = (video, { setTimeout = window.setTimeout, clearTimeout = window.clearTimeout } = {}) => {
130
130
  if (video.autoplay) {
131
131
  return;
132
132
  }
133
- if (video.readyState <
134
- HTMLMediaElementReadyState.HaveCurrentData) {
135
- // Not enough data, wait for the data and then play
136
- await new Promise((resolve, reject) => {
137
- const clearTimeoutTimer = () => {
138
- clearTimeout(timeoutId);
139
- timeoutId = 0;
140
- };
141
- const unsubscribe = () => {
142
- video.removeEventListener('loadeddata', waitForEvent);
143
- video.removeEventListener('error', handleError);
144
- video.removeEventListener('stalled', handleStalled);
145
- video.removeEventListener('ended', handleEnded);
146
- clearTimeoutTimer();
147
- };
148
- const handleEnded = () => {
149
- unsubscribe();
150
- reject(new Error('VideoEnded', { cause: video }));
151
- };
152
- const handleStalled = () => {
153
- unsubscribe();
154
- reject(new Error('VideoStalled', { cause: video }));
155
- };
156
- const handleError = (error) => {
157
- unsubscribe();
158
- reject(error.error);
159
- };
160
- const waitForEvent = () => {
161
- unsubscribe();
133
+ return Promise.all([
134
+ video.play(),
135
+ new Promise((resolve, reject) => {
136
+ if (video.readyState <
137
+ HTMLMediaElementReadyState.HaveCurrentData) {
138
+ // Not enough data, wait for the data and then play
139
+ const clearTimeoutTimer = () => {
140
+ clearTimeout(timeoutId);
141
+ timeoutId = 0;
142
+ };
143
+ const unsubscribe = () => {
144
+ video.removeEventListener('loadeddata', waitForEvent);
145
+ video.removeEventListener('error', handleError);
146
+ video.removeEventListener('stalled', handleStalled);
147
+ video.removeEventListener('ended', handleEnded);
148
+ clearTimeoutTimer();
149
+ };
150
+ const handleEnded = () => {
151
+ unsubscribe();
152
+ reject(new Error('VideoEnded', { cause: video }));
153
+ };
154
+ const handleStalled = () => {
155
+ unsubscribe();
156
+ reject(new Error('VideoStalled', { cause: video }));
157
+ };
158
+ const handleError = (error) => {
159
+ unsubscribe();
160
+ reject(error.error);
161
+ };
162
+ const waitForEvent = () => {
163
+ unsubscribe();
164
+ resolve();
165
+ };
166
+ // Use setTimeout to prevent a race to `loadeddata` event
167
+ video.addEventListener('loadeddata', waitForEvent);
168
+ video.addEventListener('error', handleError);
169
+ video.addEventListener('stalled', handleStalled);
170
+ video.addEventListener('ended', handleEnded);
171
+ let timeoutId = setTimeout(() => {
172
+ reject(new Error('PlayVideoTimeout', { cause: video }));
173
+ }, PLAY_VIDEO_TIMEOUT);
174
+ }
175
+ else {
162
176
  resolve();
163
- };
164
- // Use setTimeout to prevent a race to `loadeddata` event
165
- video.addEventListener('loadeddata', waitForEvent);
166
- video.addEventListener('error', handleError);
167
- video.addEventListener('stalled', handleStalled);
168
- video.addEventListener('ended', handleEnded);
169
- let timeoutId = setTimeout(() => {
170
- reject(new Error('PlayVideoTimeout', { cause: video }));
171
- }, PLAY_VIDEO_TIMEOUT);
172
- });
173
- }
174
- return await video.play();
177
+ }
178
+ }),
179
+ ]);
175
180
  };
176
181
  /**
177
182
  * Load image with provided image element
@@ -184,13 +189,13 @@ export const loadImage = async (image, src) => {
184
189
  });
185
190
  };
186
191
  export const toNumber = (value) => value instanceof SVGAnimatedLength ? value.baseVal.value : value;
187
- export const getCanvasRenderingContext2D = (canvas, options) => {
192
+ export function getCanvasRenderingContext2D(canvas, options) {
188
193
  const context = canvas.getContext('2d', options);
189
194
  if (!context) {
190
195
  throw new Error('Cannot get CanvasRenderingContext2D');
191
196
  }
192
197
  return context;
193
- };
198
+ }
194
199
  export const hasVideoFrameSupport = () => 'VideoFrame' in window;
195
200
  export const isVideoFrame = (t) => {
196
201
  if (hasVideoFrameSupport() && t instanceof VideoFrame) {
@@ -1,20 +1,42 @@
1
- import type { Transform } from '../types';
2
- import type { InputFrame } from './types';
3
- import type { ProcessVideoTrack } from './videoStreamTrackProcessor';
4
- import type { ProcessorOptions } from '../../common';
1
+ import type { ProcessorOptions, ProcessStatus, RendererOptions } from '../../common';
2
+ import type { Segmenter, InputProcessAdaptor } from './types';
3
+ export interface ProcessorUpdateOptions extends Omit<ProcessorOptions, 'renderOptions'> {
4
+ renderOptions?: Partial<RendererOptions>;
5
+ }
5
6
  export interface VideoProcessor {
6
- open(options?: ProcessorOptions): Promise<void>;
7
+ readonly status: ProcessStatus;
8
+ /**
9
+ * Initialize the video processor and related
10
+ */
11
+ open(options?: ProcessorUpdateOptions): Promise<void>;
12
+ /**
13
+ * Update the video processor with provided options
14
+ */
15
+ update(options: ProcessorUpdateOptions): Promise<void>;
16
+ mute(muted: boolean): void;
7
17
  /**
8
18
  * Process the video and return a MediaStream
9
19
  */
10
- process: (source: MediaStream) => Promise<MediaStream>;
20
+ process: (source: MediaStreamTrack) => Promise<MediaStreamTrack>;
11
21
  /**
12
22
  * Stop the process of video segmentation
13
23
  */
14
- close(): void;
24
+ close(): Promise<void>;
15
25
  /**
16
26
  * Destroy the segmentation wasm instance
17
27
  */
18
28
  destroy: () => Promise<void>;
29
+ /**
30
+ * Current renderer options
31
+ */
32
+ readonly renderOptions: RendererOptions;
33
+ /**
34
+ * Current processing width
35
+ */
36
+ readonly processingWidth: number;
37
+ /**
38
+ * Current processing height
39
+ */
40
+ readonly processingHeight: number;
19
41
  }
20
- export declare const createVideoProcessor: (transformers: Transform<InputFrame, InputFrame>[], processTrack: ProcessVideoTrack) => VideoProcessor;
42
+ export declare const createVideoProcessor: (segmenter: Segmenter, inputProcessAdaptor: InputProcessAdaptor) => VideoProcessor;
@@ -1,51 +1,357 @@
1
- import { stopStreamTracks } from '../utils';
2
- import { AbortReason } from './constants';
3
- export const createVideoProcessor = (transformers, processTrack) => {
1
+ import { assert, isEmpty } from '@pexip/utils';
2
+ import { BACKGROUND_BLUR_AMOUNT, BACKGROUND_THRESHOLD, DOWN_SAMPLE_FACTOR, EDGE_BLUR_AMOUNT, EXCLUDE_BYSTANDERS, FOREGROUND_THRESHOLD, LIGHT_WRAP_BLUR_AMOUNT, LIGHT_WRAP_EDGE_BAND, LIGHT_WRAP_INTENSITY, LIGHT_WRAP_TIGHTNESS, MASK_COMBINE_RATIO, MORPH_DILATE_RADIUS_PX, MORPH_ERODE_RADIUS_PX, MORPH_PASS, PERSON_CENTER, PROCESSING_HEIGHT, PROCESSING_WIDTH, SIGMA_RANGE, SIGMA_SPACE, } from '../../common/constants';
3
+ import { clamping, createObjectUpdater, compareImageRecords, } from '../../common/utils';
4
+ import { assertMediaStreamVideoTrack, isRenderEffects } from './typeGuards';
5
+ const clampInRangeFrom0To1 = clamping(0, 1);
6
+ const clampInRangeFrom0To9 = clamping(0, 9);
7
+ const createRenderOptions = ({ foregroundThreshold = FOREGROUND_THRESHOLD, backgroundThreshold = BACKGROUND_THRESHOLD, backgroundBlurAmount = BACKGROUND_BLUR_AMOUNT, edgeBlurAmount = EDGE_BLUR_AMOUNT, maskCombineRatio = MASK_COMBINE_RATIO, backgroundImageUrl = '', sigmaSpace = SIGMA_SPACE, sigmaRangeLo = SIGMA_RANGE[0], sigmaRangeHi = SIGMA_RANGE[1], effects = 'none', backend = 'webgl', personCenter = PERSON_CENTER, excludeBystanders = EXCLUDE_BYSTANDERS, morphPass = MORPH_PASS, morphErodeRadiusPx = MORPH_ERODE_RADIUS_PX, morphDilateRadiusPx = MORPH_DILATE_RADIUS_PX, lightWrapIntensity = LIGHT_WRAP_INTENSITY, downSampleFactor = DOWN_SAMPLE_FACTOR, lightWrapTightness = LIGHT_WRAP_TIGHTNESS, lightWrapBlurAmount = LIGHT_WRAP_BLUR_AMOUNT, lightWrapEdgeBand = LIGHT_WRAP_EDGE_BAND, } = {}, onChange) => {
8
+ const props = {
9
+ foregroundThreshold,
10
+ backgroundThreshold,
11
+ backgroundBlurAmount,
12
+ edgeBlurAmount,
13
+ maskCombineRatio,
14
+ backgroundImageUrl,
15
+ sigmaSpace,
16
+ sigmaRangeLo,
17
+ sigmaRangeHi,
18
+ effects,
19
+ backend,
20
+ personCenter,
21
+ excludeBystanders,
22
+ morphPass,
23
+ morphErodeRadiusPx,
24
+ morphDilateRadiusPx,
25
+ lightWrapIntensity,
26
+ downSampleFactor,
27
+ lightWrapTightness,
28
+ lightWrapBlurAmount,
29
+ lightWrapEdgeBand,
30
+ };
31
+ return {
32
+ original: props,
33
+ proxy: new Proxy(props, {
34
+ get(target, p, receiver) {
35
+ return Reflect.get(target, p, receiver);
36
+ },
37
+ set(target, p, newValue, receiver) {
38
+ switch (p) {
39
+ case 'backgroundThreshold': {
40
+ const value = clamping(0, target.foregroundThreshold)(newValue);
41
+ if (value !== target[p]) {
42
+ Reflect.set(target, p, value, receiver);
43
+ onChange(p, value);
44
+ }
45
+ break;
46
+ }
47
+ case 'foregroundThreshold': {
48
+ const value = clamping(target.backgroundThreshold, 1)(newValue);
49
+ if (value !== target[p]) {
50
+ Reflect.set(target, p, value, receiver);
51
+ onChange(p, value);
52
+ }
53
+ break;
54
+ }
55
+ case 'maskCombineRatio': {
56
+ const value = clampInRangeFrom0To1(newValue);
57
+ if (value !== target[p]) {
58
+ Reflect.set(target, p, value, receiver);
59
+ onChange(p, value);
60
+ }
61
+ break;
62
+ }
63
+ case 'lightWrapBlurAmount':
64
+ case 'backgroundBlurAmount':
65
+ case 'edgeBlurAmount': {
66
+ // Expecting integer number
67
+ const value = Math.trunc(clampInRangeFrom0To9(newValue));
68
+ if (value !== target[p]) {
69
+ Reflect.set(target, p, value, receiver);
70
+ onChange(p, value);
71
+ }
72
+ break;
73
+ }
74
+ case 'backgroundImageUrl': {
75
+ if (newValue !== target[p]) {
76
+ Reflect.set(target, p, newValue, receiver);
77
+ onChange(p, newValue);
78
+ }
79
+ break;
80
+ }
81
+ case 'effects': {
82
+ if (isRenderEffects(newValue) &&
83
+ newValue !== target[p]) {
84
+ Reflect.set(target, p, newValue, receiver);
85
+ onChange(p, newValue);
86
+ }
87
+ break;
88
+ }
89
+ case 'backend': {
90
+ if ((newValue === 'webgl' || newValue === 'webgpu') &&
91
+ newValue !== target[p]) {
92
+ Reflect.set(target, p, newValue, receiver);
93
+ onChange(p, newValue);
94
+ }
95
+ break;
96
+ }
97
+ case 'sigmaSpace': {
98
+ // less than 0 is unknown behavior, more than 12 is too much
99
+ const value = clamping(0, 12)(newValue);
100
+ if (value !== target.sigmaSpace) {
101
+ Reflect.set(target, p, value, receiver);
102
+ onChange(p, value);
103
+ }
104
+ break;
105
+ }
106
+ case 'sigmaRangeLo': {
107
+ const value = clamping(0, target.sigmaRangeHi)(newValue);
108
+ if (value !== target.sigmaRangeLo) {
109
+ Reflect.set(target, p, value, receiver);
110
+ onChange(p, value);
111
+ }
112
+ break;
113
+ }
114
+ case 'sigmaRangeHi': {
115
+ const value = clamping(target.sigmaRangeLo, 1)(newValue);
116
+ if (value !== target.sigmaRangeHi) {
117
+ Reflect.set(target, p, value, receiver);
118
+ onChange(p, value);
119
+ }
120
+ break;
121
+ }
122
+ case 'personCenter': {
123
+ if (Array.isArray(newValue)) {
124
+ const [x, y] = newValue.map(clampInRangeFrom0To1);
125
+ if (x !== undefined &&
126
+ y !== undefined &&
127
+ (x !== target.personCenter[0] ||
128
+ y !== target.personCenter[1])) {
129
+ Reflect.set(target, p, [x, y], receiver);
130
+ onChange(p, [x, y]);
131
+ }
132
+ }
133
+ break;
134
+ }
135
+ case 'excludeBystanders': {
136
+ if (newValue !== target[p]) {
137
+ Reflect.set(target, p, newValue, receiver);
138
+ onChange(p, newValue);
139
+ }
140
+ break;
141
+ }
142
+ case 'morphDilateRadiusPx':
143
+ case 'morphErodeRadiusPx': {
144
+ const value = clamping(0, 12)(newValue);
145
+ if (value !== target[p]) {
146
+ Reflect.set(target, p, value, receiver);
147
+ onChange(p, value);
148
+ }
149
+ break;
150
+ }
151
+ case 'morphPass': {
152
+ // Expecting integer number
153
+ const value = Math.trunc(clamping(0, 10)(newValue));
154
+ if (value !== target[p]) {
155
+ Reflect.set(target, p, value, receiver);
156
+ onChange(p, value);
157
+ }
158
+ break;
159
+ }
160
+ case 'lightWrapIntensity': {
161
+ const value = clampInRangeFrom0To1(newValue);
162
+ if (value !== target[p]) {
163
+ Reflect.set(target, p, value, receiver);
164
+ onChange(p, value);
165
+ }
166
+ break;
167
+ }
168
+ case 'lightWrapTightness': {
169
+ // According to the doc
170
+ const value = clamping(0.5, 4)(newValue);
171
+ if (value !== target[p]) {
172
+ Reflect.set(target, p, value, receiver);
173
+ onChange(p, value);
174
+ }
175
+ break;
176
+ }
177
+ case 'lightWrapEdgeBand': {
178
+ const value = clamping(0, (target.foregroundThreshold -
179
+ target.backgroundThreshold) /
180
+ 2)(newValue);
181
+ if (value !== target[p]) {
182
+ Reflect.set(target, p, value, receiver);
183
+ onChange(p, value);
184
+ }
185
+ break;
186
+ }
187
+ case 'downSampleFactor': {
188
+ // NOTE: There is no impl other than 2 and 4, so we just hardcode it here
189
+ if ((newValue === 2 || newValue === 4) &&
190
+ newValue !== target[p]) {
191
+ Reflect.set(target, p, newValue, receiver);
192
+ onChange(p, newValue);
193
+ }
194
+ break;
195
+ }
196
+ default: {
197
+ // Catch all the unknown keys, and just ignore the set operation
198
+ return false;
199
+ }
200
+ }
201
+ // No throw
202
+ return true;
203
+ },
204
+ }),
205
+ };
206
+ };
207
+ export const createVideoProcessor = (segmenter, inputProcessAdaptor) => {
208
+ const canUpdate = () => segmenter.status !== 'new';
4
209
  const props = {
5
210
  processing: false,
6
211
  };
7
- const open = async (options) => {
8
- await Promise.all(transformers.map(transformer => transformer.init(options)));
212
+ let changedRenderOptions = {};
213
+ const initRenderOptions = () => createRenderOptions({}, (k, v) => {
214
+ changedRenderOptions[k] = v;
215
+ });
216
+ let renderOptions = initRenderOptions();
217
+ let processorOptions = {
218
+ renderOptions: renderOptions.original,
219
+ processingWidth: PROCESSING_WIDTH,
220
+ processingHeight: PROCESSING_HEIGHT,
9
221
  };
10
- const process = async (source) => {
11
- const [track] = source.getVideoTracks();
12
- if (!track) {
13
- return source;
222
+ const updateRendererOptions = createObjectUpdater(renderOptions.proxy);
223
+ const updateChangedRendererOptions = (changed) => {
224
+ for (const [k, v] of Object.entries(changed)) {
225
+ //@ts-expect-error
226
+ processorOptions.renderOptions[k] = v;
227
+ }
228
+ };
229
+ const updateProcessorOptions = async (options) => {
230
+ const _processorOptions = {};
231
+ if (options?.renderOptions) {
232
+ updateRendererOptions(options.renderOptions);
233
+ if (!isEmpty(changedRenderOptions)) {
234
+ changedRenderOptions = {};
235
+ _processorOptions.renderOptions =
236
+ processorOptions.renderOptions;
237
+ }
238
+ }
239
+ if (options?.backgroundImage &&
240
+ !compareImageRecords(options.backgroundImage, processorOptions.backgroundImage)) {
241
+ processorOptions.backgroundImage = options.backgroundImage;
242
+ _processorOptions.backgroundImage =
243
+ processorOptions.backgroundImage;
244
+ }
245
+ if (options.processingWidth) {
246
+ processorOptions.processingWidth = options.processingWidth;
247
+ _processorOptions.processingWidth =
248
+ processorOptions.processingWidth;
249
+ }
250
+ if (options.processingHeight) {
251
+ processorOptions.processingHeight = options.processingHeight;
252
+ _processorOptions.processingHeight =
253
+ processorOptions.processingHeight;
254
+ }
255
+ if (options.imageSegmenterOptions) {
256
+ processorOptions.imageSegmenterOptions = {
257
+ ...processorOptions.imageSegmenterOptions,
258
+ ...options.imageSegmenterOptions,
259
+ };
260
+ _processorOptions.imageSegmenterOptions =
261
+ processorOptions.imageSegmenterOptions;
262
+ }
263
+ if (canUpdate() && !isEmpty(_processorOptions)) {
264
+ const changed = await segmenter.update(_processorOptions);
265
+ updateChangedRendererOptions(changed);
14
266
  }
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
267
  };
29
- const close = () => {
30
- if (!props.processing) {
31
- return;
268
+ const open = async (options) => {
269
+ if (options) {
270
+ await updateProcessorOptions(options);
271
+ }
272
+ if (segmenter.status === 'new') {
273
+ const changed = await segmenter.open(processorOptions);
274
+ updateChangedRendererOptions(changed);
32
275
  }
33
- props.abortController?.abort(AbortReason.Close);
34
- stopStreamTracks(props.outputStream);
35
- props.outputStream = undefined;
276
+ };
277
+ const update = async (options) => {
278
+ await updateProcessorOptions(options);
279
+ };
280
+ // Serialization lock: `process()` and `close()` mutate the same worker
281
+ // pipeline (segmenter + transferred tracks). If they overlap, the worker
282
+ // can tear down a writable stream while the main thread is still feeding
283
+ // it, surfacing as `InvalidStateError: Stream closed`. Chain every
284
+ // public mutation through this promise so they always run end-to-end.
285
+ let serialized = Promise.resolve();
286
+ const runSerialized = (fn) => {
287
+ const next = serialized.then(fn, fn);
288
+ serialized = next.catch(() => undefined);
289
+ return next;
290
+ };
291
+ const closeImpl = async () => {
292
+ await segmenter.close();
293
+ props.outputTrack?.stop();
294
+ props.outputTrack = undefined;
36
295
  props.processing = false;
37
- for (const transformer of transformers) {
38
- transformer.close();
296
+ };
297
+ const processImpl = async (source) => {
298
+ if (props.processing) {
299
+ await closeImpl();
300
+ }
301
+ assertMediaStreamVideoTrack(source);
302
+ if (segmenter.status === 'new') {
303
+ await segmenter.open(processorOptions);
304
+ // @ts-expect-error -- Segmenter status could be changed to Error if anything went wrong
305
+ if (segmenter.status === 'Error') {
306
+ return source;
307
+ }
39
308
  }
309
+ const processed = await segmenter.process(inputProcessAdaptor.adapt(source), processorOptions.renderOptions);
310
+ props.outputTrack = inputProcessAdaptor.outputTrack;
311
+ if (processed instanceof MediaStreamTrack) {
312
+ props.outputTrack = processed;
313
+ }
314
+ else if (!props.outputTrack) {
315
+ props.outputTrack = source;
316
+ }
317
+ assert(props.outputTrack, 'Segmenter failed to generate output track');
318
+ props.processing = true;
319
+ return props.outputTrack;
40
320
  };
321
+ const process = source => runSerialized(() => processImpl(source));
322
+ const close = () => runSerialized(closeImpl);
41
323
  const destroy = async () => {
42
- close();
43
- await Promise.all(transformers.map(transformer => transformer.destroy()));
324
+ await close();
325
+ processorOptions?.backgroundImage?.image?.close();
326
+ renderOptions = initRenderOptions();
327
+ changedRenderOptions = {};
328
+ processorOptions = {
329
+ renderOptions: renderOptions.original,
330
+ processingWidth: PROCESSING_WIDTH,
331
+ processingHeight: PROCESSING_HEIGHT,
332
+ };
333
+ await segmenter.destroy();
44
334
  };
45
335
  return {
46
336
  open,
337
+ update,
47
338
  process,
339
+ mute(muted) {
340
+ segmenter.mute(muted);
341
+ },
48
342
  close,
49
343
  destroy,
344
+ get status() {
345
+ return segmenter.status;
346
+ },
347
+ get renderOptions() {
348
+ return processorOptions.renderOptions;
349
+ },
350
+ get processingWidth() {
351
+ return processorOptions.processingWidth;
352
+ },
353
+ get processingHeight() {
354
+ return processorOptions.processingHeight;
355
+ },
50
356
  };
51
357
  };