@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.
@@ -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
+ };