@pexip/media 22.1.0 → 22.2.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 +32 -0
- package/dist/audioMixingProcessor.d.ts +15 -0
- package/dist/audioMixingProcessor.js +44 -19
- package/dist/audioProcessor.js +13 -3
- package/dist/videoProcessor.d.ts +1 -0
- package/dist/videoProcessor.js +43 -7
- package/package.json +7 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @pexip/media
|
|
2
2
|
|
|
3
|
+
## 22.2.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 72f8dde: Temporarily remove updating the backend in the pipeline
|
|
8
|
+
- 746f6b6: Add Canvas2d rendering backend support: alpha mask converter,
|
|
9
|
+
Canvas2d mask renderer, canvas dimension-aware rendering, and proper backend
|
|
10
|
+
propagation through the video processor pipeline.
|
|
11
|
+
- 265c898: Migrate test runner from Jest to Vitest.
|
|
12
|
+
- a5e8026: Fix "We can't find your microphone" error when changing the
|
|
13
|
+
microphone while sharing content without audio. The audio mixing processor no
|
|
14
|
+
longer tries to create an audio source node from a presentation stream that
|
|
15
|
+
has no audio track, and audio nodes belonging to a stale `AudioContext` are
|
|
16
|
+
recreated instead of being reused.
|
|
17
|
+
- 0878b3b: Fix race conditions in the video processing pipeline so effect
|
|
18
|
+
changes made during processor startup are correctly applied and worker option
|
|
19
|
+
diffs are computed against the previous state.
|
|
20
|
+
- Updated dependencies [5a17991]
|
|
21
|
+
- Updated dependencies [f4af45a]
|
|
22
|
+
- Updated dependencies [746f6b6]
|
|
23
|
+
- Updated dependencies [c101d02]
|
|
24
|
+
- Updated dependencies [476acbf]
|
|
25
|
+
- Updated dependencies [7c99e29]
|
|
26
|
+
- Updated dependencies [1b82fd5]
|
|
27
|
+
- Updated dependencies [265c898]
|
|
28
|
+
- Updated dependencies [a5e8026]
|
|
29
|
+
- Updated dependencies [0878b3b]
|
|
30
|
+
- @pexip/utils@17.4.1
|
|
31
|
+
- @pexip/media-processor@22.2.0
|
|
32
|
+
- @pexip/media-control@22.2.0
|
|
33
|
+
- @pexip/signal@16.9.6
|
|
34
|
+
|
|
3
35
|
## 22.1.0
|
|
4
36
|
|
|
5
37
|
### Minor Changes
|
|
@@ -5,6 +5,7 @@ interface AudioStreamProcessorProps {
|
|
|
5
5
|
mixWithAdditionalMedia?: boolean;
|
|
6
6
|
merger?: AudioNodeInit<ChannelMergerNode, ChannelMergerNode>;
|
|
7
7
|
displaySource?: AudioNodeInit<MediaStreamAudioSourceNode, MediaStreamAudioSourceNode>;
|
|
8
|
+
displayStream?: MediaStream;
|
|
8
9
|
audioGraph?: AudioGraph;
|
|
9
10
|
}
|
|
10
11
|
declare const FEATURE_KEYS: {
|
|
@@ -13,6 +14,20 @@ declare const FEATURE_KEYS: {
|
|
|
13
14
|
type FeaturePropKeys = keyof typeof FEATURE_KEYS;
|
|
14
15
|
type FeatureProps = Pick<AudioStreamProcessorProps, FeaturePropKeys>;
|
|
15
16
|
export declare const updateFeatureProps: (constraints: MediaDeviceRequest["audio"], props: FeatureProps) => FeatureProps;
|
|
17
|
+
/**
|
|
18
|
+
* Extract a `MediaStream` containing only the audio tracks of the provided
|
|
19
|
+
* stream, or `undefined` when there is no audio track to mix, e.g. sharing a
|
|
20
|
+
* screen without audio.
|
|
21
|
+
*
|
|
22
|
+
* @param stream - The stream to extract the audio tracks from
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
*
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const audioOnly = toAudioOnlyStream(displayStream);
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare const toAudioOnlyStream: (stream?: MediaStream) => MediaStream | undefined;
|
|
16
31
|
/**
|
|
17
32
|
* Create an Audio Mixing Processor and will own the stream passed-in
|
|
18
33
|
*/
|
|
@@ -28,6 +28,26 @@ export const updateFeatureProps = (constraints, props) => {
|
|
|
28
28
|
}
|
|
29
29
|
return features;
|
|
30
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Extract a `MediaStream` containing only the audio tracks of the provided
|
|
33
|
+
* stream, or `undefined` when there is no audio track to mix, e.g. sharing a
|
|
34
|
+
* screen without audio.
|
|
35
|
+
*
|
|
36
|
+
* @param stream - The stream to extract the audio tracks from
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
*
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const audioOnly = toAudioOnlyStream(displayStream);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export const toAudioOnlyStream = (stream) => {
|
|
45
|
+
const audioTracks = stream?.getAudioTracks?.() ?? [];
|
|
46
|
+
if (audioTracks.length === 0) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return new MediaStream(audioTracks);
|
|
50
|
+
};
|
|
31
51
|
/**
|
|
32
52
|
* Create an Audio Mixing Processor and will own the stream passed-in
|
|
33
53
|
*/
|
|
@@ -36,7 +56,8 @@ export const createAudioMixingProcess = (getCurrentMedia, signals, label = PROCE
|
|
|
36
56
|
return async (prevMediaTrack) => {
|
|
37
57
|
updateFeatureProps(prevMediaTrack.getConstraints(), props);
|
|
38
58
|
const displayStream = getCurrentMedia();
|
|
39
|
-
const
|
|
59
|
+
const displayAudioStream = toAudioOnlyStream(displayStream);
|
|
60
|
+
const displayTrack = displayAudioStream?.getAudioTracks().at(0);
|
|
40
61
|
const applyConstraints = async (constraints) => {
|
|
41
62
|
if (isEmpty(constraints)) {
|
|
42
63
|
return;
|
|
@@ -50,8 +71,8 @@ export const createAudioMixingProcess = (getCurrentMedia, signals, label = PROCE
|
|
|
50
71
|
}
|
|
51
72
|
if (features.mixWithAdditionalMedia) {
|
|
52
73
|
const newStream = getCurrentMedia();
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
74
|
+
const newAudioStream = toAudioOnlyStream(newStream);
|
|
75
|
+
if (!newAudioStream) {
|
|
55
76
|
return;
|
|
56
77
|
}
|
|
57
78
|
if (!props.audioGraph || !props.merger) {
|
|
@@ -59,8 +80,7 @@ export const createAudioMixingProcess = (getCurrentMedia, signals, label = PROCE
|
|
|
59
80
|
return;
|
|
60
81
|
}
|
|
61
82
|
if (props.displaySource && props.merger) {
|
|
62
|
-
if (newStream ===
|
|
63
|
-
props.displaySource.audioNode?.mediaStream) {
|
|
83
|
+
if (newStream === props.displayStream) {
|
|
64
84
|
return;
|
|
65
85
|
}
|
|
66
86
|
props.audioGraph.disconnect([
|
|
@@ -68,14 +88,13 @@ export const createAudioMixingProcess = (getCurrentMedia, signals, label = PROCE
|
|
|
68
88
|
props.merger,
|
|
69
89
|
]);
|
|
70
90
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
91
|
+
props.displayStream = newStream;
|
|
92
|
+
props.displaySource =
|
|
93
|
+
createStreamSourceGraphNode(newAudioStream);
|
|
94
|
+
props.audioGraph.connect([
|
|
95
|
+
props.displaySource,
|
|
96
|
+
props.merger,
|
|
97
|
+
]);
|
|
79
98
|
}
|
|
80
99
|
else if (features.mixWithAdditionalMedia === false) {
|
|
81
100
|
// Unmixing
|
|
@@ -83,6 +102,7 @@ export const createAudioMixingProcess = (getCurrentMedia, signals, label = PROCE
|
|
|
83
102
|
props.displaySource.release();
|
|
84
103
|
props.audioGraph?.disconnect([props.displaySource]);
|
|
85
104
|
props.displaySource = undefined;
|
|
105
|
+
props.displayStream = undefined;
|
|
86
106
|
}
|
|
87
107
|
}
|
|
88
108
|
return Promise.resolve();
|
|
@@ -118,8 +138,10 @@ export const createAudioMixingProcess = (getCurrentMedia, signals, label = PROCE
|
|
|
118
138
|
}
|
|
119
139
|
assert(prevMediaTrack.track, 'Main track should be available');
|
|
120
140
|
const mainSource = createStreamSourceGraphNode(new MediaStream([prevMediaTrack.track]));
|
|
121
|
-
props.displaySource =
|
|
122
|
-
|
|
141
|
+
props.displaySource = displayAudioStream
|
|
142
|
+
? createStreamSourceGraphNode(displayAudioStream)
|
|
143
|
+
: undefined;
|
|
144
|
+
props.displayStream = displayAudioStream ? displayStream : undefined;
|
|
123
145
|
props.merger = createChannelMergerGraphNode();
|
|
124
146
|
const destination = createStreamDestinationGraphNode({
|
|
125
147
|
channelCount: 1,
|
|
@@ -151,10 +173,13 @@ export const createAudioMixingProcess = (getCurrentMedia, signals, label = PROCE
|
|
|
151
173
|
logger.debug({ label }, 'Release Media');
|
|
152
174
|
unsubscribe();
|
|
153
175
|
// Release Props
|
|
154
|
-
await
|
|
155
|
-
props.audioGraph
|
|
156
|
-
|
|
157
|
-
|
|
176
|
+
await audioGraph.release();
|
|
177
|
+
if (props.audioGraph === audioGraph) {
|
|
178
|
+
props.audioGraph = undefined;
|
|
179
|
+
props.merger = undefined;
|
|
180
|
+
props.displaySource = undefined;
|
|
181
|
+
props.displayStream = undefined;
|
|
182
|
+
}
|
|
158
183
|
};
|
|
159
184
|
return Promise.resolve(createMediaTrack({
|
|
160
185
|
label,
|
package/dist/audioProcessor.js
CHANGED
|
@@ -173,6 +173,14 @@ label = PROCESSOR_LABELS.AudioProcessor, }) => {
|
|
|
173
173
|
[source, analyzer],
|
|
174
174
|
];
|
|
175
175
|
logger.debug({ initialAudioNodeConnection, label }, 'Initial AudioNodeConnection');
|
|
176
|
+
if (props.denoiseNode) {
|
|
177
|
+
props.denoiseNode.release();
|
|
178
|
+
props.denoiseNode = undefined;
|
|
179
|
+
}
|
|
180
|
+
if (props.analyzer) {
|
|
181
|
+
props.analyzer.release();
|
|
182
|
+
props.analyzer = undefined;
|
|
183
|
+
}
|
|
176
184
|
const audioGraph = createAudioGraphProxy(createAudioGraph(initialAudioNodeConnection, props.audioGraphOptions), {
|
|
177
185
|
connect: (target, args) => {
|
|
178
186
|
logger.debug({ label, target, args }, 'connect nodes');
|
|
@@ -199,9 +207,11 @@ label = PROCESSOR_LABELS.AudioProcessor, }) => {
|
|
|
199
207
|
track.stop();
|
|
200
208
|
// Release Props
|
|
201
209
|
await audioGraph.release();
|
|
202
|
-
props.
|
|
203
|
-
|
|
204
|
-
|
|
210
|
+
if (props.audioGraph === audioGraph) {
|
|
211
|
+
props.denoiseNode = undefined;
|
|
212
|
+
props.analyzer = undefined;
|
|
213
|
+
props.audioGraph = undefined;
|
|
214
|
+
}
|
|
205
215
|
};
|
|
206
216
|
return createMediaTrack({
|
|
207
217
|
label,
|
package/dist/videoProcessor.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ interface VideoStreamProcessProps extends Partial<VideoRenderParams> {
|
|
|
22
22
|
contentHint?: VideoContentHint;
|
|
23
23
|
}
|
|
24
24
|
declare const FEATURE_KEYS: {
|
|
25
|
+
readonly backend: "string";
|
|
25
26
|
readonly backgroundBlurAmount: "number";
|
|
26
27
|
readonly backgroundImageUrl: "string";
|
|
27
28
|
readonly backgroundThreshold: "number";
|
package/dist/videoProcessor.js
CHANGED
|
@@ -6,6 +6,7 @@ import { logger } from './logger';
|
|
|
6
6
|
import { isVideoContentHint } from './typeGuard';
|
|
7
7
|
import { PROCESSOR_LABELS } from './constants';
|
|
8
8
|
const FEATURE_KEYS = {
|
|
9
|
+
backend: 'string',
|
|
9
10
|
backgroundBlurAmount: 'number',
|
|
10
11
|
backgroundImageUrl: 'string',
|
|
11
12
|
backgroundThreshold: 'number',
|
|
@@ -217,21 +218,26 @@ const applyFeatures = async (processor, features, referenceProcessingHeight) =>
|
|
|
217
218
|
case 'downSampleFactor':
|
|
218
219
|
case 'backgroundThreshold': {
|
|
219
220
|
const value = features[k];
|
|
220
|
-
if (value !== undefined
|
|
221
|
+
if (value !== undefined &&
|
|
222
|
+
value !== processor.renderOptions[k]) {
|
|
221
223
|
renderOptions[k] = value;
|
|
222
224
|
}
|
|
223
225
|
break;
|
|
224
226
|
}
|
|
225
227
|
case 'excludeBystanders': {
|
|
226
228
|
const value = features[k];
|
|
227
|
-
if (value !== undefined
|
|
229
|
+
if (value !== undefined &&
|
|
230
|
+
value !== processor.renderOptions[k]) {
|
|
228
231
|
renderOptions[k] = value;
|
|
229
232
|
}
|
|
230
233
|
break;
|
|
231
234
|
}
|
|
232
235
|
case 'personCenterX': {
|
|
233
236
|
const value = features[k];
|
|
234
|
-
if (value !== undefined
|
|
237
|
+
if (value !== undefined &&
|
|
238
|
+
value !== processor.renderOptions.personCenter[0]) {
|
|
239
|
+
// Keep the other coordinate from the local draft or existing
|
|
240
|
+
// processor state so X/Y single-field updates stay paired.
|
|
235
241
|
renderOptions.personCenter = [
|
|
236
242
|
value,
|
|
237
243
|
renderOptions.personCenter?.[1] ??
|
|
@@ -242,7 +248,10 @@ const applyFeatures = async (processor, features, referenceProcessingHeight) =>
|
|
|
242
248
|
}
|
|
243
249
|
case 'personCenterY': {
|
|
244
250
|
const value = features[k];
|
|
245
|
-
if (value !== undefined
|
|
251
|
+
if (value !== undefined &&
|
|
252
|
+
value !== processor.renderOptions.personCenter[1]) {
|
|
253
|
+
// Keep the other coordinate from the local draft or existing
|
|
254
|
+
// processor state so X/Y single-field updates stay paired.
|
|
246
255
|
renderOptions.personCenter = [
|
|
247
256
|
renderOptions.personCenter?.[0] ??
|
|
248
257
|
processor.renderOptions.personCenter[0],
|
|
@@ -255,14 +264,20 @@ const applyFeatures = async (processor, features, referenceProcessingHeight) =>
|
|
|
255
264
|
case 'backgroundBlurAmount': {
|
|
256
265
|
const value = features[k];
|
|
257
266
|
if (value !== undefined) {
|
|
258
|
-
|
|
267
|
+
const actualValue = getBlurKernelSize(value, referenceProcessingHeight ?? processor.processingHeight, processor.processingHeight);
|
|
268
|
+
if (actualValue !== processor.renderOptions[k]) {
|
|
269
|
+
renderOptions[k] = actualValue;
|
|
270
|
+
}
|
|
259
271
|
}
|
|
260
272
|
break;
|
|
261
273
|
}
|
|
262
274
|
case 'edgeBlurAmount': {
|
|
263
275
|
const value = features[k];
|
|
264
276
|
if (value !== undefined) {
|
|
265
|
-
|
|
277
|
+
const actualValue = getTentBlurSize(value, referenceProcessingHeight ?? processor.processingHeight, processor.processingHeight);
|
|
278
|
+
if (actualValue !== processor.renderOptions[k]) {
|
|
279
|
+
renderOptions[k] = actualValue;
|
|
280
|
+
}
|
|
266
281
|
}
|
|
267
282
|
break;
|
|
268
283
|
}
|
|
@@ -273,6 +288,13 @@ const applyFeatures = async (processor, features, referenceProcessingHeight) =>
|
|
|
273
288
|
}
|
|
274
289
|
break;
|
|
275
290
|
}
|
|
291
|
+
case 'backend': {
|
|
292
|
+
const value = features[k];
|
|
293
|
+
if (value && value !== processor.renderOptions.backend) {
|
|
294
|
+
renderOptions.backend = value;
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
276
298
|
case 'backgroundImageUrl': {
|
|
277
299
|
const value = features[k];
|
|
278
300
|
if (value &&
|
|
@@ -356,9 +378,23 @@ label = PROCESSOR_LABELS.VideoProcessor, referenceProcessingHeight, maskCombineR
|
|
|
356
378
|
});
|
|
357
379
|
}
|
|
358
380
|
else {
|
|
359
|
-
await applyFeatures(videoProcessor, {
|
|
381
|
+
await applyFeatures(videoProcessor, {
|
|
382
|
+
backend: gpuAPI(),
|
|
383
|
+
...renderOptions,
|
|
384
|
+
videoSegmentation,
|
|
385
|
+
}, referenceProcessingHeight);
|
|
360
386
|
}
|
|
361
387
|
const track = await videoProcessor.process(prevMediaTrack.track);
|
|
388
|
+
// The source track constraints may have changed while `process()` was
|
|
389
|
+
// awaiting the segmentation model to load (e.g. the user toggled blur
|
|
390
|
+
// in pre-flight before the processor was ready). The features computed
|
|
391
|
+
// above are already baked into the freshly opened processor, so
|
|
392
|
+
// re-apply anything that changed in the meantime to avoid silently
|
|
393
|
+
// dropping the effect.
|
|
394
|
+
const latestFeatures = updateFeatureProps(prevMediaTrack.getConstraints(), props);
|
|
395
|
+
if (!isEmpty(latestFeatures)) {
|
|
396
|
+
await applyFeatures(videoProcessor, latestFeatures, referenceProcessingHeight);
|
|
397
|
+
}
|
|
362
398
|
// Inherit contentHint from previous track
|
|
363
399
|
track.contentHint = prevMediaTrack.track.contentHint;
|
|
364
400
|
let trackReleaseTimeoutID = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pexip/media",
|
|
3
|
-
"version": "22.
|
|
3
|
+
"version": "22.2.0",
|
|
4
4
|
"description": "Home for media related stuff",
|
|
5
5
|
"homepage": "https://developer.pexip.com",
|
|
6
6
|
"bugs": "https://gitlab.com/pexip/zoo/issues",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"clean": "rm -fr dist api-docs",
|
|
40
40
|
"dev": "pexip-bundler dev",
|
|
41
41
|
"prepack": "yarn clean && yarn build && yarn docs",
|
|
42
|
-
"test": "
|
|
42
|
+
"test": "pexip-bundler test",
|
|
43
43
|
"check-format": "prettier --ignore-path=../../.prettierignore --check .",
|
|
44
44
|
"format": "prettier --ignore-path=../../.prettierignore --write .",
|
|
45
45
|
"lint": "yarn run -T biome lint",
|
|
@@ -47,18 +47,13 @@
|
|
|
47
47
|
"docs": "typedoc"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@pexip/media-control": "22.
|
|
51
|
-
"@pexip/media-processor": "22.
|
|
52
|
-
"@pexip/signal": "16.9.
|
|
53
|
-
"@pexip/utils": "17.4.
|
|
50
|
+
"@pexip/media-control": "22.2.0",
|
|
51
|
+
"@pexip/media-processor": "22.2.0",
|
|
52
|
+
"@pexip/signal": "16.9.6",
|
|
53
|
+
"@pexip/utils": "17.4.1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@
|
|
57
|
-
"@pexip/bundler": "18.2.1",
|
|
58
|
-
"@swc/core": "^1.15.33",
|
|
59
|
-
"@swc/jest": "^0.2.39",
|
|
60
|
-
"jest": "^29.5.12",
|
|
61
|
-
"jest-junit": "^16.0.0",
|
|
56
|
+
"@pexip/bundler": "18.3.0",
|
|
62
57
|
"prettier": "^3.2.5",
|
|
63
58
|
"typedoc": "^0.28.18",
|
|
64
59
|
"typedoc-plugin-markdown": "^4.11.0",
|