2022-01-03 18:43:41 +00:00
|
|
|
/**
|
|
|
|
* Copyright 2019 Google LLC
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*global GamepadManager*/
|
|
|
|
/*eslint no-unused-vars: ["error", { "vars": "local" }]*/
|
|
|
|
|
|
|
|
|
|
|
|
class Input {
|
|
|
|
/**
|
|
|
|
* Input handling for WebRTC web app
|
|
|
|
*
|
|
|
|
* @constructor
|
|
|
|
* @param {Element} [element]
|
|
|
|
* Video element to attach events to
|
|
|
|
* @param {function} [send]
|
|
|
|
* Function used to send input events to server.
|
|
|
|
*/
|
|
|
|
constructor(element, send) {
|
|
|
|
/**
|
|
|
|
* @type {Element}
|
|
|
|
*/
|
|
|
|
this.element = element;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {function}
|
|
|
|
*/
|
|
|
|
this.send = send;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {boolean}
|
|
|
|
*/
|
|
|
|
this.mouseRelative = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {Object}
|
|
|
|
*/
|
|
|
|
this.m = null;
|
|
|
|
|
|
|
|
/**
|
2021-12-24 12:26:26 +00:00
|
|
|
* @type {Keyboard}
|
2022-01-03 18:43:41 +00:00
|
|
|
*/
|
|
|
|
this.keyboard = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {GamepadManager}
|
|
|
|
*/
|
|
|
|
this.gamepadManager = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {Integer}
|
|
|
|
*/
|
|
|
|
this.x = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {Integer}
|
|
|
|
*/
|
|
|
|
this.y = 0;
|
|
|
|
|
2022-04-14 17:12:16 +00:00
|
|
|
/**
|
|
|
|
* @type {Integer}
|
|
|
|
*/
|
|
|
|
this.lastTouch = 0;
|
|
|
|
|
2022-01-03 18:43:41 +00:00
|
|
|
/**
|
|
|
|
* @type {function}
|
|
|
|
*/
|
|
|
|
this.ongamepadconnected = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {function}
|
|
|
|
*/
|
|
|
|
this.ongamepaddisconneceted = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* List of attached listeners, record keeping used to detach all.
|
|
|
|
* @type {Array}
|
|
|
|
*/
|
|
|
|
this.listeners = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {function}
|
|
|
|
*/
|
|
|
|
this.onresizeend = null;
|
|
|
|
|
|
|
|
// internal variables used by resize start/end functions.
|
|
|
|
this._rtime = null;
|
|
|
|
this._rtimeout = false;
|
|
|
|
this._rdelta = 200;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles mouse button and motion events and sends them to WebRTC app.
|
|
|
|
* @param {MouseEvent} event
|
|
|
|
*/
|
|
|
|
_mouseButtonMovement(event) {
|
|
|
|
const down = (event.type === 'mousedown' ? 1 : 0);
|
2021-12-24 12:26:26 +00:00
|
|
|
var data = {};
|
2022-01-03 18:43:41 +00:00
|
|
|
|
|
|
|
if (event.type === 'mousemove' && !this.m) return;
|
|
|
|
|
|
|
|
if (!document.pointerLockElement) {
|
|
|
|
if (this.mouseRelative)
|
|
|
|
event.target.requestPointerLock();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hotkey to enable pointer lock, CTRL-SHIFT-LeftButton
|
|
|
|
if (down && event.button === 0 && event.ctrlKey && event.shiftKey) {
|
|
|
|
event.target.requestPointerLock();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (document.pointerLockElement) {
|
2021-12-24 12:26:26 +00:00
|
|
|
// FIXME - mark as relative!
|
|
|
|
console.warn("FIXME: Make event relative!")
|
2022-01-03 18:43:41 +00:00
|
|
|
this.x = event.movementX;
|
|
|
|
this.y = event.movementY;
|
|
|
|
} else if (event.type === 'mousemove') {
|
|
|
|
this.x = this._clientToServerX(event.clientX);
|
|
|
|
this.y = this._clientToServerY(event.clientY);
|
2021-12-24 12:26:26 +00:00
|
|
|
data["event"] = "MouseMove"
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.type === 'mousedown') {
|
|
|
|
data["event"] = "MouseButtonPress";
|
|
|
|
} else if (event.type === 'mouseup') {
|
|
|
|
data["event"] = "MouseButtonRelease";
|
2022-01-03 18:43:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (event.type === 'mousedown' || event.type === 'mouseup') {
|
2022-06-02 21:46:38 +00:00
|
|
|
data["button"] = event.button + 1;
|
2022-01-03 18:43:41 +00:00
|
|
|
}
|
|
|
|
|
2021-12-24 12:26:26 +00:00
|
|
|
data["x"] = this.x;
|
|
|
|
data["y"] = this.y;
|
2022-04-14 17:12:16 +00:00
|
|
|
data["modifier_state"] = this._modifierState(event);
|
2022-01-03 18:43:41 +00:00
|
|
|
|
2022-06-02 22:10:09 +00:00
|
|
|
this.send(data);
|
2022-01-03 18:43:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles touch events and sends them to WebRTC app.
|
|
|
|
* @param {TouchEvent} event
|
|
|
|
*/
|
|
|
|
_touch(event) {
|
2022-04-14 17:12:16 +00:00
|
|
|
var mod_state = this._modifierState(event);
|
|
|
|
|
|
|
|
// Use TouchUp for cancelled touch points
|
|
|
|
if (event.type === 'touchcancel') {
|
|
|
|
let data = {};
|
2022-01-03 18:43:41 +00:00
|
|
|
|
2022-04-14 17:12:16 +00:00
|
|
|
data["event"] = "TouchUp";
|
|
|
|
data["identifier"] = event.changedTouches[0].identifier;
|
|
|
|
data["x"] = this._clientToServerX(event.changedTouches[0].clientX);
|
|
|
|
data["y"] = this._clientToServerY(event.changedTouches[0].clientY);
|
|
|
|
data["modifier_state"] = mod_state;
|
|
|
|
|
2022-06-02 22:10:09 +00:00
|
|
|
this.send(data);
|
2022-04-14 17:12:16 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-01-03 18:43:41 +00:00
|
|
|
if (event.type === 'touchstart') {
|
2022-04-14 17:12:16 +00:00
|
|
|
var event_name = "TouchDown";
|
2022-01-03 18:43:41 +00:00
|
|
|
} else if (event.type === 'touchmove') {
|
2022-04-14 17:12:16 +00:00
|
|
|
var event_name = "TouchMotion";
|
|
|
|
} else if (event.type === 'touchend') {
|
|
|
|
var event_name = "TouchUp";
|
2022-01-03 18:43:41 +00:00
|
|
|
}
|
|
|
|
|
2022-04-14 17:12:16 +00:00
|
|
|
for (let touch of event.changedTouches) {
|
|
|
|
let data = {};
|
|
|
|
|
|
|
|
data["event"] = event_name;
|
|
|
|
data["identifier"] = touch.identifier;
|
|
|
|
data["x"] = this._clientToServerX(touch.clientX);
|
|
|
|
data["y"] = this._clientToServerY(touch.clientY);
|
|
|
|
data["modifier_state"] = mod_state;
|
|
|
|
|
|
|
|
if (event.type !== 'touchend') {
|
|
|
|
if ('force' in touch) {
|
|
|
|
data["pressure"] = touch.force;
|
|
|
|
} else {
|
|
|
|
data["pressure"] = NaN;
|
|
|
|
}
|
|
|
|
}
|
2022-01-03 18:43:41 +00:00
|
|
|
|
2022-06-02 22:10:09 +00:00
|
|
|
this.send(data);
|
2022-04-14 17:12:16 +00:00
|
|
|
}
|
2022-01-03 18:43:41 +00:00
|
|
|
|
2022-04-14 17:12:16 +00:00
|
|
|
if (event.timeStamp > this.lastTouch) {
|
|
|
|
let data = {};
|
|
|
|
|
|
|
|
data["event"] = "TouchFrame";
|
|
|
|
data["modifier_state"] = mod_state;
|
|
|
|
|
2022-06-02 22:10:09 +00:00
|
|
|
this.send(data);
|
2022-04-14 17:12:16 +00:00
|
|
|
this.lastTouch = event.timeStamp;
|
|
|
|
}
|
|
|
|
|
|
|
|
event.preventDefault();
|
2022-01-03 18:43:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles mouse wheel events and sends them to WebRTC app.
|
2021-12-24 12:26:26 +00:00
|
|
|
* @param {MouseEvent} event
|
2022-01-03 18:43:41 +00:00
|
|
|
*/
|
2021-12-24 12:26:26 +00:00
|
|
|
_wheel(event) {
|
|
|
|
let data = {
|
|
|
|
"event": "MouseScroll",
|
|
|
|
"x": this.x,
|
|
|
|
"y": this.y,
|
|
|
|
"delta_x": -event.deltaX,
|
|
|
|
"delta_y": -event.deltaY,
|
2022-04-14 17:12:16 +00:00
|
|
|
"modifier_state": this._modifierState(event),
|
2021-12-24 12:26:26 +00:00
|
|
|
};
|
|
|
|
|
2022-06-02 22:10:09 +00:00
|
|
|
this.send(data);
|
2022-01-03 18:43:41 +00:00
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Captures mouse context menu (right-click) event and prevents event propagation.
|
|
|
|
* @param {MouseEvent} event
|
|
|
|
*/
|
|
|
|
_contextMenu(event) {
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends WebRTC app command to hide the remote pointer when exiting pointer lock.
|
|
|
|
*/
|
|
|
|
_exitPointerLock() {
|
|
|
|
document.exitPointerLock();
|
|
|
|
}
|
|
|
|
|
2022-04-14 17:12:16 +00:00
|
|
|
/**
|
|
|
|
* constructs the string representation for the active modifiers on the event
|
|
|
|
*/
|
|
|
|
_modifierState(event) {
|
|
|
|
let masks = []
|
|
|
|
if (event.altKey) masks.push("alt-mask");
|
|
|
|
if (event.ctrlKey) masks.push("control-mask");
|
|
|
|
if (event.metaKey) masks.push("meta-mask");
|
|
|
|
if (event.shiftKey) masks.push("shift-mask");
|
|
|
|
return masks.join('+')
|
|
|
|
}
|
|
|
|
|
2022-01-03 18:43:41 +00:00
|
|
|
/**
|
|
|
|
* Captures display and video dimensions required for computing mouse pointer position.
|
|
|
|
* This should be fired whenever the window size changes.
|
|
|
|
*/
|
|
|
|
_windowMath() {
|
|
|
|
const windowW = this.element.offsetWidth;
|
|
|
|
const windowH = this.element.offsetHeight;
|
|
|
|
const frameW = this.element.videoWidth;
|
|
|
|
const frameH = this.element.videoHeight;
|
|
|
|
|
|
|
|
const multi = Math.min(windowW / frameW, windowH / frameH);
|
|
|
|
const vpWidth = frameW * multi;
|
|
|
|
const vpHeight = (frameH * multi);
|
|
|
|
|
2021-12-24 12:26:26 +00:00
|
|
|
var elem = this.element;
|
|
|
|
var offsetLeft = 0;
|
|
|
|
var offsetTop = 0;
|
|
|
|
do {
|
|
|
|
if (!isNaN(elem.offsetLeft)) {
|
|
|
|
offsetLeft += elem.offsetLeft;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isNaN(elem.offsetTop)) {
|
|
|
|
offsetTop += elem.offsetTop;
|
|
|
|
}
|
|
|
|
} while (elem = elem.offsetParent);
|
|
|
|
|
2022-01-03 18:43:41 +00:00
|
|
|
this.m = {
|
|
|
|
mouseMultiX: frameW / vpWidth,
|
|
|
|
mouseMultiY: frameH / vpHeight,
|
|
|
|
mouseOffsetX: Math.max((windowW - vpWidth) / 2.0, 0),
|
|
|
|
mouseOffsetY: Math.max((windowH - vpHeight) / 2.0, 0),
|
2021-12-24 12:26:26 +00:00
|
|
|
offsetLeft: offsetLeft,
|
|
|
|
offsetTop: offsetTop,
|
2022-01-03 18:43:41 +00:00
|
|
|
scrollX: window.scrollX,
|
|
|
|
scrollY: window.scrollY,
|
|
|
|
frameW,
|
|
|
|
frameH,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Translates pointer position X based on current window math.
|
|
|
|
* @param {Integer} clientX
|
|
|
|
*/
|
|
|
|
_clientToServerX(clientX) {
|
2021-12-24 12:26:26 +00:00
|
|
|
var serverX = Math.round((clientX - this.m.mouseOffsetX - this.m.offsetLeft + this.m.scrollX) * this.m.mouseMultiX);
|
2022-01-03 18:43:41 +00:00
|
|
|
|
|
|
|
if (serverX === this.m.frameW - 1) serverX = this.m.frameW;
|
|
|
|
if (serverX > this.m.frameW) serverX = this.m.frameW;
|
|
|
|
if (serverX < 0) serverX = 0;
|
|
|
|
|
|
|
|
return serverX;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Translates pointer position Y based on current window math.
|
|
|
|
* @param {Integer} clientY
|
|
|
|
*/
|
|
|
|
_clientToServerY(clientY) {
|
2021-12-24 12:26:26 +00:00
|
|
|
let serverY = Math.round((clientY - this.m.mouseOffsetY - this.m.offsetTop + this.m.scrollY) * this.m.mouseMultiY);
|
2022-01-03 18:43:41 +00:00
|
|
|
|
|
|
|
if (serverY === this.m.frameH - 1) serverY = this.m.frameH;
|
|
|
|
if (serverY > this.m.frameH) serverY = this.m.frameH;
|
|
|
|
if (serverY < 0) serverY = 0;
|
|
|
|
|
|
|
|
return serverY;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When fullscreen is entered, request keyboard and pointer lock.
|
|
|
|
*/
|
|
|
|
_onFullscreenChange() {
|
|
|
|
if (document.fullscreenElement !== null) {
|
|
|
|
// Enter fullscreen
|
|
|
|
this.requestKeyboardLock();
|
|
|
|
this.element.requestPointerLock();
|
|
|
|
}
|
|
|
|
// Reset local keyboard. When holding to exit full-screen the escape key can get stuck.
|
|
|
|
this.keyboard.reset();
|
|
|
|
|
|
|
|
// Reset stuck keys on server side.
|
2021-12-24 12:26:26 +00:00
|
|
|
// FIXME: How to implement resetting keyboard with the GstNavigation interface
|
|
|
|
// this.send("kr");
|
2022-01-03 18:43:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called when window is being resized, used to detect when resize ends so new resolution can be sent.
|
|
|
|
*/
|
|
|
|
_resizeStart() {
|
|
|
|
this._rtime = new Date();
|
|
|
|
if (this._rtimeout === false) {
|
|
|
|
this._rtimeout = true;
|
|
|
|
setTimeout(() => { this._resizeEnd() }, this._rdelta);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Called in setTimeout loop to detect if window is done being resized.
|
|
|
|
*/
|
|
|
|
_resizeEnd() {
|
|
|
|
if (new Date() - this._rtime < this._rdelta) {
|
|
|
|
setTimeout(() => { this._resizeEnd() }, this._rdelta);
|
|
|
|
} else {
|
|
|
|
this._rtimeout = false;
|
|
|
|
if (this.onresizeend !== null) {
|
|
|
|
this.onresizeend();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Attaches input event handles to docuemnt, window and element.
|
|
|
|
*/
|
|
|
|
attach() {
|
|
|
|
this.listeners.push(addListener(this.element, 'resize', this._windowMath, this));
|
2021-12-24 12:26:26 +00:00
|
|
|
this.listeners.push(addListener(this.element, 'wheel', this._wheel, this));
|
2022-01-03 18:43:41 +00:00
|
|
|
this.listeners.push(addListener(this.element, 'contextmenu', this._contextMenu, this));
|
|
|
|
this.listeners.push(addListener(this.element.parentElement, 'fullscreenchange', this._onFullscreenChange, this));
|
|
|
|
this.listeners.push(addListener(window, 'resize', this._windowMath, this));
|
|
|
|
this.listeners.push(addListener(window, 'resize', this._resizeStart, this));
|
|
|
|
|
|
|
|
if ('ontouchstart' in window) {
|
2021-12-24 12:26:26 +00:00
|
|
|
console.warning("FIXME: Enabling mouse pointer display for touch devices.");
|
2022-01-03 18:43:41 +00:00
|
|
|
} else {
|
|
|
|
this.listeners.push(addListener(this.element, 'mousemove', this._mouseButtonMovement, this));
|
|
|
|
this.listeners.push(addListener(this.element, 'mousedown', this._mouseButtonMovement, this));
|
|
|
|
this.listeners.push(addListener(this.element, 'mouseup', this._mouseButtonMovement, this));
|
|
|
|
}
|
|
|
|
|
2022-04-14 17:12:16 +00:00
|
|
|
this.listeners.push(addListener(this.element, 'touchstart', this._touch, this));
|
|
|
|
this.listeners.push(addListener(this.element, 'touchend', this._touch, this));
|
|
|
|
this.listeners.push(addListener(this.element, 'touchmove', this._touch, this));
|
|
|
|
this.listeners.push(addListener(this.element, 'touchcancel', this._touch, this));
|
|
|
|
|
2022-01-03 18:43:41 +00:00
|
|
|
// Adjust for scroll offset
|
|
|
|
this.listeners.push(addListener(window, 'scroll', () => {
|
|
|
|
this.m.scrollX = window.scrollX;
|
|
|
|
this.m.scrollY = window.scrollY;
|
|
|
|
}, this));
|
|
|
|
|
|
|
|
// Using guacamole keyboard because it has the keysym translations.
|
2022-06-02 22:07:00 +00:00
|
|
|
this.keyboard = new Keyboard(this.element);
|
2022-04-14 17:12:16 +00:00
|
|
|
this.keyboard.onkeydown = (keysym, state) => {
|
2022-06-02 22:10:09 +00:00
|
|
|
this.send({"event": "KeyPress", "key": keysym, "modifier_state": state});
|
2022-01-03 18:43:41 +00:00
|
|
|
};
|
2022-04-14 17:12:16 +00:00
|
|
|
this.keyboard.onkeyup = (keysym, state) => {
|
2022-06-02 22:10:09 +00:00
|
|
|
this.send({"event": "KeyRelease", "key": keysym, "modifier_state": state});
|
2022-01-03 18:43:41 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
this._windowMath();
|
|
|
|
}
|
|
|
|
|
|
|
|
detach() {
|
|
|
|
removeListeners(this.listeners);
|
|
|
|
this._exitPointerLock();
|
|
|
|
if (this.keyboard) {
|
|
|
|
this.keyboard.onkeydown = null;
|
|
|
|
this.keyboard.onkeyup = null;
|
|
|
|
this.keyboard.reset();
|
|
|
|
delete this.keyboard;
|
2021-12-24 12:26:26 +00:00
|
|
|
// FIXME: How to implement resetting keyboard with the GstNavigation interface
|
|
|
|
// this.send("kr");
|
2022-01-03 18:43:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Request keyboard lock, must be in fullscreen mode to work.
|
|
|
|
*/
|
|
|
|
requestKeyboardLock() {
|
|
|
|
// event codes: https://www.w3.org/TR/uievents-code/#key-alphanumeric-writing-system
|
|
|
|
const keys = [
|
|
|
|
"AltLeft",
|
|
|
|
"AltRight",
|
|
|
|
"Tab",
|
|
|
|
"Escape",
|
|
|
|
"ContextMenu",
|
|
|
|
"MetaLeft",
|
|
|
|
"MetaRight"
|
|
|
|
];
|
|
|
|
console.log("requesting keyboard lock");
|
|
|
|
navigator.keyboard.lock(keys).then(
|
|
|
|
() => {
|
|
|
|
console.log("keyboard lock success");
|
|
|
|
}
|
|
|
|
).catch(
|
|
|
|
(e) => {
|
|
|
|
console.log("keyboard lock failed: ", e);
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
getWindowResolution() {
|
|
|
|
return [
|
|
|
|
parseInt(this.element.offsetWidth * window.devicePixelRatio),
|
|
|
|
parseInt(this.element.offsetHeight * window.devicePixelRatio)
|
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper function to keep track of attached event listeners.
|
|
|
|
* @param {Object} obj
|
|
|
|
* @param {string} name
|
|
|
|
* @param {function} func
|
|
|
|
* @param {Object} ctx
|
|
|
|
*/
|
|
|
|
function addListener(obj, name, func, ctx) {
|
|
|
|
const newFunc = ctx ? func.bind(ctx) : func;
|
|
|
|
obj.addEventListener(name, newFunc);
|
|
|
|
|
|
|
|
return [obj, name, newFunc];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper function to remove all attached event listeners.
|
|
|
|
* @param {Array} listeners
|
|
|
|
*/
|
|
|
|
function removeListeners(listeners) {
|
|
|
|
for (const listener of listeners)
|
|
|
|
listener[0].removeEventListener(listener[1], listener[2]);
|
|
|
|
}
|