@pexip/media 20.2.0 → 20.3.3
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 +75 -0
- package/dist/audioMixingProcessor.d.ts +3 -3
- package/dist/audioMixingProcessor.js +9 -12
- package/dist/audioProcessor.d.ts +5 -4
- package/dist/audioProcessor.js +13 -9
- package/dist/media.js +9 -37
- package/dist/previewController.d.ts +8 -3
- package/dist/previewController.js +28 -17
- package/dist/types.d.ts +43 -46
- package/dist/userMedia.js +27 -10
- package/dist/utils.js +152 -107
- package/dist/videoProcessor.d.ts +6 -5
- package/dist/videoProcessor.js +58 -31
- package/package.json +11 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,80 @@
|
|
|
1
1
|
# @pexip/media
|
|
2
2
|
|
|
3
|
+
## 20.3.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a93f6b9: Fix audio mixer muted state
|
|
8
|
+
- @pexip/media-control@20.3.3
|
|
9
|
+
- @pexip/media-processor@20.3.3
|
|
10
|
+
|
|
11
|
+
## 20.3.2
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [aac2518]
|
|
16
|
+
- Updated dependencies [aac2518]
|
|
17
|
+
- @pexip/utils@17.1.1
|
|
18
|
+
- @pexip/signal@16.9.2
|
|
19
|
+
- @pexip/media-control@20.3.2
|
|
20
|
+
- @pexip/media-processor@20.3.2
|
|
21
|
+
|
|
22
|
+
## 20.3.1
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- af038f3: Update Biome v2
|
|
27
|
+
- Updated dependencies [4068f02]
|
|
28
|
+
- Updated dependencies [a551f01]
|
|
29
|
+
- Updated dependencies [af038f3]
|
|
30
|
+
- @pexip/utils@17.1.0
|
|
31
|
+
- @pexip/signal@16.9.1
|
|
32
|
+
- @pexip/media-processor@20.3.1
|
|
33
|
+
- @pexip/media-control@20.3.1
|
|
34
|
+
|
|
35
|
+
## 20.3.0
|
|
36
|
+
|
|
37
|
+
### Minor Changes
|
|
38
|
+
|
|
39
|
+
- 69d6be4: Refactor `MediaTrack` to decouple system/hardware mute from mute
|
|
40
|
+
|
|
41
|
+
- Add `stopped` attributes to `MediaTrack` to indicate the track `live` or
|
|
42
|
+
`ended`
|
|
43
|
+
- Add `suspended` attributes to
|
|
44
|
+
`MediaTrack to indicate the track is being system/hardware muted`
|
|
45
|
+
- Add signal supports to `MediaTrack` to be able to subscribe track related
|
|
46
|
+
signals
|
|
47
|
+
- Implement Two-hand control to secure mute consistency
|
|
48
|
+
|
|
49
|
+
When there is `muted` event from `MediaStreamTrack`, the track will follow
|
|
50
|
+
the event to set the `enabled` attribute to `false` to avoid any
|
|
51
|
+
possibilities of leaking the local media to remote parties. When the
|
|
52
|
+
`unmuted` event is triggered, it depends on whether the `enabled` attribute
|
|
53
|
+
is set before the `muted` event or not to unmute the track.
|
|
54
|
+
|
|
55
|
+
Breaking changes:
|
|
56
|
+
|
|
57
|
+
- Replace `MediaStreamTrack` related signal with the new `MediaTrack` signals
|
|
58
|
+
|
|
59
|
+
- `onStreamTrackEnded` -> `onMediaTrackStopped`
|
|
60
|
+
- `onStreamTrackMuted` -> `onMediaTrackSuspended`
|
|
61
|
+
- `onStreamTrackUnmuted` -> `onMediaTrackResumed`
|
|
62
|
+
- `onStreamTrackEnabled` -> `onMediaTrackMuted`
|
|
63
|
+
- `onAudioMuteStateChanged` -> `onMediaTrackMuted`
|
|
64
|
+
- `onVideoMuteStateChanged` -> `onMediaTrackMuted`
|
|
65
|
+
|
|
66
|
+
### Patch Changes
|
|
67
|
+
|
|
68
|
+
- f8a8159: Fix Subsequant mute/unmute events handling
|
|
69
|
+
- e347770: Handle suspended state fro preview controller
|
|
70
|
+
- 0a375c7: Delay stopping video track to ensure last frame being black
|
|
71
|
+
- Updated dependencies [69d6be4]
|
|
72
|
+
- Updated dependencies [adc1a50]
|
|
73
|
+
- @pexip/media-control@20.3.0
|
|
74
|
+
- @pexip/utils@17.0.1
|
|
75
|
+
- @pexip/media-processor@20.3.0
|
|
76
|
+
- @pexip/signal@16.9.0
|
|
77
|
+
|
|
3
78
|
## 20.2.0
|
|
4
79
|
|
|
5
80
|
### Minor Changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MediaDeviceRequest } from '@pexip/media-control';
|
|
2
2
|
import type { AudioGraph, AudioNodeInit } from '@pexip/media-processor';
|
|
3
|
-
import type { TrackProcessor } from './types';
|
|
3
|
+
import type { MediaSignals, TrackProcessor } from './types';
|
|
4
4
|
interface AudioStreamProcessorProps {
|
|
5
5
|
mixWithAdditionalMedia?: boolean;
|
|
6
6
|
merger?: AudioNodeInit<ChannelMergerNode, ChannelMergerNode>;
|
|
@@ -12,7 +12,7 @@ type FeaturePropKeys = (typeof FEATURE_KEYS)[number];
|
|
|
12
12
|
type FeatureProps = Pick<AudioStreamProcessorProps, FeaturePropKeys>;
|
|
13
13
|
export declare const updateFeatureProps: (constraints: MediaDeviceRequest["audio"], props: FeatureProps) => FeatureProps;
|
|
14
14
|
/**
|
|
15
|
-
* Create
|
|
15
|
+
* Create an Audio Mixing Processor and will own the stream passed-in
|
|
16
16
|
*/
|
|
17
|
-
export declare const createAudioMixingProcess: (getCurrentMedia: () => MediaStream | undefined, label?: "AudioMixingProcessor") => TrackProcessor;
|
|
17
|
+
export declare const createAudioMixingProcess: (getCurrentMedia: () => MediaStream | undefined, signals?: MediaSignals, label?: "AudioMixingProcessor") => TrackProcessor;
|
|
18
18
|
export {};
|
|
@@ -2,7 +2,7 @@ import { extractConstraintsWithKeys } from '@pexip/media-control';
|
|
|
2
2
|
import { isEmpty, assert } from '@pexip/utils';
|
|
3
3
|
import { createAudioGraph, createAudioGraphProxy, createChannelMergerGraphNode, createStreamDestinationGraphNode, createStreamSourceGraphNode, resumeAudioOnUnmute, } from '@pexip/media-processor';
|
|
4
4
|
import { logger } from './logger';
|
|
5
|
-
import { createMediaTrack
|
|
5
|
+
import { createMediaTrack } from './utils';
|
|
6
6
|
import { PROCESSOR_LABELS } from './constants';
|
|
7
7
|
const FEATURE_KEYS = ['mixWithAdditionalMedia'];
|
|
8
8
|
const getAudioConstraints = extractConstraintsWithKeys(FEATURE_KEYS);
|
|
@@ -19,9 +19,9 @@ export const updateFeatureProps = (constraints, props) => {
|
|
|
19
19
|
}, {});
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
|
-
* Create
|
|
22
|
+
* Create an Audio Mixing Processor and will own the stream passed-in
|
|
23
23
|
*/
|
|
24
|
-
export const createAudioMixingProcess = (getCurrentMedia, label = PROCESSOR_LABELS.AudioMixingProcessor) => {
|
|
24
|
+
export const createAudioMixingProcess = (getCurrentMedia, signals, label = PROCESSOR_LABELS.AudioMixingProcessor) => {
|
|
25
25
|
const props = {};
|
|
26
26
|
return async (prevMediaTrack) => {
|
|
27
27
|
updateFeatureProps(prevMediaTrack.getConstraints(), props);
|
|
@@ -154,25 +154,22 @@ export const createAudioMixingProcess = (getCurrentMedia, label = PROCESSOR_LABE
|
|
|
154
154
|
expectedInput: prevMediaTrack.expectedInput,
|
|
155
155
|
track,
|
|
156
156
|
overrideMute: true,
|
|
157
|
-
mute: toMute => {
|
|
158
|
-
prevMediaTrack.mute(toMute);
|
|
157
|
+
mute: (toMute, _self, soft) => {
|
|
159
158
|
for (const track of mainSource.node?.mediaStream.getAudioTracks() ??
|
|
160
159
|
[]) {
|
|
161
160
|
track.enabled = !toMute;
|
|
162
161
|
}
|
|
162
|
+
prevMediaTrack.mute(toMute, soft);
|
|
163
163
|
},
|
|
164
164
|
get muted() {
|
|
165
|
-
return
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
!!prevMediaTrack.muted ||
|
|
169
|
-
mainSource.node?.mediaStream
|
|
170
|
-
.getAudioTracks()
|
|
171
|
-
.some(isTrackMuted));
|
|
165
|
+
return mainSource.node?.mediaStream
|
|
166
|
+
.getAudioTracks()
|
|
167
|
+
.some(track => track.enabled === false);
|
|
172
168
|
},
|
|
173
169
|
constraints: prevMediaTrack.getConstraints(),
|
|
174
170
|
applyConstraints,
|
|
175
171
|
release,
|
|
172
|
+
signals,
|
|
176
173
|
getSettings: () => {
|
|
177
174
|
const mixWithAdditionalMedia = !!props.mixWithAdditionalMedia;
|
|
178
175
|
const contentHint = track.contentHint ?? '';
|
package/dist/audioProcessor.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AnalyzerNodeInit, AudioGraph, AudioGraphOptions, AudioNodeInit, DenoiseWorkletNodeInit, ThrottleOptions } from '@pexip/media-processor';
|
|
2
2
|
import type { MediaDeviceRequest } from '@pexip/media-control';
|
|
3
|
-
import type {
|
|
3
|
+
import type { AudioContentHint, DenoiseParams, MediaSignals, TrackProcessor } from './types';
|
|
4
4
|
type AudioNodeInits = AudioNodeInit[];
|
|
5
5
|
/**
|
|
6
6
|
* A function to be called to create the AudioNodes needed for the graph
|
|
@@ -60,6 +60,7 @@ interface AudioProcessOptions {
|
|
|
60
60
|
*/
|
|
61
61
|
silentThreshold?: number;
|
|
62
62
|
label?: string;
|
|
63
|
+
signals?: MediaSignals;
|
|
63
64
|
}
|
|
64
65
|
interface AudioStreamProcessorProps {
|
|
65
66
|
audioGraphOptions?: AudioGraphOptions;
|
|
@@ -80,7 +81,7 @@ type FeaturePropKeys = (typeof FEATURE_KEYS)[number];
|
|
|
80
81
|
type FeatureProps = Pick<AudioStreamProcessorProps, FeaturePropKeys>;
|
|
81
82
|
export declare const updateFeatureProps: (constraints: MediaDeviceRequest["audio"], props: FeatureProps) => FeatureProps;
|
|
82
83
|
/**
|
|
83
|
-
* Create
|
|
84
|
+
* Create an Audio Stream Processor and will own the stream passed-in
|
|
84
85
|
*/
|
|
85
|
-
export declare const createAudioStreamProcess: ({ analyzerUpdateFrequency, audioGraphOptions, audioSignalDetectionDuration, clock, createNodes, denoiseParams, fftSize, onAudioSignalDetected, onVoiceActivityDetected, shouldEnable, silentThreshold, throttleMs, label, }: AudioProcessOptions & ThrottleOptions) => TrackProcessor;
|
|
86
|
+
export declare const createAudioStreamProcess: ({ analyzerUpdateFrequency, audioGraphOptions, audioSignalDetectionDuration, clock, createNodes, denoiseParams, fftSize, onAudioSignalDetected, onVoiceActivityDetected, shouldEnable, signals, silentThreshold, throttleMs, label, }: AudioProcessOptions & ThrottleOptions) => TrackProcessor;
|
|
86
87
|
export {};
|
package/dist/audioProcessor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { extractConstraintsWithKeys } from '@pexip/media-control';
|
|
2
2
|
import { createQueue, isEmpty, assert } from '@pexip/utils';
|
|
3
|
-
import {
|
|
3
|
+
import { avg, createAnalyzerSubscribableGraphNode, createAudioGraph, createAudioGraphProxy, createAudioSignalDetector, createDenoiseWorkletGraphNode, createStreamDestinationGraphNode, createStreamSourceGraphNode, createVADetector, createVoiceDetectorFromProbability, createVoiceDetectorFromTimeData, } from '@pexip/media-processor';
|
|
4
4
|
import { PROCESSOR_LABELS } from './constants';
|
|
5
5
|
import { logger } from './logger';
|
|
6
6
|
import { isAudioContentHint } from './typeGuard';
|
|
@@ -54,12 +54,12 @@ export const updateFeatureProps = (constraints, props) => {
|
|
|
54
54
|
}, {});
|
|
55
55
|
};
|
|
56
56
|
/**
|
|
57
|
-
* Create
|
|
57
|
+
* Create an Audio Stream Processor and will own the stream passed-in
|
|
58
58
|
*/
|
|
59
59
|
export const createAudioStreamProcess = ({ analyzerUpdateFrequency = 0.5, // 0.5 Hz
|
|
60
60
|
audioGraphOptions, audioSignalDetectionDuration = 4.0, // 4 seconds
|
|
61
61
|
clock, createNodes, denoiseParams, fftSize = 2048, // FFT size
|
|
62
|
-
onAudioSignalDetected, onVoiceActivityDetected, shouldEnable, silentThreshold = 10.0 / 32767, // At least one LSB 16-bit data (compare is on absolute value).
|
|
62
|
+
onAudioSignalDetected, onVoiceActivityDetected, shouldEnable, signals, silentThreshold = 10.0 / 32767, // At least one LSB 16-bit data (compare is on absolute value).
|
|
63
63
|
throttleMs = 3000, // 3 seconds
|
|
64
64
|
label = PROCESSOR_LABELS.AudioProcessor, }) => {
|
|
65
65
|
const props = {
|
|
@@ -198,11 +198,6 @@ label = PROCESSOR_LABELS.AudioProcessor, }) => {
|
|
|
198
198
|
props.analyzer = undefined;
|
|
199
199
|
props.audioGraph = undefined;
|
|
200
200
|
};
|
|
201
|
-
const mute = (mute) => {
|
|
202
|
-
if (track) {
|
|
203
|
-
track.enabled = !mute;
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
201
|
return createMediaTrack({
|
|
207
202
|
label,
|
|
208
203
|
kind: 'audioinput',
|
|
@@ -210,9 +205,18 @@ label = PROCESSOR_LABELS.AudioProcessor, }) => {
|
|
|
210
205
|
previousMediaTrack: prevMediaTrack,
|
|
211
206
|
input: prevMediaTrack.input,
|
|
212
207
|
expectedInput: prevMediaTrack.expectedInput,
|
|
213
|
-
|
|
208
|
+
overrideMute: true,
|
|
209
|
+
mute(toMute) {
|
|
210
|
+
if (track) {
|
|
211
|
+
track.enabled = !toMute;
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
get muted() {
|
|
215
|
+
return !track.enabled;
|
|
216
|
+
},
|
|
214
217
|
track,
|
|
215
218
|
release,
|
|
219
|
+
signals,
|
|
216
220
|
applyConstraints: async (constraints) => {
|
|
217
221
|
if (isEmpty(constraints)) {
|
|
218
222
|
return;
|
package/dist/media.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createIndexedDevices,
|
|
1
|
+
import { createIndexedDevices, 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';
|
|
@@ -283,28 +283,6 @@ export const createMedia = ({ getMuteState, signals, audioProcessors, videoProce
|
|
|
283
283
|
props.devices = createIndexedDevices(await getDevices());
|
|
284
284
|
return props.devices;
|
|
285
285
|
};
|
|
286
|
-
const syncMuteState = (tracks) => {
|
|
287
|
-
const inputMuted = getMuteState();
|
|
288
|
-
for (const track of tracks) {
|
|
289
|
-
switch (track.kind) {
|
|
290
|
-
case 'audioinput': {
|
|
291
|
-
track.mute(inputMuted.audio);
|
|
292
|
-
// FIXME: this can fire too often, but we want to make sure we signal initial state as well.
|
|
293
|
-
// We should only fire this when needed.
|
|
294
|
-
signals.onAudioMuteStateChanged?.emit(track.muted);
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
case 'videoinput':
|
|
298
|
-
track.mute(inputMuted.video);
|
|
299
|
-
// FIXME: this can fire too often, but we want to make sure we signal initial state as well.
|
|
300
|
-
// We should only fire this when needed.
|
|
301
|
-
signals.onVideoMuteStateChanged?.emit(track.muted);
|
|
302
|
-
break;
|
|
303
|
-
default:
|
|
304
|
-
break;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
286
|
const processMedia = createMediaProcessor({
|
|
309
287
|
audioProcessors,
|
|
310
288
|
videoProcessors,
|
|
@@ -332,13 +310,14 @@ export const createMedia = ({ getMuteState, signals, audioProcessors, videoProce
|
|
|
332
310
|
});
|
|
333
311
|
const processAndUpdateMedia = async (media, tracks) => {
|
|
334
312
|
const processedTracks = await processMedia(tracks);
|
|
335
|
-
// Sync mute state to processed tracks
|
|
336
|
-
syncMuteState(processedTracks);
|
|
337
313
|
for (const [idx, track] of processedTracks.entries()) {
|
|
338
314
|
const originTrack = tracks.at(idx);
|
|
339
315
|
assert(originTrack, 'Processed track should always has the original track in the same order');
|
|
340
316
|
// Only replace track when they are not the same
|
|
341
317
|
if (originTrack.id !== track.id) {
|
|
318
|
+
if (originTrack.muted !== undefined) {
|
|
319
|
+
track.mute(originTrack.muted);
|
|
320
|
+
}
|
|
342
321
|
media.removeTrack(originTrack);
|
|
343
322
|
media.addTrack(track);
|
|
344
323
|
}
|
|
@@ -351,19 +330,12 @@ export const createMedia = ({ getMuteState, signals, audioProcessors, videoProce
|
|
|
351
330
|
shouldDiscardMedia: () => props.discardMedia,
|
|
352
331
|
getDefaultConstraints,
|
|
353
332
|
onMediaTracksChanged: (media, tracks) => {
|
|
354
|
-
|
|
355
|
-
syncMuteState(tracks);
|
|
333
|
+
const inputMuted = getMuteState();
|
|
356
334
|
for (const track of tracks) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
unsubscribe();
|
|
362
|
-
},
|
|
363
|
-
mute: signals.onStreamTrackMuted?.emit,
|
|
364
|
-
unmute: signals.onStreamTrackUnmuted?.emit,
|
|
365
|
-
});
|
|
366
|
-
}
|
|
335
|
+
// Sync mute state to input tracks
|
|
336
|
+
track.mute(track.kind === 'audioinput'
|
|
337
|
+
? inputMuted.audio
|
|
338
|
+
: inputMuted.video);
|
|
367
339
|
}
|
|
368
340
|
logger.debug({ media, tracks }, 'Update media tracks');
|
|
369
341
|
props.media = media;
|
|
@@ -12,11 +12,13 @@ export interface PreviewEventHandler {
|
|
|
12
12
|
audioInputError?: EventErrorCallback;
|
|
13
13
|
applyChangesError?: EventErrorCallback;
|
|
14
14
|
revertChangesError?: EventErrorCallback;
|
|
15
|
+
audioMuted?: EventCallback<boolean | undefined>;
|
|
16
|
+
audioSuspended?: EventCallback<boolean | undefined>;
|
|
17
|
+
updatingMain?: EventCallback<boolean>;
|
|
15
18
|
updatingPreviewAudio?: EventCallback<boolean>;
|
|
16
19
|
updatingPreviewVideo?: EventCallback<boolean>;
|
|
17
|
-
audioMuted?: EventCallback<boolean | undefined>;
|
|
18
20
|
videoMuted?: EventCallback<boolean | undefined>;
|
|
19
|
-
|
|
21
|
+
videoSuspended?: EventCallback<boolean | undefined>;
|
|
20
22
|
unsubscribeMain?: Unsubscribe;
|
|
21
23
|
}
|
|
22
24
|
export interface PreviewStreamParams {
|
|
@@ -24,6 +26,7 @@ export interface PreviewStreamParams {
|
|
|
24
26
|
getCurrentMedia: () => Media | undefined;
|
|
25
27
|
updateMainStream: (request: MediaDeviceRequest) => Promise<void>;
|
|
26
28
|
mainMediaSignal: MediaSignals['onMediaChanged'];
|
|
29
|
+
signals: MediaSignals;
|
|
27
30
|
onEnded?: () => void;
|
|
28
31
|
fftSize?: number;
|
|
29
32
|
queueOptions?: Partial<AsyncQueueOptions>;
|
|
@@ -76,8 +79,10 @@ export interface PreviewStreamController {
|
|
|
76
79
|
onApplyChangesError(callback: EventErrorCallback): Unsubscribe;
|
|
77
80
|
onRevertChangesError(callback: EventErrorCallback): Unsubscribe;
|
|
78
81
|
onAudioMuted(callback: EventCallback<boolean | undefined>): Unsubscribe;
|
|
82
|
+
onAudioSuspended(callback: EventCallback<boolean | undefined>): Unsubscribe;
|
|
79
83
|
onVideoMuted(callback: EventCallback<boolean | undefined>): Unsubscribe;
|
|
84
|
+
onVideoSuspended(callback: EventCallback<boolean | undefined>): Unsubscribe;
|
|
80
85
|
}
|
|
81
|
-
export declare const createPreviewStreamController: ({ getCurrentDevices, getCurrentMedia, updateMainStream, onEnded, mainMediaSignal, queueOptions, audioProcessors, videoProcessors, getDefaultConstraints, }: PreviewStreamParams) => PreviewStreamController;
|
|
86
|
+
export declare const createPreviewStreamController: ({ getCurrentDevices, getCurrentMedia, updateMainStream, onEnded, mainMediaSignal, queueOptions, audioProcessors, videoProcessors, getDefaultConstraints, signals, }: PreviewStreamParams) => PreviewStreamController;
|
|
82
87
|
export type CreatePreviewStreamController = typeof createPreviewStreamController;
|
|
83
88
|
export {};
|
|
@@ -4,7 +4,6 @@ import { AUDIO_SETTINGS_KEYS, VIDEO_SETTINGS_KEYS, createMediaProcessor, hasSett
|
|
|
4
4
|
import { isMedia } from './typeGuard';
|
|
5
5
|
import { createModuleLogger } from './logger';
|
|
6
6
|
import { createGetUserMediaProcess, requestUserMediaWithRetry, } from './userMedia';
|
|
7
|
-
import { createMediaSignals } from './signals';
|
|
8
7
|
import { createMediaUpdater } from './media';
|
|
9
8
|
const DEFAULT_QUEUE_DELAY_MS = 100;
|
|
10
9
|
const DEFAULT_QUEUE_DROP_LAST = false;
|
|
@@ -52,7 +51,7 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
|
|
|
52
51
|
throttleInMS: DEFAULT_QUEUE_THROTTLE_MS,
|
|
53
52
|
delayInMS: DEFAULT_QUEUE_DELAY_MS,
|
|
54
53
|
dropLast: DEFAULT_QUEUE_DROP_LAST,
|
|
55
|
-
}, audioProcessors, videoProcessors, getDefaultConstraints = () => ({}), }) => {
|
|
54
|
+
}, audioProcessors, videoProcessors, getDefaultConstraints = () => ({}), signals, }) => {
|
|
56
55
|
const queue = createAsyncQueue(queueOptions);
|
|
57
56
|
const eventHandlers = {};
|
|
58
57
|
const internalProps = {
|
|
@@ -62,15 +61,7 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
|
|
|
62
61
|
updatingMain: false,
|
|
63
62
|
discardMedia: false,
|
|
64
63
|
initialized: false,
|
|
65
|
-
signals
|
|
66
|
-
'onAddTrack',
|
|
67
|
-
'onAudioMuteStateChanged',
|
|
68
|
-
'onRemoveTrack',
|
|
69
|
-
'onStatusChanged',
|
|
70
|
-
'onUpdatingAudio',
|
|
71
|
-
'onUpdatingVideo',
|
|
72
|
-
'onVideoMuteStateChanged',
|
|
73
|
-
], 'PreviewStreamController'),
|
|
64
|
+
signals,
|
|
74
65
|
};
|
|
75
66
|
const logger = createModuleLogger({
|
|
76
67
|
module: 'PreviewStreamController',
|
|
@@ -129,11 +120,29 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
|
|
|
129
120
|
},
|
|
130
121
|
});
|
|
131
122
|
const subscriptions = [
|
|
132
|
-
props.signals.
|
|
133
|
-
|
|
123
|
+
props.signals.onMediaTrackMuted?.add(track => {
|
|
124
|
+
if (track.kind === 'audioinput') {
|
|
125
|
+
eventHandlers.audioMuted?.(track.muted);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
eventHandlers.videoMuted?.(track.muted);
|
|
129
|
+
}
|
|
130
|
+
}),
|
|
131
|
+
props.signals.onMediaTrackSuspended?.add(track => {
|
|
132
|
+
if (track.kind === 'audioinput') {
|
|
133
|
+
eventHandlers.audioSuspended?.(track.suspended);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
eventHandlers.videoSuspended?.(track.suspended);
|
|
137
|
+
}
|
|
134
138
|
}),
|
|
135
|
-
props.signals.
|
|
136
|
-
|
|
139
|
+
props.signals.onMediaTrackResumed?.add(track => {
|
|
140
|
+
if (track.kind === 'audioinput') {
|
|
141
|
+
eventHandlers.audioSuspended?.(track.suspended);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
eventHandlers.videoSuspended?.(track.suspended);
|
|
145
|
+
}
|
|
137
146
|
}),
|
|
138
147
|
props.signals.onAddTrack?.add(track => {
|
|
139
148
|
switch (track.kind) {
|
|
@@ -228,7 +237,7 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
|
|
|
228
237
|
return;
|
|
229
238
|
}
|
|
230
239
|
try {
|
|
231
|
-
const clonedMedia = mainMedia.clone(props.signals);
|
|
240
|
+
const clonedMedia = mainMedia.clone(props.signals, 'Preview');
|
|
232
241
|
props.media = clonedMedia;
|
|
233
242
|
queue.enqueue(async () => {
|
|
234
243
|
await processAndUpdateMedia(clonedMedia, clonedMedia.getTracks(), true);
|
|
@@ -364,7 +373,7 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
|
|
|
364
373
|
const audioSettings = extractFeaturesToConstraints(AUDIO_SETTINGS_KEYS, previewAudio);
|
|
365
374
|
// Omit the width & height settings from the preview so that the one
|
|
366
375
|
// from the main can be applied
|
|
367
|
-
const { width, height, ...videoSettings } = extractFeaturesToConstraints(VIDEO_SETTINGS_KEYS, previewVideo);
|
|
376
|
+
const { width: _width, height: _height, ...videoSettings } = extractFeaturesToConstraints(VIDEO_SETTINGS_KEYS, previewVideo);
|
|
368
377
|
logger.info({ audioInput, videoInput, audioSettings, videoSettings }, 'Apply changes to main');
|
|
369
378
|
try {
|
|
370
379
|
await replaceMainStream({
|
|
@@ -484,6 +493,8 @@ export const createPreviewStreamController = ({ getCurrentDevices, getCurrentMed
|
|
|
484
493
|
onUpdatingMain: toEvenHandler('updatingMain'),
|
|
485
494
|
onAudioMuted: toEvenHandler('audioMuted'),
|
|
486
495
|
onVideoMuted: toEvenHandler('videoMuted'),
|
|
496
|
+
onAudioSuspended: toEvenHandler('audioSuspended'),
|
|
497
|
+
onVideoSuspended: toEvenHandler('videoSuspended'),
|
|
487
498
|
applyChanges,
|
|
488
499
|
revertChanges,
|
|
489
500
|
cleanup,
|
package/dist/types.d.ts
CHANGED
|
@@ -127,8 +127,9 @@ export interface Media extends MediaAttributes {
|
|
|
127
127
|
getAudioTracks(): MediaTrack[];
|
|
128
128
|
getVideoTracks(): MediaTrack[];
|
|
129
129
|
addTrack(track: MediaTrack): void;
|
|
130
|
+
isCurrentTrack(track: MediaTrack): boolean;
|
|
130
131
|
removeTrack(track: MediaTrack): void;
|
|
131
|
-
clone(signals?: MediaSignals): Media;
|
|
132
|
+
clone(signals?: MediaSignals, label?: string): Media;
|
|
132
133
|
}
|
|
133
134
|
export interface MediaInit {
|
|
134
135
|
constraints: MediaDeviceRequest;
|
|
@@ -149,26 +150,35 @@ export interface MediaTrack {
|
|
|
149
150
|
* `track` is available otherwise it uses the `id` from the construction or the
|
|
150
151
|
* `kind`. @see MediaTrackInit
|
|
151
152
|
*/
|
|
152
|
-
id: string;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
readonly id: string;
|
|
154
|
+
/**
|
|
155
|
+
* It maps to MediaStreamTrack['readyState']
|
|
156
|
+
* When `true` the track's `readyState` is `ended`, `false` means `live` otherwise the track is not available
|
|
157
|
+
*/
|
|
158
|
+
readonly stopped?: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* Indicate that the media has been suspended by the User Agent, and it could happen at any time.
|
|
161
|
+
* `undefined` means the track is not available
|
|
162
|
+
*/
|
|
163
|
+
readonly suspended?: boolean;
|
|
164
|
+
readonly kind: 'audioinput' | 'videoinput';
|
|
165
|
+
readonly label?: string;
|
|
166
|
+
readonly previousMediaTrack?: MediaTrack;
|
|
157
167
|
/**
|
|
158
168
|
* The MediaStreamTrack object representing the track
|
|
159
169
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack
|
|
160
170
|
*/
|
|
161
|
-
track?: MediaStreamTrack;
|
|
171
|
+
readonly track?: MediaStreamTrack;
|
|
162
172
|
/**
|
|
163
173
|
* The `MediaDeviceInfo` object representing the device that the track is
|
|
164
174
|
* connected to. @see https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo
|
|
165
175
|
*/
|
|
166
|
-
input?: MediaDeviceInfoLike;
|
|
176
|
+
readonly input?: MediaDeviceInfoLike;
|
|
167
177
|
/**
|
|
168
178
|
* The `MediaDeviceInfo` object representing the device that the track is
|
|
169
179
|
* expected to be connected to. @see https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo
|
|
170
180
|
*/
|
|
171
|
-
expectedInput?: MediaDeviceInfoLike;
|
|
181
|
+
readonly expectedInput?: MediaDeviceInfoLike;
|
|
172
182
|
/**
|
|
173
183
|
* The current mute state of the track. `undefined` means the track is not available
|
|
174
184
|
*/
|
|
@@ -176,13 +186,20 @@ export interface MediaTrack {
|
|
|
176
186
|
/**
|
|
177
187
|
* The source track that it is originated from
|
|
178
188
|
*/
|
|
179
|
-
source: MediaTrack;
|
|
180
|
-
|
|
189
|
+
readonly source: MediaTrack;
|
|
190
|
+
/**
|
|
191
|
+
* Mute the track
|
|
192
|
+
*
|
|
193
|
+
* @param toMute - `true` to mute the track, `false` to unmute
|
|
194
|
+
* @param soft - `true` to indicate a soft mute i.e. setting the track to NOT `enabled`
|
|
195
|
+
* `false` or `undefined` for default mute
|
|
196
|
+
*/
|
|
197
|
+
mute(toMute: boolean, soft?: boolean): void;
|
|
181
198
|
getSettings(): ExtendedMediaTrackSettings;
|
|
182
199
|
getConstraints(): InputDeviceConstraint;
|
|
183
200
|
applyConstraints(constraints: InputDeviceConstraint): Promise<void>;
|
|
184
201
|
release(): Promise<void>;
|
|
185
|
-
clone(signals?: MediaSignals): MediaTrack;
|
|
202
|
+
clone(signals?: MediaSignals, label?: string): MediaTrack;
|
|
186
203
|
toJSON(): unknown;
|
|
187
204
|
}
|
|
188
205
|
export interface MediaTrackInit extends Partial<Omit<MediaTrack, 'mute'>> {
|
|
@@ -192,7 +209,15 @@ export interface MediaTrackInit extends Partial<Omit<MediaTrack, 'mute'>> {
|
|
|
192
209
|
constraints: InputDeviceConstraint;
|
|
193
210
|
overrideMute?: boolean;
|
|
194
211
|
signals?: MediaSignals;
|
|
195
|
-
|
|
212
|
+
/**
|
|
213
|
+
* Overriding Mute the track
|
|
214
|
+
*
|
|
215
|
+
* @param toMute - `true` to mute the track, `false` to unmute
|
|
216
|
+
* @param self - The media track itself
|
|
217
|
+
* @param soft - `true` to indicate a soft mute i.e. setting the track to NOT `enabled`
|
|
218
|
+
* `false` or `undefined` for default mute
|
|
219
|
+
*/
|
|
220
|
+
mute?: (toMute: boolean, self: MediaTrack, soft?: boolean) => void;
|
|
196
221
|
}
|
|
197
222
|
export type Process<T> = (a: T) => Promise<MediaTrack>;
|
|
198
223
|
export type TrackProcessor = Process<MediaTrack>;
|
|
@@ -521,44 +546,16 @@ export type VideoRenderParams = Omit<RendererOptions, 'effects'> & {
|
|
|
521
546
|
tilt?: boolean;
|
|
522
547
|
zoom?: boolean;
|
|
523
548
|
};
|
|
524
|
-
export interface StreamTrackSignals {
|
|
525
|
-
/**
|
|
526
|
-
* MediaStreamTrack events: mute
|
|
527
|
-
* https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack#events
|
|
528
|
-
*/
|
|
529
|
-
onStreamTrackMuted: Signal<MediaStreamTrack>;
|
|
530
|
-
/**
|
|
531
|
-
* MediaStreamTrack events: unmute
|
|
532
|
-
* https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack#events
|
|
533
|
-
*/
|
|
534
|
-
onStreamTrackUnmuted: Signal<MediaStreamTrack>;
|
|
535
|
-
/**
|
|
536
|
-
* MediaStreamTrack events: ended
|
|
537
|
-
* https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack#events
|
|
538
|
-
*/
|
|
539
|
-
onStreamTrackEnded: Signal<MediaStreamTrack>;
|
|
540
|
-
/**
|
|
541
|
-
* Emit `MediaStreamTrack` whenever a call to `Media['muteAudio']` or
|
|
542
|
-
* `Media['muteVideo']`
|
|
543
|
-
*/
|
|
544
|
-
onStreamTrackEnabled: Signal<MediaStreamTrack>;
|
|
545
|
-
}
|
|
546
|
-
/**
|
|
547
|
-
* Track state signals that used for the final track that the media pipeline
|
|
548
|
-
* produced
|
|
549
|
-
*/
|
|
550
|
-
export type StreamTrackSignalFinals = {
|
|
551
|
-
[key in keyof StreamTrackSignals as `${key}Final`]: Signal<MediaStreamTrack>;
|
|
552
|
-
};
|
|
553
549
|
export interface MediaChangesSignals {
|
|
554
550
|
onAddTrack: Signal<MediaStreamTrack>;
|
|
555
|
-
onAudioMuteStateChanged: Signal<boolean | undefined>;
|
|
556
|
-
onTrackReleased: Signal<MediaStreamTrack>;
|
|
557
551
|
onDevicesChanged: Signal<IndexedDevices>;
|
|
558
552
|
onMediaChanged: Signal<Media | undefined>;
|
|
553
|
+
onMediaTrackMuted: Signal<MediaTrack>;
|
|
554
|
+
onMediaTrackResumed: Signal<MediaTrack>;
|
|
555
|
+
onMediaTrackStopped: Signal<MediaTrack>;
|
|
556
|
+
onMediaTrackSuspended: Signal<MediaTrack>;
|
|
559
557
|
onRemoveTrack: Signal<MediaStreamTrack>;
|
|
560
558
|
onStatusChanged: Signal<UserMediaStatus>;
|
|
561
|
-
onVideoMuteStateChanged: Signal<boolean | undefined>;
|
|
562
559
|
onUpdatingAudio: Signal<boolean>;
|
|
563
560
|
onUpdatingVideo: Signal<boolean>;
|
|
564
561
|
}
|
|
@@ -566,7 +563,7 @@ export interface AudioDetectionSignals {
|
|
|
566
563
|
onVAD: Signal<undefined>;
|
|
567
564
|
onSilentDetected: Signal<boolean>;
|
|
568
565
|
}
|
|
569
|
-
export type MediaSignalsOptional = Omit<Partial<MediaChangesSignals>, 'onMediaChanged'
|
|
566
|
+
export type MediaSignalsOptional = Omit<Partial<MediaChangesSignals>, 'onMediaChanged'>;
|
|
570
567
|
export type MediaSignalsRequired = Pick<MediaChangesSignals, 'onMediaChanged'> & AudioDetectionSignals;
|
|
571
568
|
export type MediaSignals = MediaSignalsRequired & MediaSignalsOptional;
|
|
572
569
|
export declare enum DeniedDevices {
|
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 { buildMedia, createMediaTrack, findExpectedInput,
|
|
4
|
+
import { buildMedia, createMediaTrack, findExpectedInput, makeDeriveDeviceStatus, } from './utils';
|
|
5
5
|
import { logger } from './logger';
|
|
6
6
|
import { isUnknownError } from './status';
|
|
7
7
|
import { PROCESSOR_LABELS } from './constants';
|
|
@@ -473,7 +473,9 @@ export const createGetUserMediaProcess = ({ getCurrentDevices, getUserMedia, sco
|
|
|
473
473
|
return async ({ constraints, permission, originalConstraints, currentMedia, }) => {
|
|
474
474
|
// Release the current track(s)
|
|
475
475
|
if (currentMedia) {
|
|
476
|
-
await Promise.all(
|
|
476
|
+
await Promise.all(
|
|
477
|
+
// biome-ignore lint/suspicious/useIterableCallbackReturn: False positive
|
|
478
|
+
currentMedia.getTracks().map(track => {
|
|
477
479
|
switch (track.kind) {
|
|
478
480
|
case 'audioinput':
|
|
479
481
|
// When the constraints is undefined we should do nothing
|
|
@@ -556,20 +558,33 @@ export const createGetUserMediaProcess = ({ getCurrentDevices, getUserMedia, sco
|
|
|
556
558
|
let audioMediaTrack;
|
|
557
559
|
let videoMediaTrack;
|
|
558
560
|
const extractContentHint = extractConstraintsWithKeys(['contentHint']);
|
|
561
|
+
let trackReleaseTimeoutID = 0;
|
|
559
562
|
/**
|
|
560
563
|
* Release the track as mute
|
|
561
564
|
*/
|
|
562
|
-
const releaseTrackAsMute = async (track, toMute) => {
|
|
565
|
+
const releaseTrackAsMute = async (track, toMute, soft) => {
|
|
563
566
|
if (!track?.track || track.muted === toMute) {
|
|
564
567
|
return;
|
|
565
568
|
}
|
|
566
569
|
track.track.enabled = !toMute;
|
|
570
|
+
if (soft) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
567
573
|
if (toMute) {
|
|
568
574
|
if (!track.stopped) {
|
|
569
|
-
|
|
575
|
+
// Track cannot be stopped immediately as we need to ensure
|
|
576
|
+
// the last frame is black to avoid frozen frame in a gateway call
|
|
577
|
+
trackReleaseTimeoutID = window.setTimeout(() => {
|
|
578
|
+
track.release();
|
|
579
|
+
trackReleaseTimeoutID = 0;
|
|
580
|
+
}, 500);
|
|
570
581
|
}
|
|
571
582
|
}
|
|
572
583
|
else {
|
|
584
|
+
if (trackReleaseTimeoutID) {
|
|
585
|
+
window.clearTimeout(trackReleaseTimeoutID);
|
|
586
|
+
trackReleaseTimeoutID = 0;
|
|
587
|
+
}
|
|
573
588
|
if (track.stopped) {
|
|
574
589
|
await updateMedia({
|
|
575
590
|
[track.kind === 'audioinput' ? 'audio' : 'video']: track.getConstraints(),
|
|
@@ -586,7 +601,7 @@ export const createGetUserMediaProcess = ({ getCurrentDevices, getUserMedia, sco
|
|
|
586
601
|
audioTrack.contentHint = contentHint ?? '';
|
|
587
602
|
}
|
|
588
603
|
audioMediaTrack = createMediaTrack({
|
|
589
|
-
label: PROCESSOR_LABELS.GetUserMedia,
|
|
604
|
+
label: [scope, PROCESSOR_LABELS.GetUserMedia].join('|'),
|
|
590
605
|
kind: 'audioinput',
|
|
591
606
|
track: audioTrack,
|
|
592
607
|
input: audioInput,
|
|
@@ -605,17 +620,19 @@ export const createGetUserMediaProcess = ({ getCurrentDevices, getUserMedia, sco
|
|
|
605
620
|
videoTrack.contentHint = contentHint ?? '';
|
|
606
621
|
}
|
|
607
622
|
videoMediaTrack = createMediaTrack({
|
|
608
|
-
label: PROCESSOR_LABELS.GetUserMedia,
|
|
623
|
+
label: [scope, PROCESSOR_LABELS.GetUserMedia].join('|'),
|
|
609
624
|
kind: 'videoinput',
|
|
610
625
|
track: videoTrack,
|
|
611
626
|
input: videoInput,
|
|
612
627
|
expectedInput: findExpectedInput(devices, constraints.video, videoInput, 'videoinput'),
|
|
613
|
-
overrideMute
|
|
628
|
+
get overrideMute() {
|
|
629
|
+
return stopVideoTrackAsMute();
|
|
630
|
+
},
|
|
614
631
|
get muted() {
|
|
615
|
-
return
|
|
632
|
+
return !videoTrack?.enabled;
|
|
616
633
|
},
|
|
617
|
-
mute(toMute, self) {
|
|
618
|
-
releaseTrackAsMute(self, toMute);
|
|
634
|
+
mute(toMute, self, soft) {
|
|
635
|
+
releaseTrackAsMute(self, toMute, soft);
|
|
619
636
|
},
|
|
620
637
|
constraints: constraints.video,
|
|
621
638
|
signals,
|
package/dist/utils.js
CHANGED
|
@@ -19,10 +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
|
|
23
|
-
(trackInit
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
assert(!hasOwn(trackInit, 'mute') ||
|
|
23
|
+
(hasOwn(trackInit, 'mute') && hasOwn(trackInit, 'overrideMute')), 'Inconsistent overrideMute mute()');
|
|
24
|
+
assert(!hasOwn(trackInit, 'muted') ||
|
|
25
|
+
(hasOwn(trackInit, 'muted') && hasOwn(trackInit, 'overrideMute')), 'Inconsistent overrideMute muted');
|
|
26
26
|
const currentConstraints = typeof trackInit.constraints === 'boolean' ? {} : trackInit.constraints;
|
|
27
27
|
// Find the original source track through the linked list
|
|
28
28
|
let sourceMediaTrack = trackInit.previousMediaTrack;
|
|
@@ -45,6 +45,25 @@ export const createMediaTrack = (trackInit) => {
|
|
|
45
45
|
...(trackInit.getSettings?.() ?? {}),
|
|
46
46
|
};
|
|
47
47
|
};
|
|
48
|
+
// Suscribe track events
|
|
49
|
+
let trackUnsubscribe;
|
|
50
|
+
if (trackInit.track) {
|
|
51
|
+
trackUnsubscribe = createStreamTrackEventSubscriptions(trackInit.track, {
|
|
52
|
+
ended(track) {
|
|
53
|
+
assert(track.id === trackInit.track?.id, 'Same Track');
|
|
54
|
+
void mediaTrack.release();
|
|
55
|
+
trackInit.signals?.onMediaTrackStopped?.emit(mediaTrack);
|
|
56
|
+
},
|
|
57
|
+
mute(track) {
|
|
58
|
+
assert(track.id === trackInit.track?.id, 'Same Track');
|
|
59
|
+
trackInit.signals?.onMediaTrackSuspended?.emit(mediaTrack);
|
|
60
|
+
},
|
|
61
|
+
unmute(track) {
|
|
62
|
+
assert(track.id === trackInit.track?.id, 'Same Track');
|
|
63
|
+
trackInit.signals?.onMediaTrackResumed?.emit(mediaTrack);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
48
67
|
const mediaTrack = {
|
|
49
68
|
get kind() {
|
|
50
69
|
return trackInit.kind;
|
|
@@ -74,36 +93,50 @@ export const createMediaTrack = (trackInit) => {
|
|
|
74
93
|
if (trackInit.overrideMute) {
|
|
75
94
|
return trackInit.muted;
|
|
76
95
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return isTrackMuted(trackInit.track);
|
|
96
|
+
if (trackInit.track) {
|
|
97
|
+
return !trackInit.track.enabled;
|
|
80
98
|
}
|
|
81
|
-
return
|
|
99
|
+
return undefined;
|
|
82
100
|
},
|
|
83
101
|
get stopped() {
|
|
84
|
-
return trackInit.track
|
|
102
|
+
return trackInit.track && isTrackEnded(trackInit.track);
|
|
103
|
+
},
|
|
104
|
+
get suspended() {
|
|
105
|
+
// Any track that is suspended in the chain will cause this track to be suspended
|
|
106
|
+
return (trackInit.previousMediaTrack?.suspended ||
|
|
107
|
+
trackInit.track?.muted);
|
|
85
108
|
},
|
|
86
109
|
get source() {
|
|
87
|
-
return sourceMediaTrack ??
|
|
110
|
+
return sourceMediaTrack ?? this;
|
|
88
111
|
},
|
|
89
|
-
mute(toMute) {
|
|
112
|
+
mute(toMute, soft) {
|
|
113
|
+
if (this.muted === toMute) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
90
116
|
if (trackInit.overrideMute) {
|
|
91
|
-
|
|
117
|
+
trackInit.mute?.(toMute, this, soft);
|
|
92
118
|
}
|
|
93
|
-
|
|
94
|
-
trackInit.track
|
|
119
|
+
else {
|
|
120
|
+
if (trackInit.track) {
|
|
121
|
+
trackInit.track.enabled = !toMute;
|
|
122
|
+
}
|
|
123
|
+
trackInit.previousMediaTrack?.mute(toMute, soft);
|
|
95
124
|
}
|
|
96
|
-
trackInit.
|
|
125
|
+
trackInit.signals?.onMediaTrackMuted?.emit(this);
|
|
97
126
|
},
|
|
98
127
|
getSettings,
|
|
99
128
|
getConstraints() {
|
|
100
129
|
return getConstraints();
|
|
101
130
|
},
|
|
102
|
-
clone(signals) {
|
|
131
|
+
clone(signals, label) {
|
|
103
132
|
return createMediaTrack({
|
|
104
|
-
|
|
133
|
+
kind: this.kind,
|
|
134
|
+
label: [label, this.label]
|
|
135
|
+
.flatMap(a => (a ? [a] : []))
|
|
136
|
+
.join('|'),
|
|
137
|
+
input: this.input,
|
|
105
138
|
constraints: getConstraints(),
|
|
106
|
-
track:
|
|
139
|
+
track: this.track?.clone(),
|
|
107
140
|
// When Cloning a MediaTrack, overrideMute should be turned off since
|
|
108
141
|
// it might corrupt the logic of its original media pipeline
|
|
109
142
|
overrideMute: false,
|
|
@@ -146,23 +179,20 @@ export const createMediaTrack = (trackInit) => {
|
|
|
146
179
|
Object.assign(currentConstraints, resolvedConstraints);
|
|
147
180
|
},
|
|
148
181
|
async release() {
|
|
182
|
+
trackUnsubscribe?.();
|
|
149
183
|
trackInit.track?.stop();
|
|
150
|
-
if (trackInit.track) {
|
|
151
|
-
trackInit.signals?.onTrackReleased?.emit(trackInit.track);
|
|
152
|
-
trackInit.signals?.[trackInit.track.kind === 'audio'
|
|
153
|
-
? 'onAudioMuteStateChanged'
|
|
154
|
-
: 'onVideoMuteStateChanged']?.emit(undefined);
|
|
155
|
-
}
|
|
156
|
-
await trackInit.previousMediaTrack?.release();
|
|
157
184
|
await trackInit.release?.();
|
|
185
|
+
await trackInit.previousMediaTrack?.release();
|
|
186
|
+
trackInit.signals?.onMediaTrackStopped?.emit(this);
|
|
158
187
|
},
|
|
159
188
|
toJSON() {
|
|
160
189
|
return {
|
|
161
|
-
label:
|
|
162
|
-
track:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
190
|
+
label: this.label,
|
|
191
|
+
track: this.track,
|
|
192
|
+
input: this.input,
|
|
193
|
+
previousMediaTrack: this.previousMediaTrack,
|
|
194
|
+
constraints: this.getConstraints(),
|
|
195
|
+
settings: this.getSettings(),
|
|
166
196
|
};
|
|
167
197
|
},
|
|
168
198
|
};
|
|
@@ -231,6 +261,10 @@ export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevic
|
|
|
231
261
|
status: mediaInit.status,
|
|
232
262
|
devices: mediaInit.devices,
|
|
233
263
|
stream: mediaInit.stream ?? new MediaStream(),
|
|
264
|
+
// `undefined` is used to avoid double state overriding,
|
|
265
|
+
// because 2 subsequent `susppend` events could invalidate the previous assignment
|
|
266
|
+
audioAlreadyMuted: undefined,
|
|
267
|
+
videoAlreadyMuted: undefined,
|
|
234
268
|
audioTrack: mediaInit.tracks
|
|
235
269
|
.flatMap(track => {
|
|
236
270
|
return track.kind === 'audioinput' ? [track] : [];
|
|
@@ -240,73 +274,76 @@ export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevic
|
|
|
240
274
|
.flatMap(track => (track.kind === 'videoinput' ? [track] : []))
|
|
241
275
|
.at(0),
|
|
242
276
|
};
|
|
243
|
-
const subscribeTrackEvents = (track) => createStreamTrackEventSubscriptions(track, {
|
|
244
|
-
ended: track => {
|
|
245
|
-
mediaInit.signals?.[track.kind === 'audio'
|
|
246
|
-
? 'onAudioMuteStateChanged'
|
|
247
|
-
: 'onVideoMuteStateChanged']?.emit(undefined);
|
|
248
|
-
mediaInit.signals?.onStreamTrackEndedFinal?.emit(track);
|
|
249
|
-
},
|
|
250
|
-
mute: track => {
|
|
251
|
-
mediaInit.signals?.[track.kind === 'audio'
|
|
252
|
-
? 'onAudioMuteStateChanged'
|
|
253
|
-
: 'onVideoMuteStateChanged']?.emit(true);
|
|
254
|
-
mediaInit.signals?.onStreamTrackMuted?.emit(track);
|
|
255
|
-
},
|
|
256
|
-
unmute: track => {
|
|
257
|
-
mediaInit.signals?.[track.kind === 'audio'
|
|
258
|
-
? 'onAudioMuteStateChanged'
|
|
259
|
-
: 'onVideoMuteStateChanged']?.emit(!track.enabled);
|
|
260
|
-
mediaInit.signals?.onStreamTrackUnmuted?.emit(track);
|
|
261
|
-
},
|
|
262
|
-
});
|
|
263
|
-
const subscribeTrackAndSourceTrackEvents = (track) => {
|
|
264
|
-
let trackUnsubscribe;
|
|
265
|
-
let sourceTrackUnsubscribe;
|
|
266
|
-
if (track.track) {
|
|
267
|
-
trackUnsubscribe = subscribeTrackEvents(track.track);
|
|
268
|
-
if (track.source.track && track.source.track !== track.track) {
|
|
269
|
-
sourceTrackUnsubscribe = subscribeTrackEvents(track.source.track);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return () => {
|
|
273
|
-
trackUnsubscribe?.();
|
|
274
|
-
sourceTrackUnsubscribe?.();
|
|
275
|
-
};
|
|
276
|
-
};
|
|
277
|
-
const trackSubscriptions = new Map(mediaInit.tracks.flatMap(track => {
|
|
278
|
-
if (track.track) {
|
|
279
|
-
return [
|
|
280
|
-
[track.track, subscribeTrackAndSourceTrackEvents(track)],
|
|
281
|
-
];
|
|
282
|
-
}
|
|
283
|
-
return [];
|
|
284
|
-
}));
|
|
285
277
|
const getConstraints = () => {
|
|
286
278
|
const audio = props.audioTrack?.getConstraints() ?? mediaInit.constraints.audio;
|
|
287
279
|
const video = props.videoTrack?.getConstraints() ?? mediaInit.constraints.video;
|
|
288
280
|
return { audio: audio, video: video };
|
|
289
281
|
};
|
|
290
|
-
|
|
282
|
+
const updateDevices = (devices) => {
|
|
291
283
|
props.devices = devices;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
284
|
+
};
|
|
285
|
+
// Two-hand control to avoid leaking of the media
|
|
286
|
+
// When the track is suspended by the system, we also mute it if it is not already muted.
|
|
287
|
+
// As it is possible that the track could still flow data even if it is system muted
|
|
288
|
+
const handleSuspended = (track) => {
|
|
289
|
+
switch (track.kind) {
|
|
290
|
+
case 'audioinput': {
|
|
291
|
+
if (props.audioAlreadyMuted === undefined) {
|
|
292
|
+
props.audioAlreadyMuted = props.audioTrack?.muted === true;
|
|
293
|
+
}
|
|
294
|
+
if (!props.audioAlreadyMuted) {
|
|
295
|
+
props.audioTrack?.mute(true, true);
|
|
296
|
+
}
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
case 'videoinput': {
|
|
300
|
+
if (props.videoAlreadyMuted === undefined) {
|
|
301
|
+
props.videoAlreadyMuted = props.videoTrack?.muted === true;
|
|
302
|
+
}
|
|
303
|
+
if (!props.videoAlreadyMuted) {
|
|
304
|
+
props.videoTrack?.mute(true, true);
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
// When the track is resumed by the system, we only unmute the track
|
|
311
|
+
// if an only if it is muted by the suspended event
|
|
312
|
+
const handleResumed = (track) => {
|
|
313
|
+
switch (track.kind) {
|
|
314
|
+
case 'audioinput': {
|
|
315
|
+
assert(props.audioAlreadyMuted !== undefined, 'Audio Resume should not be triggerred before Suspended');
|
|
316
|
+
if (props.audioTrack?.suspended === false) {
|
|
317
|
+
if (!props.audioAlreadyMuted) {
|
|
318
|
+
props.audioTrack.mute(false);
|
|
319
|
+
}
|
|
320
|
+
props.audioAlreadyMuted = undefined;
|
|
321
|
+
}
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
case 'videoinput': {
|
|
325
|
+
assert(props.videoAlreadyMuted !== undefined, 'Video Resume should not be triggerred before Suspended');
|
|
326
|
+
if (props.videoTrack?.suspended === false) {
|
|
327
|
+
if (!props.videoAlreadyMuted) {
|
|
328
|
+
props.videoTrack.mute(false);
|
|
329
|
+
}
|
|
330
|
+
props.videoAlreadyMuted = undefined;
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
304
334
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
335
|
+
};
|
|
336
|
+
const subscribe = () => [
|
|
337
|
+
onDevicesChanged.add(updateDevices),
|
|
338
|
+
mediaInit.signals?.onMediaTrackSuspended?.add(handleSuspended),
|
|
339
|
+
mediaInit.signals?.onMediaTrackResumed?.add(handleResumed),
|
|
340
|
+
];
|
|
341
|
+
let subscriptions = subscribe();
|
|
342
|
+
const unsubscribe = () => {
|
|
343
|
+
for (const unsubscribe of subscriptions) {
|
|
344
|
+
unsubscribe?.();
|
|
309
345
|
}
|
|
346
|
+
subscriptions.length = 0;
|
|
310
347
|
};
|
|
311
348
|
return {
|
|
312
349
|
get id() {
|
|
@@ -372,16 +409,16 @@ export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevic
|
|
|
372
409
|
props.devices.size('videoinput') > 0));
|
|
373
410
|
},
|
|
374
411
|
muteAudio(mute) {
|
|
375
|
-
if (!props.audioTrack || props.audioTrack.
|
|
412
|
+
if (!props.audioTrack || props.audioTrack.suspended) {
|
|
376
413
|
return;
|
|
377
414
|
}
|
|
378
|
-
|
|
415
|
+
props.audioTrack.mute(mute);
|
|
379
416
|
},
|
|
380
417
|
muteVideo(mute) {
|
|
381
|
-
if (!props.videoTrack || props.videoTrack.
|
|
418
|
+
if (!props.videoTrack || props.videoTrack.suspended) {
|
|
382
419
|
return;
|
|
383
420
|
}
|
|
384
|
-
|
|
421
|
+
props.videoTrack?.mute(mute);
|
|
385
422
|
},
|
|
386
423
|
applyConstraints: async (constraints) => {
|
|
387
424
|
await Promise.all([
|
|
@@ -394,12 +431,7 @@ export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevic
|
|
|
394
431
|
]);
|
|
395
432
|
},
|
|
396
433
|
async release() {
|
|
397
|
-
unsubscribe
|
|
398
|
-
unsubscribe = undefined;
|
|
399
|
-
for (const unsubscribe of trackSubscriptions.values()) {
|
|
400
|
-
unsubscribe();
|
|
401
|
-
}
|
|
402
|
-
trackSubscriptions.clear();
|
|
434
|
+
unsubscribe();
|
|
403
435
|
await Promise.all([
|
|
404
436
|
props.audioTrack?.release(),
|
|
405
437
|
props.videoTrack?.release(),
|
|
@@ -409,13 +441,13 @@ export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevic
|
|
|
409
441
|
audio: props.audioTrack?.getSettings(),
|
|
410
442
|
video: props.videoTrack?.getSettings(),
|
|
411
443
|
}),
|
|
412
|
-
clone(signals) {
|
|
444
|
+
clone(signals, label) {
|
|
413
445
|
const tracks = [props.audioTrack, props.videoTrack].flatMap(track => {
|
|
414
446
|
if (!track?.source) {
|
|
415
447
|
return [];
|
|
416
448
|
}
|
|
417
|
-
const cloned = track.source.clone(signals);
|
|
418
|
-
return cloned;
|
|
449
|
+
const cloned = track.source.clone(signals, label);
|
|
450
|
+
return [cloned];
|
|
419
451
|
});
|
|
420
452
|
const stream = this.stream &&
|
|
421
453
|
new MediaStream(tracks.flatMap(track => (track.track ? [track.track] : [])));
|
|
@@ -440,6 +472,9 @@ export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevic
|
|
|
440
472
|
getVideoTracks() {
|
|
441
473
|
return props.videoTrack ? [props.videoTrack] : [];
|
|
442
474
|
},
|
|
475
|
+
isCurrentTrack(track) {
|
|
476
|
+
return track === props.audioTrack || track === props.videoTrack;
|
|
477
|
+
},
|
|
443
478
|
addTrack(track) {
|
|
444
479
|
switch (track.kind) {
|
|
445
480
|
case 'audioinput':
|
|
@@ -457,13 +492,22 @@ export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevic
|
|
|
457
492
|
default:
|
|
458
493
|
break;
|
|
459
494
|
}
|
|
495
|
+
if (subscriptions.length === 0) {
|
|
496
|
+
subscriptions = subscribe();
|
|
497
|
+
}
|
|
460
498
|
if (track.track) {
|
|
461
499
|
props.stream.addTrack(track.track);
|
|
462
|
-
if (!trackSubscriptions.has(track.track)) {
|
|
463
|
-
trackSubscriptions.set(track.track, subscribeTrackAndSourceTrackEvents(track));
|
|
464
|
-
}
|
|
465
500
|
mediaInit.signals?.onAddTrack?.emit(track.track);
|
|
466
501
|
props.stream.dispatchEvent(new MediaStreamTrackEvent('addtrack', { track: track.track }));
|
|
502
|
+
if (track.muted) {
|
|
503
|
+
mediaInit.signals?.onMediaTrackMuted?.emit(track);
|
|
504
|
+
}
|
|
505
|
+
if (track.suspended) {
|
|
506
|
+
mediaInit.signals?.onMediaTrackSuspended?.emit(track);
|
|
507
|
+
}
|
|
508
|
+
if (track.stopped) {
|
|
509
|
+
mediaInit.signals?.onMediaTrackStopped?.emit(track);
|
|
510
|
+
}
|
|
467
511
|
}
|
|
468
512
|
},
|
|
469
513
|
removeTrack(track) {
|
|
@@ -478,13 +522,14 @@ export const buildMedia = (mediaInit, onDevicesChanged = internalSignals.onDevic
|
|
|
478
522
|
}
|
|
479
523
|
if (removed && track.track) {
|
|
480
524
|
props.stream.removeTrack(track.track);
|
|
481
|
-
trackSubscriptions.get(track.track)?.();
|
|
482
|
-
trackSubscriptions.delete(track.track);
|
|
483
525
|
mediaInit.signals?.onRemoveTrack?.emit(track.track);
|
|
484
526
|
props.stream.dispatchEvent(new MediaStreamTrackEvent('removetrack', {
|
|
485
527
|
track: track.track,
|
|
486
528
|
}));
|
|
487
529
|
}
|
|
530
|
+
if (subscriptions.length) {
|
|
531
|
+
unsubscribe();
|
|
532
|
+
}
|
|
488
533
|
},
|
|
489
534
|
toJSON() {
|
|
490
535
|
return {
|
package/dist/videoProcessor.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RenderBackend, SegmentationModel, SegmentationTransform, VideoProcessor } from '@pexip/media-processor';
|
|
2
2
|
import type { MediaDeviceRequest } from '@pexip/media-control';
|
|
3
|
-
import type { TrackProcessor,
|
|
3
|
+
import type { MediaSignals, Segmenters, TrackProcessor, VideoContentHint, VideoRenderParams, VideoStreamTrackProcessorAPIs } from './types';
|
|
4
4
|
interface ProcessorDeps {
|
|
5
|
-
videoProcessor?: () => VideoProcessor;
|
|
6
|
-
transformer?: SegmentationTransform;
|
|
7
5
|
segmenters: Partial<Segmenters>;
|
|
6
|
+
transformer?: SegmentationTransform;
|
|
7
|
+
videoProcessor?: () => VideoProcessor;
|
|
8
8
|
videoSegmentationModel?: SegmentationModel;
|
|
9
9
|
}
|
|
10
10
|
interface VideoStreamProcessOptions extends Partial<VideoRenderParams>, Omit<ProcessorDeps, 'videoProcessor'> {
|
|
@@ -28,6 +28,7 @@ interface VideoStreamProcessOptions extends Partial<VideoRenderParams>, Omit<Pro
|
|
|
28
28
|
stopAsMute?: () => boolean;
|
|
29
29
|
dynamicProcessingDimensions?: () => boolean;
|
|
30
30
|
gpuAPI?: () => RenderBackend;
|
|
31
|
+
signals?: MediaSignals;
|
|
31
32
|
}
|
|
32
33
|
interface VideoStreamProcessProps extends Partial<VideoRenderParams>, Required<ProcessorDeps> {
|
|
33
34
|
hasInitialized: boolean;
|
|
@@ -37,5 +38,5 @@ declare const FEATURE_KEYS: readonly ["backgroundBlurAmount", "backgroundImageUr
|
|
|
37
38
|
type FeaturePropKeys = (typeof FEATURE_KEYS)[number];
|
|
38
39
|
type FeatureProps = Pick<Partial<VideoStreamProcessProps>, FeaturePropKeys>;
|
|
39
40
|
export declare const updateFeatureProps: (constraints: MediaDeviceRequest["video"], props: FeatureProps) => FeatureProps;
|
|
40
|
-
export declare const createVideoStreamProcess: ({ backgroundImageUrl, dynamicProcessingDimensions, edgeBlurAmount, foregroundThreshold, frameRate, gpuAPI, label, lowestProcessingHeight, maskCombineRatio, processingHeight, processingWidth, shouldEnable, stopAsMute, trackProcessorAPI, videoSegmentation, ...options }: VideoStreamProcessOptions) => TrackProcessor;
|
|
41
|
+
export declare const createVideoStreamProcess: ({ backgroundImageUrl, dynamicProcessingDimensions, edgeBlurAmount, foregroundThreshold, frameRate, gpuAPI, label, lowestProcessingHeight, maskCombineRatio, processingHeight, processingWidth, shouldEnable, stopAsMute, trackProcessorAPI, videoSegmentation, signals, ...options }: VideoStreamProcessOptions) => TrackProcessor;
|
|
41
42
|
export {};
|
package/dist/videoProcessor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createCanvasTransform, createVideoProcessor, createVideoTrackProcessor, createVideoTrackProcessorWithFallback, isRenderEffects, isSegmentationModel, } from '@pexip/media-processor';
|
|
2
|
+
import { extractConstraintsWithKeys, getValueFromConstrainNumber, muteStreamTrack, } from '@pexip/media-control';
|
|
3
3
|
import { isEmpty, assert } from '@pexip/utils';
|
|
4
4
|
import { createMediaTrack, getBlurKernelSize } from './utils';
|
|
5
5
|
import { logger, proxyWithLog } from './logger';
|
|
@@ -91,6 +91,8 @@ export const updateFeatureProps = (constraints, props) => {
|
|
|
91
91
|
}
|
|
92
92
|
return accm;
|
|
93
93
|
}
|
|
94
|
+
default:
|
|
95
|
+
return accm;
|
|
94
96
|
}
|
|
95
97
|
}, {});
|
|
96
98
|
};
|
|
@@ -142,7 +144,7 @@ const getTrackProcessor = (shouldUseStreamTrackProcessor, ...params) => {
|
|
|
142
144
|
return createVideoTrackProcessorWithFallback(...params);
|
|
143
145
|
};
|
|
144
146
|
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 }) => {
|
|
147
|
+
label = PROCESSOR_LABELS.VideoProcessor, lowestProcessingHeight, maskCombineRatio, processingHeight, processingWidth, shouldEnable, stopAsMute = () => false, trackProcessorAPI = () => 'stream', videoSegmentation, signals, ...options }) => {
|
|
146
148
|
const videoSegmentationModel = options.videoSegmentationModel ?? 'selfie';
|
|
147
149
|
const segmenter = options.segmenters[videoSegmentationModel];
|
|
148
150
|
if (!segmenter) {
|
|
@@ -253,36 +255,61 @@ label = PROCESSOR_LABELS.VideoProcessor, lowestProcessingHeight, maskCombineRati
|
|
|
253
255
|
assert(track, 'Video track should be there');
|
|
254
256
|
// Inherit contentHint from previous track
|
|
255
257
|
track.contentHint = prevMediaTrack.track.contentHint;
|
|
256
|
-
|
|
257
|
-
props.videoProcessor().close();
|
|
258
|
-
await prevMediaTrack.release();
|
|
259
|
-
_videoProcessor = undefined;
|
|
260
|
-
props.hasInitialized = false;
|
|
261
|
-
};
|
|
262
|
-
const mute = (mute) => {
|
|
263
|
-
props.transformer.effects = mute
|
|
264
|
-
? 'none'
|
|
265
|
-
: props.videoSegmentation ?? 'none';
|
|
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);
|
|
274
|
-
};
|
|
258
|
+
let trackReleaseTimeoutID = 0;
|
|
275
259
|
return createMediaTrack({
|
|
276
260
|
label,
|
|
277
261
|
kind: 'videoinput',
|
|
278
|
-
constraints
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
262
|
+
get constraints() {
|
|
263
|
+
return this.previousMediaTrack?.getConstraints();
|
|
264
|
+
},
|
|
265
|
+
get previousMediaTrack() {
|
|
266
|
+
return prevMediaTrack;
|
|
267
|
+
},
|
|
268
|
+
get input() {
|
|
269
|
+
return this.previousMediaTrack?.input;
|
|
270
|
+
},
|
|
271
|
+
get expectedInput() {
|
|
272
|
+
return this.previousMediaTrack?.expectedInput;
|
|
273
|
+
},
|
|
282
274
|
track,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
275
|
+
overrideMute: true,
|
|
276
|
+
get muted() {
|
|
277
|
+
return this.previousMediaTrack?.muted;
|
|
278
|
+
},
|
|
279
|
+
mute(mute, _self, soft) {
|
|
280
|
+
props.transformer.effects = mute
|
|
281
|
+
? 'none'
|
|
282
|
+
: props.videoSegmentation ?? 'none';
|
|
283
|
+
muteStreamTrack(stream)(mute, 'video');
|
|
284
|
+
if (stopAsMute() && !soft) {
|
|
285
|
+
if (mute) {
|
|
286
|
+
// Track cannot be stopped immediately as we need to ensure
|
|
287
|
+
// the last frame is black to avoid frozen frame in a gateway call
|
|
288
|
+
trackReleaseTimeoutID = window.setTimeout(() => {
|
|
289
|
+
for (const track of stream.getVideoTracks()) {
|
|
290
|
+
track.stop();
|
|
291
|
+
}
|
|
292
|
+
void this?.release?.();
|
|
293
|
+
trackReleaseTimeoutID = 0;
|
|
294
|
+
}, 500);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
if (trackReleaseTimeoutID) {
|
|
298
|
+
window.clearTimeout(trackReleaseTimeoutID);
|
|
299
|
+
trackReleaseTimeoutID = 0;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
this.previousMediaTrack?.mute(mute, soft);
|
|
304
|
+
},
|
|
305
|
+
signals,
|
|
306
|
+
async release() {
|
|
307
|
+
props.videoProcessor().close();
|
|
308
|
+
await this.previousMediaTrack?.release();
|
|
309
|
+
_videoProcessor = undefined;
|
|
310
|
+
props.hasInitialized = false;
|
|
311
|
+
},
|
|
312
|
+
async applyConstraints(constraints) {
|
|
286
313
|
if (isEmpty(constraints)) {
|
|
287
314
|
return;
|
|
288
315
|
}
|
|
@@ -293,8 +320,8 @@ label = PROCESSOR_LABELS.VideoProcessor, lowestProcessingHeight, maskCombineRati
|
|
|
293
320
|
}
|
|
294
321
|
applyFeatures(props.transformer, features, lowestProcessingHeight);
|
|
295
322
|
if (dynamicProcessingDimensions()) {
|
|
296
|
-
const acceptedSettings =
|
|
297
|
-
if (acceptedSettings
|
|
323
|
+
const acceptedSettings = this.previousMediaTrack?.source.getSettings();
|
|
324
|
+
if (acceptedSettings?.width && acceptedSettings?.height) {
|
|
298
325
|
props.transformer.update({
|
|
299
326
|
processingHeight: acceptedSettings.height,
|
|
300
327
|
processingWidth: acceptedSettings.width,
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pexip/media",
|
|
3
|
-
"version": "20.
|
|
3
|
+
"version": "20.3.3",
|
|
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",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://
|
|
10
|
-
"directory": "
|
|
9
|
+
"url": "https://github.com/pexip/aquila.git",
|
|
10
|
+
"directory": "packages/media"
|
|
11
11
|
},
|
|
12
12
|
"type": "module",
|
|
13
13
|
"license": "Apache-2.0",
|
|
@@ -44,23 +44,22 @@
|
|
|
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.9.
|
|
50
|
-
"@pexip/utils": "17.
|
|
47
|
+
"@pexip/media-control": "20.3.3",
|
|
48
|
+
"@pexip/media-processor": "20.3.3",
|
|
49
|
+
"@pexip/signal": "16.9.2",
|
|
50
|
+
"@pexip/utils": "17.1.1"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@jest/globals": "^
|
|
54
|
-
"@pexip/bundler": "18.1.
|
|
53
|
+
"@jest/globals": "^30.2.0",
|
|
54
|
+
"@pexip/bundler": "18.1.2",
|
|
55
55
|
"@swc/core": "^1.10.12",
|
|
56
|
-
"@swc/jest": "^0.2.
|
|
56
|
+
"@swc/jest": "^0.2.39",
|
|
57
57
|
"jest": "^29.5.12",
|
|
58
58
|
"jest-junit": "^16.0.0",
|
|
59
59
|
"prettier": "^3.2.5",
|
|
60
60
|
"typescript": "~5.7.3"
|
|
61
61
|
},
|
|
62
62
|
"publishConfig": {
|
|
63
|
-
"access": "public"
|
|
64
|
-
"registry": "https://registry.npmjs.org/"
|
|
63
|
+
"access": "public"
|
|
65
64
|
}
|
|
66
65
|
}
|