mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 23:35:43 +00:00
fix(linux/wayland): collapse 3-step capture flow into a single portal dialog
Previously on Linux/Wayland, starting a fullscreen recording required three separate picker interactions: an in-app source dropdown plus two xdg-desktop-portal dialogs. The duplicate portal dialogs were caused by: 1. resolveBrowserCaptureSource() calling desktopCapturer.getSources() in the renderer, which itself triggers the portal on Wayland. 2. setDisplayMediaRequestHandler() in main also calling getSources(), which triggers another portal. 3. Returning a pre-enumerated source id to Chromium, which on Wayland is stale and forces Chromium to re-prompt via the portal during MediaStream creation. Additionally, the editor window failed to appear after recording on some Wayland sessions because 'ready-to-show' did not fire reliably. Changes: - LaunchWindow: skip the in-app source dropdown on Linux and start recording directly; the OS portal becomes the source picker. - useScreenRecorder: introduce a 'screen:linux-portal' sentinel source. When set, route capture through navigator.mediaDevices.getDisplayMedia() so the portal handles selection in a single dialog. Skip resolveBrowserCaptureSource for the sentinel to avoid an extra getSources() call. - electron/main: in setDisplayMediaRequestHandler, when the sentinel is set, skip desktopCapturer.getSources() entirely and return a synthetic source so Chromium opens the portal exactly once for the actual capture. - electron/windows: in createEditorWindow, also call win.show() from did-finish-load as a fallback for Linux/Wayland where ready-to-show may not fire.
This commit is contained in:
+15
-1
@@ -891,8 +891,22 @@ app.whenReady().then(async () => {
|
||||
// ignored by the native capture pipeline.
|
||||
session.defaultSession.setDisplayMediaRequestHandler(async (_request, callback) => {
|
||||
try {
|
||||
const sources = await desktopCapturer.getSources({ types: ["screen", "window"] });
|
||||
const sourceId = getSelectedSourceId();
|
||||
// On Linux/Wayland, calling desktopCapturer.getSources() itself
|
||||
// invokes the xdg-desktop-portal picker. If we then return one of
|
||||
// those sources, Chromium triggers a SECOND portal because the
|
||||
// pre-enumerated source IDs are stale on Wayland. To collapse this
|
||||
// into a single portal invocation, when the Linux portal sentinel
|
||||
// is set we skip getSources entirely and hand back a synthetic
|
||||
// source id; Chromium then opens the portal once to actually
|
||||
// resolve the capture.
|
||||
const isLinuxPortalSentinel =
|
||||
process.platform === "linux" && sourceId === "screen:linux-portal";
|
||||
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[0];
|
||||
|
||||
@@ -788,6 +788,11 @@ export function createEditorWindow(): BrowserWindow {
|
||||
win.webContents.on("did-finish-load", () => {
|
||||
console.log("[editor-window] did-finish-load", win.webContents.getURL());
|
||||
win?.webContents.send("main-process-message", new Date().toLocaleString());
|
||||
// Fallback for Linux/Wayland where `ready-to-show` may not fire reliably.
|
||||
if (!win.isDestroyed() && !win.isVisible()) {
|
||||
console.log("[editor-window] forcing show after did-finish-load");
|
||||
win.show();
|
||||
}
|
||||
});
|
||||
|
||||
win.webContents.on("did-fail-load", (_event, errorCode, errorDescription, validatedURL) => {
|
||||
|
||||
@@ -1144,7 +1144,11 @@ export function LaunchWindow() {
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.recBtn} ${styles.electronNoDrag}`}
|
||||
onClick={hasSelectedSource ? toggleRecording : () => toggleDropdown("sources")}
|
||||
onClick={
|
||||
hasSelectedSource || platform === "linux"
|
||||
? toggleRecording
|
||||
: () => toggleDropdown("sources")
|
||||
}
|
||||
disabled={countdownActive}
|
||||
title={t("recording.record")}
|
||||
>
|
||||
|
||||
@@ -334,6 +334,14 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
return source;
|
||||
}
|
||||
|
||||
// Linux/Wayland portal sentinel: do NOT call getSources here, because
|
||||
// on Wayland that triggers an additional xdg-desktop-portal dialog.
|
||||
// The sentinel is handled later by routing through getDisplayMedia,
|
||||
// which lets the portal pick the source in a single dialog.
|
||||
if (source.id === "screen:linux-portal") {
|
||||
return source;
|
||||
}
|
||||
|
||||
try {
|
||||
const liveSources = await window.electronAPI.getSources({
|
||||
types: ["screen"],
|
||||
@@ -827,7 +835,13 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
setStarting(true);
|
||||
|
||||
try {
|
||||
const selectedSource = await window.electronAPI.getSelectedSource();
|
||||
const platform = await window.electronAPI.getPlatform();
|
||||
const existingSource = await window.electronAPI.getSelectedSource();
|
||||
const selectedSource =
|
||||
existingSource ??
|
||||
(platform === "linux"
|
||||
? { id: "screen:linux-portal", name: "Linux Portal" }
|
||||
: null);
|
||||
if (!selectedSource) {
|
||||
alert("Please select a source to record");
|
||||
return;
|
||||
@@ -841,8 +855,6 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
recordingSessionTimestamp.current = Date.now();
|
||||
resetRecordingClock(recordingSessionTimestamp.current);
|
||||
await prepareWebcamRecorder();
|
||||
|
||||
const platform = await window.electronAPI.getPlatform();
|
||||
const useNativeMacScreenCapture =
|
||||
platform === "darwin" &&
|
||||
(selectedSource.id?.startsWith("screen:") ||
|
||||
@@ -1018,18 +1030,32 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
|
||||
if (wantsAudioCapture) {
|
||||
let screenMediaStream: MediaStream;
|
||||
const useLinuxPortal = selectedSource.id === "screen:linux-portal";
|
||||
|
||||
if (systemAudioEnabled) {
|
||||
try {
|
||||
screenMediaStream = await mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
mandatory: {
|
||||
chromeMediaSource: CHROME_MEDIA_SOURCE,
|
||||
chromeMediaSourceId: browserCaptureSource.id,
|
||||
},
|
||||
},
|
||||
video: browserScreenVideoConstraints,
|
||||
});
|
||||
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 mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
mandatory: {
|
||||
chromeMediaSource: CHROME_MEDIA_SOURCE,
|
||||
chromeMediaSourceId: browserCaptureSource.id,
|
||||
},
|
||||
},
|
||||
video: browserScreenVideoConstraints,
|
||||
});
|
||||
} catch (audioError) {
|
||||
console.warn(
|
||||
"System audio capture failed, falling back to video-only:",
|
||||
@@ -1038,16 +1064,42 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
alert(
|
||||
"System audio is not available for this source. Recording will continue without system audio.",
|
||||
);
|
||||
screenMediaStream = await mediaDevices.getUserMedia({
|
||||
audio: false,
|
||||
video: browserScreenVideoConstraints,
|
||||
});
|
||||
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 mediaDevices.getUserMedia({
|
||||
audio: false,
|
||||
video: browserScreenVideoConstraints,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
screenMediaStream = await mediaDevices.getUserMedia({
|
||||
audio: false,
|
||||
video: browserScreenVideoConstraints,
|
||||
});
|
||||
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 mediaDevices.getUserMedia({
|
||||
audio: false,
|
||||
video: browserScreenVideoConstraints,
|
||||
});
|
||||
}
|
||||
|
||||
screenStream.current = screenMediaStream;
|
||||
|
||||
Reference in New Issue
Block a user