Add custom screen picker dialog for Windows audio capture and update version to 0.5.10
Build and Release / build (x64, ubuntu-latest, linux) (push) Has been skipped
Build and Release / build (arm64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, macos-latest, darwin) (push) Has been cancelled
Build and Release / build (x64, windows-latest, win32) (push) Has been cancelled
Build and Release / release (push) Has been cancelled

This commit is contained in:
Илья Глазунов
2026-01-15 21:14:50 +03:00
parent 76d6fc2749
commit fb6c8e3fc0
5 changed files with 387 additions and 36 deletions
+87 -27
View File
@@ -294,7 +294,21 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
console.log('Linux capture started - system audio:', mediaStream.getAudioTracks().length > 0, 'microphone mode:', audioMode);
} else {
// Windows - use display media with loopback for system audio
// Windows - show custom screen picker first
logToMain('info', '=== Starting Windows audio capture ===');
cheatingDaddy.setStatus('Choose screen to share...');
// Show screen picker dialog
const appElement = document.querySelector('cheating-daddy-app');
const pickerResult = await appElement.showScreenPickerDialog();
if (pickerResult.cancelled) {
cheatingDaddy.setStatus('Cancelled');
return;
}
cheatingDaddy.setStatus('Starting capture...');
mediaStream = await navigator.mediaDevices.getDisplayMedia({
video: {
frameRate: 1,
@@ -310,7 +324,6 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
},
});
console.log('Windows capture started with loopback audio');
const audioTracks = mediaStream.getAudioTracks();
const videoTracks = mediaStream.getVideoTracks();
@@ -326,8 +339,14 @@ async function startCapture(screenshotIntervalSeconds = 5, imageQuality = 'mediu
})),
});
// Setup audio processing for Windows loopback audio only
setupWindowsLoopbackProcessing();
if (audioTracks.length === 0) {
logToMain('warn', 'WARNING: No audio tracks! User must check "Share audio" in screen picker dialog');
cheatingDaddy.setStatus('Warning: No audio - enable "Share audio" checkbox');
} else {
logToMain('info', 'Audio track acquired, setting up processing...');
// Setup audio processing for Windows loopback audio only
setupWindowsLoopbackProcessing();
}
if (audioMode === 'mic_only' || audioMode === 'both') {
let micStream = null;
@@ -443,32 +462,73 @@ function setupLinuxSystemAudioProcessing() {
function setupWindowsLoopbackProcessing() {
// Setup audio processing for Windows loopback audio only
audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
const source = audioContext.createMediaStreamSource(mediaStream);
audioProcessor = audioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
let audioBuffer = [];
const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
audioProcessor.onaudioprocess = async e => {
const inputData = e.inputBuffer.getChannelData(0);
audioBuffer.push(...inputData);
// Process audio in chunks
while (audioBuffer.length >= samplesPerChunk) {
const chunk = audioBuffer.splice(0, samplesPerChunk);
const pcmData16 = convertFloat32ToInt16(chunk);
const base64Data = arrayBufferToBase64(pcmData16.buffer);
await ipcRenderer.invoke('send-audio-content', {
data: base64Data,
mimeType: 'audio/pcm;rate=24000',
logToMain('info', 'Setting up Windows loopback audio processing...');
try {
audioContext = new AudioContext({ sampleRate: SAMPLE_RATE });
logToMain('info', 'AudioContext created:', {
state: audioContext.state,
sampleRate: audioContext.sampleRate,
});
// Resume AudioContext if suspended (Chrome policy)
if (audioContext.state === 'suspended') {
logToMain('warn', 'AudioContext suspended, attempting resume...');
audioContext.resume().then(() => {
logToMain('info', 'AudioContext resumed successfully');
}).catch(err => {
logToMain('error', 'Failed to resume AudioContext:', err.message);
});
}
};
const source = audioContext.createMediaStreamSource(mediaStream);
audioProcessor = audioContext.createScriptProcessor(BUFFER_SIZE, 1, 1);
source.connect(audioProcessor);
audioProcessor.connect(audioContext.destination);
let audioBuffer = [];
const samplesPerChunk = SAMPLE_RATE * AUDIO_CHUNK_DURATION;
let chunkCount = 0;
let totalSamples = 0;
audioProcessor.onaudioprocess = async e => {
const inputData = e.inputBuffer.getChannelData(0);
audioBuffer.push(...inputData);
totalSamples += inputData.length;
// Process audio in chunks
while (audioBuffer.length >= samplesPerChunk) {
const chunk = audioBuffer.splice(0, samplesPerChunk);
const pcmData16 = convertFloat32ToInt16(chunk);
const base64Data = arrayBufferToBase64(pcmData16.buffer);
await ipcRenderer.invoke('send-audio-content', {
data: base64Data,
mimeType: 'audio/pcm;rate=24000',
});
chunkCount++;
// Log progress every 100 chunks (~10 seconds)
if (chunkCount === 1) {
logToMain('info', 'First audio chunk sent to AI');
cheatingDaddy.setStatus('Listening...');
} else if (chunkCount % 100 === 0) {
// Calculate max amplitude to check if we're getting real audio
const maxAmp = Math.max(...chunk.map(Math.abs));
logToMain('info', `Audio progress: ${chunkCount} chunks, maxAmplitude: ${maxAmp.toFixed(4)}`);
}
}
};
source.connect(audioProcessor);
audioProcessor.connect(audioContext.destination);
logToMain('info', 'Windows audio processing pipeline connected');
} catch (err) {
logToMain('error', 'Error setting up Windows audio:', err.message, err.stack);
cheatingDaddy.setStatus('Audio error: ' + err.message);
}
}
async function captureScreenshot(imageQuality = 'medium', isManual = false) {
+71 -8
View File
@@ -33,14 +33,54 @@ function createWindow(sendToRenderer, geminiSessionRef) {
});
const { session, desktopCapturer } = require('electron');
session.defaultSession.setDisplayMediaRequestHandler(
(request, callback) => {
desktopCapturer.getSources({ types: ['screen'] }).then(sources => {
callback({ video: sources[0], audio: 'loopback' });
});
},
{ useSystemPicker: true }
);
// 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 },
});
// 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;
}
if (source) {
callback({ video: source, audio: 'loopback' });
} else {
callback({});
}
} catch (error) {
console.error('Error in display media handler:', error);
callback({});
}
});
}
// IPC handler to set selected source
ipcMain.handle('set-selected-source', async (event, sourceId) => {
selectedSourceId = sourceId;
return { success: true };
});
mainWindow.setResizable(false);
mainWindow.setContentProtection(true);
@@ -715,6 +755,29 @@ function setupWindowIpcHandlers(mainWindow, sendToRenderer, geminiSessionRef) {
return { success: false, error: error.message };
}
});
// Get available screen sources for picker
ipcMain.handle('get-screen-sources', async () => {
try {
const { desktopCapturer } = require('electron');
const sources = await desktopCapturer.getSources({
types: ['screen', 'window'],
thumbnailSize: { width: 150, height: 150 },
});
return {
success: true,
sources: sources.map(source => ({
id: source.id,
name: source.name,
thumbnail: source.thumbnail.toDataURL(),
})),
};
} catch (error) {
console.error('Error getting screen sources:', error);
return { success: false, error: error.message };
}
});
}
module.exports = {