Skip to content

Commit 5754d0d

Browse files
committed
[IMPL] usePermission + useMediaDevices hooks and detectBrowser util
1 parent acb57e9 commit 5754d0d

16 files changed

Lines changed: 484 additions & 11 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useState } from "react";
2+
import { useMediaDevices } from "../../../../../../packages/react-tools/src"
3+
4+
/**
5+
The component uses _useMediaDevices_ to get list of all media devices and groups them into _CAMERA_ _SPEAKERS_ and _MICROPHONE_ sections.
6+
*/
7+
export const UseMediaDevices = () => {
8+
const action = useMediaDevices("devicesList");
9+
const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
10+
11+
const onClick = async () => {
12+
try {
13+
const devices = await action(async () => {
14+
setDevices(await action());
15+
});
16+
setDevices(devices.reduce((prev, curr) => {
17+
if (prev.every(el => el.deviceId !== curr.deviceId)) {
18+
prev.push(curr);
19+
}
20+
return prev;
21+
}, [] as MediaDeviceInfo[]));
22+
} catch (error) {
23+
alert((error as Error).message);
24+
}
25+
}
26+
27+
return <div style={{ display: "grid", gridTemplateRows: "auto auto", justifyContent: "center", gap: 50, maxHeight: 350, overflow: "auto" }}>
28+
<button onClick={onClick}>Acquire</button>
29+
<div style={{ display: "grid", gridTemplateColumns: "auto auto auto", justifyContent: "center", gap: 50, maxHeight: 350, overflow: "auto" }}>
30+
<div style={{textAlign: "center"}}>
31+
<p>Camera {devices.filter(el => el.kind === "videoinput").length}</p>
32+
<ul>
33+
{
34+
devices.filter(el => el.kind === "videoinput").map(el => <li key={el.label}>{el.label}</li>)
35+
}
36+
</ul>
37+
</div>
38+
<div style={{textAlign: "center"}}>
39+
<p>Spearker {devices.filter(el => el.kind === "audiooutput").length}</p>
40+
<ul>
41+
{
42+
devices.filter(el => el.kind === "audiooutput").map(el => <li key={el.label}>{el.label}</li>)
43+
}
44+
</ul>
45+
</div>
46+
<div style={{textAlign: "center"}}>
47+
<p>Microphones {devices.filter(el => el.kind === "audioinput").length}</p>
48+
<ul>
49+
{
50+
devices.filter(el => el.kind === "audioinput").map(el => <li key={el.label}>{el.label}</li>)
51+
}
52+
</ul>
53+
</div>
54+
</div>
55+
</div>
56+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { usePermission } from "../../../../../../packages/react-tools/src"
2+
3+
/**
4+
The component uses _usePermission_ hook to check permission status of accelerometer, camera, microphone, notifications ans speaker.
5+
*/
6+
export const UsePermission = () => {
7+
const [accelerometer] = usePermission("accelerometer");
8+
const [camera] = usePermission("camera");
9+
const [microphone] = usePermission("microphone");
10+
const [notifications] = usePermission("notifications");
11+
const [speaker] = usePermission("speaker-selection");
12+
13+
return <div>
14+
<p>accelerometer: {accelerometer}</p>
15+
<p>camera: {camera}</p>
16+
<p>microphone: {microphone}</p>
17+
<p>notifications: {notifications}</p>
18+
<p>speaker: {speaker}</p>
19+
</div>
20+
}

apps/react-tools-demo/src/constants/components.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const COMPONENTS = [
7070
"useHotKeys",
7171
"usePinchZoom",
7272
"usePointerLock",
73-
"useContextMenu"
73+
"useContextMenu",
7474
],
7575
//API DOM
7676
[
@@ -111,7 +111,9 @@ export const COMPONENTS = [
111111
"useAudio",
112112
"useVideo",
113113
"useEventSource",
114-
"useWebSocket"
114+
"useWebSocket",
115+
"usePermission",
116+
"useMediaDevices"
115117
]
116118
],
117119
//UTILS
@@ -122,6 +124,7 @@ export const COMPONENTS = [
122124
"isMouseEvent",
123125
"isClient",
124126
"isAsync",
125-
"hotKeyHandler"
127+
"hotKeyHandler",
128+
"detectBrowser"
126129
]
127130
] as const;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# detectBrowser
2+
It detects used browser or return __"No detection"__.
3+
4+
## API
5+
6+
```tsx
7+
detectBrowser(): "chrome" | "firefox" | "safari" | "opera" | "edge" | "No detection"
8+
```
9+
10+
> ### Params
11+
>
12+
>
13+
>
14+
15+
> ### Returns
16+
>
17+
> __result__
18+
> - _"chrome"|"firefox"|"safari"|"opera"|"edge"|"No detection"_
19+
>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# useMediaDevices
2+
Hook to use [MediaDevices](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices) interface methods, that give access to any hardware source of media data.
3+
4+
## Usage
5+
6+
```tsx
7+
export const UseMediaDevices = () => {
8+
const action = useMediaDevices("devicesList");
9+
const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
10+
11+
const onClick = async () => {
12+
try {
13+
const devices = await action(async () => {
14+
setDevices(await action());
15+
});
16+
setDevices(devices);
17+
} catch (error) {
18+
alert((error as Error).message);
19+
}
20+
}
21+
22+
return <div style={{ display: "grid", gridTemplateRows: "auto auto", justifyContent: "center", gap: 50, maxHeight: 350, overflow: "auto" }}>
23+
<button onClick={onClick}>Acquire</button>
24+
<div style={{ display: "grid", gridTemplateColumns: "auto auto auto", justifyContent: "center", gap: 50, maxHeight: 350, overflow: "auto" }}>
25+
<div style={{textAlign: "center"}}>
26+
<p>Camera {devices.filter(el => el.kind === "videoinput").length}</p>
27+
<ul>
28+
{
29+
devices.filter(el => el.kind === "videoinput").map(el => <li key={el.label}>{el.label}</li>)
30+
}
31+
</ul>
32+
</div>
33+
<div style={{textAlign: "center"}}>
34+
<p>Spearker {devices.filter(el => el.kind === "audiooutput").length}</p>
35+
<ul>
36+
{
37+
devices.filter(el => el.kind === "audiooutput").map(el => <li key={el.label}>{el.label}</li>)
38+
}
39+
</ul>
40+
</div>
41+
<div style={{textAlign: "center"}}>
42+
<p>Microphones {devices.filter(el => el.kind === "audioinput").length}</p>
43+
<ul>
44+
{
45+
devices.filter(el => el.kind === "audioinput").map(el => <li key={el.label}>{el.label}</li>)
46+
}
47+
</ul>
48+
</div>
49+
</div>
50+
</div>
51+
}
52+
```
53+
54+
> The component uses _useMediaDevices_ to get list of all media devices and groups them into _CAMERA_ _SPEAKERS_ and _MICROPHONE_ sections.
55+
56+
57+
## API
58+
59+
```tsx
60+
useMediaDevices(action: UseMediaDevicesProps): UseMediaDevicesResult
61+
```
62+
63+
> ### Params
64+
>
65+
> - __action__: _UseMediaDevicesProps_
66+
it is a string that identifies which method to return as a result. It can be _devicesList_, _supportedConstraintsList_, _DisplayCapture_, or _mediaInputCapture_.
67+
>
68+
69+
> ### Returns
70+
>
71+
> __result__: _UseMediaDevicesResult_
72+
> if __action__ is:
73+
> - _devicesList_: so _result_ is __enumeratedDevices__ method of MediaDevices interface.
74+
> - _supportedConstraintsList_: so _result_ is __getSupportedConstraints__ method of MediaDevices interface.
75+
> - _DisplayCapture_: so _result_ is __getDisplayMedia__ method of MediaDevices interface.
76+
> - _mediaInputCapture_: so _result_ is __getUserMedia__ method of MediaDevices interface.
77+
>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# usePermission
2+
Hook to query the status of API permissions attributed to the current context. Refer to [PermissionAPI](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API).
3+
4+
## Usage
5+
6+
```tsx
7+
export const UsePermission = () => {
8+
const [accelerometer] = usePermission("accelerometer");
9+
const [camera] = usePermission("camera");
10+
const [microphone] = usePermission("microphone");
11+
const [notifications] = usePermission("notifications");
12+
const [speaker] = usePermission("speaker-selection");
13+
14+
return <div>
15+
<p>accelerometer: {accelerometer}</p>
16+
<p>camera: {camera}</p>
17+
<p>microphone: {microphone}</p>
18+
<p>notifications: {notifications}</p>
19+
<p>speaker: {speaker}</p>
20+
</div>
21+
}
22+
```
23+
24+
> The component uses _usePermission_ hook to check permission status of accelerometer, camera, microphone, notifications ans speaker.
25+
26+
27+
## API
28+
29+
```tsx
30+
usePermission(permission: TPermissionName): UsePermissionResult
31+
```
32+
33+
> ### Params
34+
>
35+
> - __permission__: _TPermissionName_
36+
name of the API whose permissions you want to query.
37+
>
38+
39+
> ### Returns
40+
>
41+
> __result__: _UsePermissionResult_
42+
> Array of two elements:
43+
> - first element: current state of the request permission: one of __'asking'__, __'granted'__, __'denied'__, __'prompt'__ or __'not supported'__.
44+
> - second element: function to manual query fot permission status.
45+
>

packages/react-tools/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@
113113
- [x] useVideo
114114
- [x] useEventSource
115115
- [x] useWebSocket
116-
- [ ] useMedia
117-
- [ ] useDevicesList (https://vueuse.org/core/useDevicesList/)
116+
- [x] usePermission
117+
- [x] useMediaDevices
118118
- [ ] useDisplayMedia
119119
- [ ] useScreenShare
120120
- [ ] useMediaDevices
@@ -136,6 +136,7 @@
136136
- [x] isClient
137137
- [x] isAsync
138138
- [x] hotKeyHandler
139+
- [x] detectBrowser
139140
- [ ] lazy: lazy react-like customized
140141
- [ ] fetch-client (???ARTS-like)
141142
- [ ] useBase64 (https://vueuse.org/core/useBase64/)

packages/react-tools/src/hooks/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,6 @@ export { useAudio } from './useAudio';
9191
export { useVideo } from './useVideo';
9292
export { useEventSource } from './useEventSource';
9393
export { useWebSocket } from './useWebSocket';
94-
export { useContextMenu } from './useContextMenu';
94+
export { useContextMenu } from './useContextMenu';
95+
export { useMediaDevices } from './useMediaDevices';
96+
export { usePermission } from './usePermission';
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { useRef } from "react"
2+
import { useEffectOnce } from ".";
3+
import { UseMediaDevicesProps, UseMediaDevicesResult } from "../models";
4+
5+
let mediaDevicesOnChangeAttached = false;
6+
const mediaDevicesListeners = new Set<(evt: Event) => Promise<void> | void>();
7+
8+
/**
9+
* **`useMediaDevices`**: Hook to use [MediaDevices](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices) interface methods, that give access to any hardware source of media data.
10+
* @param {UseMediaDevicesProps} action - it is a string that identifies which method to return as a result. It can be _devicesList_, _supportedConstraintsList_, _DisplayCapture_, or _mediaInputCapture_.
11+
* @returns {UseMediaDevicesResult} result - the function returned by __action__ parameter value.
12+
* if __action__ is:
13+
* - _devicesList_: so _result_ is __enumeratedDevices__ method of MediaDevices interface.
14+
* - _supportedConstraintsList_: so _result_ is __getSupportedConstraints__ method of MediaDevices interface.
15+
* - _DisplayCapture_: so _result_ is __getDisplayMedia__ method of MediaDevices interface.
16+
* - _mediaInputCapture_: so _result_ is __getUserMedia__ method of MediaDevices interface.
17+
*/
18+
function useMediaDevices(action: "devicesList"): (onDevicesChange?: ((evt: Event) => void | Promise<void>) | undefined) => Promise<MediaDeviceInfo[]>;
19+
function useMediaDevices(action: "supportedConstraintsList"): (onDevicesChange?: ((evt: Event) => void | Promise<void>) | undefined) => MediaTrackSupportedConstraints;
20+
function useMediaDevices(action: "DisplayCapture"): (options?: DisplayMediaStreamOptions, onDevicesChange?: ((evt: Event) => void | Promise<void>) | undefined) => Promise<MediaStream>;
21+
function useMediaDevices(action: "mediaInputCapture"): (constraints?: MediaStreamConstraints, onDevicesChange?: ((evt: Event) => void | Promise<void>) | undefined) => Promise<MediaStream>;
22+
function useMediaDevices(action: UseMediaDevicesProps): UseMediaDevicesResult {
23+
const listener = useRef<(evt: Event) => Promise<void> | void>();
24+
25+
if (!mediaDevicesOnChangeAttached) {
26+
!!navigator.mediaDevices?.ondevicechange && (navigator.mediaDevices.ondevicechange = (evt: Event) => {
27+
mediaDevicesListeners.forEach(l => l(evt));
28+
})
29+
mediaDevicesOnChangeAttached = true;
30+
}
31+
32+
const getDevicesList = useRef(async (onDevicesChange?: (evt: Event) => Promise<void> | void) => {
33+
if (!navigator.mediaDevices?.enumerateDevices) {
34+
throw Error("getDevicesList not supported");
35+
}
36+
if (onDevicesChange) {
37+
if (!("ondevicechange" in navigator.mediaDevices)) {
38+
throw Error("onDevicesChange not supported");
39+
}
40+
listener.current = onDevicesChange;
41+
mediaDevicesListeners.add(listener.current);
42+
}
43+
try {
44+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
45+
stream.getTracks().forEach(t => t.stop());
46+
} catch (error) {
47+
void 0;
48+
}
49+
return navigator.mediaDevices.enumerateDevices();
50+
});
51+
52+
const getSupportedConstraintsList = useRef((onDevicesChange?: (evt: Event) => Promise<void> | void) => {
53+
if (!navigator.mediaDevices?.getSupportedConstraints) {
54+
throw Error("getSupportedConstraintsList not supported");
55+
}
56+
if (onDevicesChange) {
57+
if (!("ondevicechange" in navigator.mediaDevices)) {
58+
throw Error("onDevicesChange not supported");
59+
}
60+
listener.current = onDevicesChange;
61+
mediaDevicesListeners.add(listener.current);
62+
}
63+
return navigator.mediaDevices.getSupportedConstraints()
64+
});
65+
66+
const getDisplay = useRef((options?: DisplayMediaStreamOptions, onDevicesChange?: (evt: Event) => Promise<void> | void) => {
67+
if (!navigator.mediaDevices?.getDisplayMedia) {
68+
throw Error("getDisplay not supported");
69+
}
70+
if (onDevicesChange) {
71+
if (!("ondevicechange" in navigator.mediaDevices)) {
72+
throw Error("onDevicesChange not supported");
73+
}
74+
listener.current = onDevicesChange;
75+
mediaDevicesListeners.add(listener.current);
76+
}
77+
return navigator.mediaDevices.getDisplayMedia(options);
78+
});
79+
80+
const getMediaInput = useRef((constraints?: MediaStreamConstraints, onDevicesChange?: (evt: Event) => Promise<void> | void) => {
81+
const userMediaRef = navigator.mediaDevices.getUserMedia ?? (navigator.mediaDevices as { webkitGetUserMedia?: typeof navigator.mediaDevices.getUserMedia }).webkitGetUserMedia ?? (navigator.mediaDevices as { mozGetUserMedia?: typeof navigator.mediaDevices.getUserMedia }).mozGetUserMedia ?? null;
82+
if (!userMediaRef) {
83+
throw Error("getMediaInput not supported");
84+
}
85+
if (onDevicesChange) {
86+
if (!("ondevicechange" in navigator.mediaDevices)) {
87+
throw Error("onDevicesChange not supported");
88+
}
89+
listener.current = onDevicesChange;
90+
mediaDevicesListeners.add(listener.current);
91+
}
92+
return userMediaRef!(constraints);
93+
});
94+
95+
useEffectOnce(() => () => {
96+
!!listener.current && mediaDevicesListeners.delete(listener.current)
97+
});
98+
99+
return action === "devicesList"
100+
? getDevicesList.current
101+
: action === "supportedConstraintsList"
102+
? getSupportedConstraintsList.current
103+
: action === "DisplayCapture"
104+
? getDisplay.current
105+
: getMediaInput.current;
106+
}
107+
108+
export { useMediaDevices };

0 commit comments

Comments
 (0)