Skip to content

Commit 6f62d23

Browse files
committed
refactor: split camera and microphone permissions
1 parent 43fca48 commit 6f62d23

13 files changed

Lines changed: 183 additions & 49 deletions

File tree

browser/src/App.tsx

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ import {
1919
} from '@/jotai/device.ts';
2020
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
2121
import { mouseStyleAtom } from '@/jotai/mouse.ts';
22-
import { camera } from '@/libs/camera';
2322
import { device } from '@/libs/device';
23+
import { camera } from '@/libs/media/camera';
24+
import { checkPermission, requestCameraPermission } from '@/libs/media/permission.ts';
2425
import * as storage from '@/libs/storage';
2526
import type { Resolution } from '@/types.ts';
2627

@@ -37,7 +38,7 @@ const App = () => {
3738
const [videoRotation, setVideoRotation] = useAtom(videoRotationAtom);
3839

3940
const [isLoading, setIsLoading] = useState(true);
40-
const [isCameraAvailable, setIsCameraAvailable] = useState(false);
41+
const [isCameraGranted, setIsCameraGranted] = useState(false);
4142
const [shouldSwapDimensions, setShouldSwapDimensions] = useState(false);
4243

4344
useEffect(() => {
@@ -60,7 +61,7 @@ const App = () => {
6061
setResolution(resolution);
6162
}
6263

63-
requestMediaPermissions(resolution);
64+
requestPermission(resolution);
6465
}
6566

6667
function initRotation() {
@@ -70,41 +71,28 @@ const App = () => {
7071
}
7172
}
7273

73-
async function requestMediaPermissions(resolution?: Resolution) {
74+
async function requestPermission(resolution?: Resolution) {
7475
try {
75-
const stream = await navigator.mediaDevices.getUserMedia({
76-
video: {
77-
width: { ideal: resolution?.width || 1920 },
78-
height: { ideal: resolution?.height || 1080 },
79-
frameRate: { ideal: 60 }
80-
},
81-
audio: {
82-
echoCancellation: false,
83-
noiseSuppression: false,
84-
autoGainControl: false,
85-
sampleRate: 48000
86-
}
87-
});
88-
stream.getTracks().forEach((track) => track.stop());
89-
90-
setIsCameraAvailable(true);
76+
const isGranted = await checkPermission('camera');
77+
if (isGranted) {
78+
setIsCameraGranted(true);
79+
return;
80+
}
81+
82+
const isSuccess = await requestCameraPermission(resolution);
83+
setIsCameraGranted(isSuccess);
9184
} catch (err: any) {
9285
console.log('failed to request media permissions: ', err);
93-
if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
94-
setIsCameraAvailable(false);
95-
} else {
96-
setIsCameraAvailable(true);
97-
}
86+
} finally {
87+
setIsLoading(false);
9888
}
99-
100-
setIsLoading(false);
10189
}
10290

10391
if (isLoading) {
10492
return <Spin size="large" spinning={isLoading} tip={t('camera.tip')} fullscreen />;
10593
}
10694

107-
if (!isCameraAvailable) {
95+
if (!isCameraGranted) {
10896
return (
10997
<Result
11098
status="info"

browser/src/components/device-modal/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useAtom, useSetAtom } from 'jotai';
44
import { useTranslation } from 'react-i18next';
55

66
import { serialStateAtom, videoDeviceIdAtom, videoStateAtom } from '@/jotai/device.ts';
7-
import { camera } from '@/libs/camera';
7+
import { camera } from '@/libs/media/camera.ts';
88

99
import { SerialPort } from './serial-port';
1010
import { Video } from './video';

browser/src/components/device-modal/serial-port.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ export const SerialPort = ({ setErrMsg, onDisconnect }: SerialPortProps) => {
1818

1919
useEffect(() => {
2020
const isWebSerialSupported = 'serial' in navigator;
21-
const state = isWebSerialSupported ? 'disconnected' : 'notSupported';
22-
setSerialState(state);
21+
if (!isWebSerialSupported) {
22+
setSerialState('notSupported');
23+
}
2324
}, [setSerialState]);
2425

2526
const selectSerialPort = async () => {
@@ -45,7 +46,7 @@ export const SerialPort = ({ setErrMsg, onDisconnect }: SerialPortProps) => {
4546

4647
return (
4748
<Button
48-
type="primary"
49+
type={serialState === 'connected' ? 'primary' : 'default'}
4950
className="w-[250px]"
5051
loading={serialState === 'connecting'}
5152
onClick={selectSerialPort}

browser/src/components/device-modal/video.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useAtom, useAtomValue } from 'jotai';
44
import { useTranslation } from 'react-i18next';
55

66
import { resolutionAtom, videoDeviceIdAtom, videoStateAtom } from '@/jotai/device.ts';
7-
import { camera } from '@/libs/camera';
7+
import { camera } from '@/libs/media/camera';
88
import * as storage from '@/libs/storage';
99
import type { MediaDevice } from '@/types';
1010

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useEffect, useState } from 'react';
2+
import { Button, Modal } from 'antd';
3+
import { useSetAtom } from 'jotai';
4+
import { VolumeOffIcon } from 'lucide-react';
5+
import { useTranslation } from 'react-i18next';
6+
7+
import { videoDeviceIdAtom, videoStateAtom } from '@/jotai/device.ts';
8+
import { camera } from '@/libs/media/camera.ts';
9+
import { checkPermission, requestMicrophonePermission } from '@/libs/media/permission.ts';
10+
11+
export const Audio = () => {
12+
const { t } = useTranslation();
13+
14+
const setVideoState = useSetAtom(videoStateAtom);
15+
const setVideoDeviceId = useSetAtom(videoDeviceIdAtom);
16+
17+
const [isGranted, setIsGranted] = useState(false);
18+
const [isModalOpen, setIsModalOpen] = useState(false);
19+
20+
useEffect(() => {
21+
checkPermission('microphone').then((granted) => {
22+
setIsGranted(granted);
23+
});
24+
}, []);
25+
26+
async function requestPermission() {
27+
try {
28+
const granted = await requestMicrophonePermission();
29+
if (!granted) {
30+
setIsModalOpen(true);
31+
return;
32+
}
33+
34+
setVideoDeviceId('');
35+
setVideoState('disconnected');
36+
setIsGranted(granted);
37+
38+
camera.close();
39+
} catch (err: any) {
40+
console.log('failed to request media permissions: ', err);
41+
}
42+
}
43+
44+
function closeModal() {
45+
setIsModalOpen(false);
46+
}
47+
48+
if (isGranted) {
49+
return null;
50+
}
51+
52+
return (
53+
<>
54+
<div
55+
className="flex h-[28px] w-[28px] cursor-pointer items-center justify-center rounded text-neutral-300 hover:bg-neutral-700/70 hover:text-white"
56+
onClick={requestPermission}
57+
>
58+
<VolumeOffIcon size={18} />
59+
</div>
60+
61+
<Modal open={isModalOpen} title={t('audio.tip')} footer={null} onCancel={closeModal}>
62+
<div className="whitespace-pre-line py-5">{t('audio.permission')}</div>
63+
<a
64+
href="https://wiki.sipeed.com/hardware/en/kvm/NanoKVM_USB/quick_start.html#Authorization"
65+
target="_blank"
66+
>
67+
{t('audio.viewDoc')}
68+
</a>
69+
70+
<div className="flex w-full justify-center pt-8">
71+
<Button type="primary" className="min-w-20" onClick={closeModal}>
72+
{t('audio.ok')}
73+
</Button>
74+
</div>
75+
</Modal>
76+
</>
77+
);
78+
};

browser/src/components/menu/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Draggable from 'react-draggable';
88
import { serialStateAtom } from '@/jotai/device.ts';
99
import * as storage from '@/libs/storage';
1010

11+
import { Audio } from './audio';
1112
import { Fullscreen } from './fullscreen';
1213
import { Keyboard } from './keyboard';
1314
import { Mouse } from './mouse';
@@ -87,6 +88,7 @@ export const Menu = () => {
8788
<Divider type="vertical" />
8889

8990
<Video />
91+
<Audio />
9092

9193
{serialState === 'connected' && (
9294
<>

browser/src/components/menu/recorder/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useEffect, useRef, useState } from 'react';
22
import { Video } from 'lucide-react';
33

4-
import { camera } from '@/libs/camera';
4+
import { camera } from '@/libs/media/camera';
55

66
export const Recorder = () => {
77
const [isRecording, setIsRecording] = useState(false);

browser/src/components/menu/video/device.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { VideoIcon } from 'lucide-react';
66
import { useTranslation } from 'react-i18next';
77

88
import { resolutionAtom, videoDeviceIdAtom } from '@/jotai/device.ts';
9-
import { camera } from '@/libs/camera';
9+
import { camera } from '@/libs/media/camera';
1010
import * as storage from '@/libs/storage';
1111
import type { MediaDevice } from '@/types';
1212

browser/src/components/menu/video/resolution.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
77

88
import { resolutionAtom } from '@/jotai/device.ts';
99
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
10-
import { camera } from '@/libs/camera';
10+
import { camera } from '@/libs/media/camera.ts';
1111
import * as storage from '@/libs/storage';
1212
import type { Resolution as VideoResolution } from '@/types';
1313

browser/src/i18n/locales/en.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ const en = {
3636
cancel: 'Cancel'
3737
}
3838
},
39+
audio: {
40+
tip: 'Tip',
41+
permission:
42+
'Microphone access is required to connect your USB audio device. The operating system classifies USB inputs as microphones, so this permission is necessary.\n\nThis action is solely for device connectivity and does not enable audio recording.',
43+
viewDoc: 'View document.',
44+
ok: 'Ok'
45+
},
3946
keyboard: {
4047
paste: 'Paste',
4148
virtualKeyboard: 'Keyboard',

0 commit comments

Comments
 (0)