@pexip/media-control 17.2.0 → 17.4.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 +25 -0
- package/dist/baseLogger.d.ts +37 -0
- package/dist/baseLogger.js +29 -0
- package/dist/constants.d.ts +43 -0
- package/dist/constants.js +26 -0
- package/dist/constraints.d.ts +174 -0
- package/dist/constraints.js +653 -0
- package/dist/devices.d.ts +416 -0
- package/dist/devices.js +766 -0
- package/dist/displayMedia.d.ts +10 -0
- package/dist/displayMedia.js +31 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +105 -0
- package/dist/eventEmitter.d.ts +119 -0
- package/dist/eventEmitter.js +81 -0
- package/dist/getMediaStream.d.ts +11 -0
- package/dist/getMediaStream.js +41 -0
- package/dist/index.d.ts +18 -979
- package/dist/index.js +105 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +5 -0
- package/dist/streamTrackMap.d.ts +9 -0
- package/dist/streamTrackMap.js +79 -0
- package/dist/streams.d.ts +26 -0
- package/dist/streams.js +99 -0
- package/dist/typeGuards.d.ts +89 -0
- package/dist/typeGuards.js +185 -0
- package/dist/types.d.ts +265 -0
- package/dist/types.js +88 -0
- package/dist/utils.d.ts +37 -0
- package/dist/utils.js +47 -0
- package/package.json +7 -7
- package/dist/index.mjs +0 -1699
package/dist/devices.js
ADDED
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
import { MediaDeviceKinds } from './types';
|
|
2
|
+
import { extractConstrainNumber, extractConstrainString, normalizeDeviceConstraint, toArray, relaxInputConstraint, satisfyConstrainNumber, extractConstraintsWithKeys, } from './constraints';
|
|
3
|
+
import { isMediaDeviceInfo, isConstraintSetDevice, isInputConstraintSet, isConstrainDOMStringParameters, isConstraintDOMString, isMediaStreamTrack, } from './typeGuards';
|
|
4
|
+
import { findDevice, compareDevices, isSameDeviceKind, not, isAudioInput, isVideoInput, isAudioOutput, isDeviceGranted, } from './utils';
|
|
5
|
+
const SEPARATOR = ':';
|
|
6
|
+
/**
|
|
7
|
+
* Convert provided info to key for Map
|
|
8
|
+
*
|
|
9
|
+
* @beta
|
|
10
|
+
*/
|
|
11
|
+
export const toKey = ({ id, kind, label }) => [kind, id, label].join(SEPARATOR);
|
|
12
|
+
/**
|
|
13
|
+
* Future proofing: in case we want to alter the values or type returned
|
|
14
|
+
* by {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices | enumerateDevices}
|
|
15
|
+
*
|
|
16
|
+
* @returns
|
|
17
|
+
* a list of currently available {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo | devices}
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```javascript
|
|
21
|
+
* import {getDevices} from "media-control";
|
|
22
|
+
*
|
|
23
|
+
* const devices = await getDevices();
|
|
24
|
+
* // return MediaDeviceInfo[]
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @beta
|
|
28
|
+
*/
|
|
29
|
+
export const getDevices = async () => {
|
|
30
|
+
if ('mediaDevices' in navigator &&
|
|
31
|
+
'enumerateDevices' in navigator.mediaDevices) {
|
|
32
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
33
|
+
return devices;
|
|
34
|
+
}
|
|
35
|
+
// Return empty list as there is no enumerateDevices
|
|
36
|
+
return [];
|
|
37
|
+
};
|
|
38
|
+
export const toDeviceKey = (device) => toKey({ id: device.deviceId, kind: device.kind, label: device.label });
|
|
39
|
+
export const toDeviceTuple = (device) => [toDeviceKey(device), device];
|
|
40
|
+
export const toDevicesMap = (devices) => {
|
|
41
|
+
return new Map(devices.map(toDeviceTuple));
|
|
42
|
+
};
|
|
43
|
+
export const toUniqueDevices = (devices) => {
|
|
44
|
+
const map = toDevicesMap(devices);
|
|
45
|
+
return Array.from(map.values());
|
|
46
|
+
};
|
|
47
|
+
export const createTrackDevicesChanges = (prevDevices = []) => {
|
|
48
|
+
let seen = toDevicesMap(prevDevices);
|
|
49
|
+
return (devices) => {
|
|
50
|
+
const found = [];
|
|
51
|
+
const lost = [];
|
|
52
|
+
// Remove duplicated
|
|
53
|
+
const uniqueDevices = toUniqueDevices(devices);
|
|
54
|
+
const { authorized, unauthorized } = uniqueDevices.reduce((ds, d) => {
|
|
55
|
+
// Use label to differentiate un/authorized
|
|
56
|
+
if (d.label) {
|
|
57
|
+
ds.authorized.push(d);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
ds.unauthorized.push(d);
|
|
61
|
+
}
|
|
62
|
+
return ds;
|
|
63
|
+
}, { authorized: [], unauthorized: [] });
|
|
64
|
+
for (const device of authorized) {
|
|
65
|
+
if (!seen.delete(toDeviceKey(device))) {
|
|
66
|
+
found.push(device);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const device of seen.values()) {
|
|
70
|
+
lost.push(device);
|
|
71
|
+
}
|
|
72
|
+
seen = new Map(authorized.map(toDeviceTuple));
|
|
73
|
+
return { unauthorized, authorized, found, lost, devices: uniqueDevices };
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Unified interface for subscribing {@link DeviceChangedChanges} Event
|
|
78
|
+
*
|
|
79
|
+
* @beta
|
|
80
|
+
*/
|
|
81
|
+
export const deviceChanged = (fn) => {
|
|
82
|
+
const trackChanges = createTrackDevicesChanges();
|
|
83
|
+
const odc = async () => {
|
|
84
|
+
const devices = await getDevices();
|
|
85
|
+
const changes = trackChanges(devices);
|
|
86
|
+
fn(changes);
|
|
87
|
+
};
|
|
88
|
+
// Add extra checking to avoid calling unavailable feature
|
|
89
|
+
// There is `ondevicechange` property in `navigator.mediaDevices` in Safari
|
|
90
|
+
// 11 & 12 but it does nothing and there is no event listener functions
|
|
91
|
+
// available since Safari does not support this API
|
|
92
|
+
//
|
|
93
|
+
// Ref. https://caniuse.com/#search=ondevicechange
|
|
94
|
+
if (navigator.mediaDevices && 'ondevicechange' in navigator.mediaDevices) {
|
|
95
|
+
navigator.mediaDevices.ondevicechange = odc;
|
|
96
|
+
return () => {
|
|
97
|
+
navigator.mediaDevices.ondevicechange = null;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const it = window.setInterval(() => void odc(), 5000);
|
|
101
|
+
return () => {
|
|
102
|
+
window.clearInterval(it);
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
export const extractDeviceInfo = (track) => {
|
|
106
|
+
const { deviceId, groupId, ...settings } = track.getSettings();
|
|
107
|
+
const kind = track.kind === 'audio'
|
|
108
|
+
? MediaDeviceKinds.AUDIOINPUT
|
|
109
|
+
: MediaDeviceKinds.VIDEOINPUT;
|
|
110
|
+
return {
|
|
111
|
+
kind,
|
|
112
|
+
deviceId: deviceId ?? '',
|
|
113
|
+
groupId: groupId ?? '',
|
|
114
|
+
label: track.label,
|
|
115
|
+
settings,
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Convert `MediaStreamTrack` to `MediaDeviceInfoLike`
|
|
120
|
+
*
|
|
121
|
+
* @beta
|
|
122
|
+
*/
|
|
123
|
+
export const toMediaDeviceInfoLike = (track) => {
|
|
124
|
+
const { deviceId, label, kind, groupId } = extractDeviceInfo(track);
|
|
125
|
+
if (!deviceId) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
return { deviceId, label, kind, groupId };
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Compare the provide the MediaStreamTrack and MediaDeviceInfo to see if they
|
|
132
|
+
* are the same with best efforts
|
|
133
|
+
*
|
|
134
|
+
* @param track - `MediaStreamTrack` used for the comparison
|
|
135
|
+
* @param device - `MediaDeviceInfo` used for the comparison
|
|
136
|
+
*
|
|
137
|
+
* @returns `true` if they are the same else `false`
|
|
138
|
+
*
|
|
139
|
+
* @remarks
|
|
140
|
+
* Old browser may not have `deviceId` from getSettings()
|
|
141
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings
|
|
142
|
+
* The `label` attribute will then be used for the comparison instead of the
|
|
143
|
+
* `deviceId`
|
|
144
|
+
*
|
|
145
|
+
* @beta
|
|
146
|
+
*/
|
|
147
|
+
export const compareMediaDeviceToMediaTrack = (track) => (device) => {
|
|
148
|
+
const { deviceId } = track.getSettings();
|
|
149
|
+
const sameLabel = device.label === track.label;
|
|
150
|
+
const sameKind = device.kind.startsWith(track.kind);
|
|
151
|
+
// Old browser may not have deviceId from getSettings()
|
|
152
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings
|
|
153
|
+
if (deviceId) {
|
|
154
|
+
return sameKind && deviceId === device.deviceId;
|
|
155
|
+
}
|
|
156
|
+
return sameKind && sameLabel;
|
|
157
|
+
};
|
|
158
|
+
const toMediaDeviceInfoLikeJSON = (info) => ({
|
|
159
|
+
deviceId: info.deviceId,
|
|
160
|
+
groupId: info.groupId,
|
|
161
|
+
kind: info.kind,
|
|
162
|
+
label: info.label,
|
|
163
|
+
settings: info.settings,
|
|
164
|
+
});
|
|
165
|
+
export const toMediaDeviceInfo = (info) => ({
|
|
166
|
+
deviceId: info.deviceId,
|
|
167
|
+
groupId: info.groupId,
|
|
168
|
+
kind: info.kind,
|
|
169
|
+
label: info.label,
|
|
170
|
+
});
|
|
171
|
+
/**
|
|
172
|
+
* Find the MediaDeviceInfo from provided MediaDeviceInfo[] by comparing with
|
|
173
|
+
* provided MediaStreamTrack
|
|
174
|
+
*
|
|
175
|
+
* @param devices - A device list used for the searching
|
|
176
|
+
* @param track - A track for the searching criteria
|
|
177
|
+
*
|
|
178
|
+
* @returns `MediaDeviceInfo` if found, otherwise `undefined`
|
|
179
|
+
*
|
|
180
|
+
* @beta
|
|
181
|
+
*/
|
|
182
|
+
export const findMediaInputFromMediaStreamTrack = (devices) => (track) => {
|
|
183
|
+
if (!devices.length || !track) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
const settings = track.getSettings();
|
|
187
|
+
const devicesWithSameKind = devices.filter(d => d.kind === `${track.kind}input`);
|
|
188
|
+
// There is no need to compare since it is only device
|
|
189
|
+
const onlyDevice = devicesWithSameKind[0];
|
|
190
|
+
if (devicesWithSameKind.length === 1 && onlyDevice) {
|
|
191
|
+
onlyDevice.settings = settings;
|
|
192
|
+
onlyDevice.toJSON = () => toMediaDeviceInfoLikeJSON(onlyDevice);
|
|
193
|
+
return onlyDevice;
|
|
194
|
+
}
|
|
195
|
+
const device = devicesWithSameKind.find(compareMediaDeviceToMediaTrack(track));
|
|
196
|
+
if (device) {
|
|
197
|
+
device.settings = settings;
|
|
198
|
+
device.toJSON = () => toMediaDeviceInfoLikeJSON(device);
|
|
199
|
+
return device;
|
|
200
|
+
}
|
|
201
|
+
return device;
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* Find media input from media stream
|
|
205
|
+
*
|
|
206
|
+
* @param devices - A list of media devices from
|
|
207
|
+
* `navigator.mediaDevices.enumerateDevices()`. Will be used for search target
|
|
208
|
+
* @param stream - A media stream used for the search criteria
|
|
209
|
+
*
|
|
210
|
+
* @returns A object may contains the devices
|
|
211
|
+
*
|
|
212
|
+
* @beta
|
|
213
|
+
*/
|
|
214
|
+
export const findMediaInputFromStream = (devices) => (stream) => {
|
|
215
|
+
if (!stream || !devices.length) {
|
|
216
|
+
return {};
|
|
217
|
+
}
|
|
218
|
+
const findMediaInput = findMediaInputFromMediaStreamTrack(devices);
|
|
219
|
+
return {
|
|
220
|
+
audioInput: findMediaInput(...stream.getAudioTracks()),
|
|
221
|
+
videoInput: findMediaInput(...stream.getVideoTracks()),
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
export const compareDeviceConstraintAndTrackDevice = (device) => (constraint) => {
|
|
225
|
+
if (!device || !constraint) {
|
|
226
|
+
return Boolean(device) === Boolean(constraint);
|
|
227
|
+
}
|
|
228
|
+
const normalized = normalizeDeviceConstraint(constraint);
|
|
229
|
+
if (normalized) {
|
|
230
|
+
const requestingDevices = Object.values(normalized).flatMap(t => (Array.isArray(t) ? t : [t]));
|
|
231
|
+
const compare = compareDevices(device, device.label ? 'label' : 'deviceId');
|
|
232
|
+
return requestingDevices.some(compare);
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
};
|
|
236
|
+
export const isRequestedResolution = (request, response) => {
|
|
237
|
+
if (!response || !request) {
|
|
238
|
+
return Boolean(response) === Boolean(request);
|
|
239
|
+
}
|
|
240
|
+
if (isInputConstraintSet(request)) {
|
|
241
|
+
const [width] = extractConstrainNumber('width')(request);
|
|
242
|
+
const [height] = extractConstrainNumber('height')(request);
|
|
243
|
+
const { width: responseWidth = 0, height: responseHeight = 0 } = response.settings ?? {};
|
|
244
|
+
const portrait = responseHeight >= responseWidth;
|
|
245
|
+
const resultWidth = portrait ? responseHeight : responseWidth;
|
|
246
|
+
const resultHeight = portrait ? responseWidth : responseHeight;
|
|
247
|
+
const sameWidth = satisfyConstrainNumber(width, resultWidth);
|
|
248
|
+
const sameHeight = satisfyConstrainNumber(height, resultHeight);
|
|
249
|
+
return sameWidth && sameHeight;
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
};
|
|
253
|
+
export const isRequestedInputDevice = (request, response) => {
|
|
254
|
+
if (!response || !request) {
|
|
255
|
+
return Boolean(response) === Boolean(request);
|
|
256
|
+
}
|
|
257
|
+
const compareDevice = compareDeviceConstraintAndTrackDevice(response);
|
|
258
|
+
if (isInputConstraintSet(request)) {
|
|
259
|
+
if (isConstraintSetDevice(request.device)) {
|
|
260
|
+
const isSameDevice = compareDevice(request.device);
|
|
261
|
+
return isSameDevice;
|
|
262
|
+
}
|
|
263
|
+
if (request.deviceId) {
|
|
264
|
+
const { deviceId } = response;
|
|
265
|
+
if (!deviceId) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
if (isConstraintDOMString(request.deviceId)) {
|
|
269
|
+
const ids = toArray(request.deviceId);
|
|
270
|
+
return ids.some(id => deviceId === id);
|
|
271
|
+
}
|
|
272
|
+
if (isConstrainDOMStringParameters(request.deviceId)) {
|
|
273
|
+
return Object.values(request.deviceId)
|
|
274
|
+
.flatMap(toArray)
|
|
275
|
+
.some(id => id === deviceId);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const facingMode = response.settings?.facingMode;
|
|
279
|
+
if (request.facingMode && facingMode) {
|
|
280
|
+
const [facingModes] = extractConstrainString('facingMode')(request);
|
|
281
|
+
return facingModes?.some(fm => fm === facingMode) ?? true;
|
|
282
|
+
}
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
if (isConstraintSetDevice(request)) {
|
|
286
|
+
return compareDevice(request);
|
|
287
|
+
}
|
|
288
|
+
return !!response;
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Compare request and the input to see if the request has been fulfilled
|
|
292
|
+
*/
|
|
293
|
+
export const isRequestedInputTrack = (request, current) => {
|
|
294
|
+
if (!request || !current) {
|
|
295
|
+
return Boolean(request) === Boolean(current);
|
|
296
|
+
}
|
|
297
|
+
if (isMediaDeviceInfo(current)) {
|
|
298
|
+
return isRequestedInputDevice(request, current);
|
|
299
|
+
}
|
|
300
|
+
if (isMediaStreamTrack(current)) {
|
|
301
|
+
const device = extractDeviceInfo(current);
|
|
302
|
+
return isRequestedInputDevice(request, device);
|
|
303
|
+
}
|
|
304
|
+
return true;
|
|
305
|
+
};
|
|
306
|
+
const extractDevice = extractConstraintsWithKeys(['device', 'deviceId']);
|
|
307
|
+
export const hasRequestingDevice = (request, kind, currentDevices) => {
|
|
308
|
+
if (!request) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
const { device: [devices], deviceId: [deviceIds], } = extractDevice(request);
|
|
312
|
+
// No device specified
|
|
313
|
+
if ((!devices || devices.length === 0) &&
|
|
314
|
+
(!deviceIds || deviceIds.length === 0)) {
|
|
315
|
+
return currentDevices.some(device => device.kind === kind);
|
|
316
|
+
}
|
|
317
|
+
// Has device specified
|
|
318
|
+
const find = (device) => findDevice(device)(currentDevices);
|
|
319
|
+
return (devices?.some(device => find(device)) ??
|
|
320
|
+
deviceIds?.some(deviceId => find({ kind, deviceId, label: '', groupId: '' })) ??
|
|
321
|
+
false);
|
|
322
|
+
};
|
|
323
|
+
/**
|
|
324
|
+
* Decide if we should send a new gUM request based on the inputs
|
|
325
|
+
*
|
|
326
|
+
* @param request - Requesting input device constraint
|
|
327
|
+
* @param tracks - The current media stream tracks
|
|
328
|
+
* @param currentDevices - The current list of device of the same kind of the
|
|
329
|
+
* request
|
|
330
|
+
*
|
|
331
|
+
* @returns `true` means the request should be conducted, otherwise `false`.
|
|
332
|
+
*/
|
|
333
|
+
export const shouldRequestDevice = (request, tracks, currentDevices) => {
|
|
334
|
+
if (!request || !currentDevices.some(device => device.label)) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
const { kind } = currentDevices[0] ?? {};
|
|
338
|
+
if (!kind || currentDevices.some(device => device.kind !== kind)) {
|
|
339
|
+
throw new Error('Expect a single kind of device');
|
|
340
|
+
}
|
|
341
|
+
const [track] = tracks;
|
|
342
|
+
if (tracks.length === 0 ||
|
|
343
|
+
tracks.some(track => track.readyState === 'ended')) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
if (isRequestedInputTrack(request, track)) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
if (hasRequestingDevice(request, kind, currentDevices)) {
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
return false;
|
|
353
|
+
};
|
|
354
|
+
export const resolveInputDevice = (tracksOrDevice, findInput) => {
|
|
355
|
+
if (isMediaDeviceInfo(tracksOrDevice)) {
|
|
356
|
+
return tracksOrDevice;
|
|
357
|
+
}
|
|
358
|
+
if (Array.isArray(tracksOrDevice)) {
|
|
359
|
+
const [track] = tracksOrDevice;
|
|
360
|
+
if (track?.readyState === 'live') {
|
|
361
|
+
return findInput(track) ?? track;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return undefined;
|
|
365
|
+
};
|
|
366
|
+
export const isStreamingRequestedDevicesBase = (request, tracksOrDevices, devices) => {
|
|
367
|
+
const audioRequest = relaxInputConstraint(request.audio, devices);
|
|
368
|
+
const videoRequest = relaxInputConstraint(request.video, devices);
|
|
369
|
+
const findInput = findMediaInputFromMediaStreamTrack(devices);
|
|
370
|
+
const audioInput = resolveInputDevice(tracksOrDevices.audio, findInput);
|
|
371
|
+
const videoInput = resolveInputDevice(tracksOrDevices.video, findInput);
|
|
372
|
+
const audio = isRequestedInputTrack(audioRequest, audioInput);
|
|
373
|
+
const video = isRequestedInputTrack(videoRequest, videoInput);
|
|
374
|
+
return { audio, video };
|
|
375
|
+
};
|
|
376
|
+
/**
|
|
377
|
+
* Check if provided request has already been fulfilled
|
|
378
|
+
*
|
|
379
|
+
* @param request - A media request constraints
|
|
380
|
+
* @param stream - Current media stream
|
|
381
|
+
*
|
|
382
|
+
* @returns
|
|
383
|
+
* audio - The stream is using the same device as requested if `true`
|
|
384
|
+
* video - The stream is using the same device as requested if `true`
|
|
385
|
+
*/
|
|
386
|
+
export const isStreamingRequestedDevices = (request, stream, devices) => isStreamingRequestedDevicesBase(request, {
|
|
387
|
+
audio: stream?.getAudioTracks(),
|
|
388
|
+
video: stream?.getVideoTracks(),
|
|
389
|
+
}, devices);
|
|
390
|
+
/**
|
|
391
|
+
* Find in the list of devices which has permissions granted
|
|
392
|
+
*
|
|
393
|
+
* From MDN:
|
|
394
|
+
* For security reasons,the label field is always blank unless an active media stream
|
|
395
|
+
* exists or the user has granted persistent permission for media device access.
|
|
396
|
+
*
|
|
397
|
+
* @param devices - A list of media devices from
|
|
398
|
+
* `navigator.mediaDevices.enumerateDevices()`. Will be used for search target
|
|
399
|
+
*
|
|
400
|
+
* @returns List of devices that has permission granted
|
|
401
|
+
*
|
|
402
|
+
* @beta
|
|
403
|
+
*/
|
|
404
|
+
export const findPermissionGrantedDevices = (devices) => devices.filter(isDeviceGranted);
|
|
405
|
+
/**
|
|
406
|
+
* Use provided devices to guess the permission state. When there is no active
|
|
407
|
+
* stream and there is no device with label, 'prompt' will be returned
|
|
408
|
+
* otherwise, 'denied'.
|
|
409
|
+
*
|
|
410
|
+
* @param devices - The current devices
|
|
411
|
+
* @param anyActiveStream - Has ever got an active stream to help the fallback to
|
|
412
|
+
* guess the state more accurately
|
|
413
|
+
*/
|
|
414
|
+
export const toPermissionState = (devices, anyActiveStream = false) => {
|
|
415
|
+
const granted = devices.some(device => device.label);
|
|
416
|
+
if (anyActiveStream) {
|
|
417
|
+
return granted ? 'granted' : 'denied';
|
|
418
|
+
}
|
|
419
|
+
return granted ? 'granted' : 'prompt';
|
|
420
|
+
};
|
|
421
|
+
/**
|
|
422
|
+
* A wrapper for `navigator.permissions.query` with fallback to use
|
|
423
|
+
* `navigator.mediaDevices.enumerateDevices` to guess the `PermissionState`
|
|
424
|
+
*
|
|
425
|
+
* @param anyActiveStream - Has ever got an active stream to help the fallback to
|
|
426
|
+
* guess the state more accurately
|
|
427
|
+
*/
|
|
428
|
+
export const getInputDevicePermissionState = async (anyActiveStream = false) => {
|
|
429
|
+
try {
|
|
430
|
+
// @ts-expect-error -- Typings/dom.d.ts change blocks upgrade to typescript 4.4
|
|
431
|
+
const video = await navigator.permissions.query({ name: 'camera' });
|
|
432
|
+
// @ts-expect-error -- Typings/dom.d.ts change blocks upgrade to typescript 4.4
|
|
433
|
+
const audio = await navigator.permissions.query({ name: 'microphone' });
|
|
434
|
+
return { video: video.state, audio: audio.state };
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
const devices = await getDevices();
|
|
438
|
+
return ['videoinput', 'audioinput'].reduce((permission, kind) => {
|
|
439
|
+
const state = toPermissionState(devices.filter(device => device.kind === kind), anyActiveStream);
|
|
440
|
+
permission[kind === 'audioinput' ? 'audio' : 'video'] = state;
|
|
441
|
+
return permission;
|
|
442
|
+
}, { video: 'prompt', audio: 'prompt' });
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
/**
|
|
446
|
+
* Find current audio output id to be used to set as sinkId
|
|
447
|
+
*
|
|
448
|
+
* If you set the stale deviceId to setSink it will throw exception.
|
|
449
|
+
* So we want to check if audio output id still exist.
|
|
450
|
+
*
|
|
451
|
+
* @param audioOutput - Audio output as `MediaDeviceInfo`
|
|
452
|
+
* @param devices - A list of media devices from
|
|
453
|
+
* `navigator.mediaDevices.enumerateDevices()`. Will be used for search target
|
|
454
|
+
*
|
|
455
|
+
* @returns Audio output id or empty string if couldn't find it.
|
|
456
|
+
*
|
|
457
|
+
* @beta
|
|
458
|
+
*/
|
|
459
|
+
export const findCurrentAudioOutputId = (audioOutput, devices) => {
|
|
460
|
+
let selectedAudioOutputDeviceId = '';
|
|
461
|
+
if (audioOutput?.deviceId && devices) {
|
|
462
|
+
const latestSelectedAudioInput = findDevice(audioOutput)(devices);
|
|
463
|
+
if (latestSelectedAudioInput) {
|
|
464
|
+
selectedAudioOutputDeviceId = latestSelectedAudioInput.deviceId;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return selectedAudioOutputDeviceId;
|
|
468
|
+
};
|
|
469
|
+
/**
|
|
470
|
+
* Find videoinput device id in the stream
|
|
471
|
+
*
|
|
472
|
+
* @param stream - Media stream to do the lookup
|
|
473
|
+
*
|
|
474
|
+
* @returns A object may contains the devices
|
|
475
|
+
*
|
|
476
|
+
* @beta
|
|
477
|
+
*/
|
|
478
|
+
export const findCurrentVideoInputDeviceIdFromStream = (stream) => {
|
|
479
|
+
const [videoTrack] = stream.getVideoTracks();
|
|
480
|
+
return videoTrack && toMediaDeviceInfoLike(videoTrack)?.deviceId;
|
|
481
|
+
};
|
|
482
|
+
/**
|
|
483
|
+
* Finds device with given deviceId
|
|
484
|
+
*
|
|
485
|
+
* @param devices - A list of media devices from
|
|
486
|
+
* `navigator.mediaDevices.enumerateDevices()`. Will be used for search target
|
|
487
|
+
* @param deviceId - id that represents desired device
|
|
488
|
+
*
|
|
489
|
+
* @returns device with given deviceId
|
|
490
|
+
*
|
|
491
|
+
* @beta
|
|
492
|
+
*/
|
|
493
|
+
export const findDeviceWithDeviceId = (devices, deviceId) => devices.find(d => d.deviceId === deviceId);
|
|
494
|
+
/**
|
|
495
|
+
* Find in the list of devices with given MediaDeviceKind
|
|
496
|
+
*
|
|
497
|
+
* @param devices - A list of media devices from
|
|
498
|
+
* `navigator.mediaDevices.enumerateDevices()`. Will be used for search target
|
|
499
|
+
* @param kind - A list of media devices from
|
|
500
|
+
* `navigator.mediaDevices.enumerateDevices()`. Will be used for search target
|
|
501
|
+
*
|
|
502
|
+
* @returns List of devices that have required MediaDeviceKind
|
|
503
|
+
*
|
|
504
|
+
* @beta
|
|
505
|
+
*/
|
|
506
|
+
export const findDevicesByKind = (kind) => (devices) => devices.filter(isSameDeviceKind(kind));
|
|
507
|
+
/**
|
|
508
|
+
* Find in the list of devices only the audio input ones
|
|
509
|
+
*
|
|
510
|
+
* @param devices - A list of media devices from
|
|
511
|
+
* `navigator.mediaDevices.enumerateDevices()`. Will be used for search target
|
|
512
|
+
*
|
|
513
|
+
* @returns List of devices that are audio inputs
|
|
514
|
+
*
|
|
515
|
+
* @beta
|
|
516
|
+
*/
|
|
517
|
+
export const findAudioInputDevices = findDevicesByKind(MediaDeviceKinds.AUDIOINPUT);
|
|
518
|
+
/**
|
|
519
|
+
* Find in the list of devices only the video input ones
|
|
520
|
+
*
|
|
521
|
+
* @param devices - A list of media devices from
|
|
522
|
+
* `navigator.mediaDevices.enumerateDevices()`. Will be used for search target
|
|
523
|
+
*
|
|
524
|
+
* @returns List of devices that are video inputs
|
|
525
|
+
*
|
|
526
|
+
* @beta
|
|
527
|
+
*/
|
|
528
|
+
export const findVideoInputDevices = findDevicesByKind(MediaDeviceKinds.VIDEOINPUT);
|
|
529
|
+
/**
|
|
530
|
+
* Find in the list of devices only the audio output ones
|
|
531
|
+
*
|
|
532
|
+
* @param devices - A list of media devices from
|
|
533
|
+
* `navigator.mediaDevices.enumerateDevices()`. Will be used for search target
|
|
534
|
+
*
|
|
535
|
+
* @returns List of devices that are audio outputs
|
|
536
|
+
*
|
|
537
|
+
* @beta
|
|
538
|
+
*/
|
|
539
|
+
export const findAudioOutputDevices = findDevicesByKind(MediaDeviceKinds.AUDIOOUTPUT);
|
|
540
|
+
/**
|
|
541
|
+
* Set `MediaStreamTrack['enabled']` according to `mute` param for the provided `stream`
|
|
542
|
+
*
|
|
543
|
+
* @param stream - Media stream
|
|
544
|
+
* @param mute - disable or enable the audio stream
|
|
545
|
+
* @param mediaType - Can either be 'audio', 'video' or 'all'
|
|
546
|
+
* @defaultValue
|
|
547
|
+
* 'all'
|
|
548
|
+
*
|
|
549
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/enabled
|
|
550
|
+
*
|
|
551
|
+
* @beta
|
|
552
|
+
*/
|
|
553
|
+
export const muteStreamTrack = (stream) => (mute, mediaType = 'all') => {
|
|
554
|
+
switch (mediaType) {
|
|
555
|
+
case 'audio':
|
|
556
|
+
stream?.getAudioTracks().forEach(track => {
|
|
557
|
+
track.enabled = !mute;
|
|
558
|
+
});
|
|
559
|
+
break;
|
|
560
|
+
case 'video':
|
|
561
|
+
stream?.getVideoTracks().forEach(track => {
|
|
562
|
+
track.enabled = !mute;
|
|
563
|
+
});
|
|
564
|
+
break;
|
|
565
|
+
default:
|
|
566
|
+
stream?.getTracks().forEach(track => {
|
|
567
|
+
track.enabled = !mute;
|
|
568
|
+
});
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
/**
|
|
573
|
+
* Stops all tracks in the given stream
|
|
574
|
+
*
|
|
575
|
+
* Immediately after calling stop(), the readyState property is set to `ended`.
|
|
576
|
+
* Note that the `ended` event will not be fired in this situation
|
|
577
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/stop#description
|
|
578
|
+
*
|
|
579
|
+
* @param stream - `MediaStream` which we mutate
|
|
580
|
+
* @param onStopped - callback to be called when the track is stopped
|
|
581
|
+
*/
|
|
582
|
+
export const stopMediaStream = (stream, onStopped) => {
|
|
583
|
+
if (!stream) {
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
stream.getTracks().forEach(track => {
|
|
587
|
+
track.stop();
|
|
588
|
+
onStopped?.(track);
|
|
589
|
+
});
|
|
590
|
+
};
|
|
591
|
+
/**
|
|
592
|
+
* Checks that tracks with a given type are enabled in the stream
|
|
593
|
+
*
|
|
594
|
+
* @param stream - `MediaStream` used for comparison
|
|
595
|
+
* @param type - `MediaInput` used for comparison
|
|
596
|
+
*
|
|
597
|
+
* @returns Return true when all the tracks are enabled false otherwise
|
|
598
|
+
*
|
|
599
|
+
* @beta
|
|
600
|
+
*/
|
|
601
|
+
export const areTracksEnabled = (stream, type) => {
|
|
602
|
+
if (!stream) {
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
const tracks = type === 'audio' ? stream.getAudioTracks() : stream.getVideoTracks();
|
|
606
|
+
return tracks.length > 0 && tracks.every(track => track.enabled);
|
|
607
|
+
};
|
|
608
|
+
/**
|
|
609
|
+
* Check if list contains media inputs
|
|
610
|
+
*
|
|
611
|
+
* @param devices - A list of media devices from
|
|
612
|
+
* `navigator.mediaDevices.enumerateDevices()`. Will be used for search target
|
|
613
|
+
*
|
|
614
|
+
* @returns True when some media inputs false otherwise
|
|
615
|
+
*
|
|
616
|
+
* @beta
|
|
617
|
+
*/
|
|
618
|
+
export const hasAudioOrVideoInputs = (devices) => devices.some(not(isAudioOutput));
|
|
619
|
+
/**
|
|
620
|
+
* Check if list contains audio inputs
|
|
621
|
+
*
|
|
622
|
+
* @param devices - A list of media devices from
|
|
623
|
+
* `navigator.mediaDevices.enumerateDevices()`. Will be used for search target
|
|
624
|
+
*
|
|
625
|
+
* @returns True when some audio inputs false otherwise
|
|
626
|
+
*
|
|
627
|
+
* @beta
|
|
628
|
+
*/
|
|
629
|
+
export const hasAudioInputs = (devices) => devices.some(isAudioInput);
|
|
630
|
+
/**
|
|
631
|
+
* Check if list contains video inputs
|
|
632
|
+
*
|
|
633
|
+
* @param devices - A list of media devices from
|
|
634
|
+
* `navigator.mediaDevices.enumerateDevices()`. Will be used for search target
|
|
635
|
+
*
|
|
636
|
+
* @returns True when some video inputs false otherwise
|
|
637
|
+
*
|
|
638
|
+
* @beta
|
|
639
|
+
*/
|
|
640
|
+
export const hasVideoInputs = (devices) => devices.some(isVideoInput);
|
|
641
|
+
/**
|
|
642
|
+
* Check if provided devices have any granted input device
|
|
643
|
+
*
|
|
644
|
+
* @param devices - The devices to check
|
|
645
|
+
*/
|
|
646
|
+
export const hasAnyGrantedInput = (devices) => devices.some(device => isDeviceGranted(device) &&
|
|
647
|
+
(isVideoInput(device) || isAudioInput(device)));
|
|
648
|
+
/**
|
|
649
|
+
* Check if provided devices have any video and audio inputs
|
|
650
|
+
*
|
|
651
|
+
* @param devices - The devices to lookup
|
|
652
|
+
* @param grantedOnly - When it is `true`, the device label is also taken into
|
|
653
|
+
* consideration
|
|
654
|
+
* @defaultValue `false`
|
|
655
|
+
*
|
|
656
|
+
* @returns A tuple of `[anyAudioInput, anyVideoInput]`, e.g. `[true, false]`
|
|
657
|
+
* means there is audio input but no video input
|
|
658
|
+
*/
|
|
659
|
+
export const hasAnyInputs = (devices, grantedOnly = false) => {
|
|
660
|
+
let anyAudioDevices = false;
|
|
661
|
+
let anyVideoDevices = false;
|
|
662
|
+
for (const device of devices) {
|
|
663
|
+
if (isAudioOutput(device) ||
|
|
664
|
+
(grantedOnly && !isDeviceGranted(device))) {
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
if (isAudioInput(device)) {
|
|
668
|
+
anyAudioDevices = true;
|
|
669
|
+
}
|
|
670
|
+
if (isVideoInput(device)) {
|
|
671
|
+
anyVideoDevices = true;
|
|
672
|
+
}
|
|
673
|
+
if (anyVideoDevices && anyAudioDevices) {
|
|
674
|
+
return [true, true];
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return [anyAudioDevices, anyVideoDevices];
|
|
678
|
+
};
|
|
679
|
+
/**
|
|
680
|
+
* An utility function to check the input device has been changed
|
|
681
|
+
*
|
|
682
|
+
* @param oldInput - The previous input device
|
|
683
|
+
* @param newInput - The current input device
|
|
684
|
+
*/
|
|
685
|
+
export const hasChangedInput = (oldInput, newInput) => {
|
|
686
|
+
if (isMediaDeviceInfo(newInput)) {
|
|
687
|
+
if ((isMediaDeviceInfo(oldInput) &&
|
|
688
|
+
!compareDevices(oldInput)(newInput)) ||
|
|
689
|
+
!oldInput) {
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
else if (oldInput) {
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
return false;
|
|
697
|
+
};
|
|
698
|
+
/**
|
|
699
|
+
* An utility function to check if facing mode is supported by interpreting the
|
|
700
|
+
* device label and the API `getSupportedConstraints`
|
|
701
|
+
*
|
|
702
|
+
* @remarks
|
|
703
|
+
* Only support snooping the label for English
|
|
704
|
+
*
|
|
705
|
+
* @param currentDevices - Current list of devices available
|
|
706
|
+
* @param getSupportedConstraints - A function to get supported constraints,
|
|
707
|
+
* i.e. `navigator.mediaDevices.getSupportedConstraints()`
|
|
708
|
+
* @param tracks - Current video input track
|
|
709
|
+
*/
|
|
710
|
+
export const areMultipleFacingModeSupported = (currentDevices, getSupportedConstraints = () => navigator.mediaDevices.getSupportedConstraints()) => {
|
|
711
|
+
if (!getSupportedConstraints().facingMode) {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
const set = new Set();
|
|
715
|
+
// Device label for built-in cameras usually implies with facing capability,
|
|
716
|
+
// e.g. "Front camera", "Back camera" and "camera2 1, facing front"
|
|
717
|
+
const hasFrontAndBackCameras = currentDevices.some(device => {
|
|
718
|
+
if (device.kind !== 'videoinput') {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
if (device.label.match(/front/i)) {
|
|
722
|
+
set.add('front');
|
|
723
|
+
}
|
|
724
|
+
if (device.label.match(/(back|rear)/i)) {
|
|
725
|
+
set.add('back');
|
|
726
|
+
}
|
|
727
|
+
return set.size > 1;
|
|
728
|
+
});
|
|
729
|
+
if (!hasFrontAndBackCameras) {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
return true;
|
|
733
|
+
};
|
|
734
|
+
/**
|
|
735
|
+
* Interpret the current facing mode from the provided track, and try to get the
|
|
736
|
+
* mode from settings, or use the label to guess the facing mode when facingMode
|
|
737
|
+
* is not supported from settings
|
|
738
|
+
*
|
|
739
|
+
* @param currentTrack - The current video input track
|
|
740
|
+
*/
|
|
741
|
+
export const interpretCurrentFacingMode = (currentTrack) => {
|
|
742
|
+
if (!currentTrack || currentTrack.kind !== 'video') {
|
|
743
|
+
return undefined;
|
|
744
|
+
}
|
|
745
|
+
const settings = currentTrack.getSettings();
|
|
746
|
+
if ('getCapabilities' in currentTrack) {
|
|
747
|
+
const capabilities = currentTrack.getCapabilities();
|
|
748
|
+
if (capabilities.facingMode && capabilities.facingMode.length > 0) {
|
|
749
|
+
return settings.facingMode;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// Firefox does not support `getCapabilities`, and it always returns 'user' by default
|
|
753
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/getCapabilities
|
|
754
|
+
if (settings.facingMode) {
|
|
755
|
+
return settings.facingMode;
|
|
756
|
+
}
|
|
757
|
+
// Firefox Android does not support facingMode from settings, use the label
|
|
758
|
+
// to interpret the facing mode
|
|
759
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings/facingMode
|
|
760
|
+
if (currentTrack.label.match(/front/i)) {
|
|
761
|
+
return 'user';
|
|
762
|
+
}
|
|
763
|
+
if (currentTrack.label.match(/(back|rear)/i)) {
|
|
764
|
+
return 'environment';
|
|
765
|
+
}
|
|
766
|
+
};
|