@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.
- package/CHANGELOG.md +80 -0
- package/README.md +17 -10
- package/api-docs/README.mdx +40 -19
- package/api-docs/functions/assertMediaStreamVideoTrack.mdx +13 -0
- package/api-docs/functions/avg.mdx +1 -1
- package/api-docs/functions/createAnalyzerGraphNode.mdx +3 -1
- package/api-docs/functions/createAnalyzerSubscribableGraphNode.mdx +3 -1
- package/api-docs/functions/createAsyncCallbackLoop.mdx +6 -6
- package/api-docs/functions/createAudioDestinationGraphNode.mdx +1 -1
- package/api-docs/functions/createAudioSignalDetector.mdx +4 -4
- package/api-docs/functions/createAudioStats.mdx +1 -1
- package/api-docs/functions/createChannelMergerGraphNode.mdx +1 -1
- package/api-docs/functions/createChannelSplitterGraphNode.mdx +1 -1
- package/api-docs/functions/createDelayGraphNode.mdx +1 -1
- package/api-docs/functions/createDenoiseWorkletGraphNode.mdx +2 -2
- package/api-docs/functions/createFrameCallbackRequest.mdx +10 -4
- package/api-docs/functions/createGainGraphNode.mdx +3 -1
- package/api-docs/functions/createMediaElementSourceGraphNode.mdx +1 -1
- package/api-docs/functions/createMediaStreamTrackInputProcessAdaptor.mdx +10 -0
- package/api-docs/functions/createReadWritableStreamFallbackInputProcessAdaptor.mdx +17 -0
- package/api-docs/functions/createReadWritableStreamInputProcessAdaptor.mdx +10 -0
- package/api-docs/functions/createSegmenter.mdx +1 -1
- package/api-docs/functions/createStreamDestinationGraphNode.mdx +1 -1
- package/api-docs/functions/createStreamSourceGraphNode.mdx +1 -1
- package/api-docs/functions/createVADetector.mdx +3 -3
- package/api-docs/functions/createVideoProcessor.mdx +3 -3
- package/api-docs/functions/createVoiceDetectorFromProbability.mdx +1 -1
- package/api-docs/functions/createVoiceDetectorFromTimeData.mdx +3 -1
- package/api-docs/functions/createWindowedStats.mdx +2 -2
- package/api-docs/functions/getBezierCurveControlPoints.mdx +3 -1
- package/api-docs/functions/getDefaultInputProcessAdaptor.mdx +17 -0
- package/api-docs/functions/isMediaStreamVideoTrack.mdx +13 -0
- package/api-docs/functions/labelComponents.mdx +3 -1
- package/api-docs/functions/loadScript.mdx +1 -1
- package/api-docs/functions/loadWasms.mdx +1 -1
- package/api-docs/functions/nearestPowerOfTwo.mdx +1 -1
- package/api-docs/functions/playVideo.mdx +7 -5
- package/api-docs/functions/processAverageVolume.mdx +1 -1
- package/api-docs/functions/rms.mdx +1 -1
- package/api-docs/functions/subscribeWorkletNode.mdx +1 -1
- package/api-docs/functions/sum.mdx +1 -1
- package/api-docs/interfaces/Analyzer.mdx +4 -0
- package/api-docs/interfaces/AnalyzerSubscribableOptions.mdx +5 -3
- package/api-docs/interfaces/AudioGraph.mdx +2 -2
- package/api-docs/interfaces/AudioNodeInit.mdx +10 -6
- package/api-docs/interfaces/AudioNodeProps.mdx +3 -3
- package/api-docs/interfaces/Benchmark.mdx +17 -3
- package/api-docs/interfaces/Clock.mdx +1 -1
- package/api-docs/interfaces/Denoise.mdx +2 -2
- package/api-docs/interfaces/FrameTransformStats.mdx +9 -0
- package/api-docs/interfaces/Gain.mdx +4 -0
- package/api-docs/interfaces/ImageSegmenterOptions.mdx +0 -2
- package/api-docs/interfaces/InputProcessAdaptor.mdx +32 -0
- package/api-docs/interfaces/MPMask.mdx +1 -1
- package/api-docs/interfaces/Process.mdx +12 -7
- package/api-docs/interfaces/ProcessEventMute.mdx +6 -0
- package/api-docs/interfaces/ProcessWorkerDebugMessage.mdx +1 -1
- package/api-docs/interfaces/ProcessWorkerEventClosed.mdx +5 -0
- package/api-docs/interfaces/ProcessWorkerEventOpened.mdx +7 -0
- package/api-docs/interfaces/ProcessWorkerEventStats.mdx +6 -0
- package/api-docs/interfaces/ProcessWorkerEventUpdated.mdx +7 -0
- package/api-docs/interfaces/ProcessorEvent.mdx +3 -2
- package/api-docs/interfaces/ProcessorOptions.mdx +0 -1
- package/api-docs/interfaces/ProcessorProcessOptions.mdx +3 -1
- package/api-docs/interfaces/ProcessorUpdateOptions.mdx +17 -0
- package/api-docs/interfaces/ReadWritableStream.mdx +12 -0
- package/api-docs/interfaces/RenderEventHandlers.mdx +3 -3
- package/api-docs/interfaces/Renderer.mdx +5 -3
- package/api-docs/interfaces/RendererOptions.mdx +5 -9
- package/api-docs/interfaces/Runner.mdx +3 -3
- package/api-docs/interfaces/Segmentation.mdx +1 -1
- package/api-docs/interfaces/Segmenter.mdx +115 -16
- package/api-docs/interfaces/SegmenterOptions.mdx +0 -1
- package/api-docs/interfaces/VideoFrameLike.mdx +1 -1
- package/api-docs/interfaces/VideoProcessor.mdx +52 -8
- package/api-docs/interfaces/WorkletMessagePortOptions.mdx +2 -2
- package/api-docs/type-aliases/AsyncCallback.mdx +1 -1
- package/api-docs/type-aliases/Callback.mdx +2 -2
- package/api-docs/type-aliases/ExtractMessageEventType.mdx +1 -1
- package/api-docs/type-aliases/IncludeMessageEventDataType.mdx +1 -1
- package/api-docs/type-aliases/Logger.mdx +12 -0
- package/api-docs/type-aliases/OptionalKeys.mdx +1 -1
- package/api-docs/type-aliases/ProcessEvents.mdx +1 -0
- package/api-docs/type-aliases/ProcessInputType.mdx +3 -1
- package/api-docs/type-aliases/ProcessWorkerEvents.mdx +7 -4
- package/api-docs/type-aliases/ProcessorWorkerEvents.mdx +1 -1
- package/api-docs/type-aliases/RunnerCreator.mdx +3 -3
- package/api-docs/type-aliases/Struct.mdx +9 -0
- package/api-docs/type-aliases/TupleOf.mdx +1 -1
- package/api-docs/variables/FRAME_RATE.mdx +1 -1
- package/api-docs/variables/ImageFetchError.mdx +3 -0
- package/api-docs/variables/MediaStreamTrackTransferable.mdx +5 -0
- package/api-docs/variables/RENDERER_OPTIONS_STRUCT.mdx +3 -0
- package/api-docs/variables/cloneImageRecord.mdx +21 -0
- package/api-docs/variables/compareImageRecords.mdx +18 -0
- package/api-docs/variables/createLazyProps.mdx +1 -1
- package/api-docs/variables/createObjectUpdater.mdx +2 -2
- package/api-docs/variables/createRemoteImageBitmap.mdx +5 -5
- package/api-docs/variables/handleWebGLContextLoss.mdx +1 -1
- package/api-docs/variables/isRenderOptionsKey.mdx +13 -0
- package/api-docs/variables/isValidGpuAPI.mdx +13 -0
- package/api-docs/variables/urls.mdx +2 -2
- package/dist/common/backends/webgl/renderer.js +2 -12
- package/dist/common/backends/webgl/texture.d.ts +1 -2
- package/dist/common/backends/webgl/texture.js +2 -2
- package/dist/common/backends/webgl/webglUtils.js +8 -4
- package/dist/common/backends/webgpu/renderer.js +2 -12
- package/dist/common/backends/webgpu/tentBlur.js +1 -1
- package/dist/common/backends/webgpu/texture.d.ts +1 -2
- package/dist/common/backends/webgpu/texture.js +6 -2
- package/dist/common/constants.d.ts +3 -0
- package/dist/common/constants.js +4 -0
- package/dist/common/index.d.ts +1 -0
- package/dist/common/index.js +1 -0
- package/dist/common/tsconfig.tsbuildinfo +1 -1
- package/dist/common/typeGuards.d.ts +2 -0
- package/dist/common/typeGuards.js +7 -0
- package/dist/common/types/media.d.ts +5 -1
- package/dist/common/types/messageEvents.d.ts +34 -4
- package/dist/common/types/processor.d.ts +7 -4
- package/dist/common/types/render.d.ts +5 -2
- package/dist/common/types/render.js +23 -1
- package/dist/common/types/segmentation.d.ts +0 -12
- package/dist/common/utils.d.ts +13 -2
- package/dist/common/utils.js +54 -10
- package/dist/main/audio.js +3 -3
- package/dist/main/benchUtils.d.ts +2 -2
- package/dist/main/benchUtils.js +4 -4
- package/dist/main/index.d.ts +1 -1
- package/dist/main/index.js +1 -1
- package/dist/main/tsconfig.tsbuildinfo +1 -1
- package/dist/main/types.d.ts +0 -6
- package/dist/main/utils.d.ts +4 -0
- package/dist/main/utils.js +31 -0
- package/dist/main/video/constants.d.ts +1 -4
- package/dist/main/video/constants.js +1 -5
- package/dist/main/video/index.d.ts +2 -3
- package/dist/main/video/index.js +2 -3
- package/dist/main/video/inputProcessAdaptor.d.ts +30 -0
- package/dist/main/video/inputProcessAdaptor.js +198 -0
- package/dist/main/video/segmenter.d.ts +42 -0
- package/dist/main/video/{segmenters/mediapipe.js → segmenter.js} +130 -75
- package/dist/main/video/typeGuards.d.ts +2 -0
- package/dist/main/video/typeGuards.js +8 -1
- package/dist/main/video/types.d.ts +37 -17
- package/dist/main/video/utils.d.ts +5 -4
- package/dist/main/video/utils.js +49 -44
- package/dist/main/video/video.d.ts +30 -8
- package/dist/main/video/video.js +338 -32
- package/dist/workers/mediaWorker.js +24 -24
- package/dist/workers/tsconfig.tsbuildinfo +1 -1
- package/dist/worklets/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/api-docs/functions/createCanvasTransform.mdx +0 -14
- package/api-docs/functions/createVideoTrackProcessor.mdx +0 -9
- package/api-docs/functions/createVideoTrackProcessorWithFallback.mdx +0 -15
- package/api-docs/interfaces/Detector.mdx +0 -81
- package/api-docs/interfaces/SegmentationParams.mdx +0 -35
- package/api-docs/interfaces/Transform.mdx +0 -62
- package/api-docs/type-aliases/ProcessVideoTrack.mdx +0 -15
- package/api-docs/type-aliases/SegmentationTransform.mdx +0 -3
- package/dist/main/transformer.d.ts +0 -1
- package/dist/main/transformer.js +0 -4
- package/dist/main/video/canvasTransform.d.ts +0 -17
- package/dist/main/video/canvasTransform.js +0 -341
- package/dist/main/video/segmenters/mediapipe.d.ts +0 -22
- package/dist/main/video/transformer.d.ts +0 -9
- package/dist/main/video/transformer.js +0 -68
- package/dist/main/video/videoStreamTrackProcessor.d.ts +0 -20
- package/dist/main/video/videoStreamTrackProcessor.js +0 -86
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { ProcessStatus, RenderingEvents } from '../../common/constants';
|
|
2
|
-
import type {
|
|
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<
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
40
|
-
|
|
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
|
|
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>, [
|
|
93
|
-
start: (input:
|
|
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
|
};
|
package/dist/main/video/utils.js
CHANGED
|
@@ -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 =
|
|
129
|
+
export const playVideo = (video, { setTimeout = window.setTimeout, clearTimeout = window.clearTimeout } = {}) => {
|
|
130
130
|
if (video.autoplay) {
|
|
131
131
|
return;
|
|
132
132
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
|
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 {
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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:
|
|
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: (
|
|
42
|
+
export declare const createVideoProcessor: (segmenter: Segmenter, inputProcessAdaptor: InputProcessAdaptor) => VideoProcessor;
|
package/dist/main/video/video.js
CHANGED
|
@@ -1,51 +1,357 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
30
|
-
if (
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
};
|