main
1/*
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2025 The noVNC authors
4 * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
5 *
6 * Wrapper around the `navigator.wakeLock` api that handles reacquiring the
7 * lock on visiblility changes.
8 *
9 * The `acquire` and `release` methods may be called any number of times. The
10 * most recent call dictates the desired end-state (if `acquire` was most
11 * recently called, then we will try to acquire and hold the wake lock).
12 */
13
14import * as Log from '../core/util/logging.js';
15
16const _STATES = {
17 /* No wake lock.
18 *
19 * Can transition to:
20 * - AWAITING_VISIBLE: `acquire` called when document is hidden.
21 * - ACQUIRING: `acquire` called.
22 * - ERROR: `acquired` called when the api is not available.
23 */
24 RELEASED: 'released',
25 /* Wake lock requested, waiting for browser.
26 *
27 * Can transition to:
28 * - ACQUIRED: success
29 * - ACQUIRING_WANT_RELEASE: `release` called while waiting
30 * - ERROR
31 */
32 ACQUIRING: 'acquiring',
33 /* Wake lock requested, release called, still waiting for browser.
34 *
35 * Can transition to:
36 * - ACQUIRING: `acquire` called (but promise has not resolved yet)
37 * - RELEASED: success
38 */
39 ACQUIRING_WANT_RELEASE: 'releasing',
40 /* Wake lock held.
41 *
42 * Can transition to:
43 * - AWAITING_VISIBLE: wakelock lost due to visibility change
44 * - RELEASED: success
45 */
46 ACQUIRED: 'acquired',
47 /* Caller wants wakelock, but we can not get it due to visibility.
48 *
49 * Can transition to:
50 * - ACQUIRING: document is now visible, attempting to get wakelock.
51 * - RELEASED: when release is called.
52 */
53 AWAITING_VISIBLE: 'awaiting_visible',
54 /* An error has occurred.
55 *
56 * Can transition to:
57 * - RELEASED: will happen immediately.
58 */
59 ERROR: 'error',
60};
61
62class TestOnlyWakeLockManagerStateChangeEvent extends Event {
63 constructor(oldState, newState) {
64 super("testOnlyStateChange");
65 this.oldState = oldState;
66 this.newState = newState;
67 }
68}
69
70export default class WakeLockManager extends EventTarget {
71 constructor() {
72 super();
73
74 this._state = _STATES.RELEASED;
75 this._wakelock = null;
76
77 this._eventHandlers = {
78 wakelockAcquired: this._wakelockAcquired.bind(this),
79 wakelockReleased: this._wakelockReleased.bind(this),
80 documentVisibilityChange: this._documentVisibilityChange.bind(this),
81 };
82 }
83
84 acquire() {
85 switch (this._state) {
86 case _STATES.ACQUIRING_WANT_RELEASE:
87 // We are currently waiting to acquire the wakelock. While
88 // waiting, `release()` was called. By transitioning back to
89 // ACQUIRING, we will keep the lock after we receive it.
90 this._transitionTo(_STATES.ACQUIRING);
91 break;
92 case _STATES.AWAITING_VISIBLE:
93 case _STATES.ACQUIRING:
94 case _STATES.ACQUIRED:
95 break;
96 case _STATES.ERROR:
97 case _STATES.RELEASED:
98 if (document.hidden) {
99 // We can not acquire the wakelock while the document is
100 // hidden (eg, not the active tab). Wait until it is
101 // visible, then acquire the wakelock.
102 this._awaitVisible();
103 break;
104 }
105 this._acquireWakelockNow();
106 break;
107 }
108 }
109
110 release() {
111 switch (this._state) {
112 case _STATES.ERROR:
113 case _STATES.RELEASED:
114 case _STATES.ACQUIRING_WANT_RELEASE:
115 break;
116 case _STATES.ACQUIRING:
117 // We are have requested (but not yet received) the wakelock.
118 // Give it up as soon as we acquire it.
119 this._transitionTo(_STATES.ACQUIRING_WANT_RELEASE);
120 break;
121 case _STATES.ACQUIRED:
122 // We remove the event listener first, as we don't want to be
123 // notified about this release (it is expected).
124 this._wakelock.removeEventListener("release", this._eventHandlers.wakelockReleased);
125 this._wakelock.release();
126 this._wakelock = null;
127 this._transitionTo(_STATES.RELEASED);
128 break;
129 case _STATES.AWAITING_VISIBLE:
130 // We don't currently have the lock, but are waiting for the
131 // document to become visible. By removing the event listener,
132 // we will not attempt to get the wakelock in the future.
133 document.removeEventListener("visibilitychange", this._eventHandlers.documentVisibilityChange);
134 this._transitionTo(_STATES.RELEASED);
135 break;
136 }
137 }
138
139 _transitionTo(newState) {
140 let oldState = this._state;
141 Log.Debug(`WakelockManager transitioning ${oldState} -> ${newState}`);
142 this._state = newState;
143 this.dispatchEvent(new TestOnlyWakeLockManagerStateChangeEvent(oldState, newState));
144 }
145
146 _awaitVisible() {
147 document.addEventListener("visibilitychange", this._eventHandlers.documentVisibilityChange);
148 this._transitionTo(_STATES.AWAITING_VISIBLE);
149 }
150
151 _acquireWakelockNow() {
152 if (!("wakeLock" in navigator)) {
153 Log.Warn("Unable to request wakeLock, Browser does not have wakeLock api");
154 this._transitionTo(_STATES.ERROR);
155 this._transitionTo(_STATES.RELEASED);
156 return;
157 }
158 navigator.wakeLock.request("screen")
159 .then(this._eventHandlers.wakelockAcquired)
160 .catch((err) => {
161 Log.Warn("Error occurred while acquiring wakelock: " + err);
162 this._transitionTo(_STATES.ERROR);
163 this._transitionTo(_STATES.RELEASED);
164 });
165 this._transitionTo(_STATES.ACQUIRING);
166 }
167
168
169 _wakelockAcquired(wakelock) {
170 if (this._state === _STATES.ACQUIRING_WANT_RELEASE) {
171 // We were requested to release the wakelock while we were trying to
172 // acquire it. Now that we have acquired it, immediately release it.
173 wakelock.release();
174 this._transitionTo(_STATES.RELEASED);
175 return;
176 }
177 this._wakelock = wakelock;
178 this._wakelock.addEventListener("release", this._eventHandlers.wakelockReleased);
179 this._transitionTo(_STATES.ACQUIRED);
180 }
181
182 _wakelockReleased(event) {
183 this._wakelock = null;
184 if (document.visibilityState === "visible") {
185 Log.Warn("Lost wakelock, but document is still visible. Not reacquiring");
186 this._transitionTo(_STATES.RELEASED);
187 return;
188 }
189 this._awaitVisible();
190 }
191
192 _documentVisibilityChange(event) {
193 if (document.visibilityState !== "visible") {
194 return;
195 }
196 document.removeEventListener("visibilitychange", this._eventHandlers.documentVisibilityChange);
197 this._acquireWakelockNow();
198 }
199}