/** * Tap-gesture interpretation for the video player surface. * * Every tap acts IMMEDIATELY — there are only first and second taps, and no * deferral: * * 1st tap: toggle play/pause * 2nd tap (within the window): seek, then toggle play/pause AGAIN * * The second toggle undoes the first, so a double tap seeks while leaving the * play state exactly as it started — playing stays playing, paused stays paused. * * This replaced a design that deferred the first tap behind a 300ms timer so it * could be cancelled if a second tap arrived. That deferral raced the * compatibility `click` Android's WebView synthesizes after a touch tap: the * timer cleared its own handle *before* running the toggle, reopening the guard * that was meant to suppress the late click, which then toggled a second time. * The result was a play/pause loop about a second apart. Acting immediately * removes the timer, the window race, and the loop. * * TRACES: UR-005, UR-061 | DR-092, DR-095, DR-098 | UT-085, UT-086, UT-087, UT-088 */ /** A second tap within this window pairs with the previous one (seek + re-toggle). */ export const DOUBLE_TAP_WINDOW_MS = 300; /** * How long after a touch tap a mouse `click` is assumed to be the compatibility * event the browser synthesizes from that touch. Android's WebView can deliver it * noticeably late, so this is generous. */ export const TOUCH_CLICK_SUPPRESS_MS = 700; /** * Whether a touch landed on an interactive control rather than the bare video * surface, and so must NOT be interpreted as a play/pause or seek gesture. * * The gesture listener sits on the outer container, and touch events bubble, so * without this a tap on the bottom control bar runs the gesture handler (toggle * #1) *and* the button's own click handler (toggle #2) — the two cancel out and * the button appears dead. Buttons, links, inputs (the seek bar), and anything * inside an element marked `data-player-controls` are treated as controls. * * Takes the ancestor chain as plain tag/attribute pairs so the rule is unit * testable without a DOM. */ export function isControlSurfaceTouch( ancestors: Array<{ tag: string; isPlayerControls?: boolean; isPlayerSurface?: boolean }> ): boolean { const INTERACTIVE = new Set(["button", "a", "input", "select", "textarea", "label"]); for (const node of ancestors) { // `data-player-surface` wins over the tag check: the full-screen play overlay // is a