WIP - multi monitor refactor
This commit is contained in:
parent
68135beedd
commit
419a3a70e9
|
@ -24,7 +24,7 @@ For support with KasmVNC, post on the [KasmVNC Project](https://github.com/kasmt
|
|||
- Automatic mixing of webp and jpeg based on CPU availability on server
|
||||
- WebRTC UDP Transit
|
||||
- Lossless QOI Image format for Local LAN
|
||||
- [Dynamic jpeg/webp image coompression](https://github.com/kasmtech/KasmVNC/wiki/Video-Rendering-Options#dynamic-image-quality) quality settings based on screen change rates
|
||||
- [Dynamic jpeg/webp image compression](https://github.com/kasmtech/KasmVNC/wiki/Video-Rendering-Options#dynamic-image-quality) quality settings based on screen change rates
|
||||
- Seemless clipboard support (on Chromium based browsers)
|
||||
- Binary clipboard support for text, images, and formatted text (on Chromium based browsers)
|
||||
- Allow client to set/change most configuration settings
|
||||
|
|
105
app/ui.js
105
app/ui.js
|
@ -31,8 +31,8 @@ window.updateSetting = (name, value) => {
|
|||
}
|
||||
}
|
||||
|
||||
import "core-js/stable";
|
||||
import "regenerator-runtime/runtime";
|
||||
//import "core-js/stable";
|
||||
//import "regenerator-runtime/runtime";
|
||||
import * as Log from '../core/util/logging.js';
|
||||
import _, { l10n } from './localization.js';
|
||||
import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold, supportsBinaryClipboard, isFirefox, isWindows, isIOS, supportsPointerLock }
|
||||
|
@ -69,6 +69,8 @@ const UI = {
|
|||
reconnectCallback: null,
|
||||
reconnectPassword: null,
|
||||
|
||||
supportsBroadcastChannel: (typeof BroadcastChannel !== "undefined"),
|
||||
|
||||
prime() {
|
||||
return WebUtil.initSettings().then(() => {
|
||||
if (document.readyState === "interactive" || document.readyState === "complete") {
|
||||
|
@ -128,14 +130,13 @@ const UI = {
|
|||
UI.addExtraKeysHandlers();
|
||||
UI.addGamingHandlers();
|
||||
UI.addMachineHandlers();
|
||||
UI.addConnectionControlHandlers();
|
||||
UI.addClipboardHandlers();
|
||||
UI.addSettingsHandlers();
|
||||
UI.addMultiMonitorAddHandler();
|
||||
document.getElementById("noVNC_status")
|
||||
.addEventListener('click', UI.hideStatus);
|
||||
UI.openControlbar();
|
||||
|
||||
//
|
||||
|
||||
UI.updateVisualState('init');
|
||||
|
||||
|
@ -472,21 +473,6 @@ const UI = {
|
|||
.addEventListener('click', () => UI.rfb.machineReset());
|
||||
},
|
||||
|
||||
addConnectionControlHandlers() {
|
||||
UI.addClickHandle('noVNC_disconnect_button', UI.disconnect);
|
||||
|
||||
var connect_btn_el = document.getElementById("noVNC_connect_button");
|
||||
if (typeof(connect_btn_el) != 'undefined' && connect_btn_el != null)
|
||||
{
|
||||
connect_btn_el.addEventListener('click', UI.connect);
|
||||
}
|
||||
document.getElementById("noVNC_cancel_reconnect_button")
|
||||
.addEventListener('click', UI.cancelReconnect);
|
||||
|
||||
document.getElementById("noVNC_credentials_button")
|
||||
.addEventListener('click', UI.setCredentials);
|
||||
},
|
||||
|
||||
addClipboardHandlers() {
|
||||
UI.addClickHandle('noVNC_clipboard_button', UI.toggleClipboardPanel);
|
||||
|
||||
|
@ -588,6 +574,13 @@ const UI = {
|
|||
window.addEventListener('msfullscreenchange', UI.updateFullscreenButton);
|
||||
},
|
||||
|
||||
addMultiMonitorAddHandler() {
|
||||
if (UI.supportsBroadcastChannel) {
|
||||
UI.showControlInput("noVNC_addmonitor_button");
|
||||
UI.addClickHandle('noVNC_addmonitor_button', UI.addSecondaryMonitor);
|
||||
}
|
||||
},
|
||||
|
||||
/* ------^-------
|
||||
* /EVENT HANDLERS
|
||||
* ==============
|
||||
|
@ -676,8 +669,6 @@ const UI = {
|
|||
// State change closes dialogs as they may not be relevant
|
||||
// anymore
|
||||
UI.closeAllPanels();
|
||||
document.getElementById('noVNC_credentials_dlg')
|
||||
.classList.remove('noVNC_open');
|
||||
},
|
||||
|
||||
showStats() {
|
||||
|
@ -1380,9 +1371,12 @@ const UI = {
|
|||
UI.rfb = new RFB(document.getElementById('noVNC_container'),
|
||||
document.getElementById('noVNC_keyboardinput'),
|
||||
url,
|
||||
{ shared: UI.getSetting('shared'),
|
||||
{
|
||||
shared: UI.getSetting('shared'),
|
||||
repeaterID: UI.getSetting('repeaterID'),
|
||||
credentials: { password: password } });
|
||||
credentials: { password: password }
|
||||
},
|
||||
true );
|
||||
UI.rfb.addEventListener("connect", UI.connectFinished);
|
||||
UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
|
||||
UI.rfb.addEventListener("credentialsrequired", UI.credentials);
|
||||
|
@ -1746,57 +1740,6 @@ const UI = {
|
|||
parent.postMessage({ action: 'clipboardrx', value: event.detail.text}, '*' ); //TODO fix star
|
||||
},
|
||||
|
||||
/* ------^-------
|
||||
* /CONNECTION
|
||||
* ==============
|
||||
* PASSWORD
|
||||
* ------v------*/
|
||||
|
||||
credentials(e) {
|
||||
// FIXME: handle more types
|
||||
|
||||
document.getElementById("noVNC_username_block").classList.remove("noVNC_hidden");
|
||||
document.getElementById("noVNC_password_block").classList.remove("noVNC_hidden");
|
||||
|
||||
let inputFocus = "none";
|
||||
if (e.detail.types.indexOf("username") === -1) {
|
||||
document.getElementById("noVNC_username_block").classList.add("noVNC_hidden");
|
||||
} else {
|
||||
inputFocus = inputFocus === "none" ? "noVNC_username_input" : inputFocus;
|
||||
}
|
||||
if (e.detail.types.indexOf("password") === -1) {
|
||||
document.getElementById("noVNC_password_block").classList.add("noVNC_hidden");
|
||||
} else {
|
||||
inputFocus = inputFocus === "none" ? "noVNC_password_input" : inputFocus;
|
||||
}
|
||||
document.getElementById('noVNC_credentials_dlg')
|
||||
.classList.add('noVNC_open');
|
||||
|
||||
setTimeout(() => document
|
||||
.getElementById(inputFocus).focus(), 100);
|
||||
|
||||
Log.Warn("Server asked for credentials");
|
||||
UI.showStatus(_("Credentials are required"), "warning");
|
||||
},
|
||||
|
||||
setCredentials(e) {
|
||||
// Prevent actually submitting the form
|
||||
e.preventDefault();
|
||||
|
||||
let inputElemUsername = document.getElementById('noVNC_username_input');
|
||||
const username = inputElemUsername.value;
|
||||
|
||||
let inputElemPassword = document.getElementById('noVNC_password_input');
|
||||
const password = inputElemPassword.value;
|
||||
// Clear the input after reading the password
|
||||
inputElemPassword.value = "";
|
||||
|
||||
UI.rfb.sendCredentials({ username: username, password: password });
|
||||
UI.reconnectPassword = password;
|
||||
document.getElementById('noVNC_credentials_dlg')
|
||||
.classList.remove('noVNC_open');
|
||||
},
|
||||
|
||||
/* ------^-------
|
||||
* /PASSWORD
|
||||
* ==============
|
||||
|
@ -1867,6 +1810,20 @@ const UI = {
|
|||
UI.rfb.enableHiDpi = UI.getSetting('enable_hidpi');
|
||||
},
|
||||
|
||||
/* ------^-------
|
||||
* /MULTI-MONITOR SUPPORT
|
||||
* ==============*/
|
||||
|
||||
addSecondaryMonitor() {
|
||||
let new_display_path = window.location.pathname.replace(/[^/]*$/, '')
|
||||
let new_display_url = `${window.location.protocol}//${window.location.host}${new_display_path}screen.html`;
|
||||
|
||||
Log.Debug(`Opening a secondary display ${new_display_url}`)
|
||||
window.open(new_display_url);
|
||||
},
|
||||
|
||||
|
||||
|
||||
/* ------^-------
|
||||
* /RESIZE
|
||||
* ==============
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
|
||||
|
||||
|
||||
|
||||
const UI = {
|
||||
connected: false,
|
||||
|
||||
//Initial Loading of the UI
|
||||
prime() {
|
||||
|
||||
},
|
||||
|
||||
//Render default UI
|
||||
start() {
|
||||
window.addEventListener("beforeunload", (e) => {
|
||||
if (UI.rfb) {
|
||||
UI.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
UI.addDefaultHandlers();
|
||||
},
|
||||
|
||||
addDefaultHandlers() {
|
||||
document.getElementById('noVNC_connect_button', UI.connect);
|
||||
},
|
||||
|
||||
connect() {
|
||||
|
||||
},
|
||||
|
||||
disconnect() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
UI.prime();
|
||||
|
||||
export default UI;
|
217
core/display.js
217
core/display.js
|
@ -11,6 +11,7 @@ import * as Log from './util/logging.js';
|
|||
import Base64 from "./base64.js";
|
||||
import { toSigned32bit } from './util/int.js';
|
||||
import { isWindows } from './util/browser.js';
|
||||
import { uuidv4 } from './util/strings.js'
|
||||
|
||||
export default class Display {
|
||||
constructor(target) {
|
||||
|
@ -56,7 +57,7 @@ export default class Display {
|
|||
this._targetCtx = this._target.getContext('2d');
|
||||
|
||||
// the visible canvas viewport (i.e. what actually gets seen)
|
||||
this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height };
|
||||
//this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height };
|
||||
|
||||
Log.Debug("User Agent: " + navigator.userAgent);
|
||||
|
||||
|
@ -84,6 +85,21 @@ export default class Display {
|
|||
this._clipViewport = false;
|
||||
this._antiAliasing = 0;
|
||||
this._fps = 0;
|
||||
this._screens = [{
|
||||
screenID: uuidv4(),
|
||||
screenIndex: 0,
|
||||
width: this._target.width, //client
|
||||
height: this._target.height, //client
|
||||
serverWidth: 0, //calculated
|
||||
serverHeight: 0, //calculated
|
||||
x: 0,
|
||||
y: 0,
|
||||
relativePosition: 0,
|
||||
pixelRatio: window.devicePixelRatio,
|
||||
containerHeight: this._target.parentNode.offsetHeight,
|
||||
containerWidth: this._target.parentNode.offsetWidth,
|
||||
channel: null
|
||||
}];
|
||||
|
||||
// ===== EVENT HANDLERS =====
|
||||
|
||||
|
@ -97,6 +113,8 @@ export default class Display {
|
|||
|
||||
// ===== PROPERTIES =====
|
||||
|
||||
get screens() { return this._screens; }
|
||||
|
||||
get antiAliasing() { return this._antiAliasing; }
|
||||
set antiAliasing(value) {
|
||||
this._antiAliasing = value;
|
||||
|
@ -112,8 +130,8 @@ export default class Display {
|
|||
set clipViewport(viewport) {
|
||||
this._clipViewport = viewport;
|
||||
// May need to readjust the viewport dimensions
|
||||
const vp = this._viewportLoc;
|
||||
this.viewportChangeSize(vp.w, vp.h);
|
||||
const vp = this._screens[0];
|
||||
this.viewportChangeSize(vp.width, vp.height);
|
||||
this.viewportChangePos(0, 0);
|
||||
}
|
||||
|
||||
|
@ -136,18 +154,167 @@ export default class Display {
|
|||
|
||||
// ===== PUBLIC METHODS =====
|
||||
|
||||
getScreenSize(resolutionQuality, max_width, max_height, hiDpi, disableLimit) {
|
||||
let data = {
|
||||
screens: null,
|
||||
serverWidth: 0,
|
||||
serverHeight: 0,
|
||||
clientWidth: 0,
|
||||
clientHeight: 0
|
||||
}
|
||||
|
||||
//recalculate primary display container size
|
||||
this._screens[0].containerHeight = this._target.parentNode.offsetHeight;
|
||||
this._screens[0].containerWidth = this._target.parentNode.offsetWidth;
|
||||
|
||||
//calculate server-side resolution of each screen
|
||||
for (let i=0; i<this._screens.length; i++) {
|
||||
let width = max_width || this._screens[i].containerWidth;
|
||||
let height = max_height || this._screens[i].containerHeight;
|
||||
let scale = 0;
|
||||
|
||||
//max the resolution of a single screen to 1280
|
||||
if (width > 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 / x;
|
||||
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);
|
||||
}
|
||||
|
||||
this._screens[i].serverWidth = width;
|
||||
this._screens[i].serverHeight = height;
|
||||
|
||||
//this logic will only support monitors laid out side by side
|
||||
//TODO: two vertically stacked monitors would require light refactoring here
|
||||
data.serverWidth += width;
|
||||
data.serverHeight = Math.max(data.serverHeight, height);
|
||||
data.clientWidth += this._screens[i].width;
|
||||
data.clientHeight = Math.max(data.clientHeight, this._screens[i].height);
|
||||
this._screens[i].scale = scale;
|
||||
}
|
||||
|
||||
//calculate positions of monitors, this logic will only support two monitors side by side in either order
|
||||
if (this._screens.length > 1) {
|
||||
const primary_screen = this._screens[0];
|
||||
const secondary_screen = this._screens[1];
|
||||
switch (this._screens[1].relativePosition) {
|
||||
case 0:
|
||||
//primary screen is to left
|
||||
total_width = secondary_screen.serverWidth + primary_screen.serverWidth;
|
||||
total_height = Math.max(primary_screen.serverHeight, secondary_screen.serverHeight);
|
||||
secondary_screen.x = primary_screen.serverWidth;
|
||||
if (secondary_screen.serverHeight < primary_screen.serverHeight) {
|
||||
if ((total_height - secondary_screen.serverHeight) > 1) {
|
||||
secondary_screen.y = Math.abs(Math.round(((total_height - secondary_screen.serverHeight) / 2)))
|
||||
}
|
||||
} else {
|
||||
secondary_screen.y = 0;
|
||||
if ((total_height - secondary_screen.serverHeight) > 1) {
|
||||
this._screens[0].y = Math.abs(Math.round(((total_height - secondary_screen.serverHeight) / 2)))
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
//primary screen is to right
|
||||
total_width = primary_screen.serverWidth + secondary_screen.serverWidth;
|
||||
total_height = Math.max(primary_screen.serverHeight, secondary_screen.serverHeight);
|
||||
this._screens[0].x = secondary_screen.serverWidth;
|
||||
if (secondary_screen.serverHeight < primary_screen.serverHeight) {
|
||||
if ((total_height - secondary_screen.serverHeight) > 1) {
|
||||
secondary_screen.y = Math.abs(Math.round(((total_height - secondary_screen.serverHeight) / 2)))
|
||||
}
|
||||
} else {
|
||||
secondary_screen.y = 0;
|
||||
if ((total_height - secondary_screen.serverHeight) > 1) {
|
||||
primary_screen.y = Math.abs(Math.round(((total_height - secondary_screen.serverHeight) / 2)))
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
//TODO: It would not be hard to support vertically stacked monitors
|
||||
throw new Error("Unsupported screen orientation.");
|
||||
}
|
||||
}
|
||||
|
||||
data.screens = structuredClone(this._screens);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
addScreen(screenID, width, height, relativePosition, pixelRatio, containerHeight, containerWidth) {
|
||||
//currently only support one secondary screen
|
||||
if (this._screens.length > 1) {
|
||||
this._screens[1].channel.close();
|
||||
this._screens.pop()
|
||||
}
|
||||
screenIdx = this.screens.length;
|
||||
new_screen = {
|
||||
screenID: screenID,
|
||||
screenIndex: screenIdx,
|
||||
width: width, //client
|
||||
height: height, //client
|
||||
serverWidth: 0, //calculated
|
||||
serverHeight: 0, //calculated
|
||||
x: 0,
|
||||
y: 0,
|
||||
relativePosition: relativePosition,
|
||||
pixelRatio: pixelRatio,
|
||||
containerHeight: containerHeight,
|
||||
containerWidth: containerWidth,
|
||||
channel: null,
|
||||
scale: 0
|
||||
}
|
||||
|
||||
new_screen.channel = new BroadcastChannel(`screen_${screenID}_channel`);
|
||||
//new_screen.channel.message = this._handleSecondaryDisplayMessage().bind(this);
|
||||
|
||||
this._screens.push(new_screen);
|
||||
new_screen.channel.postMessage({ eventType: "registered", screenIndex: screenIdx });
|
||||
}
|
||||
|
||||
removeScreen(screenID) {
|
||||
for (let i=1; i<this._screens.length; i++) {
|
||||
if (this._screens[i].screenID == screenID) {
|
||||
this._screens.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
viewportChangePos(deltaX, deltaY) {
|
||||
const vp = this._viewportLoc;
|
||||
const vp = this._screens[0];
|
||||
deltaX = Math.floor(deltaX);
|
||||
deltaY = Math.floor(deltaY);
|
||||
|
||||
if (!this._clipViewport) {
|
||||
deltaX = -vp.w; // clamped later of out of bounds
|
||||
deltaY = -vp.h;
|
||||
deltaX = -vp.width; // clamped later of out of bounds
|
||||
deltaY = -vp.height;
|
||||
}
|
||||
|
||||
const vx2 = vp.x + vp.w - 1;
|
||||
const vy2 = vp.y + vp.h - 1;
|
||||
const vx2 = vp.x + vp.width - 1;
|
||||
const vy2 = vp.y + vp.height - 1;
|
||||
|
||||
// Position change
|
||||
|
||||
|
@ -192,10 +359,10 @@ export default class Display {
|
|||
height = this._fbHeight;
|
||||
}
|
||||
|
||||
const vp = this._viewportLoc;
|
||||
if (vp.w !== width || vp.h !== height) {
|
||||
vp.w = width;
|
||||
vp.h = height;
|
||||
const vp = this._screens[0];
|
||||
if (vp.width !== width || vp.height !== height) {
|
||||
vp.width = width;
|
||||
vp.height = height;
|
||||
|
||||
const canvas = this._target;
|
||||
canvas.width = width;
|
||||
|
@ -213,14 +380,14 @@ export default class Display {
|
|||
if (this._scale === 0) {
|
||||
return 0;
|
||||
}
|
||||
return toSigned32bit(x / this._scale + this._viewportLoc.x);
|
||||
return toSigned32bit(x / this._scale + this._screens[0].x);
|
||||
}
|
||||
|
||||
absY(y) {
|
||||
if (this._scale === 0) {
|
||||
return 0;
|
||||
}
|
||||
return toSigned32bit(y / this._scale + this._viewportLoc.y);
|
||||
return toSigned32bit(y / this._scale + this._screens[0].y);
|
||||
}
|
||||
|
||||
resize(width, height) {
|
||||
|
@ -253,8 +420,8 @@ export default class Display {
|
|||
|
||||
// Readjust the viewport as it may be incorrectly sized
|
||||
// and positioned
|
||||
const vp = this._viewportLoc;
|
||||
this.viewportChangeSize(vp.w, vp.h);
|
||||
const vp = this._screens[0];
|
||||
this.viewportChangeSize(vp.width, vp.height);
|
||||
this.viewportChangePos(0, 0);
|
||||
}
|
||||
|
||||
|
@ -464,14 +631,14 @@ export default class Display {
|
|||
|
||||
} else if (scaleRatio === 0) {
|
||||
|
||||
const vp = this._viewportLoc;
|
||||
const vp = this._screens[0];
|
||||
const targetAspectRatio = containerWidth / containerHeight;
|
||||
const fbAspectRatio = vp.w / vp.h;
|
||||
const fbAspectRatio = vp.width / vp.height;
|
||||
|
||||
if (fbAspectRatio >= targetAspectRatio) {
|
||||
scaleRatio = containerWidth / vp.w;
|
||||
scaleRatio = containerWidth / vp.width;
|
||||
} else {
|
||||
scaleRatio = containerHeight / vp.h;
|
||||
scaleRatio = containerHeight / vp.height;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -658,14 +825,14 @@ export default class Display {
|
|||
|
||||
_rescale(factor) {
|
||||
this._scale = factor;
|
||||
const vp = this._viewportLoc;
|
||||
const vp = this._screens[0];
|
||||
|
||||
// NB(directxman12): If you set the width directly, or set the
|
||||
// style width to a number, the canvas is cleared.
|
||||
// However, if you set the style width to a string
|
||||
// ('NNNpx'), the canvas is scaled without clearing.
|
||||
const width = factor * vp.w + 'px';
|
||||
const height = factor * vp.h + 'px';
|
||||
const width = factor * vp.width + 'px';
|
||||
const height = factor * vp.height + 'px';
|
||||
|
||||
if ((this._target.style.width !== width) ||
|
||||
(this._target.style.height !== height)) {
|
||||
|
@ -673,12 +840,12 @@ export default class Display {
|
|||
this._target.style.height = height;
|
||||
}
|
||||
|
||||
Log.Info('Pixel Ratio: ' + window.devicePixelRatio + ', VNC Scale: ' + factor + 'VNC Res: ' + vp.w + 'x' + vp.h);
|
||||
Log.Info('Pixel Ratio: ' + window.devicePixelRatio + ', VNC Scale: ' + factor + 'VNC Res: ' + vp.width + 'x' + vp.height);
|
||||
|
||||
var pixR = Math.abs(Math.ceil(window.devicePixelRatio));
|
||||
var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
|
||||
if (this.antiAliasing === 2 || (this.antiAliasing === 0 && factor === 1 && this._target.style.imageRendering !== 'pixelated' && pixR === window.devicePixelRatio && vp.w > 0)) {
|
||||
if (this.antiAliasing === 2 || (this.antiAliasing === 0 && factor === 1 && this._target.style.imageRendering !== 'pixelated' && pixR === window.devicePixelRatio && vp.width > 0)) {
|
||||
this._target.style.imageRendering = ((!isFirefox) ? 'pixelated' : 'crisp-edges' );
|
||||
Log.Debug('Smoothing disabled');
|
||||
} else if (this.antiAliasing === 1 || (this.antiAliasing === 0 && factor !== 1 && this._target.style.imageRendering !== 'auto')) {
|
||||
|
|
329
core/rfb.js
329
core/rfb.js
|
@ -10,7 +10,7 @@
|
|||
|
||||
import { toUnsigned32bit, toSigned32bit } from './util/int.js';
|
||||
import * as Log from './util/logging.js';
|
||||
import { encodeUTF8, decodeUTF8 } from './util/strings.js';
|
||||
import { encodeUTF8, decodeUTF8, uuidv4 } from './util/strings.js';
|
||||
import { hashUInt8Array } from './util/int.js';
|
||||
import { dragThreshold, supportsCursorURIs, isTouchDevice, isWindows, isMac, isIOS } from './util/browser.js';
|
||||
import { clientToElement } from './util/element.js';
|
||||
|
@ -76,7 +76,7 @@ const extendedClipboardActionNotify = 1 << 27;
|
|||
const extendedClipboardActionProvide = 1 << 28;
|
||||
|
||||
export default class RFB extends EventTargetMixin {
|
||||
constructor(target, touchInput, urlOrChannel, options) {
|
||||
constructor(target, touchInput, urlOrChannel, options, isPrimaryDisplay) {
|
||||
if (!target) {
|
||||
throw new Error("Must specify target");
|
||||
}
|
||||
|
@ -101,6 +101,7 @@ export default class RFB extends EventTargetMixin {
|
|||
this._shared = 'shared' in options ? !!options.shared : true;
|
||||
this._repeaterID = options.repeaterID || '';
|
||||
this._wsProtocols = options.wsProtocols || ['binary'];
|
||||
this._isPrimaryDisplay = (isPrimaryDisplay !== false);
|
||||
|
||||
// Internal state
|
||||
this._rfbConnectionState = '';
|
||||
|
@ -122,7 +123,8 @@ export default class RFB extends EventTargetMixin {
|
|||
this._supportsContinuousUpdates = false;
|
||||
this._enabledContinuousUpdates = false;
|
||||
this._supportsSetDesktopSize = false;
|
||||
this._screenID = 0;
|
||||
this._screenID = uuidv4();
|
||||
this._screenIndex = 0;
|
||||
this._screenFlags = 0;
|
||||
this._qemuExtKeyEventSupported = false;
|
||||
|
||||
|
@ -212,6 +214,19 @@ export default class RFB extends EventTargetMixin {
|
|||
this._gestureLastMagnitudeX = 0;
|
||||
this._gestureLastMagnitudeY = 0;
|
||||
|
||||
// Secondary Displays
|
||||
this._secondaryDisplays = {};
|
||||
this._supportsBroadcastChannel = (typeof BroadcastChannel !== "undefined");
|
||||
if (this._supportsBroadcastChannel) {
|
||||
this._controlChannel = new BroadcastChannel("registrationChannel");
|
||||
this._controlChannel.message = this._handleControlMessage.bind(this);
|
||||
Log.Debug("Attached to registrationChannel for secondary displays.")
|
||||
|
||||
}
|
||||
if (!this._isPrimaryDisplay) {
|
||||
this._screenIndex = 2;
|
||||
}
|
||||
|
||||
// Bound event handlers
|
||||
this._eventHandlers = {
|
||||
updateHiddenKeyboard: this._updateHiddenKeyboard.bind(this),
|
||||
|
@ -283,68 +298,13 @@ export default class RFB extends EventTargetMixin {
|
|||
|
||||
this._gestures = new GestureHandler();
|
||||
|
||||
this._sock = new Websock();
|
||||
this._sock.on('message', () => {
|
||||
this._handleMessage();
|
||||
});
|
||||
this._sock.on('open', () => {
|
||||
if ((this._rfbConnectionState === 'connecting') &&
|
||||
(this._rfbInitState === '')) {
|
||||
this._rfbInitState = 'ProtocolVersion';
|
||||
Log.Debug("Starting VNC handshake");
|
||||
} else {
|
||||
this._fail("Unexpected server connection while " +
|
||||
this._rfbConnectionState);
|
||||
if (this._isPrimaryDisplay) {
|
||||
this._setupWebSocket();
|
||||
}
|
||||
});
|
||||
this._sock.on('close', (e) => {
|
||||
Log.Debug("WebSocket on-close event");
|
||||
let msg = "";
|
||||
if (e.code) {
|
||||
msg = "(code: " + e.code;
|
||||
if (e.reason) {
|
||||
msg += ", reason: " + e.reason;
|
||||
}
|
||||
msg += ")";
|
||||
}
|
||||
switch (this._rfbConnectionState) {
|
||||
case 'connecting':
|
||||
this._fail("Connection closed " + msg);
|
||||
break;
|
||||
case 'connected':
|
||||
// Handle disconnects that were initiated server-side
|
||||
this._updateConnectionState('disconnecting');
|
||||
this._updateConnectionState('disconnected');
|
||||
break;
|
||||
case 'disconnecting':
|
||||
// Normal disconnection path
|
||||
this._updateConnectionState('disconnected');
|
||||
break;
|
||||
case 'disconnected':
|
||||
this._fail("Unexpected server disconnect " +
|
||||
"when already disconnected " + msg);
|
||||
break;
|
||||
default:
|
||||
this._fail("Unexpected server disconnect before connecting " +
|
||||
msg);
|
||||
break;
|
||||
}
|
||||
this._sock.off('close');
|
||||
// Delete reference to raw channel to allow cleanup.
|
||||
this._rawChannel = null;
|
||||
});
|
||||
this._sock.on('error', e => Log.Warn("WebSocket on-error event"));
|
||||
|
||||
// Slight delay of the actual connection so that the caller has
|
||||
// time to set up callbacks
|
||||
setTimeout(this._updateConnectionState.bind(this, 'connecting'));
|
||||
|
||||
Log.Debug("<< RFB.constructor");
|
||||
|
||||
// ===== PROPERTIES =====
|
||||
|
||||
|
||||
|
||||
this.dragViewport = false;
|
||||
this.focusOnClick = true;
|
||||
this.lastActiveAt = Date.now();
|
||||
|
@ -774,8 +734,10 @@ export default class RFB extends EventTargetMixin {
|
|||
if (this._rfbConnectionState === 'connected') {
|
||||
|
||||
if (this._pendingApplyVideoRes) {
|
||||
if (this._isPrimaryDisplay){
|
||||
RFB.messages.setMaxVideoResolution(this._sock, this._maxVideoResolutionX, this._maxVideoResolutionY);
|
||||
}
|
||||
}
|
||||
|
||||
if (this._pendingApplyResolutionChange) {
|
||||
this._requestRemoteResize();
|
||||
|
@ -793,15 +755,12 @@ export default class RFB extends EventTargetMixin {
|
|||
}
|
||||
|
||||
disconnect() {
|
||||
if (this._rfbConnectionState !== 'proxied') {
|
||||
this._updateConnectionState('disconnecting');
|
||||
this._sock.off('error');
|
||||
this._sock.off('message');
|
||||
this._sock.off('open');
|
||||
}
|
||||
|
||||
sendCredentials(creds) {
|
||||
this._rfbCredentials = creds;
|
||||
setTimeout(this._initMsg.bind(this), 0);
|
||||
}
|
||||
|
||||
sendCtrlAltDel() {
|
||||
|
@ -851,13 +810,21 @@ export default class RFB extends EventTargetMixin {
|
|||
|
||||
Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode);
|
||||
|
||||
RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);
|
||||
if (this._isPrimaryDisplay) {
|
||||
RFB.messages.QEMUExtendedKeyEvent(this._sock, [ keysym, down, scancode]);
|
||||
} else {
|
||||
this._proxyRFBMessage('QEMUExtendedKeyEvent', [ keysym, down, scancode ])
|
||||
}
|
||||
} else {
|
||||
if (!keysym) {
|
||||
return;
|
||||
}
|
||||
Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);
|
||||
if (this._isPrimaryDisplay) {
|
||||
RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
|
||||
} else {
|
||||
this._proxyRFBMessage('keyEvent', [ keysym, down ? 1 : 0 ])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -909,7 +876,12 @@ export default class RFB extends EventTargetMixin {
|
|||
let mimes = [ 'text/plain' ];
|
||||
dataset.push(data);
|
||||
|
||||
if (this._isPrimaryDisplay) {
|
||||
RFB.messages.sendBinaryClipboard(this._sock, dataset, mimes);
|
||||
} else {
|
||||
this._proxyRFBMessage('sendBinaryClipboard', [ dataset, mimes ]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async clipboardPasteDataFrom(clipdata) {
|
||||
|
@ -973,24 +945,34 @@ export default class RFB extends EventTargetMixin {
|
|||
|
||||
|
||||
if (dataset.length > 0) {
|
||||
if (this._isPrimaryDisplay) {
|
||||
RFB.messages.sendBinaryClipboard(this._sock, dataset, mimes);
|
||||
} else {
|
||||
this._proxyRFBMessage('sendBinaryClipboard', [ dataset, mimes ]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
requestBottleneckStats() {
|
||||
if (this._isPrimaryDisplay) {
|
||||
RFB.messages.requestStats(this._sock);
|
||||
}
|
||||
}
|
||||
|
||||
subscribeUnixRelay(name, processRelayFn) {
|
||||
if (this._isPrimaryDisplay){
|
||||
this._unixRelays = this._unixRelays || {};
|
||||
this._unixRelays[name] = processRelayFn;
|
||||
RFB.messages.sendSubscribeUnixRelay(this._sock, name);
|
||||
}
|
||||
}
|
||||
|
||||
sendUnixRelayData(name, payload) {
|
||||
if (this._isPrimaryDisplay) {
|
||||
RFB.messages.sendUnixRelay(this._sock, name, payload);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== PRIVATE METHODS =====
|
||||
|
||||
|
@ -1003,10 +985,68 @@ export default class RFB extends EventTargetMixin {
|
|||
this._transitConnectionState = value;
|
||||
}
|
||||
|
||||
_setupWebSocket() {
|
||||
this._sock = new Websock();
|
||||
this._sock.on('message', () => {
|
||||
this._handleMessage();
|
||||
});
|
||||
this._sock.on('open', () => {
|
||||
if ((this._rfbConnectionState === 'connecting') &&
|
||||
(this._rfbInitState === '')) {
|
||||
this._rfbInitState = 'ProtocolVersion';
|
||||
Log.Debug("Starting VNC handshake");
|
||||
} else {
|
||||
this._fail("Unexpected server connection while " +
|
||||
this._rfbConnectionState);
|
||||
}
|
||||
});
|
||||
this._sock.on('close', (e) => {
|
||||
Log.Debug("WebSocket on-close event");
|
||||
let msg = "";
|
||||
if (e.code) {
|
||||
msg = "(code: " + e.code;
|
||||
if (e.reason) {
|
||||
msg += ", reason: " + e.reason;
|
||||
}
|
||||
msg += ")";
|
||||
}
|
||||
switch (this._rfbConnectionState) {
|
||||
case 'connecting':
|
||||
this._fail("Connection closed " + msg);
|
||||
break;
|
||||
case 'connected':
|
||||
// Handle disconnects that were initiated server-side
|
||||
this._updateConnectionState('disconnecting');
|
||||
this._updateConnectionState('disconnected');
|
||||
break;
|
||||
case 'disconnecting':
|
||||
// Normal disconnection path
|
||||
this._updateConnectionState('disconnected');
|
||||
break;
|
||||
case 'disconnected':
|
||||
this._fail("Unexpected server disconnect " +
|
||||
"when already disconnected " + msg);
|
||||
break;
|
||||
default:
|
||||
this._fail("Unexpected server disconnect before connecting " +
|
||||
msg);
|
||||
break;
|
||||
}
|
||||
this._sock.off('close');
|
||||
// Delete reference to raw channel to allow cleanup.
|
||||
this._rawChannel = null;
|
||||
});
|
||||
this._sock.on('error', e => Log.Warn("WebSocket on-error event"));
|
||||
|
||||
// Slight delay of the actual connection so that the caller has
|
||||
// time to set up callbacks
|
||||
setTimeout(this._updateConnectionState.bind(this, 'connecting'));
|
||||
}
|
||||
|
||||
_connect() {
|
||||
Log.Debug(">> RFB.connect");
|
||||
|
||||
if (this._url) {
|
||||
if (this._url && this._isPrimaryDisplay) {
|
||||
try {
|
||||
Log.Info(`connecting to ${this._url}`);
|
||||
this._sock.open(this._url, this._wsProtocols);
|
||||
|
@ -1018,13 +1058,15 @@ export default class RFB extends EventTargetMixin {
|
|||
this._fail("Error when opening socket (" + e + ")");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
} else if (this._isPrimaryDisplay) {
|
||||
try {
|
||||
Log.Info(`attaching ${this._rawChannel} to Websock`);
|
||||
this._sock.attach(this._rawChannel);
|
||||
} catch (e) {
|
||||
this._fail("Error attaching channel (" + e + ")");
|
||||
}
|
||||
} else {
|
||||
this._registerSecondaryDisplay();
|
||||
}
|
||||
|
||||
// Make our elements part of the page
|
||||
|
@ -1085,7 +1127,7 @@ export default class RFB extends EventTargetMixin {
|
|||
this._resendClipboardNextUserDrivenEvent = true;
|
||||
|
||||
// WebRTC UDP datachannel inits
|
||||
if (typeof RTCPeerConnection !== 'undefined') {
|
||||
if (typeof RTCPeerConnection !== 'undefined' && this._isPrimaryDisplay) {
|
||||
this._udpBuffer = new Map();
|
||||
|
||||
this._udpPeer = new RTCPeerConnection({
|
||||
|
@ -1190,7 +1232,7 @@ export default class RFB extends EventTargetMixin {
|
|||
}
|
||||
}
|
||||
|
||||
if (this._useUdp && typeof RTCPeerConnection !== 'undefined') {
|
||||
if (this._useUdp && typeof RTCPeerConnection !== 'undefined' && this._isPrimaryDisplay) {
|
||||
setTimeout(function() { this._sendUdpUpgrade() }.bind(this), 3000);
|
||||
}
|
||||
|
||||
|
@ -1224,7 +1266,17 @@ export default class RFB extends EventTargetMixin {
|
|||
window.removeEventListener('focus', this._eventHandlers.handleFocusChange);
|
||||
this._keyboard.ungrab();
|
||||
this._gestures.detach();
|
||||
if (this._isPrimaryDisplay) {
|
||||
this._sock.close();
|
||||
} else {
|
||||
if (this._primaryDisplayChannel) {
|
||||
this._primaryDisplayChannel.postMessage({eventType: 'unregister', screenID: this._screenID})
|
||||
this._primaryDisplayChannel.removeEventListener('message', this._handleSecondaryDisplayMessage);
|
||||
this._primaryDisplayChannel.close();
|
||||
this._primaryDisplayChannel = null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
this._target.removeChild(this._screen);
|
||||
} catch (e) {
|
||||
|
@ -1325,7 +1377,7 @@ export default class RFB extends EventTargetMixin {
|
|||
// When clipping is enabled, the screen is limited to
|
||||
// the size of the container.
|
||||
const size = this._screenSize();
|
||||
this._display.viewportChangeSize(size.w, size.h);
|
||||
this._display.viewportChangeSize(size.screens[0].serverWidth, size.screens[0].serverHeight);
|
||||
this._fixScrollbars();
|
||||
}
|
||||
}
|
||||
|
@ -1335,7 +1387,7 @@ export default class RFB extends EventTargetMixin {
|
|||
this._display.scale = 1.0;
|
||||
} else {
|
||||
const size = this._screenSize(false);
|
||||
this._display.autoscale(size.w, size.h, size.scale);
|
||||
this._display.autoscale(size.screens[0].containerWidth, size.screens[0].containerHeight, size.screens[0].scale);
|
||||
}
|
||||
this._fixScrollbars();
|
||||
}
|
||||
|
@ -1351,16 +1403,16 @@ export default class RFB extends EventTargetMixin {
|
|||
return;
|
||||
}
|
||||
const size = this._screenSize();
|
||||
RFB.messages.setDesktopSize(this._sock,
|
||||
Math.floor(size.w), Math.floor(size.h),
|
||||
this._screenID, this._screenFlags);
|
||||
RFB.messages.setDesktopSize(this._sock, size, this._screenFlags);
|
||||
|
||||
Log.Debug('Requested new desktop size: ' +
|
||||
size.w + 'x' + size.h);
|
||||
size.serverWidth + 'x' + size.serverHeight);
|
||||
}
|
||||
|
||||
// Gets the the size of the available screen
|
||||
_screenSize (limited) {
|
||||
return this._display.getScreenSize(this.videoQuality, this.forcedResolutionX, this.forcedResolutionY, this._hiDpi, limited);
|
||||
|
||||
if (limited === undefined) {
|
||||
limited = true;
|
||||
}
|
||||
|
@ -1469,6 +1521,10 @@ export default class RFB extends EventTargetMixin {
|
|||
}
|
||||
break;
|
||||
|
||||
case 'proxied':
|
||||
//secondary display that needs to proxy messages through the broadcast channel
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.Error("Unknown connection state: " + state);
|
||||
return;
|
||||
|
@ -1486,8 +1542,10 @@ export default class RFB extends EventTargetMixin {
|
|||
this._disconnTimer = null;
|
||||
|
||||
// make sure we don't get a double event
|
||||
if (this._rfbConnectionState !== 'proxied') {
|
||||
this._sock.off('close');
|
||||
}
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
case 'connecting':
|
||||
|
@ -1550,6 +1608,62 @@ export default class RFB extends EventTargetMixin {
|
|||
{ detail: { capabilities: this._capabilities } }));
|
||||
}
|
||||
|
||||
_registerSecondaryDisplay() {
|
||||
this._primaryDisplayChannel = new BroadcastChannel(`screen_${this._screenID}_channel`);
|
||||
this._primaryDisplayChannel.message = this._handleSecondaryDisplayMessage.bind(this);
|
||||
const size = this._screenSize();
|
||||
|
||||
message = {
|
||||
eventType: 'register',
|
||||
screenID: this._screenID,
|
||||
screenIndex: this._screenIndex,
|
||||
width: size.w,
|
||||
height: size.h,
|
||||
x: 0,
|
||||
y: 0,
|
||||
relativePosition: 0,
|
||||
pixelRatio: window.devicePixelRatio,
|
||||
containerWidth: this._screen.offsetWidth,
|
||||
containerHeight: this._screen.offsetWidth,
|
||||
channel: null
|
||||
}
|
||||
this._controlChannel.postMessage(message)
|
||||
}
|
||||
|
||||
_proxyRFBMessage(messageType, data) {
|
||||
message = {
|
||||
messageType: messageType,
|
||||
data: data
|
||||
}
|
||||
this._controlChannel.postMessage(message);
|
||||
}
|
||||
|
||||
_handleControlMessage(event) {
|
||||
console.log(event);
|
||||
|
||||
switch (event.eventType) {
|
||||
case 'register':
|
||||
this._display.addScreen(event.screenID, event.width, event.height, event.relativePosition, event.pixelRatio, event.containerHeight, event.containerWidth);
|
||||
Log.Info(`Secondary monitor (${event.screenID}) has been registered.`);
|
||||
break;
|
||||
case 'unregister':
|
||||
if (this._display.removeScreen(event.screenID)) {
|
||||
Log.Info(`Secondary monitor (${event.screenID}) has been removed.`);
|
||||
} else {
|
||||
Log.Info(`Secondary monitor (${event.screenID}) not found.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
_handleSecondaryDisplayMessage(event) {
|
||||
console.log("Message Received: " + event);
|
||||
if (this._isPrimaryDisplay) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
_handleMessage() {
|
||||
if (this._sock.rQlen === 0) {
|
||||
Log.Warn("handleMessage called on an empty receive queue");
|
||||
|
@ -1790,16 +1904,23 @@ export default class RFB extends EventTargetMixin {
|
|||
//console.log("new_pos x" + x + ", y" + y);
|
||||
//console.log("lock x " + this._pointerLockPos.x + ", y " + this._pointerLockPos.y);
|
||||
//console.log("rel x " + rel_16_x + ", y " + rel_16_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 {
|
||||
RFB.messages.pointerEvent(this._sock, this._display.absX(x),
|
||||
this._display.absY(y), mask);
|
||||
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 ]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1808,7 +1929,12 @@ export default class RFB extends EventTargetMixin {
|
|||
if (this._rfbConnectionState !== 'connected') { return; }
|
||||
if (this._viewOnly) { return; } // View only, skip mouse events
|
||||
|
||||
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 ]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_handleWheel(ev) {
|
||||
|
@ -3570,7 +3696,7 @@ 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._screenID = this._sock.rQshiftBytes(4); // id
|
||||
this._screenIndex = this._sock.rQshiftBytes(4); // id
|
||||
this._sock.rQskipBytes(2); // x-position
|
||||
this._sock.rQskipBytes(2); // y-position
|
||||
this._sock.rQskipBytes(2); // width
|
||||
|
@ -4061,20 +4187,50 @@ RFB.messages = {
|
|||
}
|
||||
},
|
||||
|
||||
setDesktopSize(sock, width, height, id, flags) {
|
||||
setDesktopSize(sock, size, flags) {
|
||||
const buff = sock._sQ;
|
||||
const offset = sock._sQlen;
|
||||
|
||||
buff[offset] = 251; // msg-type
|
||||
buff[offset + 1] = 0; // padding
|
||||
buff[offset + 2] = width >> 8; // width
|
||||
buff[offset + 3] = width;
|
||||
buff[offset + 4] = height >> 8; // height
|
||||
buff[offset + 5] = height;
|
||||
buff[offset + 2] = size.serverWidth >> 8; // width
|
||||
buff[offset + 3] = size.serverWidth;
|
||||
buff[offset + 4] = size.serverHeight >> 8; // height
|
||||
buff[offset + 5] = size.serverHeight;
|
||||
|
||||
buff[offset + 6] = 1; // number-of-screens
|
||||
buff[offset + 6] = size.screens.length; // number-of-screens
|
||||
buff[offset + 7] = 0; // padding
|
||||
|
||||
let i = 8;
|
||||
for (let iS = 0; iS < size.screens.length; iS++) {
|
||||
//screen id
|
||||
buff[offset + i++] = iS >> 24;
|
||||
buff[offset + i++] = iS >> 16;
|
||||
buff[offset + i++] = iS >> 8;
|
||||
buff[offset + i++] = iS;
|
||||
//screen x position
|
||||
buff[offset + i++] = size.screens[iS].x >> 8;
|
||||
buff[offset + i++] = size.screens[iS].x;
|
||||
//screen y position
|
||||
buff[offset + i++] = size.screens[iS].y >> 8;
|
||||
buff[offset + i++] = size.screens[iS].y;
|
||||
//screen width
|
||||
buff[offset + i++] = size.screens[iS].serverWidth >> 8;
|
||||
buff[offset + i++] = size.screens[iS].serverWidth;
|
||||
//screen height
|
||||
buff[offset + i++] = size.screens[iS].serverHeight >> 8;
|
||||
buff[offset + i++] = size.screens[iS].serverHeight;
|
||||
//flags
|
||||
buff[offset + i++] = flags >> 24;
|
||||
buff[offset + i++] = flags >> 16;
|
||||
buff[offset + i++] = flags >> 8;
|
||||
buff[offset + i++] = flags;
|
||||
}
|
||||
|
||||
sock._sQlen += i;
|
||||
sock.flush();
|
||||
|
||||
/*
|
||||
// screen array
|
||||
buff[offset + 8] = id >> 24; // id
|
||||
buff[offset + 9] = id >> 16;
|
||||
|
@ -4095,6 +4251,7 @@ RFB.messages = {
|
|||
|
||||
sock._sQlen += 24;
|
||||
sock.flush();
|
||||
*/
|
||||
},
|
||||
|
||||
setMaxVideoResolution(sock, width, height) {
|
||||
|
|
|
@ -26,3 +26,9 @@ export function decodeUTF8(utf8string, allowLatin1=false) {
|
|||
export function encodeUTF8(DOMString) {
|
||||
return unescape(encodeURIComponent(DOMString));
|
||||
}
|
||||
|
||||
export function uuidv4() {
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="noVNC_loading">
|
||||
<head>
|
||||
|
||||
<!--
|
||||
noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
|
||||
This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
|
||||
-->
|
||||
<title>KasmVNC</title>
|
||||
|
||||
<meta charset="utf-8">
|
||||
|
||||
<!-- Icons (see app/images/icons/Makefile for what the sizes are for) -->
|
||||
<link rel="icon" sizes="16x16" type="image/png" href="app/images/icons/368_kasm_logo_only_16x16.png">
|
||||
<link rel="icon" sizes="24x24" type="image/png" href="app/images/icons/368_kasm_logo_only_24x24.png">
|
||||
<link rel="icon" sizes="32x32" type="image/png" href="app/images/icons/368_kasm_logo_only_32x32.png">
|
||||
<link rel="icon" sizes="48x48" type="image/png" href="app/images/icons/368_kasm_logo_only_48x48.png">
|
||||
<link rel="icon" sizes="60x60" type="image/png" href="app/images/icons/368_kasm_logo_only_60x60.png">
|
||||
<link rel="icon" sizes="64x64" type="image/png" href="app/images/icons/368_kasm_logo_only_64x64.png">
|
||||
<link rel="icon" sizes="72x72" type="image/png" href="app/images/icons/368_kasm_logo_only_72x72.png">
|
||||
<link rel="icon" sizes="76x76" type="image/png" href="app/images/icons/368_kasm_logo_only_76x76.png">
|
||||
<link rel="icon" sizes="96x96" type="image/png" href="app/images/icons/368_kasm_logo_only_96x96.png">
|
||||
<link rel="icon" sizes="120x120" type="image/png" href="app/images/icons/368_kasm_logo_only_120x120.png">
|
||||
<link rel="icon" sizes="144x144" type="image/png" href="app/images/icons/368_kasm_logo_only_144x144.png">
|
||||
<link rel="icon" sizes="152x152" type="image/png" href="app/images/icons/368_kasm_logo_only_152x152.png">
|
||||
<link rel="icon" sizes="192x192" type="image/png" href="app/images/icons/368_kasm_logo_only_192x192.png">
|
||||
<!-- Firefox currently mishandles SVG, see #1419039
|
||||
<link rel="icon" sizes="any" type="image/svg+xml" href="app/images/icons/kasm-icon.svg">
|
||||
-->
|
||||
<!-- Repeated last so that legacy handling will pick this -->
|
||||
<link rel="icon" sizes="16x16" type="image/png" href="app/images/icons/368_kasm_logo_only_16x16.png">
|
||||
|
||||
<!-- Apple iOS Safari settings -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<!-- Home Screen Icons (favourites and bookmarks use the normal icons) -->
|
||||
<link rel="apple-touch-icon" sizes="60x60" type="image/png" href="app/images/icons/368_kasm_logo_only_60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" type="image/png" href="app/images/icons/368_kasm_logo_only_76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/368_kasm_logo_only_120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" type="image/png" href="app/images/icons/368_kasm_logo_only_152x152.png">
|
||||
|
||||
<script src="vendor/interact.min.js"></script>
|
||||
|
||||
<!-- Stylesheets -->
|
||||
<link rel="stylesheet" href="app/styles/base.css">
|
||||
|
||||
<script src="app/error-handler.js"></script>
|
||||
|
||||
<script>
|
||||
let isInsideKasmVDI = false;
|
||||
try {
|
||||
isInsideKasmVDI = (window.self !== window.top);
|
||||
} catch (e) {
|
||||
isInsideKasmVDI = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script type="module" crossorigin="use-credentials" src="app/ui_screen.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="noVNC_fallback_error" class="noVNC_center">
|
||||
<div>
|
||||
<div id="noVNC_close_error" onclick="document.getElementById('noVNC_fallback_error').remove()"></div>
|
||||
<div>KasmVNC encountered an error:</div>
|
||||
<br>
|
||||
<div id="noVNC_fallback_errormsg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Status Dialog -->
|
||||
<div id="noVNC_status"></div>
|
||||
|
||||
<!-- Connect button -->
|
||||
<div class="noVNC_center">
|
||||
<div id="noVNC_connect_dlg">
|
||||
<div id="noVNC_connect_button">
|
||||
<div>
|
||||
<img alt="" src="app/images/connect.svg"> Connect
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- This is where the RFB elements will attach -->
|
||||
<div id="noVNC_container">
|
||||
<!-- Note that Google Chrome on Android doesn't respect any of these,
|
||||
html attributes which attempt to disable text suggestions on the
|
||||
on-screen keyboard. Let's hope Chrome implements the ime-mode
|
||||
style for example -->
|
||||
<textarea id="noVNC_keyboardinput" autocapitalize="off"
|
||||
autocomplete="off" spellcheck="false" tabindex="-1"></textarea>
|
||||
</div>
|
||||
</body>
|
31
vnc.html
31
vnc.html
|
@ -50,7 +50,7 @@
|
|||
<script src="vendor/interact.min.js"></script>
|
||||
|
||||
<!-- Stylesheets -->
|
||||
<!--link rel="stylesheet" href="app/styles/base.css">
|
||||
<link rel="stylesheet" href="app/styles/base.css">
|
||||
|
||||
<script src="app/error-handler.js"></script>
|
||||
|
||||
|
@ -63,7 +63,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<script type="module" crossorigin="use-credentials" src="app/ui.js"></script-->
|
||||
<script type="module" crossorigin="use-credentials" src="app/ui.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -185,6 +185,14 @@
|
|||
Fullscreen
|
||||
</div>
|
||||
|
||||
<!-- Add Second Screen -->
|
||||
<div class="noVNC_button_div noVNC_hidden" >
|
||||
<input type="image" alt="Fullscreen" src="app/images/fullscreen.svg"
|
||||
id="noVNC_addmonitor_button" class="noVNC_button"
|
||||
title="Add Monitor">
|
||||
Add Monitor
|
||||
</div>
|
||||
|
||||
<!-- Toggle game mode -->
|
||||
<div class="noVNC_button_div noVNC_hidden noVNC_hide_on_disconnect" >
|
||||
<input type="image" alt="Game Mode" src="app/images/gamepad.png"
|
||||
|
@ -548,25 +556,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Dialog -->
|
||||
<div class="noVNC_center noVNC_connect_layer">
|
||||
<div id="noVNC_credentials_dlg" class="noVNC_panel"><form>
|
||||
<ul>
|
||||
<li id="noVNC_username_block">
|
||||
<label>Username:</label>
|
||||
<input id="noVNC_username_input">
|
||||
</li>
|
||||
<li id="noVNC_password_block">
|
||||
<label>Password:</label>
|
||||
<input id="noVNC_password_input" type="password">
|
||||
</li>
|
||||
<li>
|
||||
<input id="noVNC_credentials_button" type="submit" value="Send Credentials" class="noVNC_submit">
|
||||
</li>
|
||||
</ul>
|
||||
</form></div>
|
||||
</div>
|
||||
|
||||
<!-- Transition Screens -->
|
||||
<div id="noVNC_transition">
|
||||
<div id="noVNC_transition_text"></div>
|
||||
|
|
Loading…
Reference in New Issue