Add wakelock support

Add a new configuration option `keep_device_awake` to allow noVNC to
stop the local display from going to sleep. This is especially useful
with view-only sessions.

This new option has been added to the configuration UI, making it easier
for users to configure. When this option is changed at runtime, we will
request/release the wake lock.

We only hold the view lock while connected to a server. We will also
attempt to reacquire the wakelock if we lost it due to a visibility
change (the tab becoming inactive, or during the transition into/from
fullscreen).

All existing unittests have been run, and the change has been manually
tested in Firefox 145. Additional tests will be added later.
This commit is contained in:
Greg Darke
2025-09-12 12:14:43 +10:00
parent d44f7e04fc
commit 8341fdf846
4 changed files with 210 additions and 0 deletions

View File

@@ -16,6 +16,7 @@ import KeyTable from "../core/input/keysym.js";
import keysyms from "../core/input/keysymdef.js"; import keysyms from "../core/input/keysymdef.js";
import Keyboard from "../core/input/keyboard.js"; import Keyboard from "../core/input/keyboard.js";
import RFB from "../core/rfb.js"; import RFB from "../core/rfb.js";
import WakeLockManager from './wakelock.js';
import * as WebUtil from "./webutil.js"; import * as WebUtil from "./webutil.js";
const PAGE_TITLE = "noVNC"; const PAGE_TITLE = "noVNC";
@@ -46,6 +47,8 @@ const UI = {
reconnectCallback: null, reconnectCallback: null,
reconnectPassword: null, reconnectPassword: null,
wakeLockManager: new WakeLockManager(),
async start(options={}) { async start(options={}) {
UI.customSettings = options.settings || {}; UI.customSettings = options.settings || {};
if (UI.customSettings.defaults === undefined) { if (UI.customSettings.defaults === undefined) {
@@ -189,6 +192,7 @@ const UI = {
UI.initSetting('repeaterID', ''); UI.initSetting('repeaterID', '');
UI.initSetting('reconnect', false); UI.initSetting('reconnect', false);
UI.initSetting('reconnect_delay', 5000); UI.initSetting('reconnect_delay', 5000);
UI.initSetting('keep_device_awake', false);
}, },
// Adds a link to the label elements on the corresponding input elements // Adds a link to the label elements on the corresponding input elements
setupSettingLabels() { setupSettingLabels() {
@@ -371,6 +375,8 @@ const UI = {
UI.addSettingChangeHandler('view_only', UI.updateViewOnly); UI.addSettingChangeHandler('view_only', UI.updateViewOnly);
UI.addSettingChangeHandler('show_dot'); UI.addSettingChangeHandler('show_dot');
UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor); UI.addSettingChangeHandler('show_dot', UI.updateShowDotCursor);
UI.addSettingChangeHandler('keep_device_awake');
UI.addSettingChangeHandler('keep_device_awake', UI.updateRequestWakelock);
UI.addSettingChangeHandler('host'); UI.addSettingChangeHandler('host');
UI.addSettingChangeHandler('port'); UI.addSettingChangeHandler('port');
UI.addSettingChangeHandler('path'); UI.addSettingChangeHandler('path');
@@ -1072,6 +1078,10 @@ const UI = {
url.protocol = (window.location.protocol === "https:") ? 'wss:' : 'ws:'; url.protocol = (window.location.protocol === "https:") ? 'wss:' : 'ws:';
} }
if (UI.getSetting('keep_device_awake')) {
UI.wakeLockManager.acquire();
}
try { try {
UI.rfb = new RFB(document.getElementById('noVNC_container'), UI.rfb = new RFB(document.getElementById('noVNC_container'),
url.href, url.href,
@@ -1171,6 +1181,7 @@ const UI = {
UI.connected = false; UI.connected = false;
UI.rfb = undefined; UI.rfb = undefined;
UI.wakeLockManager.release();
if (!e.detail.clean) { if (!e.detail.clean) {
UI.updateVisualState('disconnected'); UI.updateVisualState('disconnected');
@@ -1819,6 +1830,16 @@ const UI = {
document.title = e.detail.name + " - " + PAGE_TITLE; 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) { bell(e) {
if (UI.getSetting('bell') === 'on') { if (UI.getSetting('bell') === 'on') {
const promise = document.getElementById('noVNC_bell').play(); const promise = document.getElementById('noVNC_bell').play();

178
app/wakelock.js Normal file
View File

@@ -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();
}
}

View File

@@ -92,6 +92,10 @@ Currently, the following options are available:
* `logging` - The console log level. Can be one of `error`, `warn`, `info` or * `logging` - The console log level. Can be one of `error`, `warn`, `info` or
`debug`. `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 ## HTTP serving considerations
### Browser cache issue ### Browser cache issue

View File

@@ -296,6 +296,13 @@
Show dot when no cursor Show dot when no cursor
</label> </label>
</li> </li>
<li>
<label>
<input id="noVNC_setting_keep_device_awake" type="checkbox"
class="toggle">
Keep client display awake while connected
</label>
</li>
<li><hr></li> <li><hr></li>
<!-- Logging selection dropdown --> <!-- Logging selection dropdown -->
<li> <li>