From 5edf740fda1b1842a6796df4b8c8f5e3586401eb Mon Sep 17 00:00:00 2001 From: Twest2 Date: Mon, 15 Jun 2026 18:20:57 -0500 Subject: [PATCH] Fix capture degrading in eco/power-saving mode; decouple from display refresh rate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes: - Hold powerSaveBlocker.start('prevent-app-suspension') for the duration of each recording session. Windows Power Throttling (EcoQoS) can CPU-starve the capture-worker renderer mid-session, causing stream frame requests to time out and degrade to the slower in-process legacy loop. The blocker signals to the OS that this is an active workload and keeps the stream worker and main-process event loop running at full speed. Released on pause and finish. - Remove maxFrameRate: 30 from the getUserMedia constraint in the capture worker. The actual sampling rate is driven entirely by the setInterval timer, so the stream's native frame rate is irrelevant to capture correctness. Capping at 30 could silently reduce stream delivery on some drivers when the display itself runs at ≤30 Hz (common in power-saving mode), leaving the ring buffer with fewer fresh frames at click time. --- app/main.js | 34 ++++++++++++++++++++++++++++------ app/renderer/capture-worker.js | 4 +++- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/app/main.js b/app/main.js index becd078..73f7b70 100644 --- a/app/main.js +++ b/app/main.js @@ -5,7 +5,7 @@ const fs = require('node:fs'); const os = require('node:os'); const { app, BrowserWindow, ipcMain, dialog, shell, nativeTheme, globalShortcut, - clipboard, nativeImage, screen, + clipboard, nativeImage, screen, powerSaveBlocker, } = require('electron'); const { GuideStore } = require('../core/store'); @@ -498,12 +498,34 @@ function setupIpc() { if (result.ok) reindex(guideId); return result; }); + let capturePowerBlocker = -1; + const startCapturePower = () => { + if (!powerSaveBlocker.isStarted(capturePowerBlocker)) { + capturePowerBlocker = powerSaveBlocker.start('prevent-app-suspension'); + } + }; + const stopCapturePower = () => { + if (powerSaveBlocker.isStarted(capturePowerBlocker)) { + powerSaveBlocker.stop(capturePowerBlocker); + } + }; + h('capture:session', async ({ action, guideId, intervalSec }) => { - if (action === 'start') capture.startSession(guideId, { intervalSec: intervalSec ?? null }); - else if (action === 'pause') capture.togglePause(true); - else if (action === 'resume') capture.togglePause(false); - else if (action === 'finish') capture.finishSession(); - else if (action === 'interval') capture.setInterval(intervalSec); + if (action === 'start') { + capture.startSession(guideId, { intervalSec: intervalSec ?? null }); + startCapturePower(); + } else if (action === 'pause') { + capture.togglePause(true); + stopCapturePower(); + } else if (action === 'resume') { + capture.togglePause(false); + startCapturePower(); + } else if (action === 'finish') { + capture.finishSession(); + stopCapturePower(); + } else if (action === 'interval') { + capture.setInterval(intervalSec); + } const state = capture.state(); sendToRenderer('capture:state', state); return state; diff --git a/app/renderer/capture-worker.js b/app/renderer/capture-worker.js index 2e0d216..0f87084 100644 --- a/app/renderer/capture-worker.js +++ b/app/renderer/capture-worker.js @@ -80,7 +80,9 @@ maxWidth: physWidth, minHeight: physHeight, maxHeight: physHeight, - maxFrameRate: 30, + // No maxFrameRate: sampling cadence is controlled by the setInterval + // timer below, so the actual capture rate is always sampleMs-driven + // regardless of display refresh rate or power mode. }, }, });