@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 +36 -0
- package/dist/media.d.ts +1 -1
- package/dist/media.js +65 -28
- package/dist/previewController.d.ts +24 -7
- package/dist/previewController.js +107 -19
- package/dist/types.d.ts +10 -4
- package/dist/userMedia.d.ts +9 -4
- package/dist/userMedia.js +50 -19
- package/dist/utils.d.ts +3 -1
- package/dist/utils.js +38 -22
- package/dist/videoProcessor.d.ts +3 -1
- package/dist/videoProcessor.js +46 -30
- package/package.json +5 -5
- package/dist/tsconfig.build.tsbuildinfo +0 -1
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
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
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
|
-
|
|
23
|
-
|
|
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 '
|
|
217
|
+
case 'updatingAudio': {
|
|
218
|
+
if (target[p] === value) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
210
221
|
const result = Reflect.set(target, p, value);
|
|
211
|
-
signals?.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
428
|
-
return props.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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 '
|
|
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 =
|
|
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
|
|
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.
|
|
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.
|
|
319
|
+
props.updatingPreviewAudio = false;
|
|
252
320
|
}
|
|
253
321
|
};
|
|
254
322
|
const updateVideoInput = async (input) => {
|
|
255
323
|
try {
|
|
256
324
|
props.videoInput = input;
|
|
257
|
-
props.
|
|
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.
|
|
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
|
|
383
|
-
return props.
|
|
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
|
-
|
|
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
|
-
|
|
475
|
+
updatingAudio: boolean;
|
|
476
|
+
updatingVideo: boolean;
|
|
473
477
|
}
|
|
474
478
|
export interface MediaController {
|
|
475
|
-
readonly
|
|
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
|
-
|
|
562
|
+
onUpdatingAudio: Signal<boolean>;
|
|
563
|
+
onUpdatingVideo: Signal<boolean>;
|
|
558
564
|
}
|
|
559
565
|
export interface AudioDetectionSignals {
|
|
560
566
|
onVAD: Signal<undefined>;
|
package/dist/userMedia.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
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
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
const
|
|
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 (
|
|
118
|
-
assert(
|
|
120
|
+
if (nextValidAudio) {
|
|
121
|
+
assert(prevValidAudio === false, 'Only 1 valid audio');
|
|
119
122
|
// Replace Audio and Video tracks
|
|
120
|
-
if (
|
|
121
|
-
assert(
|
|
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 (
|
|
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 (
|
|
219
|
-
assert(
|
|
220
|
-
if (
|
|
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 (
|
|
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 (
|
|
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 = ({
|
|
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
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
package/dist/videoProcessor.d.ts
CHANGED
|
@@ -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: ({
|
|
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 {};
|
package/dist/videoProcessor.js
CHANGED
|
@@ -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 = ({
|
|
145
|
-
|
|
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({
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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.
|
|
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.
|
|
48
|
-
"@pexip/media-processor": "20.
|
|
49
|
-
"@pexip/signal": "16.
|
|
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.
|
|
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"}
|