From 3d80c86abf73ba0f945ddf41edcb54e375b9b32b Mon Sep 17 00:00:00 2001 From: Iisyourdad Date: Thu, 11 Jun 2026 12:45:14 -0500 Subject: [PATCH] Fixed an issue where clicking wouldn't line up with screenshot part 4 --- app/capture.js | 12 ++++++++---- tests/unit/capture.test.js | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/app/capture.js b/app/capture.js index 76e838e..bbbb2ed 100644 --- a/app/capture.js +++ b/app/capture.js @@ -25,9 +25,9 @@ const { encodePng } = require('../core/png'); // Dedupe duplicate watcher events for one physical click while still // allowing intentionally fast clicking. const CLICK_DEBOUNCE_MS = 40; -// Idle gap between frame-loop grabs; the effective refresh rate is -// grab-duration + this. -const FRAME_LOOP_IDLE_MS = 50; +// Idle gap between frame-loop grabs. Keep this at zero so the buffered +// frame stays as close to real time as possible while recording. +const FRAME_LOOP_IDLE_MS = 0; // A buffered frame older than this is too stale to pass off as "the screen // at the instant of the click". const CLICK_FRAME_MAX_AGE_MS = 600; @@ -68,6 +68,7 @@ class CaptureService { this.latestFrame = null; this.lastClickCapture = 0; this.clickWatcherButtonDown = false; + this.frameLoopInFlight = false; this.shooting = false; } @@ -332,10 +333,12 @@ class CaptureService { if (!this.frameLoopRunning) return; if (!this.session || this.session.paused) { this.frameLoopRunning = false; + this.frameLoopInFlight = false; return; } try { if (!this.shooting) { + this.frameLoopInFlight = true; const mode = this.settings.get('capture.mode') || 'fullscreen'; const grabMode = mode === 'region' ? 'fullscreen' : mode; const frame = await this.captureCurrentFrame(grabMode); @@ -344,6 +347,7 @@ class CaptureService { } catch { // Grab failures are fine — clicks fall back to a one-off fresh shot. } finally { + this.frameLoopInFlight = false; if (this.frameLoopRunning && this.session && !this.session.paused) { this.frameLoopTimer = setTimeout(tick, FRAME_LOOP_IDLE_MS); } @@ -405,7 +409,7 @@ class CaptureService { && sameDisplay; }; if (usable(this.latestFrame)) return this.latestFrame; - if (!this.frameLoopRunning) return null; + if (!this.frameLoopRunning || !this.frameLoopInFlight) return null; const deadline = Date.now() + CLICK_FRAME_WAIT_MS; while (this.frameLoopRunning && Date.now() < deadline) { const next = await this.nextFrame(Math.max(1, deadline - Date.now())); diff --git a/tests/unit/capture.test.js b/tests/unit/capture.test.js index f2aa354..8f1ecdf 100644 --- a/tests/unit/capture.test.js +++ b/tests/unit/capture.test.js @@ -114,6 +114,7 @@ test('a buffered frame from a different display is ignored for click capture', a const service = makeService(); service.session = { guideId: 'guide-display', paused: false, count: 0, intervalSec: 0 }; service.frameLoopRunning = true; + service.frameLoopInFlight = true; service.latestFrame = makeFrame('wrong-display', 0, { display: { bounds: { x: 0, y: 0, width: 100, height: 100 } }, }); @@ -156,10 +157,36 @@ test('a stale buffered frame is not reused — the click falls back to a fresh s assert.equal(shootCalled, true, 'a stale buffered frame must not be reused'); }); +test('an idle click capture does not wait for the next frame loop tick', async () => { + const service = makeService(); + service.session = { guideId: 'guide-idle', paused: false, count: 0, intervalSec: 0 }; + service.frameLoopRunning = true; + service.frameLoopInFlight = false; + + let nextFrameCalled = false; + service.nextFrame = async () => { + nextFrameCalled = true; + throw new Error('idle clicks must not wait for a new frame'); + }; + + let shootCalled = false; + service.shoot = async () => { + shootCalled = true; + return { ok: true, step: { stepId: 'idle-step' } }; + }; + + const result = await service.sessionCapture('click', { x: 1, y: 1 }); + + assert.equal(result.ok, true); + assert.equal(shootCalled, true); + assert.equal(nextFrameCalled, false); +}); + test('clicks during an in-flight grab wait for the frame instead of being dropped', async () => { const service = makeService(); service.session = { guideId: 'guide-fast', paused: false, count: 0, intervalSec: 0 }; service.frameLoopRunning = true; // a grab is in flight, no frame buffered yet + service.frameLoopInFlight = true; service.shoot = async () => { throw new Error('waiting clicks must use the loop frame, not a competing shot'); };