main
Raw Download raw file
  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}