From f5ebb37183515fda26f2c41640278d6d56c96134 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Sun, 30 Aug 2026 10:58:27 -0400 Subject: [PATCH] fix(desktop): avoid Windows capture stalls --- .../src/windowCapture/DesktopWindowCapture.ts | 44 ++++++++++++------- .../WindowsWindowCapture.test.ts | 36 +++++++++++++++ .../src/windowCapture/WindowsWindowCapture.ts | 23 ++++++++++ 3 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 apps/desktop/src/windowCapture/WindowsWindowCapture.test.ts create mode 100644 apps/desktop/src/windowCapture/WindowsWindowCapture.ts diff --git a/apps/desktop/src/windowCapture/DesktopWindowCapture.ts b/apps/desktop/src/windowCapture/DesktopWindowCapture.ts index 4a3ae36a53b9..92ccd335976b 100644 --- a/apps/desktop/src/windowCapture/DesktopWindowCapture.ts +++ b/apps/desktop/src/windowCapture/DesktopWindowCapture.ts @@ -37,6 +37,10 @@ import * as DesktopClientSettings from "../settings/DesktopClientSettings.ts"; import * as DesktopWindow from "../window/DesktopWindow.ts"; import { startGlobalShiftShortcutProcess } from "./GlobalShiftShortcutProcess.ts"; import { startMacModifierPairShortcutProcess } from "./MacModifierPairShortcutProcess.ts"; +import { + captureWindowsWindowSnapshot, + type WindowsWindowCaptureSource, +} from "./WindowsWindowCapture.ts"; import { type WindowCaptureAnimationDestination, WindowCaptureTransition, @@ -372,22 +376,32 @@ async function captureSource({ }); } - const sources = await Electron.desktopCapturer.getSources({ - types: mode === "portal" ? ["window", "screen"] : ["window"], - thumbnailSize: windowCaptureThumbnailSize(active), - fetchWindowIcons: true, - }); - const source = - mode === "portal" ? sources[0] : active ? findCaptureSource(sources, active) : undefined; - if (!source || source.thumbnail.isEmpty()) { - throw mode === "portal" - ? new DesktopWindowCaptureError({ - operation: "no-window-selected", - captureId, - }) - : new DesktopWindowCaptureError({ operation: "window-unavailable", captureId }); + let source: WindowsWindowCaptureSource | Electron.DesktopCapturerSource; + let png: Buffer; + if (platform === "win32") { + if (!active) { + throw new DesktopWindowCaptureError({ operation: "window-unavailable", captureId }); + } + ({ source, png } = await captureWindowsWindowSnapshot(active)); + } else { + const sources = await Electron.desktopCapturer.getSources({ + types: mode === "portal" ? ["window", "screen"] : ["window"], + thumbnailSize: windowCaptureThumbnailSize(active), + fetchWindowIcons: true, + }); + const selected = + mode === "portal" ? sources[0] : active ? findCaptureSource(sources, active) : undefined; + if (!selected || selected.thumbnail.isEmpty()) { + throw mode === "portal" + ? new DesktopWindowCaptureError({ + operation: "no-window-selected", + captureId, + }) + : new DesktopWindowCaptureError({ operation: "window-unavailable", captureId }); + } + source = selected; + png = selected.thumbnail.toPNG(); } - const png = source.thumbnail.toPNG(); const animationStarted = await showCaptureFeedback( transition, flash, diff --git a/apps/desktop/src/windowCapture/WindowsWindowCapture.test.ts b/apps/desktop/src/windowCapture/WindowsWindowCapture.test.ts new file mode 100644 index 000000000000..0c174599935c --- /dev/null +++ b/apps/desktop/src/windowCapture/WindowsWindowCapture.test.ts @@ -0,0 +1,36 @@ +import type * as Electron from "electron"; +import type { Result as ActiveWindow } from "get-windows"; +import { assert, it, vi } from "vite-plus/test"; + +const { screenToDipRectMock, screenshotMock } = vi.hoisted(() => ({ + screenToDipRectMock: vi.fn((_window: unknown, bounds: Electron.Rectangle) => bounds), + screenshotMock: vi.fn(), +})); + +vi.mock("electron", () => ({ + screen: { screenToDipRect: screenToDipRectMock }, +})); + +vi.mock("@crowecawcaw/xa11y", () => { + const api = { screenshot: screenshotMock }; + return { ...api, default: api }; +}); + +import { captureWindowsWindowSnapshot } from "./WindowsWindowCapture.ts"; + +it("captures the active Windows window without rendering Chromium thumbnails", async () => { + const png = Buffer.from([1, 2, 3]); + screenshotMock.mockResolvedValue({ toPng: () => png }); + const active = { + title: "Editor", + owner: { name: "Editor", processId: 123 }, + bounds: { x: 10, y: 20, width: 800, height: 600 }, + } as ActiveWindow; + + const capture = await captureWindowsWindowSnapshot(active); + + assert.deepEqual(screenToDipRectMock.mock.calls, [[null, active.bounds]]); + assert.deepEqual(screenshotMock.mock.calls, [[{ region: active.bounds }]]); + assert.strictEqual(capture.png, png); + assert.deepEqual(capture.source, { name: "Editor" }); +}); diff --git a/apps/desktop/src/windowCapture/WindowsWindowCapture.ts b/apps/desktop/src/windowCapture/WindowsWindowCapture.ts new file mode 100644 index 000000000000..0117a3ed3a49 --- /dev/null +++ b/apps/desktop/src/windowCapture/WindowsWindowCapture.ts @@ -0,0 +1,23 @@ +import * as Electron from "electron"; +import type { Result as ActiveWindow } from "get-windows"; + +export type WindowsWindowCaptureSource = { + readonly appIcon?: Electron.NativeImage; + readonly name: string; +}; + +export async function captureWindowsWindowSnapshot( + active: ActiveWindow, +): Promise<{ readonly source: WindowsWindowCaptureSource; readonly png: Buffer }> { + const imported = await import("@crowecawcaw/xa11y"); + const xa11y = (imported as unknown as { readonly default?: typeof imported }).default ?? imported; + const snapshot = await xa11y.screenshot({ + region: Electron.screen.screenToDipRect(null, active.bounds), + }); + return { + source: { + name: active.title.trim() || active.owner.name.trim() || "Window", + }, + png: snapshot.toPng(), + }; +}