Skip to content

Commit 5d32386

Browse files
committed
refactor(desktop): split camera and microphone permissions
1 parent 6f62d23 commit 5d32386

14 files changed

Lines changed: 208 additions & 59 deletions

File tree

desktop/src/common/ipc-events.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ export enum IpcEvents {
22
GET_APP_VERSION = 'get-app-version',
33
GET_PLATFORM = 'get-platform',
44
OPEN_EXTERNAL_RUL = 'open-external-url',
5-
REQUEST_MEDIA_PERMISSIONS = 'request-media-permissions',
5+
CHECK_MEDIA_PERMISSION = 'check-media-permission',
6+
REQUEST_MEDIA_PERMISSION = 'request-media-permission',
67
SET_FULL_SCREEN = 'set-full-screen',
78

89
GET_SERIAL_PORTS = 'get-serial-ports',

desktop/src/main/events/app.ts

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { app, BrowserWindow, ipcMain, shell, systemPreferences } from 'electron'
1+
import { app, BrowserWindow, ipcMain, IpcMainInvokeEvent, shell, systemPreferences } from 'electron'
22
import type { IpcMainEvent, OpenExternalOptions } from 'electron'
33

44
import { IpcEvents } from '../../common/ipc-events'
@@ -7,7 +7,8 @@ export function registerApp(): void {
77
ipcMain.handle(IpcEvents.GET_APP_VERSION, getAppVersion)
88
ipcMain.handle(IpcEvents.GET_PLATFORM, getPlatform)
99
ipcMain.on(IpcEvents.OPEN_EXTERNAL_RUL, openExternalUrl)
10-
ipcMain.handle(IpcEvents.REQUEST_MEDIA_PERMISSIONS, requestMediaPermissions)
10+
ipcMain.handle(IpcEvents.CHECK_MEDIA_PERMISSION, checkMediaPermission)
11+
ipcMain.handle(IpcEvents.REQUEST_MEDIA_PERMISSION, requestMediaPermission)
1112
ipcMain.on(IpcEvents.SET_FULL_SCREEN, setFullScreen)
1213
}
1314

@@ -23,17 +24,15 @@ function openExternalUrl(_: IpcMainEvent, url: string, options?: OpenExternalOpt
2324
shell.openExternal(url, options).catch(console.error)
2425
}
2526

26-
async function requestMediaPermissions(): Promise<{
27-
camera: boolean
28-
microphone: boolean
29-
}> {
30-
const camera = await grant('camera')
31-
const microphone = await grant('microphone')
32-
33-
return { camera, microphone }
27+
function checkMediaPermission(_: IpcMainInvokeEvent, media: 'camera' | 'microphone'): boolean {
28+
const status = systemPreferences.getMediaAccessStatus(media)
29+
return status === 'granted'
3430
}
3531

36-
async function grant(media: 'camera' | 'microphone'): Promise<boolean> {
32+
async function requestMediaPermission(
33+
_: IpcMainInvokeEvent,
34+
media: 'camera' | 'microphone'
35+
): Promise<boolean> {
3736
try {
3837
const status = systemPreferences.getMediaAccessStatus(media)
3938
if (status === 'granted') {

desktop/src/renderer/src/App.tsx

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import {
1919
} from '@renderer/jotai/device'
2020
import { isKeyboardEnableAtom } from '@renderer/jotai/keyboard'
2121
import { mouseModeAtom, mouseStyleAtom } from '@renderer/jotai/mouse'
22-
import { camera } from '@renderer/libs/camera'
22+
import { camera } from '@renderer/libs/media/camera'
23+
import { requestCameraPermission } from '@renderer/libs/media/permission'
2324
import { getVideoResolution } from '@renderer/libs/storage'
2425
import type { Resolution } from '@renderer/types'
2526

@@ -55,34 +56,9 @@ const App = (): ReactElement => {
5556

5657
async function requestMediaPermissions(resolution?: Resolution): Promise<void> {
5758
try {
58-
const platform = await window.electron.ipcRenderer.invoke(IpcEvents.GET_PLATFORM)
59-
if (platform === 'darwin') {
60-
const res = await window.electron.ipcRenderer.invoke(IpcEvents.REQUEST_MEDIA_PERMISSIONS)
61-
62-
if (!res.camera) {
63-
setState('failed')
64-
return
65-
}
66-
} else {
67-
const stream = await navigator.mediaDevices.getUserMedia({
68-
video: {
69-
width: { ideal: resolution?.width || 1920 },
70-
height: { ideal: resolution?.height || 1080 },
71-
frameRate: { ideal: 60 }
72-
},
73-
audio: {
74-
echoCancellation: false,
75-
noiseSuppression: false,
76-
autoGainControl: false,
77-
sampleRate: 48000
78-
}
79-
})
80-
stream.getTracks().forEach((track) => track.stop())
81-
}
82-
83-
setState('success')
59+
const granted = await requestCameraPermission(resolution)
60+
setState(granted ? 'success' : 'failed')
8461
} catch (err) {
85-
console.log('failed to request media permissions: ', err)
8662
if (err instanceof Error && ['NotAllowedError', 'PermissionDeniedError'].includes(err.name)) {
8763
setState('failed')
8864
} else {

desktop/src/renderer/src/components/device/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 '@renderer/jotai/device'
7-
import { camera } from '@renderer/libs/camera'
7+
import { camera } from '@renderer/libs/media/camera'
88
import * as storage from '@renderer/libs/storage'
99
import type { MediaDevice } from '@renderer/types'
1010

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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 '@renderer/jotai/device'
8+
import { camera } from '@renderer/libs/media/camera'
9+
import { checkPermission, requestMicrophonePermission } from '@renderer/libs/media/permission'
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(): Promise<void> {
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(): void {
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="py-5 whitespace-pre-line">{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+
rel="noreferrer"
67+
>
68+
{t('audio.viewDoc')}
69+
</a>
70+
71+
<div className="flex w-full justify-center pt-8">
72+
<Button type="primary" className="min-w-20" onClick={closeModal}>
73+
{t('audio.ok')}
74+
</Button>
75+
</div>
76+
</Modal>
77+
</>
78+
)
79+
}

desktop/src/renderer/src/components/menu/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Draggable from 'react-draggable'
66

77
import * as storage from '@renderer/libs/storage'
88

9+
import { Audio } from './audio'
910
import { Keyboard } from './keyboard'
1011
import { Mouse } from './mouse'
1112
import { Recorder } from './recorder'
@@ -81,6 +82,7 @@ export const Menu = (): ReactElement => {
8182
<Divider type="vertical" />
8283

8384
<Video />
85+
<Audio />
8486
<SerialPort />
8587
<Divider type="vertical" className="px-0.5" />
8688

desktop/src/renderer/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 '@renderer/libs/camera'
4+
import { camera } from '@renderer/libs/media/camera'
55

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

desktop/src/renderer/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 '@renderer/jotai/device'
9-
import { camera } from '@renderer/libs/camera'
9+
import { camera } from '@renderer/libs/media/camera'
1010
import * as storage from '@renderer/libs/storage'
1111
import type { MediaDevice } from '@renderer/types'
1212

desktop/src/renderer/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 '@renderer/jotai/device'
99
import { isKeyboardEnableAtom } from '@renderer/jotai/keyboard'
10-
import { camera } from '@renderer/libs/camera'
10+
import { camera } from '@renderer/libs/media/camera'
1111
import * as storage from '@renderer/libs/storage'
1212
import type { Resolution as VideoResolution } from '@renderer/types'
1313

desktop/src/renderer/src/i18n/locales/en.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ const en = {
3737
cancel: 'Cancel'
3838
}
3939
},
40+
audio: {
41+
tip: 'Tip',
42+
permission:
43+
'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.',
44+
viewDoc: 'View document.',
45+
ok: 'Ok'
46+
},
4047
keyboard: {
4148
paste: 'Paste',
4249
virtualKeyboard: 'Keyboard',

0 commit comments

Comments
 (0)