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