react-kasmvnc
Version:
A React Component to connect to a websockified VNC client using noVNC.
1,474 lines (1,248 loc) • 644 kB
JavaScript
'use strict';
var jsxRuntime = require('react/jsx-runtime');
var react = require('react');
var JSMpeg = require('@cycjimmy/jsmpeg-player');
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
function toUnsigned32bit(toConvert) {
return toConvert >>> 0;
}
function toSigned32bit(toConvert) {
return toConvert | 0;
}
/*
* Converts a signed 32bit integer to a signed 16bit int
* Uses second most significant bit to represent it is relative
*/
function toSignedRelative16bit(toConvert) {
// TODO: move these so they are not computed with every func call
var negmask16 = 1 << 15;
var negmask32 = 1 << 31;
var relmask16 = 1 << 14;
var converted16 = toConvert | 0;
// number is negative
if ((toConvert & negmask32) != 0) {
// clear the 32bit negative bit
// not neccessary because the last 16bits will get dropped anyway
converted16 *= -1;
// set the 16bit negative bit
converted16 |= negmask16;
// set the relative bit
converted16 |= relmask16;
} else {
// set the relative bit
converted16 |= relmask16;
}
return converted16;
}
/* Fast hashing function with low entropy */
function hashUInt8Array(data) {
if (typeof data === "string") {
data = [...data].map(character => character.charCodeAt(0));
}
let h = 0;
for (let i = 0; i < data.length; i++) {
h = Math.imul(31, h) + data[i] | 0;
}
return h;
}
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
/*
* Logging/debug routines
*/
let _logLevel = 'warn';
let Debug = () => {};
let Info = () => {};
let Warn = () => {};
let Error$1 = () => {};
function initLogging(level) {
if (typeof level === 'undefined') {
level = _logLevel;
} else {
_logLevel = level;
}
Debug = Info = Warn = Error$1 = () => {};
if (typeof window.console !== "undefined") {
/* eslint-disable no-console, no-fallthrough */
switch (level) {
case 'debug':
Debug = console.debug.bind(window.console);
case 'info':
Info = console.info.bind(window.console);
case 'warn':
Warn = console.warn.bind(window.console);
case 'error':
Error$1 = console.error.bind(window.console);
case 'none':
break;
default:
throw new window.Error("invalid logging type '" + level + "'");
}
/* eslint-enable no-console, no-fallthrough */
}
}
// Initialize logging level
initLogging();
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
// Decode from UTF-8
function decodeUTF8(utf8string, allowLatin1=false) {
try {
return decodeURIComponent(escape(utf8string));
} catch (e) {
if (e instanceof URIError) {
if (allowLatin1) {
// If we allow Latin1 we can ignore any decoding fails
// and in these cases return the original string
return utf8string;
}
}
throw e;
}
}
// Encode to UTF-8
function encodeUTF8(DOMString) {
return unescape(encodeURIComponent(DOMString));
}
function uuidv4() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*
* Browser feature support detection
*/
// Touch detection
let isTouchDevice = ('ontouchstart' in document.documentElement) ||
// requried for Chrome debugger
(document.ontouchstart !== undefined) ||
// required for MS Surface
(navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints > 0);
window.addEventListener('touchstart', function onFirstTouch() {
isTouchDevice = true;
window.removeEventListener('touchstart', onFirstTouch, false);
}, false);
// The goal is to find a certain physical width, the devicePixelRatio
// brings us a bit closer but is not optimal.
let dragThreshold = 10 * (window.devicePixelRatio || 1);
let _supportsCursorURIs = false;
try {
const target = document.createElement('canvas');
target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default';
if (target.style.cursor.indexOf("url") === 0) {
Info("Data URI scheme cursor supported");
_supportsCursorURIs = true;
} else {
Warn("Data URI scheme cursor not supported");
}
} catch (exc) {
Error$1("Data URI scheme cursor test exception: " + exc);
}
const supportsCursorURIs = _supportsCursorURIs;
let _hasScrollbarGutter = true;
try {
// Create invisible container
const container = document.createElement('div');
container.style.visibility = 'hidden';
container.style.overflow = 'scroll'; // forcing scrollbars
document.body.appendChild(container);
// Create a div and place it in the container
const child = document.createElement('div');
container.appendChild(child);
// Calculate the difference between the container's full width
// and the child's width - the difference is the scrollbars
const scrollbarWidth = (container.offsetWidth - child.offsetWidth);
// Clean up
container.parentNode.removeChild(container);
_hasScrollbarGutter = scrollbarWidth != 0;
} catch (exc) {
Error$1("Scrollbar test exception: " + exc);
}
/*
* The functions for detection of platforms and browsers below are exported
* but the use of these should be minimized as much as possible.
*
* It's better to use feature detection than platform detection.
*/
function isMac() {
return navigator && !!(/mac/i).exec(navigator.platform);
}
function isWindows() {
return navigator && !!(/win/i).exec(navigator.platform);
}
function isIOS() {
return navigator &&
(!!(/ipad/i).exec(navigator.platform) ||
!!(/iphone/i).exec(navigator.platform) ||
!!(/ipod/i).exec(navigator.platform));
}
function isSafari() {
return navigator && (navigator.userAgent.indexOf('Safari') !== -1 &&
navigator.userAgent.indexOf('Chrome') === -1);
}
//is the client a desktop like operating system
function isDesktop() {
var userAgent = navigator.userAgent;
if (isIOS() || userAgent.indexOf("OculusBrowser") != -1 || userAgent.indexOf("SamsungBrowser") != -1) {
return false
} else if (userAgent.indexOf("Windows") != -1 || userAgent.indexOf("Mac") != -1 || userAgent.indexOf("X11") != -1 || userAgent.indexOf("Linux") != -1) {
return true;
} else {
return false;
}
}
function isChromiumBased() {
return (!!window.chrome);
}
function supportsBinaryClipboard() {
//Safari does support the clipbaord API but has a lot of security restrictions
if (isSafari()) { return false; }
return (navigator.clipboard && typeof navigator.clipboard.read === "function");
}
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
/*
* HTML element utility functions
*/
function clientToElement(x, y, elem) {
const bounds = elem.getBoundingClientRect();
let pos = { x: 0, y: 0 };
// Clip to target bounds
if (x < bounds.left) {
pos.x = 0;
} else if (x >= bounds.right) {
pos.x = bounds.width - 1;
} else {
pos.x = x - bounds.left;
}
if (y < bounds.top) {
pos.y = 0;
} else if (y >= bounds.bottom) {
pos.y = bounds.height - 1;
} else {
pos.y = y - bounds.top;
}
//multiple KasmVNC screens, Window can still receive mouse events when cursor goes
//outside of the window if the mouse is down while the moving occurs
if (x > window.innerWidth) {
pos.x += (x - window.innerWidth);
}
else if (x < 0) {
pos.x = x + bounds.left;
}
if (y > window.innerHeight) {
pos.y += (y - window.innerHeight);
}
else if (y < 0) {
pos.y = y + bounds.top;
}
return pos;
}
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
function stopEvent(e) {
e.stopPropagation();
e.preventDefault();
}
// Emulate Element.setCapture() when not supported
let _captureRecursion = false;
let _elementForUnflushedEvents = null;
document.captureElement = null;
function _captureProxy(e) {
// Recursion protection as we'll see our own event
if (_captureRecursion) return;
// Clone the event as we cannot dispatch an already dispatched event
const newEv = new e.constructor(e.type, e);
_captureRecursion = true;
if (document.captureElement) {
document.captureElement.dispatchEvent(newEv);
} else {
_elementForUnflushedEvents.dispatchEvent(newEv);
}
_captureRecursion = false;
// Avoid double events
e.stopPropagation();
// Respect the wishes of the redirected event handlers
if (newEv.defaultPrevented) {
e.preventDefault();
}
// Implicitly release the capture on button release
if (e.type === "mouseup") {
releaseCapture();
}
}
// Follow cursor style of target element
function _capturedElemChanged() {
const proxyElem = document.getElementById("noVNC_mouse_capture_elem");
proxyElem.style.cursor = window.getComputedStyle(document.captureElement).cursor;
}
const _captureObserver = new MutationObserver(_capturedElemChanged);
function setCapture(target) {
if (target.setCapture) {
target.setCapture();
document.captureElement = target;
} else {
// Release any existing capture in case this method is
// called multiple times without coordination
releaseCapture();
let proxyElem = document.getElementById("noVNC_mouse_capture_elem");
if (proxyElem === null) {
proxyElem = document.createElement("div");
proxyElem.id = "noVNC_mouse_capture_elem";
proxyElem.style.position = "fixed";
proxyElem.style.top = "0px";
proxyElem.style.left = "0px";
proxyElem.style.width = "100%";
proxyElem.style.height = "100%";
proxyElem.style.zIndex = 10000;
proxyElem.style.display = "none";
document.body.appendChild(proxyElem);
// This is to make sure callers don't get confused by having
// our blocking element as the target
proxyElem.addEventListener('contextmenu', _captureProxy);
proxyElem.addEventListener('mousemove', _captureProxy);
proxyElem.addEventListener('mouseup', _captureProxy);
}
document.captureElement = target;
// Track cursor and get initial cursor
_captureObserver.observe(target, {attributes: true});
_capturedElemChanged();
proxyElem.style.display = "";
// We listen to events on window in order to keep tracking if it
// happens to leave the viewport
window.addEventListener('mousemove', _captureProxy);
window.addEventListener('mouseup', _captureProxy);
}
}
function releaseCapture() {
if (document.releaseCapture) {
document.releaseCapture();
document.captureElement = null;
} else {
if (!document.captureElement) {
return;
}
// There might be events already queued. The event proxy needs
// access to the captured element for these queued events.
// E.g. contextmenu (right-click) in Microsoft Edge
//
// Before removing the capturedElem pointer we save it to a
// temporary variable that the unflushed events can use.
_elementForUnflushedEvents = document.captureElement;
document.captureElement = null;
_captureObserver.disconnect();
const proxyElem = document.getElementById("noVNC_mouse_capture_elem");
proxyElem.style.display = "none";
window.removeEventListener('mousemove', _captureProxy);
window.removeEventListener('mouseup', _captureProxy);
}
}
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
class EventTargetMixin {
constructor() {
this._listeners = new Map();
}
addEventListener(type, callback) {
if (!this._listeners.has(type)) {
this._listeners.set(type, new Set());
}
this._listeners.get(type).add(callback);
}
removeEventListener(type, callback) {
if (this._listeners.has(type)) {
this._listeners.get(type).delete(callback);
}
}
dispatchEvent(event) {
if (!this._listeners.has(event.type)) {
return true;
}
this._listeners.get(event.type)
.forEach(callback => callback.call(this, event));
return !event.defaultPrevented;
}
}
/* The decoder is the original MPL one from Mozilla. The encoder is a faster MIT one
from https://github.com/mitschabaude/fast-base64 */
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const encodeLookup = Object.fromEntries(Array.from(alphabet).map((a, i) => [i, a.charCodeAt(0)]));
const decoder = new TextDecoder();
var Base64 = {
/* Convert data (an array of integers) to a Base64 string. */
base64Pad: '=',
encode(bytes) {
let m = bytes.length;
let k = m % 3;
let n = Math.floor(m / 3) * 4 + (k && k + 1);
let N = Math.ceil(m / 3) * 4;
let encoded = new Uint8Array(N);
for (let i = 0, j = 0; j < m; i += 4, j += 3) {
let y = (bytes[j] << 16) + (bytes[j + 1] << 8) + (bytes[j + 2] | 0);
encoded[i] = encodeLookup[y >> 18];
encoded[i + 1] = encodeLookup[(y >> 12) & 0x3f];
encoded[i + 2] = encodeLookup[(y >> 6) & 0x3f];
encoded[i + 3] = encodeLookup[y & 0x3f];
}
let base64 = decoder.decode(new Uint8Array(encoded.buffer, 0, n));
if (k === 1) base64 += '==';
if (k === 2) base64 += '=';
return base64;
},
/* Convert Base64 data to a string */
/* eslint-disable comma-spacing */
toBinaryTable: [
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
-1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
],
/* eslint-enable comma-spacing */
decode(data, offset = 0) {
let dataLength = data.indexOf('=') - offset;
if (dataLength < 0) { dataLength = data.length - offset; }
/* Every four characters is 3 resulting numbers */
const resultLength = (dataLength >> 2) * 3 + Math.floor((dataLength % 4) / 1.5);
const result = new Uint8Array(resultLength);
// Convert one by one.
let leftbits = 0; // number of bits decoded, but yet to be appended
let leftdata = 0; // bits decoded, but yet to be appended
for (let idx = 0, i = offset; i < data.length; i++) {
const c = this.toBinaryTable[data.charCodeAt(i) & 0x7f];
const padding = (data.charAt(i) === this.base64Pad);
// Skip illegal characters and whitespace
if (c === -1) {
Error$1("Illegal character code " + data.charCodeAt(i) + " at position " + i);
continue;
}
// Collect data into leftdata, update bitcount
leftdata = (leftdata << 6) | c;
leftbits += 6;
// If we have 8 or more bits, append 8 bits to the result
if (leftbits >= 8) {
leftbits -= 8;
// Append if not padding.
if (!padding) {
result[idx++] = (leftdata >> leftbits) & 0xff;
}
leftdata &= (1 << leftbits) - 1;
}
}
// If there are any bits left, the base64 string was corrupted
if (leftbits) {
const err = new Error('Corrupted base64 string');
err.name = 'Base64-Error';
throw err;
}
return result;
}
}; /* End of Base64 namespace */
/*
* KasmVNC: HTML5 VNC client
* Copyright (C) 2020 Kasm Technologies
* Copyright (C) 2019 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
class Display {
constructor(target, isPrimaryDisplay) {
Debug(">> Display.constructor");
/*
For performance reasons we use a multi dimensional array
1st Dimension of Array Represents Frames, each element is a Frame
2nd Dimension is the contents of a frame and meta data, contains 4 elements
0 - int, FrameID
1 - int, Rect Count
2 - Array of Rect objects
3 - bool, is the frame complete
4 - int, index of current rect (post-processing)
5 - int, number of times requestAnimationFrame called _pushAsyncFrame and the frame had all rects, however, the frame was not marked complete
*/
this._asyncFrameQueue = [];
this._maxAsyncFrameQueue = 3;
this._clearAsyncQueue();
this._syncFrameQueue = [];
this._transparentOverlayImg = null;
this._transparentOverlayRect = null;
this._lastTransparentRectId = "";
this._flushing = false;
// the full frame buffer (logical canvas) size
this._fbWidth = 0;
this._fbHeight = 0;
this._renderMs = 0;
this._prevDrawStyle = "";
this._target = target;
if (!this._target) {
throw new Error("Target must be set");
}
if (typeof this._target === 'string') {
throw new Error('target must be a DOM element');
}
if (!this._target.getContext) {
throw new Error("no getContext method");
}
this._targetCtx = this._target.getContext('2d');
Debug("User Agent: " + navigator.userAgent);
// performance metrics
this._flipCnt = 0;
this._lastFlip = Date.now();
this._droppedFrames = 0;
this._droppedRects = 0;
this._forcedFrameCnt = 0;
this._missingFlipRect = 0;
this._lateFlipRect = 0;
this._frameStatsInterval = setInterval(function() {
let delta = Date.now() - this._lastFlip;
if (delta > 0) {
this._fps = (this._flipCnt / (delta / 1000)).toFixed(2);
}
Debug('Dropped Frames: ' + this._droppedFrames + ' Dropped Rects: ' + this._droppedRects + ' Forced Frames: ' + this._forcedFrameCnt + ' Missing Flips: ' + this._missingFlipRect + ' Late Flips: ' + this._lateFlipRect);
this._flipCnt = 0;
this._lastFlip = Date.now();
}.bind(this), 5000);
// ===== PROPERTIES =====
this._maxScreens = 4;
this._scale = 1.0;
this._clipViewport = false;
this._antiAliasing = 0;
this._fps = 0;
this._isPrimaryDisplay = isPrimaryDisplay;
this._screenID = uuidv4();
this._screens = [{
screenID: this._screenID,
screenIndex: 0,
width: this._target.width, //client
height: this._target.height, //client
serverWidth: 0, //calculated
serverHeight: 0, //calculated
serverReportedWidth: 0,
serverReportedHeight: 0,
x: 0,
y: 0,
scale: 1,
relativePosition: 0, //left, right, up, down relative to primary display
relativePositionX: 0, //offset relative to primary monitor, always 0 for primary
relativePositionY: 0, //offset relative to primary monitor, always 0 for primary
pixelRatio: window.devicePixelRatio,
containerHeight: this._target.parentNode.offsetHeight,
containerWidth: this._target.parentNode.offsetWidth,
channel: null,
x2: 0,
y2: 0
}];
//optional offscreen canvas
this._enableCanvasBuffer = false;
this._backbuffer = document.createElement('canvas');
this._drawCtx = this._backbuffer.getContext('2d');
this._damageBounds = { left: 0, top: 0, right: this._backbuffer.width, bottom: this._backbuffer.height };
// ===== EVENT HANDLERS =====
this.onflush = () => { }; // A flush request has finished
if (!this._isPrimaryDisplay) {
this._screens[0].channel = new BroadcastChannel(`screen_${this._screenID}_channel`);
this._screens[0].channel.addEventListener('message', this._handleSecondaryDisplayMessage.bind(this));
}
Debug("<< Display.constructor");
}
// ===== PROPERTIES =====
get enableCanvasBuffer() { return this._enableCanvasBuffer; }
set enableCanvasBuffer(value) {
if (value === this._enableCanvasBuffer) { return; }
this._enableCanvasBuffer = value;
if (value && this._target)
{
//copy current visible canvas to backbuffer
let saveImg = this._targetCtx.getImageData(0, 0, this._target.width, this._target.height);
this._drawCtx.putImageData(saveImg, 0, 0);
if (this._transparentOverlayImg) {
this.drawImage(this._transparentOverlayImg, this._transparentOverlayRect.x, this._transparentOverlayRect.y, this._transparentOverlayRect.width, this._transparentOverlayRect.height, true);
}
} else if (!value && this._target) {
//copy backbuffer to canvas to clear any overlays
let saveImg = this._targetCtx.getImageData(0, 0, this._target.width, this._target.height);
this._drawCtx.putImageData(saveImg, 0, 0);
}
}
get screens() { return this._screens; }
get screenID() { return this._screenID; }
get screenIndex() {
// A secondary screen should not have a screen index of 0, but it will be 0 until registration is complete
// returning a -1 lets the caller know the screen has not been registered yet
if (!this._isPrimaryDisplay && this._screens[0].screenIndex == 0) {
return -1;
}
return this._screens[0].screenIndex;
}
get antiAliasing() { return this._antiAliasing; }
set antiAliasing(value) {
this._antiAliasing = value;
this._rescale(this._scale);
}
get scale() { return this._scale; }
set scale(scale) {
this._rescale(scale);
}
get clipViewport() { return this._clipViewport; }
set clipViewport(viewport) {
this._clipViewport = viewport;
// May need to readjust the viewport dimensions
const vp = this._screens[0];
this.viewportChangeSize(vp.width, vp.height);
this.viewportChangePos(0, 0);
}
get width() {
return this._fbWidth;
}
get height() {
return this._fbHeight;
}
get renderMs() {
return this._renderMs;
}
set renderMs(val) {
this._renderMs = val;
}
get fps() { return this._fps; }
// ===== PUBLIC METHODS =====
/*
Returns the screen index and relative coordinates given globally scoped coordinates
*/
getClientRelativeCoordinates(x, y) {
for (let i = 0; i < this._screens.length; i++) {
if (
(x >= this._screens[i].x && x <= this._screens[i].x + this._screens[i].serverWidth) &&
(y >= this._screens[i].y && y <= this._screens[i].y + this._screens[i].serverHeight)
)
{
return {
"screenIndex": i,
"x": x - this._screens[i].x,
"y": y - this._screens[i].y
}
}
}
}
/*
Returns coordinates that are server relative when multiple monitors are in use
*/
getServerRelativeCoordinates(screenIndex, x, y) {
if (screenIndex >= 0 && screenIndex < this._screens.length) {
x = toSigned32bit(x / this._screens[screenIndex].scale + this._screens[screenIndex].x);
y = toSigned32bit(y / this._screens[screenIndex].scale + this._screens[screenIndex].y);
}
return [x, y];
}
getScreenSize(resolutionQuality, max_width, max_height, hiDpi, disableLimit, disableScaling) {
let data = {
screens: null,
serverWidth: 0,
serverHeight: 0
};
let i = 0;
//recalculate primary display container size
this._screens[i].containerHeight = this._target.parentNode.offsetHeight;
this._screens[i].containerWidth = this._target.parentNode.offsetWidth;
this._screens[i].pixelRatio = window.devicePixelRatio;
this._screens[i].width = this._target.parentNode.offsetWidth;
this._screens[i].height = this._target.parentNode.offsetHeight;
//calculate server-side and client-side resolution of each screen
let width = max_width || this._screens[i].containerWidth;
let height = max_height || this._screens[i].containerHeight;
//max the resolution of a single screen to 1280
if (
(this._screens[i].serverReportedWidth > 0 && this._screens[i].serverReportedHeight > 0) &&
(
disableScaling ||
(this._screens[i].serverReportedWidth !== this._screens[i].serverWidth || this._screens[i].serverReportedHeight !== this._screens[i].serverHeight)
) &&
(!max_width && !max_height)
) {
height = this._screens[i].serverReportedHeight;
width = this._screens[i].serverReportedWidth;
}
else if (width > 1280 && !disableLimit && resolutionQuality == 1) {
height = Math.floor(1280 * (height/width)); //keeping the aspect ratio of original resolution, shrink y to match x
width = 1280;
}
//hard coded 720p
else if (resolutionQuality == 0 && !disableLimit) {
width = 1280;
height = 720;
}
//force full resolution on a high DPI monitor where the OS is scaling
else if (hiDpi) {
width = Math.floor(width * this._screens[i].pixelRatio);
height = Math.floor(height * this._screens[i].pixelRatio);
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) {
Info('Device Pixel ratio: ' + this._screens[i].pixelRatio + ' Reported Resolution: ' + width + 'x' + height);
let targetDevicePixelRatio = 1.5;
if (this._screens[i].pixelRatio > 2) { targetDevicePixelRatio = 2; }
let scaledWidth = (width * this._screens[i].pixelRatio) * (1 / targetDevicePixelRatio);
let scaleRatio = scaledWidth / width;
width = width * scaleRatio;
height = height * scaleRatio;
Info('Small device with hDPI screen detected, auto scaling at ' + scaleRatio + ' to ' + width + 'x' + height);
}
let clientServerRatioH = this._screens[i].containerHeight / height;
let clientServerRatioW = this._screens[i].containerWidth / width;
this._screens[i].height = Math.floor(height * clientServerRatioH);
this._screens[i].width = Math.floor(width * clientServerRatioW);
this._screens[i].serverWidth = width;
this._screens[i].serverHeight = height;
this._screens[i].scale = Math.min(clientServerRatioH, clientServerRatioW);
for (i = 0; i < this._screens.length; i++) {
this._screens[i].x2 = this._screens[i].x + this._screens[i].serverWidth;
this._screens[i].y2 = this._screens[i].y + this._screens[i].serverHeight;
data.serverWidth = Math.max(data.serverWidth, this._screens[i].x + this._screens[i].serverWidth);
data.serverHeight = Math.max(data.serverHeight, this._screens[i].y + this._screens[i].serverHeight);
}
data.screens = this._screens;
return data;
}
applyServerResolution(width, height, screenIndex) {
for (let z = 0; z < this._screens.length; z++) {
if (screenIndex === this._screens[z].screenIndex) {
this._screens[z].serverReportedWidth = width;
this._screens[z].serverReportedHeight = height;
}
}
}
applyScreenPlan(screenPlan) {
//check all screens for any changes, but only apply changes to primary screen, secondary screens will individually be updated and report back with their new settings
let changes = false;
for (let i = 0; i < screenPlan.screens.length; i++) {
for (let z = 0; z < this._screens.length; z++) {
if (screenPlan.screens[i].screenID === this._screens[z].screenID) {
if (this._screens[z].x !== screenPlan.screens[i].x || this._screens[z].y !== screenPlan.screens[i].y) {
if (z == 0) {
this._screens[z].x = screenPlan.screens[i].x;
this._screens[z].y = screenPlan.screens[i].y;
}
changes = true;
}
if (this._screens[z].x2 !== this._screens[z].x + this._screens[z].serverWidth || this._screens[z].y2 !== this._screens[z].y + this._screens[z].serverHeight) {
if (z == 0) {
this._screens[z].x2 = this._screens[z].x + this._screens[z].serverWidth;
this._screens[z].y2 = this._screens[z].y + this._screens[z].serverHeight;
}
changes = true;
}
}
}
}
return changes;
}
addScreen(screenID, width, height, pixelRatio, containerHeight, containerWidth, scale, serverWidth, serverHeight, x, y) {
if (!this._isPrimaryDisplay) {
throw new Error("Cannot add a screen to a secondary display.");
}
else if (containerHeight === 0 || containerWidth === 0 || pixelRatio === 0) {
Warn("Invalid screen configuration.");
}
let screenIdx = -1;
//Does the screen already exist?
for (let i = 0; i < this._screens.length; i++) {
if (this._screens[i].screenID === screenID) {
screenIdx = i;
}
}
if (screenIdx > 0) {
//existing screen, update
const existing_screen = this._screens[screenIdx];
if (existing_screen.serverHeight !== serverHeight || existing_screen.serverWidth !== serverWidth || existing_screen.width !== width || existing_screen.height !== height
|| existing_screen.containerHeight !== containerHeight || existing_screen.containerWidth !== containerWidth || existing_screen.scale !== scale || existing_screen.pixelRatio !== pixelRatio ||
existing_screen.x !== x || existing_screen.y !== y) {
existing_screen.width = width;
existing_screen.height = height;
existing_screen.containerHeight = containerHeight;
existing_screen.containerWidth = containerWidth;
existing_screen.pixelRatio = pixelRatio;
existing_screen.scale = scale;
existing_screen.serverWidth = serverWidth;
existing_screen.serverHeight = serverHeight;
existing_screen.x = x;
existing_screen.y = y;
existing_screen.x2 = existing_screen.x + existing_screen.serverWidth;
existing_screen.y2 = existing_screen.y + existing_screen.serverHeight;
return true;
}
} else {
//New Screen, add to far right until user repositions it
for (let i = 0; i < this._screens.length; i++) {
x = Math.max(x, this._screens[i].x + this._screens[i].serverWidth);
}
var new_screen = {
screenID: screenID,
screenIndex: this.screens.length,
width: width, //client
height: height, //client
serverWidth: serverWidth,
serverHeight: serverHeight,
serverReportedWidth: 0,
serverReportedHeight: 0,
x: x,
y: 0,
pixelRatio: pixelRatio,
containerHeight: containerHeight,
containerWidth: containerWidth,
channel: null,
scale: scale,
x2: x + serverWidth,
y2: serverHeight
};
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: new_screen.screenIndex });
return new_screen.screenIndex;
}
return false;
}
removeScreen(screenID) {
let removed = false;
if (this._isPrimaryDisplay) {
for (let i=1; i<this._screens.length; i++) {
if (this._screens[i].screenID == screenID) {
//flush all rects on target screen
this._flushRectsScreen(i);
this._screens[i].channel.close();
this._screens.splice(i, 1);
removed = true;
break;
}
}
//recalculate indexes and update secondary displays
for (let i=1; i<this._screens.length; i++) {
this.screens[i].screenIndex = i;
if (i > 0) {
this._screens[i].channel.postMessage({ eventType: "registered", screenIndex: i });
}
}
return removed;
} else {
throw new Error("Secondary screens only allowed on primary display.")
}
}
viewportChangePos(deltaX, deltaY) {
const vp = this._screens[0];
deltaX = Math.floor(deltaX);
deltaY = Math.floor(deltaY);
if (!this._clipViewport) {
deltaX = -vp.width; // clamped later of out of bounds
deltaY = -vp.height;
}
const vx2 = vp.x + vp.width - 1;
const vy2 = vp.y + vp.height - 1;
// Position change
if (deltaX < 0 && vp.x + deltaX < 0) {
deltaX = -vp.x;
}
if (vx2 + deltaX >= this._fbWidth) {
deltaX -= vx2 + deltaX - this._fbWidth + 1;
}
if (vp.y + deltaY < 0) {
deltaY = -vp.y;
}
if (vy2 + deltaY >= this._fbHeight) {
deltaY -= (vy2 + deltaY - this._fbHeight + 1);
}
if (deltaX === 0 && deltaY === 0) {
return;
}
Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
}
viewportChangeSize(width, height) {
if ((!this._clipViewport && this._screens.length === 1 ) ||
typeof(width) === "undefined" ||
typeof(height) === "undefined") {
Debug("Setting viewport to full display region");
width = this._fbWidth;
height = this._fbHeight;
}
width = Math.floor(width);
height = Math.floor(height);
if (width > this._fbWidth) {
width = this._fbWidth;
}
if (height > this._fbHeight) {
height = this._fbHeight;
}
const vp = this._screens[0];
const canvas = this._target;
if (canvas.width !== width || canvas.height !== height) {
let saveImg = null;
if (canvas.width > 0 && canvas.height > 0) {
saveImg = this._targetCtx.getImageData(0, 0, canvas.width, canvas.height);
}
vp.serverWidth = width;
vp.serverHeight = height;
canvas.width = width;
canvas.height = height;
if (saveImg) {
this._targetCtx.putImageData(saveImg, 0, 0);
}
// The position might need to be updated if we've grown
this.viewportChangePos(0, 0);
// Update the visible size of the target canvas
this._rescale(this._scale);
}
}
absX(x) {
if (this._scale === 0) {
return 0;
}
return toSigned32bit(x / this._scale + this._screens[0].x);
}
absY(y) {
if (this._scale === 0) {
return 0;
}
return toSigned32bit(y / this._scale + this._screens[0].y);
}
resize(width, height) {
this._prevDrawStyle = "";
this._fbWidth = width;
this._fbHeight = height;
let canvas = this._backbuffer;
if (canvas == undefined) { return; }
if (this._screens.length > 0) {
width = this._screens[0].serverWidth;
height = this._screens[0].serverHeight;
}
if (canvas.width !== width || canvas.height !== height) {
// We have to save the canvas data since changing the size will clear it
let saveImg = null;
if (canvas.width > 0 && canvas.height > 0) {
saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height);
}
if (canvas.width !== width) {
canvas.width = width;
}
if (canvas.height !== height) {
canvas.height = height;
}
if (saveImg) {
this._drawCtx.putImageData(saveImg, 0, 0);
}
}
// Readjust the viewport as it may be incorrectly sized
// and positioned
const vp = this._screens[0];
this.viewportChangeSize(vp.serverWidth, vp.serverHeight);
this.viewportChangePos(0, 0);
}
/*
* Mark the specified frame with a rect count
* @param {number} frame_id - The frame ID of the target frame
* @param {number} rect_cnt - The number of rects in the target frame
*/
flip(frame_id, rect_cnt) {
this._asyncRenderQPush({
'type': 'flip',
'frame_id': frame_id,
'rect_cnt': rect_cnt,
'screenLocations': [ { screenIndex: 0, x: 0, y: 0 } ]
});
}
/*
* Is the frame queue full
* @returns {bool} is the queue full
*/
pending() {
//is the slot in the queue for the newest frame in use
return this._asyncFrameQueue[this._maxAsyncFrameQueue - 1][0] > 0;
}
/*
* Force the oldest frame in the queue to render, whether ready or not.
* @param {bool} onflush_message - The caller wants an onflush event triggered once complete. This is
* useful for TCP, allowing the websocket to block until we are ready to process the next frame.
* UDP cannot block and thus no need to notify the caller when complete.
*/
flush(onflush_message=true) {
//force oldest frame to render
this._asyncFrameComplete(0, true);
if (onflush_message)
this.onflush();
}
/*
* Clears the buffer of anything that has not yet been displayed.
* This must be called when switching between transit modes tcp/udp
*/
clear() {
this._clearAsyncQueue();
}
/*
* Cleans up resources, should be called on a disconnect
*/
dispose() {
clearInterval(this._frameStatsInterval);
this.clear();
}
fillRect(x, y, width, height, color, frame_id, fromQueue) {
if (!fromQueue) {
let rect = {
type: 'fill',
x: x,
y: y,
width: width,
height: height,
color: color,
frame_id: frame_id
};
this._processRectScreens(rect);
this._asyncRenderQPush(rect);
} else {
this._setFillColor(color);
if (this._enableCanvasBuffer) {
this._drawCtx.fillRect(x, y, width, height);
} else {
this._targetCtx.fillRect(x, y, width, height);
}
}
}
copyImage(oldX, oldY, newX, newY, w, h, frame_id, fromQueue) {
if (!fromQueue) {
let rect = {
'type': 'copy',
'oldX': oldX,
'oldY': oldY,
'x': newX,
'y': newY,
'width': w,
'height': h,
'frame_id': frame_id
};
this._processRectScreens(rect);
this._asyncRenderQPush(rect);
} else {
let targetCtx = ((this._enableCanvasBuffer) ? this._drawCtx : this._targetCtx);
let sourceCvs = ((this._enableCanvasBuffer) ? this._backbuffer : this._target);
// Due to this bug among others [1] we need to disable the image-smoothing to
// avoid getting a blur effect when copying data.
//
// 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
//
// We need to set these every time since all properties are reset
// when the the size is changed
targetCtx.mozImageSmoothingEnabled = false;
targetCtx.webkitImageSmoothingEnabled = false;
targetCtx.msImageSmoothingEnabled = false;
targetCtx.imageSmoothingEnabled = false;
targetCtx.drawImage(sourceCvs,
oldX, oldY, w, h,
newX, newY, w, h);
}
}
imageRect(x, y, width, height, mime, arr, frame_id) {
/* The internal logic cannot handle empty images, so bail early */
if ((width === 0) || (height === 0)) {
return;
}
let rect = {
'type': 'img',
'img': null,
'x': x,
'y': y,
'width': width,
'height': height,
'frame_id': frame_id
};
this._processRectScreens(rect);
if (rect.inPrimary) {
const img = new Image();
img.src = "data: " + mime + ";base64," + Base64.encode(arr);
rect.img = img;
} else {
rect.type = "_img";
}
if (rect.inSecondary) {
rect.mime = mime;
rect.src = "data: " + mime + ";base64," + Base64.encode(arr);
}
this._asyncRenderQPush(rect);
}
transparentRect(x, y, width, height, img, frame_id, hashId) {
/* The internal logic cannot handle empty images, so bail early */
if ((width === 0) || (height === 0)) {
return;
}
var rect = {
'type': 'transparent',
'img': null,
'x': x,
'y': y,
'width': width,
'height': height,
'frame_id': frame_id,
'arr': img,
'hash_id': hashId
};
this._processRectScreens(rect);
if (rect.inPrimary) {
let imageBmpPromise = createImageBitmap(img);
imageBmpPromise.then( function(bitmap) {
this._transparentOverlayImg = bitmap;
this.enableCanvasBuffer = true;
}.bind(this) );
}
this._transparentOverlayRect = rect;
this._asyncRenderQPush(rect);
}
dummyRect(x, y, width, height, frame_id) {
let rect = {
'type': 'dummy',
'img': null,
'x': x,
'y': y,
'width': width,
'height': height,
'frame_id': frame_id
};
this._processRectScreens(rect);
this._asyncRenderQPush(rect);
}
blitImage(x, y, width, height, arr, offset, frame_id, fromQueue) {
if (!fromQueue) {
// NB(directxman12): it's technically more performant here to use preallocated arrays,
// but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
// this probably isn't getting called *nearly* as much
const newArr = new Uint8Array(width * height * 4);
newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
let rect = {
'type': 'blit',
'data': newArr,
'x': x,
'y': y,
'width': width,
'height': height,
'frame_id': frame_id
};
this._processRectScreens(rect);
this._asyncRenderQPush(rect);
} else {
// NB(directxman12): arr must be an Type Array view
let data = new Uint8ClampedArray(arr.buffer,
arr.byteOffset + offset,
width * height * 4);
let img = new ImageData(data, width, height);
if (this._enableCanvasBuffer) {
this._drawCtx.putImageData(img, x, y);
} else {
this._targetCtx.putImageData(img, x, y);
}
}
}
blitQoi(x, y, width, height, arr, offset, frame_id, fromQueue) {
if (!fromQueue) {
let rect = {
'type': 'blitQ',
'data': arr,
'x': x,
'y': y,
'width': width,
'height': height,
'frame_id': frame_id
};
this._processRectScreens(rect);
this._asyncRenderQPush(rect);
} else {
if (this._enableCanvasBuffer) {
this._drawCtx.putImageData(arr, x, y);
} else {
this._targetCtx.putImageData(arr, x, y);
}
}
}
drawImage(img, x, y, w, h, overlay=false) {
try {
let targetCtx = ((this._enableCanvasBuffer && !overlay) ? this._drawCtx : this._targetCtx);
if (img.width != w || img.height != h) {
targetCtx.drawImage(img, x, y, w, h);
} else {
targetCtx.drawImage(img, x, y);
}
} catch (error) {
Error$1('Invalid image recieved.'); //KASM-2090
}
}
autoscale(containerWidth, containerHeight, scaleRatio=0) {
if (containerWidth === 0 || containerHeight === 0) {
scaleRatio = 0;
} else if (scaleRatio === 0) {
const vp = this._screens[0];
const targetAspectRatio = containerWidth / containerHeight;
const fbAspectRatio = vp.width / vp.height;
if (fbAspectRatio >= targetAspectRatio) {
scaleRatio = containerWidth / vp.serverWidth;
} else {
scaleRatio = containerHeight / vp.serverHeight;
}
}
this._rescale(scaleRatio);
}
// ===== PRIVATE METHODS =====
_writeCtxBuffer() {
//TODO: KASM-5450 Damage tracking with transparent rect overlay support
if (this._backbuffer.width > 0) {
this._targetCtx.drawImage(this._backbuffer, 0, 0);
}
}
_handleSecondaryDisplayMessage(event) {
if (!this._isPrimaryDisplay && event.data) {
switch (event.data.eventType) {
case 'rect':
let rect = event.data.rect;
//overwrite screen locations when received on the secondary display
rect.screenLocations = [ rect.screenLocations[event.data.screenLocationIndex] ];
rect.screenLocations[0].screenIndex = 0;
switch (rect.type) {
case 'img':
case '_img':
rect.img = new Image();
rect.img.src = rect.src;
rect.type = 'img';