Skip to content
This repository was archived by the owner on Mar 25, 2026. It is now read-only.

Commit 415633c

Browse files
committed
fix(linux): add X11 audio picker support and fix null track handling
- Show audio picker for Linux X11 path (Wayland already handled in electron-main.ts) - Fix potential crash when audioTrack is undefined before addTrack() - Extract shared AudioSelection and VenmicListResult types to @types/audio-sharing.d.ts - Reduce logging verbosity by using console.debug for operational messages
1 parent 78511a7 commit 415633c

File tree

7 files changed

+90
-78
lines changed

7 files changed

+90
-78
lines changed

src/@types/audio-sharing.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import type { Node } from "@vencord/venmic";
9+
10+
/** User's audio source selection from the audio picker. */
11+
export interface AudioSelection {
12+
type: "none" | "system" | "app";
13+
node?: Node;
14+
}
15+
16+
/** Result of listing available venmic audio nodes. */
17+
export type VenmicListResult =
18+
| { ok: true; targets: Node[]; hasPipewirePulse: boolean }
19+
| { ok: false; isGlibcOutdated: boolean };

src/audio-picker-preload.cts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,7 @@ Please see LICENSE files in the repository root for full details.
1010

1111
import { contextBridge, ipcRenderer } from "electron";
1212

13-
import type { Node } from "@vencord/venmic";
14-
15-
export interface AudioSelection {
16-
type: "none" | "system" | "app";
17-
node?: Node;
18-
}
19-
20-
export interface VenmicListResult {
21-
ok: true;
22-
targets: Node[];
23-
hasPipewirePulse: boolean;
24-
}
13+
import type { AudioSelection, VenmicListResult } from "./@types/audio-sharing.js" with { "resolution-mode": "import" };
2514

2615
contextBridge.exposeInMainWorld("audioPickerAPI", {
2716
/**

src/audio-picker.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,10 @@ import { existsSync } from "node:fs";
1010
import { dirname, join } from "node:path";
1111
import { fileURLToPath } from "node:url";
1212

13-
import type { Node } from "@vencord/venmic";
14-
import {
15-
type VenmicListResult,
16-
listVenmicNodes,
17-
startVenmicDirect,
18-
startVenmicSystemDirect,
19-
stopVenmicDirect,
20-
} from "./venmic.js";
13+
import type { AudioSelection, VenmicListResult } from "./@types/audio-sharing.js";
14+
import { listVenmicNodes, startVenmicDirect, startVenmicSystemDirect, stopVenmicDirect } from "./venmic.js";
15+
16+
export type { AudioSelection };
2117

2218
const __dirname = dirname(fileURLToPath(import.meta.url));
2319

@@ -46,11 +42,6 @@ function getAudioPickerHtmlPath(): string {
4642
return candidates[0];
4743
}
4844

49-
export interface AudioSelection {
50-
type: "none" | "system" | "app";
51-
node?: Node;
52-
}
53-
5445
/**
5546
* Shows a native dialog for selecting audio sources to share.
5647
* Returns the user's selection or "none" if cancelled.

src/ipc.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,11 +148,19 @@ ipcMain.on("ipcCall", async function (_ev: IpcMainEvent, payload) {
148148
}));
149149
break;
150150
case "callDisplayMediaCallback": {
151-
const shouldIncludeAudio = getAudioRequested() && process.platform === "win32";
151+
const audioRequested = getAudioRequested();
152152
const callback = getDisplayMediaCallback();
153153
setDisplayMediaCallback(null);
154154
setAudioRequested(false);
155-
if (shouldIncludeAudio) {
155+
156+
// Show audio picker for Linux (X11 path - Wayland is handled in electron-main.ts)
157+
if (audioRequested && process.platform === "linux" && global.mainWindow) {
158+
const { showAudioPickerAndStart } = await import("./audio-picker.js");
159+
await showAudioPickerAndStart(global.mainWindow);
160+
}
161+
162+
// Include loopback audio for Windows
163+
if (audioRequested && process.platform === "win32") {
156164
await callback?.({ video: args[0], audio: "loopback" });
157165
} else {
158166
await callback?.({ video: args[0] });

src/preload.cts

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,10 @@ if (process.platform === "linux") {
118118
navigator.mediaDevices.getDisplayMedia = async function (
119119
options?: DisplayMediaStreamOptions,
120120
): Promise<MediaStream> {
121-
console.log("venmic: getDisplayMedia called with options:", options);
121+
console.debug("venmic: getDisplayMedia called with options:", options);
122122

123123
const stream = await originalGetDisplayMedia(options);
124-
console.log(
124+
console.debug(
125125
"venmic: original getDisplayMedia returned stream with tracks:",
126126
stream.getTracks().map((t) => `${t.kind}:${t.label}`),
127127
);
@@ -130,15 +130,15 @@ if (process.platform === "linux") {
130130
try {
131131
const devices = await navigator.mediaDevices.enumerateDevices();
132132
const audioInputs = devices.filter((d) => d.kind === "audioinput");
133-
console.log(
133+
console.debug(
134134
"venmic: available audio input devices:",
135135
audioInputs.map((d) => `${d.label} (${d.deviceId.slice(0, 8)}...)`),
136136
);
137137

138138
const venmicDevice = devices.find((d) => d.label === "vencord-screen-share");
139139

140140
if (venmicDevice) {
141-
console.log("venmic: found vencord-screen-share device:", venmicDevice.deviceId);
141+
console.debug("venmic: found vencord-screen-share device:", venmicDevice.deviceId);
142142

143143
// Capture audio from the venmic virtual microphone
144144
const audioStream = await navigator.mediaDevices.getUserMedia({
@@ -153,40 +153,44 @@ if (process.platform === "linux") {
153153
},
154154
});
155155

156-
console.log("venmic: captured audio stream from virtual mic");
156+
console.debug("venmic: captured audio stream from virtual mic");
157157

158158
// Remove any existing audio tracks and add the venmic audio
159-
stream.getAudioTracks().forEach((track) => {
160-
console.log("venmic: removing existing audio track:", track.label);
161-
stream.removeTrack(track);
162-
});
163159
const audioTrack = audioStream.getAudioTracks()[0];
164-
stream.addTrack(audioTrack);
165-
166-
// Clean up venmic when the audio track ends
167-
audioTrack.addEventListener("ended", () => {
168-
console.log("venmic: audio track ended, stopping venmic");
169-
void ipcRenderer.invoke("stopVenmic");
170-
});
160+
if (audioTrack) {
161+
stream.getAudioTracks().forEach((track) => {
162+
console.debug("venmic: removing existing audio track:", track.label);
163+
stream.removeTrack(track);
164+
});
165+
stream.addTrack(audioTrack);
171166

172-
// Also clean up when the video track ends (screen share stopped)
173-
const videoTrack = stream.getVideoTracks()[0];
174-
if (videoTrack) {
175-
videoTrack.addEventListener("ended", () => {
176-
console.log("venmic: video track ended, stopping venmic");
167+
// Clean up venmic when the audio track ends
168+
audioTrack.addEventListener("ended", () => {
169+
console.debug("venmic: audio track ended, stopping venmic");
177170
void ipcRenderer.invoke("stopVenmic");
178171
});
179-
}
180172

181-
console.log("venmic: audio track added to screen share stream successfully");
173+
// Also clean up when the video track ends (screen share stopped)
174+
const videoTrack = stream.getVideoTracks()[0];
175+
if (videoTrack) {
176+
videoTrack.addEventListener("ended", () => {
177+
console.debug("venmic: video track ended, stopping venmic");
178+
void ipcRenderer.invoke("stopVenmic");
179+
});
180+
}
181+
182+
console.log("venmic: audio track added to screen share stream");
183+
} else {
184+
console.warn("venmic: no audio track returned from virtual microphone");
185+
}
182186
} else {
183-
console.log("venmic: vencord-screen-share device NOT found");
187+
console.debug("venmic: vencord-screen-share device not found, audio not captured");
184188
}
185189
} catch (err) {
186190
console.error("venmic: failed to capture audio from virtual microphone:", err);
187191
}
188192

189-
console.log(
193+
console.debug(
190194
"venmic: returning stream with tracks:",
191195
stream.getTracks().map((t) => `${t.kind}:${t.label}`),
192196
);

src/venmic-inject.ts

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,23 @@ const VENMIC_PATCH_SCRIPT = `
2525
const originalGetDisplayMedia = navigator.mediaDevices.getDisplayMedia.bind(navigator.mediaDevices);
2626
2727
navigator.mediaDevices.getDisplayMedia = async function(options) {
28-
console.log('[venmic-inject] getDisplayMedia called in frame:', window.location.href);
28+
console.debug('[venmic-inject] getDisplayMedia called in frame:', window.location.href);
2929
3030
const stream = await originalGetDisplayMedia(options);
31-
console.log('[venmic-inject] original getDisplayMedia returned, tracks:',
31+
console.debug('[venmic-inject] original getDisplayMedia returned, tracks:',
3232
stream.getTracks().map(t => t.kind + ':' + t.label).join(', '));
3333
3434
// Try to find and capture from venmic virtual microphone
3535
try {
3636
const devices = await navigator.mediaDevices.enumerateDevices();
3737
const audioInputs = devices.filter(d => d.kind === 'audioinput');
38-
console.log('[venmic-inject] available audio inputs:',
38+
console.debug('[venmic-inject] available audio inputs:',
3939
audioInputs.map(d => d.label || d.deviceId.slice(0, 8)).join(', '));
4040
4141
const venmicDevice = devices.find(d => d.label === 'vencord-screen-share');
4242
4343
if (venmicDevice) {
44-
console.log('[venmic-inject] found vencord-screen-share device!');
44+
console.debug('[venmic-inject] found vencord-screen-share device');
4545
4646
const audioStream = await navigator.mediaDevices.getUserMedia({
4747
audio: {
@@ -55,32 +55,33 @@ const VENMIC_PATCH_SCRIPT = `
5555
}
5656
});
5757
58-
console.log('[venmic-inject] captured audio from venmic virtual mic');
59-
60-
// Remove any existing audio tracks
61-
stream.getAudioTracks().forEach(track => {
62-
console.log('[venmic-inject] removing existing audio track:', track.label);
63-
stream.removeTrack(track);
64-
});
65-
66-
// Add the venmic audio track
6758
const audioTrack = audioStream.getAudioTracks()[0];
68-
stream.addTrack(audioTrack);
69-
70-
console.log('[venmic-inject] venmic audio track added to stream successfully');
59+
if (audioTrack) {
60+
// Remove any existing audio tracks
61+
stream.getAudioTracks().forEach(track => {
62+
console.debug('[venmic-inject] removing existing audio track:', track.label);
63+
stream.removeTrack(track);
64+
});
65+
66+
// Add the venmic audio track
67+
stream.addTrack(audioTrack);
68+
console.log('[venmic-inject] venmic audio track added to stream');
69+
} else {
70+
console.warn('[venmic-inject] no audio track returned from virtual microphone');
71+
}
7172
} else {
72-
console.log('[venmic-inject] vencord-screen-share device not found');
73+
console.debug('[venmic-inject] vencord-screen-share device not found');
7374
}
7475
} catch (err) {
7576
console.error('[venmic-inject] failed to capture venmic audio:', err);
7677
}
7778
78-
console.log('[venmic-inject] returning stream with tracks:',
79+
console.debug('[venmic-inject] returning stream with tracks:',
7980
stream.getTracks().map(t => t.kind + ':' + t.label).join(', '));
8081
return stream;
8182
};
8283
83-
console.log('[venmic-inject] getDisplayMedia patch installed in frame:', window.location.href);
84+
console.debug('[venmic-inject] getDisplayMedia patch installed in frame:', window.location.href);
8485
})();
8586
`;
8687

@@ -96,7 +97,7 @@ export function setupVenmicInjection(webContents: WebContents): void {
9697
if (isMainFrame) {
9798
// Main frame - inject directly
9899
await webContents.executeJavaScript(VENMIC_PATCH_SCRIPT, true);
99-
console.log("venmic: patch injected into main frame");
100+
console.debug("venmic: patch injected into main frame");
100101
} else {
101102
// Subframe - need to find and inject into all frames
102103
// Get all frames including subframes
@@ -128,7 +129,7 @@ async function injectIntoAllFrames(frame: Electron.WebFrameMain): Promise<void>
128129

129130
// Inject into this frame
130131
await frame.executeJavaScript(VENMIC_PATCH_SCRIPT, true);
131-
console.log("venmic: patch injected into frame:", frame.url);
132+
console.debug("venmic: patch injected into frame:", frame.url);
132133
} catch (err) {
133134
// Frame might have been destroyed or navigated
134135
console.debug("venmic: failed to inject into frame:", err);

src/venmic.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2026 New Vector Ltd.
2+
Copyright 2025 New Vector Ltd.
33
44
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
55
Please see LICENSE files in the repository root for full details.
@@ -12,6 +12,10 @@ import { fileURLToPath } from "node:url";
1212

1313
import type { LinkData, Node, PatchBay as PatchBayType } from "@vencord/venmic";
1414

15+
import type { VenmicListResult } from "./@types/audio-sharing.js";
16+
17+
export type { VenmicListResult };
18+
1519
const __dirname = dirname(fileURLToPath(import.meta.url));
1620
const nativeRequire = createRequire(import.meta.url);
1721

@@ -24,10 +28,6 @@ let initialized = false;
2428
let hasPipewirePulse = false;
2529
let isGlibcOutdated = false;
2630

27-
export type VenmicListResult =
28-
| { ok: true; targets: Node[]; hasPipewirePulse: boolean }
29-
| { ok: false; isGlibcOutdated: boolean };
30-
3131
function importVenmic(): void {
3232
if (imported) {
3333
return;

0 commit comments

Comments
 (0)