stoat-for-desktop/src/native/virtualMic.ts

140 lines
3.9 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
// Disable any checks because node-pipewire doesn't have types for our submodule
import { app, ipcMain } from "electron";
import { sinkName, sourceName } from "../constants";
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
function getPids() {
return app.getAppMetrics().map((proc) => proc.pid ?? -1);
}
export const isWayland =
process.platform === "linux" &&
(process.env.XDG_SESSION_TYPE === "wayland" || !!process.env.WAYLAND_DISPLAY);
ipcMain.handle("getIsWayland", () => isWayland);
export async function initVirtualMic() {
// Only available on Wayland
if (!isWayland) return;
try {
const {
createPwThread,
createSink,
createSource,
getClients,
getNodes,
linkNodesNameToId,
//@ts-expect-error This module may not be found on non-linux builds.
} = await import("node-pipewire"); //eslint-disable-line
createPwThread();
// Wait for pipewire thread to start and gather neccessary data
await delay(100);
let nodes = getNodes();
let sinkFound = false;
let sourceFound = false;
for (const node of nodes) {
if (node.name === sinkName) {
sinkFound = true;
}
if (node.name === sourceName) {
sourceFound = true;
}
}
if (!sinkFound) {
createSink(sinkName, ["FL", "FR"], false);
}
if (!sourceFound) {
createSource(sourceName, ["FL", "FR"], false);
}
// Wait for source and sink to save
await delay(100);
const appName = app.getName();
nodes = getNodes();
const savedNodes: Record<number, any> = {};
const sourceNode = nodes.filter((node: any) => node.name === sourceName)[0];
const sinkNode = nodes.filter((node: any) => node.name === sinkName)[0];
linkNodesNameToId(sinkNode.name, sourceNode.id, false);
setInterval(() => {
const ourClients: Record<number, any> = {};
const paClients: any[] = [];
const pids = getPids();
const clients = getClients();
for (const client of clients) {
// If the client belongs to one of the electton processes
if (pids.includes(client.pid)) {
ourClients[client.pid] = client;
}
// If the client is a pulse audio client made on behalf of this app
if (client.application_name === appName) {
paClients.push(client);
}
}
nodes = getNodes()
// Only choose output streams
.filter(
(node: any) => node.props["media.class"] === "Stream/Output/Audio",
)
// Ignore any nodes from electron's processes
.filter(
(node: any) =>
!Object.values(ourClients)
.map((client) => client.id)
.includes(Number(node.props["client.id"])),
)
// Ignore any nodes from pulse audio processes for the app
.filter(
(node: any) =>
!paClients
.map((client) => client.id)
.includes(Number(node.props["client.id"])),
);
for (const node of nodes) {
const idAsNum = Number(node.id);
// If this node hasn't been seen before (ie. new node)
if (!savedNodes[idAsNum]) {
// Link all of the new node's outputs to our virtual sink
linkNodesNameToId(node.name, sinkNode.id, false);
savedNodes[idAsNum] = node;
}
}
// Cleanup savedNodes for nodes that are gone
for (const id in savedNodes) {
const asNum = Number(id);
let found = false;
for (const node of nodes) {
if (node.id === asNum) {
found = true;
break;
}
}
if (!found) {
savedNodes[asNum] = void 0;
}
}
}, 1000);
} catch {
console.log(
"node-pipewire failed to load. Screen share audio will not work on linux wayland.",
);
}
}