From ce00de7a4ea9e15ad93f17d34d466b97e74c8ef1 Mon Sep 17 00:00:00 2001 From: Max Medina Date: Fri, 17 Jul 2026 11:32:14 +0200 Subject: [PATCH] fix(linux): use a real screen source on X11 instead of the portal sentinel The Linux HUD does not render a source picker, so `getSelectedSource()` is always null when recording starts. startRecording then substituted the `screen:linux-portal` sentinel for every Linux session, forcing capture through getDisplayMedia and the xdg-desktop-portal ScreenCast dialog. On X11 that dialog never opens, so capture failed with: Failed to start recording: Could not start video source Main already resolves screens per session type via getScreenSourceIdForDisplay(): the portal sentinel on Wayland, a real desktop-capturer id on X11. The renderer just never asked for it. Prefer a real screen id when one is available and keep the sentinel as the fallback, so Wayland behaviour is unchanged and X11 uses direct capture. Fallback ids (screen:fallback:*) are skipped so the existing "Selected display is not available" guard still applies. Verified on Ubuntu 24.04 (GNOME, X11): resolution now yields a real source id and captures at 3840x2160@60. Refs #137, #364, #441 Co-Authored-By: Claude Opus 4.8 --- src/hooks/useScreenRecorder.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index c5cd70056..7554fb103 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -121,6 +121,34 @@ const LINUX_PORTAL_SOURCE: ProcessedDesktopSource = { sourceType: "screen", }; +// The Linux HUD has no source picker, so recording starts with no selected +// source. Main already resolves screens per session type: the portal sentinel +// on Wayland, a real desktop-capturer id on X11. Prefer a real id when one +// exists, because the X11 portal dialog never opens and capture then fails +// with "Could not start video source". +async function resolveDefaultLinuxSource(): Promise { + try { + const sources = await window.electronAPI.getSources({ + types: ["screen"], + thumbnailSize: { width: 1, height: 1 }, + fetchWindowIcons: false, + }); + const captureableScreen = sources.find( + (candidate) => + candidate.id?.startsWith("screen:") && + candidate.id !== LINUX_PORTAL_SOURCE.id && + !candidate.id.startsWith("screen:fallback:"), + ); + if (captureableScreen) { + return captureableScreen; + } + } catch (error) { + console.warn("Failed to resolve a default Linux screen source:", error); + } + + return LINUX_PORTAL_SOURCE; +} + type DesktopCaptureMediaDevices = { getUserMedia: (constraints: unknown) => Promise; getDisplayMedia: (constraints: unknown) => Promise; @@ -1379,7 +1407,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn { hideEditorOverlayCursorByDefault.current = false; const existingSource = await window.electronAPI.getSelectedSource(); const selectedSource = - existingSource ?? (platform === "linux" ? LINUX_PORTAL_SOURCE : null); + existingSource ?? (platform === "linux" ? await resolveDefaultLinuxSource() : null); if (!selectedSource) { alert("Please select a source to record"); return;