diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index dfb4747f0392..81ddfe2f5bc8 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -5510,18 +5510,22 @@ function installContextMenu(window) { }) } -// Microphone capture for the voice composer. The renderer drives mic access +// Microphone and camera capture. The voice composer drives mic access and +// renderer features (e.g. desktop plugins) can drive camera access, both // through getUserMedia, which Chromium gates behind these two session hooks. // // The naive `details.mediaTypes.includes('audio')` check works on macOS but -// breaks on Windows: Chromium frequently fires the mic permission request with -// an empty/undefined `mediaTypes`, so the strict check denies it and -// getUserMedia throws NotAllowedError ("Microphone permission was denied"). -// We therefore treat an audio-capture request as allowed whenever it's the -// 'media'/'audioCapture' permission AND mediaTypes either includes 'audio' OR -// is empty/absent (the Windows case). Video is still denied. -function isAudioCapturePermission(permission, details) { - if (permission === 'audioCapture') { +// breaks on Windows: Chromium frequently fires the request with an empty or +// undefined `mediaTypes`, so a strict check denies it and getUserMedia throws +// NotAllowedError. We therefore allow the capture permissions and treat absent +// metadata as allowed. +// +// Granting here is not the last gate: the OS still applies its own capture +// permission (macOS TCC prompts on first use, per the NSMicrophone/NSCamera +// usage strings), so the user keeps a real allow/deny and can revoke it in +// System Settings afterwards. +function isMediaCapturePermission(permission, details) { + if (permission === 'audioCapture' || permission === 'videoCapture') { return true } @@ -5531,38 +5535,31 @@ function isAudioCapturePermission(permission, details) { const mediaTypes = details?.mediaTypes + // Windows: mediaTypes is often empty for a capture request. Don't deny on + // missing metadata. if (!Array.isArray(mediaTypes) || mediaTypes.length === 0) { - // Windows: mediaTypes is often empty for a mic request. Don't deny on - // missing metadata. (A video request would carry mediaTypes:['video'].) return true } - return mediaTypes.includes('audio') && !mediaTypes.includes('video') + return mediaTypes.includes('audio') || mediaTypes.includes('video') } function installMediaPermissions() { // Async request handler: the prompt-style path (most platforms). session.defaultSession.setPermissionRequestHandler((_webContents, permission, callback, details) => { - callback(isAudioCapturePermission(permission, details)) + callback(isMediaCapturePermission(permission, details)) }) // Synchronous check handler: Chromium consults this for getUserMedia on // Windows in addition to (or instead of) the request handler. Without it, - // the check defaults to false and the mic is denied before the request + // the check defaults to false and capture is denied before the request // handler ever runs. - session.defaultSession.setPermissionCheckHandler((_webContents, permission, _origin, details) => { - if (permission === 'media' || permission === ('audioCapture' as any) /* todo: is this needed? */) { - // details.mediaType is a single string here (not the mediaTypes array). - const mediaType = details?.mediaType - - if (mediaType === 'video') { - return false - } - - return true - } - - return false + session.defaultSession.setPermissionCheckHandler((_webContents, permission) => { + return ( + permission === 'media' || + permission === ('audioCapture' as any) /* todo: is this needed? */ || + permission === ('videoCapture' as any) + ) }) }