// Thin wrapper around the Screen Wake Lock API. Held by the diashow page so a phone // driving a projector doesn't sleep. No-op on unsupported browsers (Firefox, older // Safari). // // Wake locks die when the document goes hidden; the page re-acquires on visible to // keep the screen on across short interruptions. interface SentinelLike { release: () => Promise; // WakeLockSentinel is an EventTarget; it fires `release` when the lock ends — including // when the OS drops it because the tab was hidden. addEventListener?: (type: 'release', listener: () => void) => void; } type WakeLock = { request: (t: string) => Promise }; let sentinel: SentinelLike | null = null; let visibilityHandler: (() => void) | null = null; let retryTimer: ReturnType | null = null; /** * How often to retry while visible and lock-less. * * `visibilitychange` was the ONLY retry trigger, and a kiosk never changes visibility: the * projector tab is opened once and left alone for eight hours. So a single refusal at startup * was permanent. Refusal is not exotic either — iOS declines Screen Wake Lock outright in Low * Power Mode, which is exactly the state a tablet that has been sitting on a table all * afternoon is in. The symptom is the screen sleeping mid-party and someone having to walk * over and tap it, repeatedly. */ const RETRY_INTERVAL_MS = 30_000; async function request(wakeLock: WakeLock): Promise { try { sentinel = await wakeLock.request('screen'); // The OS auto-releases the lock when the tab is hidden. Without clearing our handle // on that event, the visibility handler's `sentinel === null` guard would never be // true and the lock would never be re-acquired — the screen could then sleep after // the first time the tab lost focus. Null it on release so re-acquire can fire. sentinel.addEventListener?.('release', () => { sentinel = null; }); } catch { // User denied, or already released — nothing useful to do. sentinel = null; } } /** * Acquire the screen wake lock, and keep trying. * * Returns whether the API exists at all, so the caller can tell "the browser cannot do this, * warn the operator" (Firefox, Safari < 16.4, most TV browsers) apart from "asked for, may * still arrive". It deliberately does NOT report whether the first request succeeded — that * answer goes stale immediately, and the retry loop below is what actually matters. */ export async function acquireWakeLock(): Promise { const wakeLock = (navigator as Navigator & { wakeLock?: WakeLock }).wakeLock; if (!wakeLock) return false; await request(wakeLock); // Re-acquire when the page becomes visible again (the OS releases the lock // while the tab is hidden). if (!visibilityHandler) { visibilityHandler = () => { if (document.visibilityState === 'visible' && sentinel === null) { void request(wakeLock); } }; document.addEventListener('visibilitychange', visibilityHandler); } // The kiosk case: visible, no lock, and no visibility change ever coming. Cheap enough to // run all evening — it does nothing at all once a lock is held. if (!retryTimer) { retryTimer = setInterval(() => { if (document.visibilityState === 'visible' && sentinel === null) { void request(wakeLock); } }, RETRY_INTERVAL_MS); } return true; } export async function releaseWakeLock(): Promise { if (sentinel) { try { await sentinel.release(); } catch { // ignore — release after release is fine } sentinel = null; } if (visibilityHandler) { document.removeEventListener('visibilitychange', visibilityHandler); visibilityHandler = null; } if (retryTimer) { clearInterval(retryTimer); retryTimer = null; } }