const { exec } = require('child_process'); const util = require('util'); const execAsync = util.promisify(exec); const VIRT_MIC_NAME = 'simplescreenshare-audio'; // Pipewire helper class to isolate application audio class PipewireHelper { // Create a virtual microphone (a null-audio-sink) that Electron can listen to static async createVirtualMic() { try { await execAsync(`pw-cli destroy ${VIRT_MIC_NAME}`).catch(() => {}); // Cleanup old const cmd = `pw-cli create-node adapter '{ factory.name=support.null-audio-sink node.name=${VIRT_MIC_NAME} node.description="SimpleScreenshare Audio" media.class=Audio/Source/Virtual object.linger=1 audio.position=[FL,FR] }'`; const { stdout } = await execAsync(cmd); console.log("Created virtual mic:", stdout.trim()); // Wait a moment for Pipewire to register it await new Promise(resolve => setTimeout(resolve, 500)); return true; } catch (error) { console.error("Failed to create virtual mic:", error); return false; } } // Destroy the virtual microphone static async destroyVirtualMic() { try { // Find node ID const { stdout } = await execAsync(`pw-cli dump short Node`); const lines = stdout.split('\\n'); for (const line of lines) { if (line.includes(VIRT_MIC_NAME)) { const id = line.split(',')[0].trim(); await execAsync(`pw-cli destroy ${id}`); console.log(`Destroyed virtual mic (ID: ${id})`); } } } catch (error) { console.error("Failed to destroy virtual mic:", error); } } // Get all running applications currently playing audio static async getAudioApplications() { try { // pw-dump outputs the entire graph as JSON, increase buffer! const { stdout } = await execAsync('pw-dump', { maxBuffer: 1024 * 1024 * 50 }); const dump = JSON.parse(stdout); // Filter out audio output streams (applications playing sound) const apps = dump.filter(node => node.info && node.info.props && node.info.props['media.class'] === 'Stream/Output/Audio' && node.info.props['application.name'] ).map(node => ({ id: node.id, name: node.info.props['application.name'], mediaName: node.info.props['media.name'] || 'Unknown Stream' })); // Deduplicate by application name and return const uniqueApps = []; const seen = new Set(); for (const app of apps) { if (!seen.has(app.name)) { seen.add(app.name); uniqueApps.push(app); } } return uniqueApps; } catch (error) { console.error("Failed to get audio applications:", error); return []; } } // Link a target application's output to our Virtual Microphone static async linkApplicationToMic(targetAppName) { try { // 1. Get the dump with massive buffer const { stdout: dumpOut } = await execAsync('pw-dump', { maxBuffer: 1024 * 1024 * 50 }); const dump = JSON.parse(dumpOut); // 2. Find the target application node const targetNode = dump.find(node => node.info && node.info.props && node.info.props['media.class'] === 'Stream/Output/Audio' && node.info.props['application.name'] === targetAppName ); // 3. Find our virtual mic node const micNode = dump.find(node => node.info && node.info.props && node.info.props['node.name'] === VIRT_MIC_NAME ); if (!targetNode || !micNode) { console.error("Could not find target node or virtual mic node"); return false; } console.log(`Linking ${targetAppName} (ID: ${targetNode.id}) to ${VIRT_MIC_NAME} (ID: ${micNode.id})`); // 4. Find the Ports for both nodes const ports = dump.filter(n => n.type === 'PipeWire:Interface:Port'); // Target App Output Ports const targetPorts = ports.filter(p => p.info.props['node.id'] === targetNode.id && p.info.direction === 'output'); const targetFL = targetPorts.find(p => p.info.props['audio.channel'] === 'FL'); const targetFR = targetPorts.find(p => p.info.props['audio.channel'] === 'FR'); // Virtual Mic Input Ports const micPorts = ports.filter(p => p.info.props['node.id'] === micNode.id && p.info.direction === 'input'); const micFL = micPorts.find(p => p.info.props['audio.channel'] === 'FL'); const micFR = micPorts.find(p => p.info.props['audio.channel'] === 'FR'); if (!targetFL || !targetFR || !micFL || !micFR) { console.error("Could not find stereo ports for linking"); return false; } // 5. Link them using port aliases! const targetFlAlias = targetFL.info.props['port.alias'] || targetFL.info.props['object.path'] || targetFL.id; const targetFrAlias = targetFR.info.props['port.alias'] || targetFR.info.props['object.path'] || targetFR.id; const micFlAlias = micFL.info.props['port.alias'] || micFL.info.props['object.path'] || micFL.id; const micFrAlias = micFR.info.props['port.alias'] || micFR.info.props['object.path'] || micFR.id; await execAsync(`pw-link "${targetFlAlias}" "${micFlAlias}"`).catch(e => console.log("pw-link output:", e.message)); await execAsync(`pw-link "${targetFrAlias}" "${micFrAlias}"`).catch(e => console.log("pw-link output:", e.message)); console.log("Successfully linked audio."); return true; } catch (error) { console.error("Failed to link application:", error); return false; } } } module.exports = PipewireHelper;