128 lines
4.5 KiB
TypeScript
Executable file
128 lines
4.5 KiB
TypeScript
Executable file
// 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<WakeLockSentinel>
|
|
// • 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<void>
|
|
addEventListener: (type: 'release', listener: () => void) => void
|
|
removeEventListener: (type: 'release', listener: () => void) => void
|
|
}
|
|
|
|
interface WakeLockLike {
|
|
request: (type: 'screen') => Promise<WakeLockSentinelLike>
|
|
}
|
|
|
|
/** 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<WakeLockLike>
|
|
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<void> {
|
|
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)
|
|
}
|