@pexip/media 18.4.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 +130 -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 -170
- 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 +12 -7
package/dist/media.js
CHANGED
|
@@ -1,15 +1,103 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createAsyncQueue, isEmpty } from '@pexip/utils';
|
|
3
|
-
import {
|
|
4
|
-
import { getPermissionStatus, deriveInitialPermissionStatus, isInitialPermissions, isInitialPermissionsGranted, isVideoDeviceInUse, isDevicesInUse, isAudioDeviceInUse, } from './status';
|
|
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';
|
|
5
4
|
import { UserMediaStatus } from './types';
|
|
6
5
|
import { createModuleLogger, logger } from './logger';
|
|
7
6
|
import { createGetUserMediaProcess, requestUserMediaWithRetry, } from './userMedia';
|
|
8
|
-
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';
|
|
9
9
|
import { isMedia } from './typeGuard';
|
|
10
10
|
import { updateFeatureProps as getVideoFeatures } from './videoProcessor';
|
|
11
11
|
import { updateFeatureProps as getAudioFeatures } from './audioProcessor';
|
|
12
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
|
+
};
|
|
13
101
|
/**
|
|
14
102
|
* Proxy handler for Media Props
|
|
15
103
|
*/
|
|
@@ -25,8 +113,11 @@ const createMediaPropsHandler = (signals) => ({
|
|
|
25
113
|
return true;
|
|
26
114
|
}
|
|
27
115
|
if (p === 'devices') {
|
|
116
|
+
if (!isIndexedDevices(value)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
28
119
|
const nextDevices = value;
|
|
29
|
-
const changes = getDevicesChanges(target[p].
|
|
120
|
+
const changes = getDevicesChanges(target[p].get(), nextDevices.get());
|
|
30
121
|
if (isEmpty(changes.found) && isEmpty(changes.lost)) {
|
|
31
122
|
return true;
|
|
32
123
|
}
|
|
@@ -41,20 +132,19 @@ const createMediaPropsHandler = (signals) => ({
|
|
|
41
132
|
}, `Update Props[${p}]`);
|
|
42
133
|
switch (p) {
|
|
43
134
|
case 'devices': {
|
|
44
|
-
if (!
|
|
135
|
+
if (!isIndexedDevices(value)) {
|
|
45
136
|
return false;
|
|
46
137
|
}
|
|
47
|
-
|
|
48
|
-
target[p]
|
|
49
|
-
|
|
50
|
-
target.media.devices = devices;
|
|
138
|
+
target[p] = value;
|
|
139
|
+
signals?.onDevicesChanged?.emit(target[p]);
|
|
140
|
+
internalSignals.onDevicesChanged.emit(target[p]);
|
|
51
141
|
return true;
|
|
52
142
|
}
|
|
53
143
|
case 'media': {
|
|
54
144
|
if (!isMedia(value)) {
|
|
55
145
|
return false;
|
|
56
146
|
}
|
|
57
|
-
const currentStatus = target[p]
|
|
147
|
+
const currentStatus = target[p]?.status;
|
|
58
148
|
target[p] = value;
|
|
59
149
|
signals?.onMediaChanged?.emit(value);
|
|
60
150
|
if (currentStatus !== value.status) {
|
|
@@ -62,9 +152,13 @@ const createMediaPropsHandler = (signals) => ({
|
|
|
62
152
|
}
|
|
63
153
|
return true;
|
|
64
154
|
}
|
|
155
|
+
case 'updatingMedia': {
|
|
156
|
+
const result = Reflect.set(target, p, value);
|
|
157
|
+
signals?.onUpdatingMedia?.emit(value);
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
65
160
|
default: {
|
|
66
|
-
Reflect.set(target, p, value);
|
|
67
|
-
return true;
|
|
161
|
+
return Reflect.set(target, p, value);
|
|
68
162
|
}
|
|
69
163
|
}
|
|
70
164
|
},
|
|
@@ -75,93 +169,129 @@ const createMediaPropsHandler = (signals) => ({
|
|
|
75
169
|
*
|
|
76
170
|
* @param options - @see MediaOptions
|
|
77
171
|
*/
|
|
78
|
-
export const createMedia = ({ getMuteState, signals,
|
|
79
|
-
const initMedia = (status = UserMediaStatus.Initial, devices = [], constraints) => buildMedia(() => ({ status, devices, constraints }), signals.onStatusChanged?.emit);
|
|
172
|
+
export const createMedia = ({ getMuteState, signals, audioProcessors, videoProcessors, getDefaultConstraints = () => ({}), }) => {
|
|
80
173
|
const _props = {
|
|
81
|
-
devices: [],
|
|
82
|
-
media: initMedia(),
|
|
174
|
+
devices: createIndexedDevices([]),
|
|
83
175
|
discardMedia: false,
|
|
176
|
+
updatingMedia: false,
|
|
84
177
|
};
|
|
85
178
|
const props = new Proxy(_props, createMediaPropsHandler(signals));
|
|
86
|
-
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
|
+
});
|
|
87
201
|
const logger = createModuleLogger({
|
|
88
202
|
module: 'Media',
|
|
89
203
|
props: _props,
|
|
90
204
|
get mediaTracks() {
|
|
91
205
|
return _props.media?.stream?.getTracks();
|
|
92
206
|
},
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
}
|
|
95
246
|
},
|
|
96
247
|
});
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
}
|
|
100
265
|
}
|
|
101
|
-
// carry the most recent status over for the next time
|
|
102
|
-
const status = await deriveInitialPermissionStatus(props.media.status);
|
|
103
|
-
props.media = initMedia(status, props.devices, props.media.constraints);
|
|
104
266
|
};
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
logger.debug({ trackInResult: track, intendToMute: muted }, `mute ${track.kind}`);
|
|
123
|
-
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,
|
|
124
284
|
});
|
|
125
285
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
trackSubscriptions?.forEach(unsubscribe => unsubscribe());
|
|
136
|
-
await media.release();
|
|
137
|
-
await cleanup();
|
|
138
|
-
};
|
|
139
|
-
const applyConstraints = async (constraints) => {
|
|
140
|
-
await media.applyConstraints(constraints);
|
|
141
|
-
const videoFeatures = getVideoFeatures(constraints.video, {});
|
|
142
|
-
const audioFeatures = getAudioFeatures(constraints.audio, {});
|
|
143
|
-
media.stream
|
|
144
|
-
?.getAudioTracks()
|
|
145
|
-
.forEach(applyContentHint(audioFeatures.contentHint));
|
|
146
|
-
media.stream
|
|
147
|
-
?.getVideoTracks()
|
|
148
|
-
.forEach(applyContentHint(videoFeatures.contentHint));
|
|
149
|
-
};
|
|
150
|
-
logger.debug({ finalAudioMute: audio, finalVideoMute: video }, 'End of media pipeline');
|
|
151
|
-
return wrapToJSON(shallowCopy(media, {
|
|
152
|
-
release,
|
|
153
|
-
muteAudio: mute => {
|
|
154
|
-
muteAudio(mute);
|
|
155
|
-
signals.onAudioMuteStateChanged?.emit(mute);
|
|
156
|
-
},
|
|
157
|
-
muteVideo: mute => {
|
|
158
|
-
muteVideo(mute);
|
|
159
|
-
signals.onVideoMuteStateChanged?.emit(mute);
|
|
160
|
-
},
|
|
161
|
-
applyConstraints,
|
|
162
|
-
}));
|
|
163
|
-
}),
|
|
164
|
-
]);
|
|
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
|
+
});
|
|
165
295
|
/**
|
|
166
296
|
* A function to update the media only when necessary, aka the current setup
|
|
167
297
|
* for the stream is different from the new constraints
|
|
@@ -169,81 +299,12 @@ export const createMedia = ({ getMuteState, signals, mediaProcessors, getDefault
|
|
|
169
299
|
* @param constraints - @see MediaDeviceRequest
|
|
170
300
|
*/
|
|
171
301
|
const updateMedia = async (constraints) => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// Check if the audio device was requested before and is currently in use by another application
|
|
176
|
-
const isTryingToRequestSameUsedAudioDevice = (isAudioDeviceInUse(props.media.status) ||
|
|
177
|
-
isDevicesInUse(props.media.status)) &&
|
|
178
|
-
props.media.expectedAudioInput?.deviceId &&
|
|
179
|
-
props.media.expectedAudioInput?.deviceId ===
|
|
180
|
-
extractConstrainDevice(constraints.audio)?.[0]?.[0]?.deviceId;
|
|
181
|
-
// Check if the video device was requested before and is currently in use by another application
|
|
182
|
-
const isTryingToRequestSameUsedVideoDevice = (isVideoDeviceInUse(props.media.status) ||
|
|
183
|
-
isDevicesInUse(props.media.status)) &&
|
|
184
|
-
props.media.expectedVideoInput?.deviceId &&
|
|
185
|
-
props.media.expectedVideoInput?.deviceId ===
|
|
186
|
-
extractConstrainDevice(constraints?.video)?.[0]?.[0]?.deviceId;
|
|
187
|
-
const shouldRequestAudio = !isTryingToRequestSameUsedAudioDevice &&
|
|
188
|
-
shouldRequestDevice(constraints.audio, props.media?.rawStream?.getAudioTracks() ?? [], props.devices.filter(device => device.kind === 'audioinput'));
|
|
189
|
-
const shouldRequestVideo = !isTryingToRequestSameUsedVideoDevice &&
|
|
190
|
-
shouldRequestDevice(constraints.video, props.media?.rawStream?.getVideoTracks() ?? [], props.devices.filter(device => device.kind === 'videoinput'));
|
|
191
|
-
const { audio: [prevAudioSettings], video: [prevVideoSettings], } = props.media.getSettings();
|
|
192
|
-
const videoFeatures = getVideoFeatures(constraints.video, {});
|
|
193
|
-
const audioFeatures = getAudioFeatures(constraints.audio, {});
|
|
194
|
-
const mixingFeatures = getMixingFeatures(constraints.audio, {});
|
|
195
|
-
const hasRequestedResolution = ['blur', 'overlay'].includes(videoFeatures.videoSegmentation ?? '') || // Skip when there is a render effect to modify the video
|
|
196
|
-
!props.devices.some(device => device.kind === 'videoinput' && device.label) || // Skip when no authorized video found
|
|
197
|
-
isTryingToRequestSameUsedVideoDevice ||
|
|
198
|
-
isRequestedResolution(constraints.video, props.media?.videoInput);
|
|
199
|
-
const audioFeaturesChanged = hasSettingsChanged(AUDIO_SETTINGS_KEYS)(prevAudioSettings, audioFeatures);
|
|
200
|
-
const videoFeaturesChanged = hasSettingsChanged(VIDEO_SETTINGS_KEYS)(prevVideoSettings, videoFeatures);
|
|
201
|
-
const mixingFeaturesChanged = hasSettingsChanged(MIXING_SETTINGS_KEYS)(prevAudioSettings, mixingFeatures);
|
|
202
|
-
const ptzFeaturesChanges = hasPtzFeature() &&
|
|
203
|
-
hasSettingsChanged(['pan', 'tilt', 'zoom'])(prevVideoSettings, videoFeatures);
|
|
204
|
-
logger.debug({
|
|
205
|
-
constraints,
|
|
206
|
-
stream: props.media.stream,
|
|
207
|
-
currentDevices: props.devices,
|
|
208
|
-
shouldRequestAudio,
|
|
209
|
-
shouldRequestVideo,
|
|
210
|
-
sameResolution: hasRequestedResolution,
|
|
211
|
-
audioFeaturesChanged,
|
|
212
|
-
videoFeaturesChanged,
|
|
213
|
-
mixingFeaturesChanged,
|
|
214
|
-
}, 'Is currently streaming requested stream');
|
|
215
|
-
if (shouldRequestAudio ||
|
|
216
|
-
shouldRequestVideo ||
|
|
217
|
-
ptzFeaturesChanges ||
|
|
218
|
-
!hasRequestedResolution ||
|
|
219
|
-
!props.media.stream) {
|
|
220
|
-
if (props.media) {
|
|
221
|
-
await props.media.release();
|
|
222
|
-
}
|
|
223
|
-
if (props.discardMedia) {
|
|
224
|
-
props.discardMedia = false;
|
|
225
|
-
}
|
|
226
|
-
const media = await mediaPipeline.execute(constraints);
|
|
227
|
-
media.stream
|
|
228
|
-
?.getAudioTracks()
|
|
229
|
-
.forEach(applyContentHint(audioFeatures.contentHint));
|
|
230
|
-
media.stream
|
|
231
|
-
?.getVideoTracks()
|
|
232
|
-
.forEach(applyContentHint(videoFeatures.contentHint));
|
|
233
|
-
if (props.discardMedia) {
|
|
234
|
-
logger.debug('Discard media');
|
|
235
|
-
return await media.release();
|
|
236
|
-
}
|
|
237
|
-
props.media = media;
|
|
302
|
+
try {
|
|
303
|
+
props.updatingMedia = true;
|
|
304
|
+
await updateMediaProcess(constraints, props.media);
|
|
238
305
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
mixingFeaturesChanged) {
|
|
242
|
-
props.media.constraints = constraints;
|
|
243
|
-
await props.media.applyConstraints({
|
|
244
|
-
audio: audioFeatures,
|
|
245
|
-
video: videoFeatures,
|
|
246
|
-
});
|
|
306
|
+
finally {
|
|
307
|
+
props.updatingMedia = false;
|
|
247
308
|
}
|
|
248
309
|
};
|
|
249
310
|
const mergeMediaConstraints = (constraints) => {
|
|
@@ -266,25 +327,33 @@ export const createMedia = ({ getMuteState, signals, mediaProcessors, getDefault
|
|
|
266
327
|
};
|
|
267
328
|
const tryAndGetUserMedia = constraints => {
|
|
268
329
|
queue.enqueue(async () => {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
props.devices = await getDevices();
|
|
274
|
-
if (!mergedConstraints.audio && !mergedConstraints.video) {
|
|
275
|
-
return;
|
|
330
|
+
const permission = await getInputDevicePermissionState();
|
|
331
|
+
const status = getPermissionStatus(permission);
|
|
332
|
+
if (isInitialPermissionsGranted(status)) {
|
|
333
|
+
await updateMedia(mergeMediaConstraints(constraints));
|
|
276
334
|
}
|
|
277
|
-
|
|
278
|
-
|
|
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
|
+
});
|
|
279
346
|
}
|
|
280
347
|
});
|
|
281
348
|
};
|
|
282
349
|
// Media Control Event handlers
|
|
283
350
|
const handleNoInputs = (_availableDevices) => {
|
|
284
|
-
props.media
|
|
351
|
+
if (props.media) {
|
|
352
|
+
props.media.status = UserMediaStatus.NoDevicesFound;
|
|
353
|
+
}
|
|
285
354
|
};
|
|
286
355
|
const handleDeviceChange = (devices) => {
|
|
287
|
-
props.devices = devices;
|
|
356
|
+
props.devices = createIndexedDevices(devices);
|
|
288
357
|
};
|
|
289
358
|
// Subscribe Media Events for devices
|
|
290
359
|
subscribe(event => {
|
|
@@ -306,6 +375,9 @@ export const createMedia = ({ getMuteState, signals, mediaProcessors, getDefault
|
|
|
306
375
|
}
|
|
307
376
|
});
|
|
308
377
|
return {
|
|
378
|
+
get updatingMedia() {
|
|
379
|
+
return props.updatingMedia;
|
|
380
|
+
},
|
|
309
381
|
get media() {
|
|
310
382
|
return props.media;
|
|
311
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 {};
|