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 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.3.0...HEAD
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,2 @@
1
+ export * from "../src/index"
2
+ export {default} from "../src/index"
@@ -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
+ export * from "../src/index"
2
+ export {default} from "../src/index"
@@ -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.3.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.esm.js",
10
- "main": "dist/index.js",
11
- "typings": "dist/index.d.ts",
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
- "start": "dts watch",
25
- "build": "dts build",
29
+ "prepare": "vite build --sourcemap",
26
30
  "test": "./bin/run-tests",
27
- "test:unit": "dts test",
28
- "test:lint": "dts lint src",
29
- "prepare": "dts build",
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": "dts lint src"
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
- "size-limit": [
51
- {
52
- "path": "dist/media-devices.cjs.production.min.js",
53
- "limit": "10 KB"
57
+ "eslintConfig": {
58
+ "parser": "@typescript-eslint/parser",
59
+ "parserOptions": {
60
+ "sourceType": "module"
54
61
  },
55
- {
56
- "path": "dist/media-devices.esm.js",
57
- "limit": "10 KB"
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
- "@size-limit/preset-small-lib": "7.0.3",
62
- "dts-cli": "0.20.0",
63
- "husky": "7.0.4",
64
- "size-limit": "7.0.3",
65
- "tslib": "2.3.1",
66
- "typescript": "4.5.2"
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();
@@ -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 extends EventEmitter {
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.listenerCount('devicechange') || this.ondevicechange) {
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 { DeviceInfo, DeviceKind } from './enumerate-devices';
5
- export { OperationType, DeviceChange } from './device-manager';
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();
@@ -1,3 +1,4 @@
1
+ /* istanbul ignore file */
1
2
  import EventEmitter from 'events';
2
3
 
3
4
  export class MockMediaDeviceInfo implements MediaDeviceInfo {
@@ -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 {};