fix: macos screen capture prompt

This commit is contained in:
2026-02-10 19:40:42 +01:00
parent d349b226aa
commit 84a9f6f8bd
6 changed files with 82 additions and 3 deletions

View File

@@ -36,6 +36,7 @@ interface HeliumElectronAPI {
venmicLink: (options: VenmicLinkOptions) => Promise<boolean>; venmicLink: (options: VenmicLinkOptions) => Promise<boolean>;
venmicUnlink: () => Promise<boolean>; venmicUnlink: () => Promise<boolean>;
checkScreenPermission: () => Promise<string>; checkScreenPermission: () => Promise<string>;
openScreenPermissionSettings: () => Promise<boolean>;
} }
declare global { declare global {
@@ -212,6 +213,28 @@ export function useElectron() {
return platformInfo.value.supportsLoopbackAudio || platformInfo.value.supportsVenmic; return platformInfo.value.supportsLoopbackAudio || platformInfo.value.supportsVenmic;
}); });
const getScreenPermissionStatus = async (): Promise<string> => {
if (!checkElectron()) return "granted";
try {
return await window.heliumElectron!.checkScreenPermission();
} catch (error) {
console.error("[useElectron] Failed to check screen permission:", error);
return "unknown";
}
};
const openScreenPermissionSettings = async (): Promise<boolean> => {
if (!checkElectron()) return false;
try {
return await window.heliumElectron!.openScreenPermissionSettings();
} catch (error) {
console.error("[useElectron] Failed to open screen permission settings:", error);
return false;
}
};
onMounted(() => { onMounted(() => {
checkElectron(); checkElectron();
if (isElectron.value) { if (isElectron.value) {
@@ -246,9 +269,10 @@ export function useElectron() {
linkAllAudio, linkAllAudio,
linkAppAudio, linkAppAudio,
unlinkVenmicAudio, unlinkVenmicAudio,
getScreenPermissionStatus,
openScreenPermissionSettings,
startScreenShareWithAudio, startScreenShareWithAudio,
stopScreenShare, stopScreenShare,
}; };
} }

View File

@@ -56,6 +56,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useWebSocket } from "@vueuse/core"; import { useWebSocket } from "@vueuse/core";
import { toast } from "vue-sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@@ -66,6 +67,7 @@ import { useElectron } from "~/composables/useElectron";
import PresetSelect from "~/components/app/PresetSelect.vue"; import PresetSelect from "~/components/app/PresetSelect.vue";
const streamerStore = useStreamerStore(); const streamerStore = useStreamerStore();
const { t } = useI18n();
const videofeedRef = ref<HTMLVideoElement | null>(null); const videofeedRef = ref<HTMLVideoElement | null>(null);
const localStream = ref<MediaStream | null>(null); const localStream = ref<MediaStream | null>(null);
const wsUrl = useWebSocketUrl(); const wsUrl = useWebSocketUrl();
@@ -83,6 +85,8 @@ const {
linkAllAudio, linkAllAudio,
linkAppAudio, linkAppAudio,
unlinkVenmicAudio, unlinkVenmicAudio,
getScreenPermissionStatus,
openScreenPermissionSettings,
} = useElectron(); } = useElectron();
onMounted(async () => { onMounted(async () => {
@@ -231,6 +235,7 @@ async function startScreenShare() {
); );
} catch (error) { } catch (error) {
console.error("Failed to start screen share:", error); console.error("Failed to start screen share:", error);
await handleScreenShareError(error);
cleanupStreaming(); cleanupStreaming();
} }
} }
@@ -293,9 +298,36 @@ async function changeScreenShareSource() {
} }
} catch (error) { } catch (error) {
console.error("Failed to change screen share source:", error); console.error("Failed to change screen share source:", error);
await handleScreenShareError(error);
} }
} }
async function handleScreenShareError(error: unknown): Promise<void> {
const isPermissionDeniedError =
error instanceof DOMException && error.name === "NotAllowedError";
if (!isPermissionDeniedError || !isElectron.value || !platformInfo.value?.isMac) {
toast.error(t("failedToStartScreenShare"));
return;
}
const permissionStatus = await getScreenPermissionStatus();
if (permissionStatus === "granted") {
toast.error(t("failedToStartScreenShare"));
return;
}
const openedSettings = await openScreenPermissionSettings();
if (openedSettings) {
toast.error(t("screenRecordingPermissionRequired"));
return;
}
toast.error(t("screenRecordingPermissionRequiredNoShortcut"));
}
async function cleanupStreaming() { async function cleanupStreaming() {
if (localStream.value) { if (localStream.value) {
localStream.value.getTracks().forEach((track) => { localStream.value.getTracks().forEach((track) => {

View File

@@ -4,6 +4,7 @@ import {
ipcMain, ipcMain,
desktopCapturer, desktopCapturer,
session, session,
shell,
systemPreferences, systemPreferences,
type IpcMainInvokeEvent, type IpcMainInvokeEvent,
type DesktopCapturerSource, type DesktopCapturerSource,
@@ -208,6 +209,20 @@ ipcMain.handle('helium:check-screen-permission', () => {
return 'granted'; return 'granted';
}); });
ipcMain.handle('helium:open-screen-permission-settings', async () => {
if (!isMac) {
return false;
}
try {
await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture');
return true;
} catch (error) {
console.error('[Helium] Failed to open macOS screen recording settings:', error);
return false;
}
});
const gotTheLock = app.requestSingleInstanceLock(); const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) { if (!gotTheLock) {

View File

@@ -57,6 +57,8 @@ const heliumElectronAPI = {
venmicUnlink: (): Promise<boolean> => ipcRenderer.invoke('helium:venmic-unlink'), venmicUnlink: (): Promise<boolean> => ipcRenderer.invoke('helium:venmic-unlink'),
checkScreenPermission: (): Promise<string> => ipcRenderer.invoke('helium:check-screen-permission'), checkScreenPermission: (): Promise<string> => ipcRenderer.invoke('helium:check-screen-permission'),
openScreenPermissionSettings: (): Promise<boolean> =>
ipcRenderer.invoke('helium:open-screen-permission-settings'),
}; };
contextBridge.exposeInMainWorld('heliumElectron', heliumElectronAPI); contextBridge.exposeInMainWorld('heliumElectron', heliumElectronAPI);

View File

@@ -59,5 +59,8 @@
"audioSource": "Audio Source", "audioSource": "Audio Source",
"allSystemAudio": "All System Audio", "allSystemAudio": "All System Audio",
"refreshSources": "Refresh Sources", "refreshSources": "Refresh Sources",
"audioSupported": "Audio Supported" "audioSupported": "Audio Supported",
"failedToStartScreenShare": "Failed to start screen share.",
"screenRecordingPermissionRequired": "macOS blocked screen capture. Allow Helium in System Settings > Privacy & Security > Screen Recording, then restart Helium.",
"screenRecordingPermissionRequiredNoShortcut": "macOS blocked screen capture. Open System Settings > Privacy & Security > Screen Recording, allow Helium, then restart Helium."
} }

View File

@@ -59,5 +59,8 @@
"audioSource": "Fuente de audio", "audioSource": "Fuente de audio",
"allSystemAudio": "Todo el audio del sistema", "allSystemAudio": "Todo el audio del sistema",
"refreshSources": "Actualizar fuentes", "refreshSources": "Actualizar fuentes",
"audioSupported": "Audio soportado" "audioSupported": "Audio soportado",
"failedToStartScreenShare": "No se pudo iniciar el uso compartido de pantalla.",
"screenRecordingPermissionRequired": "macOS bloqueó la captura de pantalla. Permite Helium en Configuración del Sistema > Privacidad y seguridad > Grabación de pantalla y luego reinicia Helium.",
"screenRecordingPermissionRequiredNoShortcut": "macOS bloqueó la captura de pantalla. Abre Configuración del Sistema > Privacidad y seguridad > Grabación de pantalla, permite Helium y luego reinicia Helium."
} }