@pexip/media 19.0.0 → 20.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/CHANGELOG.md CHANGED
@@ -1,5 +1,80 @@
1
1
  # @pexip/media
2
2
 
3
+ ## 20.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 2bfd9e1: Adds esm entry points.
8
+ - c975587: `dynamicProcessingDimensions` is a func.
9
+ - 7679b89: Removes `adjustResolution`.
10
+
11
+ ### Minor Changes
12
+
13
+ - dd5a4eb: Adds optional `gpuAPI` getter, sets the transformer backend when
14
+ opened.
15
+ - 34bf3dd: Add isVideoStreamTrackProcessorAPIs
16
+ - 5855473: Updates the segmenter processing width and height based on the track
17
+ settings if `dynamicProcessingDimensions` is `true` in `videoProcessor`.
18
+ - 3555d9d: Adds `build:sourcemap` script.
19
+ - 7d94f5c: Take unauthorized device into account
20
+ - ed53b05: Upgrade Typescript
21
+
22
+ ### Patch Changes
23
+
24
+ - 4146978: Bump logger proxy set to info level
25
+ - 0beec97: Fix mergeStatus to take requesting single device into consideration
26
+
27
+ When changing 1 device should not affect another type of device
28
+
29
+ - 67a18d0: Use recommended linter rules
30
+ - e1882e7: Request media only when there is no prompt permission
31
+ - 3882f98: No unused imports
32
+ - 50d0af0: Assert track and input
33
+ - 5adb1aa: Update according Logger changes
34
+ - 79c52c9: No signals reuse for clone
35
+ - 43d0b9c: Always use the merged constraints
36
+ - 6470495: Fix applyFeatures
37
+ - 6d68da2: Stop processing non-live track
38
+ - 58ecca1: Fix mergeStatus fallback case
39
+ - e76a15e: Migrate to biome linter
40
+ - Updated dependencies [2bfd9e1]
41
+ - Updated dependencies [4fe77de]
42
+ - Updated dependencies [b0d8af9]
43
+ - Updated dependencies [2bbff64]
44
+ - Updated dependencies [4baa8a4]
45
+ - Updated dependencies [db3a370]
46
+ - Updated dependencies [4ea0492]
47
+ - Updated dependencies [3555d9d]
48
+ - Updated dependencies [d3909ed]
49
+ - Updated dependencies [0416d6c]
50
+ - Updated dependencies [1601985]
51
+ - Updated dependencies [7d94f5c]
52
+ - Updated dependencies [3cb5aee]
53
+ - Updated dependencies [fa06b8b]
54
+ - Updated dependencies [84956b8]
55
+ - Updated dependencies [ed53b05]
56
+ - Updated dependencies [5855473]
57
+ - Updated dependencies [941363c]
58
+ - Updated dependencies [d681fd3]
59
+ - Updated dependencies [e76a15e]
60
+ - @pexip/media-processor@20.0.0
61
+ - @pexip/media-control@20.0.0
62
+ - @pexip/utils@17.0.0
63
+ - @pexip/signal@16.8.0
64
+
65
+ ## 19.1.0
66
+
67
+ ### Minor Changes
68
+
69
+ - 7271998: Fix `syncMuteState` and
70
+ `onAudioMuteStateChanged`/`onVideoMuteStateChanged`
71
+
72
+ ### Patch Changes
73
+
74
+ - Updated dependencies [e3a9d84]
75
+ - @pexip/media-processor@19.1.0
76
+ - @pexip/media-control@19.1.0
77
+
3
78
  ## 19.0.0
4
79
 
5
80
  ### Major Changes
@@ -10,7 +10,7 @@ interface AudioStreamProcessorProps {
10
10
  declare const FEATURE_KEYS: ['mixWithAdditionalMedia'];
11
11
  type FeaturePropKeys = (typeof FEATURE_KEYS)[number];
12
12
  type FeatureProps = Pick<AudioStreamProcessorProps, FeaturePropKeys>;
13
- export declare const updateFeatureProps: (constraints: MediaDeviceRequest['audio'], props: FeatureProps) => FeatureProps;
13
+ export declare const updateFeatureProps: (constraints: MediaDeviceRequest["audio"], props: FeatureProps) => FeatureProps;
14
14
  /**
15
15
  * Create a Audio Mixing Processor and will own the stream passed-in
16
16
  */
@@ -12,7 +12,8 @@ export const updateFeatureProps = (constraints, props) => {
12
12
  const [feature] = extracted[key];
13
13
  if (feature !== undefined && props[key] !== feature) {
14
14
  props[key] = feature;
15
- return { ...accm, [key]: feature };
15
+ accm[key] = feature;
16
+ return accm;
16
17
  }
17
18
  return accm;
18
19
  }, {});
@@ -155,11 +156,10 @@ export const createAudioMixingProcess = (getCurrentMedia, label = PROCESSOR_LABE
155
156
  overrideMute: true,
156
157
  mute: toMute => {
157
158
  prevMediaTrack.mute(toMute);
158
- mainSource.node?.mediaStream
159
- .getAudioTracks()
160
- .forEach(track => {
159
+ for (const track of mainSource.node?.mediaStream.getAudioTracks() ??
160
+ []) {
161
161
  track.enabled = !toMute;
162
- });
162
+ }
163
163
  },
164
164
  get muted() {
165
165
  return (
@@ -78,7 +78,7 @@ interface AudioStreamProcessorProps {
78
78
  declare const FEATURE_KEYS: ['denoise', 'vad', 'asd', 'contentHint'];
79
79
  type FeaturePropKeys = (typeof FEATURE_KEYS)[number];
80
80
  type FeatureProps = Pick<AudioStreamProcessorProps, FeaturePropKeys>;
81
- export declare const updateFeatureProps: (constraints: MediaDeviceRequest['audio'], props: FeatureProps) => FeatureProps;
81
+ export declare const updateFeatureProps: (constraints: MediaDeviceRequest["audio"], props: FeatureProps) => FeatureProps;
82
82
  /**
83
83
  * Create a Audio Stream Processor and will own the stream passed-in
84
84
  */
@@ -32,7 +32,8 @@ export const updateFeatureProps = (constraints, props) => {
32
32
  const [[feature] = []] = extracted[key];
33
33
  if (isAudioContentHint(feature) && props[key] !== feature) {
34
34
  props[key] = feature;
35
- return { ...accm, [key]: feature };
35
+ accm[key] = feature;
36
+ return accm;
36
37
  }
37
38
  return accm;
38
39
  }
@@ -42,7 +43,8 @@ export const updateFeatureProps = (constraints, props) => {
42
43
  const [feature] = extracted[key];
43
44
  if (feature !== undefined && props[key] !== feature) {
44
45
  props[key] = feature;
45
- return { ...accm, [key]: feature };
46
+ accm[key] = feature;
47
+ return accm;
46
48
  }
47
49
  return accm;
48
50
  }
@@ -9,7 +9,7 @@ export declare enum LogLevels {
9
9
  warn = 40,
10
10
  error = 50,
11
11
  fatal = 60,
12
- silent
12
+ silent = 9007199254740991
13
13
  }
14
14
  export type LogLevelsString = keyof typeof LogLevels;
15
15
  export type LogMethods = {
@@ -21,14 +21,23 @@ export type LogMethods = {
21
21
  */
22
22
  export interface Logger extends LogMethods {
23
23
  /**
24
- * Adds a value to the redaction set, which makes it replaced by [REDACTED] when logged to file.
24
+ * Add a pattern to the redactor and an optional mask for the redaction.
25
25
  *
26
- * @remarks
27
- * The redaction set is applied globally, and only applies to the log file, not console logs.
26
+ * @see {@link Redactor}
28
27
  *
29
- * @param value - the string to redact
28
+ * @param value - The pattern to add.
29
+ * @param replaceTo - The mask to use for redaction.
30
30
  */
31
- redact(value: string): void;
31
+ addRedactPattern(value: string, redactTo?: string): void;
32
+ /**
33
+ * Redact the provided string by replacing all occurrences of the patterns
34
+ * and return a new string with redacted values.
35
+ *
36
+ * @see {@link Redactor}
37
+ *
38
+ * @param text - The string to redact.
39
+ */
40
+ redact(text: string): string;
32
41
  }
33
42
  /**
34
43
  * Create a logger with console API, and map fatal to error, skipping trace
@@ -6,8 +6,7 @@ export var LogLevels;
6
6
  LogLevels[LogLevels["warn"] = 40] = "warn";
7
7
  LogLevels[LogLevels["error"] = 50] = "error";
8
8
  LogLevels[LogLevels["fatal"] = 60] = "fatal";
9
- // eslint-disable-next-line @typescript-eslint/prefer-literal-enum-member -- needs infinity
10
- LogLevels[LogLevels["silent"] = Number.MAX_SAFE_INTEGER] = "silent";
9
+ LogLevels[LogLevels["silent"] = 9007199254740991] = "silent";
11
10
  })(LogLevels || (LogLevels = {}));
12
11
  /**
13
12
  * Create a logger with console API, and map fatal to error, skipping trace
@@ -15,15 +14,22 @@ export var LogLevels;
15
14
  */
16
15
  export function createConsoleLogger() {
17
16
  return Object.freeze({
18
- /* eslint-disable no-console -- set logger to console */
19
17
  fatal: (meta, message) => console.error(message, meta),
20
18
  error: (meta, message) => console.error(message, meta),
21
19
  warn: (meta, message) => console.warn(message, meta),
22
20
  info: (meta, message) => console.info(message, meta),
23
21
  debug: (meta, message) => console.debug(message, meta),
24
- trace() { }, // Noop
25
- silent() { }, // Noop
26
- redact() { }, // Noop
27
- /* eslint-enable no-console -- set logger to console */
22
+ trace() {
23
+ // Noop
24
+ },
25
+ silent() {
26
+ // Noop
27
+ },
28
+ redact(text) {
29
+ return text;
30
+ },
31
+ addRedactPattern() {
32
+ // Noop
33
+ },
28
34
  });
29
35
  }
@@ -5,3 +5,4 @@ export declare const PROCESSOR_LABELS: Readonly<{
5
5
  AudioMixingProcessor: "AudioMixingProcessor";
6
6
  }>;
7
7
  export declare const GET_USER_MEDIA_TIMEOUT_MS = 10000;
8
+ export declare const DEVICE_CHANGE_DEBOUNCE_TIMEOUT_MS = 500;
package/dist/constants.js CHANGED
@@ -5,3 +5,4 @@ export const PROCESSOR_LABELS = Object.freeze({
5
5
  AudioMixingProcessor: 'AudioMixingProcessor',
6
6
  });
7
7
  export const GET_USER_MEDIA_TIMEOUT_MS = 10000;
8
+ export const DEVICE_CHANGE_DEBOUNCE_TIMEOUT_MS = 500;
@@ -1,3 +1,3 @@
1
1
  import type { DisplayMediaOptions } from '@pexip/media-control';
2
- export declare const createGetDisplayMedia: (getDefaultConstraints: () => DisplayMediaOptions, getDisplayMedia?: MediaDevices['getDisplayMedia']) => (constraints?: DisplayMediaOptions) => Promise<MediaStream | undefined>;
2
+ export declare const createGetDisplayMedia: (getDefaultConstraints: () => DisplayMediaOptions, getDisplayMedia?: MediaDevices["getDisplayMedia"]) => (constraints?: DisplayMediaOptions) => Promise<MediaStream | undefined>;
3
3
  export type GetDisplayMedia = ReturnType<typeof createGetDisplayMedia>;
package/dist/index.d.ts CHANGED
@@ -1,11 +1,12 @@
1
- export { createMedia } from './media';
2
- export { createAudioStreamProcess } from './audioProcessor';
3
- export { createVideoStreamProcess } from './videoProcessor';
4
- export * from './types';
5
- export { setLogger } from './logger';
1
+ export * from './displayMedia';
6
2
  export * from './previewController';
7
- export * from './status';
8
3
  export * from './signals';
4
+ export * from './status';
5
+ export * from './typeGuard';
6
+ export * from './types';
9
7
  export { applyContentHint } from './utils';
10
8
  export { createAudioMixingProcess } from './audioMixingProcessor';
11
- export * from './displayMedia';
9
+ export { createAudioStreamProcess } from './audioProcessor';
10
+ export { createMedia } from './media';
11
+ export { createVideoStreamProcess } from './videoProcessor';
12
+ export { setLogger } from './logger';
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
- export { createMedia } from './media';
2
- export { createAudioStreamProcess } from './audioProcessor';
3
- export { createVideoStreamProcess } from './videoProcessor';
4
- export * from './types';
5
- export { setLogger } from './logger';
1
+ export * from './displayMedia';
6
2
  export * from './previewController';
7
- export * from './status';
8
3
  export * from './signals';
4
+ export * from './status';
5
+ export * from './typeGuard';
6
+ export * from './types';
9
7
  export { applyContentHint } from './utils';
10
8
  export { createAudioMixingProcess } from './audioMixingProcessor';
11
- export * from './displayMedia';
9
+ export { createAudioStreamProcess } from './audioProcessor';
10
+ export { createMedia } from './media';
11
+ export { createVideoStreamProcess } from './videoProcessor';
12
+ export { setLogger } from './logger';
package/dist/logger.js CHANGED
@@ -30,11 +30,8 @@ export const createLogProxyHandler = (logger, name, scope) => {
30
30
  logger.debug({ scope, name, prop: p, value }, `called get ${name}[${String(p)}]`);
31
31
  return value;
32
32
  },
33
- // eslint-disable-next-line max-params --- from lib.es2015 and log
34
33
  set: (target, p, value, receiver) => {
35
- logger.debug(
36
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- it's a log call...
37
- { scope, name, prop: p, value }, `called set ${name}[${String(p)}]`);
34
+ logger.info({ scope, name, prop: p, value }, `called set ${name}[${String(p)}]`);
38
35
  return Reflect.set(target, p, value, receiver);
39
36
  },
40
37
  };
package/dist/media.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { IndexedDevices, MediaDeviceRequest, InputDevicePermission } from '@pexip/media-control';
1
+ import type { IndexedDevices, MediaDeviceRequest, InputDevicePermission, InputConstraintSet } from '@pexip/media-control';
2
2
  import type { GetUserMediaProcess, Media, MediaController, MediaOptions, MediaSignals, MediaTrack } from './types';
3
3
  export interface MediaUpdaterOptions {
4
4
  getUserMedia: GetUserMediaProcess;
@@ -7,8 +7,12 @@ export interface MediaUpdaterOptions {
7
7
  onMediaTracksChanged: (media: Media, tracks: MediaTrack[]) => void;
8
8
  getInputDevicePermission?: () => Promise<InputDevicePermission>;
9
9
  signals?: MediaSignals;
10
+ getDefaultConstraints?: () => {
11
+ audio?: InputConstraintSet | false;
12
+ video?: InputConstraintSet | false;
13
+ };
10
14
  }
11
- export declare const createMediaUpdater: ({ getUserMedia, getCurrentDevices, shouldDiscardMedia: shouldDisgardMedia, onMediaTracksChanged, getInputDevicePermission, signals, }: MediaUpdaterOptions) => (constraints: MediaDeviceRequest, currentMedia: Media | undefined) => Promise<void>;
15
+ export declare const createMediaUpdater: ({ getUserMedia, getCurrentDevices, shouldDiscardMedia: shouldDisgardMedia, onMediaTracksChanged, getInputDevicePermission, getDefaultConstraints, signals, }: MediaUpdaterOptions) => (constraints: MediaDeviceRequest, currentMedia: Media | undefined) => Promise<void>;
12
16
  /**
13
17
  * Create an object to interact with the media scream, which is usually used for
14
18
  * our main stream.
package/dist/media.js CHANGED
@@ -1,17 +1,17 @@
1
- import { MediaEventType, createIndexedDevices, createStreamTrackEventSubscriptions, getDevices, getInputDevicePermissionState, isIndexedDevices, isRequestedResolution, mergeConstraints, subscribe, } from '@pexip/media-control';
1
+ import { createIndexedDevices, createStreamTrackEventSubscriptions, getDevices, getInputDevicePermissionState, isIndexedDevices, isRequestedResolution, mergeConstraints, } from '@pexip/media-control';
2
2
  import { createAsyncQueue, isEmpty, assert } from '@pexip/utils';
3
3
  import { internalSignals } from './signals';
4
4
  import { UserMediaStatus } from './types';
5
5
  import { createModuleLogger, logger } from './logger';
6
6
  import { createGetUserMediaProcess, requestUserMediaWithRetry, } from './userMedia';
7
- import { AUDIO_SETTINGS_KEYS, MIXING_SETTINGS_KEYS, VIDEO_SETTINGS_KEYS, buildMedia, createMediaProcessor, diffSettings, getDevicesChanges, hasPtzFeature, mergeSettings, refineMediaConstraints, } from './utils';
8
- import { getPermissionStatus, isInitialPermissionsGranted } from './status';
7
+ import { AUDIO_SETTINGS_KEYS, MIXING_SETTINGS_KEYS, PAN_TILT_ZOOM_SETTINGS_KEYS, VIDEO_SETTINGS_KEYS, buildMedia, createMediaProcessor, diffSettings, getDevicesChanges, getSettingsFromKeys, hasPtzFeature, isApplyingRenderEffect, mergeSettings, refineMediaConstraints, } from './utils';
8
+ import { getPermissionStatus, isRequestPermissionNeeded } from './status';
9
9
  import { isMedia } from './typeGuard';
10
10
  import { updateFeatureProps as getVideoFeatures } from './videoProcessor';
11
11
  import { updateFeatureProps as getAudioFeatures } from './audioProcessor';
12
12
  import { updateFeatureProps as getMixingFeatures } from './audioMixingProcessor';
13
- import { GET_USER_MEDIA_TIMEOUT_MS } from './constants';
14
- export const createMediaUpdater = ({ getUserMedia, getCurrentDevices, shouldDiscardMedia: shouldDisgardMedia, onMediaTracksChanged, getInputDevicePermission = getInputDevicePermissionState, signals, }) => {
13
+ import { GET_USER_MEDIA_TIMEOUT_MS, DEVICE_CHANGE_DEBOUNCE_TIMEOUT_MS, } from './constants';
14
+ export const createMediaUpdater = ({ getUserMedia, getCurrentDevices, shouldDiscardMedia: shouldDisgardMedia, onMediaTracksChanged, getInputDevicePermission = getInputDevicePermissionState, getDefaultConstraints, signals, }) => {
15
15
  return async (constraints, currentMedia) => {
16
16
  const currentDevices = await getCurrentDevices();
17
17
  const permission = await getInputDevicePermission();
@@ -41,14 +41,26 @@ export const createMediaUpdater = ({ getUserMedia, getCurrentDevices, shouldDisc
41
41
  const videoFeatures = getVideoFeatures(constraints.video, {});
42
42
  const audioFeatures = getAudioFeatures(constraints.audio, {});
43
43
  const mixingFeatures = getMixingFeatures(constraints.audio, {});
44
- const hasRequestedResolution = ['blur', 'overlay'].includes(videoFeatures.videoSegmentation ?? '') || // Skip when there is a render effect to modify the video
44
+ const hasRequestedResolution = isApplyingRenderEffect(videoFeatures.videoSegmentation ?? 'none') || // Skip when there is a render effect to modify the video
45
45
  currentDevices.size('videoinput') === 0 || // Skip when no authorized video found
46
46
  isRequestedResolution(constraints.video, currentMedia?.getVideoTracks().at(0)?.getSettings());
47
- const audioFeaturesChanged = diffSettings(AUDIO_SETTINGS_KEYS)(prevAudioSettings, audioFeatures);
48
- const videoFeaturesChanged = diffSettings(VIDEO_SETTINGS_KEYS)(prevVideoSettings, videoFeatures);
49
- const mixingFeaturesChanged = diffSettings(MIXING_SETTINGS_KEYS)(prevAudioSettings, mixingFeatures);
50
- const ptzFeaturesChanges = hasPtzFeature() &&
51
- diffSettings(['pan', 'tilt', 'zoom'])(prevVideoSettings, videoFeatures);
47
+ const defaultAudioSettings = getSettingsFromKeys(AUDIO_SETTINGS_KEYS, getDefaultConstraints?.().audio);
48
+ const audioFeaturesChanged = defaultAudioSettings === false
49
+ ? undefined
50
+ : diffSettings(AUDIO_SETTINGS_KEYS)(prevAudioSettings ?? defaultAudioSettings, audioFeatures);
51
+ const defaultVideoSettings = getSettingsFromKeys(VIDEO_SETTINGS_KEYS, getDefaultConstraints?.().video);
52
+ const videoFeaturesChanged = defaultVideoSettings === false
53
+ ? undefined
54
+ : diffSettings(VIDEO_SETTINGS_KEYS)(prevVideoSettings ?? defaultVideoSettings, videoFeatures);
55
+ const defaultMixingSettings = getSettingsFromKeys(MIXING_SETTINGS_KEYS, getDefaultConstraints?.().audio);
56
+ const mixingFeaturesChanged = defaultMixingSettings === false
57
+ ? undefined
58
+ : diffSettings(MIXING_SETTINGS_KEYS)(prevAudioSettings ?? defaultMixingSettings, mixingFeatures);
59
+ const defaultPTZSettings = getSettingsFromKeys(PAN_TILT_ZOOM_SETTINGS_KEYS, getDefaultConstraints?.().video);
60
+ const ptzFeaturesChanges = defaultPTZSettings === false
61
+ ? undefined
62
+ : hasPtzFeature() &&
63
+ diffSettings(PAN_TILT_ZOOM_SETTINGS_KEYS)(prevVideoSettings ?? defaultPTZSettings, videoFeatures);
52
64
  const audioRequest = refineMediaConstraints({
53
65
  kind: 'audioinput',
54
66
  request: constraints.audio,
@@ -62,9 +74,9 @@ export const createMediaUpdater = ({ getUserMedia, getCurrentDevices, shouldDisc
62
74
  currentDevices,
63
75
  permission: permission.video,
64
76
  currentMediaTracks: currentMedia?.getVideoTracks() ?? [],
65
- force: !hasRequestedResolution || Boolean(ptzFeaturesChanges),
77
+ force: !hasRequestedResolution || !isEmpty(ptzFeaturesChanges),
66
78
  });
67
- if (audioRequest !== undefined || videoRequest !== undefined) {
79
+ if (audioRequest || videoRequest) {
68
80
  const media = await getUserMedia({
69
81
  constraints: { audio: audioRequest, video: videoRequest },
70
82
  originalConstraints: constraints,
@@ -80,6 +92,48 @@ export const createMediaUpdater = ({ getUserMedia, getCurrentDevices, shouldDisc
80
92
  ...(videoRequest ? media.getVideoTracks() : []),
81
93
  ]);
82
94
  }
95
+ else if (currentMedia?.status) {
96
+ // No media request, update status only
97
+ const anyAudioTrack = currentMedia
98
+ .getAudioTracks()
99
+ .some(track => track.track?.readyState === 'live');
100
+ const anyVideoTrack = currentMedia
101
+ .getVideoTracks()
102
+ .some(track => track.track?.readyState === 'live');
103
+ if (anyAudioTrack) {
104
+ if (anyVideoTrack) {
105
+ currentMedia.status = UserMediaStatus.PermissionsGranted;
106
+ }
107
+ else if (currentDevices.size('videoinput') === 0) {
108
+ currentMedia.status =
109
+ UserMediaStatus.PermissionsOnlyAudioinputNoVideoDevices;
110
+ }
111
+ else {
112
+ currentMedia.status =
113
+ UserMediaStatus.PermissionsOnlyAudioinput;
114
+ }
115
+ }
116
+ else {
117
+ if (anyVideoTrack) {
118
+ if (currentDevices.size('audioinput') === 0) {
119
+ currentMedia.status =
120
+ UserMediaStatus.PermissionsOnlyVideoinputNoAudioDevices;
121
+ }
122
+ else {
123
+ currentMedia.status =
124
+ UserMediaStatus.PermissionsOnlyVideoinput;
125
+ }
126
+ }
127
+ else if (currentDevices.size('audioinput') === 0 &&
128
+ currentDevices.size('videoinput') === 0) {
129
+ currentMedia.status = UserMediaStatus.NoDevicesFound;
130
+ }
131
+ else {
132
+ currentMedia.status = UserMediaStatus.PermissionsRejected;
133
+ }
134
+ }
135
+ logger.debug({ status: currentMedia.status }, 'Update existing media status');
136
+ }
83
137
  const audioDiff = constraints.audio &&
84
138
  audioRequest === undefined &&
85
139
  currentMedia?.getAudioTracks().at(0)
@@ -206,21 +260,26 @@ export const createMedia = ({ getMuteState, signals, audioProcessors, videoProce
206
260
  },
207
261
  });
208
262
  const getCurrentDevices = async () => {
209
- if (props.devices.size()) {
210
- return props.devices;
211
- }
212
263
  props.devices = createIndexedDevices(await getDevices());
213
264
  return props.devices;
214
265
  };
215
266
  const syncMuteState = (tracks) => {
216
267
  const inputMuted = getMuteState();
217
268
  for (const track of tracks) {
269
+ const prevMuted = track.muted;
218
270
  switch (track.kind) {
219
- case 'audioinput':
271
+ case 'audioinput': {
220
272
  track.mute(inputMuted.audio);
273
+ if (prevMuted !== track.muted) {
274
+ signals.onAudioMuteStateChanged?.emit(track.muted);
275
+ }
221
276
  break;
277
+ }
222
278
  case 'videoinput':
223
279
  track.mute(inputMuted.video);
280
+ if (prevMuted !== track.muted) {
281
+ signals.onVideoMuteStateChanged?.emit(track.muted);
282
+ }
224
283
  break;
225
284
  default:
226
285
  break;
@@ -269,10 +328,11 @@ export const createMedia = ({ getMuteState, signals, audioProcessors, videoProce
269
328
  getUserMedia: getUserMediaProcess,
270
329
  getCurrentDevices,
271
330
  shouldDiscardMedia: () => props.discardMedia,
331
+ getDefaultConstraints,
272
332
  onMediaTracksChanged: (media, tracks) => {
273
333
  // Sync mute state to input tracks
274
- syncMuteState(media.getTracks());
275
- tracks.forEach(track => {
334
+ syncMuteState(tracks);
335
+ for (const track of tracks) {
276
336
  if (track.track) {
277
337
  const unsubscribe = createStreamTrackEventSubscriptions(track.track, {
278
338
  ended: track => {
@@ -283,7 +343,8 @@ export const createMedia = ({ getMuteState, signals, audioProcessors, videoProce
283
343
  unmute: signals.onStreamTrackUnmuted?.emit,
284
344
  });
285
345
  }
286
- });
346
+ }
347
+ logger.debug({ media, tracks }, 'Update media tracks');
287
348
  props.media = media;
288
349
  processAndUpdateMedia(media, tracks).catch((error) => {
289
350
  if (error instanceof Error) {
@@ -329,13 +390,14 @@ export const createMedia = ({ getMuteState, signals, audioProcessors, videoProce
329
390
  queue.enqueue(async () => {
330
391
  const permission = await getInputDevicePermissionState();
331
392
  const status = getPermissionStatus(permission);
332
- if (isInitialPermissionsGranted(status)) {
393
+ logger.info({ permission, status }, 'tryAndGetUserMedia');
394
+ if (!isRequestPermissionNeeded(status)) {
333
395
  await updateMedia(mergeMediaConstraints(constraints));
334
396
  }
335
397
  else {
336
398
  const currentDevices = await getCurrentDevices();
337
399
  props.media = buildMedia({
338
- constraints,
400
+ constraints: mergeMediaConstraints(constraints),
339
401
  permission,
340
402
  devices: currentDevices,
341
403
  status,
@@ -346,34 +408,22 @@ export const createMedia = ({ getMuteState, signals, audioProcessors, videoProce
346
408
  }
347
409
  });
348
410
  };
349
- // Media Control Event handlers
350
- const handleNoInputs = (_availableDevices) => {
351
- if (props.media) {
352
- props.media.status = UserMediaStatus.NoDevicesFound;
411
+ let deviceChangeTimeoutId = 0;
412
+ const clearDeviceChangeTimeout = () => {
413
+ if (deviceChangeTimeoutId) {
414
+ clearTimeout(deviceChangeTimeoutId);
415
+ deviceChangeTimeoutId = 0;
353
416
  }
354
417
  };
355
- const handleDeviceChange = (devices) => {
356
- props.devices = createIndexedDevices(devices);
418
+ const handleDeviceChange = () => {
419
+ logger.debug('devicechange event triggered');
420
+ clearDeviceChangeTimeout();
421
+ deviceChangeTimeoutId = setTimeout(getCurrentDevices, DEVICE_CHANGE_DEBOUNCE_TIMEOUT_MS);
357
422
  };
358
423
  // Subscribe Media Events for devices
359
- subscribe(event => {
360
- switch (event.detail.type) {
361
- case MediaEventType.NoInputDevices:
362
- logger.debug(event.detail, 'NoInputDevices emitted');
363
- handleNoInputs(event.detail.devices);
364
- break;
365
- case MediaEventType.DevicesFound:
366
- case MediaEventType.DevicesLost:
367
- logger.debug(event.detail, 'DevicesFound/Lost emitted');
368
- handleDeviceChange([
369
- ...event.detail.authorizedDevices,
370
- ...event.detail.unauthorizedDevices,
371
- ]);
372
- break;
373
- default:
374
- break;
375
- }
376
- });
424
+ if ('mediaDevices' in navigator) {
425
+ navigator.mediaDevices.addEventListener('devicechange', handleDeviceChange);
426
+ }
377
427
  return {
378
428
  get updatingMedia() {
379
429
  return props.updatingMedia;
@@ -21,7 +21,8 @@ const extractFeaturesToConstraints = (keysToLookFor, settings) => {
21
21
  }
22
22
  return keysToLookFor.reduce((set, key) => {
23
23
  if (settings[key] !== undefined) {
24
- return { ...set, [key]: settings[key] };
24
+ // @ts-expect-error performance
25
+ set[key] = settings[key];
25
26
  }
26
27
  return set;
27
28
  }, {});
@@ -289,11 +290,9 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
289
290
  const { audio: previewAudio, video: previewVideo } = props.media?.getSettings() ?? { audio: {}, video: {} };
290
291
  if (force || hasChanges()) {
291
292
  const audioSettings = extractFeaturesToConstraints(AUDIO_SETTINGS_KEYS, previewAudio);
292
- const videoSettings = extractFeaturesToConstraints(VIDEO_SETTINGS_KEYS, previewVideo);
293
293
  // Omit the width & height settings from the preview so that the one
294
294
  // from the main can be applied
295
- delete videoSettings.width;
296
- delete videoSettings.height;
295
+ const { width, height, ...videoSettings } = extractFeaturesToConstraints(VIDEO_SETTINGS_KEYS, previewVideo);
297
296
  logger.info({ audioInput, videoInput, audioSettings, videoSettings }, 'Apply changes to main');
298
297
  try {
299
298
  await replaceMainStream({
@@ -321,6 +320,7 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
321
320
  if (mainMedia && mainStream) {
322
321
  // Workaround for iOS Safari bug
323
322
  // https://bugs.webkit.org/show_bug.cgi?id=179363
323
+ // https://github.com/pexip/mcu/issues/43138
324
324
  const mainMuted = !!mainStream.getTracks().some(t => t.muted);
325
325
  const current = mainMedia.audioInput;
326
326
  const prev = props.originalMainAudioInput;
package/dist/signals.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Signal, SignalVariant } from '@pexip/signal';
2
2
  import type { IndexedDevices } from '@pexip/media-control';
3
- import type { MediaSignals } from './types';
3
+ import type { MediaSignals, MediaSignalsOptional } from './types';
4
4
  /**
5
5
  * Create a general signal with consistent scoped name
6
6
  *
@@ -8,7 +8,7 @@ import type { MediaSignals } from './types';
8
8
  * @param crucial - Signify if the signal is unmissable. @defaultValue true
9
9
  * @param variant - The variant of the signal @see Signal @defaultValue 'generic'
10
10
  */
11
- export declare const createMediaSignal: <T = undefined>(name: string, crucial?: boolean, variant?: SignalVariant) => Signal<T>;
11
+ export declare const createMediaSignal: <T = undefined>(name: string, crucial?: boolean, variant?: Exclude<SignalVariant, "batched">) => Signal<T, T>;
12
12
  export declare const REQUIRED_SIGNAL_KEYS: readonly ["onMediaChanged", "onVAD", "onSilentDetected"];
13
13
  /**
14
14
  * Create and return all required and optional (if specified with `more`),
@@ -23,12 +23,12 @@ export declare const REQUIRED_SIGNAL_KEYS: readonly ["onMediaChanged", "onVAD",
23
23
  *
24
24
  * @see REQUIRED_SIGNAL_KEYS
25
25
  */
26
- export declare const createMediaSignals: <K extends "onAddTrack" | "onAudioMuteStateChanged" | "onTrackReleased" | "onDevicesChanged" | "onRemoveTrack" | "onStatusChanged" | "onVideoMuteStateChanged" | "onUpdatingMedia" | keyof import("./types").StreamTrackSignals | "onStreamTrackMutedFinal" | "onStreamTrackUnmutedFinal" | "onStreamTrackEndedFinal" | "onStreamTrackEnabledFinal">(more: K[], scope?: string) => Pick<Required<MediaSignals>, "onMediaChanged" | "onVAD" | "onSilentDetected" | K>;
26
+ export declare const createMediaSignals: <K extends keyof MediaSignalsOptional>(more: K[], scope?: string) => Pick<Required<MediaSignals>, "onMediaChanged" | "onVAD" | "onSilentDetected" | K>;
27
27
  export type GeneratedMediaSignals<K extends keyof MediaSignals> = Pick<Required<MediaSignals>, K>;
28
28
  /**
29
29
  * Internal signals
30
30
  * @internal
31
31
  */
32
32
  export declare const internalSignals: {
33
- onDevicesChanged: Signal<IndexedDevices>;
33
+ onDevicesChanged: Signal<IndexedDevices, IndexedDevices>;
34
34
  };
package/dist/signals.js CHANGED
@@ -6,7 +6,9 @@ import { createSignal } from '@pexip/signal';
6
6
  * @param crucial - Signify if the signal is unmissable. @defaultValue true
7
7
  * @param variant - The variant of the signal @see Signal @defaultValue 'generic'
8
8
  */
9
- export const createMediaSignal = (name, crucial = true, variant = 'generic') => createSignal({
9
+ export const createMediaSignal = (name, crucial = true,
10
+ // FIXME: need `schedule` function to support `batched` variant
11
+ variant = 'generic') => createSignal({
10
12
  name: `media/${name}`,
11
13
  allowEmittingWithoutObserver: !crucial,
12
14
  variant,
@@ -31,10 +33,11 @@ export const REQUIRED_SIGNAL_KEYS = [
31
33
  */
32
34
  export const createMediaSignals = (more, scope = '') => {
33
35
  const signalScope = scope && [scope, ':'].join('');
34
- return [...REQUIRED_SIGNAL_KEYS, ...more].reduce((signals, key) => ({
35
- ...signals,
36
- [key]: createMediaSignal(`${signalScope}${key}`),
37
- }), {});
36
+ return [...REQUIRED_SIGNAL_KEYS, ...more].reduce((signals, key) => {
37
+ // @ts-expect-error
38
+ signals[key] = createMediaSignal(`${signalScope}${key}`);
39
+ return signals;
40
+ }, {});
38
41
  };
39
42
  /**
40
43
  * Internal signals