@pexip/media 18.5.0 → 19.0.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 +113 -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 +242 -169
- package/dist/previewController.d.ts +10 -9
- package/dist/previewController.js +99 -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 +487 -186
- 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,129 @@ 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
|
+
switch (track.kind) {
|
|
219
|
+
case 'audioinput':
|
|
220
|
+
track.mute(inputMuted.audio);
|
|
221
|
+
break;
|
|
222
|
+
case 'videoinput':
|
|
223
|
+
track.mute(inputMuted.video);
|
|
224
|
+
break;
|
|
225
|
+
default:
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const processMedia = createMediaProcessor({
|
|
231
|
+
audioProcessors,
|
|
232
|
+
videoProcessors,
|
|
233
|
+
onProcessingError(error, track) {
|
|
234
|
+
logger.error({ error, track }, 'Failed to process media track');
|
|
235
|
+
assert(props.media);
|
|
236
|
+
switch (track.kind) {
|
|
237
|
+
case 'audioinput':
|
|
238
|
+
// FIXME: It should not be device-not-found error, will be fixed with https://gitlab.com/pexip/zoo/-/issues/3793
|
|
239
|
+
props.media.status = UserMediaStatus.AudioDeviceNotFound;
|
|
240
|
+
break;
|
|
241
|
+
case 'videoinput':
|
|
242
|
+
// FIXME: It should not be device-not-found error, will be fixed with https://gitlab.com/pexip/zoo/-/issues/3793
|
|
243
|
+
props.media.status = UserMediaStatus.VideoDeviceNotFound;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
94
246
|
},
|
|
95
247
|
});
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
248
|
+
const getUserMediaProcess = createGetUserMediaProcess({
|
|
249
|
+
getUserMedia: requestUserMediaWithRetry(getCurrentDevices),
|
|
250
|
+
getCurrentDevices,
|
|
251
|
+
signals,
|
|
252
|
+
});
|
|
253
|
+
const processAndUpdateMedia = async (media, tracks) => {
|
|
254
|
+
const processedTracks = await processMedia(tracks);
|
|
255
|
+
// Sync mute state to processed tracks
|
|
256
|
+
syncMuteState(processedTracks);
|
|
257
|
+
for (const [idx, track] of processedTracks.entries()) {
|
|
258
|
+
const originTrack = tracks.at(idx);
|
|
259
|
+
assert(originTrack, 'Processed track should always has the original track in the same order');
|
|
260
|
+
// Only replace track when they are not the same
|
|
261
|
+
if (originTrack.id !== track.id) {
|
|
262
|
+
media.removeTrack(originTrack);
|
|
263
|
+
media.addTrack(track);
|
|
264
|
+
}
|
|
99
265
|
}
|
|
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
266
|
};
|
|
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);
|
|
267
|
+
const updateMediaProcess = createMediaUpdater({
|
|
268
|
+
signals,
|
|
269
|
+
getUserMedia: getUserMediaProcess,
|
|
270
|
+
getCurrentDevices,
|
|
271
|
+
shouldDiscardMedia: () => props.discardMedia,
|
|
272
|
+
onMediaTracksChanged: (media, tracks) => {
|
|
273
|
+
// Sync mute state to input tracks
|
|
274
|
+
syncMuteState(media.getTracks());
|
|
275
|
+
tracks.forEach(track => {
|
|
276
|
+
if (track.track) {
|
|
277
|
+
const unsubscribe = createStreamTrackEventSubscriptions(track.track, {
|
|
278
|
+
ended: track => {
|
|
279
|
+
signals.onStreamTrackEnded?.emit(track);
|
|
280
|
+
unsubscribe();
|
|
281
|
+
},
|
|
282
|
+
mute: signals.onStreamTrackMuted?.emit,
|
|
283
|
+
unmute: signals.onStreamTrackUnmuted?.emit,
|
|
123
284
|
});
|
|
124
285
|
}
|
|
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
|
-
]);
|
|
286
|
+
});
|
|
287
|
+
props.media = media;
|
|
288
|
+
processAndUpdateMedia(media, tracks).catch((error) => {
|
|
289
|
+
if (error instanceof Error) {
|
|
290
|
+
logger.error(error, 'Failed to process media');
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
});
|
|
164
295
|
/**
|
|
165
296
|
* A function to update the media only when necessary, aka the current setup
|
|
166
297
|
* for the stream is different from the new constraints
|
|
@@ -168,81 +299,12 @@ export const createMedia = ({ getMuteState, signals, mediaProcessors, getDefault
|
|
|
168
299
|
* @param constraints - @see MediaDeviceRequest
|
|
169
300
|
*/
|
|
170
301
|
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;
|
|
302
|
+
try {
|
|
303
|
+
props.updatingMedia = true;
|
|
304
|
+
await updateMediaProcess(constraints, props.media);
|
|
237
305
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
mixingFeaturesChanged) {
|
|
241
|
-
props.media.constraints = constraints;
|
|
242
|
-
await props.media.applyConstraints({
|
|
243
|
-
audio: audioFeatures,
|
|
244
|
-
video: videoFeatures,
|
|
245
|
-
});
|
|
306
|
+
finally {
|
|
307
|
+
props.updatingMedia = false;
|
|
246
308
|
}
|
|
247
309
|
};
|
|
248
310
|
const mergeMediaConstraints = (constraints) => {
|
|
@@ -265,25 +327,33 @@ export const createMedia = ({ getMuteState, signals, mediaProcessors, getDefault
|
|
|
265
327
|
};
|
|
266
328
|
const tryAndGetUserMedia = constraints => {
|
|
267
329
|
queue.enqueue(async () => {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
props.devices = await getDevices();
|
|
273
|
-
if (!mergedConstraints.audio && !mergedConstraints.video) {
|
|
274
|
-
return;
|
|
330
|
+
const permission = await getInputDevicePermissionState();
|
|
331
|
+
const status = getPermissionStatus(permission);
|
|
332
|
+
if (isInitialPermissionsGranted(status)) {
|
|
333
|
+
await updateMedia(mergeMediaConstraints(constraints));
|
|
275
334
|
}
|
|
276
|
-
|
|
277
|
-
|
|
335
|
+
else {
|
|
336
|
+
const currentDevices = await getCurrentDevices();
|
|
337
|
+
props.media = buildMedia({
|
|
338
|
+
constraints,
|
|
339
|
+
permission,
|
|
340
|
+
devices: currentDevices,
|
|
341
|
+
status,
|
|
342
|
+
stream: undefined,
|
|
343
|
+
signals,
|
|
344
|
+
tracks: [],
|
|
345
|
+
});
|
|
278
346
|
}
|
|
279
347
|
});
|
|
280
348
|
};
|
|
281
349
|
// Media Control Event handlers
|
|
282
350
|
const handleNoInputs = (_availableDevices) => {
|
|
283
|
-
props.media
|
|
351
|
+
if (props.media) {
|
|
352
|
+
props.media.status = UserMediaStatus.NoDevicesFound;
|
|
353
|
+
}
|
|
284
354
|
};
|
|
285
355
|
const handleDeviceChange = (devices) => {
|
|
286
|
-
props.devices = devices;
|
|
356
|
+
props.devices = createIndexedDevices(devices);
|
|
287
357
|
};
|
|
288
358
|
// Subscribe Media Events for devices
|
|
289
359
|
subscribe(event => {
|
|
@@ -305,6 +375,9 @@ export const createMedia = ({ getMuteState, signals, mediaProcessors, getDefault
|
|
|
305
375
|
}
|
|
306
376
|
});
|
|
307
377
|
return {
|
|
378
|
+
get updatingMedia() {
|
|
379
|
+
return props.updatingMedia;
|
|
380
|
+
},
|
|
308
381
|
get media() {
|
|
309
382
|
return props.media;
|
|
310
383
|
},
|
|
@@ -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 {};
|