// OVE — Screen Wake Lock manager (v7.11). // // Mobile browsers dim and eventually turn the screen off after a few seconds // of inactivity — correct for reading, fatal for a music visualizer propped // on a table. While audio is actually playing (demo / mic / file / link — // anything but idle) the app holds a screen wake lock so the visuals stay // visible; the moment playback stops the lock is released and the phone's // normal inactivity timeout takes over again. // // API notes (developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API): // • navigator.wakeLock.request('screen') → Promise // • The browser SILENTLY releases the sentinel when the page becomes // hidden (app switch / screen lock) — so a 'visibilitychange' listener // re-acquires it when the visitor comes back and music is still going. // • request() rejects when unsupported (older iOS Safari, Firefox Android // pre-126), when the document is hidden, or when a platform setting // refuses — every rejection is swallowed: the lock is a nice-to-have, // never a blocker. // • Wake locks are free — no battery drain beyond the screen itself, and // the page already renders at full tilt while playing. /** Minimal structural type — lib.dom versions vary in Wake Lock coverage. */ interface WakeLockSentinelLike { readonly released: boolean release: () => Promise addEventListener: (type: 'release', listener: () => void) => void removeEventListener: (type: 'release', listener: () => void) => void } interface WakeLockLike { request: (type: 'screen') => Promise } /** The held sentinel (null = not held / browser released it). */ let sentinel: WakeLockSentinelLike | null = null /** Whether a lock is currently WANTED (music playing). */ let wantActive = false /** Guards against overlapping request() calls. */ let requesting = false /** The sentinel's own release listener (kept so it can be detached). */ let releaseListener: (() => void) | null = null function navigatorWakeLock(): WakeLockLike | null { if (typeof navigator === 'undefined') return null const nav = navigator as Navigator & { wakeLock?: unknown } const wl = nav.wakeLock if (!wl || typeof wl !== 'object') return null const typed = wl as Partial return typeof typed.request === 'function' ? (typed as WakeLockLike) : null } /** Drop our reference when the browser auto-releases (page hidden). */ function onSentinelRelease(): void { sentinel = null releaseListener = null } async function acquire(): Promise { if (sentinel !== null || requesting) return const wl = navigatorWakeLock() if (!wl) return requesting = true try { const s = await wl.request('screen') // state may have flipped to "stopped" while the promise was in flight if (!wantActive) { try { await s.release() } catch { /* already gone */ } return } sentinel = s releaseListener = onSentinelRelease s.addEventListener('release', releaseListener) } catch { /* unsupported / hidden / refused — degrade silently */ } finally { requesting = false } } function dropSentinel(): void { const s = sentinel sentinel = null if (s) { if (releaseListener) s.removeEventListener('release', releaseListener) releaseListener = null s.release().catch(() => { /* already released */ }) } } /** * Hold or release the screen wake lock according to playback. Called with * `true` whenever a source starts, `false` whenever the engine goes idle. */ export function syncWakeLock(active: boolean): void { wantActive = active if (!active) { dropSentinel() return } if (typeof document !== 'undefined' && document.visibilityState === 'visible') { void acquire() } // hidden right now (rare — playback needs a gesture, but a queued playlist // could land here): the visibilitychange listener below acquires on return. } /** Explicit teardown (hook unmount / pagehide). */ export function releaseWakeLock(): void { syncWakeLock(false) } /** * Re-acquire the lock when the page becomes visible again while music is * still playing (the browser always drops it on hide). Returns a cleanup. */ export function watchWakeLockVisibility(): () => void { const onVisibility = () => { if (document.visibilityState === 'visible' && wantActive) void acquire() } document.addEventListener('visibilitychange', onVisibility) return () => document.removeEventListener('visibilitychange', onVisibility) }