@pexip/media-processor 18.5.0 → 19.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/dist/common/backends/webgl/blender.js +3 -1
- package/dist/common/backends/webgl/blur.js +20 -5
- package/dist/common/backends/webgl/canvas.js +3 -0
- package/dist/common/backends/webgl/renderer.d.ts +2 -8
- package/dist/common/backends/webgl/renderer.js +46 -54
- package/dist/common/backends/webgl/smoothingMask.js +3 -1
- package/dist/common/backends/webgl/texture.js +3 -1
- package/dist/common/backends/webgl/textureToTexture.js +3 -1
- package/dist/common/backends/webgl/webglUtils.js +1 -0
- package/dist/common/canvasRenderUtils.d.ts +14 -9
- package/dist/common/canvasRenderUtils.js +26 -5
- package/dist/common/constants.d.ts +38 -6
- package/dist/common/constants.js +37 -6
- package/dist/common/tsconfig.tsbuildinfo +1 -1
- package/dist/common/types/messageEvents.d.ts +40 -5
- package/dist/common/types/processor.d.ts +13 -3
- package/dist/common/types/render.d.ts +11 -5
- package/dist/common/utils.d.ts +4 -3
- package/dist/common/utils.js +15 -6
- package/dist/main/tsconfig.tsbuildinfo +1 -1
- package/dist/main/video/canvasTransform.js +9 -0
- package/dist/main/video/constants.d.ts +1 -0
- package/dist/main/video/constants.js +1 -0
- package/dist/main/video/segmenters/mediapipe.d.ts +2 -1
- package/dist/main/video/segmenters/mediapipe.js +128 -34
- package/dist/main/video/transformer.js +14 -2
- package/dist/main/video/types.d.ts +4 -1
- package/dist/main/video/utils.d.ts +4 -1
- package/dist/main/video/utils.js +42 -15
- package/dist/main/video/videoStreamTrackProcessor.js +6 -11
- package/dist/workers/mediaWorker.js +18 -18
- package/dist/workers/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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 = {
|
|
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
|
-
|
|
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 === '
|
|
27
|
-
|
|
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 '
|
|
46
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
props.status
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
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
|
*/
|
package/dist/main/video/utils.js
CHANGED
|
@@ -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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
video.
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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();
|