fix(linux/wayland): address PR review feedback

- electron/main.ts: treat missing sourceId on linux as portal sentinel
  so the request handler never calls getSources() (which itself opens
  an extra portal dialog) on fresh sessions.
- useScreenRecorder.ts: persist the synthesized portal sentinel via
  selectSource() so main has the source set before getDisplayMedia.
  Extract acquireLinuxPortalStream() helper to dedupe the three
  duplicated getDisplayMedia constraint blocks.
- LaunchWindow.tsx: hide the screen-source selector button (and its
  separator) on Linux so users cannot trigger an extra portal dialog
  via the dropdown.
This commit is contained in:
Uri
2026-04-18 16:38:52 +03:00
parent 571bbb9434
commit b200deddca
3 changed files with 52 additions and 53 deletions
+6 -1
View File
@@ -900,8 +900,13 @@ app.whenReady().then(async () => {
// is set we skip getSources entirely and hand back a synthetic
// source id; Chromium then opens the portal once to actually
// resolve the capture.
// Default to the sentinel on Linux when no source has been
// 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 isLinuxPortalSentinel =
process.platform === "linux" && sourceId === "screen:linux-portal";
process.platform === "linux" &&
(sourceId === "screen:linux-portal" || !sourceId);
if (isLinuxPortalSentinel) {
callback({ video: { id: "screen:0:0", name: "Entire screen" } });
return;
+20 -16
View File
@@ -1093,23 +1093,27 @@ export function LaunchWindow() {
const idleControls = (
<>
<button
type="button"
className={`${styles.screenSel} ${styles.electronNoDrag}`}
onClick={() => toggleDropdown("sources")}
title={selectedSource}
>
<Monitor size={16} />
<ContentClamp className={styles.sourceLabel} truncateLength={36}>
{selectedSource}
</ContentClamp>
<ChevronUp
size={10}
className={`text-[#6b6b78] ml-0.5 transition-transform duration-200 ${activeDropdown === "sources" ? "" : "rotate-180"}`}
/>
</button>
{platform !== "linux" && (
<>
<button
type="button"
className={`${styles.screenSel} ${styles.electronNoDrag}`}
onClick={() => toggleDropdown("sources")}
title={selectedSource}
>
<Monitor size={16} />
<ContentClamp className={styles.sourceLabel} truncateLength={36}>
{selectedSource}
</ContentClamp>
<ChevronUp
size={10}
className={`text-[#6b6b78] ml-0.5 transition-transform duration-200 ${activeDropdown === "sources" ? "" : "rotate-180"}`}
/>
</button>
<Separator />
<Separator />
</>
)}
<IconButton
onClick={toggleMicrophone}
+26 -36
View File
@@ -846,6 +846,16 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
alert("Please select a source to record");
return;
}
// Persist the synthetic Linux portal sentinel to main so that the
// setDisplayMediaRequestHandler can short-circuit getSources() and
// avoid triggering an extra portal dialog.
if (!existingSource && selectedSource.id === "screen:linux-portal") {
try {
await window.electronAPI.selectSource(selectedSource);
} catch (err) {
console.warn("Failed to persist Linux portal sentinel source:", err);
}
}
const permissionsReady = await preparePermissions();
if (!permissionsReady) {
@@ -1031,22 +1041,24 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
if (wantsAudioCapture) {
let screenMediaStream: MediaStream;
const useLinuxPortal = selectedSource.id === "screen:linux-portal";
const acquireLinuxPortalStream = (withAudio: boolean) =>
mediaDevices.getDisplayMedia({
audio: withAudio,
video: {
displaySurface: "monitor",
width: { ideal: TARGET_WIDTH, max: TARGET_WIDTH },
height: { ideal: TARGET_HEIGHT, max: TARGET_HEIGHT },
frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE },
cursor: "never",
},
selfBrowserSurface: "exclude",
surfaceSwitching: "exclude",
});
if (systemAudioEnabled) {
try {
screenMediaStream = useLinuxPortal
? await mediaDevices.getDisplayMedia({
audio: true,
video: {
displaySurface: "monitor",
width: { ideal: TARGET_WIDTH, max: TARGET_WIDTH },
height: { ideal: TARGET_HEIGHT, max: TARGET_HEIGHT },
frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE },
cursor: "never",
},
selfBrowserSurface: "exclude",
surfaceSwitching: "exclude",
})
? await acquireLinuxPortalStream(true)
: await mediaDevices.getUserMedia({
audio: {
mandatory: {
@@ -1065,18 +1077,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
"System audio is not available for this source. Recording will continue without system audio.",
);
screenMediaStream = useLinuxPortal
? await mediaDevices.getDisplayMedia({
audio: false,
video: {
displaySurface: "monitor",
width: { ideal: TARGET_WIDTH, max: TARGET_WIDTH },
height: { ideal: TARGET_HEIGHT, max: TARGET_HEIGHT },
frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE },
cursor: "never",
},
selfBrowserSurface: "exclude",
surfaceSwitching: "exclude",
})
? await acquireLinuxPortalStream(false)
: await mediaDevices.getUserMedia({
audio: false,
video: browserScreenVideoConstraints,
@@ -1084,18 +1085,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}
} else {
screenMediaStream = useLinuxPortal
? await mediaDevices.getDisplayMedia({
audio: false,
video: {
displaySurface: "monitor",
width: { ideal: TARGET_WIDTH, max: TARGET_WIDTH },
height: { ideal: TARGET_HEIGHT, max: TARGET_HEIGHT },
frameRate: { ideal: TARGET_FRAME_RATE, max: TARGET_FRAME_RATE },
cursor: "never",
},
selfBrowserSurface: "exclude",
surfaceSwitching: "exclude",
})
? await acquireLinuxPortalStream(false)
: await mediaDevices.getUserMedia({
audio: false,
video: browserScreenVideoConstraints,