diff --git a/electron/main.ts b/electron/main.ts index 0b2d7725b..77a94a9d1 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -65,6 +65,7 @@ import { showPermissionsWindow, showPermissionsWindowIfNeeded, } from "./permissions"; +import { setDisplaySleepBlocked } from "./recording/displaySleepBlocker"; import { offersStarPrompt, REPO_URL, storeReviewUrl } from "./star-prompt"; import { registerSttIpc, shutdownStt } from "./stt"; import { checkLatestRelease } from "./update-checker"; @@ -1372,6 +1373,7 @@ appReady?.then(async () => { (recording: boolean, sourceName: string) => { selectedSourceName = sourceName; isRecording = recording; + setDisplaySleepBlocked(recording); if (!tray) createTray(); updateTrayMenu(recording); // `canOfferUpdateCheck()` now answers "not mid-take" too, and the app/Help menus are diff --git a/electron/recording/displaySleepBlocker.test.ts b/electron/recording/displaySleepBlocker.test.ts new file mode 100644 index 000000000..ff420d52c --- /dev/null +++ b/electron/recording/displaySleepBlocker.test.ts @@ -0,0 +1,52 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { start, stop } = vi.hoisted(() => { + let nextId = 1; + return { start: vi.fn(() => nextId++), stop: vi.fn() }; +}); + +vi.mock("electron", () => ({ powerSaveBlocker: { start, stop } })); + +describe("setDisplaySleepBlocked", () => { + let setDisplaySleepBlocked: (recording: boolean) => void; + + beforeEach(async () => { + vi.resetModules(); + start.mockClear(); + stop.mockClear(); + ({ setDisplaySleepBlocked } = await import("./displaySleepBlocker")); + }); + + it("starts one display-sleep blocker per recording and stops that same one", () => { + setDisplaySleepBlocked(true); + expect(start).toHaveBeenCalledOnce(); + expect(start).toHaveBeenCalledWith("prevent-display-sleep"); + + setDisplaySleepBlocked(false); + expect(stop).toHaveBeenCalledOnce(); + expect(stop).toHaveBeenCalledWith(start.mock.results[0].value); + }); + + it("never stacks a second blocker when a start is reported twice", () => { + setDisplaySleepBlocked(true); + setDisplaySleepBlocked(true); + setDisplaySleepBlocked(false); + expect(start).toHaveBeenCalledOnce(); + expect(stop).toHaveBeenCalledOnce(); + }); + + it("ignores a stop with nothing started, as a failed start then cancel reports", () => { + setDisplaySleepBlocked(false); + expect(stop).not.toHaveBeenCalled(); + }); + + it("leaves nothing running across consecutive takes", () => { + for (let take = 0; take < 3; take++) { + setDisplaySleepBlocked(true); + setDisplaySleepBlocked(false); + setDisplaySleepBlocked(false); + } + expect(start).toHaveBeenCalledTimes(3); + expect(stop.mock.calls.map(([id]) => id)).toEqual(start.mock.results.map((r) => r.value)); + }); +}); diff --git a/electron/recording/displaySleepBlocker.ts b/electron/recording/displaySleepBlocker.ts new file mode 100644 index 000000000..c58407493 --- /dev/null +++ b/electron/recording/displaySleepBlocker.ts @@ -0,0 +1,18 @@ +import { powerSaveBlocker } from "electron"; + +let blockerId: number | null = null; + +/** + * Keeps the display awake for the whole take, pauses included (#935). Driven by the + * same recording flag as the tray, which every backend's start and every stop, + * cancel and failure path already report, so it is idempotent in both directions: + * a repeated `true` never stacks a second blocker, a stray `false` is a no-op. + */ +export function setDisplaySleepBlocked(recording: boolean) { + if (recording && blockerId === null) { + blockerId = powerSaveBlocker.start("prevent-display-sleep"); + } else if (!recording && blockerId !== null) { + powerSaveBlocker.stop(blockerId); + blockerId = null; + } +} diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index 65d4ac003..ae86f3c7d 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -2028,6 +2028,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn { "error", () => { setRecording(false); + // The main process was told a take started (below), so it has to hear + // that it ended too, or the tray and the display-sleep blocker stay on. + window.electronAPI?.setRecordingState(false); }, { once: true }, );