diff --git a/app/ui.js b/app/ui.js index bbdea60d..cc435efe 100644 --- a/app/ui.js +++ b/app/ui.js @@ -16,6 +16,7 @@ import KeyTable from "../core/input/keysym.js"; import keysyms from "../core/input/keysymdef.js"; import Keyboard from "../core/input/keyboard.js"; import RFB from "../core/rfb.js"; +import WakeLockManager from './wakelock.js'; import * as WebUtil from "./webutil.js"; const PAGE_TITLE = "noVNC"; @@ -46,6 +47,8 @@ const UI = { reconnectCallback: null, reconnectPassword: null, + wakeLockManager: new WakeLockManager(), + async start(options={}) { UI.customSettings = options.settings || {}; if (UI.customSettings.defaults === undefined) { @@ -189,6 +192,7 @@ const UI = { UI.initSetting('repeaterID', ''); UI.initSetting('reconnect', false); UI.initSetting('reconnect_delay', 5000); + UI.initSetting('keep_device_awake', false); }, // Adds a link to the label elements on the corresponding input elements setupSettingLabels() { @@ -371,6 +375,8 @@ const UI = { UI.addSettingChangeHandler('view_only', UI.updateViewOnly); UI.addSettingChangeHandler('show_dot'); UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor); + UI.addSettingChangeHandler('keep_device_awake'); + UI.addSettingChangeHandler('keep_device_awake', UI.updateRequestWakelock); UI.addSettingChangeHandler('host'); UI.addSettingChangeHandler('port'); UI.addSettingChangeHandler('path'); @@ -1072,6 +1078,10 @@ const UI = { url.protocol = (window.location.protocol === "https:") ? 'wss:' : 'ws:'; } + if (UI.getSetting('keep_device_awake')) { + UI.wakeLockManager.acquire(); + } + try { UI.rfb = new RFB(document.getElementById('noVNC_container'), url.href, @@ -1171,6 +1181,7 @@ const UI = { UI.connected = false; UI.rfb = undefined; + UI.wakeLockManager.release(); if (!e.detail.clean) { UI.updateVisualState('disconnected'); @@ -1819,6 +1830,16 @@ const UI = { document.title = e.detail.name + " - " + PAGE_TITLE; }, + updateRequestWakelock() { + if (!UI.rfb) return; + if (UI.getSetting('keep_device_awake')) { + UI.wakeLockManager.acquire(); + } else { + UI.wakeLockManager.release(); + } + }, + + bell(e) { if (UI.getSetting('bell') === 'on') { const promise = document.getElementById('noVNC_bell').play(); diff --git a/app/wakelock.js b/app/wakelock.js new file mode 100644 index 00000000..1dce96c7 --- /dev/null +++ b/app/wakelock.js @@ -0,0 +1,178 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2025 The noVNC authors + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + * + * Wrapper around the `navigator.wakeLock` api that handles reacquiring the + * lock on visiblility changes. + * + * The `acquire` and `release` methods may be called any number of times. The + * most recent call dictates the desired end-state (if `acquire` was most + * recently called, then we will try to acquire and hold the wake lock). + */ + +import * as Log from '../core/util/logging.js'; + +const _STATES = { + /* No wake lock. + * + * Can transition to: + * - AWAITING_VISIBLE: `acquire` called when document is hidden. + * - ACQUIRING: `acquire` called. + * - RELEASED: `acquired` called when the api is not available. + */ + RELEASED: 'released', + /* Wake lock requested, waiting for browser. + * + * Can transition to: + * - ACQUIRED: success + * - ACQUIRING_WANT_RELEASE: `release` called while waiting + * - RELEASED: On error + */ + ACQUIRING: 'acquiring', + /* Wake lock requested, release called, still waiting for browser. + * + * Can transition to: + * - ACQUIRING: `acquire` called (but promise has not resolved yet) + * - RELEASED: success + */ + ACQUIRING_WANT_RELEASE: 'releasing', + /* Wake lock held. + * + * Can transition to: + * - AWAITING_VISIBLE: wakelock lost due to visibility change + * - RELEASED: success + */ + ACQUIRED: 'acquired', + /* Caller wants wakelock, but we can not get it due to visibility. + * + * Can transition to: + * - ACQUIRING: document is now visible, attempting to get wakelock. + * - RELEASED: when release is called. + */ + AWAITING_VISIBLE: 'awaiting_visible', +}; + +export default class WakeLockManager { + constructor() { + this._state = _STATES.RELEASED; + this._wakelock = null; + + this._eventHandlers = { + wakelockAcquired: this._wakelockAcquired.bind(this), + wakelockReleased: this._wakelockReleased.bind(this), + documentVisibilityChange: this._documentVisibilityChange.bind(this), + }; + } + + acquire() { + switch (this._state) { + case _STATES.ACQUIRING_WANT_RELEASE: + // We are currently waiting to acquire the wakelock. While + // waiting, `release()` was called. By transitioning back to + // ACQUIRING, we will keep the lock after we receive it. + this._transitionTo(_STATES.ACQUIRING); + break; + case _STATES.AWAITING_VISIBLE: + case _STATES.ACQUIRING: + case _STATES.ACQUIRED: + break; + case _STATES.RELEASED: + if (document.hidden) { + // We can not acquire the wakelock while the document is + // hidden (eg, not the active tab). Wait until it is + // visible, then acquire the wakelock. + this._awaitVisible(); + break; + } + this._acquireWakelockNow(); + break; + } + } + + release() { + switch (this._state) { + case _STATES.RELEASED: + case _STATES.ACQUIRING_WANT_RELEASE: + break; + case _STATES.ACQUIRING: + // We are have requested (but not yet received) the wakelock. + // Give it up as soon as we acquire it. + this._transitionTo(_STATES.ACQUIRING_WANT_RELEASE); + break; + case _STATES.ACQUIRED: + // We remove the event listener first, as we don't want to be + // notified about this release (it is expected). + this._wakelock.removeEventListener("release", this._eventHandlers.wakelockReleased); + this._wakelock.release(); + this._wakelock = null; + this._transitionTo(_STATES.RELEASED); + break; + case _STATES.AWAITING_VISIBLE: + // We don't currently have the lock, but are waiting for the + // document to become visible. By removing the event listener, + // we will not attempt to get the wakelock in the future. + document.removeEventListener("visibilitychange", this._eventHandlers.documentVisibilityChange); + this._transitionTo(_STATES.RELEASED); + break; + } + } + + _transitionTo(newState) { + let oldState = this._state; + Log.Debug(`WakelockManager transitioning ${oldState} -> ${newState}`); + this._state = newState; + } + + _awaitVisible() { + document.addEventListener("visibilitychange", this._eventHandlers.documentVisibilityChange); + this._transitionTo(_STATES.AWAITING_VISIBLE); + } + + _acquireWakelockNow() { + if (!("wakeLock" in navigator)) { + Log.Warn("Unable to request wakeLock, Browser does not have wakeLock api"); + this._transitionTo(_STATES.RELEASED); + return; + } + navigator.wakeLock.request("screen") + .then(this._eventHandlers.wakelockAcquired) + .catch((err) => { + Log.Warn("Error occurred while acquiring wakelock: " + err); + this._transitionTo(_STATES.RELEASED); + }); + this._transitionTo(_STATES.ACQUIRING); + } + + + _wakelockAcquired(wakelock) { + if (this._state === _STATES.ACQUIRING_WANT_RELEASE) { + // We were requested to release the wakelock while we were trying to + // acquire it. Now that we have acquired it, immediatly release it. + wakelock.release(); + this._transitionTo(_STATES.RELEASED); + return; + } + this._wakelock = wakelock; + this._wakelock.addEventListener("release", this._eventHandlers.wakelockReleased); + this._transitionTo(_STATES.ACQUIRED); + } + + _wakelockReleased(event) { + this._wakelock = null; + if (document.visibilityState === "visible") { + Log.Warn("Lost wakelock, but document is still visible. Not reacquiring"); + this._transitionTo(_STATES.RELEASED); + return; + } + this._awaitVisible(); + } + + _documentVisibilityChange(event) { + if (document.visibilityState !== "visible") { + return; + } + document.removeEventListener("visibilitychange", this._eventHandlers.documentVisibilityChange); + this._acquireWakelockNow(); + } +} diff --git a/docs/EMBEDDING.md b/docs/EMBEDDING.md index 26bcce3f..3019218d 100644 --- a/docs/EMBEDDING.md +++ b/docs/EMBEDDING.md @@ -92,6 +92,10 @@ Currently, the following options are available: * `logging` - The console log level. Can be one of `error`, `warn`, `info` or `debug`. +* `keep_device_awake` - Should we prevent the (local) display from going into + sleep mode while a connection is active? Useful for view-only sessions where + there unlikely to be any keyboard/mouse activity to keep the device active. + ## HTTP serving considerations ### Browser cache issue diff --git a/vnc.html b/vnc.html index 82cacd58..c36a0f07 100644 --- a/vnc.html +++ b/vnc.html @@ -296,6 +296,13 @@ Show dot when no cursor +