@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/dist/utils.js CHANGED
@@ -1,41 +1,165 @@
1
- import { applyConstraints, createTrackDevicesChanges, extractConstraintsWithKeys, findDeviceFromConstraints, muteStreamTrack, relaxInputConstraint, stopMediaStream, isAudioInput, isVideoInput, } from '@pexip/media-control';
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 { UserMediaStatus } from './types';
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 (!constraints.audio && constraints.video) {
9
- return video;
10
- }
11
- if (!constraints.video && constraints.audio) {
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 createMediaProcess = (process) => async (mediaP) => {
17
- const media = await mediaP;
18
- return process(media) ?? media;
19
- };
20
- export const createMediaPipeline = (init) => {
21
- const getPipeline = () => (typeof init === 'function' ? init() : init);
22
- return {
23
- pipe: (process) => {
24
- getPipeline().push(process);
25
- },
26
- execute: async (m) => {
27
- const [first, ...processes] = getPipeline();
28
- if (first) {
29
- const piped = processes.reduce((prev, next) => next(prev), first(m));
30
- return piped;
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
- const media = m instanceof Promise ? (await m) : m;
33
- if (isMedia(media)) {
34
- return Promise.resolve(media);
83
+ trackInit.previousMediaTrack?.mute(toMute);
84
+ if (trackInit.track) {
85
+ trackInit.track.enabled = !toMute;
35
86
  }
36
- throw new Error('Expect a media input or a processor');
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
- * Memorized Expected Input
55
- */
56
- export const createMemorizedGetExpectedInput = () => {
57
- const props = {
58
- cachedExpectedInputs: new Map(),
59
- };
60
- return (constraints, getInfo) => {
61
- if (props.cachedExpectedInputs.has(constraints)) {
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 tracks - The tracks can be got from `MediaStream['getAudioStats']` or
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 isMuted = (tracks) => {
96
- if (!tracks?.length) {
204
+ export const isTrackMuted = (track) => {
205
+ if (!track || track.readyState === 'ended') {
97
206
  return undefined;
98
207
  }
99
- return !tracks.some(track => !track.muted && track.enabled);
208
+ return track.muted || !track.enabled;
100
209
  };
101
- export const buildMedia = (getMedia, onSetStatus) => {
210
+ export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevicesChanged) => {
102
211
  const props = {
103
- status: getMedia().status ?? UserMediaStatus.Initial,
104
- devices: getMedia().devices ?? [],
105
- constraints: getMedia().constraints,
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 getExpectedAudioInput = createMemorizedGetExpectedInput();
108
- const getExpectedVideoInput = createMemorizedGetExpectedInput();
109
- const muteTrack = (kind) => (muted) => {
110
- const { muteAudio, muteVideo, stream } = getMedia();
111
- const mute = kind === 'audio' ? muteAudio : muteVideo;
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 muteStreamTrack(stream)(muted, kind);
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
- const release = () => {
118
- const { release, stream } = getMedia();
119
- if (release) {
120
- return release();
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 constraints() {
129
- return props.constraints;
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 getMedia().stream;
304
+ return props.stream;
139
305
  },
140
306
  get expectedAudioInput() {
141
- const media = getMedia();
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
- const media = getMedia();
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 getMedia().audioInput;
313
+ return props.audioTrack?.input;
160
314
  },
161
315
  get videoInput() {
162
- return getMedia().videoInput;
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 = status;
169
- onSetStatus?.(status);
170
- },
171
- set constraints(value) {
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 isMuted(getMedia().stream?.getAudioTracks());
328
+ return props.audioTrack?.muted;
176
329
  },
177
330
  get videoMuted() {
178
- return isMuted(getMedia().stream?.getVideoTracks());
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
- const { stream, applyConstraints: prevApplyConstraints } = getMedia();
184
- if (prevApplyConstraints) {
185
- return await prevApplyConstraints(constraints);
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
- return await applyConstraints(stream?.getTracks(), constraints);
188
- },
189
- release,
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 (getSettings) {
199
- return getSettings();
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
- audio: stream
203
- .getAudioTracks()
204
- .map(track => track.getSettings()),
205
- video: stream
206
- .getVideoTracks()
207
- .map(track => track.getSettings()),
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 toJSON = (media) => {
318
- return {
319
- constraints: media.constraints,
320
- devices: media.devices,
321
- stream: media.stream,
322
- rawStream: media.rawStream,
323
- audioInput: media.audioInput,
324
- videoInput: media.videoInput,
325
- expectedAudioInput: media.expectedAudioInput,
326
- expectedVideoInput: media.expectedVideoInput,
327
- status: media.status,
328
- audioMuted: media.audioMuted,
329
- videoMuted: media.videoMuted,
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 wrapToJSON = (media) => {
333
- media.toJSON = () => toJSON(media);
334
- return media;
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
+ };