Fix capture degrading in eco/power-saving mode; decouple from display refresh rate
Template tests / tests (push) Successful in 1m47s

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.
This commit is contained in:
2026-06-15 18:20:57 -05:00
parent c39262c3fa
commit 5edf740fda
2 changed files with 31 additions and 7 deletions
+28 -6
View File
@@ -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;
+3 -1
View File
@@ -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.
},
},
});