@pexip/media-processor 18.5.0 → 19.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 (34) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/common/backends/webgl/blender.js +3 -1
  3. package/dist/common/backends/webgl/blur.js +20 -5
  4. package/dist/common/backends/webgl/canvas.js +3 -0
  5. package/dist/common/backends/webgl/renderer.d.ts +2 -8
  6. package/dist/common/backends/webgl/renderer.js +46 -54
  7. package/dist/common/backends/webgl/smoothingMask.js +3 -1
  8. package/dist/common/backends/webgl/texture.js +3 -1
  9. package/dist/common/backends/webgl/textureToTexture.js +3 -1
  10. package/dist/common/backends/webgl/webglUtils.js +1 -0
  11. package/dist/common/canvasRenderUtils.d.ts +14 -9
  12. package/dist/common/canvasRenderUtils.js +26 -5
  13. package/dist/common/constants.d.ts +38 -6
  14. package/dist/common/constants.js +37 -6
  15. package/dist/common/tsconfig.tsbuildinfo +1 -1
  16. package/dist/common/types/messageEvents.d.ts +40 -5
  17. package/dist/common/types/processor.d.ts +13 -3
  18. package/dist/common/types/render.d.ts +11 -5
  19. package/dist/common/utils.d.ts +4 -3
  20. package/dist/common/utils.js +15 -6
  21. package/dist/main/tsconfig.tsbuildinfo +1 -1
  22. package/dist/main/video/canvasTransform.js +9 -0
  23. package/dist/main/video/constants.d.ts +1 -0
  24. package/dist/main/video/constants.js +1 -0
  25. package/dist/main/video/segmenters/mediapipe.d.ts +2 -1
  26. package/dist/main/video/segmenters/mediapipe.js +128 -34
  27. package/dist/main/video/transformer.js +14 -2
  28. package/dist/main/video/types.d.ts +4 -1
  29. package/dist/main/video/utils.d.ts +4 -1
  30. package/dist/main/video/utils.js +42 -15
  31. package/dist/main/video/videoStreamTrackProcessor.js +6 -11
  32. package/dist/workers/mediaWorker.js +14 -14
  33. package/dist/workers/tsconfig.tsbuildinfo +1 -1
  34. package/package.json +4 -4
@@ -1,4 +1,5 @@
1
- import { PROCESS_STATUS, PROCESSING_WIDTH, PROCESSING_HEIGHT, } from '../../../common/constants';
1
+ import { assert } from '@pexip/utils';
2
+ import { PROCESS_STATUS, PROCESSING_WIDTH, PROCESSING_HEIGHT, RENDERING_EVENTS, } from '../../../common/constants';
2
3
  const cloneImageRecord = async (image) => {
3
4
  const cloned = await createImageBitmap(image.image);
4
5
  return { image: cloned, key: image.key };
@@ -11,49 +12,98 @@ export const createSegmenter = (
11
12
  * @see {@link
12
13
  * https://ai.google.dev/edge/api/mediapipe/js/tasks-vision.filesetresolver#filesetresolverforvisiontasks | FileResolver}
13
14
  */
14
- basePath = '/', { processingWidth = PROCESSING_WIDTH, processingHeight = PROCESSING_HEIGHT, workerScriptUrl = new URL('../../../workers/mediaWorker.js', import.meta.url), credentials = 'same-origin', delegate, ...options }) => {
15
- const props = { status: PROCESS_STATUS.New, modelAsset: options.modelAsset };
15
+ basePath = '/', { processingWidth = PROCESSING_WIDTH, processingHeight = PROCESSING_HEIGHT, workerScriptUrl = new URL('../../../workers/mediaWorker.js', import.meta.url), credentials = 'same-origin', delegate, log, ...options }) => {
16
+ const props = {
17
+ status: PROCESS_STATUS.New,
18
+ modelAsset: options.modelAsset,
19
+ eventHandlers: new Map(),
20
+ };
16
21
  const initWorker = () => {
17
22
  const mediaWorker = new Worker(workerScriptUrl, {
18
23
  type: 'classic',
19
24
  credentials,
20
25
  });
21
26
  mediaWorker.addEventListener('error', error => {
22
- throw error.error;
27
+ log?.(`${error.message}`, error);
28
+ error.preventDefault();
23
29
  });
24
30
  mediaWorker.addEventListener('message', (event) => {
25
31
  const { type } = event.data;
26
- if (type === 'error') {
27
- throw event.data.error;
32
+ if (type === 'debug') {
33
+ log?.(event.data.data.message, event.data.data.context);
34
+ }
35
+ if (type === 'event') {
36
+ log?.(event.data.data.type, event.data.data.message);
37
+ props.eventHandlers
38
+ .get(event.data.data.type)
39
+ ?.forEach(cb => {
40
+ cb();
41
+ });
42
+ switch (event.data.data.type) {
43
+ case RENDERING_EVENTS.ProcessorRestarted:
44
+ break;
45
+ case RENDERING_EVENTS.ContextRestored:
46
+ void handleContextRestored();
47
+ break;
48
+ default:
49
+ props.status = PROCESS_STATUS.Error;
50
+ break;
51
+ }
28
52
  }
29
53
  });
30
54
  return mediaWorker;
31
55
  };
56
+ const handleContextRestored = async () => {
57
+ try {
58
+ await postMsg('updated', {
59
+ type: 'update',
60
+ data: { restart: true },
61
+ });
62
+ props.status = PROCESS_STATUS.Updated;
63
+ props.eventHandlers.get('ProcessorRestarted')?.forEach(cb => {
64
+ cb();
65
+ });
66
+ }
67
+ catch (err) {
68
+ log?.('Failed to restart process', {
69
+ error: err,
70
+ });
71
+ props.status = PROCESS_STATUS.Error;
72
+ }
73
+ };
32
74
  async function postMsg(expectedResponseType, message, transfer) {
33
75
  if (!expectedResponseType) {
34
76
  return props.mediaWorker?.postMessage(message, transfer ?? []);
35
77
  }
36
78
  return await new Promise((resolve, reject) => {
37
79
  const handleMessage = (ev) => {
38
- removeListener();
39
80
  switch (ev.data.type) {
40
81
  case expectedResponseType: {
82
+ removeListener();
41
83
  // @ts-expect-error --- Call `resolve` is expecting to pass an argument or nothing
42
84
  resolve(ev.data.data);
43
85
  break;
44
86
  }
45
- case 'error': {
46
- reject(ev.data.error);
87
+ case 'debug': {
88
+ // Nothing to handle
89
+ break;
90
+ }
91
+ case 'event': {
92
+ removeListener();
93
+ reject(new Error(ev.data.data.type, {
94
+ cause: ev.data.data.message,
95
+ }));
47
96
  break;
48
97
  }
49
98
  default: {
50
- throw new Error(`UnhandledEvent: ${ev.data.type}`);
99
+ removeListener();
100
+ reject(new Error(`UnhandledEvent: ${ev.data.type}`));
51
101
  }
52
102
  }
53
103
  };
54
104
  const handleError = (error) => {
55
105
  removeListener();
56
- reject(error.error);
106
+ reject(error.message);
57
107
  };
58
108
  const removeListener = () => {
59
109
  props.mediaWorker?.removeEventListener('message', handleMessage);
@@ -70,9 +120,8 @@ basePath = '/', { processingWidth = PROCESSING_WIDTH, processingHeight = PROCESS
70
120
  props.output = processorOptions?.output;
71
121
  const backgroundImage = processorOptions?.backgroundImage &&
72
122
  (await cloneImageRecord(processorOptions.backgroundImage));
73
- await postMsg('opened', {
74
- type: 'open',
75
- data: {
123
+ try {
124
+ const processorSettings = {
76
125
  basePath,
77
126
  processingWidth,
78
127
  processingHeight,
@@ -83,40 +132,75 @@ basePath = '/', { processingWidth = PROCESSING_WIDTH, processingHeight = PROCESS
83
132
  ...processorOptions?.imageSegmenterOptions,
84
133
  },
85
134
  backgroundImage,
86
- },
87
- }, [processorOptions?.output, backgroundImage?.image].filter(Boolean));
88
- props.status = PROCESS_STATUS.Opened;
135
+ };
136
+ log?.('Init processor with config', { config: processorSettings });
137
+ await postMsg('opened', {
138
+ type: 'open',
139
+ data: processorSettings,
140
+ }, [processorOptions?.output, backgroundImage?.image].filter(Boolean));
141
+ props.status = PROCESS_STATUS.Opened;
142
+ }
143
+ catch (error) {
144
+ props.status = PROCESS_STATUS.Error;
145
+ log?.('Failed to open process', {
146
+ error: error,
147
+ });
148
+ }
89
149
  };
90
150
  const process = async (input, options) => {
151
+ if (props.status === PROCESS_STATUS.Error) {
152
+ return input;
153
+ }
91
154
  props.status = PROCESS_STATUS.Processing;
92
- const resultFrame = await postMsg('processed', {
93
- type: 'process',
94
- data: { videoFrame: input, renderOptions: options },
95
- }, [input.frame]);
96
- if (props.status === PROCESS_STATUS.Processing) {
97
- props.status = PROCESS_STATUS.Idle;
155
+ try {
156
+ const resultFrame = await postMsg('processed', {
157
+ type: 'process',
158
+ data: { videoFrame: input, renderOptions: options },
159
+ }, [input.frame]);
160
+ if (props.status === PROCESS_STATUS.Processing) {
161
+ props.status = PROCESS_STATUS.Idle;
162
+ }
163
+ return resultFrame;
164
+ }
165
+ catch (error) {
166
+ props.status = PROCESS_STATUS.Error;
167
+ log?.('Error during the processing, return original instead.', {
168
+ error,
169
+ });
170
+ return input;
98
171
  }
99
- return resultFrame;
100
172
  };
101
173
  const update = async (options) => {
102
- const backgroundImage = options.backgroundImage &&
103
- (await cloneImageRecord(options.backgroundImage));
104
- const transfer = backgroundImage ? [backgroundImage.image] : [];
105
- await postMsg(undefined, {
106
- type: 'update',
107
- data: {
108
- ...options,
109
- backgroundImage,
110
- },
111
- }, transfer);
174
+ try {
175
+ const backgroundImage = options.backgroundImage &&
176
+ (await cloneImageRecord(options.backgroundImage));
177
+ log?.('Update processor config', { config: options });
178
+ const transfer = backgroundImage ? [backgroundImage.image] : [];
179
+ await postMsg('updated', {
180
+ type: 'update',
181
+ data: {
182
+ ...options,
183
+ backgroundImage,
184
+ },
185
+ }, transfer);
186
+ }
187
+ catch (error) {
188
+ props.status = PROCESS_STATUS.Error;
189
+ log?.('Failed to update process', {
190
+ error: error,
191
+ });
192
+ }
112
193
  };
113
194
  const close = () => {
114
195
  props.status = PROCESS_STATUS.Closing;
196
+ log?.('Close processor');
115
197
  void postMsg(undefined, { type: 'close' });
198
+ props.eventHandlers.clear();
116
199
  props.status = PROCESS_STATUS.Closed;
117
200
  };
118
201
  const destroy = async () => {
119
202
  props.status = PROCESS_STATUS.Destroying;
203
+ log?.('Destroy processor');
120
204
  await postMsg(undefined, { type: 'destroy' });
121
205
  props.mediaWorker?.terminate();
122
206
  props.mediaWorker = undefined;
@@ -139,6 +223,16 @@ basePath = '/', { processingWidth = PROCESSING_WIDTH, processingHeight = PROCESS
139
223
  get height() {
140
224
  return processingHeight;
141
225
  },
226
+ subscribe: (event, callback) => {
227
+ let handlers = props.eventHandlers.get(event);
228
+ if (!handlers) {
229
+ props.eventHandlers.set(event, new Set());
230
+ handlers = props.eventHandlers.get(event);
231
+ }
232
+ assert(handlers);
233
+ handlers.add(callback);
234
+ return () => handlers?.delete(callback);
235
+ },
142
236
  open,
143
237
  update,
144
238
  process,
@@ -17,8 +17,20 @@ const wrapTransformController = (videoFrame, controller) => {
17
17
  frame instanceof HTMLImageElement ||
18
18
  frame instanceof ImageBitmap) {
19
19
  const timestamp = videoFrame.timestamp ?? 0;
20
- videoFrame.close();
21
- return controller.enqueue(new VideoFrame(frame, { timestamp, alpha: 'discard' }));
20
+ try {
21
+ // Constructing a new VideoFrame with the modified frame
22
+ // could throw an error
23
+ const newFrame = new VideoFrame(frame, {
24
+ timestamp,
25
+ alpha: 'discard',
26
+ });
27
+ controller.enqueue(newFrame);
28
+ videoFrame.close();
29
+ }
30
+ catch {
31
+ controller.enqueue(videoFrame);
32
+ }
33
+ return;
22
34
  }
23
35
  throw new Error('Unexpected input frame');
24
36
  },
@@ -1,4 +1,4 @@
1
- import type { ProcessStatus } from '../../common/constants';
1
+ import type { ProcessStatus, RenderingEvents } from '../../common/constants';
2
2
  import type { ProcessInputType, VideoFrameLike } from '../../common/types/media';
3
3
  import type { RendererOptions } from '../../common/types/render';
4
4
  import type { SegmentationModelAsset } from '../../common/types/segmentation';
@@ -36,12 +36,15 @@ export interface BackgroundImage {
36
36
  key: string;
37
37
  image: Blob;
38
38
  }
39
+ type Unsubscribe = () => void;
40
+ type Callback<T> = (args: T) => void;
39
41
  export interface Segmenter extends Process, ProcessingSize {
40
42
  modelAsset: SegmentationModelAsset;
41
43
  backgroundImage?: BackgroundImage;
42
44
  open(options?: ProcessorOptions): Promise<void>;
43
45
  update(options: ProcessorOptions): Promise<void>;
44
46
  process(input: VideoFrameLike, options?: RendererOptions): Promise<VideoFrameLike | undefined>;
47
+ subscribe(event: RenderingEvents, callback: Callback<void>): Unsubscribe;
45
48
  }
46
49
  export interface Detector<T> extends Process {
47
50
  detect(input: ProcessInputType): Promise<T>;
@@ -55,7 +55,10 @@ export declare const createOffscreenCanvas: (width: number, height: number) => H
55
55
  export declare const createVideoElement: (width: number, height: number) => HTMLVideoElement;
56
56
  export declare const setVideoElementSrc: (video: HTMLVideoElement, src: MediaProvider) => void;
57
57
  export declare const toVideoElement: (input: MediaStream, width: number, height: number) => HTMLVideoElement;
58
- export declare const playVideo: (video: HTMLVideoElement) => Promise<void>;
58
+ export declare const playVideo: (video: HTMLVideoElement, { setTimeout, clearTimeout }?: {
59
+ setTimeout?: (((handler: TimerHandler, timeout?: number | undefined, ...arguments: any[]) => number) & typeof setTimeout) | undefined;
60
+ clearTimeout?: (((id: number | undefined) => void) & typeof clearTimeout) | undefined;
61
+ }) => Promise<void>;
59
62
  /**
60
63
  * Load image with provided image element
61
64
  */
@@ -1,4 +1,5 @@
1
1
  import { createAsyncCallbackLoop, subscribeVisibilityChangeEvent, } from '../utils';
2
+ import { PLAY_VIDEO_TIMEOUT } from './constants';
2
3
  /**
3
4
  * Draw the source onto the provided canvas according to the destination rect
4
5
  *
@@ -113,6 +114,10 @@ export const toVideoElement = (input, width, height) => {
113
114
  setVideoElementSrc(video, input);
114
115
  return video;
115
116
  };
117
+ /**
118
+ * The HTMLMediaElement.readyState property indicates the readiness state of the media
119
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/readyState}
120
+ */
116
121
  var HTMLMediaElementReadyState;
117
122
  (function (HTMLMediaElementReadyState) {
118
123
  HTMLMediaElementReadyState[HTMLMediaElementReadyState["HaveNothing"] = 0] = "HaveNothing";
@@ -121,28 +126,50 @@ var HTMLMediaElementReadyState;
121
126
  HTMLMediaElementReadyState[HTMLMediaElementReadyState["HaveFutureData"] = 3] = "HaveFutureData";
122
127
  HTMLMediaElementReadyState[HTMLMediaElementReadyState["HaveEnoughData"] = 4] = "HaveEnoughData";
123
128
  })(HTMLMediaElementReadyState || (HTMLMediaElementReadyState = {}));
124
- export const playVideo = async (video) => {
129
+ export const playVideo = async (video, { setTimeout = window.setTimeout, clearTimeout = window.clearTimeout } = {}) => {
125
130
  if (video.autoplay) {
126
131
  return;
127
132
  }
128
133
  if (video.readyState <
129
134
  HTMLMediaElementReadyState.HaveCurrentData) {
130
135
  // 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
- }),
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();
162
+ resolve();
163
+ };
139
164
  // Use setTimeout to prevent a race to `loadeddata` event
140
- new Promise(resolve => {
141
- setTimeout(() => {
142
- resolve();
143
- }, 3000);
144
- }),
145
- ]);
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
+ });
146
173
  }
147
174
  return await video.play();
148
175
  };
@@ -1,3 +1,4 @@
1
+ import { assert } from '@pexip/utils';
1
2
  import { createMediaStreamTrackProcessor } from '../processor';
2
3
  import { createMediaStreamTrackGenerator } from '../generator';
3
4
  import { createStreamTransformer } from '../transformer';
@@ -46,16 +47,12 @@ export const createVideoTrackProcessorWithFallback = ({ width = PROCESSING_WIDTH
46
47
  if (!transformer?.transform) {
47
48
  return track;
48
49
  }
49
- if (transformers.length > 1) {
50
- throw new Error('Multi-transformer is NOT supported');
51
- }
50
+ assert(transformers.length <= 1, 'Multi-transformer is NOT supported');
52
51
  const outputCanvas = createCanvas(width, height);
53
52
  const ctx = getCanvasRenderingContext2D(outputCanvas, { alpha: false });
54
53
  const render = (input) => {
55
- if (!transformer.transform) {
56
- throw new Error('Transform is undefined');
57
- }
58
- return transformer.transform(input, nullTransformController({
54
+ assert(transformer.transform);
55
+ return Promise.resolve(transformer.transform(input, nullTransformController({
59
56
  enqueue: frame => {
60
57
  if (!frame) {
61
58
  return;
@@ -71,7 +68,7 @@ export const createVideoTrackProcessorWithFallback = ({ width = PROCESSING_WIDTH
71
68
  terminate: () => {
72
69
  stop();
73
70
  },
74
- }));
71
+ })));
75
72
  };
76
73
  const runner = createFrameCallbackRequest(render, frameRate);
77
74
  const videoElement = toVideoElement(new MediaStream([track]), width, height);
@@ -79,9 +76,7 @@ export const createVideoTrackProcessorWithFallback = ({ width = PROCESSING_WIDTH
79
76
  await runner.start(videoElement);
80
77
  const stream = outputCanvas.captureStream(frameRate);
81
78
  const [trackGenerated] = stream.getVideoTracks();
82
- if (!trackGenerated) {
83
- throw new Error('Canvas captureStream returns no video track');
84
- }
79
+ assert(trackGenerated, 'Canvas captureStream returns no video track');
85
80
  const stop = () => {
86
81
  stopStreamTracks(stream);
87
82
  runner.stop();