UNPKG

novnc

Version:
673 lines (644 loc) 25.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var Log = _interopRequireWildcard(require("./util/logging.js")); var _base = _interopRequireDefault(require("./base64.js")); var _int = require("./util/int.js"); var _browser = require("./util/browser.js"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } var Display = /*#__PURE__*/function () { function Display(target) { var _this = this; _classCallCheck(this, Display); Log.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) */ this._asyncFrameQueue = []; this._maxAsyncFrameQueue = 3; this._clearAsyncQueue(); 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'); // the visible canvas viewport (i.e. what actually gets seen) this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height }; Log.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 () { var delta = Date.now() - this._lastFlip; if (delta > 0) { this._fps = (this._flipCnt / (delta / 1000)).toFixed(2); } Log.Info('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._scale = 1.0; this._clipViewport = false; this._antiAliasing = 0; this._fps = 0; // ===== EVENT HANDLERS ===== this.onflush = function () {}; // A flush request has finished // Use requestAnimationFrame to write to canvas, to match display refresh rate this._animationFrameID = window.requestAnimationFrame(function () { _this._pushAsyncFrame(); }); Log.Debug("<< Display.constructor"); } // ===== PROPERTIES ===== _createClass(Display, [{ key: "antiAliasing", get: function get() { return this._antiAliasing; }, set: function set(value) { this._antiAliasing = value; this._rescale(this._scale); } }, { key: "scale", get: function get() { return this._scale; }, set: function set(scale) { this._rescale(scale); } }, { key: "clipViewport", get: function get() { return this._clipViewport; }, set: function set(viewport) { this._clipViewport = viewport; // May need to readjust the viewport dimensions var vp = this._viewportLoc; this.viewportChangeSize(vp.w, vp.h); this.viewportChangePos(0, 0); } }, { key: "width", get: function get() { return this._fbWidth; } }, { key: "height", get: function get() { return this._fbHeight; } }, { key: "renderMs", get: function get() { return this._renderMs; }, set: function set(val) { this._renderMs = val; } }, { key: "fps", get: function get() { return this._fps; } // ===== PUBLIC METHODS ===== }, { key: "viewportChangePos", value: function viewportChangePos(deltaX, deltaY) { var vp = this._viewportLoc; deltaX = Math.floor(deltaX); deltaY = Math.floor(deltaY); if (!this._clipViewport) { deltaX = -vp.w; // clamped later of out of bounds deltaY = -vp.h; } var vx2 = vp.x + vp.w - 1; var vy2 = vp.y + vp.h - 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; } Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); } }, { key: "viewportChangeSize", value: function viewportChangeSize(width, height) { if (!this._clipViewport || typeof width === "undefined" || typeof height === "undefined") { Log.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; } var vp = this._viewportLoc; if (vp.w !== width || vp.h !== height) { vp.w = width; vp.h = height; var canvas = this._target; canvas.width = width; canvas.height = height; // 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); } } }, { key: "absX", value: function absX(x) { if (this._scale === 0) { return 0; } return (0, _int.toSigned32bit)(x / this._scale + this._viewportLoc.x); } }, { key: "absY", value: function absY(y) { if (this._scale === 0) { return 0; } return (0, _int.toSigned32bit)(y / this._scale + this._viewportLoc.y); } }, { key: "resize", value: function resize(width, height) { this._prevDrawStyle = ""; this._fbWidth = width; this._fbHeight = height; var canvas = this._target; if (canvas == undefined) { return; } if (canvas.width !== width || canvas.height !== height) { // We have to save the canvas data since changing the size will clear it var saveImg = null; if (canvas.width > 0 && canvas.height > 0) { saveImg = this._targetCtx.getImageData(0, 0, canvas.width, canvas.height); } if (canvas.width !== width) { canvas.width = width; } if (canvas.height !== height) { canvas.height = height; } if (saveImg) { this._targetCtx.putImageData(saveImg, 0, 0); } } // Readjust the viewport as it may be incorrectly sized // and positioned var vp = this._viewportLoc; this.viewportChangeSize(vp.w, vp.h); 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 */ }, { key: "flip", value: function flip(frame_id, rect_cnt) { this._asyncRenderQPush({ 'type': 'flip', 'frame_id': frame_id, 'rect_cnt': rect_cnt }); } /* * Is the frame queue full * @returns {bool} is the queue full */ }, { key: "pending", value: function 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. */ }, { key: "flush", value: function flush() { var onflush_message = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; //force oldest frame to render this._asyncFrameComplete(0, true); if (onflush_message) this._flushing = true; } /* * Clears the buffer of anything that has not yet been displayed. * This must be called when switching between transit modes tcp/udp */ }, { key: "clear", value: function clear() { this._clearAsyncQueue(); } /* * Cleans up resources, should be called on a disconnect */ }, { key: "dispose", value: function dispose() { clearInterval(this._frameStatsInterval); cancelAnimationFrame(this._animationFrameID); this.clear(); } }, { key: "fillRect", value: function fillRect(x, y, width, height, color, frame_id, fromQueue) { if (!fromQueue) { this._asyncRenderQPush({ 'type': 'fill', 'x': x, 'y': y, 'width': width, 'height': height, 'color': color, 'frame_id': frame_id }); } else { this._setFillColor(color); this._targetCtx.fillRect(x, y, width, height); } } }, { key: "copyImage", value: function copyImage(oldX, oldY, newX, newY, w, h, frame_id, fromQueue) { if (!fromQueue) { this._asyncRenderQPush({ 'type': 'copy', 'oldX': oldX, 'oldY': oldY, 'x': newX, 'y': newY, 'width': w, 'height': h, 'frame_id': frame_id }); } else { // 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 this._targetCtx.mozImageSmoothingEnabled = false; this._targetCtx.webkitImageSmoothingEnabled = false; this._targetCtx.msImageSmoothingEnabled = false; this._targetCtx.imageSmoothingEnabled = false; this._targetCtx.drawImage(this._target, oldX, oldY, w, h, newX, newY, w, h); } } }, { key: "imageRect", value: function 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; } var img = new Image(); img.src = "data: " + mime + ";base64," + _base["default"].encode(arr); this._asyncRenderQPush({ 'type': 'img', 'img': img, 'x': x, 'y': y, 'width': width, 'height': height, 'frame_id': frame_id }); } }, { key: "blitImage", value: function 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 var newArr = new Uint8Array(width * height * 4); newArr.set(new Uint8Array(arr.buffer, 0, newArr.length)); this._asyncRenderQPush({ 'type': 'blit', 'data': newArr, 'x': x, 'y': y, 'width': width, 'height': height, 'frame_id': frame_id }); } else { // NB(directxman12): arr must be an Type Array view var data = new Uint8ClampedArray(arr.buffer, arr.byteOffset + offset, width * height * 4); var img = new ImageData(data, width, height); this._targetCtx.putImageData(img, x, y); } } }, { key: "blitQoi", value: function blitQoi(x, y, width, height, arr, offset, frame_id, fromQueue) { if (!fromQueue) { this._asyncRenderQPush({ 'type': 'blitQ', 'data': arr, 'x': x, 'y': y, 'width': width, 'height': height, 'frame_id': frame_id }); } else { this._targetCtx.putImageData(arr, x, y); } } }, { key: "drawImage", value: function drawImage(img, x, y, w, h) { try { if (img.width != w || img.height != h) { this._targetCtx.drawImage(img, x, y, w, h); } else { this._targetCtx.drawImage(img, x, y); } } catch (error) { Log.Error('Invalid image recieved.'); //KASM-2090 } } }, { key: "autoscale", value: function autoscale(containerWidth, containerHeight) { var scaleRatio = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; if (containerWidth === 0 || containerHeight === 0) { scaleRatio = 0; } else if (scaleRatio === 0) { var vp = this._viewportLoc; var targetAspectRatio = containerWidth / containerHeight; var fbAspectRatio = vp.w / vp.h; if (fbAspectRatio >= targetAspectRatio) { scaleRatio = containerWidth / vp.w; } else { scaleRatio = containerHeight / vp.h; } } this._rescale(scaleRatio); } // ===== PRIVATE METHODS ===== /* Process incoming rects into a frame buffer, assume rects are out of order due to either UDP or parallel processing of decoding */ }, { key: "_asyncRenderQPush", value: function _asyncRenderQPush(rect) { var frameIx = -1; var oldestFrameID = Number.MAX_SAFE_INTEGER; var newestFrameID = 0; for (var i = 0; i < this._asyncFrameQueue.length; i++) { if (rect.frame_id == this._asyncFrameQueue[i][0]) { this._asyncFrameQueue[i][2].push(rect); frameIx = i; break; } else if (this._asyncFrameQueue[i][0] == 0) { var rect_cnt = rect.type == "flip" ? rect.rect_cnt : 0; this._asyncFrameQueue[i][0] = rect.frame_id; this._asyncFrameQueue[i][2].push(rect); this._asyncFrameQueue[i][3] = rect_cnt == 1; frameIx = i; break; } oldestFrameID = Math.min(oldestFrameID, this._asyncFrameQueue[i][0]); newestFrameID = Math.max(newestFrameID, this._asyncFrameQueue[i][0]); } if (frameIx >= 0) { if (rect.type == "flip") { //flip rect contains the rect count for the frame if (this._asyncFrameQueue[frameIx][1] !== 0) { Log.Warn("Redundant flip rect, current rect_cnt: " + this._asyncFrameQueue[frameIx][1] + ", new rect_cnt: " + rect.rect_cnt); } this._asyncFrameQueue[frameIx][1] = rect.rect_cnt; if (rect.rect_cnt == 0) { Log.Warn("Invalid rect count"); } } if (this._asyncFrameQueue[frameIx][1] == this._asyncFrameQueue[frameIx][2].length) { //frame is complete this._asyncFrameComplete(frameIx); } } else { if (rect.frame_id < oldestFrameID) { //rect is older than any frame in the queue, drop it this._droppedRects++; if (rect.type == "flip") { this._lateFlipRect++; } return; } else if (rect.frame_id > newestFrameID) { //frame is newer than any frame in the queue, drop old frames this._asyncFrameQueue.shift(); var _rect_cnt = rect.type == "flip" ? rect.rect_cnt : 0; this._asyncFrameQueue.push([rect.frame_id, _rect_cnt, [rect], _rect_cnt == 1, 0]); this._droppedFrames++; } } } /* Clear the async frame buffer */ }, { key: "_clearAsyncQueue", value: function _clearAsyncQueue() { this._droppedFrames += this._asyncFrameQueue.length; this._asyncFrameQueue = []; for (var i = 0; i < this._maxAsyncFrameQueue; i++) { this._asyncFrameQueue.push([0, 0, [], false, 0]); } } /* Pre-processing required before displaying a finished frame If marked force, unloaded images will be skipped and the frame will be marked complete and ready for rendering */ }, { key: "_asyncFrameComplete", value: function _asyncFrameComplete(frameIx) { var _this2 = this; var force = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; var currentFrameRectIx = this._asyncFrameQueue[frameIx][4]; if (force) { if (this._asyncFrameQueue[frameIx][1] == 0) { this._missingFlipRect++; //at minimum the flip rect is missing } else if (this._asyncFrameQueue[frameIx][1] !== this._asyncFrameQueue[frameIx][2].length) { this._droppedRects += this._asyncFrameQueue[frameIx][1] - this._asyncFrameQueue[frameIx][2].length; if (this._asyncFrameQueue[frameIx][2].length > this._asyncFrameQueue[frameIx][1]) { Log.Warn("Frame has more rects than the reported rect_cnt."); } } while (currentFrameRectIx < this._asyncFrameQueue[frameIx][2].length) { if (this._asyncFrameQueue[frameIx][2][currentFrameRectIx].type == 'img' && !this._asyncFrameQueue[frameIx][2][currentFrameRectIx].img.complete) { this._asyncFrameQueue[frameIx][2][currentFrameRectIx].type = 'skip'; this._droppedRects++; } currentFrameRectIx++; } } else { while (currentFrameRectIx < this._asyncFrameQueue[frameIx][2].length) { if (this._asyncFrameQueue[frameIx][2][currentFrameRectIx].type == 'img' && !this._asyncFrameQueue[frameIx][2][currentFrameRectIx].img.complete) { this._asyncFrameQueue[frameIx][2][currentFrameRectIx].img.addEventListener('load', function () { _this2._asyncFrameComplete(frameIx); }); this._asyncFrameQueue[frameIx][4] = currentFrameRectIx; return; } currentFrameRectIx++; } } this._asyncFrameQueue[frameIx][4] = currentFrameRectIx; this._asyncFrameQueue[frameIx][3] = true; } /* Push the oldest frame in the buffer to the canvas if it is marked ready */ }, { key: "_pushAsyncFrame", value: function _pushAsyncFrame() { var _this3 = this; var force = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; if (this._asyncFrameQueue[0][3]) { var frame = this._asyncFrameQueue.shift()[2]; if (this._asyncFrameQueue.length < this._maxAsyncFrameQueue) { this._asyncFrameQueue.push([0, 0, [], false, 0]); } //render the selected frame for (var i = 0; i < frame.length; i++) { var a = frame[i]; switch (a.type) { case 'copy': this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, a.frame_id, true); break; case 'fill': this.fillRect(a.x, a.y, a.width, a.height, a.color, a.frame_id, true); break; case 'blit': this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, a.frame_id, true); break; case 'blitQ': this.blitQoi(a.x, a.y, a.width, a.height, a.data, 0, a.frame_id, true); break; case 'img': this.drawImage(a.img, a.x, a.y, a.width, a.height); break; } } this._flipCnt += 1; if (this._flushing) { this._flushing = false; this.onflush(); } } if (!force) { window.requestAnimationFrame(function () { _this3._pushAsyncFrame(); }); } } }, { key: "_rescale", value: function _rescale(factor) { this._scale = factor; var vp = this._viewportLoc; // NB(directxman12): If you set the width directly, or set the // style width to a number, the canvas is cleared. // However, if you set the style width to a string // ('NNNpx'), the canvas is scaled without clearing. var width = factor * vp.w + 'px'; var height = factor * vp.h + 'px'; if (this._target.style.width !== width || this._target.style.height !== height) { this._target.style.width = width; this._target.style.height = height; } Log.Info('Pixel Ratio: ' + window.devicePixelRatio + ', VNC Scale: ' + factor + 'VNC Res: ' + vp.w + 'x' + vp.h); var pixR = Math.abs(Math.ceil(window.devicePixelRatio)); var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; if (this.antiAliasing === 2 || this.antiAliasing === 0 && factor === 1 && this._target.style.imageRendering !== 'pixelated' && pixR === window.devicePixelRatio && vp.w > 0) { this._target.style.imageRendering = !isFirefox ? 'pixelated' : 'crisp-edges'; Log.Debug('Smoothing disabled'); } else if (this.antiAliasing === 1 || this.antiAliasing === 0 && factor !== 1 && this._target.style.imageRendering !== 'auto') { this._target.style.imageRendering = 'auto'; //auto is really smooth (blurry) using trilinear of linear Log.Debug('Smoothing enabled'); } } }, { key: "_setFillColor", value: function _setFillColor(color) { var newStyle = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')'; if (newStyle !== this._prevDrawStyle) { this._targetCtx.fillStyle = newStyle; this._prevDrawStyle = newStyle; } } }]); return Display; }(); exports["default"] = Display;