@pexip/media 20.1.0 → 20.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 CHANGED
@@ -1,5 +1,41 @@
1
1
  # @pexip/media
2
2
 
3
+ ## 20.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 574cf03: Adjusts blur passes based on processing height:
8
+
9
+ - `getBlurKernelSize` can now take `lowestProcessingHeight` so that the blur
10
+ passes are adjusted compared to the lowest processing height used by the
11
+ consumer. This fixes an issue with different blur amount being applied to
12
+ frames of different resolutions.
13
+ - background blur amount is updated every time processing dimensions are
14
+ updated.
15
+
16
+ - be12a15: Add `getDefaultConstraints` and rename `mediaSignals` to
17
+ `mainMediaSignal`
18
+
19
+ - Add `getDefaultConstraints` to be consistent to the main one
20
+ - Rename `mediaSignals` to `mainMediaSignal`
21
+ - Fix `Media['clone']` to avoid interfering the original pipeline
22
+
23
+ - b4c3bad: Stop camera as Mute
24
+
25
+ - Decouple updatingMedia to updatingAudio and updatingVideo
26
+ - Stop camera when mute, and re-request the same camera when unmute
27
+
28
+ - 752005c: Release the track when providing false constraints independently
29
+
30
+ ### Patch Changes
31
+
32
+ - Updated dependencies [f5e5b51]
33
+ - Updated dependencies [6fddc11]
34
+ - @pexip/media-processor@20.2.0
35
+ - @pexip/signal@16.9.0
36
+ - @pexip/media-control@20.2.0
37
+ - @pexip/utils@17.0.0
38
+
3
39
  ## 20.1.0
4
40
 
5
41
  ### Minor Changes
package/dist/media.d.ts CHANGED
@@ -19,4 +19,4 @@ export declare const createMediaUpdater: ({ getUserMedia, getCurrentDevices, sho
19
19
  *
20
20
  * @param options - @see MediaOptions
21
21
  */
22
- export declare const createMedia: ({ getMuteState, signals, audioProcessors, videoProcessors, getDefaultConstraints, }: MediaOptions) => MediaController;
22
+ export declare const createMedia: ({ getMuteState, signals, audioProcessors, videoProcessors, stopVideoTrackAsMute, getDefaultConstraints, }: MediaOptions) => MediaController;
package/dist/media.js CHANGED
@@ -15,27 +15,35 @@ export const createMediaUpdater = ({ getUserMedia, getCurrentDevices, shouldDisc
15
15
  return async (constraints, currentMedia) => {
16
16
  const currentDevices = await getCurrentDevices();
17
17
  const permission = await getInputDevicePermission();
18
- const requestNothing = !constraints.audio && !constraints.video;
19
- const permissionRejected = permission.audio === 'denied' && permission.video === 'denied';
20
- if (requestNothing || permissionRejected) {
18
+ const releaseAudio = constraints.audio === false || permission.audio === 'denied';
19
+ const releaseVideo = constraints.video === false || permission.video === 'denied';
20
+ if (releaseAudio || releaseVideo) {
21
+ // Release related
21
22
  for (const track of currentMedia?.getTracks() ?? []) {
22
- await track.release();
23
- currentMedia?.removeTrack(track);
23
+ if ((track.kind === 'audioinput' && releaseAudio) ||
24
+ (track.kind === 'videoinput' && releaseVideo)) {
25
+ await track.release();
26
+ currentMedia?.removeTrack(track);
27
+ }
28
+ }
29
+ // When both are true, there is nothing to request
30
+ if (releaseAudio && releaseVideo) {
31
+ currentMedia?.setOriginalConstraints(constraints);
32
+ const media = currentMedia ??
33
+ buildMedia({
34
+ constraints,
35
+ permission,
36
+ devices: currentDevices,
37
+ status: permission.audio === 'denied' &&
38
+ permission.video === 'denied'
39
+ ? UserMediaStatus.PermissionsRejected
40
+ : UserMediaStatus.PermissionsGranted,
41
+ stream: undefined,
42
+ signals,
43
+ tracks: [],
44
+ });
45
+ return onMediaTracksChanged(media, []);
24
46
  }
25
- currentMedia?.setOriginalConstraints(constraints);
26
- const media = currentMedia ??
27
- buildMedia({
28
- constraints,
29
- permission,
30
- devices: currentDevices,
31
- status: permissionRejected
32
- ? UserMediaStatus.PermissionsRejected
33
- : UserMediaStatus.PermissionsGranted,
34
- stream: undefined,
35
- signals,
36
- tracks: [],
37
- });
38
- return onMediaTracksChanged(media, []);
39
47
  }
40
48
  const { audio: prevAudioSettings, video: prevVideoSettings } = currentMedia?.getSettings() ?? {};
41
49
  const videoFeatures = getVideoFeatures(constraints.video, {});
@@ -206,9 +214,20 @@ const createMediaPropsHandler = (signals) => ({
206
214
  }
207
215
  return true;
208
216
  }
209
- case 'updatingMedia': {
217
+ case 'updatingAudio': {
218
+ if (target[p] === value) {
219
+ return true;
220
+ }
210
221
  const result = Reflect.set(target, p, value);
211
- signals?.onUpdatingMedia?.emit(value);
222
+ signals?.onUpdatingAudio?.emit(value);
223
+ return result;
224
+ }
225
+ case 'updatingVideo': {
226
+ if (target[p] === value) {
227
+ return true;
228
+ }
229
+ const result = Reflect.set(target, p, value);
230
+ signals?.onUpdatingVideo?.emit(value);
212
231
  return result;
213
232
  }
214
233
  default: {
@@ -223,11 +242,12 @@ const createMediaPropsHandler = (signals) => ({
223
242
  *
224
243
  * @param options - @see MediaOptions
225
244
  */
226
- export const createMedia = ({ getMuteState, signals, audioProcessors, videoProcessors, getDefaultConstraints = () => ({}), }) => {
245
+ export const createMedia = ({ getMuteState, signals, audioProcessors, videoProcessors, stopVideoTrackAsMute = () => true, getDefaultConstraints = () => ({}), }) => {
227
246
  const _props = {
228
247
  devices: createIndexedDevices([]),
229
248
  discardMedia: false,
230
- updatingMedia: false,
249
+ updatingAudio: false,
250
+ updatingVideo: false,
231
251
  };
232
252
  const props = new Proxy(_props, createMediaPropsHandler(signals));
233
253
  const queue = createAsyncQueue({
@@ -304,9 +324,11 @@ export const createMedia = ({ getMuteState, signals, audioProcessors, videoProce
304
324
  },
305
325
  });
306
326
  const getUserMediaProcess = createGetUserMediaProcess({
307
- getUserMedia: requestUserMediaWithRetry(getCurrentDevices),
308
327
  getCurrentDevices,
328
+ getUserMedia: requestUserMediaWithRetry(getCurrentDevices),
309
329
  signals,
330
+ stopVideoTrackAsMute,
331
+ updateMedia: constraints => updateMedia(constraints),
310
332
  });
311
333
  const processAndUpdateMedia = async (media, tracks) => {
312
334
  const processedTracks = await processMedia(tracks);
@@ -359,12 +381,24 @@ export const createMedia = ({ getMuteState, signals, audioProcessors, videoProce
359
381
  * @param constraints - @see MediaDeviceRequest
360
382
  */
361
383
  const updateMedia = async (constraints) => {
384
+ const updateAudio = constraints.audio !== undefined;
385
+ const updateVideo = constraints.video !== undefined;
362
386
  try {
363
- props.updatingMedia = true;
387
+ if (updateAudio) {
388
+ props.updatingAudio = true;
389
+ }
390
+ if (updateVideo) {
391
+ props.updatingVideo = true;
392
+ }
364
393
  await updateMediaProcess(constraints, props.media);
365
394
  }
366
395
  finally {
367
- props.updatingMedia = false;
396
+ if (updateAudio) {
397
+ props.updatingAudio = false;
398
+ }
399
+ if (updateVideo) {
400
+ props.updatingVideo = false;
401
+ }
368
402
  }
369
403
  };
370
404
  const mergeMediaConstraints = (constraints) => {
@@ -424,8 +458,11 @@ export const createMedia = ({ getMuteState, signals, audioProcessors, videoProce
424
458
  navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange);
425
459
  }
426
460
  return {
427
- get updatingMedia() {
428
- return props.updatingMedia;
461
+ get updatingAudio() {
462
+ return props.updatingAudio;
463
+ },
464
+ get updatingVideo() {
465
+ return props.updatingVideo;
429
466
  },
430
467
  get media() {
431
468
  return props.media;
@@ -1,4 +1,4 @@
1
- import type { IndexedDevices, MediaDeviceInfoLike, MediaDeviceRequest } from '@pexip/media-control';
1
+ import type { IndexedDevices, InputConstraintSet, MediaDeviceInfoLike, MediaDeviceRequest } from '@pexip/media-control';
2
2
  import type { AsyncQueueOptions } from '@pexip/utils';
3
3
  import type { Media, MediaSignals, TrackProcessor, Unsubscribe } from './types';
4
4
  type EventCallback<T> = (event: T) => void;
@@ -12,7 +12,10 @@ export interface PreviewEventHandler {
12
12
  audioInputError?: EventErrorCallback;
13
13
  applyChangesError?: EventErrorCallback;
14
14
  revertChangesError?: EventErrorCallback;
15
- updatingPreview?: EventCallback<boolean>;
15
+ updatingPreviewAudio?: EventCallback<boolean>;
16
+ updatingPreviewVideo?: EventCallback<boolean>;
17
+ audioMuted?: EventCallback<boolean | undefined>;
18
+ videoMuted?: EventCallback<boolean | undefined>;
16
19
  updatingMain?: EventCallback<boolean>;
17
20
  unsubscribeMain?: Unsubscribe;
18
21
  }
@@ -20,22 +23,31 @@ export interface PreviewStreamParams {
20
23
  getCurrentDevices: () => IndexedDevices;
21
24
  getCurrentMedia: () => Media | undefined;
22
25
  updateMainStream: (request: MediaDeviceRequest) => Promise<void>;
23
- mediaSignal: MediaSignals['onMediaChanged'];
26
+ mainMediaSignal: MediaSignals['onMediaChanged'];
24
27
  onEnded?: () => void;
25
28
  fftSize?: number;
26
29
  queueOptions?: Partial<AsyncQueueOptions>;
27
30
  audioProcessors: TrackProcessor[];
28
31
  videoProcessors: TrackProcessor[];
32
+ /**
33
+ * Pass default constraints to use with get media wrappers
34
+ */
35
+ getDefaultConstraints?: () => {
36
+ audio?: InputConstraintSet | false;
37
+ video?: InputConstraintSet | false;
38
+ };
29
39
  }
30
40
  export interface PreviewControllerProps {
31
41
  media: Media | undefined;
32
42
  audioInput?: MediaDeviceInfoLike;
33
43
  videoInput?: MediaDeviceInfoLike;
34
- updatingPreview: boolean;
44
+ updatingPreviewAudio: boolean;
45
+ updatingPreviewVideo: boolean;
35
46
  updatingMain: boolean;
36
47
  originalMainAudioInput?: MediaDeviceInfoLike;
37
48
  discardMedia: boolean;
38
49
  initialized: boolean;
50
+ signals: MediaSignals;
39
51
  }
40
52
  export interface PreviewStreamController {
41
53
  media: Media | undefined;
@@ -44,8 +56,10 @@ export interface PreviewStreamController {
44
56
  inputChanged: boolean;
45
57
  audioInput: PreviewInput;
46
58
  videoInput: PreviewInput;
47
- updatingPreview: boolean;
48
59
  updatingMain: boolean;
60
+ updatingPreviewAudio: boolean;
61
+ updatingPreviewVideo: boolean;
62
+ updatePreviewInput(input: PreviewInput): void;
49
63
  updateAudioInput(id: string): void;
50
64
  updateVideoInput(id: string): void;
51
65
  applyChanges(force?: boolean): Promise<void>;
@@ -54,13 +68,16 @@ export interface PreviewStreamController {
54
68
  onMediaChanged(callback: EventCallback<Media>): Unsubscribe;
55
69
  onAudioInputChanged(callback: EventCallback<PreviewInput>): Unsubscribe;
56
70
  onVideoInputChanged(callback: EventCallback<PreviewInput>): Unsubscribe;
57
- onUpdatingPreview(callback: EventCallback<boolean>): Unsubscribe;
71
+ onUpdatingPreviewAudio(callback: EventCallback<boolean>): Unsubscribe;
72
+ onUpdatingPreviewVideo(callback: EventCallback<boolean>): Unsubscribe;
58
73
  onUpdatingMain(callback: EventCallback<boolean>): Unsubscribe;
59
74
  onAudioInputError(callback: EventErrorCallback): Unsubscribe;
60
75
  onVideoInputError(callback: EventErrorCallback): Unsubscribe;
61
76
  onApplyChangesError(callback: EventErrorCallback): Unsubscribe;
62
77
  onRevertChangesError(callback: EventErrorCallback): Unsubscribe;
78
+ onAudioMuted(callback: EventCallback<boolean | undefined>): Unsubscribe;
79
+ onVideoMuted(callback: EventCallback<boolean | undefined>): Unsubscribe;
63
80
  }
64
- export declare const createPreviewStreamController: ({ getCurrentDevices, getCurrentMedia, updateMainStream, onEnded, mediaSignal, queueOptions, audioProcessors, videoProcessors, }: PreviewStreamParams) => PreviewStreamController;
81
+ export declare const createPreviewStreamController: ({ getCurrentDevices, getCurrentMedia, updateMainStream, onEnded, mainMediaSignal, queueOptions, audioProcessors, videoProcessors, getDefaultConstraints, }: PreviewStreamParams) => PreviewStreamController;
65
82
  export type CreatePreviewStreamController = typeof createPreviewStreamController;
66
83
  export {};
@@ -1,9 +1,10 @@
1
- import { hasChangedInput, isMediaDeviceInfo, reverseDeviceId, } from '@pexip/media-control';
1
+ import { hasChangedInput, isMediaDeviceInfo, mergeConstraints, reverseDeviceId, } from '@pexip/media-control';
2
2
  import { createAsyncQueue, assert } from '@pexip/utils';
3
- import { AUDIO_SETTINGS_KEYS, VIDEO_SETTINGS_KEYS, createMediaProcessor, hasSettingsChanged, } from './utils';
3
+ import { AUDIO_SETTINGS_KEYS, VIDEO_SETTINGS_KEYS, createMediaProcessor, hasSettingsChanged, isTrackEnded, isTrackMuted, } from './utils';
4
4
  import { isMedia } from './typeGuard';
5
5
  import { createModuleLogger } from './logger';
6
6
  import { createGetUserMediaProcess, requestUserMediaWithRetry, } from './userMedia';
7
+ import { createMediaSignals } from './signals';
7
8
  import { createMediaUpdater } from './media';
8
9
  const DEFAULT_QUEUE_DELAY_MS = 100;
9
10
  const DEFAULT_QUEUE_DROP_LAST = false;
@@ -46,20 +47,30 @@ const createAudioVideoProcessingSettingsChangeDetector = (getCurrentMedia, getPr
46
47
  return changed;
47
48
  };
48
49
  };
49
- export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMedia, updateMainStream, onEnded, mediaSignal, queueOptions = {
50
+ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMedia, updateMainStream, onEnded, mainMediaSignal, queueOptions = {
50
51
  size: DEFAULT_QUEUE_SIZE,
51
52
  throttleInMS: DEFAULT_QUEUE_THROTTLE_MS,
52
53
  delayInMS: DEFAULT_QUEUE_DELAY_MS,
53
54
  dropLast: DEFAULT_QUEUE_DROP_LAST,
54
- }, audioProcessors, videoProcessors, }) => {
55
+ }, audioProcessors, videoProcessors, getDefaultConstraints = () => ({}), }) => {
55
56
  const queue = createAsyncQueue(queueOptions);
56
57
  const eventHandlers = {};
57
58
  const internalProps = {
58
59
  media: undefined,
59
- updatingPreview: false,
60
+ updatingPreviewAudio: false,
61
+ updatingPreviewVideo: false,
60
62
  updatingMain: false,
61
63
  discardMedia: false,
62
64
  initialized: false,
65
+ signals: createMediaSignals([
66
+ 'onAddTrack',
67
+ 'onAudioMuteStateChanged',
68
+ 'onRemoveTrack',
69
+ 'onStatusChanged',
70
+ 'onUpdatingAudio',
71
+ 'onUpdatingVideo',
72
+ 'onVideoMuteStateChanged',
73
+ ], 'PreviewStreamController'),
63
74
  };
64
75
  const logger = createModuleLogger({
65
76
  module: 'PreviewStreamController',
@@ -102,7 +113,8 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
102
113
  return true;
103
114
  }
104
115
  case 'updatingMain':
105
- case 'updatingPreview': {
116
+ case 'updatingPreviewVideo':
117
+ case 'updatingPreviewAudio': {
106
118
  if (typeof value !== 'boolean') {
107
119
  return false;
108
120
  }
@@ -116,10 +128,56 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
116
128
  }
117
129
  },
118
130
  });
131
+ const subscriptions = [
132
+ props.signals.onAudioMuteStateChanged?.add(muted => {
133
+ eventHandlers.audioMuted?.(muted);
134
+ }),
135
+ props.signals.onVideoMuteStateChanged?.add(muted => {
136
+ eventHandlers.videoMuted?.(muted);
137
+ }),
138
+ props.signals.onAddTrack?.add(track => {
139
+ switch (track.kind) {
140
+ case 'audio': {
141
+ eventHandlers.audioMuted?.(isTrackMuted(track) || isTrackEnded(track));
142
+ break;
143
+ }
144
+ case 'video': {
145
+ eventHandlers.videoMuted?.(isTrackMuted(track) || isTrackEnded(track));
146
+ break;
147
+ }
148
+ }
149
+ }),
150
+ props.signals.onRemoveTrack?.add(track => {
151
+ switch (track.kind) {
152
+ case 'audio': {
153
+ eventHandlers.audioMuted?.(isTrackMuted(track) || isTrackEnded(track));
154
+ break;
155
+ }
156
+ case 'video': {
157
+ eventHandlers.videoMuted?.(isTrackMuted(track) || isTrackEnded(track));
158
+ break;
159
+ }
160
+ }
161
+ }),
162
+ ];
119
163
  const getUserMedia = createGetUserMediaProcess({
120
164
  getUserMedia: requestUserMediaWithRetry(() => Promise.resolve(getCurrentDevices())),
165
+ signals: props.signals,
121
166
  getCurrentDevices: () => Promise.resolve(getCurrentDevices()),
167
+ stopVideoTrackAsMute: () => false,
168
+ updateMedia: constraints => updateMedia(constraints),
122
169
  });
170
+ const mergeMediaConstraints = (constraints) => {
171
+ const { audio, video } = getDefaultConstraints();
172
+ return {
173
+ audio: audio === false
174
+ ? false
175
+ : mergeConstraints(audio)(constraints.audio),
176
+ video: video === false
177
+ ? false
178
+ : mergeConstraints(video)(constraints.video),
179
+ };
180
+ };
123
181
  const processMedia = createMediaProcessor({
124
182
  audioProcessors,
125
183
  videoProcessors,
@@ -127,7 +185,7 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
127
185
  logger.error({ error, track }, 'Failed to process media track');
128
186
  },
129
187
  });
130
- const processAndUpdateMedia = async (media, tracks) => {
188
+ const processAndUpdateMedia = async (media, tracks, sync = false) => {
131
189
  try {
132
190
  const processedTracks = await processMedia(tracks);
133
191
  for (const [idx, track] of processedTracks.entries()) {
@@ -135,6 +193,11 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
135
193
  assert(originTrack, 'Processed track should always has the original track in the same order');
136
194
  // Only replace track when they are not the same
137
195
  if (originTrack.id !== track.id) {
196
+ if (sync) {
197
+ if (originTrack.track && track.track) {
198
+ track.track.enabled = originTrack.track.enabled;
199
+ }
200
+ }
138
201
  media.removeTrack(originTrack);
139
202
  media.addTrack(track);
140
203
  }
@@ -146,24 +209,29 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
146
209
  };
147
210
  const updateMediaProcess = createMediaUpdater({
148
211
  getUserMedia,
212
+ signals: props.signals,
213
+ getDefaultConstraints,
149
214
  getCurrentDevices: () => Promise.resolve(getCurrentDevices()),
150
215
  shouldDiscardMedia: () => props.discardMedia,
151
216
  onMediaTracksChanged: (media, tracks) => {
152
217
  props.media = media;
153
218
  queue.enqueue(async () => {
154
- await processAndUpdateMedia(media, tracks);
219
+ await processAndUpdateMedia(media, tracks, false);
155
220
  });
156
221
  },
157
222
  });
223
+ const updateMedia = async (constraints) => {
224
+ await updateMediaProcess(mergeMediaConstraints(constraints), props.media);
225
+ };
158
226
  const initFromMain = (mainMedia) => {
159
227
  if (!mainMedia?.stream) {
160
228
  return;
161
229
  }
162
230
  try {
163
- const clonedMedia = mainMedia.clone();
231
+ const clonedMedia = mainMedia.clone(props.signals);
164
232
  props.media = clonedMedia;
165
233
  queue.enqueue(async () => {
166
- await processAndUpdateMedia(clonedMedia, clonedMedia.getTracks());
234
+ await processAndUpdateMedia(clonedMedia, clonedMedia.getTracks(), true);
167
235
  });
168
236
  props.audioInput = mainMedia.audioInput;
169
237
  props.videoInput = mainMedia.videoInput;
@@ -184,7 +252,7 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
184
252
  initFromMain(mainMedia);
185
253
  }
186
254
  else {
187
- eventHandlers.unsubscribeMain = mediaSignal.add(initFromMain);
255
+ eventHandlers.unsubscribeMain = mainMediaSignal.add(initFromMain);
188
256
  }
189
257
  const replaceMainStream = async (constraints) => {
190
258
  logger.debug({ constraints }, 'Replacing main stream');
@@ -194,7 +262,7 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
194
262
  };
195
263
  const updatePreviewMedia = async (constraints) => {
196
264
  logger.debug({ constraints }, 'Requesting a new preview stream');
197
- await updateMediaProcess(constraints, props.media);
265
+ await updateMedia(constraints);
198
266
  logger.debug({ media: props.media }, 'Preview media updated');
199
267
  };
200
268
  const releaseAudio = async () => {
@@ -211,7 +279,7 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
211
279
  const request = { audio: { device: { exact: input } } };
212
280
  try {
213
281
  props.audioInput = input;
214
- props.updatingPreview = true;
282
+ props.updatingPreviewAudio = true;
215
283
  if (props.audioInput === undefined) {
216
284
  return await releaseAudio();
217
285
  }
@@ -248,13 +316,13 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
248
316
  }
249
317
  }
250
318
  finally {
251
- props.updatingPreview = false;
319
+ props.updatingPreviewAudio = false;
252
320
  }
253
321
  };
254
322
  const updateVideoInput = async (input) => {
255
323
  try {
256
324
  props.videoInput = input;
257
- props.updatingPreview = true;
325
+ props.updatingPreviewVideo = true;
258
326
  if (input === undefined) {
259
327
  return await releaseVideo();
260
328
  }
@@ -268,13 +336,17 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
268
336
  throw error;
269
337
  }
270
338
  finally {
271
- props.updatingPreview = false;
339
+ props.updatingPreviewVideo = false;
272
340
  }
273
341
  };
274
342
  const cleanup = async () => {
275
343
  logger.debug('Cleanup preview controller');
276
344
  await props.media?.release();
277
345
  cleanUpMainSubscription();
346
+ for (const unsub of subscriptions) {
347
+ unsub?.();
348
+ }
349
+ subscriptions.length = 0;
278
350
  props.audioInput = undefined;
279
351
  props.videoInput = undefined;
280
352
  onEnded?.();
@@ -379,12 +451,25 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
379
451
  get videoInput() {
380
452
  return props.videoInput;
381
453
  },
382
- get updatingPreview() {
383
- return props.updatingPreview;
454
+ get updatingPreviewAudio() {
455
+ return props.updatingPreviewAudio;
456
+ },
457
+ get updatingPreviewVideo() {
458
+ return props.updatingPreviewVideo;
384
459
  },
385
460
  get updatingMain() {
386
461
  return props.updatingMain;
387
462
  },
463
+ updatePreviewInput: input => {
464
+ switch (input?.kind) {
465
+ case 'audioinput': {
466
+ return updateAudioInput(input);
467
+ }
468
+ case 'videoinput': {
469
+ return updateVideoInput(input);
470
+ }
471
+ }
472
+ },
388
473
  updateAudioInput: updateInput(input => props.initialized && hasChangedInput(props.audioInput, input), updateAudioInput),
389
474
  updateVideoInput: updateInput(input => props.initialized && hasChangedInput(props.videoInput, input), updateVideoInput),
390
475
  onMediaChanged: toEvenHandler('media'),
@@ -394,8 +479,11 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
394
479
  onVideoInputError: toEvenHandler('videoInputError'),
395
480
  onApplyChangesError: toEvenHandler('applyChangesError'),
396
481
  onRevertChangesError: toEvenHandler('revertChangesError'),
397
- onUpdatingPreview: toEvenHandler('updatingPreview'),
482
+ onUpdatingPreviewAudio: toEvenHandler('updatingPreviewAudio'),
483
+ onUpdatingPreviewVideo: toEvenHandler('updatingPreviewVideo'),
398
484
  onUpdatingMain: toEvenHandler('updatingMain'),
485
+ onAudioMuted: toEvenHandler('audioMuted'),
486
+ onVideoMuted: toEvenHandler('videoMuted'),
399
487
  applyChanges,
400
488
  revertChanges,
401
489
  cleanup,
package/dist/types.d.ts CHANGED
@@ -150,6 +150,7 @@ export interface MediaTrack {
150
150
  * `kind`. @see MediaTrackInit
151
151
  */
152
152
  id: string;
153
+ stopped: boolean;
153
154
  kind: 'audioinput' | 'videoinput';
154
155
  label?: string;
155
156
  previousMediaTrack?: MediaTrack;
@@ -184,13 +185,14 @@ export interface MediaTrack {
184
185
  clone(signals?: MediaSignals): MediaTrack;
185
186
  toJSON(): unknown;
186
187
  }
187
- export interface MediaTrackInit extends Partial<MediaTrack> {
188
+ export interface MediaTrackInit extends Partial<Omit<MediaTrack, 'mute'>> {
188
189
  kind: 'audioinput' | 'videoinput';
189
190
  input: MediaDeviceInfoLike | undefined;
190
191
  expectedInput?: MediaDeviceInfoLike | undefined;
191
192
  constraints: InputDeviceConstraint;
192
193
  overrideMute?: boolean;
193
194
  signals?: MediaSignals;
195
+ mute?: (toMute: boolean, self: MediaTrack) => void;
194
196
  }
195
197
  export type Process<T> = (a: T) => Promise<MediaTrack>;
196
198
  export type TrackProcessor = Process<MediaTrack>;
@@ -444,6 +446,7 @@ export interface MediaOptions {
444
446
  */
445
447
  audioProcessors: TrackProcessor[];
446
448
  videoProcessors: TrackProcessor[];
449
+ stopVideoTrackAsMute?: () => boolean;
447
450
  /**
448
451
  * A function to get the devices' mute state
449
452
  */
@@ -469,10 +472,12 @@ export interface MediaProps {
469
472
  * When should we discard the requested MediaStream
470
473
  */
471
474
  discardMedia: boolean;
472
- updatingMedia: boolean;
475
+ updatingAudio: boolean;
476
+ updatingVideo: boolean;
473
477
  }
474
478
  export interface MediaController {
475
- readonly updatingMedia: boolean;
479
+ readonly updatingAudio: boolean;
480
+ readonly updatingVideo: boolean;
476
481
  /**
477
482
  * Current Media
478
483
  */
@@ -554,7 +559,8 @@ export interface MediaChangesSignals {
554
559
  onRemoveTrack: Signal<MediaStreamTrack>;
555
560
  onStatusChanged: Signal<UserMediaStatus>;
556
561
  onVideoMuteStateChanged: Signal<boolean | undefined>;
557
- onUpdatingMedia: Signal<boolean>;
562
+ onUpdatingAudio: Signal<boolean>;
563
+ onUpdatingVideo: Signal<boolean>;
558
564
  }
559
565
  export interface AudioDetectionSignals {
560
566
  onVAD: Signal<undefined>;
@@ -12,7 +12,10 @@ export declare const toSameDeviceStatus: ({ audio, video, }: {
12
12
  video: boolean;
13
13
  }) => UserMediaStatus.PermissionsGranted | UserMediaStatus.PermissionsGrantedFallback | UserMediaStatus.PermissionsGrantedFallbackAudioinput | UserMediaStatus.PermissionsGrantedFallbackVideoinput;
14
14
  export declare const toOnlyDeviceStatus: (kind: "audioinput" | "videoinput", matched: boolean, devices: IndexedDevices) => UserMediaStatus.PermissionsRejectedAudioInput | UserMediaStatus.PermissionsRejectedVideoInput | UserMediaStatus.PermissionsOnlyAudioinput | UserMediaStatus.PermissionsOnlyAudioinputNoVideoDevices | UserMediaStatus.PermissionsOnlyAudioinputFallback | UserMediaStatus.PermissionsOnlyAudioinputFallbackNoVideoDevices | UserMediaStatus.PermissionsOnlyVideoinput | UserMediaStatus.PermissionsOnlyVideoinputNoAudioDevices | UserMediaStatus.PermissionsOnlyVideoinputFallback | UserMediaStatus.PermissionsOnlyVideoinputFallbackNoAudioDevices;
15
- export declare const hasLiveTrack: (kind: "audio" | "video", tracks: MediaStreamTrack[]) => boolean;
15
+ /**
16
+ * Check if there is any track match with the `kind`
17
+ */
18
+ export declare const hasTrack: (kind: "audio" | "video", tracks: MediaStreamTrack[]) => boolean;
16
19
  /**
17
20
  * Merge the previous status with the next status and assuming requesting both
18
21
  * video and audio inputs.
@@ -35,13 +38,15 @@ export declare const mergeNoDeviceStatus: (constraints: MediaDeviceRequest, anyD
35
38
  export declare const deriveUserMediaStatus: (devices: IndexedDevices, constraints: MediaDeviceRequest, prevStatus: UserMediaStatus) => UserMediaStatus;
36
39
  export declare const requestUserMediaWithRetry: (getCurrentDevices: GetCurrentDevices, createRequestUserMedia?: (getCurrentDevices: GetCurrentDevices, getMedia?: ({ audio, video, }: MediaDeviceRequest) => Promise<MediaStream>) => GetUserMedia, gUM?: ({ audio, video, }: MediaDeviceRequest) => Promise<MediaStream>) => GetUserMedia;
37
40
  interface Options {
38
- getUserMedia: GetUserMedia;
39
41
  getCurrentDevices: GetCurrentDevices;
40
- signals?: MediaSignals;
42
+ getUserMedia: GetUserMedia;
41
43
  scope?: string;
44
+ signals?: MediaSignals;
45
+ stopVideoTrackAsMute: () => boolean;
46
+ updateMedia: (constraints: MediaDeviceRequest) => Promise<void>;
42
47
  }
43
48
  /**
44
49
  * A process to get user media
45
50
  */
46
- export declare const createGetUserMediaProcess: ({ getUserMedia, getCurrentDevices, signals, scope, }: Options) => GetUserMediaProcess;
51
+ export declare const createGetUserMediaProcess: ({ getCurrentDevices, getUserMedia, scope, signals, stopVideoTrackAsMute, updateMedia, }: Options) => GetUserMediaProcess;
47
52
  export {};
package/dist/userMedia.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { MediaDeviceFailure, extractConstraintsWithKeys, findMediaInputFromMediaStreamTrack, getUserMedia, isExactDeviceConstraint, isStreamingRequestedDevices, relaxInputConstraint, } from '@pexip/media-control';
2
2
  import { assert } from '@pexip/utils';
3
3
  import { UserMediaStatus } from './types';
4
- import { makeDeriveDeviceStatus, buildMedia, createMediaTrack, findExpectedInput, } from './utils';
4
+ import { buildMedia, createMediaTrack, findExpectedInput, isTrackMuted, makeDeriveDeviceStatus, } from './utils';
5
5
  import { logger } from './logger';
6
6
  import { isUnknownError } from './status';
7
7
  import { PROCESSOR_LABELS } from './constants';
@@ -103,22 +103,25 @@ export const toOnlyDeviceStatus = (kind, matched, devices) => {
103
103
  ? UserMediaStatus.PermissionsOnlyAudioinputFallbackNoVideoDevices
104
104
  : UserMediaStatus.PermissionsOnlyVideoinputFallbackNoAudioDevices;
105
105
  };
106
- export const hasLiveTrack = (kind, tracks) => tracks.some(track => track.kind === kind && track.readyState === 'live');
106
+ /**
107
+ * Check if there is any track match with the `kind`
108
+ */
109
+ export const hasTrack = (kind, tracks) => tracks.some(track => track.kind === kind);
107
110
  /**
108
111
  * Merge the previous status with the next status and assuming requesting both
109
112
  * video and audio inputs.
110
113
  */
111
114
  export const mergeStatus = (prevStatus, nextStatus, prevTracks = [], nextTracks = []) => {
112
- const hasPrevLiveAudio = hasLiveTrack('audio', prevTracks);
113
- const hasPrevLiveVideo = hasLiveTrack('video', prevTracks);
114
- const hasNextLiveAudio = hasLiveTrack('audio', nextTracks);
115
- const hasNextLiveVideo = hasLiveTrack('video', nextTracks);
115
+ const prevValidAudio = hasTrack('audio', prevTracks);
116
+ const preValidVideo = hasTrack('video', prevTracks);
117
+ const nextValidAudio = hasTrack('audio', nextTracks);
118
+ const nextValidVideo = hasTrack('video', nextTracks);
116
119
  // Replace Audio Track
117
- if (hasNextLiveAudio) {
118
- assert(hasPrevLiveAudio === false, 'Only 1 live audio');
120
+ if (nextValidAudio) {
121
+ assert(prevValidAudio === false, 'Only 1 valid audio');
119
122
  // Replace Audio and Video tracks
120
- if (hasNextLiveVideo) {
121
- assert(hasPrevLiveVideo === false, 'Only 1 live video');
123
+ if (nextValidVideo) {
124
+ assert(preValidVideo === false, 'Only 1 valid video');
122
125
  // No need to consider the previous status
123
126
  switch (nextStatus) {
124
127
  case UserMediaStatus.PermissionsOnlyVideoinput:
@@ -132,7 +135,7 @@ export const mergeStatus = (prevStatus, nextStatus, prevTracks = [], nextTracks
132
135
  return nextStatus;
133
136
  }
134
137
  }
135
- if (hasPrevLiveVideo) {
138
+ if (preValidVideo) {
136
139
  // Merge previous video status with next audio status
137
140
  switch (prevStatus) {
138
141
  case UserMediaStatus.PermissionsGranted:
@@ -215,9 +218,9 @@ export const mergeStatus = (prevStatus, nextStatus, prevTracks = [], nextTracks
215
218
  }
216
219
  }
217
220
  // Replace Video Track Only
218
- if (hasNextLiveVideo) {
219
- assert(hasPrevLiveVideo === false, 'Only 1 live video');
220
- if (hasPrevLiveAudio) {
221
+ if (nextValidVideo) {
222
+ assert(preValidVideo === false, 'Only 1 valid video');
223
+ if (prevValidAudio) {
221
224
  switch (prevStatus) {
222
225
  case UserMediaStatus.PermissionsGranted:
223
226
  case UserMediaStatus.PermissionsGrantedFallbackVideoinput:
@@ -254,7 +257,7 @@ export const mergeStatus = (prevStatus, nextStatus, prevTracks = [], nextTracks
254
257
  }
255
258
  }
256
259
  // Failed to get a new track
257
- if (hasPrevLiveAudio) {
260
+ if (prevValidAudio) {
258
261
  switch (nextStatus) {
259
262
  case UserMediaStatus.PermissionsRejected:
260
263
  case UserMediaStatus.PermissionsRejectedVideoInput: {
@@ -300,7 +303,7 @@ export const mergeStatus = (prevStatus, nextStatus, prevTracks = [], nextTracks
300
303
  return prevStatus;
301
304
  }
302
305
  }
303
- if (hasPrevLiveVideo) {
306
+ if (preValidVideo) {
304
307
  switch (nextStatus) {
305
308
  case UserMediaStatus.PermissionsRejected:
306
309
  case UserMediaStatus.PermissionsRejectedAudioInput:
@@ -466,7 +469,7 @@ export const requestUserMediaWithRetry = (getCurrentDevices, createRequestUserMe
466
469
  /**
467
470
  * A process to get user media
468
471
  */
469
- export const createGetUserMediaProcess = ({ getUserMedia, getCurrentDevices, signals, scope = 'media', }) => {
472
+ export const createGetUserMediaProcess = ({ getCurrentDevices, getUserMedia, scope = 'media', signals, stopVideoTrackAsMute, updateMedia, }) => {
470
473
  return async ({ constraints, permission, originalConstraints, currentMedia, }) => {
471
474
  // Release the current track(s)
472
475
  if (currentMedia) {
@@ -553,10 +556,31 @@ export const createGetUserMediaProcess = ({ getUserMedia, getCurrentDevices, sig
553
556
  let audioMediaTrack;
554
557
  let videoMediaTrack;
555
558
  const extractContentHint = extractConstraintsWithKeys(['contentHint']);
559
+ /**
560
+ * Release the track as mute
561
+ */
562
+ const releaseTrackAsMute = async (track, toMute) => {
563
+ if (!track?.track || track.muted === toMute) {
564
+ return;
565
+ }
566
+ track.track.enabled = !toMute;
567
+ if (toMute) {
568
+ if (!track.stopped) {
569
+ await track.release();
570
+ }
571
+ }
572
+ else {
573
+ if (track.stopped) {
574
+ await updateMedia({
575
+ [track.kind === 'audioinput' ? 'audio' : 'video']: track.getConstraints(),
576
+ });
577
+ }
578
+ }
579
+ };
556
580
  if (constraints.audio !== undefined) {
557
581
  const audioTrack = stream?.getAudioTracks().at(0);
558
582
  const audioInput = findInput(audioTrack);
559
- assert(Boolean(audioTrack) === Boolean(audioInput), 'audioTrack <=> audioInput');
583
+ assert(Boolean(audioTrack) === Boolean(audioInput), 'Inconsistent audioTrack and audioInput');
560
584
  const { contentHint: [[contentHint] = []], } = extractContentHint(constraints.audio);
561
585
  if (audioTrack) {
562
586
  audioTrack.contentHint = contentHint ?? '';
@@ -575,7 +599,7 @@ export const createGetUserMediaProcess = ({ getUserMedia, getCurrentDevices, sig
575
599
  if (constraints.video !== undefined) {
576
600
  const videoTrack = stream?.getVideoTracks().at(0);
577
601
  const videoInput = findInput(videoTrack);
578
- assert(Boolean(videoTrack) === Boolean(videoInput), 'videoTrack <=> videoInput');
602
+ assert(Boolean(videoTrack) === Boolean(videoInput), 'Inconsistent videoTrack and videoInput');
579
603
  const { contentHint: [[contentHint] = []], } = extractContentHint(constraints.video);
580
604
  if (videoTrack) {
581
605
  videoTrack.contentHint = contentHint ?? '';
@@ -586,6 +610,13 @@ export const createGetUserMediaProcess = ({ getUserMedia, getCurrentDevices, sig
586
610
  track: videoTrack,
587
611
  input: videoInput,
588
612
  expectedInput: findExpectedInput(devices, constraints.video, videoInput, 'videoinput'),
613
+ overrideMute: stopVideoTrackAsMute(),
614
+ get muted() {
615
+ return isTrackMuted(videoTrack);
616
+ },
617
+ mute(toMute, self) {
618
+ releaseTrackAsMute(self, toMute);
619
+ },
589
620
  constraints: constraints.video,
590
621
  signals,
591
622
  });
package/dist/utils.d.ts CHANGED
@@ -28,6 +28,7 @@ export declare const findExpectedInput: (devices: IndexedDevices, constraints: I
28
28
  * there is no track to check
29
29
  */
30
30
  export declare const isTrackMuted: (track: MediaStreamTrack | undefined) => boolean | undefined;
31
+ export declare const isTrackEnded: (track: MediaStreamTrack) => boolean;
31
32
  export declare const buildMedia: (mediaInit: MediaInit, onDevicesChanged?: import("@pexip/signal").Signal<IndexedDevices, IndexedDevices>) => Media;
32
33
  /**
33
34
  * Shallow copy the provided object and override with provided overriding
@@ -58,11 +59,12 @@ export declare const mergeSettings: (settingsA: ExtendedMediaTrackSettings | und
58
59
  * @param percentage - The percentage of image height to calculate the blur
59
60
  * kernel size
60
61
  * @param height - The image height
62
+ * @param lowestProcessingHeight - The lowest image height that is processed by the consumer, used to adjust the blur kernel size across different resolutions
61
63
  * @param max - The upper bound
62
64
  *
63
65
  * @returns blur kernel size
64
66
  */
65
- export declare const getBlurKernelSize: (percentage: number, height: number, max?: number) => number;
67
+ export declare const getBlurKernelSize: (percentage: number, height: number, lowestProcessingHeight?: number, max?: number) => number;
66
68
  /**
67
69
  * Apply the content hint to the track
68
70
  *
package/dist/utils.js CHANGED
@@ -19,6 +19,10 @@ export const createMediaTrack = (trackInit) => {
19
19
  (trackInit.kind === trackInit.input?.kind &&
20
20
  trackInit.kind === toMediaDeviceInputKind(trackInit.track));
21
21
  assert(trackConsistency, `Inconsistent track kind: ${trackInit.input?.kind} ${trackInit.track && toMediaDeviceInputKind(trackInit.track)} vs ${trackInit.kind}`);
22
+ assert(!trackInit.overrideMute ||
23
+ (trackInit.overrideMute &&
24
+ Object.hasOwn(trackInit, 'mute') &&
25
+ Object.hasOwn(trackInit, 'muted')), 'Inconsistent overrideMute');
22
26
  const currentConstraints = typeof trackInit.constraints === 'boolean' ? {} : trackInit.constraints;
23
27
  // Find the original source track through the linked list
24
28
  let sourceMediaTrack = trackInit.previousMediaTrack;
@@ -52,9 +56,10 @@ export const createMediaTrack = (trackInit) => {
52
56
  return trackInit.track;
53
57
  },
54
58
  get input() {
55
- return trackInit.track?.readyState !== 'live'
56
- ? undefined
57
- : trackInit.input;
59
+ if (!trackInit.track) {
60
+ return undefined;
61
+ }
62
+ return trackInit.input;
58
63
  },
59
64
  get expectedInput() {
60
65
  return trackInit.expectedInput;
@@ -69,23 +74,26 @@ export const createMediaTrack = (trackInit) => {
69
74
  if (trackInit.overrideMute) {
70
75
  return trackInit.muted;
71
76
  }
72
- return (
73
- // If the source track muted, the following processed track will
74
- // be muted as well since there is no data to process
75
- !!isTrackMuted(this.source.track) ||
76
- isTrackMuted(trackInit.track));
77
+ // The source is itself, just access the track's state
78
+ if (this.source === this) {
79
+ return isTrackMuted(trackInit.track);
80
+ }
81
+ return this.source.muted;
82
+ },
83
+ get stopped() {
84
+ return trackInit.track ? isTrackEnded(trackInit.track) : true;
77
85
  },
78
86
  get source() {
79
87
  return sourceMediaTrack ?? mediaTrack;
80
88
  },
81
89
  mute(toMute) {
82
90
  if (trackInit.overrideMute) {
83
- return trackInit.mute?.(toMute);
91
+ return trackInit.mute?.(toMute, this);
84
92
  }
85
- trackInit.previousMediaTrack?.mute(toMute);
86
93
  if (trackInit.track) {
87
94
  trackInit.track.enabled = !toMute;
88
95
  }
96
+ trackInit.previousMediaTrack?.mute(toMute);
89
97
  },
90
98
  getSettings,
91
99
  getConstraints() {
@@ -96,6 +104,9 @@ export const createMediaTrack = (trackInit) => {
96
104
  ...trackInit,
97
105
  constraints: getConstraints(),
98
106
  track: trackInit.track?.clone(),
107
+ // When Cloning a MediaTrack, overrideMute should be turned off since
108
+ // it might corrupt the logic of its original media pipeline
109
+ overrideMute: false,
99
110
  signals,
100
111
  });
101
112
  },
@@ -161,7 +172,7 @@ export const createTrackProcessingPipeline = (processors) => async (track) => {
161
172
  let lastTrack = track;
162
173
  for (const process of processors) {
163
174
  if (lastTrack.track?.readyState !== 'live') {
164
- return lastTrack;
175
+ continue;
165
176
  }
166
177
  lastTrack = await process(lastTrack);
167
178
  }
@@ -208,11 +219,12 @@ export const findExpectedInput = (devices, constraints, input, kind) => {
208
219
  * there is no track to check
209
220
  */
210
221
  export const isTrackMuted = (track) => {
211
- if (!track || track.readyState === 'ended') {
222
+ if (!track) {
212
223
  return undefined;
213
224
  }
214
225
  return track.muted || !track.enabled;
215
226
  };
227
+ export const isTrackEnded = (track) => track.readyState === 'ended';
216
228
  export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevicesChanged) => {
217
229
  const props = {
218
230
  originalConstraints: mediaInit.originalConstraints ?? mediaInit.constraints,
@@ -360,13 +372,13 @@ export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevic
360
372
  props.devices.size('videoinput') > 0));
361
373
  },
362
374
  muteAudio(mute) {
363
- if (!props.audioTrack) {
375
+ if (!props.audioTrack || props.audioTrack.muted === mute) {
364
376
  return;
365
377
  }
366
378
  muteTrack(props.audioTrack, mute);
367
379
  },
368
380
  muteVideo(mute) {
369
- if (!props.videoTrack) {
381
+ if (!props.videoTrack || props.videoTrack.muted === mute) {
370
382
  return;
371
383
  }
372
384
  muteTrack(props.videoTrack, mute);
@@ -403,20 +415,19 @@ export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevic
403
415
  return [];
404
416
  }
405
417
  const cloned = track.source.clone(signals);
406
- // Restore the enabled state for all cloned track
407
- cloned.mute(false);
408
418
  return cloned;
409
419
  });
410
420
  const stream = this.stream &&
411
421
  new MediaStream(tracks.flatMap(track => (track.track ? [track.track] : [])));
412
422
  const clonedMedia = buildMedia({
413
- tracks,
414
- devices: this.devices,
415
- status: this.status,
416
- stream,
417
423
  constraints: this.getConstraints(),
424
+ devices: this.devices,
418
425
  originalConstraints: this.getOriginalConstraints(),
419
426
  permission: mediaInit.permission,
427
+ signals,
428
+ status: this.status,
429
+ stream,
430
+ tracks,
420
431
  });
421
432
  return clonedMedia;
422
433
  },
@@ -634,15 +645,20 @@ export const mergeSettings = (settingsA, settingsB) => {
634
645
  * @param percentage - The percentage of image height to calculate the blur
635
646
  * kernel size
636
647
  * @param height - The image height
648
+ * @param lowestProcessingHeight - The lowest image height that is processed by the consumer, used to adjust the blur kernel size across different resolutions
637
649
  * @param max - The upper bound
638
650
  *
639
651
  * @returns blur kernel size
640
652
  */
641
- export const getBlurKernelSize = (percentage, height, max = calculateMaxBlurPass(height)) => {
653
+ export const getBlurKernelSize = (percentage, height, lowestProcessingHeight, max = calculateMaxBlurPass(height)) => {
642
654
  if (height <= 0 || percentage <= 0 || max <= 0) {
643
655
  return 0;
644
656
  }
645
- return Math.min(Math.ceil(percentage * 0.01 * max), max);
657
+ const minHeightMaxBlurPass = lowestProcessingHeight
658
+ ? calculateMaxBlurPass(lowestProcessingHeight)
659
+ : max;
660
+ return Math.min(Math.ceil(percentage * 0.01 * max) +
661
+ Math.max(0, max - minHeightMaxBlurPass), max);
646
662
  };
647
663
  /**
648
664
  * Apply the content hint to the track
@@ -18,12 +18,14 @@ interface VideoStreamProcessOptions extends Partial<VideoRenderParams>, Omit<Pro
18
18
  * Whether or to enable this processor
19
19
  */
20
20
  shouldEnable: () => boolean;
21
+ lowestProcessingHeight?: number;
21
22
  processingWidth: number;
22
23
  processingHeight: number;
23
24
  hasInitializedDeps?: boolean;
24
25
  width?: number;
25
26
  height?: number;
26
27
  label?: string;
28
+ stopAsMute?: () => boolean;
27
29
  dynamicProcessingDimensions?: () => boolean;
28
30
  gpuAPI?: () => RenderBackend;
29
31
  }
@@ -35,5 +37,5 @@ declare const FEATURE_KEYS: readonly ["backgroundBlurAmount", "backgroundImageUr
35
37
  type FeaturePropKeys = (typeof FEATURE_KEYS)[number];
36
38
  type FeatureProps = Pick<Partial<VideoStreamProcessProps>, FeaturePropKeys>;
37
39
  export declare const updateFeatureProps: (constraints: MediaDeviceRequest["video"], props: FeatureProps) => FeatureProps;
38
- export declare const createVideoStreamProcess: ({ trackProcessorAPI, processingWidth, processingHeight, shouldEnable, frameRate, videoSegmentation, foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, label, dynamicProcessingDimensions, gpuAPI, ...options }: VideoStreamProcessOptions) => TrackProcessor;
40
+ export declare const createVideoStreamProcess: ({ backgroundImageUrl, dynamicProcessingDimensions, edgeBlurAmount, foregroundThreshold, frameRate, gpuAPI, label, lowestProcessingHeight, maskCombineRatio, processingHeight, processingWidth, shouldEnable, stopAsMute, trackProcessorAPI, videoSegmentation, ...options }: VideoStreamProcessOptions) => TrackProcessor;
39
41
  export {};
@@ -94,7 +94,7 @@ export const updateFeatureProps = (constraints, props) => {
94
94
  }
95
95
  }, {});
96
96
  };
97
- const applyFeatures = (transformer, features) => {
97
+ const applyFeatures = (transformer, features, lowestProcessingHeight) => {
98
98
  for (const key in features) {
99
99
  const k = key;
100
100
  switch (k) {
@@ -110,7 +110,7 @@ const applyFeatures = (transformer, features) => {
110
110
  case 'backgroundBlurAmount': {
111
111
  const value = features[k];
112
112
  if (value !== undefined) {
113
- transformer[k] = getBlurKernelSize(value, transformer.height);
113
+ transformer[k] = getBlurKernelSize(value, transformer.height, lowestProcessingHeight);
114
114
  }
115
115
  break;
116
116
  }
@@ -141,19 +141,15 @@ const getTrackProcessor = (shouldUseStreamTrackProcessor, ...params) => {
141
141
  }
142
142
  return createVideoTrackProcessorWithFallback(...params);
143
143
  };
144
- export const createVideoStreamProcess = ({ trackProcessorAPI = () => 'stream', processingWidth, processingHeight, shouldEnable, frameRate,
145
- //backgroundBlurAmount,
146
- videoSegmentation,
147
- //edgeBlurAmount,
148
- foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, label = PROCESSOR_LABELS.VideoProcessor, dynamicProcessingDimensions = () => false, gpuAPI = () => 'webgl', // dynamically changing the config will not be picked up, not a priority for now
149
- ...options }) => {
144
+ export const createVideoStreamProcess = ({ backgroundImageUrl, dynamicProcessingDimensions = () => false, edgeBlurAmount, foregroundThreshold, frameRate, gpuAPI = () => 'webgl', // dynamically changing the config will not be picked up, not a priority for now
145
+ label = PROCESSOR_LABELS.VideoProcessor, lowestProcessingHeight, maskCombineRatio, processingHeight, processingWidth, shouldEnable, stopAsMute = () => false, trackProcessorAPI = () => 'stream', videoSegmentation, ...options }) => {
150
146
  const videoSegmentationModel = options.videoSegmentationModel ?? 'selfie';
151
147
  const segmenter = options.segmenters[videoSegmentationModel];
152
148
  if (!segmenter) {
153
149
  throw new Error('Segmenter is undefined');
154
150
  }
155
151
  const backgroundBlurAmount = options.backgroundBlurAmount &&
156
- getBlurKernelSize(options.backgroundBlurAmount, processingHeight);
152
+ getBlurKernelSize(options.backgroundBlurAmount, processingHeight, lowestProcessingHeight);
157
153
  const transformer = options.transformer ??
158
154
  createCanvasTransform(segmenter, {
159
155
  width: processingWidth,
@@ -199,29 +195,39 @@ foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, label
199
195
  const features = updateFeatureProps(prevMediaTrack.getConstraints(), props);
200
196
  const shouldEnabled = shouldEnable();
201
197
  if (!shouldEnabled || !prevMediaTrack.track) {
202
- logger.debug({ label, features, shouldEnabled }, 'Video processing is skipped');
198
+ logger.debug({
199
+ label,
200
+ features,
201
+ shouldEnabled,
202
+ }, 'Video processing is skipped');
203
203
  return prevMediaTrack;
204
204
  }
205
205
  if (!props.hasInitialized) {
206
206
  if (dynamicProcessingDimensions()) {
207
207
  const { width, height } = prevMediaTrack.getSettings();
208
- /**
209
- * when the processor is closed, the segmenter is not killed, but the track is released.
210
- * this means we can't listen on the track settings changes when we change quality
211
- * we need to update instead of open if the segmenter is already running
212
- */
213
- if (props.transformer.segmenter.status === 'new') {
214
- await props.videoProcessor().open({
215
- processingWidth: width,
216
- processingHeight: height,
217
- });
218
- props.transformer.backend = gpuAPI();
219
- }
220
- else {
221
- props.transformer.update({
222
- processingWidth: width,
223
- processingHeight: height,
224
- });
208
+ if (width && height) {
209
+ /**
210
+ * when the processor is closed, the segmenter is not killed, but the track is released.
211
+ * this means we can't listen on the track settings changes when we change quality
212
+ * we need to update instead of open if the segmenter is already running
213
+ */
214
+ if (props.transformer.segmenter.status === 'new') {
215
+ await props.videoProcessor().open({
216
+ processingWidth: width,
217
+ processingHeight: height,
218
+ });
219
+ props.transformer.backend = gpuAPI();
220
+ }
221
+ else {
222
+ props.transformer.update({
223
+ processingWidth: width,
224
+ processingHeight: height,
225
+ });
226
+ if (props.backgroundBlurAmount !== undefined) {
227
+ props.transformer.backgroundBlurAmount =
228
+ getBlurKernelSize(props.backgroundBlurAmount, height, lowestProcessingHeight);
229
+ }
230
+ }
225
231
  }
226
232
  }
227
233
  else {
@@ -230,7 +236,7 @@ foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, label
230
236
  }
231
237
  props.hasInitialized = true;
232
238
  }
233
- applyFeatures(props.transformer, features);
239
+ applyFeatures(props.transformer, features, lowestProcessingHeight);
234
240
  const model = features.videoSegmentationModel &&
235
241
  props.segmenters[features.videoSegmentationModel];
236
242
  if (features.videoSegmentationModel &&
@@ -257,8 +263,14 @@ foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, label
257
263
  props.transformer.effects = mute
258
264
  ? 'none'
259
265
  : props.videoSegmentation ?? 'none';
260
- prevMediaTrack.mute(mute);
261
266
  muteStreamTrack(stream)(mute, 'video');
267
+ if (mute && stopAsMute()) {
268
+ for (const track of stream.getVideoTracks()) {
269
+ track.stop();
270
+ }
271
+ release();
272
+ }
273
+ prevMediaTrack.mute(mute);
262
274
  };
263
275
  return createMediaTrack({
264
276
  label,
@@ -279,7 +291,7 @@ foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, label
279
291
  if (isEmpty(features)) {
280
292
  return;
281
293
  }
282
- applyFeatures(props.transformer, features);
294
+ applyFeatures(props.transformer, features, lowestProcessingHeight);
283
295
  if (dynamicProcessingDimensions()) {
284
296
  const acceptedSettings = prevMediaTrack.source.getSettings();
285
297
  if (acceptedSettings.width && acceptedSettings.height) {
@@ -287,6 +299,10 @@ foregroundThreshold, backgroundImageUrl, maskCombineRatio, edgeBlurAmount, label
287
299
  processingHeight: acceptedSettings.height,
288
300
  processingWidth: acceptedSettings.width,
289
301
  });
302
+ if (props.backgroundBlurAmount !== undefined) {
303
+ props.transformer.backgroundBlurAmount =
304
+ getBlurKernelSize(props.backgroundBlurAmount, acceptedSettings.height, lowestProcessingHeight);
305
+ }
290
306
  }
291
307
  }
292
308
  const model = features.videoSegmentationModel &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pexip/media",
3
- "version": "20.1.0",
3
+ "version": "20.2.0",
4
4
  "description": "Home for media related stuff",
5
5
  "homepage": "https://gitlab.com/pexip/zoo",
6
6
  "bugs": "https://gitlab.com/pexip/zoo/issues",
@@ -44,14 +44,14 @@
44
44
  "typecheck": "yarn tsc --noEmit -p ."
45
45
  },
46
46
  "dependencies": {
47
- "@pexip/media-control": "20.1.0",
48
- "@pexip/media-processor": "20.1.0",
49
- "@pexip/signal": "16.8.0",
47
+ "@pexip/media-control": "20.2.0",
48
+ "@pexip/media-processor": "20.2.0",
49
+ "@pexip/signal": "16.9.0",
50
50
  "@pexip/utils": "17.0.0"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@jest/globals": "^29.7.0",
54
- "@pexip/bundler": "18.1.0",
54
+ "@pexip/bundler": "18.1.1",
55
55
  "@swc/core": "^1.10.12",
56
56
  "@swc/jest": "^0.2.37",
57
57
  "jest": "^29.5.12",
@@ -1 +0,0 @@
1
- {"root":["../src/audioMixingProcessor.ts","../src/audioProcessor.ts","../src/baseLogger.ts","../src/constants.ts","../src/displayMedia.ts","../src/index.ts","../src/logger.ts","../src/media.ts","../src/previewController.ts","../src/signals.ts","../src/status.ts","../src/typeGuard.ts","../src/types.ts","../src/userMedia.ts","../src/utils.ts","../src/videoProcessor.ts","../../../@types/globals.d.ts"],"version":"5.7.3"}