mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 23:05:49 +00:00
Merge remote-tracking branch 'origin/main' into fix/linux-hud-menu-bounds
# Conflicts: # electron/hudOverlayBounds.test.ts # electron/hudOverlayBounds.ts # electron/windows.ts
This commit is contained in:
Vendored
+1
@@ -208,6 +208,7 @@ interface Window {
|
||||
hudOverlayHide: () => void;
|
||||
hudOverlayClose: () => void;
|
||||
hudOverlayRendererReady: () => void;
|
||||
hudOverlaySetWebcamPreviewVisible: (visible: boolean) => void;
|
||||
getHudOverlayCaptureProtection: () => Promise<{ success: boolean; enabled: boolean }>;
|
||||
getHudOverlayMousePassthroughSupported: () => Promise<{
|
||||
success: boolean;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getHudOverlayWindowBounds,
|
||||
resizeHudOverlayFallbackBounds,
|
||||
shouldExpandHudOverlayFallback,
|
||||
shouldResizeHudOverlayFallback,
|
||||
} from "./hudOverlayBounds";
|
||||
|
||||
@@ -158,3 +159,35 @@ describe("shouldResizeHudOverlayFallback", () => {
|
||||
expect(shouldResizeHudOverlayFallback(false, true)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldExpandHudOverlayFallback", () => {
|
||||
it("expands while recording only when the floating webcam preview is visible", () => {
|
||||
expect(
|
||||
shouldExpandHudOverlayFallback({
|
||||
fallbackExpanded: false,
|
||||
recordingActive: true,
|
||||
webcamPreviewVisible: true,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the compact recording fallback when there is no webcam preview", () => {
|
||||
expect(
|
||||
shouldExpandHudOverlayFallback({
|
||||
fallbackExpanded: false,
|
||||
recordingActive: true,
|
||||
webcamPreviewVisible: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves manual fallback expansion outside recording", () => {
|
||||
expect(
|
||||
shouldExpandHudOverlayFallback({
|
||||
fallbackExpanded: true,
|
||||
recordingActive: false,
|
||||
webcamPreviewVisible: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,6 +45,18 @@ export function shouldResizeHudOverlayFallback(
|
||||
return !mousePassthroughSupported && !recordingActive;
|
||||
}
|
||||
|
||||
export function shouldExpandHudOverlayFallback({
|
||||
fallbackExpanded,
|
||||
recordingActive,
|
||||
webcamPreviewVisible,
|
||||
}: {
|
||||
fallbackExpanded: boolean;
|
||||
recordingActive: boolean;
|
||||
webcamPreviewVisible: boolean;
|
||||
}): boolean {
|
||||
return fallbackExpanded || (recordingActive && webcamPreviewVisible);
|
||||
}
|
||||
|
||||
export function resizeHudOverlayFallbackBounds(
|
||||
workArea: HudOverlayWorkArea,
|
||||
currentBounds: HudOverlayWorkArea,
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
getScreenSourceIdForDisplay,
|
||||
LINUX_PORTAL_SCREEN_SOURCE_ID,
|
||||
shouldUseSyntheticLinuxPortalSource,
|
||||
} from "./sourceMapping";
|
||||
|
||||
describe("getScreenSourceIdForDisplay", () => {
|
||||
it("keeps the live Electron screen source when one is available", () => {
|
||||
expect(
|
||||
getScreenSourceIdForDisplay({
|
||||
displayId: "42",
|
||||
matchedSourceId: "screen:42:0",
|
||||
platform: "linux",
|
||||
}),
|
||||
).toBe("screen:42:0");
|
||||
});
|
||||
|
||||
it("routes unmatched Linux Wayland screens through the portal sentinel", () => {
|
||||
expect(
|
||||
getScreenSourceIdForDisplay({
|
||||
displayId: "42",
|
||||
env: { XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0" },
|
||||
matchedSourceId: null,
|
||||
platform: "linux",
|
||||
}),
|
||||
).toBe(LINUX_PORTAL_SCREEN_SOURCE_ID);
|
||||
});
|
||||
|
||||
it("keeps unmatched Linux X11 screens on the explicit fallback id", () => {
|
||||
expect(
|
||||
getScreenSourceIdForDisplay({
|
||||
displayId: "42",
|
||||
env: { XDG_SESSION_TYPE: "x11", DISPLAY: ":0" },
|
||||
matchedSourceId: null,
|
||||
platform: "linux",
|
||||
}),
|
||||
).toBe("screen:fallback:42");
|
||||
});
|
||||
|
||||
it("keeps non-Linux unmatched screens on the explicit fallback id", () => {
|
||||
expect(
|
||||
getScreenSourceIdForDisplay({
|
||||
displayId: "42",
|
||||
matchedSourceId: undefined,
|
||||
platform: "win32",
|
||||
}),
|
||||
).toBe("screen:fallback:42");
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldUseSyntheticLinuxPortalSource", () => {
|
||||
it("keeps Wayland portal capture on the synthetic source path", () => {
|
||||
expect(
|
||||
shouldUseSyntheticLinuxPortalSource({
|
||||
env: { XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0" },
|
||||
platform: "linux",
|
||||
sourceId: LINUX_PORTAL_SCREEN_SOURCE_ID,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("lets X11 use Electron desktopCapturer sources instead of a synthetic id", () => {
|
||||
expect(
|
||||
shouldUseSyntheticLinuxPortalSource({
|
||||
env: { XDG_SESSION_TYPE: "x11", DISPLAY: ":0" },
|
||||
platform: "linux",
|
||||
sourceId: LINUX_PORTAL_SCREEN_SOURCE_ID,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("recovers stale fallback ids through the synthetic path on Wayland", () => {
|
||||
expect(
|
||||
shouldUseSyntheticLinuxPortalSource({
|
||||
env: { WAYLAND_DISPLAY: "wayland-0" },
|
||||
platform: "linux",
|
||||
sourceId: "screen:fallback:0",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("defaults unknown Linux sessions with WAYLAND_DISPLAY to the synthetic path", () => {
|
||||
expect(
|
||||
shouldUseSyntheticLinuxPortalSource({
|
||||
env: { WAYLAND_DISPLAY: "wayland-0" },
|
||||
platform: "linux",
|
||||
sourceId: null,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not synthesize for concrete source ids", () => {
|
||||
expect(
|
||||
shouldUseSyntheticLinuxPortalSource({
|
||||
env: { XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0" },
|
||||
platform: "linux",
|
||||
sourceId: "screen:42:0",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not synthesize outside Linux", () => {
|
||||
expect(
|
||||
shouldUseSyntheticLinuxPortalSource({
|
||||
env: { XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0" },
|
||||
platform: "win32",
|
||||
sourceId: LINUX_PORTAL_SCREEN_SOURCE_ID,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
export const LINUX_PORTAL_SCREEN_SOURCE_ID = "screen:linux-portal";
|
||||
|
||||
export function isLikelyLinuxWaylandSession(env: NodeJS.ProcessEnv) {
|
||||
const sessionType = env.XDG_SESSION_TYPE?.trim().toLowerCase();
|
||||
if (sessionType === "wayland") {
|
||||
return true;
|
||||
}
|
||||
if (sessionType === "x11") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(env.WAYLAND_DISPLAY);
|
||||
}
|
||||
|
||||
export function getScreenSourceIdForDisplay({
|
||||
displayId,
|
||||
env = process.env,
|
||||
matchedSourceId,
|
||||
platform,
|
||||
}: {
|
||||
displayId: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
matchedSourceId?: string | null;
|
||||
platform: NodeJS.Platform | string;
|
||||
}) {
|
||||
if (matchedSourceId) {
|
||||
return matchedSourceId;
|
||||
}
|
||||
|
||||
if (platform === "linux" && isLikelyLinuxWaylandSession(env)) {
|
||||
return LINUX_PORTAL_SCREEN_SOURCE_ID;
|
||||
}
|
||||
|
||||
return `screen:fallback:${displayId}`;
|
||||
}
|
||||
|
||||
export function shouldUseSyntheticLinuxPortalSource({
|
||||
env,
|
||||
platform,
|
||||
sourceId,
|
||||
}: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
platform: NodeJS.Platform | string;
|
||||
sourceId?: string | null;
|
||||
}) {
|
||||
if (platform !== "linux") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
sourceId &&
|
||||
sourceId !== LINUX_PORTAL_SCREEN_SOURCE_ID &&
|
||||
!sourceId.startsWith("screen:fallback:")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isLikelyLinuxWaylandSession(env);
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { app, BrowserWindow, desktopCapturer, ipcMain } from "electron";
|
||||
import { reassertHudOverlayMousePassthrough } from "../../windows";
|
||||
import { ALLOW_RECORDLY_WINDOW_CAPTURE } from "../constants";
|
||||
import {
|
||||
getNativeMacWindowSources,
|
||||
resolveLinuxWindowBounds,
|
||||
resolveMacWindowBounds,
|
||||
resolveWindowsWindowBounds,
|
||||
stopWindowBoundsCapture,
|
||||
} from "../cursor/bounds";
|
||||
import { getDisplayBoundsForSource, getDisplayWorkAreaForSource } from "../recording/ffmpeg";
|
||||
import { selectedSource, setSelectedSource } from "../state";
|
||||
import type { SelectedSource } from "../types";
|
||||
import { getScreen, parseWindowId } from "../utils";
|
||||
import { getDisplayBoundsForSource, getDisplayWorkAreaForSource } from "../recording/ffmpeg";
|
||||
import {
|
||||
getNativeMacWindowSources,
|
||||
resolveMacWindowBounds,
|
||||
resolveWindowsWindowBounds,
|
||||
resolveLinuxWindowBounds,
|
||||
stopWindowBoundsCapture,
|
||||
} from "../cursor/bounds";
|
||||
import { reassertHudOverlayMousePassthrough } from "../../windows";
|
||||
import { getScreenSourceIdForDisplay } from "./sourceMapping";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const SOURCE_LIST_CACHE_TTL_MS = 1200;
|
||||
@@ -125,7 +126,12 @@ export function registerSourceHandlers({
|
||||
: `Screen ${index + 1}`;
|
||||
|
||||
return {
|
||||
id: matchedSource?.id ?? `screen:fallback:${displayId}`,
|
||||
id: getScreenSourceIdForDisplay({
|
||||
displayId,
|
||||
env: process.env,
|
||||
matchedSourceId: matchedSource?.id,
|
||||
platform: process.platform,
|
||||
}),
|
||||
name: displayName,
|
||||
originalName: matchedSource?.name ?? displayName,
|
||||
display_id: displayId,
|
||||
|
||||
+10
-3
@@ -25,6 +25,7 @@ import {
|
||||
killWindowsCaptureProcess,
|
||||
registerIpcHandlers,
|
||||
} from "./ipc/handlers";
|
||||
import { shouldUseSyntheticLinuxPortalSource } from "./ipc/register/sourceMapping";
|
||||
import { ensureMediaServer } from "./mediaServer";
|
||||
import { ensurePackagedRendererServer } from "./rendererServer";
|
||||
import type { UpdateToastPayload } from "./updater";
|
||||
@@ -1014,12 +1015,18 @@ 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
|
||||
// Default to the sentinel on Linux/Wayland 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" || !sourceId);
|
||||
// X11 does not need this synthetic path; use Electron's documented
|
||||
// desktopCapturer source flow there so getDisplayMedia receives a
|
||||
// real source id instead of a Wayland-only portal sentinel.
|
||||
const isLinuxPortalSentinel = shouldUseSyntheticLinuxPortalSource({
|
||||
env: process.env,
|
||||
platform: process.platform,
|
||||
sourceId,
|
||||
});
|
||||
if (isLinuxPortalSentinel) {
|
||||
callback({ video: { id: "screen:0:0", name: "Entire screen" } });
|
||||
return;
|
||||
|
||||
@@ -179,6 +179,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
hudOverlayRendererReady: () => {
|
||||
ipcRenderer.send("hud-overlay-renderer-ready");
|
||||
},
|
||||
hudOverlaySetWebcamPreviewVisible: (visible: boolean) => {
|
||||
ipcRenderer.send("hud-overlay-set-webcam-preview-visible", visible);
|
||||
},
|
||||
getHudOverlayCaptureProtection: () => {
|
||||
return ipcRenderer.invoke("get-hud-overlay-capture-protection");
|
||||
},
|
||||
|
||||
+21
-1
@@ -8,6 +8,7 @@ import { USER_DATA_PATH } from "./appPaths";
|
||||
import {
|
||||
getHudOverlayWindowBounds,
|
||||
resizeHudOverlayFallbackBounds,
|
||||
shouldExpandHudOverlayFallback,
|
||||
shouldResizeHudOverlayFallback,
|
||||
} from "./hudOverlayBounds";
|
||||
import { getPackagedRendererBaseUrl } from "./rendererServer";
|
||||
@@ -33,6 +34,7 @@ let hudOverlayFallbackExpanded = false;
|
||||
let hudOverlayIgnoringMouse = true;
|
||||
let hudOverlayMouseReassertTimer: NodeJS.Timeout | null = null;
|
||||
let hudOverlayRecordingActive = false;
|
||||
let hudOverlayWebcamPreviewVisible = false;
|
||||
let countdownWindow: BrowserWindow | null = null;
|
||||
let updateToastWindow: BrowserWindow | null = null;
|
||||
|
||||
@@ -193,10 +195,15 @@ function getHudOverlayDisplay() {
|
||||
|
||||
function getHudOverlayBounds() {
|
||||
const { workArea } = getHudOverlayDisplay();
|
||||
const fallbackExpanded = shouldExpandHudOverlayFallback({
|
||||
fallbackExpanded: hudOverlayFallbackExpanded,
|
||||
recordingActive: hudOverlayRecordingActive,
|
||||
webcamPreviewVisible: hudOverlayWebcamPreviewVisible,
|
||||
});
|
||||
return getHudOverlayWindowBounds(
|
||||
workArea,
|
||||
isHudOverlayMousePassthroughSupported() && !hudOverlayRecordingActive,
|
||||
hudOverlayFallbackExpanded,
|
||||
fallbackExpanded,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -398,6 +405,18 @@ ipcMain.handle("get-hud-overlay-mouse-passthrough-supported", () => {
|
||||
};
|
||||
});
|
||||
|
||||
ipcMain.on("hud-overlay-set-webcam-preview-visible", (_event, visible: boolean) => {
|
||||
const nextVisible = Boolean(visible);
|
||||
if (hudOverlayWebcamPreviewVisible === nextVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
hudOverlayWebcamPreviewVisible = nextVisible;
|
||||
if (hudOverlayRecordingActive) {
|
||||
applyHudOverlayBounds();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean) => {
|
||||
loadHudOverlayCaptureProtectionSetting();
|
||||
hudOverlayHiddenFromCapture = Boolean(enabled);
|
||||
@@ -420,6 +439,7 @@ ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean)
|
||||
export function createHudOverlayWindow(): BrowserWindow {
|
||||
loadHudOverlayCaptureProtectionSetting();
|
||||
hudOverlayFallbackExpanded = false;
|
||||
hudOverlayWebcamPreviewVisible = false;
|
||||
const initialBounds = getHudOverlayBounds();
|
||||
let hasShownHudWindow = false;
|
||||
|
||||
|
||||
@@ -152,6 +152,16 @@ function LaunchWindowContent() {
|
||||
hudOverlayMousePassthroughSupported,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
window.electronAPI?.hudOverlaySetWebcamPreviewVisible?.(showRecordingWebcamPreview);
|
||||
}, [showRecordingWebcamPreview]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
window.electronAPI?.hudOverlaySetWebcamPreviewVisible?.(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const {
|
||||
recordingHudOffset,
|
||||
isHudDragging,
|
||||
|
||||
@@ -705,6 +705,7 @@ export default function VideoEditor() {
|
||||
borderRadius,
|
||||
padding: { ...padding },
|
||||
frame,
|
||||
cropRegion: { ...cropRegion },
|
||||
webcam: { ...webcam },
|
||||
aspectRatio,
|
||||
exportEncodingMode,
|
||||
@@ -756,6 +757,7 @@ export default function VideoEditor() {
|
||||
borderRadius,
|
||||
padding,
|
||||
frame,
|
||||
cropRegion,
|
||||
webcam,
|
||||
aspectRatio,
|
||||
exportEncodingMode,
|
||||
@@ -848,6 +850,7 @@ export default function VideoEditor() {
|
||||
setBorderRadius(snapshot.borderRadius);
|
||||
setPadding({ ...snapshot.padding });
|
||||
setFrame(snapshot.frame);
|
||||
setCropRegion({ ...snapshot.cropRegion });
|
||||
setWebcam({ ...snapshot.webcam });
|
||||
setAspectRatio(snapshot.aspectRatio);
|
||||
setExportEncodingMode(snapshot.exportEncodingMode);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
saveEditorPreferences,
|
||||
saveEditorPresets,
|
||||
} from "./editorPreferences";
|
||||
import { DEFAULT_AUTO_CAPTION_SETTINGS } from "./types";
|
||||
import { DEFAULT_AUTO_CAPTION_SETTINGS, DEFAULT_CROP_REGION } from "./types";
|
||||
|
||||
function createStorageMock(initialValues: Record<string, string> = {}): Storage {
|
||||
const store = new Map(Object.entries(initialValues));
|
||||
@@ -358,6 +358,7 @@ describe("editorPreferences", () => {
|
||||
updatedAt: "2026-05-01T00:00:00.000Z",
|
||||
snapshot: {
|
||||
...DEFAULT_EDITOR_PREFERENCES,
|
||||
cropRegion: DEFAULT_CROP_REGION,
|
||||
autoCaptionSettings: DEFAULT_AUTO_CAPTION_SETTINGS,
|
||||
},
|
||||
},
|
||||
@@ -385,6 +386,7 @@ describe("editorPreferences", () => {
|
||||
updatedAt: "2026-05-02T00:00:00.000Z",
|
||||
snapshot: {
|
||||
...DEFAULT_EDITOR_PREFERENCES,
|
||||
cropRegion: DEFAULT_CROP_REGION,
|
||||
autoCaptionSettings: DEFAULT_AUTO_CAPTION_SETTINGS,
|
||||
},
|
||||
},
|
||||
@@ -399,6 +401,34 @@ describe("editorPreferences", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves crop region in editor preset snapshots", () => {
|
||||
const localStorage = createStorageMock();
|
||||
vi.stubGlobal("localStorage", localStorage);
|
||||
|
||||
expect(
|
||||
saveEditorPresets([
|
||||
{
|
||||
id: "preset-1",
|
||||
name: "Cropped Demo",
|
||||
createdAt: "2026-05-01T00:00:00.000Z",
|
||||
updatedAt: "2026-05-01T00:00:00.000Z",
|
||||
snapshot: {
|
||||
...DEFAULT_EDITOR_PREFERENCES,
|
||||
cropRegion: { x: 0.08, y: 0.12, width: 0.8, height: 0.7 },
|
||||
autoCaptionSettings: DEFAULT_AUTO_CAPTION_SETTINGS,
|
||||
},
|
||||
},
|
||||
]),
|
||||
).toBe(true);
|
||||
|
||||
expect(loadEditorPresets()[0]?.snapshot.cropRegion).toEqual({
|
||||
x: 0.08,
|
||||
y: 0.12,
|
||||
width: 0.8,
|
||||
height: 0.7,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns false when preset persistence fails", () => {
|
||||
const localStorage = createStorageMock();
|
||||
localStorage.setItem = () => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { loadAppSetting, saveAppSetting } from "../../lib/appSettings";
|
||||
import {
|
||||
normalizeExportBackendPreference,
|
||||
normalizeExportMp4FrameRate,
|
||||
normalizeExportPipelineModel,
|
||||
normalizeProjectEditor,
|
||||
stripPersistedDevMotionBlurSettings,
|
||||
type ProjectEditorState,
|
||||
stripPersistedDevMotionBlurSettings,
|
||||
} from "./projectPersistence";
|
||||
import { loadAppSetting, saveAppSetting } from "../../lib/appSettings";
|
||||
|
||||
type PersistedEditorControls = Pick<
|
||||
ProjectEditorState,
|
||||
@@ -61,8 +61,10 @@ type PersistedEditorControls = Pick<
|
||||
type PartialEditorControls = Partial<PersistedEditorControls>;
|
||||
|
||||
type PresetAutoCaptionSettings = ProjectEditorState["autoCaptionSettings"];
|
||||
type PresetCropRegion = ProjectEditorState["cropRegion"];
|
||||
|
||||
export interface EditorPresetSnapshot extends PersistedEditorControls {
|
||||
cropRegion: PresetCropRegion;
|
||||
autoCaptionSettings: PresetAutoCaptionSettings;
|
||||
whisperExecutablePath: string | null;
|
||||
whisperModelPath: string | null;
|
||||
@@ -196,9 +198,13 @@ function normalizeEditorPresetSnapshot(candidate: unknown): EditorPresetSnapshot
|
||||
candidate && typeof candidate === "object"
|
||||
? (candidate as Partial<EditorPresetSnapshot>)
|
||||
: {};
|
||||
const normalizedCropRegion = normalizeProjectEditor({
|
||||
cropRegion: raw.cropRegion,
|
||||
}).cropRegion;
|
||||
|
||||
return {
|
||||
...normalizeEditorControls(normalizedPreferences, normalizedPreferences),
|
||||
cropRegion: normalizedCropRegion,
|
||||
autoCaptionSettings: normalizePresetAutoCaptionSettings(raw.autoCaptionSettings),
|
||||
whisperExecutablePath:
|
||||
normalizeNullablePath(raw.whisperExecutablePath) ??
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
normalizeBrowserMicrophoneProfile,
|
||||
resolveBrowserCaptureCursorPolicy,
|
||||
resolveLinuxPortalCursorPresentation,
|
||||
shouldUseLinuxPortalCapture,
|
||||
shouldUseNativeWindowsCaptureForSource,
|
||||
} from "./useScreenRecorder";
|
||||
|
||||
@@ -207,6 +208,44 @@ describe("resolveLinuxPortalCursorPresentation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldUseLinuxPortalCapture", () => {
|
||||
it("uses the portal when the selected source is the Linux sentinel", () => {
|
||||
expect(
|
||||
shouldUseLinuxPortalCapture({
|
||||
browserCaptureSourceId: "screen:linux-portal",
|
||||
selectedSourceId: "screen:linux-portal",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the portal when a stale screen fallback resolves to the Linux sentinel", () => {
|
||||
expect(
|
||||
shouldUseLinuxPortalCapture({
|
||||
browserCaptureSourceId: "screen:linux-portal",
|
||||
selectedSourceId: "screen:fallback:42",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps live Electron screen sources on browser getUserMedia", () => {
|
||||
expect(
|
||||
shouldUseLinuxPortalCapture({
|
||||
browserCaptureSourceId: "screen:42:0",
|
||||
selectedSourceId: "screen:42:0",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("prefers a live Electron source over stale portal selection state", () => {
|
||||
expect(
|
||||
shouldUseLinuxPortalCapture({
|
||||
browserCaptureSourceId: "screen:42:0",
|
||||
selectedSourceId: "screen:linux-portal",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getScreenCaptureCursorSetting", () => {
|
||||
it("normalizes only supported screen-capture cursor settings", () => {
|
||||
expect(getScreenCaptureCursorSetting({ cursor: "motion" } as MediaTrackSettings)).toBe(
|
||||
|
||||
@@ -128,6 +128,23 @@ type DesktopCaptureMediaDevices = {
|
||||
getDisplayMedia: (constraints: unknown) => Promise<MediaStream>;
|
||||
};
|
||||
|
||||
export function shouldUseLinuxPortalCapture({
|
||||
browserCaptureSourceId,
|
||||
selectedSourceId,
|
||||
}: {
|
||||
browserCaptureSourceId?: string;
|
||||
selectedSourceId?: string;
|
||||
}) {
|
||||
if (browserCaptureSourceId && browserCaptureSourceId !== LINUX_PORTAL_SOURCE.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
selectedSourceId === LINUX_PORTAL_SOURCE.id ||
|
||||
browserCaptureSourceId === LINUX_PORTAL_SOURCE.id
|
||||
);
|
||||
}
|
||||
|
||||
type UseScreenRecorderReturn = {
|
||||
recording: boolean;
|
||||
paused: boolean;
|
||||
@@ -684,7 +701,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
// 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") {
|
||||
if (source.id === LINUX_PORTAL_SOURCE.id) {
|
||||
return source;
|
||||
}
|
||||
|
||||
@@ -1430,7 +1447,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
// 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") {
|
||||
if (!existingSource && selectedSource.id === LINUX_PORTAL_SOURCE.id) {
|
||||
try {
|
||||
await window.electronAPI.selectSource(selectedSource);
|
||||
} catch (err) {
|
||||
@@ -1655,7 +1672,10 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
|
||||
let videoTrack: MediaStreamTrack | undefined;
|
||||
let systemAudioIncluded = false;
|
||||
const mediaDevices = navigator.mediaDevices as DesktopCaptureMediaDevices;
|
||||
const useLinuxPortal = selectedSource.id === "screen:linux-portal";
|
||||
const useLinuxPortal = shouldUseLinuxPortalCapture({
|
||||
browserCaptureSourceId: browserCaptureSource.id,
|
||||
selectedSourceId: selectedSource.id,
|
||||
});
|
||||
const browserScreenVideoConstraints = {
|
||||
mandatory: {
|
||||
chromeMediaSource: CHROME_MEDIA_SOURCE,
|
||||
|
||||
Reference in New Issue
Block a user