diff --git a/app/main.py b/app/main.py index 9e1b2b258..712ee047d 100755 --- a/app/main.py +++ b/app/main.py @@ -92,14 +92,15 @@ def socket_mouse_event(message): mouse_move_event = mouse_event_request.parse_mouse_event(message) except mouse_event_request.Error as e: logger.error('Failed to parse mouse event request: %s', e) - return + return {'success': False} try: fake_mouse.send_mouse_event(mouse_path, mouse_move_event.buttons, mouse_move_event.relative_x, mouse_move_event.relative_y) except hid_write.WriteError as e: logger.error('Failed to forward mouse event: %s', e) - return + return {'success': False} + return {'success': True} @socketio.on('keyRelease') diff --git a/app/static/js/app.js b/app/static/js/app.js index 06e0ca877..08cf9553b 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -109,6 +109,30 @@ function isIgnoredKeystroke(keyCode) { return isModifierKeyCode(keyCode) && isKeycodeAlreadyPressed(keyCode); } +function recalculateMouseEventThrottle( + currentThrottle, + lastRtt, + lastWriteSucceeded +) { + const maxThrottleInMilliseconds = 2000; + if (!lastWriteSucceeded) { + // Apply a 500 ms penalty to the throttle every time an event fails. + return Math.min(currentThrottle + 500, maxThrottleInMilliseconds); + } + // Assume that the server can process messages in roughly half the round trip + // time between an event message and its response. + const roughSendTime = lastRtt / 2; + + // Set the new throttle to a weighted average between the last throttle time + // and the last send time, with a 2/3 bias toward the last send time. + const newThrottle = (roughSendTime * 2 + currentThrottle) / 3; + return Math.min(newThrottle, maxThrottleInMilliseconds); +} + +function unixTime() { + return new Date().getTime(); +} + function browserLanguage() { if (navigator.languages) { return navigator.languages[0]; @@ -190,11 +214,25 @@ function sendMouseEvent(buttons, relativeX, relativeY) { if (!connectedToServer) { return; } - socket.emit("mouse-event", { - buttons, - relativeX, - relativeY, - }); + const remoteScreen = document.getElementById("remote-screen"); + const requestStartTime = unixTime(); + socket.emit( + "mouse-event", + { + buttons, + relativeX, + relativeY, + }, + (response) => { + const requestEndTime = unixTime(); + const requestRtt = requestEndTime - requestStartTime; + remoteScreen.millisecondsBetweenMouseEvents = recalculateMouseEventThrottle( + remoteScreen.millisecondsBetweenMouseEvents, + requestRtt, + response.success + ); + } + ); } function onKeyUp(evt) { diff --git a/app/templates/custom-elements/remote-screen.html b/app/templates/custom-elements/remote-screen.html index e8f89f01e..7ebea4037 100644 --- a/app/templates/custom-elements/remote-screen.html +++ b/app/templates/custom-elements/remote-screen.html @@ -52,7 +52,7 @@ constructor() { super(); this.onWindowResize = this.onWindowResize.bind(this); - // Define this when the _.throttle function is loaded. + this.isUnderscoreLibraryLoaded = false; this.throttledSendMouseEvent = undefined; // Prevent drag on screen for Firefox. @@ -78,15 +78,9 @@ this.shadowRoot .getElementById("underscore-library") .addEventListener("load", () => { - // To avoid overwhelming the connection to the backend or its - // fake HID interface, throttle mouse move events. - const eventsPerSecond = 20; - this.throttledSendMouseEvent = _.throttle( - function (evt) { - this.sendMouseEvent(evt); - }, - /*wait=*/ 1000 / eventsPerSecond, - { trailing: false } + this.isUnderscoreLibraryLoaded = true; + this.throttledSendMouseEvent = this.makeThrottledSendMouseEvent( + this.millisecondsBetweenMouseEvents ); }); @@ -95,11 +89,8 @@ screenImg.addEventListener("mousemove", (evt) => { // Ensure that mouse drags don't attempt to drag the image on the screen. evt.preventDefault(); - if (this.throttledSendMouseEvent) { - this.throttledSendMouseEvent(evt); - } else { - this.sendMouseEvent(evt); - } + + this.throttledSendMouseEvent(evt); }); screenImg.addEventListener("mousedown", this.sendMouseEvent); screenImg.addEventListener("mouseup", this.sendMouseEvent); @@ -125,6 +116,16 @@ this.setAttribute("fullscreen", newValue); } + get millisecondsBetweenMouseEvents() { + return parseInt( + this.getAttribute("milliseconds-between-mouse-events") + ); + } + + set millisecondsBetweenMouseEvents(newValue) { + this.setAttribute("milliseconds-between-mouse-events", newValue); + } + get cursor() { return this.shadowRoot .querySelector(".screen-wrapper") @@ -138,7 +139,7 @@ } static get observedAttributes() { - return ["fullscreen"]; + return ["fullscreen", "milliseconds-between-mouse-events"]; } attributeChangedCallback(name, oldValue, newValue) { @@ -146,6 +147,21 @@ this.shadowRoot .querySelector(".screen-wrapper") .requestFullscreen(); + } else if ( + name === "milliseconds-between-mouse-events" && + oldValue !== newValue + ) { + // Note: This is a bit of a hack. When we replace the throttled + // event, it resets the throttle. To *sort of* preserve the + // throttle, wait to replace it until 90% of the last throttle time + // elapsed. + const newThrottle = parseInt(newValue); + const oldThrottle = parseInt(oldValue) || newThrottle; + setTimeout(() => { + this.throttledSendMouseEvent = this.makeThrottledSendMouseEvent( + newThrottle + ); + }, Math.min(oldThrottle, newThrottle) * 0.9); } } @@ -171,6 +187,19 @@ } } + makeThrottledSendMouseEvent(waitInMilliseconds) { + if (!this.isUnderscoreLibraryLoaded) { + return this.sendMouseEvent; + } + return _.throttle( + function (evt) { + this.sendMouseEvent(evt); + }, + waitInMilliseconds, + { trailing: false } + ); + } + sendMouseEvent(evt) { const boundingRect = evt.target.getBoundingClientRect(); const cursorX = Math.max(0, evt.clientX - boundingRect.left); diff --git a/app/templates/index.html b/app/templates/index.html index 7d30041b6..c4e656677 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -32,7 +32,10 @@

Error Type

- + {% include "components/keyboard-panel.html" %}