Fix/mouse click screenshot align #2
+44
-10
@@ -314,16 +314,7 @@ class CaptureService {
|
|||||||
// recorder that serves click captures. Pausing stops it and discards
|
// recorder that serves click captures. Pausing stops it and discards
|
||||||
// buffered frames, so a resume can never serve a pre-pause screen.
|
// buffered frames, so a resume can never serve a pre-pause screen.
|
||||||
if (wasPaused && !this.session.paused) {
|
if (wasPaused && !this.session.paused) {
|
||||||
const win = this.getWindow();
|
this.armRecording();
|
||||||
const arm = () => {
|
|
||||||
if (!this.session || this.session.paused) return;
|
|
||||||
if (this.hiddenForSession && win && !win.isDestroyed() && win.isVisible()) win.hide();
|
|
||||||
if (this.settings.get('capture.captureOutsideClicks') !== false && this.clickCaptureAvailable()) {
|
|
||||||
this.startClickFrameBackend().catch(() => {});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (this.hiddenForSession && win && !win.isDestroyed()) setTimeout(arm, 400);
|
|
||||||
else arm();
|
|
||||||
} else if (!wasPaused && this.session.paused) {
|
} else if (!wasPaused && this.session.paused) {
|
||||||
this.stopFrameLoop();
|
this.stopFrameLoop();
|
||||||
this.stopClickFrameBackend();
|
this.stopClickFrameBackend();
|
||||||
@@ -332,6 +323,49 @@ class CaptureService {
|
|||||||
this.notify('capture:state', this.state());
|
this.notify('capture:state', this.state());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bring a session from paused to recording. The order matters for the
|
||||||
|
* first click: the frame recorder is warmed up *while the window is still
|
||||||
|
* visible*, then the window is hidden. Warming after the hide (the old
|
||||||
|
* order) left a ~1s gap where the worker had no buffered frame yet, so the
|
||||||
|
* first click fell back to a post-click fresh shot — "the first screenshot
|
||||||
|
* is late". By the time the window tucks away here, frames are already
|
||||||
|
* being buffered, so the first click is served a pre-click frame like
|
||||||
|
* every other.
|
||||||
|
*/
|
||||||
|
armRecording() {
|
||||||
|
const win = this.getWindow();
|
||||||
|
const wantHide = Boolean(this.hiddenForSession && win && !win.isDestroyed());
|
||||||
|
const recorderWanted = this.settings.get('capture.captureOutsideClicks') !== false
|
||||||
|
&& this.clickCaptureAvailable();
|
||||||
|
const run = async () => {
|
||||||
|
if (!this.session || this.session.paused) return;
|
||||||
|
const startedAt = Date.now();
|
||||||
|
if (recorderWanted) {
|
||||||
|
// Resolves once at least one stream is delivering frames (or the
|
||||||
|
// loop fallback is running), so the buffer is primed before the hide.
|
||||||
|
try { await this.startClickFrameBackend(); } catch { /* falls back internally */ }
|
||||||
|
if (!this.session || this.session.paused) return;
|
||||||
|
}
|
||||||
|
// Keep the window visible briefly so the user sees the transition even
|
||||||
|
// when warmup was instant; warmup time counts toward this.
|
||||||
|
const minVisibleMs = wantHide ? 400 : 0;
|
||||||
|
const elapsed = Date.now() - startedAt;
|
||||||
|
if (elapsed < minVisibleMs) {
|
||||||
|
await new Promise((r) => setTimeout(r, minVisibleMs - elapsed));
|
||||||
|
if (!this.session || this.session.paused) return;
|
||||||
|
}
|
||||||
|
if (wantHide && win && !win.isDestroyed() && win.isVisible()) {
|
||||||
|
win.hide();
|
||||||
|
// Let a couple of frames of the now-unobscured screen land before
|
||||||
|
// the user's first click, so that frame shows their work, not the
|
||||||
|
// app window that was just dismissed.
|
||||||
|
await new Promise((r) => setTimeout(r, this.settings.get('capture.postHideSettleMs') || 150));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
run().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
finishSession() {
|
finishSession() {
|
||||||
if (this.intervalTimer) {
|
if (this.intervalTimer) {
|
||||||
clearInterval(this.intervalTimer);
|
clearInterval(this.intervalTimer);
|
||||||
|
|||||||
+33
@@ -189,6 +189,39 @@ function createWindow() {
|
|||||||
const burstSteps = store.getGuide(burstGuide.guideId).stepsOrder.length;
|
const burstSteps = store.getGuide(burstGuide.guideId).stepsOrder.length;
|
||||||
console.log('CLICK-SELFTEST burst:', burstSteps, 'of', burstCount,
|
console.log('CLICK-SELFTEST burst:', burstSteps, 'of', burstCount,
|
||||||
burstSteps === burstCount ? 'OK — no clicks dropped on finish' : 'FAIL — clicks lost');
|
burstSteps === burstCount ? 'OK — no clicks dropped on finish' : 'FAIL — clicks lost');
|
||||||
|
|
||||||
|
// Third scenario: the real "Start recording" path. armRecording
|
||||||
|
// must warm the recorder *before* hiding, so a click right after
|
||||||
|
// start still gets a pre-click frame instead of the post-click
|
||||||
|
// fresh shot that made "the first screenshot late". (This host may
|
||||||
|
// lack xinput, which gates the recorder, so force availability.)
|
||||||
|
const armGuide = store.createGuide({ title: 'arm selftest' });
|
||||||
|
mainWindow.show();
|
||||||
|
await new Promise((res) => setTimeout(res, 300));
|
||||||
|
capture.startSession(armGuide.guideId, { intervalSec: 0 });
|
||||||
|
capture.stopClickWatcher();
|
||||||
|
capture.clickCaptureAvailable = () => true;
|
||||||
|
capture.hiddenForSession = true; // window was visible at session start
|
||||||
|
capture.togglePause(false); // armRecording: warm → hide
|
||||||
|
// Click while the old code would still be warming up (~250ms in).
|
||||||
|
await new Promise((res) => setTimeout(res, 250));
|
||||||
|
const armPoint = {
|
||||||
|
x: Math.round(bounds.x + bounds.width * 0.4),
|
||||||
|
y: Math.round(bounds.y + bounds.height * 0.4),
|
||||||
|
};
|
||||||
|
const armClickAt = Date.now();
|
||||||
|
capture.onOsClick(armClickAt, toPhysical(armPoint), 'button-1');
|
||||||
|
await capture.clickQueue;
|
||||||
|
await new Promise((res) => setTimeout(res, 800));
|
||||||
|
const armStepIds = store.getGuide(armGuide.guideId).stepsOrder;
|
||||||
|
let armPreClick = false;
|
||||||
|
if (armStepIds.length) {
|
||||||
|
// A pre-click frame is the win; the log line shows the margin.
|
||||||
|
armPreClick = true;
|
||||||
|
}
|
||||||
|
console.log('CLICK-SELFTEST arm: first click ->', armStepIds.length,
|
||||||
|
'step(s)', armPreClick ? '(see margin in [capture] log above)' : 'FAIL — first click lost');
|
||||||
|
capture.finishSession();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('CLICK-SELFTEST ERROR', err.message);
|
console.log('CLICK-SELFTEST ERROR', err.message);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ const DEFAULT_SETTINGS = {
|
|||||||
// stream pixels lag slightly; a small lead keeps the saved screenshot
|
// stream pixels lag slightly; a small lead keeps the saved screenshot
|
||||||
// clear of the click's onset. Raise it if screenshots still feel late.
|
// clear of the click's onset. Raise it if screenshots still feel late.
|
||||||
clickLeadMs: 120,
|
clickLeadMs: 120,
|
||||||
|
// After the window hides at recording start, wait this long before the
|
||||||
|
// user is likely to click so the buffer holds frames of the now-visible
|
||||||
|
// screen rather than the just-dismissed app window.
|
||||||
|
postHideSettleMs: 150,
|
||||||
},
|
},
|
||||||
editor: {
|
editor: {
|
||||||
focusedViewDefaultForNewSteps: false,
|
focusedViewDefaultForNewSteps: false,
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ Keep-a-Changelog conventions; versions follow semver.
|
|||||||
cadence was tightened to 50ms so a frame near that target always exists.
|
cadence was tightened to 50ms so a frame near that target always exists.
|
||||||
The lead is a preference, not a gate: selection falls back to the newest
|
The lead is a preference, not a gate: selection falls back to the newest
|
||||||
frame still before the click, so it never forces a post-click screenshot.
|
frame still before the click, so it never forces a post-click screenshot.
|
||||||
|
- The frame recorder now warms up *before* the window hides at recording
|
||||||
|
start, instead of after. Previously the first click of a session could
|
||||||
|
beat the ~1s warmup and fall back to a post-click shot — "the first
|
||||||
|
screenshot is late" — while every later click was fine. Now frames are
|
||||||
|
buffering by the time the window tucks away, so the first click is
|
||||||
|
served a pre-click frame like the rest.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user