Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions electron-builder.json5
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@
"!electron/native/bin/**/whisper-quantize*",
"!electron/native/bin/**/whisper-server*",
"!electron/native/bin/**/whisper-vad-speech-segments*",
"!node_modules/ffprobe-static/bin/darwin/**",
"!node_modules/ffprobe-static/bin/linux/**",
"!node_modules/ffprobe-static/bin/win32/ia32/**",
"!node_modules/ffprobe-static/bin/**/ia32/**",
"!electron/native/**/build/**",
"!*.png",
"!preview*.png",
Expand Down
40 changes: 5 additions & 35 deletions electron/gpuSwitches.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,56 +12,26 @@ describe("shouldForceLinuxEgl", () => {
).toBe(false);
});

it("does not force EGL when Wayland is explicitly requested via Ozone", () => {
expect(
shouldForceLinuxEgl({
OZONE_PLATFORM: "wayland",
XDG_SESSION_TYPE: "x11",
}),
).toBe(false);
});

it("falls back to Electron's ozone hint when OZONE_PLATFORM is invalid", () => {
expect(
shouldForceLinuxEgl({
OZONE_PLATFORM: "auto",
ELECTRON_OZONE_PLATFORM_HINT: "wayland",
XDG_SESSION_TYPE: "x11",
}),
).toBe(false);
});

it("forces EGL in an X11 session", () => {
expect(shouldForceLinuxEgl({ XDG_SESSION_TYPE: "x11" })).toBe(true);
});

it("forces EGL when x11 is explicitly requested via Electron's ozone hint", () => {
expect(
shouldForceLinuxEgl({
ELECTRON_OZONE_PLATFORM_HINT: "x11",
WAYLAND_DISPLAY: "wayland-0",
}),
).toBe(true);
it("does not force EGL on X11 session", () => {
expect(shouldForceLinuxEgl({ XDG_SESSION_TYPE: "x11" })).toBe(false);
});
});

describe("getGpuSwitches", () => {
it("returns the Linux VAAPI workaround without forcing EGL on Wayland", () => {
it("returns the Linux VAAPI workaround on Linux Wayland", () => {
expect(
getGpuSwitches("linux", {
XDG_SESSION_TYPE: "wayland",
WAYLAND_DISPLAY: "wayland-0",
}),
).toEqual({
useGl: undefined,
disableFeatures: ["VaapiVideoDecoder", "VaapiVideoEncoder"],
});
});

it("returns the X11 EGL workaround on Linux X11", () => {
it("returns VAAPI + WebRTCPipeWireCapturer disable on Linux X11", () => {
expect(getGpuSwitches("linux", { XDG_SESSION_TYPE: "x11" })).toEqual({
useGl: "egl",
disableFeatures: ["VaapiVideoDecoder", "VaapiVideoEncoder"],
disableFeatures: ["VaapiVideoDecoder", "VaapiVideoEncoder", "WebRTCPipeWireCapturer"],
});
});
});
60 changes: 24 additions & 36 deletions electron/gpuSwitches.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,29 @@
import { isLikelyLinuxWaylandSession } from "./ipc/register/sourceMapping";

export interface GpuSwitches {
useAngle?: string;
useGl?: string;
disableFeatures?: string[];
}

function normalizeLinuxWindowSystem(value: string | undefined): "wayland" | "x11" | null {
const normalized = value?.trim().toLowerCase();
if (normalized === "wayland" || normalized === "x11") {
return normalized;
}

return null;
}

function getForcedLinuxWindowSystem(env: NodeJS.ProcessEnv): "wayland" | "x11" | null {
return (
normalizeLinuxWindowSystem(env.OZONE_PLATFORM) ??
normalizeLinuxWindowSystem(env.ELECTRON_OZONE_PLATFORM_HINT)
);
}

export function shouldForceLinuxEgl(env: NodeJS.ProcessEnv): boolean {
const forcedWindowSystem = getForcedLinuxWindowSystem(env);
if (forcedWindowSystem === "wayland") {
return false;
}
if (forcedWindowSystem === "x11") {
return true;
}

const sessionType = env.XDG_SESSION_TYPE?.toLowerCase();
if (sessionType === "wayland") {
return false;
}
if (sessionType === "x11") {
return true;
}

return !env.WAYLAND_DISPLAY;
/**
* Determines whether Linux EGL rendering switch should be forced.
* Returns false on all environments to avoid GPU driver crashes.
*
* @param _env - Process environment variables.
* @returns Boolean flag indicating if EGL should be forced.
*/
export function shouldForceLinuxEgl(_env: NodeJS.ProcessEnv): boolean {
return false;
}

/**
* Returns platform-specific GPU command-line switches and disabled feature flags.
*
* @param platform - Target operating system platform.
* @param env - Process environment variables.
* @returns Object containing GPU switches and disabled features.
*/
export function getGpuSwitches(
platform: NodeJS.Platform,
env: NodeJS.ProcessEnv = process.env,
Expand All @@ -56,9 +40,13 @@ export function getGpuSwitches(
}

if (platform === "linux") {
const isWayland = isLikelyLinuxWaylandSession(env);
const disableFeatures = ["VaapiVideoDecoder", "VaapiVideoEncoder"];
if (!isWayland) {
disableFeatures.push("WebRTCPipeWireCapturer");
}
return {
useGl: shouldForceLinuxEgl(env) ? "egl" : undefined,
disableFeatures: ["VaapiVideoDecoder", "VaapiVideoEncoder"],
disableFeatures,
};
}

Expand Down
44 changes: 34 additions & 10 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import {
BrowserWindow,
desktopCapturer,
dialog,
webContents as electronWebContents,
ipcMain,
Menu,
nativeImage,
session,
shell,
systemPreferences,
Tray,
webContents as electronWebContents,
} from "electron";
import { RECORDINGS_DIR } from "./appPaths";
import { showCursor } from "./cursorHider";
Expand All @@ -25,6 +25,7 @@ import {
killWindowsCaptureProcess,
registerIpcHandlers,
} from "./ipc/handlers";
import { isLikelyLinuxWaylandSession } from "./ipc/register/sourceMapping";
import { ensureMediaServer } from "./mediaServer";
import { hardenWebContentsNavigation, shouldHardenWebContentsType } from "./navigationPolicy";
import { shouldGrantDisplayCapture, shouldGrantMediaPermission } from "./permissionPolicy";
Expand Down Expand Up @@ -1058,21 +1059,33 @@ app.whenReady().then(async () => {
try {
const frame = request.frame;
const isLiveFrame = Boolean(frame && !frame.isDestroyed());
const requestingWebContents =
isLiveFrame && frame ? electronWebContents.fromFrame(frame) : undefined;
const hudWindow = getHudOverlayWindow();
const requestingWebContents = frame
? isLiveFrame
? (electronWebContents.fromFrame(frame) ?? undefined)
: undefined
: hudWindow && !hudWindow.isDestroyed()
? hudWindow.webContents
: undefined;
const isHudMainFrame = Boolean(
isLiveFrame &&
requestingWebContents &&
requestingWebContents &&
isHudWebContents(requestingWebContents) &&
frame === requestingWebContents.mainFrame,
(!frame || frame === requestingWebContents.mainFrame || frame.parent === null),
);

if (
!shouldGrantDisplayCapture(
{
isTrustedCaptureWindow: isHudMainFrame,
isMainFrame: Boolean(isLiveFrame && frame?.parent === null),
currentDocumentUrl: isLiveFrame ? (frame?.url ?? "") : "",
isMainFrame: Boolean(isLiveFrame && frame ? frame.parent === null : true),
currentDocumentUrl:
isLiveFrame && frame?.url
? frame.url
: requestingWebContents && !requestingWebContents.isDestroyed()
? requestingWebContents.getURL()
: hudWindow && !hudWindow.isDestroyed()
? hudWindow.webContents.getURL()
: "",
securityOrigin: request.securityOrigin,
videoRequested: request.videoRequested,
},
Expand Down Expand Up @@ -1100,15 +1113,26 @@ app.whenReady().then(async () => {
// pre-selected (e.g. fresh session where the renderer skipped the
// source picker entirely). This avoids calling getSources() which
// would itself trigger an extra portal dialog.
const isWayland = isLikelyLinuxWaylandSession(process.env);
const isLinuxPortalSentinel =
process.platform === "linux" && (sourceId === "screen:linux-portal" || !sourceId);
process.platform === "linux" && isWayland && (sourceId === "screen:linux-portal" || !sourceId);
if (isLinuxPortalSentinel) {
callback({ video: { id: "screen:0:0", name: "Entire screen" } });
return;
}
const sources = await desktopCapturer.getSources({ types: ["screen", "window"] });
const source = sourceId
? (sources.find((s) => s.id === sourceId) ?? sources[0])
? (sources.find((s) => s.id === sourceId) ??
(sourceId.startsWith("screen:")
? sources.find(
(s) =>
s.display_id &&
(sourceId === s.display_id ||
sourceId === `screen:${s.display_id}:0` ||
sourceId.endsWith(`:${s.display_id}`)),
)
: undefined) ??
sources[0])
: sources[0];
if (source) {
callback({
Expand Down
13 changes: 13 additions & 0 deletions electron/mediaTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,28 @@ export const MEDIA_CONTENT_TYPES: Record<string, string> = {
".wav": "audio/wav",
".mp3": "audio/mpeg",
".ogg": "audio/ogg",
".m4a": "audio/mp4",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
};

/**
* Resolves the MIME content type for a given local media file path.
*
* @param filePath - Local path to the media file.
* @returns Standard MIME type string or application/octet-stream fallback.
*/
export function getMediaContentType(filePath: string): string {
return MEDIA_CONTENT_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream";
}

/**
* Checks whether the file extension of a given path is a supported media format.
*
* @param filePath - Local path to the media file.
* @returns Boolean indicating whether the format is supported.
*/
export function isSupportedLocalMediaPath(filePath: string): boolean {
return path.extname(filePath).toLowerCase() in MEDIA_CONTENT_TYPES;
}
9 changes: 6 additions & 3 deletions electron/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,11 @@ function setHudOverlayFallbackExpanded(expanded: boolean) {
}
}

/**
* Updates HUD overlay mouse passthrough and expands/contracts fallback bounds when needed.
*
* @param ignore - Whether mouse events should pass through the HUD window.
*/
function setHudOverlayMousePassthrough(ignore: boolean) {
hudOverlayIgnoringMouse =
hudOverlaySourceSelectionActive && !hudOverlayRecordingActive ? true : ignore;
Expand All @@ -312,9 +317,7 @@ function setHudOverlayMousePassthrough(ignore: boolean) {
}

if (!isHudOverlayMousePassthroughSupported()) {
if (process.platform !== "linux") {
setHudOverlayFallbackExpanded(!ignore);
}
setHudOverlayFallbackExpanded(!ignore);
hudOverlayWindow.setIgnoreMouseEvents(false);
return;
}
Expand Down
Loading