diff --git a/electron-builder.json5 b/electron-builder.json5 index d9fee6b..372cdf7 100644 --- a/electron-builder.json5 +++ b/electron-builder.json5 @@ -52,8 +52,7 @@ "NSMicrophoneUsageDescription": "OpenScreen needs microphone access to record voice audio.", "NSCameraUsageDescription": "OpenScreen needs camera access to record webcam video.", "NSScreenCaptureUsageDescription": "OpenScreen needs screen recording permission to detect and capture windows.", - "NSCameraUseContinuityCameraDeviceType": true, - "com.apple.security.device.audio-input": true + "NSCameraUseContinuityCameraDeviceType": true } }, "linux": { diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 82acdf9..431cda1 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -718,6 +718,32 @@ export function registerIpcHandlers( } }); + ipcMain.handle("request-screen-access", async () => { + if (process.platform !== "darwin") { + return { success: true, granted: true, status: "granted" }; + } + + try { + const status = systemPreferences.getMediaAccessStatus("screen"); + if (status === "granted") { + return { success: true, granted: true, status }; + } + + // Screen recording has no askForMediaAccess equivalent — the TCC prompt + // is triggered by desktopCapturer.getSources(). Fire it and return so + // the renderer can re-check status after the user responds. + if (status === "not-determined") { + desktopCapturer.getSources({ types: ["screen"] }).catch(() => {}); + return { success: true, granted: false, status: "not-determined" }; + } + + return { success: true, granted: false, status }; + } catch (error) { + console.error("Failed to request screen access:", error); + return { success: false, granted: false, status: "unknown", error: String(error) }; + } + }); + // macOS Accessibility prompt for global click capture. First call shows the // system dialog; the user has to toggle the app in System Settings (no // programmatic grant exists for Accessibility). diff --git a/electron/main.ts b/electron/main.ts index 007df33..4e443a9 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url"; import { app, BrowserWindow, + desktopCapturer, ipcMain, Menu, nativeImage, @@ -449,23 +450,48 @@ app.whenReady().then(async () => { app.dock?.show(); } - // Allow microphone/media permission checks + // Allow microphone/media/screen permission checks session.defaultSession.setPermissionCheckHandler((_webContents, permission) => { - const allowed = ["media", "audioCapture", "microphone", "videoCapture", "camera"]; + const allowed = [ + "media", + "audioCapture", + "microphone", + "videoCapture", + "camera", + "screen", + "display-capture", + ]; return allowed.includes(permission); }); session.defaultSession.setPermissionRequestHandler((_webContents, permission, callback) => { - const allowed = ["media", "audioCapture", "microphone", "videoCapture", "camera"]; + const allowed = [ + "media", + "audioCapture", + "microphone", + "videoCapture", + "camera", + "screen", + "display-capture", + ]; callback(allowed.includes(permission)); }); - // Request microphone permission from macOS + // Request microphone and screen recording permissions from macOS if (process.platform === "darwin") { const micStatus = systemPreferences.getMediaAccessStatus("microphone"); if (micStatus !== "granted") { await systemPreferences.askForMediaAccess("microphone"); } + + // Screen recording has no askForMediaAccess equivalent — the TCC prompt is + // triggered by the first desktopCapturer.getSources() call. Firing it here + // at startup settles the permission state early and prevents repeated prompts + // driven by later getSources() calls (fixes repeated permission dialog). + const screenStatus = systemPreferences.getMediaAccessStatus("screen"); + if (screenStatus === "not-determined") { + desktopCapturer.getSources({ types: ["screen"] }).catch(() => {}); + } } // Listen for HUD overlay quit event (macOS only)