From 90dd0b58d83b37c08c72d22cc0c945a4d0df4847 Mon Sep 17 00:00:00 2001 From: feederbox826 Date: Mon, 1 Dec 2025 20:57:54 -0500 Subject: [PATCH] add WakeLockSentinel (#6331) * add WakeLockSentinel prevents screen from sleeping ONLY in secure contexts (localhost, https) closes #2884 * format, add types * [wake-sentinel] add more releases, comments release wakelock on dispose and end, call out secure contexts in error message --- ui/v2.5/package.json | 1 + ui/v2.5/pnpm-lock.yaml | 8 +++ .../components/ScenePlayer/ScenePlayer.tsx | 2 + .../components/ScenePlayer/wake-sentinel.ts | 65 +++++++++++++++++++ ui/v2.5/tsconfig.json | 2 +- 5 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 ui/v2.5/src/components/ScenePlayer/wake-sentinel.ts diff --git a/ui/v2.5/package.json b/ui/v2.5/package.json index 0971dbc3a..5913540db 100644 --- a/ui/v2.5/package.json +++ b/ui/v2.5/package.json @@ -92,6 +92,7 @@ "@graphql-codegen/typescript-react-apollo": "^4.1.0", "@types/apollo-upload-client": "^18.0.0", "@types/crypto-js": "^4.2.2", + "@types/dom-screen-wake-lock": "^1.0.3", "@types/lodash-es": "^4.17.6", "@types/mousetrap": "^1.6.11", "@types/node": "^18.13.0", diff --git a/ui/v2.5/pnpm-lock.yaml b/ui/v2.5/pnpm-lock.yaml index 2791682ef..16fef0a19 100644 --- a/ui/v2.5/pnpm-lock.yaml +++ b/ui/v2.5/pnpm-lock.yaml @@ -216,6 +216,9 @@ importers: '@types/crypto-js': specifier: ^4.2.2 version: 4.2.2 + '@types/dom-screen-wake-lock': + specifier: ^1.0.3 + version: 1.0.3 '@types/lodash-es': specifier: ^4.17.6 version: 4.17.12 @@ -1907,6 +1910,9 @@ packages: '@types/crypto-js@4.2.2': resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + '@types/dom-screen-wake-lock@1.0.3': + resolution: {integrity: sha512-3Iten7X3Zgwvk6kh6/NRdwN7WbZ760YgFCsF5AxDifltUQzW1RaW+WRmcVtgwFzLjaNu64H+0MPJ13yRa8g3Dw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -7315,6 +7321,8 @@ snapshots: '@types/crypto-js@4.2.2': {} + '@types/dom-screen-wake-lock@1.0.3': {} + '@types/estree@1.0.8': {} '@types/extract-files@13.0.2': {} diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 77c0d2b19..5aeb56e96 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -23,6 +23,7 @@ import "./big-buttons"; import "./track-activity"; import "./vrmode"; import "./media-session"; +import "./wake-sentinel"; import cx from "classnames"; import { useSceneSaveActivity, @@ -399,6 +400,7 @@ export const ScenePlayer: React.FC = PatchComponent( createButtons: uiConfig?.showAbLoopControls ?? false, }, mediaSession: {}, + wakeSentinel: {}, }, }; diff --git a/ui/v2.5/src/components/ScenePlayer/wake-sentinel.ts b/ui/v2.5/src/components/ScenePlayer/wake-sentinel.ts new file mode 100644 index 000000000..a51c050f5 --- /dev/null +++ b/ui/v2.5/src/components/ScenePlayer/wake-sentinel.ts @@ -0,0 +1,65 @@ +import videojs, { VideoJsPlayer } from "video.js"; + +class WakeSentinelPlugin extends videojs.getPlugin("plugin") { + public wakeLock: WakeLockSentinel | null = null; + public wakeLockFail: boolean = false; + constructor(player: VideoJsPlayer) { + super(player); + + // listen for visibility change events + document.addEventListener("visibilitychange", async () => { + if (document.visibilityState === "visible") { + // reacquire the wake lock when the page becomes visible + await this.acquireWakeLock(); + } + }); + + // acquire wake lock on ready and play + player.ready(async () => { + player.addClass("vjs-wake-sentinel"); + await this.acquireWakeLock(true); + }); + player.on("play", () => this.acquireWakeLock()); + + // release wake lock on pause, dispose and end + player.on("pause", () => this.releaseWakeLock()); + player.on("dispose", () => this.releaseWakeLock()); + player.on("ended", () => this.releaseWakeLock()); + } + + private async releaseWakeLock(): Promise { + this.wakeLock?.release().then(() => (this.wakeLock = null)); + } + + private async acquireWakeLock(log = false): Promise { + // if wake lock failed, don't even try + if (this.wakeLockFail) return; + // check for wake lock on startup + if ("wakeLock" in navigator) { + try { + this.wakeLock = await navigator.wakeLock.request("screen"); + } catch (err) { + if (log) console.error("Failed to obtain Screen Wake Lock:", err); + this.wakeLockFail = true; + } + } else { + if (log) { + console.warn( + "Screen Wake Lock API not supported. Secure context (https or localhost) and modern browser required." + ); + } + this.wakeLockFail = true; + } + } +} + +videojs.registerPlugin("wakeSentinel", WakeSentinelPlugin); + +/* eslint-disable @typescript-eslint/naming-convention */ +declare module "video.js" { + interface VideoJsPlayer { + wakeSentinel: () => WakeSentinelPlugin; + } +} + +export default WakeSentinelPlugin; diff --git a/ui/v2.5/tsconfig.json b/ui/v2.5/tsconfig.json index 93c8c0b5a..4b68980fd 100644 --- a/ui/v2.5/tsconfig.json +++ b/ui/v2.5/tsconfig.json @@ -19,7 +19,7 @@ "isolatedModules": true, "noFallthroughCasesInSwitch": true, "useDefineForClassFields": true, - "types": ["vite/client"] + "types": ["vite/client", "dom-screen-wake-lock"] }, "include": ["src"] }