@pexip/media 18.5.0 → 19.1.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 +126 -0
- package/README.md +9 -8
- package/dist/audioMixingProcessor.d.ts +2 -2
- package/dist/audioMixingProcessor.js +113 -100
- package/dist/audioProcessor.d.ts +4 -4
- package/dist/audioProcessor.js +117 -152
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +7 -0
- package/dist/media.d.ts +12 -2
- package/dist/media.js +250 -169
- package/dist/previewController.d.ts +10 -9
- package/dist/previewController.js +100 -68
- package/dist/signals.d.ts +10 -1
- package/dist/signals.js +9 -0
- package/dist/status.d.ts +5 -2
- package/dist/status.js +3 -3
- package/dist/types.d.ts +135 -40
- package/dist/userMedia.d.ts +10 -8
- package/dist/userMedia.js +122 -178
- package/dist/utils.d.ts +25 -47
- package/dist/utils.js +489 -187
- package/dist/videoProcessor.d.ts +3 -7
- package/dist/videoProcessor.js +103 -139
- package/package.json +6 -5
package/dist/media.js
CHANGED
|
@@ -1,14 +1,103 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createAsyncQueue, isEmpty } from '@pexip/utils';
|
|
3
|
-
import {
|
|
1
|
+
import { MediaEventType, createIndexedDevices, createStreamTrackEventSubscriptions, getDevices, getInputDevicePermissionState, isIndexedDevices, isRequestedResolution, mergeConstraints, subscribe, } from '@pexip/media-control';
|
|
2
|
+
import { createAsyncQueue, isEmpty, assert } from '@pexip/utils';
|
|
3
|
+
import { internalSignals } from './signals';
|
|
4
4
|
import { UserMediaStatus } from './types';
|
|
5
5
|
import { createModuleLogger, logger } from './logger';
|
|
6
6
|
import { createGetUserMediaProcess, requestUserMediaWithRetry, } from './userMedia';
|
|
7
|
-
import {
|
|
7
|
+
import { AUDIO_SETTINGS_KEYS, MIXING_SETTINGS_KEYS, VIDEO_SETTINGS_KEYS, buildMedia, createMediaProcessor, diffSettings, getDevicesChanges, hasPtzFeature, mergeSettings, refineMediaConstraints, } from './utils';
|
|
8
|
+
import { getPermissionStatus, isInitialPermissionsGranted } from './status';
|
|
8
9
|
import { isMedia } from './typeGuard';
|
|
9
10
|
import { updateFeatureProps as getVideoFeatures } from './videoProcessor';
|
|
10
11
|
import { updateFeatureProps as getAudioFeatures } from './audioProcessor';
|
|
11
12
|
import { updateFeatureProps as getMixingFeatures } from './audioMixingProcessor';
|
|
13
|
+
import { GET_USER_MEDIA_TIMEOUT_MS } from './constants';
|
|
14
|
+
export const createMediaUpdater = ({ getUserMedia, getCurrentDevices, shouldDiscardMedia: shouldDisgardMedia, onMediaTracksChanged, getInputDevicePermission = getInputDevicePermissionState, signals, }) => {
|
|
15
|
+
return async (constraints, currentMedia) => {
|
|
16
|
+
const currentDevices = await getCurrentDevices();
|
|
17
|
+
const permission = await getInputDevicePermission();
|
|
18
|
+
const requestNothing = !constraints.audio && !constraints.video;
|
|
19
|
+
const permissionRejected = permission.audio === 'denied' && permission.video === 'denied';
|
|
20
|
+
if (requestNothing || permissionRejected) {
|
|
21
|
+
for (const track of currentMedia?.getTracks() ?? []) {
|
|
22
|
+
await track.release();
|
|
23
|
+
currentMedia?.removeTrack(track);
|
|
24
|
+
}
|
|
25
|
+
currentMedia?.setOriginalConstraints(constraints);
|
|
26
|
+
const media = currentMedia ??
|
|
27
|
+
buildMedia({
|
|
28
|
+
constraints,
|
|
29
|
+
permission,
|
|
30
|
+
devices: currentDevices,
|
|
31
|
+
status: permissionRejected
|
|
32
|
+
? UserMediaStatus.PermissionsRejected
|
|
33
|
+
: UserMediaStatus.PermissionsGranted,
|
|
34
|
+
stream: undefined,
|
|
35
|
+
signals,
|
|
36
|
+
tracks: [],
|
|
37
|
+
});
|
|
38
|
+
return onMediaTracksChanged(media, []);
|
|
39
|
+
}
|
|
40
|
+
const { audio: prevAudioSettings, video: prevVideoSettings } = currentMedia?.getSettings() ?? {};
|
|
41
|
+
const videoFeatures = getVideoFeatures(constraints.video, {});
|
|
42
|
+
const audioFeatures = getAudioFeatures(constraints.audio, {});
|
|
43
|
+
const mixingFeatures = getMixingFeatures(constraints.audio, {});
|
|
44
|
+
const hasRequestedResolution = ['blur', 'overlay'].includes(videoFeatures.videoSegmentation ?? '') || // Skip when there is a render effect to modify the video
|
|
45
|
+
currentDevices.size('videoinput') === 0 || // Skip when no authorized video found
|
|
46
|
+
isRequestedResolution(constraints.video, currentMedia?.getVideoTracks().at(0)?.getSettings());
|
|
47
|
+
const audioFeaturesChanged = diffSettings(AUDIO_SETTINGS_KEYS)(prevAudioSettings, audioFeatures);
|
|
48
|
+
const videoFeaturesChanged = diffSettings(VIDEO_SETTINGS_KEYS)(prevVideoSettings, videoFeatures);
|
|
49
|
+
const mixingFeaturesChanged = diffSettings(MIXING_SETTINGS_KEYS)(prevAudioSettings, mixingFeatures);
|
|
50
|
+
const ptzFeaturesChanges = hasPtzFeature() &&
|
|
51
|
+
diffSettings(['pan', 'tilt', 'zoom'])(prevVideoSettings, videoFeatures);
|
|
52
|
+
const audioRequest = refineMediaConstraints({
|
|
53
|
+
kind: 'audioinput',
|
|
54
|
+
request: constraints.audio,
|
|
55
|
+
currentDevices,
|
|
56
|
+
permission: permission.audio,
|
|
57
|
+
currentMediaTracks: currentMedia?.getAudioTracks() ?? [],
|
|
58
|
+
});
|
|
59
|
+
const videoRequest = refineMediaConstraints({
|
|
60
|
+
kind: 'videoinput',
|
|
61
|
+
request: constraints.video,
|
|
62
|
+
currentDevices,
|
|
63
|
+
permission: permission.video,
|
|
64
|
+
currentMediaTracks: currentMedia?.getVideoTracks() ?? [],
|
|
65
|
+
force: !hasRequestedResolution || Boolean(ptzFeaturesChanges),
|
|
66
|
+
});
|
|
67
|
+
if (audioRequest !== undefined || videoRequest !== undefined) {
|
|
68
|
+
const media = await getUserMedia({
|
|
69
|
+
constraints: { audio: audioRequest, video: videoRequest },
|
|
70
|
+
originalConstraints: constraints,
|
|
71
|
+
permission,
|
|
72
|
+
currentMedia,
|
|
73
|
+
});
|
|
74
|
+
if (shouldDisgardMedia()) {
|
|
75
|
+
logger.debug('Discard media');
|
|
76
|
+
return await media.release();
|
|
77
|
+
}
|
|
78
|
+
onMediaTracksChanged(media, [
|
|
79
|
+
...(audioRequest ? media.getAudioTracks() : []),
|
|
80
|
+
...(videoRequest ? media.getVideoTracks() : []),
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
const audioDiff = constraints.audio &&
|
|
84
|
+
audioRequest === undefined &&
|
|
85
|
+
currentMedia?.getAudioTracks().at(0)
|
|
86
|
+
? mergeSettings(audioFeaturesChanged, mixingFeaturesChanged)
|
|
87
|
+
: undefined;
|
|
88
|
+
const videoDiff = constraints.video &&
|
|
89
|
+
videoRequest === undefined &&
|
|
90
|
+
currentMedia?.getVideoTracks().at(0)
|
|
91
|
+
? videoFeaturesChanged
|
|
92
|
+
: undefined;
|
|
93
|
+
if (audioDiff || videoDiff) {
|
|
94
|
+
await currentMedia?.applyConstraints({
|
|
95
|
+
audio: audioDiff,
|
|
96
|
+
video: videoDiff,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
};
|
|
12
101
|
/**
|
|
13
102
|
* Proxy handler for Media Props
|
|
14
103
|
*/
|
|
@@ -24,8 +113,11 @@ const createMediaPropsHandler = (signals) => ({
|
|
|
24
113
|
return true;
|
|
25
114
|
}
|
|
26
115
|
if (p === 'devices') {
|
|
116
|
+
if (!isIndexedDevices(value)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
27
119
|
const nextDevices = value;
|
|
28
|
-
const changes = getDevicesChanges(target[p].
|
|
120
|
+
const changes = getDevicesChanges(target[p].get(), nextDevices.get());
|
|
29
121
|
if (isEmpty(changes.found) && isEmpty(changes.lost)) {
|
|
30
122
|
return true;
|
|
31
123
|
}
|
|
@@ -40,20 +132,19 @@ const createMediaPropsHandler = (signals) => ({
|
|
|
40
132
|
}, `Update Props[${p}]`);
|
|
41
133
|
switch (p) {
|
|
42
134
|
case 'devices': {
|
|
43
|
-
if (!
|
|
135
|
+
if (!isIndexedDevices(value)) {
|
|
44
136
|
return false;
|
|
45
137
|
}
|
|
46
|
-
|
|
47
|
-
target[p]
|
|
48
|
-
|
|
49
|
-
target.media.devices = devices;
|
|
138
|
+
target[p] = value;
|
|
139
|
+
signals?.onDevicesChanged?.emit(target[p]);
|
|
140
|
+
internalSignals.onDevicesChanged.emit(target[p]);
|
|
50
141
|
return true;
|
|
51
142
|
}
|
|
52
143
|
case 'media': {
|
|
53
144
|
if (!isMedia(value)) {
|
|
54
145
|
return false;
|
|
55
146
|
}
|
|
56
|
-
const currentStatus = target[p]
|
|
147
|
+
const currentStatus = target[p]?.status;
|
|
57
148
|
target[p] = value;
|
|
58
149
|
signals?.onMediaChanged?.emit(value);
|
|
59
150
|
if (currentStatus !== value.status) {
|
|
@@ -61,9 +152,13 @@ const createMediaPropsHandler = (signals) => ({
|
|
|
61
152
|
}
|
|
62
153
|
return true;
|
|
63
154
|
}
|
|
155
|
+
case 'updatingMedia': {
|
|
156
|
+
const result = Reflect.set(target, p, value);
|
|
157
|
+
signals?.onUpdatingMedia?.emit(value);
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
64
160
|
default: {
|
|
65
|
-
Reflect.set(target, p, value);
|
|
66
|
-
return true;
|
|
161
|
+
return Reflect.set(target, p, value);
|
|
67
162
|
}
|
|
68
163
|
}
|
|
69
164
|
},
|
|
@@ -74,93 +169,137 @@ const createMediaPropsHandler = (signals) => ({
|
|
|
74
169
|
*
|
|
75
170
|
* @param options - @see MediaOptions
|
|
76
171
|
*/
|
|
77
|
-
export const createMedia = ({ getMuteState, signals,
|
|
78
|
-
const initMedia = (status = UserMediaStatus.Initial, devices = [], constraints) => buildMedia(() => ({ status, devices, constraints }), signals.onStatusChanged?.emit);
|
|
172
|
+
export const createMedia = ({ getMuteState, signals, audioProcessors, videoProcessors, getDefaultConstraints = () => ({}), }) => {
|
|
79
173
|
const _props = {
|
|
80
|
-
devices: [],
|
|
81
|
-
media: initMedia(),
|
|
174
|
+
devices: createIndexedDevices([]),
|
|
82
175
|
discardMedia: false,
|
|
176
|
+
updatingMedia: false,
|
|
83
177
|
};
|
|
84
178
|
const props = new Proxy(_props, createMediaPropsHandler(signals));
|
|
85
|
-
const queue = createAsyncQueue(
|
|
179
|
+
const queue = createAsyncQueue({
|
|
180
|
+
timeoutInMS: GET_USER_MEDIA_TIMEOUT_MS,
|
|
181
|
+
handleTimeout: async () => {
|
|
182
|
+
const status = UserMediaStatus.NoDevicesFound;
|
|
183
|
+
if (props.media) {
|
|
184
|
+
props.media.status = status;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
const permission = await getInputDevicePermissionState();
|
|
188
|
+
const devices = await getCurrentDevices();
|
|
189
|
+
props.media = buildMedia({
|
|
190
|
+
constraints: getDefaultConstraints(),
|
|
191
|
+
permission,
|
|
192
|
+
devices,
|
|
193
|
+
status: UserMediaStatus.NoDevicesFound,
|
|
194
|
+
stream: undefined,
|
|
195
|
+
signals,
|
|
196
|
+
tracks: [],
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
});
|
|
86
201
|
const logger = createModuleLogger({
|
|
87
202
|
module: 'Media',
|
|
88
203
|
props: _props,
|
|
89
204
|
get mediaTracks() {
|
|
90
205
|
return _props.media?.stream?.getTracks();
|
|
91
206
|
},
|
|
92
|
-
|
|
93
|
-
|
|
207
|
+
});
|
|
208
|
+
const getCurrentDevices = async () => {
|
|
209
|
+
if (props.devices.size()) {
|
|
210
|
+
return props.devices;
|
|
211
|
+
}
|
|
212
|
+
props.devices = createIndexedDevices(await getDevices());
|
|
213
|
+
return props.devices;
|
|
214
|
+
};
|
|
215
|
+
const syncMuteState = (tracks) => {
|
|
216
|
+
const inputMuted = getMuteState();
|
|
217
|
+
for (const track of tracks) {
|
|
218
|
+
const prevMuted = track.muted;
|
|
219
|
+
switch (track.kind) {
|
|
220
|
+
case 'audioinput': {
|
|
221
|
+
track.mute(inputMuted.audio);
|
|
222
|
+
if (prevMuted !== track.muted) {
|
|
223
|
+
signals.onAudioMuteStateChanged?.emit(track.muted);
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
case 'videoinput':
|
|
228
|
+
track.mute(inputMuted.video);
|
|
229
|
+
if (prevMuted !== track.muted) {
|
|
230
|
+
signals.onVideoMuteStateChanged?.emit(track.muted);
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
default:
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const processMedia = createMediaProcessor({
|
|
239
|
+
audioProcessors,
|
|
240
|
+
videoProcessors,
|
|
241
|
+
onProcessingError(error, track) {
|
|
242
|
+
logger.error({ error, track }, 'Failed to process media track');
|
|
243
|
+
assert(props.media);
|
|
244
|
+
switch (track.kind) {
|
|
245
|
+
case 'audioinput':
|
|
246
|
+
// FIXME: It should not be device-not-found error, will be fixed with https://gitlab.com/pexip/zoo/-/issues/3793
|
|
247
|
+
props.media.status = UserMediaStatus.AudioDeviceNotFound;
|
|
248
|
+
break;
|
|
249
|
+
case 'videoinput':
|
|
250
|
+
// FIXME: It should not be device-not-found error, will be fixed with https://gitlab.com/pexip/zoo/-/issues/3793
|
|
251
|
+
props.media.status = UserMediaStatus.VideoDeviceNotFound;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
94
254
|
},
|
|
95
255
|
});
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
256
|
+
const getUserMediaProcess = createGetUserMediaProcess({
|
|
257
|
+
getUserMedia: requestUserMediaWithRetry(getCurrentDevices),
|
|
258
|
+
getCurrentDevices,
|
|
259
|
+
signals,
|
|
260
|
+
});
|
|
261
|
+
const processAndUpdateMedia = async (media, tracks) => {
|
|
262
|
+
const processedTracks = await processMedia(tracks);
|
|
263
|
+
// Sync mute state to processed tracks
|
|
264
|
+
syncMuteState(processedTracks);
|
|
265
|
+
for (const [idx, track] of processedTracks.entries()) {
|
|
266
|
+
const originTrack = tracks.at(idx);
|
|
267
|
+
assert(originTrack, 'Processed track should always has the original track in the same order');
|
|
268
|
+
// Only replace track when they are not the same
|
|
269
|
+
if (originTrack.id !== track.id) {
|
|
270
|
+
media.removeTrack(originTrack);
|
|
271
|
+
media.addTrack(track);
|
|
272
|
+
}
|
|
99
273
|
}
|
|
100
|
-
// carry the most recent status over for the next time
|
|
101
|
-
const status = await deriveInitialPermissionStatus(props.media.status);
|
|
102
|
-
props.media = initMedia(status, props.devices, props.media.constraints);
|
|
103
274
|
};
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
logger.debug({ trackInResult: track, intendToMute: muted }, `mute ${track.kind}`);
|
|
122
|
-
signals.onStreamTrackEnabled?.emit(track);
|
|
275
|
+
const updateMediaProcess = createMediaUpdater({
|
|
276
|
+
signals,
|
|
277
|
+
getUserMedia: getUserMediaProcess,
|
|
278
|
+
getCurrentDevices,
|
|
279
|
+
shouldDiscardMedia: () => props.discardMedia,
|
|
280
|
+
onMediaTracksChanged: (media, tracks) => {
|
|
281
|
+
// Sync mute state to input tracks
|
|
282
|
+
syncMuteState(tracks);
|
|
283
|
+
tracks.forEach(track => {
|
|
284
|
+
if (track.track) {
|
|
285
|
+
const unsubscribe = createStreamTrackEventSubscriptions(track.track, {
|
|
286
|
+
ended: track => {
|
|
287
|
+
signals.onStreamTrackEnded?.emit(track);
|
|
288
|
+
unsubscribe();
|
|
289
|
+
},
|
|
290
|
+
mute: signals.onStreamTrackMuted?.emit,
|
|
291
|
+
unmute: signals.onStreamTrackUnmuted?.emit,
|
|
123
292
|
});
|
|
124
293
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
trackSubscriptions?.forEach(unsubscribe => unsubscribe());
|
|
135
|
-
await media.release();
|
|
136
|
-
await cleanup();
|
|
137
|
-
};
|
|
138
|
-
const applyConstraints = async (constraints) => {
|
|
139
|
-
await media.applyConstraints(constraints);
|
|
140
|
-
const videoFeatures = getVideoFeatures(constraints.video, {});
|
|
141
|
-
const audioFeatures = getAudioFeatures(constraints.audio, {});
|
|
142
|
-
media.stream
|
|
143
|
-
?.getAudioTracks()
|
|
144
|
-
.forEach(applyContentHint(audioFeatures.contentHint));
|
|
145
|
-
media.stream
|
|
146
|
-
?.getVideoTracks()
|
|
147
|
-
.forEach(applyContentHint(videoFeatures.contentHint));
|
|
148
|
-
};
|
|
149
|
-
logger.debug({ finalAudioMute: audio, finalVideoMute: video }, 'End of media pipeline');
|
|
150
|
-
return wrapToJSON(shallowCopy(media, {
|
|
151
|
-
release,
|
|
152
|
-
muteAudio: mute => {
|
|
153
|
-
muteAudio(mute);
|
|
154
|
-
signals.onAudioMuteStateChanged?.emit(mute);
|
|
155
|
-
},
|
|
156
|
-
muteVideo: mute => {
|
|
157
|
-
muteVideo(mute);
|
|
158
|
-
signals.onVideoMuteStateChanged?.emit(mute);
|
|
159
|
-
},
|
|
160
|
-
applyConstraints,
|
|
161
|
-
}));
|
|
162
|
-
}),
|
|
163
|
-
]);
|
|
294
|
+
});
|
|
295
|
+
props.media = media;
|
|
296
|
+
processAndUpdateMedia(media, tracks).catch((error) => {
|
|
297
|
+
if (error instanceof Error) {
|
|
298
|
+
logger.error(error, 'Failed to process media');
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
});
|
|
164
303
|
/**
|
|
165
304
|
* A function to update the media only when necessary, aka the current setup
|
|
166
305
|
* for the stream is different from the new constraints
|
|
@@ -168,81 +307,12 @@ export const createMedia = ({ getMuteState, signals, mediaProcessors, getDefault
|
|
|
168
307
|
* @param constraints - @see MediaDeviceRequest
|
|
169
308
|
*/
|
|
170
309
|
const updateMedia = async (constraints) => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// Check if the audio device was requested before and is currently in use by another application
|
|
175
|
-
const isTryingToRequestSameUsedAudioDevice = (isAudioDeviceInUse(props.media.status) ||
|
|
176
|
-
isDevicesInUse(props.media.status)) &&
|
|
177
|
-
props.media.expectedAudioInput?.deviceId &&
|
|
178
|
-
props.media.expectedAudioInput?.deviceId ===
|
|
179
|
-
extractConstrainDevice(constraints.audio)?.[0]?.[0]?.deviceId;
|
|
180
|
-
// Check if the video device was requested before and is currently in use by another application
|
|
181
|
-
const isTryingToRequestSameUsedVideoDevice = (isVideoDeviceInUse(props.media.status) ||
|
|
182
|
-
isDevicesInUse(props.media.status)) &&
|
|
183
|
-
props.media.expectedVideoInput?.deviceId &&
|
|
184
|
-
props.media.expectedVideoInput?.deviceId ===
|
|
185
|
-
extractConstrainDevice(constraints?.video)?.[0]?.[0]?.deviceId;
|
|
186
|
-
const shouldRequestAudio = !isTryingToRequestSameUsedAudioDevice &&
|
|
187
|
-
shouldRequestDevice(constraints.audio, props.media?.rawStream?.getAudioTracks() ?? [], props.devices.filter(device => device.kind === 'audioinput'));
|
|
188
|
-
const shouldRequestVideo = !isTryingToRequestSameUsedVideoDevice &&
|
|
189
|
-
shouldRequestDevice(constraints.video, props.media?.rawStream?.getVideoTracks() ?? [], props.devices.filter(device => device.kind === 'videoinput'));
|
|
190
|
-
const { audio: [prevAudioSettings], video: [prevVideoSettings], } = props.media.getSettings();
|
|
191
|
-
const videoFeatures = getVideoFeatures(constraints.video, {});
|
|
192
|
-
const audioFeatures = getAudioFeatures(constraints.audio, {});
|
|
193
|
-
const mixingFeatures = getMixingFeatures(constraints.audio, {});
|
|
194
|
-
const hasRequestedResolution = ['blur', 'overlay'].includes(videoFeatures.videoSegmentation ?? '') || // Skip when there is a render effect to modify the video
|
|
195
|
-
!props.devices.some(device => device.kind === 'videoinput' && device.label) || // Skip when no authorized video found
|
|
196
|
-
isTryingToRequestSameUsedVideoDevice ||
|
|
197
|
-
isRequestedResolution(constraints.video, props.media?.videoInput);
|
|
198
|
-
const audioFeaturesChanged = hasSettingsChanged(AUDIO_SETTINGS_KEYS)(prevAudioSettings, audioFeatures);
|
|
199
|
-
const videoFeaturesChanged = hasSettingsChanged(VIDEO_SETTINGS_KEYS)(prevVideoSettings, videoFeatures);
|
|
200
|
-
const mixingFeaturesChanged = hasSettingsChanged(MIXING_SETTINGS_KEYS)(prevAudioSettings, mixingFeatures);
|
|
201
|
-
const ptzFeaturesChanges = hasPtzFeature() &&
|
|
202
|
-
hasSettingsChanged(['pan', 'tilt', 'zoom'])(prevVideoSettings, videoFeatures);
|
|
203
|
-
logger.debug({
|
|
204
|
-
constraints,
|
|
205
|
-
stream: props.media.stream,
|
|
206
|
-
currentDevices: props.devices,
|
|
207
|
-
shouldRequestAudio,
|
|
208
|
-
shouldRequestVideo,
|
|
209
|
-
sameResolution: hasRequestedResolution,
|
|
210
|
-
audioFeaturesChanged,
|
|
211
|
-
videoFeaturesChanged,
|
|
212
|
-
mixingFeaturesChanged,
|
|
213
|
-
}, 'Is currently streaming requested stream');
|
|
214
|
-
if (shouldRequestAudio ||
|
|
215
|
-
shouldRequestVideo ||
|
|
216
|
-
ptzFeaturesChanges ||
|
|
217
|
-
!hasRequestedResolution ||
|
|
218
|
-
!props.media.stream) {
|
|
219
|
-
if (props.media) {
|
|
220
|
-
await props.media.release();
|
|
221
|
-
}
|
|
222
|
-
if (props.discardMedia) {
|
|
223
|
-
props.discardMedia = false;
|
|
224
|
-
}
|
|
225
|
-
const media = await mediaPipeline.execute(constraints);
|
|
226
|
-
media.stream
|
|
227
|
-
?.getAudioTracks()
|
|
228
|
-
.forEach(applyContentHint(audioFeatures.contentHint));
|
|
229
|
-
media.stream
|
|
230
|
-
?.getVideoTracks()
|
|
231
|
-
.forEach(applyContentHint(videoFeatures.contentHint));
|
|
232
|
-
if (props.discardMedia) {
|
|
233
|
-
logger.debug('Discard media');
|
|
234
|
-
return await media.release();
|
|
235
|
-
}
|
|
236
|
-
props.media = media;
|
|
310
|
+
try {
|
|
311
|
+
props.updatingMedia = true;
|
|
312
|
+
await updateMediaProcess(constraints, props.media);
|
|
237
313
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
mixingFeaturesChanged) {
|
|
241
|
-
props.media.constraints = constraints;
|
|
242
|
-
await props.media.applyConstraints({
|
|
243
|
-
audio: audioFeatures,
|
|
244
|
-
video: videoFeatures,
|
|
245
|
-
});
|
|
314
|
+
finally {
|
|
315
|
+
props.updatingMedia = false;
|
|
246
316
|
}
|
|
247
317
|
};
|
|
248
318
|
const mergeMediaConstraints = (constraints) => {
|
|
@@ -265,25 +335,33 @@ export const createMedia = ({ getMuteState, signals, mediaProcessors, getDefault
|
|
|
265
335
|
};
|
|
266
336
|
const tryAndGetUserMedia = constraints => {
|
|
267
337
|
queue.enqueue(async () => {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
props.devices = await getDevices();
|
|
273
|
-
if (!mergedConstraints.audio && !mergedConstraints.video) {
|
|
274
|
-
return;
|
|
338
|
+
const permission = await getInputDevicePermissionState();
|
|
339
|
+
const status = getPermissionStatus(permission);
|
|
340
|
+
if (isInitialPermissionsGranted(status)) {
|
|
341
|
+
await updateMedia(mergeMediaConstraints(constraints));
|
|
275
342
|
}
|
|
276
|
-
|
|
277
|
-
|
|
343
|
+
else {
|
|
344
|
+
const currentDevices = await getCurrentDevices();
|
|
345
|
+
props.media = buildMedia({
|
|
346
|
+
constraints,
|
|
347
|
+
permission,
|
|
348
|
+
devices: currentDevices,
|
|
349
|
+
status,
|
|
350
|
+
stream: undefined,
|
|
351
|
+
signals,
|
|
352
|
+
tracks: [],
|
|
353
|
+
});
|
|
278
354
|
}
|
|
279
355
|
});
|
|
280
356
|
};
|
|
281
357
|
// Media Control Event handlers
|
|
282
358
|
const handleNoInputs = (_availableDevices) => {
|
|
283
|
-
props.media
|
|
359
|
+
if (props.media) {
|
|
360
|
+
props.media.status = UserMediaStatus.NoDevicesFound;
|
|
361
|
+
}
|
|
284
362
|
};
|
|
285
363
|
const handleDeviceChange = (devices) => {
|
|
286
|
-
props.devices = devices;
|
|
364
|
+
props.devices = createIndexedDevices(devices);
|
|
287
365
|
};
|
|
288
366
|
// Subscribe Media Events for devices
|
|
289
367
|
subscribe(event => {
|
|
@@ -305,6 +383,9 @@ export const createMedia = ({ getMuteState, signals, mediaProcessors, getDefault
|
|
|
305
383
|
}
|
|
306
384
|
});
|
|
307
385
|
return {
|
|
386
|
+
get updatingMedia() {
|
|
387
|
+
return props.updatingMedia;
|
|
388
|
+
},
|
|
308
389
|
get media() {
|
|
309
390
|
return props.media;
|
|
310
391
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { IndexedDevices, MediaDeviceInfoLike, MediaDeviceRequest } from '@pexip/media-control';
|
|
2
2
|
import type { AsyncQueueOptions } from '@pexip/utils';
|
|
3
|
-
import type { Media,
|
|
3
|
+
import type { Media, MediaSignals, TrackProcessor, Unsubscribe } from './types';
|
|
4
4
|
type EventCallback<T> = (event: T) => void;
|
|
5
5
|
type EventErrorCallback = (error: Error) => void;
|
|
6
6
|
export type PreviewInput = MediaDeviceInfoLike | undefined;
|
|
@@ -17,17 +17,18 @@ export interface PreviewEventHandler {
|
|
|
17
17
|
unsubscribeMain?: Unsubscribe;
|
|
18
18
|
}
|
|
19
19
|
export interface PreviewStreamParams {
|
|
20
|
-
getCurrentDevices: () =>
|
|
20
|
+
getCurrentDevices: () => IndexedDevices;
|
|
21
21
|
getCurrentMedia: () => Media | undefined;
|
|
22
22
|
updateMainStream: (request: MediaDeviceRequest) => Promise<void>;
|
|
23
23
|
mediaSignal: MediaSignals['onMediaChanged'];
|
|
24
24
|
onEnded?: () => void;
|
|
25
25
|
fftSize?: number;
|
|
26
26
|
queueOptions?: Partial<AsyncQueueOptions>;
|
|
27
|
-
|
|
27
|
+
audioProcessors: TrackProcessor[];
|
|
28
|
+
videoProcessors: TrackProcessor[];
|
|
28
29
|
}
|
|
29
30
|
export interface PreviewControllerProps {
|
|
30
|
-
media: Media;
|
|
31
|
+
media: Media | undefined;
|
|
31
32
|
audioInput?: MediaDeviceInfoLike;
|
|
32
33
|
videoInput?: MediaDeviceInfoLike;
|
|
33
34
|
updatingPreview: boolean;
|
|
@@ -37,7 +38,7 @@ export interface PreviewControllerProps {
|
|
|
37
38
|
initialized: boolean;
|
|
38
39
|
}
|
|
39
40
|
export interface PreviewStreamController {
|
|
40
|
-
media: Media;
|
|
41
|
+
media: Media | undefined;
|
|
41
42
|
audioInputChanged: boolean;
|
|
42
43
|
videoInputChanged: boolean;
|
|
43
44
|
inputChanged: boolean;
|
|
@@ -45,8 +46,8 @@ export interface PreviewStreamController {
|
|
|
45
46
|
videoInput: PreviewInput;
|
|
46
47
|
updatingPreview: boolean;
|
|
47
48
|
updatingMain: boolean;
|
|
48
|
-
updateAudioInput(
|
|
49
|
-
updateVideoInput(
|
|
49
|
+
updateAudioInput(id: string): void;
|
|
50
|
+
updateVideoInput(id: string): void;
|
|
50
51
|
applyChanges(force?: boolean): Promise<void>;
|
|
51
52
|
revertChanges(): Promise<void>;
|
|
52
53
|
cleanup: () => Promise<void>;
|
|
@@ -60,6 +61,6 @@ export interface PreviewStreamController {
|
|
|
60
61
|
onApplyChangesError(callback: EventErrorCallback): Unsubscribe;
|
|
61
62
|
onRevertChangesError(callback: EventErrorCallback): Unsubscribe;
|
|
62
63
|
}
|
|
63
|
-
export declare const createPreviewStreamController: ({ getCurrentDevices, getCurrentMedia, updateMainStream, onEnded, mediaSignal, queueOptions,
|
|
64
|
+
export declare const createPreviewStreamController: ({ getCurrentDevices, getCurrentMedia, updateMainStream, onEnded, mediaSignal, queueOptions, audioProcessors, videoProcessors, }: PreviewStreamParams) => PreviewStreamController;
|
|
64
65
|
export type CreatePreviewStreamController = typeof createPreviewStreamController;
|
|
65
66
|
export {};
|