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
|
- Automatic mixing of webp and jpeg based on CPU availability on server
|
||||||
- WebRTC UDP Transit
|
- WebRTC UDP Transit
|
||||||
- Lossless QOI Image format for Local LAN
|
- 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)
|
- Seemless clipboard support (on Chromium based browsers)
|
||||||
- Binary clipboard support for text, images, and formatted text (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
|
- Allow client to set/change most configuration settings
|
||||||
|
|
107
app/ui.js
107
app/ui.js
|
@ -31,8 +31,8 @@ window.updateSetting = (name, value) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import "core-js/stable";
|
//import "core-js/stable";
|
||||||
import "regenerator-runtime/runtime";
|
//import "regenerator-runtime/runtime";
|
||||||
import * as Log from '../core/util/logging.js';
|
import * as Log from '../core/util/logging.js';
|
||||||
import _, { l10n } from './localization.js';
|
import _, { l10n } from './localization.js';
|
||||||
import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold, supportsBinaryClipboard, isFirefox, isWindows, isIOS, supportsPointerLock }
|
import { isTouchDevice, isSafari, hasScrollbarGutter, dragThreshold, supportsBinaryClipboard, isFirefox, isWindows, isIOS, supportsPointerLock }
|
||||||
|
@ -69,6 +69,8 @@ const UI = {
|
||||||
reconnectCallback: null,
|
reconnectCallback: null,
|
||||||
reconnectPassword: null,
|
reconnectPassword: null,
|
||||||
|
|
||||||
|
supportsBroadcastChannel: (typeof BroadcastChannel !== "undefined"),
|
||||||
|
|
||||||
prime() {
|
prime() {
|
||||||
return WebUtil.initSettings().then(() => {
|
return WebUtil.initSettings().then(() => {
|
||||||
if (document.readyState === "interactive" || document.readyState === "complete") {
|
if (document.readyState === "interactive" || document.readyState === "complete") {
|
||||||
|
@ -128,14 +130,13 @@ const UI = {
|
||||||
UI.addExtraKeysHandlers();
|
UI.addExtraKeysHandlers();
|
||||||
UI.addGamingHandlers();
|
UI.addGamingHandlers();
|
||||||
UI.addMachineHandlers();
|
UI.addMachineHandlers();
|
||||||
UI.addConnectionControlHandlers();
|
|
||||||
UI.addClipboardHandlers();
|
UI.addClipboardHandlers();
|
||||||
UI.addSettingsHandlers();
|
UI.addSettingsHandlers();
|
||||||
|
UI.addMultiMonitorAddHandler();
|
||||||
document.getElementById("noVNC_status")
|
document.getElementById("noVNC_status")
|
||||||
.addEventListener('click', UI.hideStatus);
|
.addEventListener('click', UI.hideStatus);
|
||||||
UI.openControlbar();
|
UI.openControlbar();
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
UI.updateVisualState('init');
|
UI.updateVisualState('init');
|
||||||
|
|
||||||
|
@ -472,21 +473,6 @@ const UI = {
|
||||||
.addEventListener('click', () => UI.rfb.machineReset());
|
.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() {
|
addClipboardHandlers() {
|
||||||
UI.addClickHandle('noVNC_clipboard_button', UI.toggleClipboardPanel);
|
UI.addClickHandle('noVNC_clipboard_button', UI.toggleClipboardPanel);
|
||||||
|
|
||||||
|
@ -588,6 +574,13 @@ const UI = {
|
||||||
window.addEventListener('msfullscreenchange', UI.updateFullscreenButton);
|
window.addEventListener('msfullscreenchange', UI.updateFullscreenButton);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addMultiMonitorAddHandler() {
|
||||||
|
if (UI.supportsBroadcastChannel) {
|
||||||
|
UI.showControlInput("noVNC_addmonitor_button");
|
||||||
|
UI.addClickHandle('noVNC_addmonitor_button', UI.addSecondaryMonitor);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/* ------^-------
|
/* ------^-------
|
||||||
* /EVENT HANDLERS
|
* /EVENT HANDLERS
|
||||||
* ==============
|
* ==============
|
||||||
|
@ -676,8 +669,6 @@ const UI = {
|
||||||
// State change closes dialogs as they may not be relevant
|
// State change closes dialogs as they may not be relevant
|
||||||
// anymore
|
// anymore
|
||||||
UI.closeAllPanels();
|
UI.closeAllPanels();
|
||||||
document.getElementById('noVNC_credentials_dlg')
|
|
||||||
.classList.remove('noVNC_open');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
showStats() {
|
showStats() {
|
||||||
|
@ -1380,9 +1371,12 @@ const UI = {
|
||||||
UI.rfb = new RFB(document.getElementById('noVNC_container'),
|
UI.rfb = new RFB(document.getElementById('noVNC_container'),
|
||||||
document.getElementById('noVNC_keyboardinput'),
|
document.getElementById('noVNC_keyboardinput'),
|
||||||
url,
|
url,
|
||||||
{ shared: UI.getSetting('shared'),
|
{
|
||||||
repeaterID: UI.getSetting('repeaterID'),
|
shared: UI.getSetting('shared'),
|
||||||
credentials: { password: password } });
|
repeaterID: UI.getSetting('repeaterID'),
|
||||||
|
credentials: { password: password }
|
||||||
|
},
|
||||||
|
true );
|
||||||
UI.rfb.addEventListener("connect", UI.connectFinished);
|
UI.rfb.addEventListener("connect", UI.connectFinished);
|
||||||
UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
|
UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
|
||||||
UI.rfb.addEventListener("credentialsrequired", UI.credentials);
|
UI.rfb.addEventListener("credentialsrequired", UI.credentials);
|
||||||
|
@ -1746,57 +1740,6 @@ const UI = {
|
||||||
parent.postMessage({ action: 'clipboardrx', value: event.detail.text}, '*' ); //TODO fix star
|
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
|
* /PASSWORD
|
||||||
* ==============
|
* ==============
|
||||||
|
@ -1867,6 +1810,20 @@ const UI = {
|
||||||
UI.rfb.enableHiDpi = UI.getSetting('enable_hidpi');
|
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
|
* /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 Base64 from "./base64.js";
|
||||||
import { toSigned32bit } from './util/int.js';
|
import { toSigned32bit } from './util/int.js';
|
||||||
import { isWindows } from './util/browser.js';
|
import { isWindows } from './util/browser.js';
|
||||||
|
import { uuidv4 } from './util/strings.js'
|
||||||
|
|
||||||
export default class Display {
|
export default class Display {
|
||||||
constructor(target) {
|
constructor(target) {
|
||||||
|
@ -56,7 +57,7 @@ export default class Display {
|
||||||
this._targetCtx = this._target.getContext('2d');
|
this._targetCtx = this._target.getContext('2d');
|
||||||
|
|
||||||
// the visible canvas viewport (i.e. what actually gets seen)
|
// 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);
|
Log.Debug("User Agent: " + navigator.userAgent);
|
||||||
|
|
||||||
|
@ -84,6 +85,21 @@ export default class Display {
|
||||||
this._clipViewport = false;
|
this._clipViewport = false;
|
||||||
this._antiAliasing = 0;
|
this._antiAliasing = 0;
|
||||||
this._fps = 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 =====
|
// ===== EVENT HANDLERS =====
|
||||||
|
|
||||||
|
@ -97,6 +113,8 @@ export default class Display {
|
||||||
|
|
||||||
// ===== PROPERTIES =====
|
// ===== PROPERTIES =====
|
||||||
|
|
||||||
|
get screens() { return this._screens; }
|
||||||
|
|
||||||
get antiAliasing() { return this._antiAliasing; }
|
get antiAliasing() { return this._antiAliasing; }
|
||||||
set antiAliasing(value) {
|
set antiAliasing(value) {
|
||||||
this._antiAliasing = value;
|
this._antiAliasing = value;
|
||||||
|
@ -112,8 +130,8 @@ export default class Display {
|
||||||
set clipViewport(viewport) {
|
set clipViewport(viewport) {
|
||||||
this._clipViewport = viewport;
|
this._clipViewport = viewport;
|
||||||
// May need to readjust the viewport dimensions
|
// May need to readjust the viewport dimensions
|
||||||
const vp = this._viewportLoc;
|
const vp = this._screens[0];
|
||||||
this.viewportChangeSize(vp.w, vp.h);
|
this.viewportChangeSize(vp.width, vp.height);
|
||||||
this.viewportChangePos(0, 0);
|
this.viewportChangePos(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,18 +154,167 @@ export default class Display {
|
||||||
|
|
||||||
// ===== PUBLIC METHODS =====
|
// ===== 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) {
|
viewportChangePos(deltaX, deltaY) {
|
||||||
const vp = this._viewportLoc;
|
const vp = this._screens[0];
|
||||||
deltaX = Math.floor(deltaX);
|
deltaX = Math.floor(deltaX);
|
||||||
deltaY = Math.floor(deltaY);
|
deltaY = Math.floor(deltaY);
|
||||||
|
|
||||||
if (!this._clipViewport) {
|
if (!this._clipViewport) {
|
||||||
deltaX = -vp.w; // clamped later of out of bounds
|
deltaX = -vp.width; // clamped later of out of bounds
|
||||||
deltaY = -vp.h;
|
deltaY = -vp.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
const vx2 = vp.x + vp.w - 1;
|
const vx2 = vp.x + vp.width - 1;
|
||||||
const vy2 = vp.y + vp.h - 1;
|
const vy2 = vp.y + vp.height - 1;
|
||||||
|
|
||||||
// Position change
|
// Position change
|
||||||
|
|
||||||
|
@ -192,10 +359,10 @@ export default class Display {
|
||||||
height = this._fbHeight;
|
height = this._fbHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
const vp = this._viewportLoc;
|
const vp = this._screens[0];
|
||||||
if (vp.w !== width || vp.h !== height) {
|
if (vp.width !== width || vp.height !== height) {
|
||||||
vp.w = width;
|
vp.width = width;
|
||||||
vp.h = height;
|
vp.height = height;
|
||||||
|
|
||||||
const canvas = this._target;
|
const canvas = this._target;
|
||||||
canvas.width = width;
|
canvas.width = width;
|
||||||
|
@ -213,14 +380,14 @@ export default class Display {
|
||||||
if (this._scale === 0) {
|
if (this._scale === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return toSigned32bit(x / this._scale + this._viewportLoc.x);
|
return toSigned32bit(x / this._scale + this._screens[0].x);
|
||||||
}
|
}
|
||||||
|
|
||||||
absY(y) {
|
absY(y) {
|
||||||
if (this._scale === 0) {
|
if (this._scale === 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return toSigned32bit(y / this._scale + this._viewportLoc.y);
|
return toSigned32bit(y / this._scale + this._screens[0].y);
|
||||||
}
|
}
|
||||||
|
|
||||||
resize(width, height) {
|
resize(width, height) {
|
||||||
|
@ -253,8 +420,8 @@ export default class Display {
|
||||||
|
|
||||||
// Readjust the viewport as it may be incorrectly sized
|
// Readjust the viewport as it may be incorrectly sized
|
||||||
// and positioned
|
// and positioned
|
||||||
const vp = this._viewportLoc;
|
const vp = this._screens[0];
|
||||||
this.viewportChangeSize(vp.w, vp.h);
|
this.viewportChangeSize(vp.width, vp.height);
|
||||||
this.viewportChangePos(0, 0);
|
this.viewportChangePos(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -464,14 +631,14 @@ export default class Display {
|
||||||
|
|
||||||
} else if (scaleRatio === 0) {
|
} else if (scaleRatio === 0) {
|
||||||
|
|
||||||
const vp = this._viewportLoc;
|
const vp = this._screens[0];
|
||||||
const targetAspectRatio = containerWidth / containerHeight;
|
const targetAspectRatio = containerWidth / containerHeight;
|
||||||
const fbAspectRatio = vp.w / vp.h;
|
const fbAspectRatio = vp.width / vp.height;
|
||||||
|
|
||||||
if (fbAspectRatio >= targetAspectRatio) {
|
if (fbAspectRatio >= targetAspectRatio) {
|
||||||
scaleRatio = containerWidth / vp.w;
|
scaleRatio = containerWidth / vp.width;
|
||||||
} else {
|
} else {
|
||||||
scaleRatio = containerHeight / vp.h;
|
scaleRatio = containerHeight / vp.height;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -658,14 +825,14 @@ export default class Display {
|
||||||
|
|
||||||
_rescale(factor) {
|
_rescale(factor) {
|
||||||
this._scale = factor;
|
this._scale = factor;
|
||||||
const vp = this._viewportLoc;
|
const vp = this._screens[0];
|
||||||
|
|
||||||
// NB(directxman12): If you set the width directly, or set the
|
// NB(directxman12): If you set the width directly, or set the
|
||||||
// style width to a number, the canvas is cleared.
|
// style width to a number, the canvas is cleared.
|
||||||
// However, if you set the style width to a string
|
// However, if you set the style width to a string
|
||||||
// ('NNNpx'), the canvas is scaled without clearing.
|
// ('NNNpx'), the canvas is scaled without clearing.
|
||||||
const width = factor * vp.w + 'px';
|
const width = factor * vp.width + 'px';
|
||||||
const height = factor * vp.h + 'px';
|
const height = factor * vp.height + 'px';
|
||||||
|
|
||||||
if ((this._target.style.width !== width) ||
|
if ((this._target.style.width !== width) ||
|
||||||
(this._target.style.height !== height)) {
|
(this._target.style.height !== height)) {
|
||||||
|
@ -673,12 +840,12 @@ export default class Display {
|
||||||
this._target.style.height = height;
|
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 pixR = Math.abs(Math.ceil(window.devicePixelRatio));
|
||||||
var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
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' );
|
this._target.style.imageRendering = ((!isFirefox) ? 'pixelated' : 'crisp-edges' );
|
||||||
Log.Debug('Smoothing disabled');
|
Log.Debug('Smoothing disabled');
|
||||||
} else if (this.antiAliasing === 1 || (this.antiAliasing === 0 && factor !== 1 && this._target.style.imageRendering !== 'auto')) {
|
} else if (this.antiAliasing === 1 || (this.antiAliasing === 0 && factor !== 1 && this._target.style.imageRendering !== 'auto')) {
|
||||||
|
|
367
core/rfb.js
367
core/rfb.js
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
import { toUnsigned32bit, toSigned32bit } from './util/int.js';
|
import { toUnsigned32bit, toSigned32bit } from './util/int.js';
|
||||||
import * as Log from './util/logging.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 { hashUInt8Array } from './util/int.js';
|
||||||
import { dragThreshold, supportsCursorURIs, isTouchDevice, isWindows, isMac, isIOS } from './util/browser.js';
|
import { dragThreshold, supportsCursorURIs, isTouchDevice, isWindows, isMac, isIOS } from './util/browser.js';
|
||||||
import { clientToElement } from './util/element.js';
|
import { clientToElement } from './util/element.js';
|
||||||
|
@ -76,7 +76,7 @@ const extendedClipboardActionNotify = 1 << 27;
|
||||||
const extendedClipboardActionProvide = 1 << 28;
|
const extendedClipboardActionProvide = 1 << 28;
|
||||||
|
|
||||||
export default class RFB extends EventTargetMixin {
|
export default class RFB extends EventTargetMixin {
|
||||||
constructor(target, touchInput, urlOrChannel, options) {
|
constructor(target, touchInput, urlOrChannel, options, isPrimaryDisplay) {
|
||||||
if (!target) {
|
if (!target) {
|
||||||
throw new Error("Must specify 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._shared = 'shared' in options ? !!options.shared : true;
|
||||||
this._repeaterID = options.repeaterID || '';
|
this._repeaterID = options.repeaterID || '';
|
||||||
this._wsProtocols = options.wsProtocols || ['binary'];
|
this._wsProtocols = options.wsProtocols || ['binary'];
|
||||||
|
this._isPrimaryDisplay = (isPrimaryDisplay !== false);
|
||||||
|
|
||||||
// Internal state
|
// Internal state
|
||||||
this._rfbConnectionState = '';
|
this._rfbConnectionState = '';
|
||||||
|
@ -122,7 +123,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._supportsContinuousUpdates = false;
|
this._supportsContinuousUpdates = false;
|
||||||
this._enabledContinuousUpdates = false;
|
this._enabledContinuousUpdates = false;
|
||||||
this._supportsSetDesktopSize = false;
|
this._supportsSetDesktopSize = false;
|
||||||
this._screenID = 0;
|
this._screenID = uuidv4();
|
||||||
|
this._screenIndex = 0;
|
||||||
this._screenFlags = 0;
|
this._screenFlags = 0;
|
||||||
this._qemuExtKeyEventSupported = false;
|
this._qemuExtKeyEventSupported = false;
|
||||||
|
|
||||||
|
@ -212,6 +214,19 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._gestureLastMagnitudeX = 0;
|
this._gestureLastMagnitudeX = 0;
|
||||||
this._gestureLastMagnitudeY = 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
|
// Bound event handlers
|
||||||
this._eventHandlers = {
|
this._eventHandlers = {
|
||||||
updateHiddenKeyboard: this._updateHiddenKeyboard.bind(this),
|
updateHiddenKeyboard: this._updateHiddenKeyboard.bind(this),
|
||||||
|
@ -283,68 +298,13 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
this._gestures = new GestureHandler();
|
this._gestures = new GestureHandler();
|
||||||
|
|
||||||
this._sock = new Websock();
|
if (this._isPrimaryDisplay) {
|
||||||
this._sock.on('message', () => {
|
this._setupWebSocket();
|
||||||
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'));
|
|
||||||
|
|
||||||
Log.Debug("<< RFB.constructor");
|
Log.Debug("<< RFB.constructor");
|
||||||
|
|
||||||
// ===== PROPERTIES =====
|
// ===== PROPERTIES =====
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.dragViewport = false;
|
this.dragViewport = false;
|
||||||
this.focusOnClick = true;
|
this.focusOnClick = true;
|
||||||
this.lastActiveAt = Date.now();
|
this.lastActiveAt = Date.now();
|
||||||
|
@ -774,7 +734,9 @@ export default class RFB extends EventTargetMixin {
|
||||||
if (this._rfbConnectionState === 'connected') {
|
if (this._rfbConnectionState === 'connected') {
|
||||||
|
|
||||||
if (this._pendingApplyVideoRes) {
|
if (this._pendingApplyVideoRes) {
|
||||||
RFB.messages.setMaxVideoResolution(this._sock, this._maxVideoResolutionX, this._maxVideoResolutionY);
|
if (this._isPrimaryDisplay){
|
||||||
|
RFB.messages.setMaxVideoResolution(this._sock, this._maxVideoResolutionX, this._maxVideoResolutionY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._pendingApplyResolutionChange) {
|
if (this._pendingApplyResolutionChange) {
|
||||||
|
@ -793,15 +755,12 @@ export default class RFB extends EventTargetMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
this._updateConnectionState('disconnecting');
|
if (this._rfbConnectionState !== 'proxied') {
|
||||||
this._sock.off('error');
|
this._updateConnectionState('disconnecting');
|
||||||
this._sock.off('message');
|
this._sock.off('error');
|
||||||
this._sock.off('open');
|
this._sock.off('message');
|
||||||
}
|
this._sock.off('open');
|
||||||
|
}
|
||||||
sendCredentials(creds) {
|
|
||||||
this._rfbCredentials = creds;
|
|
||||||
setTimeout(this._initMsg.bind(this), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sendCtrlAltDel() {
|
sendCtrlAltDel() {
|
||||||
|
@ -851,13 +810,21 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode);
|
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 {
|
} else {
|
||||||
if (!keysym) {
|
if (!keysym) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);
|
Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);
|
||||||
RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
|
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' ];
|
let mimes = [ 'text/plain' ];
|
||||||
dataset.push(data);
|
dataset.push(data);
|
||||||
|
|
||||||
RFB.messages.sendBinaryClipboard(this._sock, dataset, mimes);
|
if (this._isPrimaryDisplay) {
|
||||||
|
RFB.messages.sendBinaryClipboard(this._sock, dataset, mimes);
|
||||||
|
} else {
|
||||||
|
this._proxyRFBMessage('sendBinaryClipboard', [ dataset, mimes ]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async clipboardPasteDataFrom(clipdata) {
|
async clipboardPasteDataFrom(clipdata) {
|
||||||
|
@ -973,23 +945,33 @@ export default class RFB extends EventTargetMixin {
|
||||||
|
|
||||||
|
|
||||||
if (dataset.length > 0) {
|
if (dataset.length > 0) {
|
||||||
RFB.messages.sendBinaryClipboard(this._sock, dataset, mimes);
|
if (this._isPrimaryDisplay) {
|
||||||
|
RFB.messages.sendBinaryClipboard(this._sock, dataset, mimes);
|
||||||
|
} else {
|
||||||
|
this._proxyRFBMessage('sendBinaryClipboard', [ dataset, mimes ]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
requestBottleneckStats() {
|
requestBottleneckStats() {
|
||||||
RFB.messages.requestStats(this._sock);
|
if (this._isPrimaryDisplay) {
|
||||||
|
RFB.messages.requestStats(this._sock);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeUnixRelay(name, processRelayFn) {
|
subscribeUnixRelay(name, processRelayFn) {
|
||||||
this._unixRelays = this._unixRelays || {};
|
if (this._isPrimaryDisplay){
|
||||||
this._unixRelays[name] = processRelayFn;
|
this._unixRelays = this._unixRelays || {};
|
||||||
RFB.messages.sendSubscribeUnixRelay(this._sock, name);
|
this._unixRelays[name] = processRelayFn;
|
||||||
|
RFB.messages.sendSubscribeUnixRelay(this._sock, name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendUnixRelayData(name, payload) {
|
sendUnixRelayData(name, payload) {
|
||||||
RFB.messages.sendUnixRelay(this._sock, name, payload);
|
if (this._isPrimaryDisplay) {
|
||||||
|
RFB.messages.sendUnixRelay(this._sock, name, payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== PRIVATE METHODS =====
|
// ===== PRIVATE METHODS =====
|
||||||
|
@ -1003,10 +985,68 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._transitConnectionState = value;
|
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() {
|
_connect() {
|
||||||
Log.Debug(">> RFB.connect");
|
Log.Debug(">> RFB.connect");
|
||||||
|
|
||||||
if (this._url) {
|
if (this._url && this._isPrimaryDisplay) {
|
||||||
try {
|
try {
|
||||||
Log.Info(`connecting to ${this._url}`);
|
Log.Info(`connecting to ${this._url}`);
|
||||||
this._sock.open(this._url, this._wsProtocols);
|
this._sock.open(this._url, this._wsProtocols);
|
||||||
|
@ -1018,13 +1058,15 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._fail("Error when opening socket (" + e + ")");
|
this._fail("Error when opening socket (" + e + ")");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if (this._isPrimaryDisplay) {
|
||||||
try {
|
try {
|
||||||
Log.Info(`attaching ${this._rawChannel} to Websock`);
|
Log.Info(`attaching ${this._rawChannel} to Websock`);
|
||||||
this._sock.attach(this._rawChannel);
|
this._sock.attach(this._rawChannel);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._fail("Error attaching channel (" + e + ")");
|
this._fail("Error attaching channel (" + e + ")");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this._registerSecondaryDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make our elements part of the page
|
// Make our elements part of the page
|
||||||
|
@ -1085,7 +1127,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._resendClipboardNextUserDrivenEvent = true;
|
this._resendClipboardNextUserDrivenEvent = true;
|
||||||
|
|
||||||
// WebRTC UDP datachannel inits
|
// WebRTC UDP datachannel inits
|
||||||
if (typeof RTCPeerConnection !== 'undefined') {
|
if (typeof RTCPeerConnection !== 'undefined' && this._isPrimaryDisplay) {
|
||||||
this._udpBuffer = new Map();
|
this._udpBuffer = new Map();
|
||||||
|
|
||||||
this._udpPeer = new RTCPeerConnection({
|
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);
|
setTimeout(function() { this._sendUdpUpgrade() }.bind(this), 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1224,7 +1266,17 @@ export default class RFB extends EventTargetMixin {
|
||||||
window.removeEventListener('focus', this._eventHandlers.handleFocusChange);
|
window.removeEventListener('focus', this._eventHandlers.handleFocusChange);
|
||||||
this._keyboard.ungrab();
|
this._keyboard.ungrab();
|
||||||
this._gestures.detach();
|
this._gestures.detach();
|
||||||
this._sock.close();
|
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 {
|
try {
|
||||||
this._target.removeChild(this._screen);
|
this._target.removeChild(this._screen);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -1325,7 +1377,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
// When clipping is enabled, the screen is limited to
|
// When clipping is enabled, the screen is limited to
|
||||||
// the size of the container.
|
// the size of the container.
|
||||||
const size = this._screenSize();
|
const size = this._screenSize();
|
||||||
this._display.viewportChangeSize(size.w, size.h);
|
this._display.viewportChangeSize(size.screens[0].serverWidth, size.screens[0].serverHeight);
|
||||||
this._fixScrollbars();
|
this._fixScrollbars();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1335,7 +1387,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._display.scale = 1.0;
|
this._display.scale = 1.0;
|
||||||
} else {
|
} else {
|
||||||
const size = this._screenSize(false);
|
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();
|
this._fixScrollbars();
|
||||||
}
|
}
|
||||||
|
@ -1351,20 +1403,20 @@ export default class RFB extends EventTargetMixin {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const size = this._screenSize();
|
const size = this._screenSize();
|
||||||
RFB.messages.setDesktopSize(this._sock,
|
RFB.messages.setDesktopSize(this._sock, size, this._screenFlags);
|
||||||
Math.floor(size.w), Math.floor(size.h),
|
|
||||||
this._screenID, this._screenFlags);
|
|
||||||
|
|
||||||
Log.Debug('Requested new desktop size: ' +
|
Log.Debug('Requested new desktop size: ' +
|
||||||
size.w + 'x' + size.h);
|
size.serverWidth + 'x' + size.serverHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the the size of the available screen
|
// Gets the the size of the available screen
|
||||||
_screenSize (limited) {
|
_screenSize (limited) {
|
||||||
|
return this._display.getScreenSize(this.videoQuality, this.forcedResolutionX, this.forcedResolutionY, this._hiDpi, limited);
|
||||||
|
|
||||||
if (limited === undefined) {
|
if (limited === undefined) {
|
||||||
limited = true;
|
limited = true;
|
||||||
}
|
}
|
||||||
var x = this.forcedResolutionX || this._screen.offsetWidth;
|
var x = this.forcedResolutionX || this._screen.offsetWidth;
|
||||||
var y = this.forcedResolutionY || this._screen.offsetHeight;
|
var y = this.forcedResolutionY || this._screen.offsetHeight;
|
||||||
var scale = 0; // 0=auto
|
var scale = 0; // 0=auto
|
||||||
try {
|
try {
|
||||||
|
@ -1469,6 +1521,10 @@ export default class RFB extends EventTargetMixin {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'proxied':
|
||||||
|
//secondary display that needs to proxy messages through the broadcast channel
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
Log.Error("Unknown connection state: " + state);
|
Log.Error("Unknown connection state: " + state);
|
||||||
return;
|
return;
|
||||||
|
@ -1486,7 +1542,9 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._disconnTimer = null;
|
this._disconnTimer = null;
|
||||||
|
|
||||||
// make sure we don't get a double event
|
// make sure we don't get a double event
|
||||||
this._sock.off('close');
|
if (this._rfbConnectionState !== 'proxied') {
|
||||||
|
this._sock.off('close');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (state) {
|
switch (state) {
|
||||||
|
@ -1550,6 +1608,62 @@ export default class RFB extends EventTargetMixin {
|
||||||
{ detail: { capabilities: this._capabilities } }));
|
{ 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() {
|
_handleMessage() {
|
||||||
if (this._sock.rQlen === 0) {
|
if (this._sock.rQlen === 0) {
|
||||||
Log.Warn("handleMessage called on an empty receive queue");
|
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("new_pos x" + x + ", y" + y);
|
||||||
//console.log("lock x " + this._pointerLockPos.x + ", y " + this._pointerLockPos.y);
|
//console.log("lock x " + this._pointerLockPos.x + ", y " + this._pointerLockPos.y);
|
||||||
//console.log("rel x " + rel_16_x + ", y " + rel_16_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
|
// reset the cursor position to center
|
||||||
this._mousePos = { x: this._pointerLockPos.x , y: this._pointerLockPos.y };
|
this._mousePos = { x: this._pointerLockPos.x , y: this._pointerLockPos.y };
|
||||||
this._cursor.move(this._pointerLockPos.x, this._pointerLockPos.y);
|
this._cursor.move(this._pointerLockPos.x, this._pointerLockPos.y);
|
||||||
} else {
|
} else {
|
||||||
RFB.messages.pointerEvent(this._sock, this._display.absX(x),
|
if (this._isPrimaryDisplay) {
|
||||||
this._display.absY(y), mask);
|
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._rfbConnectionState !== 'connected') { return; }
|
||||||
if (this._viewOnly) { return; } // View only, skip mouse events
|
if (this._viewOnly) { return; } // View only, skip mouse events
|
||||||
|
|
||||||
RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), 0, dX, dY);
|
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) {
|
_handleWheel(ev) {
|
||||||
|
@ -3570,7 +3696,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
for (let i = 0; i < numberOfScreens; i += 1) {
|
for (let i = 0; i < numberOfScreens; i += 1) {
|
||||||
// Save the id and flags of the first screen
|
// Save the id and flags of the first screen
|
||||||
if (i === 0) {
|
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); // x-position
|
||||||
this._sock.rQskipBytes(2); // y-position
|
this._sock.rQskipBytes(2); // y-position
|
||||||
this._sock.rQskipBytes(2); // width
|
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 buff = sock._sQ;
|
||||||
const offset = sock._sQlen;
|
const offset = sock._sQlen;
|
||||||
|
|
||||||
buff[offset] = 251; // msg-type
|
buff[offset] = 251; // msg-type
|
||||||
buff[offset + 1] = 0; // padding
|
buff[offset + 1] = 0; // padding
|
||||||
buff[offset + 2] = width >> 8; // width
|
buff[offset + 2] = size.serverWidth >> 8; // width
|
||||||
buff[offset + 3] = width;
|
buff[offset + 3] = size.serverWidth;
|
||||||
buff[offset + 4] = height >> 8; // height
|
buff[offset + 4] = size.serverHeight >> 8; // height
|
||||||
buff[offset + 5] = 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
|
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
|
// screen array
|
||||||
buff[offset + 8] = id >> 24; // id
|
buff[offset + 8] = id >> 24; // id
|
||||||
buff[offset + 9] = id >> 16;
|
buff[offset + 9] = id >> 16;
|
||||||
|
@ -4095,6 +4251,7 @@ RFB.messages = {
|
||||||
|
|
||||||
sock._sQlen += 24;
|
sock._sQlen += 24;
|
||||||
sock.flush();
|
sock.flush();
|
||||||
|
*/
|
||||||
},
|
},
|
||||||
|
|
||||||
setMaxVideoResolution(sock, width, height) {
|
setMaxVideoResolution(sock, width, height) {
|
||||||
|
|
|
@ -26,3 +26,9 @@ export function decodeUTF8(utf8string, allowLatin1=false) {
|
||||||
export function encodeUTF8(DOMString) {
|
export function encodeUTF8(DOMString) {
|
||||||
return unescape(encodeURIComponent(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>
|
<script src="vendor/interact.min.js"></script>
|
||||||
|
|
||||||
<!-- Stylesheets -->
|
<!-- Stylesheets -->
|
||||||
<!--link rel="stylesheet" href="app/styles/base.css">
|
<link rel="stylesheet" href="app/styles/base.css">
|
||||||
|
|
||||||
<script src="app/error-handler.js"></script>
|
<script src="app/error-handler.js"></script>
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script type="module" crossorigin="use-credentials" src="app/ui.js"></script-->
|
<script type="module" crossorigin="use-credentials" src="app/ui.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
@ -185,6 +185,14 @@
|
||||||
Fullscreen
|
Fullscreen
|
||||||
</div>
|
</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 -->
|
<!-- Toggle game mode -->
|
||||||
<div class="noVNC_button_div noVNC_hidden noVNC_hide_on_disconnect" >
|
<div class="noVNC_button_div noVNC_hidden noVNC_hide_on_disconnect" >
|
||||||
<input type="image" alt="Game Mode" src="app/images/gamepad.png"
|
<input type="image" alt="Game Mode" src="app/images/gamepad.png"
|
||||||
|
@ -548,25 +556,6 @@
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Transition Screens -->
|
||||||
<div id="noVNC_transition">
|
<div id="noVNC_transition">
|
||||||
<div id="noVNC_transition_text"></div>
|
<div id="noVNC_transition_text"></div>
|
||||||
|
|
Loading…
Reference in New Issue