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