feat(web): hold a Screen Wake Lock while reading or listening
Release / build-and-push (push) Successful in 3m12s
Release / build-and-push (push) Successful in 3m12s
New wake-lock.ts engine (sanctioned island, dependency-injected for tests): feature-detected navigator.wakeLock, silent no-op when unsupported or denied, and the spec-mandated force-release-on-hide → re-acquire-on-visible loop handled via the sentinel release event. The player acquires on play and releases on pause (sleep-timer fades ride the same path); the reader holds the lock for the session. Both gate on the keep_screen_awake preference — data-keep-awake on the player root, keepScreenAwake in readerInitExpr. Hidden-tab playback intentionally does not keep the screen awake: the spec forbids screen wake locks for hidden documents.
This commit is contained in:
1 parent
c645b8e207
commit
afbf1029a9
8 files changed
+375
-56
No files matched your search
@@ -0,0 +1,97 @@
|
||||
// Screen Wake Lock manager — sanctioned JS island (engine, not a UI
|
||||
// renderer). Holds the display awake while the reader/player session is
|
||||
// active, gated by the user's keep_screen_awake profile preference
|
||||
// (default on).
|
||||
//
|
||||
// Spec boundaries this module works within:
|
||||
// - The browser force-releases every screen wake lock when the page
|
||||
// hides (tab switch, minimize, screen off). The `release` event
|
||||
// fires; we drop the stale sentinel and re-acquire when the page
|
||||
// becomes visible again while still wanted.
|
||||
// - request() rejects with NotAllowedError while hidden, in power
|
||||
// saver, or on low battery. The lock is best-effort: every failure
|
||||
// path is silent, and visibility regain re-attempts.
|
||||
// - Unsupported browsers (pre-126 Firefox, old iOS) simply never
|
||||
// acquire — no polyfill, no video-element hack.
|
||||
|
||||
export type WakeLockSentinelLike = {
|
||||
release(): Promise<void>;
|
||||
addEventListener(type: "release", listener: () => void): void;
|
||||
};
|
||||
|
||||
export type WakeLockAPILike = {
|
||||
request(type: "screen"): Promise<WakeLockSentinelLike>;
|
||||
};
|
||||
|
||||
/** Minimal document surface used here — injectable for tests. */
|
||||
export type WakeLockDoc = {
|
||||
visibilityState: DocumentVisibilityState;
|
||||
addEventListener(type: "visibilitychange", listener: () => void): void;
|
||||
};
|
||||
|
||||
function defaultAPI(): WakeLockAPILike | null {
|
||||
if (typeof navigator === "undefined" || !("wakeLock" in navigator)) return null;
|
||||
return (navigator as Navigator & { wakeLock?: WakeLockAPILike }).wakeLock ?? null;
|
||||
}
|
||||
|
||||
export class WakeLockManager {
|
||||
private api: WakeLockAPILike | null;
|
||||
private doc: WakeLockDoc;
|
||||
private sentinel: WakeLockSentinelLike | null = null;
|
||||
private wanted = false;
|
||||
|
||||
constructor(api: WakeLockAPILike | null = defaultAPI(), doc: WakeLockDoc = document) {
|
||||
this.api = api;
|
||||
this.doc = doc;
|
||||
doc.addEventListener("visibilitychange", () => this.syncVisibility());
|
||||
}
|
||||
|
||||
/** Whether a lock is currently held (exposed for tests). */
|
||||
get held(): boolean {
|
||||
return this.sentinel !== null;
|
||||
}
|
||||
|
||||
/** Begin wanting the screen awake. Safe to call repeatedly. */
|
||||
async acquire(): Promise<void> {
|
||||
this.wanted = true;
|
||||
if (!this.api || this.sentinel || this.doc.visibilityState !== "visible") return;
|
||||
try {
|
||||
const sentinel = await this.api.request("screen");
|
||||
// A release()/hide can race the async request — drop a lock that
|
||||
// no longer serves the page state it was requested for.
|
||||
if (!this.wanted || this.doc.visibilityState !== "visible") {
|
||||
void sentinel.release().catch(() => {});
|
||||
return;
|
||||
}
|
||||
sentinel.addEventListener("release", () => {
|
||||
// Browser-side release (page hidden, screen off). Drop the
|
||||
// handle; syncVisibility() re-acquires on visible while wanted.
|
||||
this.sentinel = null;
|
||||
});
|
||||
this.sentinel = sentinel;
|
||||
} catch {
|
||||
// NotAllowedError etc — stay silent; syncVisibility() re-attempts
|
||||
// when the page becomes visible again, if still wanted.
|
||||
this.sentinel = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Stop wanting the screen awake. Safe when never acquired. */
|
||||
release(): void {
|
||||
this.wanted = false;
|
||||
const sentinel = this.sentinel;
|
||||
this.sentinel = null;
|
||||
void sentinel?.release().catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-acquire after a browser-side release once the page is visible
|
||||
* again (bound to visibilitychange in the constructor; exposed for
|
||||
* tests to drive directly).
|
||||
*/
|
||||
syncVisibility(): void {
|
||||
if (this.wanted && !this.sentinel && this.doc.visibilityState === "visible") {
|
||||
void this.acquire();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user