UNPKG

react-kasmvnc

Version:

A React Component to connect to a websockified VNC client using noVNC.

1,474 lines (1,248 loc) 644 kB
'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';