feat: Implement an Electron-based broadcasting client with screen/window and audio source selection, including Pipewire integration, and add auto-unmute to the viewer.
This commit is contained in:
148
client/pipewire.js
Normal file
148
client/pipewire.js
Normal file
@@ -0,0 +1,148 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user