before its fucked again

This commit is contained in:
2026-02-23 05:31:33 +01:00
parent 831d76c10e
commit 0d7a51ddcd
8 changed files with 686 additions and 137 deletions

View File

@@ -12,7 +12,7 @@ class PipewireHelper {
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 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] audio.rate=48000 audio.channels=2 }\'`;
const { stdout } = await execAsync(cmd);
console.log("Created virtual mic:", stdout.trim());
@@ -79,6 +79,47 @@ class PipewireHelper {
}
}
// Remove all existing links TO the virtual mic's input ports
// This prevents echo from stale connections when switching audio sources
static async unlinkAllFromMic() {
try {
// IMPORTANT: Use `pw-link -l` NOT `pw-link -l -I` — the -I flag hangs when piped
const { stdout } = await execAsync(`pw-link -l`, { maxBuffer: 1024 * 1024, timeout: 3000 }).catch(() => ({ stdout: '' }));
if (!stdout) return;
const lines = stdout.split('\n');
// pw-link -l format:
// alsa_output...:monitor_FL (source port - NOT indented)
// |-> simplescreenshare-audio:input_FL (outgoing link - indented with |->)
let currentSourcePort = null;
for (const line of lines) {
if (!line.trim()) continue;
// Non-indented line = port declaration
if (!line.startsWith(' ')) {
currentSourcePort = line.trim();
continue;
}
// Indented line with |-> targeting our virtual mic
const trimmed = line.trim();
if (trimmed.startsWith('|->') && (trimmed.includes(`${VIRT_MIC_NAME}:input_`) || trimmed.includes('SimpleScreenshare Audio:input_'))) {
const targetPort = trimmed.replace('|->', '').trim();
if (currentSourcePort && targetPort) {
console.log(`Unlinking: "${currentSourcePort}" -> "${targetPort}"`);
await execAsync(`pw-link -d "${currentSourcePort}" "${targetPort}"`).catch(e =>
console.log("pw-link unlink:", e.message)
);
}
}
}
} catch (error) {
console.error("Failed to unlink from mic:", error);
}
}
// Link a target application's output to our Virtual Microphone
static async linkApplicationToMic(targetAppName) {
try {
@@ -108,6 +149,9 @@ class PipewireHelper {
console.log(`Linking ${targetAppName} (ID: ${targetNode.id}) to ${VIRT_MIC_NAME} (ID: ${micNode.id})`);
// Clean up any existing links to prevent echo from stale connections
await this.unlinkAllFromMic();
// 4. Find the Ports for both nodes
const ports = dump.filter(n => n.type === 'PipeWire:Interface:Port');
@@ -143,6 +187,73 @@ class PipewireHelper {
return false;
}
}
// Link the system's default audio output monitor to our Virtual Microphone
// This captures ALL desktop audio cleanly via Pipewire without Chromium's broken desktop audio capture
static async linkMonitorToMic() {
try {
const { stdout: dumpOut } = await execAsync('pw-dump', { maxBuffer: 1024 * 1024 * 50 });
const dump = JSON.parse(dumpOut);
// Find the default audio sink (the system's main output)
const sinkNode = dump.find(node =>
node.info &&
node.info.props &&
node.info.props['media.class'] === 'Audio/Sink' &&
(node.info.props['node.name'] || '').includes('output')
);
// Find our virtual mic node
const micNode = dump.find(node =>
node.info &&
node.info.props &&
node.info.props['node.name'] === VIRT_MIC_NAME
);
if (!sinkNode || !micNode) {
console.error("Could not find default sink or virtual mic node");
return false;
}
console.log(`Linking system monitor (ID: ${sinkNode.id}) to ${VIRT_MIC_NAME} (ID: ${micNode.id})`);
// Clean up any existing links to prevent echo from stale connections
await this.unlinkAllFromMic();
const ports = dump.filter(n => n.type === 'PipeWire:Interface:Port');
// The monitor ports on a sink are "output" direction (they output what the sink is playing)
const sinkMonitorPorts = ports.filter(p =>
p.info.props['node.id'] === sinkNode.id && p.info.direction === 'output'
);
const sinkFL = sinkMonitorPorts.find(p => p.info.props['audio.channel'] === 'FL');
const sinkFR = sinkMonitorPorts.find(p => p.info.props['audio.channel'] === 'FR');
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 (!sinkFL || !sinkFR || !micFL || !micFR) {
console.error("Could not find stereo monitor/mic ports for linking");
return false;
}
const sinkFlAlias = sinkFL.info.props['port.alias'] || sinkFL.info.props['object.path'] || sinkFL.id;
const sinkFrAlias = sinkFR.info.props['port.alias'] || sinkFR.info.props['object.path'] || sinkFR.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 "${sinkFlAlias}" "${micFlAlias}"`).catch(e => console.log("pw-link output:", e.message));
await execAsync(`pw-link "${sinkFrAlias}" "${micFrAlias}"`).catch(e => console.log("pw-link output:", e.message));
console.log("Successfully linked system monitor audio.");
return true;
} catch (error) {
console.error("Failed to link monitor:", error);
return false;
}
}
}
module.exports = PipewireHelper;