media-devices 0.3.0 → 0.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 +10 -1
- package/README.md +12 -1
- package/dist/media-devices.es.d.ts +2 -0
- package/dist/media-devices.es.js +132 -0
- package/dist/media-devices.es.js.map +1 -0
- package/dist/media-devices.umd.d.ts +2 -0
- package/dist/media-devices.umd.js +2 -0
- package/dist/media-devices.umd.js.map +1 -0
- package/package.json +59 -28
- package/src/__tests__/device-manager.test.ts +0 -15
- package/src/device-manager.ts +2 -6
- package/src/index.ts +5 -2
- package/src/test-utils/mocks.ts +1 -0
- package/dist/device-manager.d.ts +0 -82
- package/dist/enumerate-devices.d.ts +0 -40
- package/dist/get-user-media.d.ts +0 -1
- package/dist/index.d.ts +0 -6
- package/dist/index.js +0 -8
- package/dist/media-devices.cjs.development.js +0 -1179
- package/dist/media-devices.cjs.development.js.map +0 -1
- package/dist/media-devices.cjs.production.min.js +0 -2
- package/dist/media-devices.cjs.production.min.js.map +0 -1
- package/dist/media-devices.esm.js +0 -1170
- package/dist/media-devices.esm.js.map +0 -1
- package/dist/support-detection.d.ts +0 -7
- package/dist/test-utils/index.d.ts +0 -3
- package/dist/test-utils/mocks.d.ts +0 -22
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [UNRELEASED]
|
|
8
|
+
|
|
9
|
+
## [0.4.0] - 2022-05-30
|
|
10
|
+
|
|
11
|
+
### Removed
|
|
12
|
+
|
|
13
|
+
- The deprecated `on('devicechange')` interface has been removed. Use `ondevicechange` instead.
|
|
14
|
+
|
|
7
15
|
## [0.3.0] - 2021-12-04
|
|
8
16
|
|
|
9
17
|
### Added
|
|
@@ -47,7 +55,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
|
47
55
|
- A device list-diffing implementation of `ondevicechange`.
|
|
48
56
|
- Support detection via `supportsMediaDevices()`.
|
|
49
57
|
|
|
50
|
-
[Unreleased]: https://github.com/PsychoLlama/media-devices/compare/v0.
|
|
58
|
+
[Unreleased]: https://github.com/PsychoLlama/media-devices/compare/v0.4.0...HEAD
|
|
59
|
+
[0.4.0]: https://github.com/PsychoLlama/media-devices/compare/v0.3.0...v0.4.0
|
|
51
60
|
[0.3.0]: https://github.com/PsychoLlama/media-devices/compare/v0.2.0...v0.3.0
|
|
52
61
|
[0.2.0]: https://github.com/PsychoLlama/media-devices/compare/v0.1.0...v0.2.0
|
|
53
62
|
[0.1.0]: https://github.com/PsychoLlama/media-devices/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<h1>Media Devices</h1>
|
|
3
3
|
<p>Easily manage media devices in the browser</p>
|
|
4
|
-
|
|
4
|
+
|
|
5
5
|
<div>
|
|
6
6
|
<a href="https://github.com/PsychoLlama/media-devices/actions/workflows/main.yml">
|
|
7
7
|
<img src="https://img.shields.io/github/workflow/status/PsychoLlama/media-devices/CI/main" alt="Build status" />
|
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
16
|
## Purpose
|
|
17
|
+
|
|
17
18
|
`media-devices` wraps the [native browser API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/mediaDevices) and tries to normalize as many cross-browser quirks as possible ([reference](#known-quirks)). It also provides a device list diffing observer that notifies you as devices are added, removed, or updated.
|
|
18
19
|
|
|
19
20
|
## API
|
|
21
|
+
|
|
20
22
|
The API is a carbon copy of [`navigator.mediaDevices`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/mediaDevices), with the exception of `ondevicechange` which was replaced for more bells and whistles.
|
|
21
23
|
|
|
22
24
|
Here's the gist:
|
|
@@ -40,6 +42,7 @@ MediaDevices.ondevicechange = ({ changes }) => {
|
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
### `supportsMediaDevices()`
|
|
45
|
+
|
|
43
46
|
Exported as a separate utility function, this helps determine if your browser supports the `navigator.mediaDevices` API. Be aware that some browsers only expose it on secure sites.
|
|
44
47
|
|
|
45
48
|
```js
|
|
@@ -51,6 +54,7 @@ if (supportsMediaDevices()) {
|
|
|
51
54
|
```
|
|
52
55
|
|
|
53
56
|
### `ondevicechange`
|
|
57
|
+
|
|
54
58
|
`MediaDevices` emits this event whenever the list of devices changes. It passes two things:
|
|
55
59
|
|
|
56
60
|
1. A list of changes
|
|
@@ -92,14 +96,17 @@ Update events are odd. Browsers redact information until the user explicitly gra
|
|
|
92
96
|
---------------
|
|
93
97
|
|
|
94
98
|
## Known Quirks
|
|
99
|
+
|
|
95
100
|
For the curious...
|
|
96
101
|
|
|
97
102
|
### Duplicate Devices
|
|
103
|
+
|
|
98
104
|
Preferred devices are represented through list order: preferred devices are show up first. Chrome has a "feature" where preferred devices are duplicated in the list with a `"default"` device ID. You'll notice some meeting apps get confused this and list them twice in their device dropdowns. I can't find any sources or justified reasoning, and they're the only browser that does it.
|
|
99
105
|
|
|
100
106
|
Since that information is already available in list ordering, `media-devices` strips out the duplicates.
|
|
101
107
|
|
|
102
108
|
### Redacted Device Names
|
|
109
|
+
|
|
103
110
|
Until the first approved `getUserMedia(...)` query, browsers assume you're not trusted enough to see the list of device names. That's fair. Device names are an easy target for user fingerprinting. They patched it by setting `device.label` to an empty string.
|
|
104
111
|
|
|
105
112
|
It works, but it can break certain UIs if they're not carefully checking for empty strings. `media-devices` makes this behavior explicit by setting the label to `null`.
|
|
@@ -107,6 +114,7 @@ It works, but it can break certain UIs if they're not carefully checking for emp
|
|
|
107
114
|
This library updates the device list after a successful `getUserMedia(...)` query ensuring your device state is as accurate as possible.
|
|
108
115
|
|
|
109
116
|
### Redacted Device IDs
|
|
117
|
+
|
|
110
118
|
According to the spec, device IDs are meant to persist until the user clears site data, which is a dream come true if you're one of those assholes writing fingerprinting software. Some browsers thwart those efforts by redacting the device ID until you've been approved a `getUserMedia(...)` request.
|
|
111
119
|
|
|
112
120
|
That makes it hard to tell whether the device list actually changed. This library handles the heavy lifting of fuzzy matching devices to determine if new ones were added, others were removed, or if you just got permission to see the real ID/label.
|
|
@@ -114,16 +122,19 @@ That makes it hard to tell whether the device list actually changed. This librar
|
|
|
114
122
|
Device IDs are set to `null` in this case.
|
|
115
123
|
|
|
116
124
|
### Missing Group IDs
|
|
125
|
+
|
|
117
126
|
As of Safari v14, even with permissions, the browser doesn't provide group IDs. Why? Because they're monsters.
|
|
118
127
|
|
|
119
128
|
Group IDs are `null` in Safari.
|
|
120
129
|
|
|
121
130
|
### Hidden Devices
|
|
131
|
+
|
|
122
132
|
Chrome and Safari only show the first of each device type (mic, camera, speakers) until `getUserMedia(...)` is approved. Other options are hidden. If you have 10 cameras, you'll only see the first until you're authorized. Even then, Chrome only shows you cameras, microphones are still hidden.
|
|
123
133
|
|
|
124
134
|
While we can't work around it, we can automatically identify that old camera in the list of 10 and show the other 9 as added devices.
|
|
125
135
|
|
|
126
136
|
### Speaker Replacement
|
|
137
|
+
|
|
127
138
|
There's a subtle difference between wired speakers vs bluetooth devices. It seems that by default, many computers list internal speakers as a single device (expected), but if you plug in an auxiliary jack, it swaps the label in-place and uses the same device ID (unexpected). So if you plug in headphones but you've also got speakers on your computer, it may only list one device.
|
|
128
139
|
|
|
129
140
|
Bluetooth behaves more as you'd expect. They are shown as distinct devices and you can switch between them.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => {
|
|
4
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
5
|
+
return value;
|
|
6
|
+
};
|
|
7
|
+
function supportsMediaDevices() {
|
|
8
|
+
return typeof navigator !== "undefined" && !!navigator.mediaDevices;
|
|
9
|
+
}
|
|
10
|
+
function getMediaDevicesApi() {
|
|
11
|
+
if (!supportsMediaDevices()) {
|
|
12
|
+
throw new Error(`The media devices API isn't supported here.`);
|
|
13
|
+
}
|
|
14
|
+
return navigator.mediaDevices;
|
|
15
|
+
}
|
|
16
|
+
async function enumerateDevices() {
|
|
17
|
+
const devices = await getMediaDevicesApi().enumerateDevices();
|
|
18
|
+
return devices.filter(isPhysicalDevice).map(normalizeDeviceInfo);
|
|
19
|
+
}
|
|
20
|
+
function isPhysicalDevice(device) {
|
|
21
|
+
return device.deviceId !== "default";
|
|
22
|
+
}
|
|
23
|
+
function normalizeDeviceInfo(device) {
|
|
24
|
+
return {
|
|
25
|
+
label: device.label || null,
|
|
26
|
+
kind: device.kind,
|
|
27
|
+
deviceId: device.deviceId || null,
|
|
28
|
+
groupId: device.groupId || null
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
var DeviceKind = /* @__PURE__ */ ((DeviceKind2) => {
|
|
32
|
+
DeviceKind2["VideoInput"] = "videoinput";
|
|
33
|
+
DeviceKind2["AudioInput"] = "audioinput";
|
|
34
|
+
DeviceKind2["AudioOutput"] = "audiooutput";
|
|
35
|
+
return DeviceKind2;
|
|
36
|
+
})(DeviceKind || {});
|
|
37
|
+
async function getUserMedia(constraints) {
|
|
38
|
+
return getMediaDevicesApi().getUserMedia(constraints);
|
|
39
|
+
}
|
|
40
|
+
class DeviceManager {
|
|
41
|
+
constructor() {
|
|
42
|
+
__publicField(this, "_knownDevices", []);
|
|
43
|
+
__publicField(this, "_gainedScreenAccessOnce", false);
|
|
44
|
+
__publicField(this, "ondevicechange", null);
|
|
45
|
+
__publicField(this, "getUserMedia", async (constraints) => {
|
|
46
|
+
const stream = await getUserMedia(constraints);
|
|
47
|
+
this.enumerateDevices();
|
|
48
|
+
return stream;
|
|
49
|
+
});
|
|
50
|
+
__publicField(this, "getDisplayMedia", async (constraints) => {
|
|
51
|
+
const stream = await getMediaDevicesApi().getDisplayMedia(constraints);
|
|
52
|
+
if (!this._gainedScreenAccessOnce) {
|
|
53
|
+
this._gainedScreenAccessOnce = true;
|
|
54
|
+
this.enumerateDevices();
|
|
55
|
+
}
|
|
56
|
+
return stream;
|
|
57
|
+
});
|
|
58
|
+
__publicField(this, "enumerateDevices", async () => {
|
|
59
|
+
const devices = await enumerateDevices();
|
|
60
|
+
this._checkForDeviceChanges(devices);
|
|
61
|
+
return devices;
|
|
62
|
+
});
|
|
63
|
+
__publicField(this, "getSupportedConstraints", () => {
|
|
64
|
+
return getMediaDevicesApi().getSupportedConstraints();
|
|
65
|
+
});
|
|
66
|
+
if (supportsMediaDevices()) {
|
|
67
|
+
getMediaDevicesApi().addEventListener("devicechange", () => {
|
|
68
|
+
if (this.ondevicechange) {
|
|
69
|
+
return this.enumerateDevices();
|
|
70
|
+
}
|
|
71
|
+
return Promise.resolve();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
_checkForDeviceChanges(newDevices) {
|
|
76
|
+
var _a;
|
|
77
|
+
const oldDevices = this._knownDevices;
|
|
78
|
+
this._knownDevices = newDevices;
|
|
79
|
+
const changes = this._calculateDeviceDiff(newDevices, oldDevices);
|
|
80
|
+
if (changes.length) {
|
|
81
|
+
(_a = this.ondevicechange) == null ? void 0 : _a.call(this, { changes, devices: newDevices });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
_calculateDeviceDiff(newDevices, oldDevices) {
|
|
85
|
+
const removals = oldDevices.slice();
|
|
86
|
+
const updates = [];
|
|
87
|
+
const additions = newDevices.filter((newDevice) => {
|
|
88
|
+
const oldDeviceIndex = removals.findIndex((oldDevice) => {
|
|
89
|
+
return isIdenticalDevice(newDevice, oldDevice);
|
|
90
|
+
});
|
|
91
|
+
if (oldDeviceIndex > -1) {
|
|
92
|
+
const [oldDevice] = removals.splice(oldDeviceIndex, 1);
|
|
93
|
+
if (newDevice.label !== oldDevice.label) {
|
|
94
|
+
const update = {
|
|
95
|
+
type: OperationType.Update,
|
|
96
|
+
newInfo: newDevice,
|
|
97
|
+
oldInfo: oldDevice
|
|
98
|
+
};
|
|
99
|
+
updates.push(update);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return oldDeviceIndex === -1;
|
|
103
|
+
});
|
|
104
|
+
return [
|
|
105
|
+
...updates,
|
|
106
|
+
...removals.map((device) => {
|
|
107
|
+
return { type: OperationType.Remove, device };
|
|
108
|
+
}),
|
|
109
|
+
...additions.map((device) => {
|
|
110
|
+
return { type: OperationType.Add, device };
|
|
111
|
+
})
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function isIdenticalDevice(newDevice, oldDevice) {
|
|
116
|
+
if (oldDevice.deviceId) {
|
|
117
|
+
return newDevice.deviceId === oldDevice.deviceId;
|
|
118
|
+
}
|
|
119
|
+
function toCrudeId(device) {
|
|
120
|
+
return `${device.kind}:${device.groupId}`;
|
|
121
|
+
}
|
|
122
|
+
return toCrudeId(newDevice) === toCrudeId(oldDevice);
|
|
123
|
+
}
|
|
124
|
+
var OperationType = /* @__PURE__ */ ((OperationType2) => {
|
|
125
|
+
OperationType2["Add"] = "add";
|
|
126
|
+
OperationType2["Remove"] = "remove";
|
|
127
|
+
OperationType2["Update"] = "update";
|
|
128
|
+
return OperationType2;
|
|
129
|
+
})(OperationType || {});
|
|
130
|
+
var index = new DeviceManager();
|
|
131
|
+
export { DeviceKind, OperationType, index as default, supportsMediaDevices };
|
|
132
|
+
//# sourceMappingURL=media-devices.es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-devices.es.js","sources":["../src/support-detection.ts","../src/enumerate-devices.ts","../src/get-user-media.ts","../src/device-manager.ts","../src/index.ts"],"sourcesContent":["/**\n * Not all browsers support media devices, and some restrict access for\n * insecure sites and private contexts. This is often reflected by removing\n * the `mediaDevices` API entirely.\n */\nexport function supportsMediaDevices() {\n return typeof navigator !== 'undefined' && !!navigator.mediaDevices;\n}\n\nexport function getMediaDevicesApi() {\n if (!supportsMediaDevices()) {\n throw new Error(`The media devices API isn't supported here.`);\n }\n\n return navigator.mediaDevices;\n}\n","import { getMediaDevicesApi } from './support-detection';\n\n/**\n * A normalization layer over `MediaDevices.enumerateDevices()`:\n * https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo\n *\n * The API is fraught with cross-browser quirks and fingerprinting blocks.\n * This interface seeks to normalize some of those quirks and make the\n * security tradeoffs obvious.\n */\nexport default async function enumerateDevices(): Promise<Array<DeviceInfo>> {\n const devices = await getMediaDevicesApi().enumerateDevices();\n return devices.filter(isPhysicalDevice).map(normalizeDeviceInfo);\n}\n\n/**\n * Chromium does this really annoying thing where it duplicates preferred\n * devices by substituting the ID with \"default\". No other browser does this,\n * and preferred devices are already represented by list order.\n *\n * Since those meta-devices don't add relevant information and risk confusing\n * device UIs, I simply remove them.\n */\nfunction isPhysicalDevice(device: MediaDeviceInfo) {\n return device.deviceId !== 'default';\n}\n\n// Make nullable fields explicit.\nfunction normalizeDeviceInfo(device: MediaDeviceInfo): DeviceInfo {\n return {\n label: device.label || null,\n kind: device.kind as DeviceKind,\n deviceId: device.deviceId || null,\n groupId: device.groupId || null,\n };\n}\n\nexport interface DeviceInfo {\n /**\n * The device list is obfuscated until you gain elevated permissions.\n * Browsers will use an empty string for the device label until the first\n * successful `getUserMedia(...)` request.\n */\n label: null | string;\n\n /**\n * A unique identifier persistent across sessions. Note: In Chromium\n * browsers, this can be unset if you haven't received permission for the\n * media resource yet.\n */\n deviceId: null | string;\n\n /**\n * A unique identifier grouping one or more devices together. Two devices\n * with the same group ID symbolise that both devices belong to the same\n * hardware, e.g. a webcam with an integrated microphone. Note: Safari\n * doesn't support group IDs.\n */\n groupId: null | string;\n\n /**\n * Declares the type of media provided. This covers microphones, cameras,\n * and speakers.\n */\n kind: DeviceKind;\n}\n\nexport enum DeviceKind {\n VideoInput = 'videoinput',\n AudioInput = 'audioinput',\n AudioOutput = 'audiooutput',\n}\n","import { getMediaDevicesApi } from './support-detection';\n\nexport default async function getUserMedia(\n constraints: MediaStreamConstraints\n): Promise<MediaStream> {\n return getMediaDevicesApi().getUserMedia(constraints);\n}\n","import enumerateDevices, { DeviceInfo } from './enumerate-devices';\nimport { getMediaDevicesApi, supportsMediaDevices } from './support-detection';\nimport getUserMedia from './get-user-media';\n\n/**\n * Monitors the set of devices for changes and calculates convenient diffs\n * between updates. Steps are taken to handle cross-browser quirks and\n * attempts graceful integration with browser fingerprinting countermeasures.\n */\nexport default class DeviceManager {\n private _knownDevices: Array<DeviceInfo> = [];\n private _gainedScreenAccessOnce = false;\n\n /**\n * Specifies a function to be called whenever the list of available devices\n * changes.\n *\n * Note: this is different from the native event. It passes the changeset\n * and full list of devices as a parameter.\n */\n ondevicechange: null | DeviceChangeListener = null;\n\n constructor() {\n // Listen for changes at the OS level. If the device list changes and\n // someone's around to see it, refresh the device list. Refreshing has\n // a side effect of performing a diff and telling all subscribers about\n // the change.\n if (supportsMediaDevices()) {\n getMediaDevicesApi().addEventListener('devicechange', () => {\n if (this.ondevicechange) {\n return this.enumerateDevices();\n }\n\n return Promise.resolve();\n });\n }\n }\n\n /**\n * Request a live media stream from audio and/or video devices. Streams are\n * configurable through constraints.\n * See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia\n */\n getUserMedia = async (constraints: MediaStreamConstraints) => {\n const stream = await getUserMedia(constraints);\n\n // The browser considers us trusted after the first approved GUM query and\n // allows access to more information in the device list, which is an\n // implicit device change event. Refresh to update the cache.\n //\n // We do this for every GUM request because some browsers only allow\n // access to the subset of devices you've been approved for. While\n // reasonable from a security perspective, it means we're never sure if\n // the cache is stale.\n this.enumerateDevices();\n\n return stream;\n };\n\n /**\n * Ask the user to share their screen. Resolves with a media stream carrying\n * video, and potentially audio from the application window.\n * See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia\n */\n getDisplayMedia = async (\n constraints?: MediaStreamConstraints\n ): Promise<MediaStream> => {\n const stream = await getMediaDevicesApi().getDisplayMedia(constraints);\n\n // Similar to `getUserMedia(...)`, granting access to your screen implies\n // a certain level of trust. Some browsers will remove the fingerprinting\n // protections after the first successful call. However, it's unlikely\n // that another will tell us anything more, so we only refresh devices\n // after the first success.\n if (!this._gainedScreenAccessOnce) {\n this._gainedScreenAccessOnce = true;\n this.enumerateDevices();\n }\n\n return stream;\n };\n\n /**\n * Lists every available hardware device, including microphones, cameras,\n * and speakers (depending on browser support). May contain redacted\n * information depending on application permissions.\n */\n enumerateDevices = async (): Promise<Array<DeviceInfo>> => {\n const devices = await enumerateDevices();\n this._checkForDeviceChanges(devices);\n\n return devices;\n };\n\n /**\n * Returns an object containing every media constraint supported by the\n * browser.\n * See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getSupportedConstraints\n */\n getSupportedConstraints = (): MediaTrackSupportedConstraints => {\n return getMediaDevicesApi().getSupportedConstraints();\n };\n\n private _checkForDeviceChanges(newDevices: Array<DeviceInfo>) {\n const oldDevices = this._knownDevices;\n this._knownDevices = newDevices; // Replace the old devices.\n\n const changes: Array<DeviceChange> = this._calculateDeviceDiff(\n newDevices,\n oldDevices\n );\n\n if (changes.length) {\n this.ondevicechange?.({ changes, devices: newDevices });\n }\n }\n\n /**\n * Note: The device enumeration API may return null values for device IDs\n * and labels. To avoid creating erroneous \"Device Added\" notifications,\n * a best effort should be made to detect when devices are identical.\n *\n * Order is significant. Preferred devices are listed first, which helps\n * correlate devices from permissioned requests with unpermissioned\n * requests.\n */\n private _calculateDeviceDiff(\n newDevices: Array<DeviceInfo>,\n oldDevices: Array<DeviceInfo>\n ): Array<DeviceChange> {\n const removals = oldDevices.slice();\n const updates: Array<DeviceChange> = [];\n\n // If a \"new\" device exists in the list of old devices, then it obviously\n // wasn't just added and clearly we haven't removed it either. It's the\n // same device.\n const additions = newDevices.filter((newDevice) => {\n const oldDeviceIndex = removals.findIndex((oldDevice) => {\n return isIdenticalDevice(newDevice, oldDevice);\n });\n\n // Note: Nasty state mutation hides here.\n // Maps/Sets are out of the question due to poor TS support. Plus IDs\n // are far too unreliable in this context. Iteration and splice() are\n // ugly and gross, but they work.\n if (oldDeviceIndex > -1) {\n const [oldDevice] = removals.splice(oldDeviceIndex, 1);\n\n if (newDevice.label !== oldDevice.label) {\n const update: DeviceUpdateEvent = {\n type: OperationType.Update,\n newInfo: newDevice,\n oldInfo: oldDevice,\n };\n\n updates.push(update);\n }\n }\n\n // Only count it as an \"addition\" if we couldn't find the same device in\n // the older set.\n return oldDeviceIndex === -1;\n });\n\n return [\n ...updates,\n\n // A device was just removed.\n ...removals.map((device) => {\n return { type: OperationType.Remove, device } as DeviceRemoveEvent;\n }),\n\n // A device was just plugged in.\n ...additions.map((device) => {\n return { type: OperationType.Add, device } as DeviceAddEvent;\n }),\n ];\n }\n}\n\n/**\n * Due to fingerprinting countermeasures, the device ID might be an empty\n * string. We have to resort to vague comparisons. After the first successful\n * `getUserMedia(...)` query, the device ID for all related device kinds\n * should be revealed. In that case the new device will have an ID but the old\n * device won't. It should be safe to assume the inverse never happens.\n *\n * Note: Chromium browsers take a private stance by hiding your extra devices.\n * Even if you have a hundred cameras plugged in, until that first GUM query,\n * you'll only see the preferred one. Same for microphones and output devices.\n */\nfunction isIdenticalDevice(newDevice: DeviceInfo, oldDevice: DeviceInfo) {\n if (oldDevice.deviceId) {\n return newDevice.deviceId === oldDevice.deviceId;\n }\n\n // These are the only credible fields we have to go on. It may yield a false\n // positive if you're changing devices before the first GUM query, but since\n // the lists are ordered by priority, that should be unlikely. It's\n // certainly preferable to \"new device\" false positives.\n function toCrudeId(device: DeviceInfo) {\n return `${device.kind}:${device.groupId}`;\n }\n\n return toCrudeId(newDevice) === toCrudeId(oldDevice);\n}\n\nexport type DeviceChange =\n | DeviceAddEvent\n | DeviceRemoveEvent\n | DeviceUpdateEvent;\n\ninterface DeviceAddEvent {\n type: OperationType.Add;\n device: DeviceInfo;\n}\n\ninterface DeviceRemoveEvent {\n type: OperationType.Remove;\n device: DeviceInfo;\n}\n\ninterface DeviceUpdateEvent {\n type: OperationType.Update;\n newInfo: DeviceInfo;\n oldInfo: DeviceInfo;\n}\n\nexport enum OperationType {\n Add = 'add',\n Remove = 'remove',\n Update = 'update',\n}\n\ninterface DeviceChangeListener {\n (update: {\n changes: Array<DeviceChange>;\n devices: Array<DeviceInfo>;\n }): unknown;\n}\n","import DeviceManager from './device-manager';\n\nexport { supportsMediaDevices } from './support-detection';\nexport { DeviceKind } from './enumerate-devices';\nexport { OperationType } from './device-manager';\n\nexport type { DeviceInfo } from './enumerate-devices';\nexport type { DeviceChange } from './device-manager';\n\nexport default new DeviceManager();\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
(function(t,i){typeof exports=="object"&&typeof module!="undefined"?i(exports):typeof define=="function"&&define.amd?define(["exports"],i):(t=typeof globalThis!="undefined"?globalThis:t||self,i(t["media-devices"]={}))})(this,function(t){"use strict";var A=Object.defineProperty;var k=(t,i,s)=>i in t?A(t,i,{enumerable:!0,configurable:!0,writable:!0,value:s}):t[i]=s;var a=(t,i,s)=>(k(t,typeof i!="symbol"?i+"":i,s),s);function i(){return typeof navigator!="undefined"&&!!navigator.mediaDevices}function s(){if(!i())throw new Error("The media devices API isn't supported here.");return navigator.mediaDevices}async function p(){return(await s().enumerateDevices()).filter(h).map(g)}function h(e){return e.deviceId!=="default"}function g(e){return{label:e.label||null,kind:e.kind,deviceId:e.deviceId||null,groupId:e.groupId||null}}var f=(e=>(e.VideoInput="videoinput",e.AudioInput="audioinput",e.AudioOutput="audiooutput",e))(f||{});async function m(e){return s().getUserMedia(e)}class D{constructor(){a(this,"_knownDevices",[]);a(this,"_gainedScreenAccessOnce",!1);a(this,"ondevicechange",null);a(this,"getUserMedia",async n=>{const r=await m(n);return this.enumerateDevices(),r});a(this,"getDisplayMedia",async n=>{const r=await s().getDisplayMedia(n);return this._gainedScreenAccessOnce||(this._gainedScreenAccessOnce=!0,this.enumerateDevices()),r});a(this,"enumerateDevices",async()=>{const n=await p();return this._checkForDeviceChanges(n),n});a(this,"getSupportedConstraints",()=>s().getSupportedConstraints());i()&&s().addEventListener("devicechange",()=>this.ondevicechange?this.enumerateDevices():Promise.resolve())}_checkForDeviceChanges(n){var o;const r=this._knownDevices;this._knownDevices=n;const c=this._calculateDeviceDiff(n,r);c.length&&((o=this.ondevicechange)==null||o.call(this,{changes:c,devices:n}))}_calculateDeviceDiff(n,r){const c=r.slice(),o=[],_=n.filter(d=>{const v=c.findIndex(l=>I(d,l));if(v>-1){const[l]=c.splice(v,1);if(d.label!==l.label){const M={type:u.Update,newInfo:d,oldInfo:l};o.push(M)}}return v===-1});return[...o,...c.map(d=>({type:u.Remove,device:d})),..._.map(d=>({type:u.Add,device:d}))]}}function I(e,n){if(n.deviceId)return e.deviceId===n.deviceId;function r(c){return`${c.kind}:${c.groupId}`}return r(e)===r(n)}var u=(e=>(e.Add="add",e.Remove="remove",e.Update="update",e))(u||{}),y=new D;t.DeviceKind=f,t.OperationType=u,t.default=y,t.supportsMediaDevices=i,Object.defineProperties(t,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})});
|
|
2
|
+
//# sourceMappingURL=media-devices.umd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-devices.umd.js","sources":["../src/support-detection.ts","../src/enumerate-devices.ts","../src/get-user-media.ts","../src/device-manager.ts","../src/index.ts"],"sourcesContent":["/**\n * Not all browsers support media devices, and some restrict access for\n * insecure sites and private contexts. This is often reflected by removing\n * the `mediaDevices` API entirely.\n */\nexport function supportsMediaDevices() {\n return typeof navigator !== 'undefined' && !!navigator.mediaDevices;\n}\n\nexport function getMediaDevicesApi() {\n if (!supportsMediaDevices()) {\n throw new Error(`The media devices API isn't supported here.`);\n }\n\n return navigator.mediaDevices;\n}\n","import { getMediaDevicesApi } from './support-detection';\n\n/**\n * A normalization layer over `MediaDevices.enumerateDevices()`:\n * https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo\n *\n * The API is fraught with cross-browser quirks and fingerprinting blocks.\n * This interface seeks to normalize some of those quirks and make the\n * security tradeoffs obvious.\n */\nexport default async function enumerateDevices(): Promise<Array<DeviceInfo>> {\n const devices = await getMediaDevicesApi().enumerateDevices();\n return devices.filter(isPhysicalDevice).map(normalizeDeviceInfo);\n}\n\n/**\n * Chromium does this really annoying thing where it duplicates preferred\n * devices by substituting the ID with \"default\". No other browser does this,\n * and preferred devices are already represented by list order.\n *\n * Since those meta-devices don't add relevant information and risk confusing\n * device UIs, I simply remove them.\n */\nfunction isPhysicalDevice(device: MediaDeviceInfo) {\n return device.deviceId !== 'default';\n}\n\n// Make nullable fields explicit.\nfunction normalizeDeviceInfo(device: MediaDeviceInfo): DeviceInfo {\n return {\n label: device.label || null,\n kind: device.kind as DeviceKind,\n deviceId: device.deviceId || null,\n groupId: device.groupId || null,\n };\n}\n\nexport interface DeviceInfo {\n /**\n * The device list is obfuscated until you gain elevated permissions.\n * Browsers will use an empty string for the device label until the first\n * successful `getUserMedia(...)` request.\n */\n label: null | string;\n\n /**\n * A unique identifier persistent across sessions. Note: In Chromium\n * browsers, this can be unset if you haven't received permission for the\n * media resource yet.\n */\n deviceId: null | string;\n\n /**\n * A unique identifier grouping one or more devices together. Two devices\n * with the same group ID symbolise that both devices belong to the same\n * hardware, e.g. a webcam with an integrated microphone. Note: Safari\n * doesn't support group IDs.\n */\n groupId: null | string;\n\n /**\n * Declares the type of media provided. This covers microphones, cameras,\n * and speakers.\n */\n kind: DeviceKind;\n}\n\nexport enum DeviceKind {\n VideoInput = 'videoinput',\n AudioInput = 'audioinput',\n AudioOutput = 'audiooutput',\n}\n","import { getMediaDevicesApi } from './support-detection';\n\nexport default async function getUserMedia(\n constraints: MediaStreamConstraints\n): Promise<MediaStream> {\n return getMediaDevicesApi().getUserMedia(constraints);\n}\n","import enumerateDevices, { DeviceInfo } from './enumerate-devices';\nimport { getMediaDevicesApi, supportsMediaDevices } from './support-detection';\nimport getUserMedia from './get-user-media';\n\n/**\n * Monitors the set of devices for changes and calculates convenient diffs\n * between updates. Steps are taken to handle cross-browser quirks and\n * attempts graceful integration with browser fingerprinting countermeasures.\n */\nexport default class DeviceManager {\n private _knownDevices: Array<DeviceInfo> = [];\n private _gainedScreenAccessOnce = false;\n\n /**\n * Specifies a function to be called whenever the list of available devices\n * changes.\n *\n * Note: this is different from the native event. It passes the changeset\n * and full list of devices as a parameter.\n */\n ondevicechange: null | DeviceChangeListener = null;\n\n constructor() {\n // Listen for changes at the OS level. If the device list changes and\n // someone's around to see it, refresh the device list. Refreshing has\n // a side effect of performing a diff and telling all subscribers about\n // the change.\n if (supportsMediaDevices()) {\n getMediaDevicesApi().addEventListener('devicechange', () => {\n if (this.ondevicechange) {\n return this.enumerateDevices();\n }\n\n return Promise.resolve();\n });\n }\n }\n\n /**\n * Request a live media stream from audio and/or video devices. Streams are\n * configurable through constraints.\n * See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia\n */\n getUserMedia = async (constraints: MediaStreamConstraints) => {\n const stream = await getUserMedia(constraints);\n\n // The browser considers us trusted after the first approved GUM query and\n // allows access to more information in the device list, which is an\n // implicit device change event. Refresh to update the cache.\n //\n // We do this for every GUM request because some browsers only allow\n // access to the subset of devices you've been approved for. While\n // reasonable from a security perspective, it means we're never sure if\n // the cache is stale.\n this.enumerateDevices();\n\n return stream;\n };\n\n /**\n * Ask the user to share their screen. Resolves with a media stream carrying\n * video, and potentially audio from the application window.\n * See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia\n */\n getDisplayMedia = async (\n constraints?: MediaStreamConstraints\n ): Promise<MediaStream> => {\n const stream = await getMediaDevicesApi().getDisplayMedia(constraints);\n\n // Similar to `getUserMedia(...)`, granting access to your screen implies\n // a certain level of trust. Some browsers will remove the fingerprinting\n // protections after the first successful call. However, it's unlikely\n // that another will tell us anything more, so we only refresh devices\n // after the first success.\n if (!this._gainedScreenAccessOnce) {\n this._gainedScreenAccessOnce = true;\n this.enumerateDevices();\n }\n\n return stream;\n };\n\n /**\n * Lists every available hardware device, including microphones, cameras,\n * and speakers (depending on browser support). May contain redacted\n * information depending on application permissions.\n */\n enumerateDevices = async (): Promise<Array<DeviceInfo>> => {\n const devices = await enumerateDevices();\n this._checkForDeviceChanges(devices);\n\n return devices;\n };\n\n /**\n * Returns an object containing every media constraint supported by the\n * browser.\n * See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getSupportedConstraints\n */\n getSupportedConstraints = (): MediaTrackSupportedConstraints => {\n return getMediaDevicesApi().getSupportedConstraints();\n };\n\n private _checkForDeviceChanges(newDevices: Array<DeviceInfo>) {\n const oldDevices = this._knownDevices;\n this._knownDevices = newDevices; // Replace the old devices.\n\n const changes: Array<DeviceChange> = this._calculateDeviceDiff(\n newDevices,\n oldDevices\n );\n\n if (changes.length) {\n this.ondevicechange?.({ changes, devices: newDevices });\n }\n }\n\n /**\n * Note: The device enumeration API may return null values for device IDs\n * and labels. To avoid creating erroneous \"Device Added\" notifications,\n * a best effort should be made to detect when devices are identical.\n *\n * Order is significant. Preferred devices are listed first, which helps\n * correlate devices from permissioned requests with unpermissioned\n * requests.\n */\n private _calculateDeviceDiff(\n newDevices: Array<DeviceInfo>,\n oldDevices: Array<DeviceInfo>\n ): Array<DeviceChange> {\n const removals = oldDevices.slice();\n const updates: Array<DeviceChange> = [];\n\n // If a \"new\" device exists in the list of old devices, then it obviously\n // wasn't just added and clearly we haven't removed it either. It's the\n // same device.\n const additions = newDevices.filter((newDevice) => {\n const oldDeviceIndex = removals.findIndex((oldDevice) => {\n return isIdenticalDevice(newDevice, oldDevice);\n });\n\n // Note: Nasty state mutation hides here.\n // Maps/Sets are out of the question due to poor TS support. Plus IDs\n // are far too unreliable in this context. Iteration and splice() are\n // ugly and gross, but they work.\n if (oldDeviceIndex > -1) {\n const [oldDevice] = removals.splice(oldDeviceIndex, 1);\n\n if (newDevice.label !== oldDevice.label) {\n const update: DeviceUpdateEvent = {\n type: OperationType.Update,\n newInfo: newDevice,\n oldInfo: oldDevice,\n };\n\n updates.push(update);\n }\n }\n\n // Only count it as an \"addition\" if we couldn't find the same device in\n // the older set.\n return oldDeviceIndex === -1;\n });\n\n return [\n ...updates,\n\n // A device was just removed.\n ...removals.map((device) => {\n return { type: OperationType.Remove, device } as DeviceRemoveEvent;\n }),\n\n // A device was just plugged in.\n ...additions.map((device) => {\n return { type: OperationType.Add, device } as DeviceAddEvent;\n }),\n ];\n }\n}\n\n/**\n * Due to fingerprinting countermeasures, the device ID might be an empty\n * string. We have to resort to vague comparisons. After the first successful\n * `getUserMedia(...)` query, the device ID for all related device kinds\n * should be revealed. In that case the new device will have an ID but the old\n * device won't. It should be safe to assume the inverse never happens.\n *\n * Note: Chromium browsers take a private stance by hiding your extra devices.\n * Even if you have a hundred cameras plugged in, until that first GUM query,\n * you'll only see the preferred one. Same for microphones and output devices.\n */\nfunction isIdenticalDevice(newDevice: DeviceInfo, oldDevice: DeviceInfo) {\n if (oldDevice.deviceId) {\n return newDevice.deviceId === oldDevice.deviceId;\n }\n\n // These are the only credible fields we have to go on. It may yield a false\n // positive if you're changing devices before the first GUM query, but since\n // the lists are ordered by priority, that should be unlikely. It's\n // certainly preferable to \"new device\" false positives.\n function toCrudeId(device: DeviceInfo) {\n return `${device.kind}:${device.groupId}`;\n }\n\n return toCrudeId(newDevice) === toCrudeId(oldDevice);\n}\n\nexport type DeviceChange =\n | DeviceAddEvent\n | DeviceRemoveEvent\n | DeviceUpdateEvent;\n\ninterface DeviceAddEvent {\n type: OperationType.Add;\n device: DeviceInfo;\n}\n\ninterface DeviceRemoveEvent {\n type: OperationType.Remove;\n device: DeviceInfo;\n}\n\ninterface DeviceUpdateEvent {\n type: OperationType.Update;\n newInfo: DeviceInfo;\n oldInfo: DeviceInfo;\n}\n\nexport enum OperationType {\n Add = 'add',\n Remove = 'remove',\n Update = 'update',\n}\n\ninterface DeviceChangeListener {\n (update: {\n changes: Array<DeviceChange>;\n devices: Array<DeviceInfo>;\n }): unknown;\n}\n","import DeviceManager from './device-manager';\n\nexport { supportsMediaDevices } from './support-detection';\nexport { DeviceKind } from './enumerate-devices';\nexport { OperationType } from './device-manager';\n\nexport type { DeviceInfo } from './enumerate-devices';\nexport type { DeviceChange } from './device-manager';\n\nexport default new DeviceManager();\n"],"names":[],"mappings":"kaAKuC,YAAA,CACrC,MAAO,OAAO,YAAc,aAAe,CAAC,CAAC,UAAU,YACzD,CAEqC,YAAA,CAC/B,GAAA,CAAC,IACG,KAAA,IAAI,OAAM,6CAA6C,EAG/D,MAAO,WAAU,YACnB,CCL6E,kBAAA,CAE3E,MAAO,AADS,MAAM,GAAmB,EAAE,iBAAiB,GAC7C,OAAO,CAAgB,EAAE,IAAI,CAAmB,CACjE,CAUA,WAA0B,EAAyB,CACjD,MAAO,GAAO,WAAa,SAC7B,CAGA,WAA6B,EAAqC,CACzD,MAAA,CACL,MAAO,EAAO,OAAS,KACvB,KAAM,EAAO,KACb,SAAU,EAAO,UAAY,KAC7B,QAAS,EAAO,SAAW,IAAA,CAE/B,CAgCY,GAAA,IAAA,GACG,GAAA,WAAA,aACA,EAAA,WAAA,aACC,EAAA,YAAA,cAHJ,IAAA,GAAA,CAAA,CAAA,ECjEZ,iBACE,EACsB,CACf,MAAA,GAAqB,EAAA,aAAa,CAAW,CACtD,CCGA,MAAqB,CAAc,CAajC,aAAc,CAZN,uBAAmC,CAAA,GAC3C,iCAAkC,IASlC,wBAA8C,MAuB9C,sBAAe,KAAO,IAAwC,CACtD,KAAA,GAAS,KAAM,GAAa,CAAW,EAU7C,YAAK,iBAAiB,EAEf,CAAA,GAQT,yBAAkB,KAChB,IACyB,CACzB,KAAM,GAAS,KAAM,GAAmB,EAAE,gBAAgB,CAAW,EAOjE,MAAC,MAAK,yBACR,MAAK,wBAA0B,GAC/B,KAAK,iBAAiB,GAGjB,CAAA,GAQT,0BAAmB,SAAwC,CACnD,KAAA,GAAU,KAAM,KACtB,YAAK,uBAAuB,CAAO,EAE5B,CAAA,GAQT,iCAA0B,IACjB,EAAA,EAAqB,2BAzE5B,AAAI,KACiB,EAAA,EAAE,iBAAiB,eAAgB,IAChD,KAAK,eACA,KAAK,mBAGP,QAAQ,SAChB,CAEL,CAmEQ,uBAAuB,EAA+B,OAC5D,KAAM,GAAa,KAAK,cACxB,KAAK,cAAgB,EAErB,KAAM,GAA+B,KAAK,qBACxC,EACA,CACF,EAEA,AAAI,EAAQ,QACV,SAAK,iBAAL,kBAAsB,CAAE,UAAS,QAAS,CAAY,GAE1D,CAWQ,qBACN,EACA,EACqB,CACf,KAAA,GAAW,EAAW,QACtB,EAA+B,CAAA,EAK/B,EAAY,EAAW,OAAO,AAAC,GAAc,CACjD,KAAM,GAAiB,EAAS,UAAU,AAAC,GAClC,EAAkB,EAAW,CAAS,CAC9C,EAMD,GAAI,EAAiB,GAAI,CACvB,KAAM,CAAC,GAAa,EAAS,OAAO,EAAgB,CAAC,EAEjD,GAAA,EAAU,QAAU,EAAU,MAAO,CACvC,KAAM,GAA4B,CAChC,KAAM,EAAc,OACpB,QAAS,EACT,QAAS,CAAA,EAGX,EAAQ,KAAK,CAAM,CACrB,CACF,CAIA,MAAO,KAAmB,EAAA,CAC3B,EAEM,MAAA,CACL,GAAG,EAGH,GAAG,EAAS,IAAI,AAAC,GACR,EAAE,KAAM,EAAc,OAAQ,QAAO,EAC7C,EAGD,GAAG,EAAU,IAAI,AAAC,GACT,EAAE,KAAM,EAAc,IAAK,QAAO,EAC1C,CAAA,CAEL,CACF,CAaA,WAA2B,EAAuB,EAAuB,CACvE,GAAI,EAAU,SACL,MAAA,GAAU,WAAa,EAAU,SAO1C,WAAmB,EAAoB,CAC9B,MAAA,GAAG,EAAO,QAAQ,EAAO,SAClC,CAEA,MAAO,GAAU,CAAS,IAAM,EAAU,CAAS,CACrD,CAuBY,GAAA,IAAA,GACJ,GAAA,IAAA,MACG,EAAA,OAAA,SACA,EAAA,OAAA,SAHC,IAAA,GAAA,CAAA,CAAA,EC3NG,EAAA,GAAI"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "media-devices",
|
|
3
3
|
"description": "Easily manage media devices in the browser.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jesse Gibson <JesseTheGibson@gmail.com>",
|
|
7
7
|
"repository": "github:PsychoLlama/media-devices",
|
|
8
8
|
"homepage": "https://github.com/PsychoLlama/media-devices#readme",
|
|
9
|
-
"module": "dist/media-devices.
|
|
10
|
-
"main": "dist/
|
|
11
|
-
"
|
|
9
|
+
"module": "./dist/media-devices.es.js",
|
|
10
|
+
"main": "./dist/media-devices.umd.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"require": "./dist/media-devices.umd.js",
|
|
14
|
+
"import": "./dist/media-devices.es.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
12
17
|
"files": [
|
|
13
18
|
"dist",
|
|
14
19
|
"src"
|
|
@@ -21,48 +26,74 @@
|
|
|
21
26
|
"mediadevices"
|
|
22
27
|
],
|
|
23
28
|
"scripts": {
|
|
24
|
-
"
|
|
25
|
-
"build": "dts build",
|
|
29
|
+
"prepare": "vite build --sourcemap",
|
|
26
30
|
"test": "./bin/run-tests",
|
|
27
|
-
"test:unit": "
|
|
28
|
-
"test:lint": "
|
|
29
|
-
"
|
|
30
|
-
"size": "size-limit",
|
|
31
|
-
"analyze": "size-limit --why"
|
|
31
|
+
"test:unit": "jest --color",
|
|
32
|
+
"test:lint": "eslint src --ext ts --color",
|
|
33
|
+
"test:fmt": "prettier --check src"
|
|
32
34
|
},
|
|
33
35
|
"husky": {
|
|
34
36
|
"hooks": {
|
|
35
|
-
"pre-commit": "
|
|
37
|
+
"pre-commit": "lint-staged"
|
|
36
38
|
}
|
|
37
39
|
},
|
|
40
|
+
"lint-staged": {
|
|
41
|
+
"*.tsx?": [
|
|
42
|
+
"eslint",
|
|
43
|
+
"prettier --check"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
38
46
|
"prettier": {
|
|
39
|
-
"printWidth": 80,
|
|
40
|
-
"semi": true,
|
|
41
47
|
"singleQuote": true,
|
|
42
48
|
"trailingComma": "es5"
|
|
43
49
|
},
|
|
44
50
|
"jest": {
|
|
51
|
+
"preset": "ts-jest",
|
|
45
52
|
"testEnvironment": "jsdom",
|
|
46
53
|
"setupFiles": [
|
|
47
54
|
"<rootDir>/src/test-utils/index.ts"
|
|
48
55
|
]
|
|
49
56
|
},
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
57
|
+
"eslintConfig": {
|
|
58
|
+
"parser": "@typescript-eslint/parser",
|
|
59
|
+
"parserOptions": {
|
|
60
|
+
"sourceType": "module"
|
|
54
61
|
},
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
"overrides": [
|
|
63
|
+
{
|
|
64
|
+
"files": [
|
|
65
|
+
"./**/__tests__/*.ts{x,}"
|
|
66
|
+
],
|
|
67
|
+
"env": {
|
|
68
|
+
"jest": true
|
|
69
|
+
},
|
|
70
|
+
"rules": {
|
|
71
|
+
"@typescript-eslint/no-explicit-any": "off"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
"extends": [
|
|
76
|
+
"eslint:recommended",
|
|
77
|
+
"plugin:@typescript-eslint/recommended"
|
|
78
|
+
],
|
|
79
|
+
"rules": {
|
|
80
|
+
"@typescript-eslint/no-use-before-define": "off"
|
|
58
81
|
}
|
|
59
|
-
|
|
82
|
+
},
|
|
60
83
|
"devDependencies": {
|
|
61
|
-
"@
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
84
|
+
"@types/jest": "27.5.1",
|
|
85
|
+
"@typescript-eslint/eslint-plugin": "5.27.0",
|
|
86
|
+
"@typescript-eslint/parser": "5.27.0",
|
|
87
|
+
"eslint": "8.16.0",
|
|
88
|
+
"husky": "8.0.1",
|
|
89
|
+
"jest": "28.1.0",
|
|
90
|
+
"jest-environment-jsdom": "28.1.0",
|
|
91
|
+
"lint-staged": "12.4.3",
|
|
92
|
+
"prettier": "2.6.2",
|
|
93
|
+
"size-limit": "7.0.8",
|
|
94
|
+
"ts-jest": "28.0.3",
|
|
95
|
+
"typescript": "4.7.2",
|
|
96
|
+
"vite": "2.9.9",
|
|
97
|
+
"vite-dts": "1.0.4"
|
|
67
98
|
}
|
|
68
99
|
}
|
|
@@ -177,21 +177,6 @@ describe('DeviceManager', () => {
|
|
|
177
177
|
expect(getMediaDevicesApi().enumerateDevices).not.toHaveBeenCalled();
|
|
178
178
|
});
|
|
179
179
|
|
|
180
|
-
it('supports deprecated listeners on change events', async () => {
|
|
181
|
-
const devices = new DeviceManager();
|
|
182
|
-
const handler = jest.fn();
|
|
183
|
-
devices.on('devicechange', handler);
|
|
184
|
-
|
|
185
|
-
setDeviceList([{ label: 'Drone' }]);
|
|
186
|
-
const [listener] = (getMediaDevicesApi() as any).listeners('devicechange');
|
|
187
|
-
await listener();
|
|
188
|
-
|
|
189
|
-
expect(handler).toHaveBeenCalledWith(
|
|
190
|
-
[expect.objectContaining({ type: OperationType.Add })],
|
|
191
|
-
expect.any(Array)
|
|
192
|
-
);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
180
|
it('refreshes the device list after a successful GUM query', async () => {
|
|
196
181
|
setDeviceList([{ label: '' }]);
|
|
197
182
|
const { devices } = setup();
|
package/src/device-manager.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import EventEmitter from 'events';
|
|
2
1
|
import enumerateDevices, { DeviceInfo } from './enumerate-devices';
|
|
3
2
|
import { getMediaDevicesApi, supportsMediaDevices } from './support-detection';
|
|
4
3
|
import getUserMedia from './get-user-media';
|
|
@@ -8,7 +7,7 @@ import getUserMedia from './get-user-media';
|
|
|
8
7
|
* between updates. Steps are taken to handle cross-browser quirks and
|
|
9
8
|
* attempts graceful integration with browser fingerprinting countermeasures.
|
|
10
9
|
*/
|
|
11
|
-
export default class DeviceManager
|
|
10
|
+
export default class DeviceManager {
|
|
12
11
|
private _knownDevices: Array<DeviceInfo> = [];
|
|
13
12
|
private _gainedScreenAccessOnce = false;
|
|
14
13
|
|
|
@@ -22,15 +21,13 @@ export default class DeviceManager extends EventEmitter {
|
|
|
22
21
|
ondevicechange: null | DeviceChangeListener = null;
|
|
23
22
|
|
|
24
23
|
constructor() {
|
|
25
|
-
super();
|
|
26
|
-
|
|
27
24
|
// Listen for changes at the OS level. If the device list changes and
|
|
28
25
|
// someone's around to see it, refresh the device list. Refreshing has
|
|
29
26
|
// a side effect of performing a diff and telling all subscribers about
|
|
30
27
|
// the change.
|
|
31
28
|
if (supportsMediaDevices()) {
|
|
32
29
|
getMediaDevicesApi().addEventListener('devicechange', () => {
|
|
33
|
-
if (this.
|
|
30
|
+
if (this.ondevicechange) {
|
|
34
31
|
return this.enumerateDevices();
|
|
35
32
|
}
|
|
36
33
|
|
|
@@ -114,7 +111,6 @@ export default class DeviceManager extends EventEmitter {
|
|
|
114
111
|
);
|
|
115
112
|
|
|
116
113
|
if (changes.length) {
|
|
117
|
-
this.emit('devicechange', changes, newDevices);
|
|
118
114
|
this.ondevicechange?.({ changes, devices: newDevices });
|
|
119
115
|
}
|
|
120
116
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import DeviceManager from './device-manager';
|
|
2
2
|
|
|
3
3
|
export { supportsMediaDevices } from './support-detection';
|
|
4
|
-
export {
|
|
5
|
-
export { OperationType
|
|
4
|
+
export { DeviceKind } from './enumerate-devices';
|
|
5
|
+
export { OperationType } from './device-manager';
|
|
6
|
+
|
|
7
|
+
export type { DeviceInfo } from './enumerate-devices';
|
|
8
|
+
export type { DeviceChange } from './device-manager';
|
|
6
9
|
|
|
7
10
|
export default new DeviceManager();
|
package/src/test-utils/mocks.ts
CHANGED
package/dist/device-manager.d.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
import EventEmitter from 'events';
|
|
3
|
-
import { DeviceInfo } from './enumerate-devices';
|
|
4
|
-
/**
|
|
5
|
-
* Monitors the set of devices for changes and calculates convenient diffs
|
|
6
|
-
* between updates. Steps are taken to handle cross-browser quirks and
|
|
7
|
-
* attempts graceful integration with browser fingerprinting countermeasures.
|
|
8
|
-
*/
|
|
9
|
-
export default class DeviceManager extends EventEmitter {
|
|
10
|
-
private _knownDevices;
|
|
11
|
-
private _gainedScreenAccessOnce;
|
|
12
|
-
/**
|
|
13
|
-
* Specifies a function to be called whenever the list of available devices
|
|
14
|
-
* changes.
|
|
15
|
-
*
|
|
16
|
-
* Note: this is different from the native event. It passes the changeset
|
|
17
|
-
* and full list of devices as a parameter.
|
|
18
|
-
*/
|
|
19
|
-
ondevicechange: null | DeviceChangeListener;
|
|
20
|
-
constructor();
|
|
21
|
-
/**
|
|
22
|
-
* Request a live media stream from audio and/or video devices. Streams are
|
|
23
|
-
* configurable through constraints.
|
|
24
|
-
* See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
|
25
|
-
*/
|
|
26
|
-
getUserMedia: (constraints: MediaStreamConstraints) => Promise<MediaStream>;
|
|
27
|
-
/**
|
|
28
|
-
* Ask the user to share their screen. Resolves with a media stream carrying
|
|
29
|
-
* video, and potentially audio from the application window.
|
|
30
|
-
* See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia
|
|
31
|
-
*/
|
|
32
|
-
getDisplayMedia: (constraints?: MediaStreamConstraints | undefined) => Promise<MediaStream>;
|
|
33
|
-
/**
|
|
34
|
-
* Lists every available hardware device, including microphones, cameras,
|
|
35
|
-
* and speakers (depending on browser support). May contain redacted
|
|
36
|
-
* information depending on application permissions.
|
|
37
|
-
*/
|
|
38
|
-
enumerateDevices: () => Promise<Array<DeviceInfo>>;
|
|
39
|
-
/**
|
|
40
|
-
* Returns an object containing every media constraint supported by the
|
|
41
|
-
* browser.
|
|
42
|
-
* See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getSupportedConstraints
|
|
43
|
-
*/
|
|
44
|
-
getSupportedConstraints: () => MediaTrackSupportedConstraints;
|
|
45
|
-
private _checkForDeviceChanges;
|
|
46
|
-
/**
|
|
47
|
-
* Note: The device enumeration API may return null values for device IDs
|
|
48
|
-
* and labels. To avoid creating erroneous "Device Added" notifications,
|
|
49
|
-
* a best effort should be made to detect when devices are identical.
|
|
50
|
-
*
|
|
51
|
-
* Order is significant. Preferred devices are listed first, which helps
|
|
52
|
-
* correlate devices from permissioned requests with unpermissioned
|
|
53
|
-
* requests.
|
|
54
|
-
*/
|
|
55
|
-
private _calculateDeviceDiff;
|
|
56
|
-
}
|
|
57
|
-
export declare type DeviceChange = DeviceAddEvent | DeviceRemoveEvent | DeviceUpdateEvent;
|
|
58
|
-
interface DeviceAddEvent {
|
|
59
|
-
type: OperationType.Add;
|
|
60
|
-
device: DeviceInfo;
|
|
61
|
-
}
|
|
62
|
-
interface DeviceRemoveEvent {
|
|
63
|
-
type: OperationType.Remove;
|
|
64
|
-
device: DeviceInfo;
|
|
65
|
-
}
|
|
66
|
-
interface DeviceUpdateEvent {
|
|
67
|
-
type: OperationType.Update;
|
|
68
|
-
newInfo: DeviceInfo;
|
|
69
|
-
oldInfo: DeviceInfo;
|
|
70
|
-
}
|
|
71
|
-
export declare enum OperationType {
|
|
72
|
-
Add = "add",
|
|
73
|
-
Remove = "remove",
|
|
74
|
-
Update = "update"
|
|
75
|
-
}
|
|
76
|
-
interface DeviceChangeListener {
|
|
77
|
-
(update: {
|
|
78
|
-
changes: Array<DeviceChange>;
|
|
79
|
-
devices: Array<DeviceInfo>;
|
|
80
|
-
}): unknown;
|
|
81
|
-
}
|
|
82
|
-
export {};
|