From 5d32386f2c92b247c4a191d083d9331bf8c945fa Mon Sep 17 00:00:00 2001 From: wj-xiao Date: Wed, 4 Feb 2026 14:43:36 +0800 Subject: [PATCH] refactor(desktop): split camera and microphone permissions --- desktop/src/common/ipc-events.ts | 3 +- desktop/src/main/events/app.ts | 21 +++-- desktop/src/renderer/src/App.tsx | 32 +------- .../renderer/src/components/device/video.tsx | 2 +- .../src/components/menu/audio/index.tsx | 79 +++++++++++++++++++ .../renderer/src/components/menu/index.tsx | 2 + .../src/components/menu/recorder/index.tsx | 2 +- .../src/components/menu/video/device.tsx | 2 +- .../src/components/menu/video/resolution.tsx | 2 +- desktop/src/renderer/src/i18n/locales/en.ts | 7 ++ desktop/src/renderer/src/i18n/locales/ja.ts | 6 +- desktop/src/renderer/src/i18n/locales/zh.ts | 7 ++ .../libs/{camera/index.ts => media/camera.ts} | 27 ++++--- .../src/renderer/src/libs/media/permission.ts | 75 ++++++++++++++++++ 14 files changed, 208 insertions(+), 59 deletions(-) create mode 100644 desktop/src/renderer/src/components/menu/audio/index.tsx rename desktop/src/renderer/src/libs/{camera/index.ts => media/camera.ts} (71%) create mode 100644 desktop/src/renderer/src/libs/media/permission.ts diff --git a/desktop/src/common/ipc-events.ts b/desktop/src/common/ipc-events.ts index b554167..b2b43e2 100644 --- a/desktop/src/common/ipc-events.ts +++ b/desktop/src/common/ipc-events.ts @@ -2,7 +2,8 @@ export enum IpcEvents { GET_APP_VERSION = 'get-app-version', GET_PLATFORM = 'get-platform', OPEN_EXTERNAL_RUL = 'open-external-url', - REQUEST_MEDIA_PERMISSIONS = 'request-media-permissions', + CHECK_MEDIA_PERMISSION = 'check-media-permission', + REQUEST_MEDIA_PERMISSION = 'request-media-permission', SET_FULL_SCREEN = 'set-full-screen', GET_SERIAL_PORTS = 'get-serial-ports', diff --git a/desktop/src/main/events/app.ts b/desktop/src/main/events/app.ts index f522e55..721aeac 100644 --- a/desktop/src/main/events/app.ts +++ b/desktop/src/main/events/app.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, ipcMain, shell, systemPreferences } from 'electron' +import { app, BrowserWindow, ipcMain, IpcMainInvokeEvent, shell, systemPreferences } from 'electron' import type { IpcMainEvent, OpenExternalOptions } from 'electron' import { IpcEvents } from '../../common/ipc-events' @@ -7,7 +7,8 @@ export function registerApp(): void { ipcMain.handle(IpcEvents.GET_APP_VERSION, getAppVersion) ipcMain.handle(IpcEvents.GET_PLATFORM, getPlatform) ipcMain.on(IpcEvents.OPEN_EXTERNAL_RUL, openExternalUrl) - ipcMain.handle(IpcEvents.REQUEST_MEDIA_PERMISSIONS, requestMediaPermissions) + ipcMain.handle(IpcEvents.CHECK_MEDIA_PERMISSION, checkMediaPermission) + ipcMain.handle(IpcEvents.REQUEST_MEDIA_PERMISSION, requestMediaPermission) ipcMain.on(IpcEvents.SET_FULL_SCREEN, setFullScreen) } @@ -23,17 +24,15 @@ function openExternalUrl(_: IpcMainEvent, url: string, options?: OpenExternalOpt shell.openExternal(url, options).catch(console.error) } -async function requestMediaPermissions(): Promise<{ - camera: boolean - microphone: boolean -}> { - const camera = await grant('camera') - const microphone = await grant('microphone') - - return { camera, microphone } +function checkMediaPermission(_: IpcMainInvokeEvent, media: 'camera' | 'microphone'): boolean { + const status = systemPreferences.getMediaAccessStatus(media) + return status === 'granted' } -async function grant(media: 'camera' | 'microphone'): Promise { +async function requestMediaPermission( + _: IpcMainInvokeEvent, + media: 'camera' | 'microphone' +): Promise { try { const status = systemPreferences.getMediaAccessStatus(media) if (status === 'granted') { diff --git a/desktop/src/renderer/src/App.tsx b/desktop/src/renderer/src/App.tsx index 75477da..85edcaf 100644 --- a/desktop/src/renderer/src/App.tsx +++ b/desktop/src/renderer/src/App.tsx @@ -19,7 +19,8 @@ import { } from '@renderer/jotai/device' import { isKeyboardEnableAtom } from '@renderer/jotai/keyboard' import { mouseModeAtom, mouseStyleAtom } from '@renderer/jotai/mouse' -import { camera } from '@renderer/libs/camera' +import { camera } from '@renderer/libs/media/camera' +import { requestCameraPermission } from '@renderer/libs/media/permission' import { getVideoResolution } from '@renderer/libs/storage' import type { Resolution } from '@renderer/types' @@ -55,34 +56,9 @@ const App = (): ReactElement => { async function requestMediaPermissions(resolution?: Resolution): Promise { try { - const platform = await window.electron.ipcRenderer.invoke(IpcEvents.GET_PLATFORM) - if (platform === 'darwin') { - const res = await window.electron.ipcRenderer.invoke(IpcEvents.REQUEST_MEDIA_PERMISSIONS) - - if (!res.camera) { - setState('failed') - return - } - } else { - const stream = await navigator.mediaDevices.getUserMedia({ - video: { - width: { ideal: resolution?.width || 1920 }, - height: { ideal: resolution?.height || 1080 }, - frameRate: { ideal: 60 } - }, - audio: { - echoCancellation: false, - noiseSuppression: false, - autoGainControl: false, - sampleRate: 48000 - } - }) - stream.getTracks().forEach((track) => track.stop()) - } - - setState('success') + const granted = await requestCameraPermission(resolution) + setState(granted ? 'success' : 'failed') } catch (err) { - console.log('failed to request media permissions: ', err) if (err instanceof Error && ['NotAllowedError', 'PermissionDeniedError'].includes(err.name)) { setState('failed') } else { diff --git a/desktop/src/renderer/src/components/device/video.tsx b/desktop/src/renderer/src/components/device/video.tsx index c9890a0..d43142f 100644 --- a/desktop/src/renderer/src/components/device/video.tsx +++ b/desktop/src/renderer/src/components/device/video.tsx @@ -4,7 +4,7 @@ import { useAtom, useAtomValue } from 'jotai' import { useTranslation } from 'react-i18next' import { resolutionAtom, videoDeviceIdAtom, videoStateAtom } from '@renderer/jotai/device' -import { camera } from '@renderer/libs/camera' +import { camera } from '@renderer/libs/media/camera' import * as storage from '@renderer/libs/storage' import type { MediaDevice } from '@renderer/types' diff --git a/desktop/src/renderer/src/components/menu/audio/index.tsx b/desktop/src/renderer/src/components/menu/audio/index.tsx new file mode 100644 index 0000000..ee7ea58 --- /dev/null +++ b/desktop/src/renderer/src/components/menu/audio/index.tsx @@ -0,0 +1,79 @@ +import { useEffect, useState } from 'react' +import { Button, Modal } from 'antd' +import { useSetAtom } from 'jotai' +import { VolumeOffIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' + +import { videoDeviceIdAtom, videoStateAtom } from '@renderer/jotai/device' +import { camera } from '@renderer/libs/media/camera' +import { checkPermission, requestMicrophonePermission } from '@renderer/libs/media/permission' + +export const Audio = () => { + const { t } = useTranslation() + + const setVideoState = useSetAtom(videoStateAtom) + const setVideoDeviceId = useSetAtom(videoDeviceIdAtom) + + const [isGranted, setIsGranted] = useState(false) + const [isModalOpen, setIsModalOpen] = useState(false) + + useEffect(() => { + checkPermission('microphone').then((granted) => { + setIsGranted(granted) + }) + }, []) + + async function requestPermission(): Promise { + try { + const granted = await requestMicrophonePermission() + if (!granted) { + setIsModalOpen(true) + return + } + + setVideoDeviceId('') + setVideoState('disconnected') + setIsGranted(granted) + + camera.close() + } catch (err: any) { + console.log('failed to request media permissions: ', err) + } + } + + function closeModal(): void { + setIsModalOpen(false) + } + + if (isGranted) { + return null + } + + return ( + <> +
+ +
+ + +
{t('audio.permission')}
+ + {t('audio.viewDoc')} + + +
+ +
+
+ + ) +} diff --git a/desktop/src/renderer/src/components/menu/index.tsx b/desktop/src/renderer/src/components/menu/index.tsx index 98f6747..17ba3f6 100644 --- a/desktop/src/renderer/src/components/menu/index.tsx +++ b/desktop/src/renderer/src/components/menu/index.tsx @@ -6,6 +6,7 @@ import Draggable from 'react-draggable' import * as storage from '@renderer/libs/storage' +import { Audio } from './audio' import { Keyboard } from './keyboard' import { Mouse } from './mouse' import { Recorder } from './recorder' @@ -81,6 +82,7 @@ export const Menu = (): ReactElement => {