From 4825e12f6203bc09a7988f6230f9defaafe7e8d3 Mon Sep 17 00:00:00 2001 From: Matt McClaskey Date: Fri, 26 Jan 2024 11:09:56 -0500 Subject: [PATCH] Bugfix/kasm 5453 serverside fixed resolutions 2 (#90) * fix serverside fixed resolutions * refactor of multi-monitor screen planning * KASM-5488 mouse up fix * Fixed display manager not getting updates on primary scren size, static size fixes * fix phantom screen * fix mouse off on secondary screen when scaled * fix scaling off * fix exception on setting static resolution * completely disable client set static resolution on secondary displays --------- Co-authored-by: mattmcclaskey Co-authored-by: Chris Hunt --- app/ui.js | 23 +++-- app/ui_screen.js | 39 +++++-- core/display.js | 203 +++++++++++++++++++++++-------------- core/rfb.js | 258 ++++++++++++++++++++++++++++++++--------------- vnc.html | 8 ++ 5 files changed, 360 insertions(+), 171 deletions(-) diff --git a/app/ui.js b/app/ui.js index 6dfb13c5..d2a4ad9e 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1745,10 +1745,9 @@ const UI = { if (UI.rfb) { UI.rfb.forcedResolutionX = event.data.value_x; UI.rfb.forcedResolutionY = event.data.value_y; + UI.forceSetting('forced_resolution_x', event.data.value_x, false); + UI.forceSetting('forced_resolution_y', event.data.value_y, false); UI.applyResizeMode(); - UI.rfb.forcedResolutionX = null; - UI.rfb.forcedResolutionY = null; - UI.rfb._resizeSession = UI.getSetting('resize') === 'remote'; } break; case 'set_perf_stats': @@ -1861,13 +1860,24 @@ const UI = { // Apply remote resizing or local scaling applyResizeMode() { if (!UI.rfb) return; - - UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; - UI.rfb.resizeSession = UI.getSetting('resize') === 'remote' || UI.rfb.forcedResolutionX && UI.rfb.forcedResolutionY; + const resize_setting = UI.getSetting('resize'); + UI.rfb.clipViewport = resize_setting !== 'off'; + UI.rfb.scaleViewport = resize_setting === 'scale'; + UI.rfb.resizeSession = resize_setting === 'remote'; UI.rfb.idleDisconnect = UI.getSetting('idle_disconnect'); UI.rfb.videoQuality = UI.getSetting('video_quality'); UI.rfb.enableWebP = UI.getSetting('enable_webp'); UI.rfb.enableHiDpi = UI.getSetting('enable_hidpi'); + + if (UI.rfb.resizeSession) { + UI.rfb.forcedResolutionX = null; + UI.rfb.forcedResolutionY = null; + } else { + UI.rfb.forcedResolutionX = UI.getSetting('forced_resolution_x', false); + UI.rfb.forcedResolutionY = UI.getSetting('forced_resolution_y', false); + } + + UI.rfb.updateConnectionSettings(); }, /* ------^------- @@ -2582,6 +2592,7 @@ const UI = { } else { UI.rfb.enableHiDpi = false; } + UI.applyResizeMode(); } }, diff --git a/app/ui_screen.js b/app/ui_screen.js index 9d0ccfd0..bb08dedc 100644 --- a/app/ui_screen.js +++ b/app/ui_screen.js @@ -142,7 +142,8 @@ const UI = { } } - UI.rfb = new RFB(document.getElementById('noVNC_container'), + if (!UI.rfb) { + UI.rfb = new RFB(document.getElementById('noVNC_container'), document.getElementById('noVNC_keyboardinput'), "", //URL { @@ -153,11 +154,18 @@ const UI = { }, false // Not a primary display ); + } + + UI.rfb.addEventListener("connect", UI.connectFinished); //UI.rfb.addEventListener("disconnect", UI.disconnectFinished); - UI.rfb.clipViewport = UI.getSetting('view_clip'); - UI.rfb.scaleViewport = UI.getSetting('resize', false, 'remote') === 'scale'; - UI.rfb.resizeSession = UI.getSetting('resize', false, 'remote') === 'remote'; + //TODO: add support for forced static resolution for multiple monitors + //UI.rfb.forcedResolutionX = UI.getSetting('forced_resolution_x', false); + //UI.rfb.forcedResolutionY = UI.getSetting('forced_resolution_y', false); + const resize_setting = UI.getSetting('resize', false, 'remote'); + UI.rfb.clipViewport = resize_setting !== 'off'; + UI.rfb.scaleViewport = resize_setting === 'scale'; + UI.rfb.resizeSession = resize_setting === 'remote'; UI.rfb.qualityLevel = parseInt(UI.getSetting('quality')); UI.rfb.dynamicQualityMin = parseInt(UI.getSetting('dynamic_quality_min')); UI.rfb.dynamicQualityMax = parseInt(UI.getSetting('dynamic_quality_max')); @@ -194,13 +202,9 @@ const UI = { } //attach this secondary display to the primary display - if (UI.screenID === null) { - const screen = UI.rfb.attachSecondaryDisplay(details); - UI.screenID = screen.screenID - UI.screen = screen - } else { - UI.rfb.reattachSecondaryDisplay(UI.screen, details); - } + const screen = UI.rfb.attachSecondaryDisplay(details); + UI.screenID = screen.screenID + UI.screen = screen document.querySelector('title').textContent = 'Display ' + UI.screenID @@ -406,6 +410,19 @@ const UI = { return val; }, + // Apply remote resizing or local scaling + applyResizeMode() { + if (!UI.rfb) return; + const resize_setting = UI.getSetting('resize'); + UI.rfb.clipViewport = resize_setting !== 'off'; + UI.rfb.scaleViewport = resize_setting === 'scale'; + UI.rfb.resizeSession = resize_setting === 'remote' || UI.rfb.forcedResolutionX && UI.rfb.forcedResolutionY; + UI.rfb.idleDisconnect = UI.getSetting('idle_disconnect'); + UI.rfb.videoQuality = UI.getSetting('video_quality'); + UI.rfb.enableWebP = UI.getSetting('enable_webp'); + UI.rfb.enableHiDpi = UI.getSetting('enable_hidpi'); + }, + } UI.prime(); diff --git a/core/display.js b/core/display.js index 6ec68b37..3b100378 100644 --- a/core/display.js +++ b/core/display.js @@ -96,15 +96,20 @@ export default class Display { height: this._target.height, //client serverWidth: 0, //calculated serverHeight: 0, //calculated + serverReportedWidth: 0, + serverReportedHeight: 0, x: 0, y: 0, + scale: 1, relativePosition: 0, //left, right, up, down relative to primary display relativePositionX: 0, //offset relative to primary monitor, always 0 for primary relativePositionY: 0, //offset relative to primary monitor, always 0 for primary pixelRatio: window.devicePixelRatio, containerHeight: this._target.parentNode.offsetHeight, containerWidth: this._target.parentNode.offsetWidth, - channel: null + channel: null, + x2: 0, + y2: 0 }]; //optional offscreen canvas @@ -224,75 +229,87 @@ export default class Display { */ getServerRelativeCoordinates(screenIndex, x, y) { if (screenIndex >= 0 && screenIndex < this._screens.length) { - x += this._screens[screenIndex].x; - y += this._screens[screenIndex].y; + x = toSigned32bit(x / this._screens[screenIndex].scale + this._screens[screenIndex].x); + y = toSigned32bit(y / this._screens[screenIndex].scale + this._screens[screenIndex].y); } return [x, y]; } - getScreenSize(resolutionQuality, max_width, max_height, hiDpi, disableLimit) { + getScreenSize(resolutionQuality, max_width, max_height, hiDpi, disableLimit, disableScaling) { let data = { screens: null, serverWidth: 0, serverHeight: 0 } + let i = 0; + //recalculate primary display container size - this._screens[0].containerHeight = this._target.parentNode.offsetHeight; - this._screens[0].containerWidth = this._target.parentNode.offsetWidth; - this._screens[0].pixelRatio = window.devicePixelRatio; - this._screens[0].width = this._target.parentNode.offsetWidth; - this._screens[0].height = this._target.parentNode.offsetHeight; + this._screens[i].containerHeight = this._target.parentNode.offsetHeight; + this._screens[i].containerWidth = this._target.parentNode.offsetWidth; + this._screens[i].pixelRatio = window.devicePixelRatio; + this._screens[i].width = this._target.parentNode.offsetWidth; + this._screens[i].height = this._target.parentNode.offsetHeight; //calculate server-side and client-side resolution of each screen - for (let i=0; i 1280 && !disableLimit && resolutionQuality == 1) { - height = 1280 * (height/width); //keeping the aspect ratio of original resolution, shrink y to match x - width = 1280; - } - //hard coded 720p - else if (resolutionQuality == 0 && !disableLimit) { - width = 1280; - height = 720; - } - //force full resolution on a high DPI monitor where the OS is scaling - else if (hiDpi) { - width = width * this._screens[i].pixelRatio; - height = height * this._screens[i].pixelRatio; - scale = 1 / this._screens[i].pixelRatio; - } - //physically small device with high DPI - else if (this._antiAliasing === 0 && this._screens[i].pixelRatio > 1 && width < 1000 & width > 0) { - Log.Info('Device Pixel ratio: ' + this._screens[i].pixelRatio + ' Reported Resolution: ' + width + 'x' + height); - let targetDevicePixelRatio = 1.5; - if (this._screens[i].pixelRatio > 2) { targetDevicePixelRatio = 2; } - let scaledWidth = (width * this._screens[i].pixelRatio) * (1 / targetDevicePixelRatio); - let scaleRatio = scaledWidth / width; - width = width * scaleRatio; - height = height * scaleRatio; - scale = 1 / scaleRatio; - Log.Info('Small device with hDPI screen detected, auto scaling at ' + scaleRatio + ' to ' + width + 'x' + height); - } - - let clientServerRatioH = this._screens[i].containerHeight / height; - let clientServerRatioW = this._screens[i].containerWidth / width; - - this._screens[i].height = Math.floor(height * clientServerRatioH); - this._screens[i].width = Math.floor(width * clientServerRatioW); - this._screens[i].serverWidth = width; - this._screens[i].serverHeight = height; - this._screens[i].scale = scale; + //max the resolution of a single screen to 1280 + if ( + (this._screens[i].serverReportedWidth > 0 && this._screens[i].serverReportedHeight > 0) && + ( + disableScaling || + (this._screens[i].serverReportedWidth !== this._screens[i].serverWidth || this._screens[i].serverReportedHeight !== this._screens[i].serverHeight) + ) && + (!max_width && !max_height) + ) { + height = this._screens[i].serverReportedHeight; + width = this._screens[i].serverReportedWidth; + } + else if (width > 1280 && !disableLimit && resolutionQuality == 1) { + height = Math.floor(1280 * (height/width)); //keeping the aspect ratio of original resolution, shrink y to match x + width = 1280; + } + //hard coded 720p + else if (resolutionQuality == 0 && !disableLimit) { + width = 1280; + height = 720; + } + //force full resolution on a high DPI monitor where the OS is scaling + else if (hiDpi) { + width = Math.floor(width * this._screens[i].pixelRatio); + height = Math.floor(height * this._screens[i].pixelRatio); + scale = 1 / this._screens[i].pixelRatio; + } + //physically small device with high DPI + else if (this._antiAliasing === 0 && this._screens[i].pixelRatio > 1 && width < 1000 & width > 0) { + Log.Info('Device Pixel ratio: ' + this._screens[i].pixelRatio + ' Reported Resolution: ' + width + 'x' + height); + let targetDevicePixelRatio = 1.5; + if (this._screens[i].pixelRatio > 2) { targetDevicePixelRatio = 2; } + let scaledWidth = (width * this._screens[i].pixelRatio) * (1 / targetDevicePixelRatio); + let scaleRatio = scaledWidth / width; + width = width * scaleRatio; + height = height * scaleRatio; + scale = 1 / scaleRatio; + Log.Info('Small device with hDPI screen detected, auto scaling at ' + scaleRatio + ' to ' + width + 'x' + height); + } + + let clientServerRatioH = this._screens[i].containerHeight / height; + let clientServerRatioW = this._screens[i].containerWidth / width; + + this._screens[i].height = Math.floor(height * clientServerRatioH); + this._screens[i].width = Math.floor(width * clientServerRatioW); + this._screens[i].serverWidth = width; + this._screens[i].serverHeight = height; + this._screens[i].scale = this._screens[i].width / this._screens[i].serverWidth; + + + for (i = 0; i < this._screens.length; i++) { this._screens[i].x2 = this._screens[i].x + this._screens[i].serverWidth; this._screens[i].y2 = this._screens[i].y + this._screens[i].serverHeight; - } - - for (let i = 0; i < this._screens.length; i++) { data.serverWidth = Math.max(data.serverWidth, this._screens[i].x + this._screens[i].serverWidth); data.serverHeight = Math.max(data.serverHeight, this._screens[i].y + this._screens[i].serverHeight); } @@ -302,21 +319,43 @@ export default class Display { return data; } - applyScreenPlan(screenPlan) { - for (let i = 0; i < screenPlan.screens.length; i++) { - for (let z = 0; z < this._screens.length; z++) { - if (screenPlan.screens[i].screenID === this._screens[z].screenID) { - this._screens[z].x = screenPlan.screens[i].x; - this._screens[z].y = screenPlan.screens[i].y; - } + applyServerResolution(width, height, screenIndex) { + for (let z = 0; z < this._screens.length; z++) { + if (screenIndex === this._screens[z].screenIndex) { + this._screens[z].serverReportedWidth = width; + this._screens[z].serverReportedHeight = height; } } } - addScreen(screenID, width, height, pixelRatio, containerHeight, containerWidth) { + applyScreenPlan(screenPlan) { + let changes = false; + for (let i = 0; i < screenPlan.screens.length; i++) { + for (let z = 0; z < this._screens.length; z++) { + if (screenPlan.screens[i].screenID === this._screens[z].screenID) { + if (this._screens[z].x !== screenPlan.screens[i].x || this._screens[z].y !== screenPlan.screens[i].y) { + this._screens[z].x = screenPlan.screens[i].x; + this._screens[z].y = screenPlan.screens[i].y; + changes = true; + } + if (this._screens[z].x2 !== this._screens[z].x + this._screens[z].serverWidth || this._screens[z].y2 !== this._screens[z].y + this._screens[z].serverHeight) { + this._screens[z].x2 = this._screens[z].x + this._screens[z].serverWidth + this._screens[z].y2 = this._screens[z].y + this._screens[z].serverHeight + changes = true; + } + } + } + } + return changes; + } + + addScreen(screenID, width, height, pixelRatio, containerHeight, containerWidth, scale, serverWidth, serverHeight) { if (!this._isPrimaryDisplay) { throw new Error("Cannot add a screen to a secondary display."); } + else if (containerHeight === 0 || containerWidth === 0 || pixelRatio === 0) { + Log.Warn("Invalid screen configuration."); + } let screenIdx = -1; //Does the screen already exist? @@ -329,12 +368,19 @@ export default class Display { if (screenIdx > 0) { //existing screen, update const screen = this._screens[screenIdx]; - screen.width = width; - screen.height = height; - screen.containerHeight = containerHeight; - screen.containerWidth = containerWidth; - screen.pixelRatio = pixelRatio; - + if (screen.serverHeight !== serverHeight || screen.serverWidth !== serverWidth || screen.width !== width || screen.height !== height || screen.containerHeight !== containerHeight || screen.containerWidth !== containerWidth || screen.scale !== screen.scale || screen.pixelRatio !== screen.pixelRatio) { + screen.width = width; + screen.height = height; + screen.containerHeight = containerHeight; + screen.containerWidth = containerWidth; + screen.pixelRatio = pixelRatio; + screen.scale = scale; + screen.serverWidth = serverWidth; + screen.serverHeight = serverHeight; + screen.x2 = screen.x + screen.serverWidth; + screen.y2 = screen.y + screen.serverHeight; + return true; + } } else { //New Screen, add to far right until user repositions it let x = 0; @@ -347,15 +393,19 @@ export default class Display { screenIndex: this.screens.length, width: width, //client height: height, //client - serverWidth: 0, //calculated - serverHeight: 0, //calculated + serverWidth: serverWidth, + serverHeight: serverHeight, + serverReportedWidth: 0, + serverReportedHeight: 0, x: x, y: 0, pixelRatio: pixelRatio, containerHeight: containerHeight, containerWidth: containerWidth, channel: null, - scale: 0 + scale: scale, + x2: x + serverWidth, + y2: serverHeight } new_screen.channel = new BroadcastChannel(`screen_${screenID}_channel`); @@ -363,7 +413,11 @@ export default class Display { this._screens.push(new_screen); new_screen.channel.postMessage({ eventType: "registered", screenIndex: new_screen.screenIndex }); + + return new_screen.screenIndex; } + + return false; } removeScreen(screenID) { @@ -790,7 +844,6 @@ export default class Display { } autoscale(containerWidth, containerHeight, scaleRatio=0) { - if (containerWidth === 0 || containerHeight === 0) { scaleRatio = 0; @@ -814,7 +867,9 @@ export default class Display { _writeCtxBuffer() { //TODO: KASM-5450 Damage tracking with transparent rect overlay support - this._targetCtx.drawImage(this._backbuffer, 0, 0); + if (this._backbuffer.width > 0) { + this._targetCtx.drawImage(this._backbuffer, 0, 0); + } } _handleSecondaryDisplayMessage(event) { @@ -836,7 +891,9 @@ export default class Display { let imageBmpPromise = createImageBitmap(rect.arr); imageBmpPromise.then( function(img) { this._transparentOverlayImg = img; - this.enableCanvasBuffer = true; + if (!this.enableCanvasBuffer) { + this._enableCanvasBuffer = true; + } }.bind(this) ); this._transparentOverlayRect = rect; break; diff --git a/core/rfb.js b/core/rfb.js index e7ee9362..83d63ef2 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -124,7 +124,6 @@ export default class RFB extends EventTargetMixin { this._enabledContinuousUpdates = false; this._supportsSetDesktopSize = false; this._connectionID = window.location.href.split('?')[0].match(/^(.+)(\/)/)[0]; - this._screenIndex = 0; this._screenFlags = 0; this._qemuExtKeyEventSupported = false; @@ -419,20 +418,13 @@ export default class RFB extends EventTargetMixin { get clipViewport() { return this._clipViewport; } set clipViewport(viewport) { this._clipViewport = viewport; - this._updateClip(); } get scaleViewport() { return this._scaleViewport; } set scaleViewport(scale) { - this._scaleViewport = scale; - // Scaling trumps clipping, so we may need to adjust - // clipping when enabling or disabling scaling - if (scale && this._clipViewport) { - this._updateClip(); - } - this._updateScale(); - if (!scale && this._clipViewport) { - this._updateClip(); + if (this._scaleViewport !== scale) { + this._scaleViewport = scale; + this._pendingApplyResolutionChange = true; } } @@ -440,8 +432,8 @@ export default class RFB extends EventTargetMixin { set resizeSession(resize) { this._resizeSession = resize; if (resize) { - this._requestRemoteResize(); this.scaleViewport = true; + this._pendingApplyResolutionChange = true; } } @@ -671,9 +663,20 @@ export default class RFB extends EventTargetMixin { } get forcedResolutionX() { return this._forcedResolutionX; } - set forcedResolutionX(value) {this._forcedResolutionX = value;} + set forcedResolutionX(value) { + if (value !== this._forcedResolutionX) { + this._forcedResolutionX = value; + this._pendingApplyResolutionChange = true; + } + } + get forcedResolutionY() { return this._forcedResolutionY; } - set forcedResolutionY(value) {this._forcedResolutionY = value;} + set forcedResolutionY(value) { + if (value !== this._forcedResolutionY) { + this._forcedResolutionY = value; + this._pendingApplyResolutionChange = true; + } + } get qualityLevel() { return this._qualityLevel; @@ -732,16 +735,20 @@ export default class RFB extends EventTargetMixin { set enableHiDpi(value) { if (value !== this._hiDpi) { this._hiDpi = value; - this._requestRemoteResize(); - if (this._display.screens.length > 1) { - //force secondary displays to re-register and thus apply new hdpi setting - this._proxyRFBMessage('forceResize', [ value ]); - } + this._pendingApplyResolutionChange = true; + this._display.applyServerResolution(0, 0, 0); } } // ===== PUBLIC METHODS ===== + refreshSecondaryDisplays() { + //send secondary displays new settings + if (this._display.screens.length > 1) { + this._proxyRFBMessage('applySettings', [ this._hiDpi, this._clipViewport, this._scaleViewport, this._resizeSession, this._videoQuality, this._forcedResolutionX, this._forcedResolutionY ]); + } + } + attachSecondaryDisplay(details) { this._updateConnectionState('connecting'); const screen = this._registerSecondaryDisplay(false, details); @@ -783,10 +790,23 @@ export default class RFB extends EventTargetMixin { throw new Error("Screen plan contained fewer screens then there are registered.") } - this._display.applyScreenPlan(screenPlan); - const size = this._screenSize(); - RFB.messages.setDesktopSize(this._sock, size, this._screenFlags); - this._updateContinuousUpdates(); + //apply screen plan on primary display + let changes = this._display.applyScreenPlan(screenPlan); + + if (changes) { + //send updates to secondary screens + for (let i = 0; i < screenPlan.screens.length; i++) { + for (let z = 1; z < fullPlan.screens.length; z++) { + if (screenPlan.screens[i].screenID == fullPlan.screens[z].screenID) { + this._proxyRFBMessage('applyScreenPlan', [ fullPlan.screens[z].screenID, fullPlan.screens[z].screenIndex, screenPlan.screens[i].width, screenPlan.screens[i].height, screenPlan.screens[i].x, screenPlan.screens[i].y ]); + } + } + } + + this._pendingApplyResolutionChange = true; + } + + return changes; } } @@ -818,16 +838,33 @@ export default class RFB extends EventTargetMixin { This function must be called after changing any properties that effect rendering quality */ updateConnectionSettings() { - if (this._rfbConnectionState === 'connected') { + if (this._rfbConnectionState === 'connected' && this._isPrimaryDisplay) { if (this._pendingApplyVideoRes) { - if (this._isPrimaryDisplay){ - RFB.messages.setMaxVideoResolution(this._sock, this._maxVideoResolutionX, this._maxVideoResolutionY); - } + RFB.messages.setMaxVideoResolution(this._sock, this._maxVideoResolutionX, this._maxVideoResolutionY); } if (this._pendingApplyResolutionChange) { - this._requestRemoteResize(); + // Scaling trumps clipping, so we may need to adjust + // clipping when enabling or disabling scaling + if (this._scaleViewport && this._clipViewport) { + this._updateClip(); + } + this._updateScale(); + if (!this._scaleViewport && this._clipViewport) { + this._updateClip(); + } + + if (this._display.screens.length > 1) { + this.refreshSecondaryDisplays(); + } + + if (this._resizeSession || (this._forcedResolutionX && this._forcedResolutionY)) { + this._screenSize(); + this.dispatchEvent(new CustomEvent("screenregistered", {})); + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500); + } } if (this._pendingApplyEncodingChanges) { @@ -837,6 +874,20 @@ export default class RFB extends EventTargetMixin { this._pendingApplyVideoRes = false; this._pendingApplyEncodingChanges = false; this._pendingApplyResolutionChange = false; + } else if (!this._isPrimaryDisplay) { + if (this._pendingApplyResolutionChange) { + if (this._scaleViewport && this._clipViewport) { + this._updateClip(); + } + this._updateScale(); + if (!this._scaleViewport && this._clipViewport) { + this._updateClip(); + } + } + + if (this._resizeSession || (this._forcedResolutionX && this._forcedResolutionY)) { + this._requestRemoteResize(); + } } } @@ -851,6 +902,7 @@ export default class RFB extends EventTargetMixin { } else { this._updateConnectionState('disconnecting'); this._unregisterSecondaryDisplay(); + this._rfbConnectionState = ""; } } @@ -1475,10 +1527,13 @@ export default class RFB extends EventTargetMixin { // If the window resized then our screen element might have // as well. Update the viewport dimensions. window.requestAnimationFrame(() => { + this._screenSize(); this._updateClip(); this._updateScale(); }); + this.dispatchEvent(new CustomEvent("screenregistered", { })); + if (this._resizeSession) { // Request changing the resolution of the remote display to // the size of the local browser viewport. @@ -1531,10 +1586,18 @@ export default class RFB extends EventTargetMixin { this._resizeTimeout = null; if (this._isPrimaryDisplay) { - if (!this._resizeSession || this._viewOnly || - !this._supportsSetDesktopSize) { + if ( + (this._viewOnly || !this._supportsSetDesktopSize) || + (!this._resizeSession && !this._forcedResolutionX && !this._forcedResolutionY) + ) { return; } + + //zero out the server reported resolution + for (let i=0; i < this._display.screens.length; i++) { + this._display.applyServerResolution(0, 0, this._display.screens[i].screenIndex); + } + const size = this._screenSize(); RFB.messages.setDesktopSize(this._sock, size, this._screenFlags); @@ -1550,17 +1613,14 @@ export default class RFB extends EventTargetMixin { top: window.screenTop } } - this._registerSecondaryDisplay(false, details); - } - - if (this._display.screens.length > 1) { - this.dispatchEvent(new CustomEvent("screenregistered", {})); + + this._registerSecondaryDisplay(this._display.screens[0], details); } } // Gets the the size of the available screen _screenSize (limited) { - return this._display.getScreenSize(this.videoQuality, this.forcedResolutionX, this.forcedResolutionY, this._hiDpi, limited); + return this._display.getScreenSize(this.videoQuality, this.forcedResolutionX, this.forcedResolutionY, this._hiDpi, limited, !this._resizeSession); } _fixScrollbars() { @@ -1740,22 +1800,22 @@ export default class RFB extends EventTargetMixin { ...event.data.details, screenID: event.data.screenID } - this._display.addScreen(event.data.screenID, event.data.width, event.data.height, event.data.pixelRatio, event.data.containerHeight, event.data.containerWidth); - size = this._screenSize(); - RFB.messages.setDesktopSize(this._sock, size, this._screenFlags); - this._sendEncodings(); - this._updateContinuousUpdates(); + let screenIndex = this._display.addScreen(event.data.screenID, event.data.width, event.data.height, event.data.pixelRatio, event.data.containerHeight, event.data.containerWidth, event.data.scale, event.data.serverWidth, event.data.serverHeight); + this._proxyRFBMessage('screenRegistrationConfirmed', [ this._display.screens[screenIndex].screenID, screenIndex ]); + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500); this.dispatchEvent(new CustomEvent("screenregistered", { detail: details })); Log.Info(`Secondary monitor (${event.data.screenID}) has been registered.`); break; case 'reattach': - this._display.addScreen(event.data.screenID, event.data.width, event.data.height, event.data.pixelRatio, event.data.containerHeight, event.data.containerWidth); - size = this._screenSize(); - RFB.messages.setDesktopSize(this._sock, size, this._screenFlags); - this._sendEncodings(); - this._updateContinuousUpdates(); - this.dispatchEvent(new CustomEvent("screenregistered", {})); - Log.Info(`Secondary monitor (${event.data.screenID}) has been reattached.`); + let changes = this._display.addScreen(event.data.screenID, event.data.width, event.data.height, event.data.pixelRatio, event.data.containerHeight, event.data.containerWidth, event.data.scale, event.data.serverWidth, event.data.serverHeight); + + if (changes) { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500); + this.dispatchEvent(new CustomEvent("screenregistered", {})); + Log.Info(`Secondary monitor (${event.data.screenID}) has been reattached.`); + } break; case 'unregister': if (this._display.removeScreen(event.data.screenID)) { @@ -1799,9 +1859,14 @@ export default class RFB extends EventTargetMixin { coords = this._display.getServerRelativeCoordinates(event.data.screenIndex, event.data.args[0], event.data.args[1]); this._mouseLastScreenIndex = event.data.screenIndex; this._mousePos = { 'x': coords[0], 'y': coords[1] }; - this._mouseButtonMask &= event.data.args[2]; + this._mouseButtonMask &= ~event.data.args[2]; RFB.messages.pointerEvent(this._sock, this._mousePos.x, this._mousePos.y, this._mouseButtonMask); break; + case 'scroll': + coords = this._display.getServerRelativeCoordinates(event.data.screenIndex, event.data.args[0], event.data.args[1]); + this._mouseLastScreenIndex = event.data.screenIndex; + this._mousePos = { 'x': coords[0], 'y': coords[1] }; + RFB.messages.pointerEvent(this._sock, this._mousePos.x, this._mousePos.y, 0, event.data.args[2], event.data.args[3]); case 'keyEvent': RFB.messages.keyEvent(this._sock, ...event.data.args); break; @@ -1831,10 +1896,38 @@ export default class RFB extends EventTargetMixin { this.disconnect(); window.close(); break; - case 'forceResize': - this._hiDpi = event.data.args[0]; - this._updateScale(); - this._requestRemoteResize(); + case 'applySettings': + if (!this._isPrimaryDisplay) { + this.enableHiDpi = event.data.args[0]; + this.clipViewport = event.data.args[1]; + this.scaleViewport = event.data.args[2]; + this.resizeSession = event.data.args[3]; + this.videoQuality = event.data.args[4]; + //TODO: add support for forced static resolution for multiple monitors + //this._forcedResolutionX = event.data.args[5]; + //this._forcedResolutionY = event.data.args[6]; + + //TODO, do we need to do this twice + this.scaleViewport = event.data.args[3]; + this.updateConnectionSettings(); + } + + break; + case 'applyScreenPlan': + if (event.data.args[0] == this._display.screenID) { + this._display.screens[0].screenIndex = event.data.args[1]; + this._display.screens[0].width = event.data.args[2]; + this._display.screens[0].height = event.data.args[3]; + this._display.screens[0].x = event.data.args[4]; + this._display.screens[0].y = event.data.args[5]; + + this.updateConnectionSettings(); + } + break; + case 'screenRegistrationConfirmed': + if (event.data.args[0] == this._display.screenID) { + this._display.screens[0].screenIndex = event.data.args[1]; + } break; } } @@ -1854,23 +1947,25 @@ export default class RFB extends EventTargetMixin { _registerSecondaryDisplay(currentScreen = false, details = null) { if (!this._isPrimaryDisplay) { - //let screen = this._screenSize().screens[0]; - // + const registerType = (currentScreen) ? 'reattach' : 'register' + let size = this._screenSize(); this._display.resize(size.screens[0].serverWidth, size.screens[0].serverHeight); this._display.autoscale(size.screens[0].serverWidth, size.screens[0].serverHeight, size.screens[0].scale); - screen = this._screenSize().screens[0]; - const registertype = (currentScreen) ? 'reattach' : 'register' - + let screen = size.screens[0]; + let message = { - eventType: registertype, + eventType: registerType, screenID: screen.screenID, width: screen.width, height: screen.height, x: currentScreen.x || 0, y: currentScreen.y || 0, pixelRatio: screen.pixelRatio, + scale: screen.scale, + serverWidth: screen.serverWidth, + serverHeight: screen.serverHeight, containerWidth: screen.containerWidth, containerHeight: screen.containerHeight, channel: null, @@ -1941,7 +2036,13 @@ export default class RFB extends EventTargetMixin { //Ensure the window was not moved to a different screen with a different pixel ratio if (this._display.screens[0].pixelRatio !== window.devicePixelRatio) { Log.Debug("Window moved to another screen with different pixel ratio, sending resize request."); - this._requestRemoteResize(); + if (this._isPrimaryDisplay && this._display.screens.length > 1) { + //this.refreshSecondaryDisplays(); + this.dispatchEvent(new CustomEvent("screenregistered", {})); + } else { + this._requestRemoteResize(); + } + } } else { Log.Debug("Mouse left Window"); @@ -2187,6 +2288,7 @@ export default class RFB extends EventTargetMixin { _sendMouse(x, y, mask) { if (this._rfbConnectionState !== 'connected') { return; } if (this._viewOnly) { return; } // View only, skip mouse events + if (!this._isPrimaryDisplay) { return; } if (this._pointerLock && this._pointerRelativeEnabled) { @@ -2194,22 +2296,13 @@ export default class RFB extends EventTargetMixin { var rel_16_x = toSignedRelative16bit(x - this._pointerLockPos.x); var rel_16_y = toSignedRelative16bit(y - this._pointerLockPos.y); - if (this._isPrimaryDisplay){ - RFB.messages.pointerEvent(this._sock, rel_16_x, rel_16_y, mask); - } else { - this._proxyRFBMessage('pointerEvent', [ rel_16_x, rel_16_y, mask ]); - } + RFB.messages.pointerEvent(this._sock, rel_16_x, rel_16_y, mask); // reset the cursor position to center this._mousePos = { x: this._pointerLockPos.x , y: this._pointerLockPos.y }; this._cursor.move(this._pointerLockPos.x, this._pointerLockPos.y); } else { - if (this._isPrimaryDisplay) { - RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), mask); - } else { - this._proxyRFBMessage('pointerEvent', [ this._display.absX(x), this._display.absY(y), mask ]); - } - + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), mask); } } @@ -2218,12 +2311,11 @@ export default class RFB extends EventTargetMixin { if (this._rfbConnectionState !== 'connected') { return; } if (this._viewOnly) { return; } // View only, skip mouse events - if (this._isPrimaryDisplay){ + if (this._isPrimaryDisplay) { RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), 0, dX, dY); } else { - this._proxyRFBMessage('pointerEvent', [ this._display.absX(x), this._display.absY(y), 0, dX, dY ]); + this._proxyRFBMessage('scroll', [ x, y, dX, dY ]); } - } _handleWheel(ev) { @@ -4000,16 +4092,20 @@ export default class RFB extends EventTargetMixin { for (let i = 0; i < numberOfScreens; i += 1) { // Save the id and flags of the first screen - if (i === 0) { - this._screenIndex = this._sock.rQshiftBytes(4); // id - this._sock.rQskipBytes(2); // x-position - this._sock.rQskipBytes(2); // y-position - this._sock.rQskipBytes(2); // width - this._sock.rQskipBytes(2); // height + let sI = this._sock.rQshift32(); // id + let x = this._sock.rQshift16(); // width + let y = this._sock.rQshift16(); // height + let w = this._sock.rQshift16(); // width + let h = this._sock.rQshift16(); // height + if (i == 0) { + this._screenIndex = 0; this._screenFlags = this._sock.rQshiftBytes(4); // flags } else { - this._sock.rQskipBytes(16); + this._sock.rQskipBytes(4); } + + this._display.applyServerResolution(w, h, i); + Log.Debug(`Server reported screen ${sI} with resolution ${w}x${h} at ${x}x${y}`); } /* diff --git a/vnc.html b/vnc.html index 76b438f3..e5b51775 100644 --- a/vnc.html +++ b/vnc.html @@ -426,6 +426,14 @@ +
  • + + +
  • +
  • + + +