From 430895d9ab1e13632a965823d631e4ebd95d397b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=BB=D1=8C=D1=8F=20=D0=93=D0=BB=D0=B0=D0=B7=D1=83?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2?= Date: Fri, 13 Feb 2026 22:11:01 +0300 Subject: [PATCH] small fixes --- src/utils/renderer.js | 149 ++++++++++++++++++++++++++++++++++++++---- src/utils/window.js | 88 +++++++++++++++---------- 2 files changed, 190 insertions(+), 47 deletions(-) diff --git a/src/utils/renderer.js b/src/utils/renderer.js index 256270c..2618d88 100644 --- a/src/utils/renderer.js +++ b/src/utils/renderer.js @@ -201,6 +201,80 @@ ipcRenderer.on('push-to-talk-toggle', () => { ipcRenderer.send('push-to-talk-toggle'); }); +/** + * Checks screen capture permission status on macOS + * @returns {Promise} true if permission is granted + */ +async function checkMacOSScreenCapturePermission() { + if (!isMacOS) { + console.log('[Permission Check] Not macOS, skipping'); + return true; + } + + try { + console.log('[Permission Check] Checking screen capture permission on macOS...'); + const result = await ipcRenderer.invoke('check-screen-capture-permission'); + console.log('[Permission Check] Result:', result); + if (!result.granted) { + console.warn('[Permission Check] Screen capture permission not granted:', result.status); + logToMain('warn', 'Screen capture permission check:', result); + } else { + console.log('[Permission Check] Screen capture permission granted'); + } + return result.granted; + } catch (error) { + console.error('[Permission Check] Error checking permission:', error); + logToMain('error', 'Permission check error:', error); + return false; + } +} + +/** + * Wrapper for getDisplayMedia with timeout and retries + * @param {object} constraints - Media constraints + * @param {number} timeout - Timeout in milliseconds (default 30 seconds) + * @param {number} retries - Number of retry attempts (default 1) + * @returns {Promise} + */ +async function getDisplayMediaWithTimeout(constraints, timeout = 30000, retries = 1) { + for (let attempt = 1; attempt <= retries + 1; attempt++) { + try { + console.log(`[Attempt ${attempt}/${retries + 1}] Requesting display media...`); + console.log('[getDisplayMedia] Constraints:', JSON.stringify(constraints)); + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Display media request timed out')), timeout); + }); + + const mediaPromise = navigator.mediaDevices.getDisplayMedia(constraints); + + const stream = await Promise.race([mediaPromise, timeoutPromise]); + + console.log('Display media obtained successfully'); + console.log( + 'Stream tracks:', + stream.getTracks().map(t => ({ kind: t.kind, label: t.label, enabled: t.enabled })) + ); + return stream; + } catch (error) { + console.error(`[Attempt ${attempt}/${retries + 1}] Error type:`, error.constructor.name); + console.error(`[Attempt ${attempt}/${retries + 1}] Error message:`, error.message); + console.error(`[Attempt ${attempt}/${retries + 1}] Error name:`, error.name); + console.error(`[Attempt ${attempt}/${retries + 1}] Full error:`, error); + console.error(`[Attempt ${attempt}/${retries + 1}] Error stack:`, error.stack); + + if (attempt <= retries) { + console.log(`Retrying in 2 seconds...`); + const errorMsg = error.message || error.name || 'Unknown error'; + mastermind?.setStatus(`Error: ${errorMsg}. Retrying...`); + await new Promise(resolve => setTimeout(resolve, 2000)); + } else { + throw error; + } + } + } +} + async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'medium') { // Store the image quality for manual screenshots currentImageQuality = imageQuality; @@ -210,9 +284,30 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu const audioMode = preferencesCache.audioMode || 'speaker_only'; try { + // Pre-flight check for macOS if (isMacOS) { - // On macOS, use SystemAudioDump for audio and getDisplayMedia for screen + mastermind.setStatus('Checking permissions...'); + const hasPermission = await checkMacOSScreenCapturePermission(); + if (!hasPermission) { + throw new Error('Screen recording permission not granted. Please enable in System Settings.'); + } + } + + if (isMacOS) { + // On macOS, use SystemAudioDump for audio and custom picker for screen console.log('Starting macOS capture with SystemAudioDump...'); + mastermind.setStatus('Choose screen to share...'); + + // Show screen picker dialog (like Windows) + const appElement = document.querySelector('mastermind-app'); + const pickerResult = await appElement.showScreenPickerDialog(); + + if (pickerResult.cancelled) { + mastermind.setStatus('Cancelled'); + return; + } + + mastermind.setStatus('Starting capture...'); // Start macOS audio capture const audioResult = await ipcRenderer.invoke('start-macos-audio'); @@ -220,15 +315,19 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu throw new Error('Failed to start macOS audio capture: ' + audioResult.error); } - // Get screen capture for screenshots - mediaStream = await navigator.mediaDevices.getDisplayMedia({ - video: { - frameRate: 1, - width: { ideal: 1920 }, - height: { ideal: 1080 }, + // Get screen capture for screenshots - use wrapper with timeout + mediaStream = await getDisplayMediaWithTimeout( + { + video: { + frameRate: 1, + width: { ideal: 1920 }, + height: { ideal: 1080 }, + }, + audio: false, // Don't use browser audio on macOS }, - audio: false, // Don't use browser audio on macOS - }); + 45000, + 2 + ); // 45 seconds timeout, 2 retry attempts console.log('macOS screen capture started - audio handled by SystemAudioDump'); @@ -399,22 +498,44 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu // Manual mode only - screenshots captured on demand via shortcut console.log('Manual mode enabled - screenshots will be captured on demand only'); } catch (err) { - console.error('Error starting capture:', err); + console.error('Error starting capture - Type:', err.constructor.name); + console.error('Error starting capture - Message:', err.message); + console.error('Error starting capture - Name:', err.name); + console.error('Error starting capture - Full:', err); + console.error('Error starting capture - Stack:', err.stack); + console.error('Error starting capture - Keys:', Object.keys(err)); + logToMain('error', 'Capture error:', { + type: err.constructor.name, + message: err.message, + name: err.name, + stack: err.stack, + }); // Provide more helpful error messages based on error type - let errorMessage = err.message || 'Failed to start capture'; + let errorMessage = err.message || err.name || 'Failed to start capture'; if (errorMessage.toLowerCase().includes('timeout')) { - errorMessage = 'Screen capture timed out. Please try again and select a screen quickly.'; + errorMessage = + 'Screen capture timed out. This may be due to:\n' + + '• System permissions not granted\n' + + '• Screen Recording permission needs to be enabled in System Settings\n' + + '• System picker dialog was closed\n\n' + + 'Please check System Settings > Privacy & Security > Screen Recording'; } else if (errorMessage.toLowerCase().includes('permission') || errorMessage.toLowerCase().includes('denied')) { - errorMessage = 'Screen capture permission denied. Please grant screen recording permission in System Settings.'; + errorMessage = + 'Screen capture permission denied.\n' + + 'Please grant Screen Recording permission in:\n' + + 'System Settings > Privacy & Security > Screen Recording'; } else if (errorMessage.toLowerCase().includes('not found') || errorMessage.toLowerCase().includes('no sources')) { errorMessage = 'No screen sources found. Please ensure a display is connected.'; } else if (errorMessage.toLowerCase().includes('aborted') || errorMessage.toLowerCase().includes('cancel')) { - errorMessage = 'Screen selection was cancelled. Please try again.'; + errorMessage = 'Screen selection was cancelled. Click Start to try again.'; } mastermind.setStatus('Error: ' + errorMessage); + + // Show retry button in UI + mastermind?.showRetryButton?.(); } } diff --git a/src/utils/window.js b/src/utils/window.js index 98cbb33..9253a5e 100644 --- a/src/utils/window.js +++ b/src/utils/window.js @@ -9,7 +9,6 @@ let windowResizing = false; let resizeAnimation = null; const RESIZE_ANIMATION_DURATION = 500; // milliseconds - function createWindow(sendToRenderer, geminiSessionRef) { // Get layout preference (default to 'normal') let windowWidth = 1100; @@ -38,44 +37,46 @@ function createWindow(sendToRenderer, geminiSessionRef) { // Store selected source for Windows custom picker let selectedSourceId = null; - // Setup display media handler based on platform - if (process.platform === 'darwin') { - // macOS: Use native system picker - session.defaultSession.setDisplayMediaRequestHandler( - (request, callback) => { - desktopCapturer.getSources({ types: ['screen'] }).then(sources => { - callback({ video: sources[0], audio: 'loopback' }); - }); - }, - { useSystemPicker: true } - ); - } else { - // Windows/Linux: Use selected source from custom picker - session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => { - try { - const sources = await desktopCapturer.getSources({ - types: ['screen', 'window'], - thumbnailSize: { width: 0, height: 0 }, - }); + // Setup display media handler - unified approach for all platforms + // Use custom picker instead of system picker to avoid macOS timeout issues + session.defaultSession.setDisplayMediaRequestHandler(async (request, callback) => { + try { + console.log('[Display Media Handler] Request received, fetching sources...'); - // Find the selected source or use first screen - let source = sources[0]; - if (selectedSourceId) { - const found = sources.find(s => s.id === selectedSourceId); - if (found) source = found; - } + const sources = await desktopCapturer.getSources({ + types: ['screen', 'window'], + thumbnailSize: { width: 0, height: 0 }, + fetchWindowIcons: false, // Optimization for faster fetching + }); - if (source) { - callback({ video: source, audio: 'loopback' }); + console.log('[Display Media Handler] Sources fetched:', sources.length); + + // Find the selected source or use first screen + let source = sources[0]; + if (selectedSourceId) { + const found = sources.find(s => s.id === selectedSourceId); + if (found) { + source = found; + console.log('[Display Media Handler] Using selected source:', source.name); } else { - callback({}); + console.warn('[Display Media Handler] Selected source not found, using first available'); } - } catch (error) { - console.error('Error in display media handler:', error); + } else { + console.log('[Display Media Handler] No source selected, using first available:', source?.name); + } + + if (source) { + callback({ video: source, audio: 'loopback' }); + console.log('[Display Media Handler] Callback invoked with source:', source.id); + } else { + console.error('[Display Media Handler] No sources available'); callback({}); } - }); - } + } catch (error) { + console.error('[Display Media Handler] Error:', error); + callback({}); + } + }); // IPC handler to set selected source ipcMain.handle('set-selected-source', async (event, sourceId) => { @@ -83,6 +84,27 @@ function createWindow(sendToRenderer, geminiSessionRef) { return { success: true }; }); + // IPC handler to check screen capture permission (macOS) + ipcMain.handle('check-screen-capture-permission', async () => { + if (process.platform !== 'darwin') { + return { granted: true }; + } + + try { + const { systemPreferences } = require('electron'); + const status = systemPreferences.getMediaAccessStatus('screen'); + + return { + granted: status === 'granted', + status: status, + message: status === 'granted' ? 'Screen recording permission is granted' : `Screen recording permission status: ${status}`, + }; + } catch (error) { + console.error('Error checking screen capture permission:', error); + return { granted: false, error: error.message }; + } + }); + mainWindow.setResizable(false); mainWindow.setContentProtection(true); mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });