const { app, BrowserWindow, desktopCapturer, ipcMain } = require('electron'); const path = require('path'); const PipewireHelper = require('./pipewire'); // Ensure Chromium tries to use PipeWire for screen sharing on Linux app.commandLine.appendSwitch('enable-features', 'WebRTCPipeWireCapturer'); function createWindow() { const win = new BrowserWindow({ width: 1000, height: 800, webPreferences: { preload: path.join(__dirname, 'preload.js'), nodeIntegration: false, contextIsolation: true, } }); win.loadFile('index.html'); } app.whenReady().then(async () => { // Setup the virtual microphone as soon as the app starts await PipewireHelper.createVirtualMic(); createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on('will-quit', async () => { await PipewireHelper.destroyVirtualMic(); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); // Handle IPC request from renderer to get screen/audio sources ipcMain.handle('get-sources', async () => { let inputSources = await desktopCapturer.getSources({ types: ['window', 'screen'], fetchWindowIcons: true }); // Wayland Workaround: If we only see generic "WebRTC PipeWire capturer" windows, // try to fetch real window titles via our python helper try { const genericNames = ['webrtc pipewire capturer', 'screen 1', 'screen 2']; const hasGeneric = inputSources.some(s => genericNames.includes(s.name.toLowerCase())); if (hasGeneric || inputSources.length === 1) { const { execSync } = require('child_process'); const pyPath = path.join(__dirname, 'wayland-helper.py'); const out = execSync(`python3 ${pyPath}`, { timeout: 2000 }).toString(); const waylandWindows = JSON.parse(out); if (waylandWindows && waylandWindows.length > 0) { // If we only have 1 capturer source (common on Wayland compositors), // rename it to the first active window title we found to be helpful. if (inputSources.length === 1 && waylandWindows[0].title) { inputSources[0].name = waylandWindows[0].title; } } } } catch (e) { console.error("Wayland helper failed:", e.message); } return inputSources.map(source => ({ id: source.id, name: source.name, thumbnail: source.thumbnail.toDataURL() })); }); // Handle Pipewire specific audio isolation requests ipcMain.handle('get-audio-apps', async () => { return await PipewireHelper.getAudioApplications(); }); ipcMain.handle('link-app-audio', async (event, appName) => { return await PipewireHelper.linkApplicationToMic(appName); }); ipcMain.handle('link-monitor-audio', async () => { return await PipewireHelper.linkMonitorToMic(); }); // Handle saving and loading the config.json profile const fs = require('fs'); const configPath = path.join(__dirname, 'config.json'); ipcMain.handle('get-config', () => { try { if (fs.existsSync(configPath)) { return JSON.parse(fs.readFileSync(configPath, 'utf8')); } } catch (e) { console.error("Could not read config", e); } return { serverUrl: 'http://localhost:3000', serverPassword: '' }; }); ipcMain.handle('save-config', (event, newConfig) => { fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), 'utf8'); });