@pexip/media 18.2.0 → 18.4.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,38 @@
1
1
  # @pexip/media
2
2
 
3
+ ## 18.4.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 9179b97: Added hasPtzFeature function to utils.ts
8
+
9
+ Added isDevicesInUse function to status.ts
10
+
11
+ Added isTryingToRequestSameUsedAudioDevice /
12
+ isTryingToRequestSameUsedVideoDevice check to updateMedia function in
13
+ media.ts. This avoids infinity reloads in Firefox if a blocked (e.g. by
14
+ another application) device is selected.
15
+
16
+ - Updated dependencies [36d5672]
17
+ - Updated dependencies [4bfce08]
18
+ - Updated dependencies [31e4c17]
19
+ - @pexip/media-processor@18.4.0
20
+ - @pexip/media-control@18.4.0
21
+ - @pexip/signal@16.7.0
22
+ - @pexip/utils@16.12.0
23
+
24
+ ## 18.3.0
25
+
26
+ ### Minor Changes
27
+
28
+ - a67b9eb: Introduce `onAudioMuteStateChanged` and `onVideoMuteStateChanged`
29
+ signals
30
+
31
+ ### Patch Changes
32
+
33
+ - @pexip/media-control@18.3.0
34
+ - @pexip/media-processor@18.3.0
35
+
3
36
  ## 18.2.0
4
37
 
5
38
  ### Patch Changes
package/README.md CHANGED
@@ -53,21 +53,6 @@ that's specified from the
53
53
  [Media Capture and Streams](https://w3c.github.io/mediacapture-main) spec, we
54
54
  have extended some additional media features as the following:
55
55
 
56
- - Able to pass `MediaDeviceInfo` directly to audio/video
57
- - Add `vad` to use our own implementation of Voice Activity Detection feature
58
- - Add `asd` to use our own implementation of Audio Signal Detection feature
59
- - Add `denoise` to use our own implementation of noise suppression
60
- - Add `backgroundBlurAmount` to adjust the blur amount when using the background
61
- blur feature
62
- - Add `edgeBlurAmount` to adjust the blur amount for edge of the segmented
63
- person
64
- - Add `foregroundThreshold` to adjust the threshold to be considered as
65
- foreground aka the segmented person
66
- - Add `bgImageUrl` for the image to be used for background replacement
67
- - Add `videoSegmentation` for specified effects of video segmentation
68
- - Add `videoSegmentationModel` for specifying segmentation model to be used
69
- - Add `flipHorizontal` to flip horizontally
70
-
71
56
  ```typescript
72
57
  export interface InputConstraintSet extends MediaTrackConstraints {
73
58
  /**
@@ -100,9 +85,10 @@ export interface InputConstraintSet extends MediaTrackConstraints {
100
85
  */
101
86
  asd?: ConstrainBoolean;
102
87
  /**
103
- * Flip the video horizontally
88
+ * Audio Signal Detection for the purpose of checking if the audio input is
89
+ * hardware muted or unusable
104
90
  */
105
- flipHorizontal?: ConstrainBoolean;
91
+ mixWithAdditionalMedia?: ConstrainBoolean;
106
92
  /**
107
93
  * Blur size/level parameter when using video segmentation with `blur`
108
94
  * effects
@@ -119,7 +105,26 @@ export interface InputConstraintSet extends MediaTrackConstraints {
119
105
  /**
120
106
  * Image Url that is being used for video overlay effects
121
107
  */
122
- bgImageUrl?: ConstrainDOMString;
108
+ backgroundImageUrl?: ConstrainDOMString;
109
+ /**
110
+ * The ratio to be used for smoothing segmentation mask
111
+ */
112
+ maskCombineRatio?: ConstrainDouble;
113
+ /**
114
+ * Image Url that is being used for video overlay effects
115
+ */
116
+ resizeMode?: ConstrainDOMString;
117
+ /**
118
+ * Current pan, tilt and zoom level for a PTZ controllable camera on web
119
+ */
120
+ pan?: boolean;
121
+ tilt?: boolean;
122
+ zoom?: boolean;
123
+
124
+ /**
125
+ * Content Hint for the track to apply
126
+ */
127
+ contentHint?: ConstrainDOMString;
123
128
  }
124
129
  ```
125
130
 
@@ -127,60 +132,35 @@ export interface InputConstraintSet extends MediaTrackConstraints {
127
132
 
128
133
  ```typescript
129
134
  import {createMedia, createMediaSignals, UserMediaStatus} from '@pexip/media';
130
- import type {VideoRenderParams} from '@pexip/media';
135
+ import type {RenderEffects, Segmenters, Media} from '@pexip/media';
131
136
  import {
132
- urls as mpUrls,
133
- createMediapipeSegmenter,
134
- createPsySegSegmenter,
137
+ createSegmenter,
135
138
  createCanvasTransform,
136
- createVideoTrackProcessor,
137
- createVideoTrackProcessorWithFallback,
138
139
  createVideoProcessor,
139
- PROCESSING_WIDTH,
140
- PROCESSING_HEIGHT,
141
- FOREGROUND_THRESHOLD,
142
- BACKGROUND_BLUR_AMOUNT,
143
- EDGE_BLUR_AMOUNT,
144
- FLIP_HORIZONTAL,
145
140
  } from '@pexip/media-processor';
146
141
 
147
- // Need some state for the app
148
- interface State extends VideoRenderParams {
149
- mute: {
150
- audio: boolean;
151
- video: boolean;
152
- };
153
- noiseSuppression: boolean;
154
- vad: boolean;
155
- asd: boolean;
156
- updateFrequency: number;
157
- silentDetectionDuration: number;
158
- vadThrottleMS: number;
159
- monitor: boolean;
160
- channelCount: number;
161
- }
162
- export const mediaState: State = {
163
- mute: {
142
+ // App states
143
+ const states: {
144
+ muted: {video: boolean; audio: boolean};
145
+ effects: RenderEffects;
146
+ foregroundThreshold: number;
147
+ backgroundBlurAmount: number;
148
+ maskCombineRatio: number;
149
+ backgroundImageUrl: string;
150
+ media?: Media;
151
+ status: UserMediaStatus;
152
+ } = {
153
+ muted: {
164
154
  audio: false,
165
155
  video: false,
166
156
  },
167
- videoSegmentation: 'none',
168
- noiseSuppression: false,
169
- vad: false,
170
- asd: false,
171
- updateFrequency: 0.5,
172
- silentDetectionDuration: 4.0,
173
- vadThrottleMS: 3000,
174
- width: PROCESSING_WIDTH,
175
- height: PROCESSING_HEIGHT,
176
- frameRate: 30,
177
- bgImageUrl: '',
178
- monitor: false,
179
- channelCount: 1,
180
- foregroundThreshold: FOREGROUND_THRESHOLD,
181
- backgroundBlurAmount: BACKGROUND_BLUR_AMOUNT,
182
- edgeBlurAmount: EDGE_BLUR_AMOUNT,
183
- flipHorizontal: FLIP_HORIZONTAL,
157
+ // Could be `none`, `blur` or `overlay`
158
+ effects: 'none',
159
+ foregroundThreshold: 0.5, // 0.0 - 1.0
160
+ backgroundBlurAmount: 30, // 0 - 100
161
+ maskCombineRatio: 0.5, // 0.0 - 1.0
162
+ backgroundImageUrl: '/some/path/to/an/image/for/background/replacement.png',
163
+ status: UserMediaStatus.Initial,
184
164
  };
185
165
 
186
166
  // Create required signals with additional `onDevicesChanged`,
@@ -191,71 +171,61 @@ export const signals = createMediaSignals('demo', [
191
171
  'onStreamTrackEnabled',
192
172
  ]);
193
173
 
194
- const audioProcessor = createAudioStreamProcess({
195
- shouldEnable: () => true,
196
- denoiseParams: {
197
- wasmURL: denoiseWasm.href,
198
- workletModule: mpUrls.denoise.href,
199
- },
200
- analyzerUpdateFrequency: mediaState.updateFrequency,
201
- audioSignalDetectionDuration: mediaState.silentDetectionDuration,
202
- throttleMs: mediaState.vadThrottleMS,
203
- onAudioSignalDetected: signals.onSilentDetected.emit,
204
- onVoiceActivityDetected: signals.onVAD.emit,
174
+ // Setting the path to that `@mediapipe/tasks-vision` assets
175
+ // It will be passed direct to
176
+ // [FilesetResolver.forVisionTasks()](https://ai.google.dev/edge/api/mediapipe/js/tasks-vision.filesetresolver#filesetresolverforvisiontasks)
177
+ const tasksVisionBasePath =
178
+ 'A base path to specify the directory the Wasm files should be loaded from';
179
+
180
+ const modelAsset = {
181
+ /**
182
+ * Path to mediapipe selfie segmentation model asset
183
+ */
184
+ path: 'A path to selfie segmentation model',
185
+ modelName: 'selfie' as const,
186
+ };
187
+
188
+ const segmenter = createSegmenter(tasksVisionBasePath, {modelAssets});
189
+ // Create a processing transformer and set the effects to `blur`
190
+ const transformer = createCanvasTransform(segmenter, {
191
+ effects: states.effects,
192
+ foregroundThreshold: states.foregroundThreshold,
193
+ backgroundBlurAmount: states.backgroundBlurAmount,
194
+ maskCombineRatio: states.maskCombineRatio,
195
+ backgroundImageUrl: states.backgroundImageUrl,
205
196
  });
206
197
 
207
- // Create symbolic link to link bg-blur/selfie_segmentation to a public folder
208
- // Thus we can create a URL object to get the link to be used for the `gluePath`
209
- const selfieJs = new URL(
210
- `./selfie_segmentation/selfie_segmentation.js`,
211
- document.baseURI,
212
- );
213
- const mediapipeSegmenter = createMediapipeSegmenter('selfie_segmentation', {
214
- modelType: 'general',
215
- processingWidth: PROCESSING_WIDTH,
216
- processingHeight: PROCESSING_HEIGHT,
217
- gluePath: selfieJs.href,
198
+ const delegate = isChromium() ? 'GPU' : 'CPU';
199
+ const selfie = createSegmenter(taskVisionBasePath, {
200
+ modelAsset,
201
+ delegate,
218
202
  });
219
203
 
220
- const segmenters: Segmenters = {
221
- mediapipeSelfie: mediapipeSegmenter,
204
+ export const segmenters: Partial<Segmenters> = {
205
+ selfie,
222
206
  };
223
207
 
224
- const transformer = createCanvasTransform(mediapipeSegmenter, {
225
- effects: mediaState.videoSegmentation,
226
- width: mediaState.width,
227
- height: mediaState.height,
228
- foregroundThreshold: mediaState.foregroundThreshold,
229
- backgroundBlurAmount: mediaState.backgroundBlurAmount,
230
- edgeBlurAmount: mediaState.edgeBlurAmount,
231
- flipHorizontal: mediaState.flipHorizontal,
232
- });
233
208
  const videoProcessor = createVideoProcessor([transformer]);
234
209
 
235
210
  const videoStreamProcessor = createVideoStreamProcess({
236
- trackProcessorAPI: 'stream', // Use Stream API when available
237
211
  shouldEnable: () => true, // Always Process Video Track
238
- videoSegmentationModel: 'mediapipeSelfie', // Segmentation Modal
239
212
  segmenters,
240
213
  transformer,
241
214
  videoProcessor,
242
- processingWidth: mediaState.width,
243
- processingHeight: mediaState.height,
244
- onError: error => {
245
- console.error(error);
246
- },
247
215
  });
248
216
 
249
217
  // Instantiate the media object
250
218
  export const mediaController = createMedia({
251
- getMuteState: () => mediaState.mute,
219
+ getMuteState: () => states.muted,
252
220
  signals,
253
- mediaProcessors: [videoStreamProcessor, audioProcessor],
221
+ mediaProcessors: [videoStreamProcessor],
254
222
  });
255
223
 
256
224
  // Hook-up the signals to get the update
257
225
  // Subscribe the onMediaChanged signal to get the latest Media object change event
258
- signals.onMediaChanged.add(media => setLocalMedia(media)); // Assume there is a local function `setLocalMedia`
226
+ signals.onMediaChanged.add(media => {
227
+ states.media = media;
228
+ }); // Assume there is a local function `setLocalMedia`
259
229
  // Subscribe the onStatusChanged signal to get the latest Media status change event
260
230
  signals.onStatusChanged.add(media => setMediaStatus(media)); // Assume there is a local function `setMediaStatus`
261
231
  // Subscribe onStreamTrackEnabled signal to get the MediaStreamTrack['enabled'] change event
@@ -265,16 +235,13 @@ signals.onStreamTrackEnabled.add(track => {
265
235
 
266
236
  // Later we can initialize a gUM to get a MediaStream
267
237
  mediaController.getUserMedia({
268
- audio: {
269
- sampleRate: 48000, // Ideal sample rate for our own noise suppression implementation
270
- denoise: true, // Use our own implementation of noise suppression
271
- noiseSuppression: false, // Disable the built-in noise suppression
272
- vad: true, // Use Voice Activity Detection
273
- asd: true, // Use Audio Signal Detection to check if the microphone is malfunctioned
274
- },
238
+ audio: true,
275
239
  video: {
276
240
  videoSegmentation: 'blur', // Use background blur
277
- blurLevel: 21,
241
+ foregroundThreshold: states.foregroundThreshold,
242
+ backgroundBlurAmount: states.backgroundBlurAmount,
243
+ maskCombineRatio: states.maskCombineRatio,
244
+ backgroundImageUrl: states.backgroundImageUrl,
278
245
  },
279
246
  });
280
247
 
@@ -314,39 +281,3 @@ await mediaController.media.applyConstraints({
314
281
  audio: {denoise: false, noiseSuppression: true},
315
282
  });
316
283
  ```
317
-
318
- ### `createPreviewController`
319
-
320
- ```typescript
321
- const {createPreviewController} = require('@pexip/media');
322
-
323
- // Some states from the App
324
- interface AppState {
325
- media: Media;
326
- devices: MediaDeviceInfo[];
327
- }
328
- // Some functions from the App
329
- const updateStream = (constraints: MediaDeviceRequest) => Promise<void>;
330
-
331
- const onMediaChangedSignal = createSignal<Media>();
332
-
333
- // Instantiate the controller
334
- const controller = createPreviewController({
335
- getDesiredResolution: () => ({}),
336
- getCurrentDevices: () => state.devices,
337
- getCurrentMedia: () => state.media,
338
- updateMainStream: updateStream,
339
- mediaSignal: onMediaChangedSignal,
340
- getSegmentationOptions: () => ({
341
- effects: mediaState.videoSegmentation,
342
- erode: mediaState.erode,
343
- blurLevel: mediaState.blurLevel,
344
- }),
345
- videoProcessingEnabled: true,
346
- updateMainStream: mc.getUserMediaAsync,
347
- onEnded: () => {
348
- close();
349
- },
350
- psySegScriptLoaded: true,
351
- });
352
- ```
package/dist/media.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { subscribe, MediaEventType, MediaDeviceFailure, createStreamTrackEventSubscriptions, getDevices, isRequestedResolution, mergeConstraints, shouldRequestDevice, } from '@pexip/media-control';
2
2
  import { createAsyncQueue, isEmpty } from '@pexip/utils';
3
- import { getPermissionStatus, deriveInitialPermissionStatus, isInitialPermissions, isInitialPermissionsGranted, } from './status';
3
+ import { extractConstrainDevice } from '@pexip/media-control/dist/constraints';
4
+ import { getPermissionStatus, deriveInitialPermissionStatus, isInitialPermissions, isInitialPermissionsGranted, isVideoDeviceInUse, isDevicesInUse, isAudioDeviceInUse, } from './status';
4
5
  import { UserMediaStatus } from './types';
5
6
  import { createModuleLogger, logger } from './logger';
6
7
  import { createGetUserMediaProcess, requestUserMediaWithRetry, } from './userMedia';
7
- import { createMediaPipeline, createMediaProcess, buildMedia, getDevicesChanges, shallowCopy, wrapToJSON, hasSettingsChanged, AUDIO_SETTINGS_KEYS, VIDEO_SETTINGS_KEYS, MIXING_SETTINGS_KEYS, applyContentHint, } from './utils';
8
+ import { createMediaPipeline, createMediaProcess, buildMedia, getDevicesChanges, shallowCopy, wrapToJSON, hasSettingsChanged, AUDIO_SETTINGS_KEYS, VIDEO_SETTINGS_KEYS, MIXING_SETTINGS_KEYS, applyContentHint, hasPtzFeature, } from './utils';
8
9
  import { isMedia } from './typeGuard';
9
10
  import { updateFeatureProps as getVideoFeatures } from './videoProcessor';
10
11
  import { updateFeatureProps as getAudioFeatures } from './audioProcessor';
@@ -149,8 +150,14 @@ export const createMedia = ({ getMuteState, signals, mediaProcessors, getDefault
149
150
  logger.debug({ finalAudioMute: audio, finalVideoMute: video }, 'End of media pipeline');
150
151
  return wrapToJSON(shallowCopy(media, {
151
152
  release,
152
- muteAudio,
153
- muteVideo,
153
+ muteAudio: mute => {
154
+ muteAudio(mute);
155
+ signals.onAudioMuteStateChanged?.emit(mute);
156
+ },
157
+ muteVideo: mute => {
158
+ muteVideo(mute);
159
+ signals.onVideoMuteStateChanged?.emit(mute);
160
+ },
154
161
  applyConstraints,
155
162
  }));
156
163
  }),
@@ -165,19 +172,35 @@ export const createMedia = ({ getMuteState, signals, mediaProcessors, getDefault
165
172
  if (!constraints.audio && !constraints.video) {
166
173
  throw new Error(MediaDeviceFailure.MissingConstraintsError);
167
174
  }
168
- const shouldRequestAudio = shouldRequestDevice(constraints.audio, props.media?.rawStream?.getAudioTracks() ?? [], props.devices.filter(device => device.kind === 'audioinput'));
169
- const shouldRequestVideo = shouldRequestDevice(constraints.video, props.media?.rawStream?.getVideoTracks() ?? [], props.devices.filter(device => device.kind === 'videoinput'));
175
+ // Check if the audio device was requested before and is currently in use by another application
176
+ const isTryingToRequestSameUsedAudioDevice = (isAudioDeviceInUse(props.media.status) ||
177
+ isDevicesInUse(props.media.status)) &&
178
+ props.media.expectedAudioInput?.deviceId &&
179
+ props.media.expectedAudioInput?.deviceId ===
180
+ extractConstrainDevice(constraints.audio)?.[0]?.[0]?.deviceId;
181
+ // Check if the video device was requested before and is currently in use by another application
182
+ const isTryingToRequestSameUsedVideoDevice = (isVideoDeviceInUse(props.media.status) ||
183
+ isDevicesInUse(props.media.status)) &&
184
+ props.media.expectedVideoInput?.deviceId &&
185
+ props.media.expectedVideoInput?.deviceId ===
186
+ extractConstrainDevice(constraints?.video)?.[0]?.[0]?.deviceId;
187
+ const shouldRequestAudio = !isTryingToRequestSameUsedAudioDevice &&
188
+ shouldRequestDevice(constraints.audio, props.media?.rawStream?.getAudioTracks() ?? [], props.devices.filter(device => device.kind === 'audioinput'));
189
+ const shouldRequestVideo = !isTryingToRequestSameUsedVideoDevice &&
190
+ shouldRequestDevice(constraints.video, props.media?.rawStream?.getVideoTracks() ?? [], props.devices.filter(device => device.kind === 'videoinput'));
170
191
  const { audio: [prevAudioSettings], video: [prevVideoSettings], } = props.media.getSettings();
171
192
  const videoFeatures = getVideoFeatures(constraints.video, {});
172
193
  const audioFeatures = getAudioFeatures(constraints.audio, {});
173
194
  const mixingFeatures = getMixingFeatures(constraints.audio, {});
174
195
  const hasRequestedResolution = ['blur', 'overlay'].includes(videoFeatures.videoSegmentation ?? '') || // Skip when there is a render effect to modify the video
175
196
  !props.devices.some(device => device.kind === 'videoinput' && device.label) || // Skip when no authorized video found
197
+ isTryingToRequestSameUsedVideoDevice ||
176
198
  isRequestedResolution(constraints.video, props.media?.videoInput);
177
199
  const audioFeaturesChanged = hasSettingsChanged(AUDIO_SETTINGS_KEYS)(prevAudioSettings, audioFeatures);
178
200
  const videoFeaturesChanged = hasSettingsChanged(VIDEO_SETTINGS_KEYS)(prevVideoSettings, videoFeatures);
179
201
  const mixingFeaturesChanged = hasSettingsChanged(MIXING_SETTINGS_KEYS)(prevAudioSettings, mixingFeatures);
180
- const ptzFeaturesChanges = hasSettingsChanged(['pan', 'tilt', 'zoom'])(prevVideoSettings, videoFeatures);
202
+ const ptzFeaturesChanges = hasPtzFeature() &&
203
+ hasSettingsChanged(['pan', 'tilt', 'zoom'])(prevVideoSettings, videoFeatures);
181
204
  logger.debug({
182
205
  constraints,
183
206
  stream: props.media.stream,
package/dist/signals.d.ts CHANGED
@@ -22,4 +22,4 @@ export declare const REQUIRED_SIGNAL_KEYS: readonly ["onMediaChanged", "onVAD",
22
22
  *
23
23
  * @see REQUIRED_SIGNAL_KEYS
24
24
  */
25
- export declare const createMediaSignals: <K extends "onDevicesChanged" | "onStatusChanged" | keyof import("./types").StreamTrackSignals>(more: K[], scope?: string) => Pick<Required<MediaSignals>, "onMediaChanged" | "onVAD" | "onSilentDetected" | K>;
25
+ export declare const createMediaSignals: <K extends "onDevicesChanged" | "onStatusChanged" | "onAudioMuteStateChanged" | "onVideoMuteStateChanged" | keyof import("./types").StreamTrackSignals>(more: K[], scope?: string) => Pick<Required<MediaSignals>, "onMediaChanged" | "onVAD" | "onSilentDetected" | K>;
package/dist/status.d.ts CHANGED
@@ -29,6 +29,7 @@ export declare const isInitialPermissionsNotGranted: UserMediaValidator;
29
29
  export declare const isInitialPermissionsGranted: UserMediaValidator;
30
30
  export declare const isAudioDeviceInUse: UserMediaValidator;
31
31
  export declare const isDeviceInUse: UserMediaValidator;
32
+ export declare const isDevicesInUse: UserMediaValidator;
32
33
  export declare const isVideoDeviceInUse: UserMediaValidator;
33
34
  export declare const toDeniedDevices: (status?: UserMediaStatus) => DeniedDevices | undefined;
34
35
  export declare const getPermissionStatus: (getPermissionState?: (anyActiveStream?: boolean | undefined) => Promise<import("@pexip/media-control").InputDevicePermission>) => Promise<UserMediaStatus.Initial | UserMediaStatus.InitialPermissionsGranted | UserMediaStatus.InitialPermissionsNotGranted | UserMediaStatus.InitialPermissionsVideoInputDenied | UserMediaStatus.InitialPermissionsAudioInputDenied | UserMediaStatus.InitialPermissionsVideoInputGranted | UserMediaStatus.InitialPermissionsAudioInputGranted | UserMediaStatus.InitialPermissionsGrantedVideoInputDenied | UserMediaStatus.InitialPermissionsGrantedAudioInputDenied | UserMediaStatus.PermissionsRejected>;
package/dist/status.js CHANGED
@@ -121,6 +121,7 @@ export const isDeviceInUse = status => [
121
121
  UserMediaStatus.AudioDeviceInUse,
122
122
  UserMediaStatus.VideoDeviceInUse,
123
123
  ].includes(status);
124
+ export const isDevicesInUse = status => status === UserMediaStatus.DevicesInUse;
124
125
  export const isVideoDeviceInUse = status => status === UserMediaStatus.VideoDeviceInUse;
125
126
  export const toDeniedDevices = (status) => {
126
127
  if (!status) {
package/dist/types.d.ts CHANGED
@@ -458,12 +458,14 @@ export interface MediaChangesSignals {
458
458
  onDevicesChanged: Signal<MediaDeviceInfoLike[]>;
459
459
  onStatusChanged: Signal<UserMediaStatus>;
460
460
  onMediaChanged: Signal<Media>;
461
+ onAudioMuteStateChanged: Signal<boolean>;
462
+ onVideoMuteStateChanged: Signal<boolean>;
461
463
  }
462
464
  export interface AudioDetectionSignals {
463
465
  onVAD: Signal<undefined>;
464
466
  onSilentDetected: Signal<boolean>;
465
467
  }
466
- export type MediaSignalsOptional = Pick<Partial<MediaChangesSignals>, 'onDevicesChanged' | 'onStatusChanged'> & Partial<StreamTrackSignals>;
468
+ export type MediaSignalsOptional = Pick<Partial<MediaChangesSignals>, 'onDevicesChanged' | 'onStatusChanged' | 'onAudioMuteStateChanged' | 'onVideoMuteStateChanged'> & Partial<StreamTrackSignals>;
467
469
  export type MediaSignalsRequired = Pick<MediaChangesSignals, 'onMediaChanged'> & AudioDetectionSignals;
468
470
  export type MediaSignals = MediaSignalsRequired & MediaSignalsOptional;
469
471
  export declare enum DeniedDevices {
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MediaDeviceRequest, MediaDeviceInfoLike } from '@pexip/media-control';
1
+ import type { MediaDeviceInfoLike, MediaDeviceRequest } from '@pexip/media-control';
2
2
  import { UserMediaStatus } from './types';
3
3
  import type { Media, MediaAttributes, Pipeline, Process, ProcessMedia, ExtendedMediaTrackSettingsKey, ExtendedMediaTrackSettings } from './types';
4
4
  export declare const makeDeriveDeviceStatus: (constraints: MediaDeviceRequest) => (audio: UserMediaStatus, video: UserMediaStatus, both: UserMediaStatus) => UserMediaStatus;
@@ -105,4 +105,9 @@ export declare const getBlurKernelSize: (percentage: number, height: number, max
105
105
  * @param track - The track to be applied
106
106
  */
107
107
  export declare const applyContentHint: <T extends "" | "speech" | "speech-recognition" | "music" | "motion" | "detail" | "text">(hint?: T | undefined) => (track: MediaStreamTrack) => void;
108
+ /**
109
+ * Check if the browser supports PTZ feature
110
+ * @returns true if the browser supports PTZ feature, otherwise false
111
+ */
112
+ export declare const hasPtzFeature: () => boolean;
108
113
  export {};
package/dist/utils.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { applyConstraints, createTrackDevicesChanges, extractConstraintsWithKeys, findDeviceFromConstraints, muteStreamTrack, relaxInputConstraint, stopMediaStream, isAudioInput, isVideoInput, } from '@pexip/media-control';
2
2
  import { calculateMaxBlurPass } from '@pexip/media-processor';
3
+ import { hasOwn } from '@pexip/utils';
3
4
  import { UserMediaStatus } from './types';
4
5
  import { isOverConstrained } from './status';
5
6
  import { isMedia } from './typeGuard';
@@ -359,3 +360,13 @@ export const applyContentHint = (hint) => (track) => {
359
360
  track.contentHint = hint;
360
361
  }
361
362
  };
363
+ /**
364
+ * Check if the browser supports PTZ feature
365
+ * @returns true if the browser supports PTZ feature, otherwise false
366
+ */
367
+ export const hasPtzFeature = () => {
368
+ const supports = navigator.mediaDevices.getSupportedConstraints();
369
+ return (hasOwn(supports, 'pan') &&
370
+ hasOwn(supports, 'tilt') &&
371
+ hasOwn(supports, 'zoom'));
372
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pexip/media",
3
- "version": "18.2.0",
3
+ "version": "18.4.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",
@@ -32,14 +32,14 @@
32
32
  "typecheck": "yarn tsc --noEmit -p ."
33
33
  },
34
34
  "dependencies": {
35
- "@pexip/media-control": "18.2.0",
36
- "@pexip/media-processor": "18.2.0",
35
+ "@pexip/media-control": "18.4.0",
36
+ "@pexip/media-processor": "18.4.0",
37
37
  "@pexip/signal": "16.7.0",
38
38
  "@pexip/utils": "16.12.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@jest/globals": "^29.7.0",
42
- "@pexip/bundler": "16.6.1",
42
+ "@pexip/bundler": "16.7.0",
43
43
  "prettier": "^3.2.5",
44
44
  "typescript": "~5.3.0"
45
45
  },