@pexip/media 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.
- package/CHANGELOG.md +126 -0
- package/README.md +9 -8
- package/dist/audioMixingProcessor.d.ts +2 -2
- package/dist/audioMixingProcessor.js +113 -100
- package/dist/audioProcessor.d.ts +4 -4
- package/dist/audioProcessor.js +117 -152
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +7 -0
- package/dist/media.d.ts +12 -2
- package/dist/media.js +250 -169
- package/dist/previewController.d.ts +10 -9
- package/dist/previewController.js +100 -68
- package/dist/signals.d.ts +10 -1
- package/dist/signals.js +9 -0
- package/dist/status.d.ts +5 -2
- package/dist/status.js +3 -3
- package/dist/types.d.ts +135 -40
- package/dist/userMedia.d.ts +10 -8
- package/dist/userMedia.js +122 -178
- package/dist/utils.d.ts +25 -47
- package/dist/utils.js +489 -187
- package/dist/videoProcessor.d.ts +3 -7
- package/dist/videoProcessor.js +103 -139
- package/package.json +6 -5
package/dist/utils.js
CHANGED
|
@@ -1,41 +1,165 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createStreamTrackEventSubscriptions, createTrackDevicesChanges, extractConstraintsWithKeys, findDeviceFromConstraints, relaxInputConstraint, resolveMediaDeviceConstraints, shouldRequestDevice, toMediaDeviceInputKind, } from '@pexip/media-control';
|
|
2
2
|
import { calculateMaxBlurPass } from '@pexip/media-processor';
|
|
3
|
-
import { hasOwn } from '@pexip/utils';
|
|
4
|
-
import {
|
|
5
|
-
import { isOverConstrained } from './status';
|
|
6
|
-
import { isMedia } from './typeGuard';
|
|
3
|
+
import { hasOwn, assert, isEmpty } from '@pexip/utils';
|
|
4
|
+
import { internalSignals } from './signals';
|
|
7
5
|
export const makeDeriveDeviceStatus = (constraints) => (audio, video, both) => {
|
|
8
|
-
if (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
if (constraints.audio === undefined || constraints.audio) {
|
|
7
|
+
if (constraints.video === undefined || constraints.video) {
|
|
8
|
+
return both;
|
|
9
|
+
}
|
|
12
10
|
return audio;
|
|
13
11
|
}
|
|
12
|
+
if (constraints.video === undefined || constraints.video) {
|
|
13
|
+
return video;
|
|
14
|
+
}
|
|
14
15
|
return both;
|
|
15
16
|
};
|
|
16
|
-
export const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
17
|
+
export const createMediaTrack = (trackInit) => {
|
|
18
|
+
assert(trackInit.track === undefined ||
|
|
19
|
+
(trackInit.kind === trackInit.input?.kind &&
|
|
20
|
+
trackInit.kind === toMediaDeviceInputKind(trackInit.track),
|
|
21
|
+
'Inconsistent track kind'));
|
|
22
|
+
const currentConstraints = typeof trackInit.constraints === 'boolean' ? {} : trackInit.constraints;
|
|
23
|
+
// Find the original source track through the linked list
|
|
24
|
+
let sourceMediaTrack = trackInit.previousMediaTrack;
|
|
25
|
+
while (sourceMediaTrack?.previousMediaTrack) {
|
|
26
|
+
sourceMediaTrack = sourceMediaTrack.previousMediaTrack;
|
|
27
|
+
}
|
|
28
|
+
const getConstraints = () => {
|
|
29
|
+
if (trackInit.constraints === false) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
...currentConstraints,
|
|
34
|
+
...trackInit.track?.getConstraints(),
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const getSettings = () => {
|
|
38
|
+
return {
|
|
39
|
+
...trackInit.previousMediaTrack?.getSettings(),
|
|
40
|
+
...trackInit.track?.getSettings(),
|
|
41
|
+
...(trackInit.getSettings?.() ?? {}),
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
const mediaTrack = {
|
|
45
|
+
get kind() {
|
|
46
|
+
return trackInit.kind;
|
|
47
|
+
},
|
|
48
|
+
get id() {
|
|
49
|
+
return trackInit.id ?? trackInit.track?.id ?? trackInit.kind;
|
|
50
|
+
},
|
|
51
|
+
get track() {
|
|
52
|
+
return trackInit.track;
|
|
53
|
+
},
|
|
54
|
+
get input() {
|
|
55
|
+
return trackInit.input;
|
|
56
|
+
},
|
|
57
|
+
get expectedInput() {
|
|
58
|
+
return trackInit.expectedInput;
|
|
59
|
+
},
|
|
60
|
+
get label() {
|
|
61
|
+
return trackInit.label;
|
|
62
|
+
},
|
|
63
|
+
get previousMediaTrack() {
|
|
64
|
+
return trackInit.previousMediaTrack;
|
|
65
|
+
},
|
|
66
|
+
get muted() {
|
|
67
|
+
if (trackInit.overrideMute) {
|
|
68
|
+
return trackInit.muted;
|
|
69
|
+
}
|
|
70
|
+
return (
|
|
71
|
+
// If the source track muted, the following processed track will
|
|
72
|
+
// be muted as well since there is no data to process
|
|
73
|
+
!!isTrackMuted(this.source.track) ||
|
|
74
|
+
isTrackMuted(trackInit.track));
|
|
75
|
+
},
|
|
76
|
+
get source() {
|
|
77
|
+
return sourceMediaTrack ?? mediaTrack;
|
|
78
|
+
},
|
|
79
|
+
mute(toMute) {
|
|
80
|
+
if (trackInit.overrideMute) {
|
|
81
|
+
return trackInit.mute?.(toMute);
|
|
31
82
|
}
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
|
|
83
|
+
trackInit.previousMediaTrack?.mute(toMute);
|
|
84
|
+
if (trackInit.track) {
|
|
85
|
+
trackInit.track.enabled = !toMute;
|
|
35
86
|
}
|
|
36
|
-
|
|
87
|
+
},
|
|
88
|
+
getSettings,
|
|
89
|
+
getConstraints() {
|
|
90
|
+
return getConstraints();
|
|
91
|
+
},
|
|
92
|
+
clone() {
|
|
93
|
+
return createMediaTrack({
|
|
94
|
+
...trackInit,
|
|
95
|
+
constraints: getConstraints(),
|
|
96
|
+
track: trackInit.track?.clone(),
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
async applyConstraints(constraints) {
|
|
100
|
+
const resolvedConstraints = resolveMediaDeviceConstraints(constraints);
|
|
101
|
+
if (typeof resolvedConstraints === 'boolean' ||
|
|
102
|
+
resolvedConstraints === undefined) {
|
|
103
|
+
return Promise.resolve();
|
|
104
|
+
}
|
|
105
|
+
await trackInit.previousMediaTrack?.applyConstraints(resolvedConstraints);
|
|
106
|
+
await trackInit.applyConstraints?.(constraints);
|
|
107
|
+
if (trackInit.track) {
|
|
108
|
+
if ('contentHint' in resolvedConstraints &&
|
|
109
|
+
typeof resolvedConstraints.contentHint === 'string' &&
|
|
110
|
+
trackInit.track.contentHint !==
|
|
111
|
+
resolvedConstraints.contentHint) {
|
|
112
|
+
trackInit.track.contentHint =
|
|
113
|
+
resolvedConstraints.contentHint;
|
|
114
|
+
}
|
|
115
|
+
const settings = trackInit.track.getSettings();
|
|
116
|
+
const reducedConstraints = Object.keys(resolvedConstraints).reduce((accm, key) => {
|
|
117
|
+
const constraintKey = key;
|
|
118
|
+
if (constraintKey in settings) {
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment --- Type issue and need to be fixed when we have time
|
|
120
|
+
// @ts-expect-error
|
|
121
|
+
accm[constraintKey] =
|
|
122
|
+
resolvedConstraints[constraintKey];
|
|
123
|
+
}
|
|
124
|
+
return accm;
|
|
125
|
+
}, {});
|
|
126
|
+
// Firefox will throw an error if passing an empty constraint object
|
|
127
|
+
if (!isEmpty(reducedConstraints)) {
|
|
128
|
+
await trackInit.track.applyConstraints(reducedConstraints);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Update the current constraints
|
|
132
|
+
Object.assign(currentConstraints, resolvedConstraints);
|
|
133
|
+
},
|
|
134
|
+
async release() {
|
|
135
|
+
trackInit.track?.stop();
|
|
136
|
+
if (trackInit.track) {
|
|
137
|
+
trackInit.signals?.onTrackReleased?.emit(trackInit.track);
|
|
138
|
+
trackInit.signals?.[trackInit.track.kind === 'audio'
|
|
139
|
+
? 'onAudioMuteStateChanged'
|
|
140
|
+
: 'onVideoMuteStateChanged']?.emit(undefined);
|
|
141
|
+
}
|
|
142
|
+
await trackInit.previousMediaTrack?.release();
|
|
143
|
+
await trackInit.release?.();
|
|
144
|
+
},
|
|
145
|
+
toJSON() {
|
|
146
|
+
return {
|
|
147
|
+
label: trackInit.label,
|
|
148
|
+
track: trackInit.track,
|
|
149
|
+
previousMediaTrack: trackInit.previousMediaTrack,
|
|
150
|
+
constraints: getConstraints(),
|
|
151
|
+
settings: getSettings(),
|
|
152
|
+
};
|
|
37
153
|
},
|
|
38
154
|
};
|
|
155
|
+
return mediaTrack;
|
|
156
|
+
};
|
|
157
|
+
export const createTrackProcessingPipeline = (processors) => async (track) => {
|
|
158
|
+
let lastTrack = track;
|
|
159
|
+
for (const process of processors) {
|
|
160
|
+
lastTrack = await process(lastTrack);
|
|
161
|
+
}
|
|
162
|
+
return lastTrack;
|
|
39
163
|
};
|
|
40
164
|
/**
|
|
41
165
|
* Interpret provided input to resolve to a MediaDeviceInfoLike when possible
|
|
@@ -50,30 +174,15 @@ export const interpretInput = (input, getCurrentInput) => {
|
|
|
50
174
|
}
|
|
51
175
|
return input;
|
|
52
176
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return props.cachedExpectedInputs.get(constraints);
|
|
63
|
-
}
|
|
64
|
-
const { devices, input } = getInfo();
|
|
65
|
-
// Update cache
|
|
66
|
-
const relaxedConstraints = relaxInputConstraint(constraints, devices);
|
|
67
|
-
const { device: [[device] = []], } = extractConstraintsWithKeys(['device'])(relaxedConstraints);
|
|
68
|
-
// The result from `findDeviceFromConstraints` has more restrictive
|
|
69
|
-
// result since it also consider if the device can be found from the
|
|
70
|
-
// device list
|
|
71
|
-
const found = device ?? findDeviceFromConstraints(constraints, devices);
|
|
72
|
-
const expectedInput = interpretInput(found, () => input);
|
|
73
|
-
props.cachedExpectedInputs.clear();
|
|
74
|
-
props.cachedExpectedInputs.set(constraints, expectedInput);
|
|
75
|
-
return expectedInput;
|
|
76
|
-
};
|
|
177
|
+
export const findExpectedInput = (devices, constraints, input, kind) => {
|
|
178
|
+
const relaxedConstraints = relaxInputConstraint(kind, constraints, devices);
|
|
179
|
+
const { device: [[device] = []], } = extractConstraintsWithKeys(['device'])(relaxedConstraints);
|
|
180
|
+
// The result from `findDeviceFromConstraints` has more restrictive
|
|
181
|
+
// result since it also consider if the device can be found from the
|
|
182
|
+
// device list
|
|
183
|
+
const found = device ?? findDeviceFromConstraints(constraints, devices);
|
|
184
|
+
const expectedInput = interpretInput(found, () => input);
|
|
185
|
+
return expectedInput;
|
|
77
186
|
};
|
|
78
187
|
/**
|
|
79
188
|
* A utility function to check if the provided track is muted. There are
|
|
@@ -86,150 +195,294 @@ export const createMemorizedGetExpectedInput = () => {
|
|
|
86
195
|
* | false | false | true |
|
|
87
196
|
* ```
|
|
88
197
|
*
|
|
89
|
-
* @param
|
|
198
|
+
* @param track - The tracks can be got from `MediaStream['getAudioStats']` or
|
|
90
199
|
* `MediaStream['getVideoTracks']`
|
|
91
200
|
*
|
|
92
201
|
* @returns `true` means muted, `false` means not muted and `undefined` means
|
|
93
202
|
* there is no track to check
|
|
94
203
|
*/
|
|
95
|
-
export const
|
|
96
|
-
if (!
|
|
204
|
+
export const isTrackMuted = (track) => {
|
|
205
|
+
if (!track || track.readyState === 'ended') {
|
|
97
206
|
return undefined;
|
|
98
207
|
}
|
|
99
|
-
return
|
|
208
|
+
return track.muted || !track.enabled;
|
|
100
209
|
};
|
|
101
|
-
export const buildMedia = (
|
|
210
|
+
export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevicesChanged) => {
|
|
102
211
|
const props = {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
212
|
+
originalConstraints: mediaInit.originalConstraints ?? mediaInit.constraints,
|
|
213
|
+
status: mediaInit.status,
|
|
214
|
+
devices: mediaInit.devices,
|
|
215
|
+
stream: mediaInit.stream ?? new MediaStream(),
|
|
216
|
+
audioTrack: mediaInit.tracks
|
|
217
|
+
.flatMap(track => {
|
|
218
|
+
return track.kind === 'audioinput' ? [track] : [];
|
|
219
|
+
})
|
|
220
|
+
.at(0),
|
|
221
|
+
videoTrack: mediaInit.tracks
|
|
222
|
+
.flatMap(track => (track.kind === 'videoinput' ? [track] : []))
|
|
223
|
+
.at(0),
|
|
224
|
+
};
|
|
225
|
+
const subscribeTrackEvents = (track) => createStreamTrackEventSubscriptions(track, {
|
|
226
|
+
ended: track => {
|
|
227
|
+
mediaInit.signals?.[track.kind === 'audio'
|
|
228
|
+
? 'onAudioMuteStateChanged'
|
|
229
|
+
: 'onVideoMuteStateChanged']?.emit(undefined);
|
|
230
|
+
mediaInit.signals?.onStreamTrackEndedFinal?.emit(track);
|
|
231
|
+
},
|
|
232
|
+
mute: track => {
|
|
233
|
+
mediaInit.signals?.[track.kind === 'audio'
|
|
234
|
+
? 'onAudioMuteStateChanged'
|
|
235
|
+
: 'onVideoMuteStateChanged']?.emit(true);
|
|
236
|
+
mediaInit.signals?.onStreamTrackMuted?.emit(track);
|
|
237
|
+
},
|
|
238
|
+
unmute: track => {
|
|
239
|
+
mediaInit.signals?.[track.kind === 'audio'
|
|
240
|
+
? 'onAudioMuteStateChanged'
|
|
241
|
+
: 'onVideoMuteStateChanged']?.emit(!track.enabled);
|
|
242
|
+
mediaInit.signals?.onStreamTrackUnmuted?.emit(track);
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
const subscribeTrackAndSourceTrackEvents = (track) => {
|
|
246
|
+
let trackUnsubscribe;
|
|
247
|
+
let sourceTrackUnsubscribe;
|
|
248
|
+
if (track.track) {
|
|
249
|
+
trackUnsubscribe = subscribeTrackEvents(track.track);
|
|
250
|
+
if (track.source.track && track.source.track !== track.track) {
|
|
251
|
+
sourceTrackUnsubscribe = subscribeTrackEvents(track.source.track);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return () => {
|
|
255
|
+
trackUnsubscribe?.();
|
|
256
|
+
sourceTrackUnsubscribe?.();
|
|
257
|
+
};
|
|
106
258
|
};
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (mute) {
|
|
113
|
-
return mute(muted);
|
|
259
|
+
const trackSubscriptions = new Map(mediaInit.tracks.flatMap(track => {
|
|
260
|
+
if (track.track) {
|
|
261
|
+
return [
|
|
262
|
+
[track.track, subscribeTrackAndSourceTrackEvents(track)],
|
|
263
|
+
];
|
|
114
264
|
}
|
|
115
|
-
return
|
|
265
|
+
return [];
|
|
266
|
+
}));
|
|
267
|
+
const getConstraints = () => {
|
|
268
|
+
const audio = props.audioTrack?.getConstraints() ?? mediaInit.constraints.audio;
|
|
269
|
+
const video = props.videoTrack?.getConstraints() ?? mediaInit.constraints.video;
|
|
270
|
+
return { audio: audio, video: video };
|
|
116
271
|
};
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
272
|
+
let unsubscribe = onDevicesChanged.add(devices => {
|
|
273
|
+
props.devices = devices;
|
|
274
|
+
});
|
|
275
|
+
const muteTrack = (track, mute) => {
|
|
276
|
+
const previousMuteState = track.muted;
|
|
277
|
+
const previousEnabledState = track.track?.enabled;
|
|
278
|
+
track.mute(mute);
|
|
279
|
+
const currentMuteState = track.muted;
|
|
280
|
+
const currentEnabledState = track.track?.enabled;
|
|
281
|
+
if (track.track &&
|
|
282
|
+
previousEnabledState !== undefined &&
|
|
283
|
+
currentEnabledState !== undefined &&
|
|
284
|
+
previousEnabledState !== currentEnabledState) {
|
|
285
|
+
mediaInit.signals?.onStreamTrackEnabled?.emit(track.track);
|
|
286
|
+
}
|
|
287
|
+
if (previousMuteState !== currentMuteState) {
|
|
288
|
+
mediaInit.signals?.[track.kind === 'audioinput'
|
|
289
|
+
? 'onAudioMuteStateChanged'
|
|
290
|
+
: 'onVideoMuteStateChanged']?.emit(currentMuteState);
|
|
121
291
|
}
|
|
122
|
-
return new Promise(resolve => {
|
|
123
|
-
stopMediaStream(stream);
|
|
124
|
-
resolve();
|
|
125
|
-
});
|
|
126
292
|
};
|
|
127
293
|
return {
|
|
128
|
-
get
|
|
129
|
-
return props.
|
|
294
|
+
get id() {
|
|
295
|
+
return props.stream.id;
|
|
296
|
+
},
|
|
297
|
+
get active() {
|
|
298
|
+
return !!props.stream.active;
|
|
130
299
|
},
|
|
131
300
|
get devices() {
|
|
132
301
|
return props.devices;
|
|
133
302
|
},
|
|
134
|
-
set devices(newDevices) {
|
|
135
|
-
props.devices = newDevices;
|
|
136
|
-
},
|
|
137
303
|
get stream() {
|
|
138
|
-
return
|
|
304
|
+
return props.stream;
|
|
139
305
|
},
|
|
140
306
|
get expectedAudioInput() {
|
|
141
|
-
|
|
142
|
-
return getExpectedAudioInput(props.constraints?.audio, () => ({
|
|
143
|
-
devices: media.devices?.filter(isAudioInput) ?? [],
|
|
144
|
-
input: media.audioInput,
|
|
145
|
-
}));
|
|
307
|
+
return props.audioTrack?.expectedInput;
|
|
146
308
|
},
|
|
147
309
|
get expectedVideoInput() {
|
|
148
|
-
|
|
149
|
-
return getExpectedVideoInput(props.constraints?.video, () => ({
|
|
150
|
-
devices: media.devices?.filter(isVideoInput) ?? [],
|
|
151
|
-
input: media.videoInput,
|
|
152
|
-
}));
|
|
153
|
-
},
|
|
154
|
-
get rawStream() {
|
|
155
|
-
const { rawStream, stream } = getMedia();
|
|
156
|
-
return rawStream ?? stream;
|
|
310
|
+
return props.videoTrack?.expectedInput;
|
|
157
311
|
},
|
|
158
312
|
get audioInput() {
|
|
159
|
-
return
|
|
313
|
+
return props.audioTrack?.input;
|
|
160
314
|
},
|
|
161
315
|
get videoInput() {
|
|
162
|
-
return
|
|
316
|
+
return props.videoTrack?.input;
|
|
163
317
|
},
|
|
164
318
|
get status() {
|
|
165
319
|
return props.status;
|
|
166
320
|
},
|
|
167
321
|
set status(status) {
|
|
168
|
-
props.status
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
props.constraints = value;
|
|
322
|
+
if (props.status !== status) {
|
|
323
|
+
props.status = status;
|
|
324
|
+
mediaInit.signals?.onStatusChanged?.emit(status);
|
|
325
|
+
}
|
|
173
326
|
},
|
|
174
327
|
get audioMuted() {
|
|
175
|
-
return
|
|
328
|
+
return props.audioTrack?.muted;
|
|
176
329
|
},
|
|
177
330
|
get videoMuted() {
|
|
178
|
-
return
|
|
331
|
+
return props.videoTrack?.muted;
|
|
332
|
+
},
|
|
333
|
+
getConstraints,
|
|
334
|
+
getOriginalConstraints() {
|
|
335
|
+
return props.originalConstraints;
|
|
336
|
+
},
|
|
337
|
+
isAudioInputUnavailable() {
|
|
338
|
+
return (mediaInit.permission.audio === 'granted' &&
|
|
339
|
+
props.devices.size('audioinput') === 0);
|
|
340
|
+
},
|
|
341
|
+
isVideoInputUnavailable() {
|
|
342
|
+
return (mediaInit.permission.video === 'granted' &&
|
|
343
|
+
props.devices.size('videoinput') === 0);
|
|
344
|
+
},
|
|
345
|
+
setOriginalConstraints(constraints) {
|
|
346
|
+
props.originalConstraints = constraints;
|
|
347
|
+
},
|
|
348
|
+
requestedAudio() {
|
|
349
|
+
return (Boolean(props.audioTrack?.track) ||
|
|
350
|
+
Boolean(props.originalConstraints.audio));
|
|
351
|
+
},
|
|
352
|
+
requestedVideo() {
|
|
353
|
+
return (Boolean(props.videoTrack?.track) ||
|
|
354
|
+
Boolean(props.originalConstraints.video));
|
|
355
|
+
},
|
|
356
|
+
muteAudio(mute) {
|
|
357
|
+
if (!props.audioTrack) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
muteTrack(props.audioTrack, mute);
|
|
361
|
+
},
|
|
362
|
+
muteVideo(mute) {
|
|
363
|
+
if (!props.videoTrack) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
muteTrack(props.videoTrack, mute);
|
|
179
367
|
},
|
|
180
|
-
muteAudio: muteTrack('audio'),
|
|
181
|
-
muteVideo: muteTrack('video'),
|
|
182
368
|
applyConstraints: async (constraints) => {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
369
|
+
await Promise.all([
|
|
370
|
+
typeof constraints.audio !== 'object'
|
|
371
|
+
? Promise.resolve()
|
|
372
|
+
: props.audioTrack?.applyConstraints(constraints.audio),
|
|
373
|
+
typeof constraints.video !== 'object'
|
|
374
|
+
? Promise.resolve()
|
|
375
|
+
: props.videoTrack?.applyConstraints(constraints.video),
|
|
376
|
+
]);
|
|
377
|
+
},
|
|
378
|
+
async release() {
|
|
379
|
+
unsubscribe?.();
|
|
380
|
+
unsubscribe = undefined;
|
|
381
|
+
trackSubscriptions.forEach(unsubscribe => unsubscribe());
|
|
382
|
+
trackSubscriptions.clear();
|
|
383
|
+
await Promise.all([
|
|
384
|
+
props.audioTrack?.release(),
|
|
385
|
+
props.videoTrack?.release(),
|
|
386
|
+
]);
|
|
387
|
+
},
|
|
388
|
+
getSettings: () => ({
|
|
389
|
+
audio: props.audioTrack?.getSettings(),
|
|
390
|
+
video: props.videoTrack?.getSettings(),
|
|
391
|
+
}),
|
|
392
|
+
clone() {
|
|
393
|
+
const tracks = [props.audioTrack, props.videoTrack].flatMap(track => {
|
|
394
|
+
if (!track?.source) {
|
|
395
|
+
return [];
|
|
396
|
+
}
|
|
397
|
+
const cloned = track.source.clone();
|
|
398
|
+
// Restore the enabled state for all cloned track
|
|
399
|
+
cloned.mute(false);
|
|
400
|
+
return cloned;
|
|
401
|
+
});
|
|
402
|
+
const stream = this.stream &&
|
|
403
|
+
new MediaStream(tracks.flatMap(track => (track.track ? [track.track] : [])));
|
|
404
|
+
const clonedMedia = buildMedia({
|
|
405
|
+
tracks,
|
|
406
|
+
devices: this.devices,
|
|
407
|
+
status: this.status,
|
|
408
|
+
stream,
|
|
409
|
+
constraints: this.getConstraints(),
|
|
410
|
+
originalConstraints: this.getOriginalConstraints(),
|
|
411
|
+
permission: mediaInit.permission,
|
|
412
|
+
});
|
|
413
|
+
return clonedMedia;
|
|
414
|
+
},
|
|
415
|
+
getTracks() {
|
|
416
|
+
return [props.audioTrack, props.videoTrack].flatMap(track => track ? [track] : []);
|
|
417
|
+
},
|
|
418
|
+
getAudioTracks() {
|
|
419
|
+
return props.audioTrack ? [props.audioTrack] : [];
|
|
420
|
+
},
|
|
421
|
+
getVideoTracks() {
|
|
422
|
+
return props.videoTrack ? [props.videoTrack] : [];
|
|
423
|
+
},
|
|
424
|
+
addTrack(track) {
|
|
425
|
+
switch (track.kind) {
|
|
426
|
+
case 'audioinput':
|
|
427
|
+
if (track.id === props.audioTrack?.id) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
props.audioTrack = track;
|
|
431
|
+
break;
|
|
432
|
+
case 'videoinput':
|
|
433
|
+
if (track.id === props.videoTrack?.id) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
props.videoTrack = track;
|
|
437
|
+
break;
|
|
438
|
+
default:
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
if (track.track) {
|
|
442
|
+
props.stream.addTrack(track.track);
|
|
443
|
+
if (!trackSubscriptions.has(track.track)) {
|
|
444
|
+
trackSubscriptions.set(track.track, subscribeTrackAndSourceTrackEvents(track));
|
|
445
|
+
}
|
|
446
|
+
mediaInit.signals?.onAddTrack?.emit(track.track);
|
|
447
|
+
props.stream.dispatchEvent(new MediaStreamTrackEvent('addtrack', { track: track.track }));
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
removeTrack(track) {
|
|
451
|
+
let removed = false;
|
|
452
|
+
if (track.id === props.audioTrack?.id) {
|
|
453
|
+
props.audioTrack = undefined;
|
|
454
|
+
removed = true;
|
|
186
455
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
getSettings: () => {
|
|
191
|
-
const { getSettings, stream } = getMedia();
|
|
192
|
-
if (!stream) {
|
|
193
|
-
return {
|
|
194
|
-
audio: [],
|
|
195
|
-
video: [],
|
|
196
|
-
};
|
|
456
|
+
if (track.id === props.videoTrack?.id) {
|
|
457
|
+
props.videoTrack = undefined;
|
|
458
|
+
removed = true;
|
|
197
459
|
}
|
|
198
|
-
if (
|
|
199
|
-
|
|
460
|
+
if (removed && track.track) {
|
|
461
|
+
props.stream.removeTrack(track.track);
|
|
462
|
+
trackSubscriptions.get(track.track)?.();
|
|
463
|
+
trackSubscriptions.delete(track.track);
|
|
464
|
+
mediaInit.signals?.onRemoveTrack?.emit(track.track);
|
|
465
|
+
props.stream.dispatchEvent(new MediaStreamTrackEvent('removetrack', {
|
|
466
|
+
track: track.track,
|
|
467
|
+
}));
|
|
200
468
|
}
|
|
469
|
+
},
|
|
470
|
+
toJSON() {
|
|
201
471
|
return {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
472
|
+
constraints: this.getConstraints(),
|
|
473
|
+
devices: this.devices,
|
|
474
|
+
stream: this.stream,
|
|
475
|
+
audioInput: this.audioInput,
|
|
476
|
+
videoInput: this.videoInput,
|
|
477
|
+
expectedAudioInput: this.expectedAudioInput,
|
|
478
|
+
expectedVideoInput: this.expectedVideoInput,
|
|
479
|
+
status: this.status,
|
|
480
|
+
audioMuted: this.audioMuted,
|
|
481
|
+
videoMuted: this.videoMuted,
|
|
208
482
|
};
|
|
209
483
|
},
|
|
210
|
-
toJSON: () => toJSON(getMedia()),
|
|
211
484
|
};
|
|
212
485
|
};
|
|
213
|
-
/**
|
|
214
|
-
* Clone the media from the rawStream (if any), otherwise, stream
|
|
215
|
-
*/
|
|
216
|
-
export const cloneMedia = async (media) => {
|
|
217
|
-
const stream = (media.rawStream ?? media.stream)?.clone();
|
|
218
|
-
// Restore the enabled state for all cloned track
|
|
219
|
-
stream?.getTracks().forEach(track => (track.enabled = true));
|
|
220
|
-
const { audio, video } = media.getSettings();
|
|
221
|
-
const clonedMedia = buildMedia(() => ({
|
|
222
|
-
stream,
|
|
223
|
-
constraints: media?.constraints,
|
|
224
|
-
devices: media?.devices,
|
|
225
|
-
status: media?.status,
|
|
226
|
-
rawStream: stream,
|
|
227
|
-
audioInput: media?.audioInput,
|
|
228
|
-
videoInput: media?.videoInput,
|
|
229
|
-
getSettings: () => ({ audio, video }),
|
|
230
|
-
}));
|
|
231
|
-
return Promise.resolve(clonedMedia);
|
|
232
|
-
};
|
|
233
486
|
/**
|
|
234
487
|
* Shallow copy the provided object and override with provided overriding
|
|
235
488
|
*
|
|
@@ -246,28 +499,8 @@ export const getDevicesChanges = (prev, next) => {
|
|
|
246
499
|
const trackChanges = createTrackDevicesChanges(prev);
|
|
247
500
|
return trackChanges(next);
|
|
248
501
|
};
|
|
249
|
-
/**
|
|
250
|
-
* Apply Extended constraints on top of the original
|
|
251
|
-
*
|
|
252
|
-
* @param media - The media from the media pipeline
|
|
253
|
-
* @param applyExtended - The function to be called when the previous
|
|
254
|
-
* `applyConstraints` is done
|
|
255
|
-
*/
|
|
256
|
-
export const applyExtendedConstraints = (media, applyExtended) =>
|
|
257
|
-
/**
|
|
258
|
-
* Apply constraints
|
|
259
|
-
* @param constraints - The constraints to be applied to the media
|
|
260
|
-
*/
|
|
261
|
-
async (constraints) => {
|
|
262
|
-
await media.applyConstraints(constraints);
|
|
263
|
-
if (!isOverConstrained(media.status)) {
|
|
264
|
-
await applyExtended(constraints);
|
|
265
|
-
}
|
|
266
|
-
};
|
|
267
502
|
export const AUDIO_SETTINGS_KEYS = [
|
|
268
503
|
'denoise',
|
|
269
|
-
'vad',
|
|
270
|
-
'asd',
|
|
271
504
|
'contentHint',
|
|
272
505
|
];
|
|
273
506
|
export const VIDEO_SETTINGS_KEYS = [
|
|
@@ -279,8 +512,6 @@ export const VIDEO_SETTINGS_KEYS = [
|
|
|
279
512
|
'edgeBlurAmount',
|
|
280
513
|
'maskCombineRatio',
|
|
281
514
|
'backgroundImageUrl',
|
|
282
|
-
'width',
|
|
283
|
-
'height',
|
|
284
515
|
'contentHint',
|
|
285
516
|
];
|
|
286
517
|
export const MIXING_SETTINGS_KEYS = [
|
|
@@ -314,24 +545,44 @@ export const hasSettingsChanged = (keysToLookFor) => {
|
|
|
314
545
|
return cache.result;
|
|
315
546
|
};
|
|
316
547
|
};
|
|
317
|
-
export const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
548
|
+
export const diffSettings = (keysToLookFor) => {
|
|
549
|
+
const cache = {};
|
|
550
|
+
return (settingsA, settingsB) => {
|
|
551
|
+
// Same as before
|
|
552
|
+
if (cache.settingsA === settingsA && cache.settingsB === settingsB) {
|
|
553
|
+
return cache.diff;
|
|
554
|
+
}
|
|
555
|
+
cache.settingsA = settingsA;
|
|
556
|
+
cache.settingsB = settingsB;
|
|
557
|
+
// Nothing changed
|
|
558
|
+
if (settingsA === settingsB) {
|
|
559
|
+
cache.diff = undefined;
|
|
560
|
+
return cache.diff;
|
|
561
|
+
}
|
|
562
|
+
if (settingsA === undefined || settingsB === undefined) {
|
|
563
|
+
cache.diff = settingsB;
|
|
564
|
+
return cache.diff;
|
|
565
|
+
}
|
|
566
|
+
for (const key of keysToLookFor) {
|
|
567
|
+
if (settingsA[key] !== settingsB[key]) {
|
|
568
|
+
if (cache.diff === undefined) {
|
|
569
|
+
cache.diff = {};
|
|
570
|
+
}
|
|
571
|
+
// @ts-expect-error --- Type issue and need to be fixed when we have time
|
|
572
|
+
cache.diff[key] = settingsB[key];
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return cache.diff;
|
|
330
576
|
};
|
|
331
577
|
};
|
|
332
|
-
export const
|
|
333
|
-
|
|
334
|
-
|
|
578
|
+
export const mergeSettings = (settingsA, settingsB) => {
|
|
579
|
+
if (settingsA === undefined) {
|
|
580
|
+
return settingsB;
|
|
581
|
+
}
|
|
582
|
+
if (settingsB === undefined) {
|
|
583
|
+
return settingsA;
|
|
584
|
+
}
|
|
585
|
+
return { ...settingsA, ...settingsB };
|
|
335
586
|
};
|
|
336
587
|
/**
|
|
337
588
|
* A function to get the blur kernel size of image height
|
|
@@ -370,3 +621,54 @@ export const hasPtzFeature = () => {
|
|
|
370
621
|
hasOwn(supports, 'tilt') &&
|
|
371
622
|
hasOwn(supports, 'zoom'));
|
|
372
623
|
};
|
|
624
|
+
export const refineMediaConstraints = (state) => {
|
|
625
|
+
switch (state.permission) {
|
|
626
|
+
case 'denied':
|
|
627
|
+
return false;
|
|
628
|
+
case 'granted': {
|
|
629
|
+
const requestNeeded = shouldRequestDevice({
|
|
630
|
+
kind: state.kind,
|
|
631
|
+
permission: state.permission,
|
|
632
|
+
currentDevices: state.currentDevices,
|
|
633
|
+
request: state.request,
|
|
634
|
+
tracks: state.currentMediaTracks.flatMap(track => track.source?.track ? [track.source.track] : []),
|
|
635
|
+
});
|
|
636
|
+
if (requestNeeded || state.force) {
|
|
637
|
+
return state.request;
|
|
638
|
+
}
|
|
639
|
+
if (state.request) {
|
|
640
|
+
// We already have all we need, skip the request
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
return state.request;
|
|
644
|
+
}
|
|
645
|
+
case 'prompt':
|
|
646
|
+
default:
|
|
647
|
+
return state.request;
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
export const createMediaProcessor = ({ audioProcessors, videoProcessors, onProcessingError, }) => {
|
|
651
|
+
const processAudioTrack = createTrackProcessingPipeline(audioProcessors);
|
|
652
|
+
const processVideoTrack = createTrackProcessingPipeline(videoProcessors);
|
|
653
|
+
return async (newTracks) => {
|
|
654
|
+
const processTrack = async (track) => {
|
|
655
|
+
try {
|
|
656
|
+
switch (track.kind) {
|
|
657
|
+
case 'audioinput':
|
|
658
|
+
return await processAudioTrack(track);
|
|
659
|
+
case 'videoinput':
|
|
660
|
+
return await processVideoTrack(track);
|
|
661
|
+
default:
|
|
662
|
+
assert(false, `Unexpected track kind: ${track.kind}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
if (error instanceof Error) {
|
|
667
|
+
onProcessingError?.(error, track);
|
|
668
|
+
}
|
|
669
|
+
return track;
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
return await Promise.all(newTracks.map(track => processTrack(track)));
|
|
673
|
+
};
|
|
674
|
+
};
|