fix(player): guard the play overlay against the synthesized touch click
After the DR-098 tap rewrite, pausing became impossible while unpausing always worked — an asymmetry that pointed straight at the overlay. Pausing renders a full-screen play-overlay button over the video. The compatibility click Android synthesizes from the tap arrives ~30-130ms later, by which time that button exists, so the click lands on the OVERLAY rather than the <video>. Its onclick called togglePlayPause with no guard at all, resuming immediately. Unpausing was unaffected because it removes the overlay, leaving nothing to intercept the click. The suppression rule was only wired into the video element's handler. Extract it as isSynthesizedTouchClick() in tapGestures.ts (unit-tested) and use it from every click target layered over the video, the overlay included. Verified: 724 frontend tests pass, svelte-check clean. Bumped to 0.2.5 so the APK installs over 2004.
This commit is contained in:
@@ -8,6 +8,8 @@ import {
|
||||
resolveSeekTarget,
|
||||
clampSeekTarget,
|
||||
END_SEEK_MARGIN_SECONDS,
|
||||
isSynthesizedTouchClick,
|
||||
TOUCH_CLICK_SUPPRESS_MS,
|
||||
} from "./tapGestures";
|
||||
|
||||
const SCREEN_WIDTH = 1000;
|
||||
@@ -197,6 +199,32 @@ describe("seek target resolution", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("synthesized touch-click suppression", () => {
|
||||
// Regression: pausing renders a full-screen play-overlay button over the
|
||||
// video, so the compatibility click Android synthesizes from the tap lands on
|
||||
// the OVERLAY, not the <video>. With no guard there it re-toggled and undid
|
||||
// the pause — pausing looked impossible while unpausing worked fine (the
|
||||
// overlay is removed when playing, so nothing intercepted that direction).
|
||||
it("suppresses a click with detail 0 (clearly synthesized)", () => {
|
||||
expect(isSynthesizedTouchClick(0, 10_000, 0)).toBe(true);
|
||||
});
|
||||
|
||||
it("suppresses a real-detail click that closely follows a touch tap", () => {
|
||||
const tapAt = 10_000;
|
||||
expect(isSynthesizedTouchClick(1, tapAt + 120, tapAt)).toBe(true);
|
||||
expect(isSynthesizedTouchClick(1, tapAt + TOUCH_CLICK_SUPPRESS_MS - 1, tapAt)).toBe(true);
|
||||
});
|
||||
|
||||
it("allows a genuine mouse click well after any touch", () => {
|
||||
const tapAt = 10_000;
|
||||
expect(isSynthesizedTouchClick(1, tapAt + TOUCH_CLICK_SUPPRESS_MS + 1, tapAt)).toBe(false);
|
||||
});
|
||||
|
||||
it("allows a genuine mouse click when no touch has ever happened", () => {
|
||||
expect(isSynthesizedTouchClick(1, 10_000, 0)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("seek target clamping (shared by skip and seek-bar drag)", () => {
|
||||
it("keeps a mid-stream target untouched", () => {
|
||||
expect(clampSeekTarget(100, 600)).toBe(100);
|
||||
|
||||
Reference in New Issue
Block a user