@pexip/media 20.1.0 → 20.3.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.
@@ -1,6 +1,6 @@
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';
@@ -46,20 +46,22 @@ const createAudioVideoProcessingSettingsChangeDetector = (getCurrentMedia, getPr
46
46
  return changed;
47
47
  };
48
48
  };
49
- export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMedia, updateMainStream, onEnded, mediaSignal, queueOptions = {
49
+ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMedia, updateMainStream, onEnded, mainMediaSignal, queueOptions = {
50
50
  size: DEFAULT_QUEUE_SIZE,
51
51
  throttleInMS: DEFAULT_QUEUE_THROTTLE_MS,
52
52
  delayInMS: DEFAULT_QUEUE_DELAY_MS,
53
53
  dropLast: DEFAULT_QUEUE_DROP_LAST,
54
- }, audioProcessors, videoProcessors, }) => {
54
+ }, audioProcessors, videoProcessors, getDefaultConstraints = () => ({}), signals, }) => {
55
55
  const queue = createAsyncQueue(queueOptions);
56
56
  const eventHandlers = {};
57
57
  const internalProps = {
58
58
  media: undefined,
59
- updatingPreview: false,
59
+ updatingPreviewAudio: false,
60
+ updatingPreviewVideo: false,
60
61
  updatingMain: false,
61
62
  discardMedia: false,
62
63
  initialized: false,
64
+ signals,
63
65
  };
64
66
  const logger = createModuleLogger({
65
67
  module: 'PreviewStreamController',
@@ -102,7 +104,8 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
102
104
  return true;
103
105
  }
104
106
  case 'updatingMain':
105
- case 'updatingPreview': {
107
+ case 'updatingPreviewVideo':
108
+ case 'updatingPreviewAudio': {
106
109
  if (typeof value !== 'boolean') {
107
110
  return false;
108
111
  }
@@ -116,10 +119,74 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
116
119
  }
117
120
  },
118
121
  });
122
+ const subscriptions = [
123
+ props.signals.onMediaTrackMuted?.add(track => {
124
+ if (track.kind === 'audioinput') {
125
+ eventHandlers.audioMuted?.(track.muted);
126
+ }
127
+ else {
128
+ eventHandlers.videoMuted?.(track.muted);
129
+ }
130
+ }),
131
+ props.signals.onMediaTrackSuspended?.add(track => {
132
+ if (track.kind === 'audioinput') {
133
+ eventHandlers.audioSuspended?.(track.suspended);
134
+ }
135
+ else {
136
+ eventHandlers.videoSuspended?.(track.suspended);
137
+ }
138
+ }),
139
+ props.signals.onMediaTrackResumed?.add(track => {
140
+ if (track.kind === 'audioinput') {
141
+ eventHandlers.audioSuspended?.(track.suspended);
142
+ }
143
+ else {
144
+ eventHandlers.videoSuspended?.(track.suspended);
145
+ }
146
+ }),
147
+ props.signals.onAddTrack?.add(track => {
148
+ switch (track.kind) {
149
+ case 'audio': {
150
+ eventHandlers.audioMuted?.(isTrackMuted(track) || isTrackEnded(track));
151
+ break;
152
+ }
153
+ case 'video': {
154
+ eventHandlers.videoMuted?.(isTrackMuted(track) || isTrackEnded(track));
155
+ break;
156
+ }
157
+ }
158
+ }),
159
+ props.signals.onRemoveTrack?.add(track => {
160
+ switch (track.kind) {
161
+ case 'audio': {
162
+ eventHandlers.audioMuted?.(isTrackMuted(track) || isTrackEnded(track));
163
+ break;
164
+ }
165
+ case 'video': {
166
+ eventHandlers.videoMuted?.(isTrackMuted(track) || isTrackEnded(track));
167
+ break;
168
+ }
169
+ }
170
+ }),
171
+ ];
119
172
  const getUserMedia = createGetUserMediaProcess({
120
173
  getUserMedia: requestUserMediaWithRetry(() => Promise.resolve(getCurrentDevices())),
174
+ signals: props.signals,
121
175
  getCurrentDevices: () => Promise.resolve(getCurrentDevices()),
176
+ stopVideoTrackAsMute: () => false,
177
+ updateMedia: constraints => updateMedia(constraints),
122
178
  });
179
+ const mergeMediaConstraints = (constraints) => {
180
+ const { audio, video } = getDefaultConstraints();
181
+ return {
182
+ audio: audio === false
183
+ ? false
184
+ : mergeConstraints(audio)(constraints.audio),
185
+ video: video === false
186
+ ? false
187
+ : mergeConstraints(video)(constraints.video),
188
+ };
189
+ };
123
190
  const processMedia = createMediaProcessor({
124
191
  audioProcessors,
125
192
  videoProcessors,
@@ -127,7 +194,7 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
127
194
  logger.error({ error, track }, 'Failed to process media track');
128
195
  },
129
196
  });
130
- const processAndUpdateMedia = async (media, tracks) => {
197
+ const processAndUpdateMedia = async (media, tracks, sync = false) => {
131
198
  try {
132
199
  const processedTracks = await processMedia(tracks);
133
200
  for (const [idx, track] of processedTracks.entries()) {
@@ -135,6 +202,11 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
135
202
  assert(originTrack, 'Processed track should always has the original track in the same order');
136
203
  // Only replace track when they are not the same
137
204
  if (originTrack.id !== track.id) {
205
+ if (sync) {
206
+ if (originTrack.track && track.track) {
207
+ track.track.enabled = originTrack.track.enabled;
208
+ }
209
+ }
138
210
  media.removeTrack(originTrack);
139
211
  media.addTrack(track);
140
212
  }
@@ -146,24 +218,29 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
146
218
  };
147
219
  const updateMediaProcess = createMediaUpdater({
148
220
  getUserMedia,
221
+ signals: props.signals,
222
+ getDefaultConstraints,
149
223
  getCurrentDevices: () => Promise.resolve(getCurrentDevices()),
150
224
  shouldDiscardMedia: () => props.discardMedia,
151
225
  onMediaTracksChanged: (media, tracks) => {
152
226
  props.media = media;
153
227
  queue.enqueue(async () => {
154
- await processAndUpdateMedia(media, tracks);
228
+ await processAndUpdateMedia(media, tracks, false);
155
229
  });
156
230
  },
157
231
  });
232
+ const updateMedia = async (constraints) => {
233
+ await updateMediaProcess(mergeMediaConstraints(constraints), props.media);
234
+ };
158
235
  const initFromMain = (mainMedia) => {
159
236
  if (!mainMedia?.stream) {
160
237
  return;
161
238
  }
162
239
  try {
163
- const clonedMedia = mainMedia.clone();
240
+ const clonedMedia = mainMedia.clone(props.signals, 'Preview');
164
241
  props.media = clonedMedia;
165
242
  queue.enqueue(async () => {
166
- await processAndUpdateMedia(clonedMedia, clonedMedia.getTracks());
243
+ await processAndUpdateMedia(clonedMedia, clonedMedia.getTracks(), true);
167
244
  });
168
245
  props.audioInput = mainMedia.audioInput;
169
246
  props.videoInput = mainMedia.videoInput;
@@ -184,7 +261,7 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
184
261
  initFromMain(mainMedia);
185
262
  }
186
263
  else {
187
- eventHandlers.unsubscribeMain = mediaSignal.add(initFromMain);
264
+ eventHandlers.unsubscribeMain = mainMediaSignal.add(initFromMain);
188
265
  }
189
266
  const replaceMainStream = async (constraints) => {
190
267
  logger.debug({ constraints }, 'Replacing main stream');
@@ -194,7 +271,7 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
194
271
  };
195
272
  const updatePreviewMedia = async (constraints) => {
196
273
  logger.debug({ constraints }, 'Requesting a new preview stream');
197
- await updateMediaProcess(constraints, props.media);
274
+ await updateMedia(constraints);
198
275
  logger.debug({ media: props.media }, 'Preview media updated');
199
276
  };
200
277
  const releaseAudio = async () => {
@@ -211,7 +288,7 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
211
288
  const request = { audio: { device: { exact: input } } };
212
289
  try {
213
290
  props.audioInput = input;
214
- props.updatingPreview = true;
291
+ props.updatingPreviewAudio = true;
215
292
  if (props.audioInput === undefined) {
216
293
  return await releaseAudio();
217
294
  }
@@ -248,13 +325,13 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
248
325
  }
249
326
  }
250
327
  finally {
251
- props.updatingPreview = false;
328
+ props.updatingPreviewAudio = false;
252
329
  }
253
330
  };
254
331
  const updateVideoInput = async (input) => {
255
332
  try {
256
333
  props.videoInput = input;
257
- props.updatingPreview = true;
334
+ props.updatingPreviewVideo = true;
258
335
  if (input === undefined) {
259
336
  return await releaseVideo();
260
337
  }
@@ -268,13 +345,17 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
268
345
  throw error;
269
346
  }
270
347
  finally {
271
- props.updatingPreview = false;
348
+ props.updatingPreviewVideo = false;
272
349
  }
273
350
  };
274
351
  const cleanup = async () => {
275
352
  logger.debug('Cleanup preview controller');
276
353
  await props.media?.release();
277
354
  cleanUpMainSubscription();
355
+ for (const unsub of subscriptions) {
356
+ unsub?.();
357
+ }
358
+ subscriptions.length = 0;
278
359
  props.audioInput = undefined;
279
360
  props.videoInput = undefined;
280
361
  onEnded?.();
@@ -379,12 +460,25 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
379
460
  get videoInput() {
380
461
  return props.videoInput;
381
462
  },
382
- get updatingPreview() {
383
- return props.updatingPreview;
463
+ get updatingPreviewAudio() {
464
+ return props.updatingPreviewAudio;
465
+ },
466
+ get updatingPreviewVideo() {
467
+ return props.updatingPreviewVideo;
384
468
  },
385
469
  get updatingMain() {
386
470
  return props.updatingMain;
387
471
  },
472
+ updatePreviewInput: input => {
473
+ switch (input?.kind) {
474
+ case 'audioinput': {
475
+ return updateAudioInput(input);
476
+ }
477
+ case 'videoinput': {
478
+ return updateVideoInput(input);
479
+ }
480
+ }
481
+ },
388
482
  updateAudioInput: updateInput(input => props.initialized && hasChangedInput(props.audioInput, input), updateAudioInput),
389
483
  updateVideoInput: updateInput(input => props.initialized && hasChangedInput(props.videoInput, input), updateVideoInput),
390
484
  onMediaChanged: toEvenHandler('media'),
@@ -394,8 +488,13 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
394
488
  onVideoInputError: toEvenHandler('videoInputError'),
395
489
  onApplyChangesError: toEvenHandler('applyChangesError'),
396
490
  onRevertChangesError: toEvenHandler('revertChangesError'),
397
- onUpdatingPreview: toEvenHandler('updatingPreview'),
491
+ onUpdatingPreviewAudio: toEvenHandler('updatingPreviewAudio'),
492
+ onUpdatingPreviewVideo: toEvenHandler('updatingPreviewVideo'),
398
493
  onUpdatingMain: toEvenHandler('updatingMain'),
494
+ onAudioMuted: toEvenHandler('audioMuted'),
495
+ onVideoMuted: toEvenHandler('videoMuted'),
496
+ onAudioSuspended: toEvenHandler('audioSuspended'),
497
+ onVideoSuspended: toEvenHandler('videoSuspended'),
399
498
  applyChanges,
400
499
  revertChanges,
401
500
  cleanup,
package/dist/types.d.ts CHANGED
@@ -127,8 +127,9 @@ export interface Media extends MediaAttributes {
127
127
  getAudioTracks(): MediaTrack[];
128
128
  getVideoTracks(): MediaTrack[];
129
129
  addTrack(track: MediaTrack): void;
130
+ isCurrentTrack(track: MediaTrack): boolean;
130
131
  removeTrack(track: MediaTrack): void;
131
- clone(signals?: MediaSignals): Media;
132
+ clone(signals?: MediaSignals, label?: string): Media;
132
133
  }
133
134
  export interface MediaInit {
134
135
  constraints: MediaDeviceRequest;
@@ -149,25 +150,35 @@ export interface MediaTrack {
149
150
  * `track` is available otherwise it uses the `id` from the construction or the
150
151
  * `kind`. @see MediaTrackInit
151
152
  */
152
- id: string;
153
- kind: 'audioinput' | 'videoinput';
154
- label?: string;
155
- previousMediaTrack?: MediaTrack;
153
+ readonly id: string;
154
+ /**
155
+ * It maps to MediaStreamTrack['readyState']
156
+ * When `true` the track's `readyState` is `ended`, `false` means `live` otherwise the track is not available
157
+ */
158
+ readonly stopped?: boolean;
159
+ /**
160
+ * Indicate that the media has been suspended by the User Agent, and it could happen at any time.
161
+ * `undefined` means the track is not available
162
+ */
163
+ readonly suspended?: boolean;
164
+ readonly kind: 'audioinput' | 'videoinput';
165
+ readonly label?: string;
166
+ readonly previousMediaTrack?: MediaTrack;
156
167
  /**
157
168
  * The MediaStreamTrack object representing the track
158
169
  * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack
159
170
  */
160
- track?: MediaStreamTrack;
171
+ readonly track?: MediaStreamTrack;
161
172
  /**
162
173
  * The `MediaDeviceInfo` object representing the device that the track is
163
174
  * connected to. @see https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo
164
175
  */
165
- input?: MediaDeviceInfoLike;
176
+ readonly input?: MediaDeviceInfoLike;
166
177
  /**
167
178
  * The `MediaDeviceInfo` object representing the device that the track is
168
179
  * expected to be connected to. @see https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo
169
180
  */
170
- expectedInput?: MediaDeviceInfoLike;
181
+ readonly expectedInput?: MediaDeviceInfoLike;
171
182
  /**
172
183
  * The current mute state of the track. `undefined` means the track is not available
173
184
  */
@@ -175,22 +186,38 @@ export interface MediaTrack {
175
186
  /**
176
187
  * The source track that it is originated from
177
188
  */
178
- source: MediaTrack;
179
- mute(toMute: boolean): void;
189
+ readonly source: MediaTrack;
190
+ /**
191
+ * Mute the track
192
+ *
193
+ * @param toMute - `true` to mute the track, `false` to unmute
194
+ * @param soft - `true` to indicate a soft mute i.e. setting the track to NOT `enabled`
195
+ * `false` or `undefined` for default mute
196
+ */
197
+ mute(toMute: boolean, soft?: boolean): void;
180
198
  getSettings(): ExtendedMediaTrackSettings;
181
199
  getConstraints(): InputDeviceConstraint;
182
200
  applyConstraints(constraints: InputDeviceConstraint): Promise<void>;
183
201
  release(): Promise<void>;
184
- clone(signals?: MediaSignals): MediaTrack;
202
+ clone(signals?: MediaSignals, label?: string): MediaTrack;
185
203
  toJSON(): unknown;
186
204
  }
187
- export interface MediaTrackInit extends Partial<MediaTrack> {
205
+ export interface MediaTrackInit extends Partial<Omit<MediaTrack, 'mute'>> {
188
206
  kind: 'audioinput' | 'videoinput';
189
207
  input: MediaDeviceInfoLike | undefined;
190
208
  expectedInput?: MediaDeviceInfoLike | undefined;
191
209
  constraints: InputDeviceConstraint;
192
210
  overrideMute?: boolean;
193
211
  signals?: MediaSignals;
212
+ /**
213
+ * Overriding Mute the track
214
+ *
215
+ * @param toMute - `true` to mute the track, `false` to unmute
216
+ * @param self - The media track itself
217
+ * @param soft - `true` to indicate a soft mute i.e. setting the track to NOT `enabled`
218
+ * `false` or `undefined` for default mute
219
+ */
220
+ mute?: (toMute: boolean, self: MediaTrack, soft?: boolean) => void;
194
221
  }
195
222
  export type Process<T> = (a: T) => Promise<MediaTrack>;
196
223
  export type TrackProcessor = Process<MediaTrack>;
@@ -444,6 +471,7 @@ export interface MediaOptions {
444
471
  */
445
472
  audioProcessors: TrackProcessor[];
446
473
  videoProcessors: TrackProcessor[];
474
+ stopVideoTrackAsMute?: () => boolean;
447
475
  /**
448
476
  * A function to get the devices' mute state
449
477
  */
@@ -469,10 +497,12 @@ export interface MediaProps {
469
497
  * When should we discard the requested MediaStream
470
498
  */
471
499
  discardMedia: boolean;
472
- updatingMedia: boolean;
500
+ updatingAudio: boolean;
501
+ updatingVideo: boolean;
473
502
  }
474
503
  export interface MediaController {
475
- readonly updatingMedia: boolean;
504
+ readonly updatingAudio: boolean;
505
+ readonly updatingVideo: boolean;
476
506
  /**
477
507
  * Current Media
478
508
  */
@@ -516,51 +546,24 @@ export type VideoRenderParams = Omit<RendererOptions, 'effects'> & {
516
546
  tilt?: boolean;
517
547
  zoom?: boolean;
518
548
  };
519
- export interface StreamTrackSignals {
520
- /**
521
- * MediaStreamTrack events: mute
522
- * https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack#events
523
- */
524
- onStreamTrackMuted: Signal<MediaStreamTrack>;
525
- /**
526
- * MediaStreamTrack events: unmute
527
- * https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack#events
528
- */
529
- onStreamTrackUnmuted: Signal<MediaStreamTrack>;
530
- /**
531
- * MediaStreamTrack events: ended
532
- * https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack#events
533
- */
534
- onStreamTrackEnded: Signal<MediaStreamTrack>;
535
- /**
536
- * Emit `MediaStreamTrack` whenever a call to `Media['muteAudio']` or
537
- * `Media['muteVideo']`
538
- */
539
- onStreamTrackEnabled: Signal<MediaStreamTrack>;
540
- }
541
- /**
542
- * Track state signals that used for the final track that the media pipeline
543
- * produced
544
- */
545
- export type StreamTrackSignalFinals = {
546
- [key in keyof StreamTrackSignals as `${key}Final`]: Signal<MediaStreamTrack>;
547
- };
548
549
  export interface MediaChangesSignals {
549
550
  onAddTrack: Signal<MediaStreamTrack>;
550
- onAudioMuteStateChanged: Signal<boolean | undefined>;
551
- onTrackReleased: Signal<MediaStreamTrack>;
552
551
  onDevicesChanged: Signal<IndexedDevices>;
553
552
  onMediaChanged: Signal<Media | undefined>;
553
+ onMediaTrackMuted: Signal<MediaTrack>;
554
+ onMediaTrackResumed: Signal<MediaTrack>;
555
+ onMediaTrackStopped: Signal<MediaTrack>;
556
+ onMediaTrackSuspended: Signal<MediaTrack>;
554
557
  onRemoveTrack: Signal<MediaStreamTrack>;
555
558
  onStatusChanged: Signal<UserMediaStatus>;
556
- onVideoMuteStateChanged: Signal<boolean | undefined>;
557
- onUpdatingMedia: Signal<boolean>;
559
+ onUpdatingAudio: Signal<boolean>;
560
+ onUpdatingVideo: Signal<boolean>;
558
561
  }
559
562
  export interface AudioDetectionSignals {
560
563
  onVAD: Signal<undefined>;
561
564
  onSilentDetected: Signal<boolean>;
562
565
  }
563
- export type MediaSignalsOptional = Omit<Partial<MediaChangesSignals>, 'onMediaChanged'> & Partial<StreamTrackSignals> & Partial<StreamTrackSignalFinals>;
566
+ export type MediaSignalsOptional = Omit<Partial<MediaChangesSignals>, 'onMediaChanged'>;
564
567
  export type MediaSignalsRequired = Pick<MediaChangesSignals, 'onMediaChanged'> & AudioDetectionSignals;
565
568
  export type MediaSignals = MediaSignalsRequired & MediaSignalsOptional;
566
569
  export declare enum DeniedDevices {
@@ -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, 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,16 +556,50 @@ export const createGetUserMediaProcess = ({ getUserMedia, getCurrentDevices, sig
553
556
  let audioMediaTrack;
554
557
  let videoMediaTrack;
555
558
  const extractContentHint = extractConstraintsWithKeys(['contentHint']);
559
+ let trackReleaseTimeoutID = 0;
560
+ /**
561
+ * Release the track as mute
562
+ */
563
+ const releaseTrackAsMute = async (track, toMute, soft) => {
564
+ if (!track?.track || track.muted === toMute) {
565
+ return;
566
+ }
567
+ track.track.enabled = !toMute;
568
+ if (soft) {
569
+ return;
570
+ }
571
+ if (toMute) {
572
+ if (!track.stopped) {
573
+ // Track cannot be stopped immediately as we need to ensure
574
+ // the last frame is black to avoid frozen frame in a gateway call
575
+ trackReleaseTimeoutID = window.setTimeout(() => {
576
+ track.release();
577
+ trackReleaseTimeoutID = 0;
578
+ }, 500);
579
+ }
580
+ }
581
+ else {
582
+ if (trackReleaseTimeoutID) {
583
+ window.clearTimeout(trackReleaseTimeoutID);
584
+ trackReleaseTimeoutID = 0;
585
+ }
586
+ if (track.stopped) {
587
+ await updateMedia({
588
+ [track.kind === 'audioinput' ? 'audio' : 'video']: track.getConstraints(),
589
+ });
590
+ }
591
+ }
592
+ };
556
593
  if (constraints.audio !== undefined) {
557
594
  const audioTrack = stream?.getAudioTracks().at(0);
558
595
  const audioInput = findInput(audioTrack);
559
- assert(Boolean(audioTrack) === Boolean(audioInput), 'audioTrack <=> audioInput');
596
+ assert(Boolean(audioTrack) === Boolean(audioInput), 'Inconsistent audioTrack and audioInput');
560
597
  const { contentHint: [[contentHint] = []], } = extractContentHint(constraints.audio);
561
598
  if (audioTrack) {
562
599
  audioTrack.contentHint = contentHint ?? '';
563
600
  }
564
601
  audioMediaTrack = createMediaTrack({
565
- label: PROCESSOR_LABELS.GetUserMedia,
602
+ label: [scope, PROCESSOR_LABELS.GetUserMedia].join('|'),
566
603
  kind: 'audioinput',
567
604
  track: audioTrack,
568
605
  input: audioInput,
@@ -575,17 +612,26 @@ export const createGetUserMediaProcess = ({ getUserMedia, getCurrentDevices, sig
575
612
  if (constraints.video !== undefined) {
576
613
  const videoTrack = stream?.getVideoTracks().at(0);
577
614
  const videoInput = findInput(videoTrack);
578
- assert(Boolean(videoTrack) === Boolean(videoInput), 'videoTrack <=> videoInput');
615
+ assert(Boolean(videoTrack) === Boolean(videoInput), 'Inconsistent videoTrack and videoInput');
579
616
  const { contentHint: [[contentHint] = []], } = extractContentHint(constraints.video);
580
617
  if (videoTrack) {
581
618
  videoTrack.contentHint = contentHint ?? '';
582
619
  }
583
620
  videoMediaTrack = createMediaTrack({
584
- label: PROCESSOR_LABELS.GetUserMedia,
621
+ label: [scope, PROCESSOR_LABELS.GetUserMedia].join('|'),
585
622
  kind: 'videoinput',
586
623
  track: videoTrack,
587
624
  input: videoInput,
588
625
  expectedInput: findExpectedInput(devices, constraints.video, videoInput, 'videoinput'),
626
+ get overrideMute() {
627
+ return stopVideoTrackAsMute();
628
+ },
629
+ get muted() {
630
+ return !videoTrack?.enabled;
631
+ },
632
+ mute(toMute, self, soft) {
633
+ releaseTrackAsMute(self, toMute, soft);
634
+ },
589
635
  constraints: constraints.video,
590
636
  signals,
591
637
  });