Major refactor of display class, support for QOI (#43)
* Major refactor of display.js queue to assume async rect processing in order to accommodate threaded decode or udp * QOI lossless decoder on worker threads using WASM * rfb.js class to provide frame_id and rect counts per frame Co-authored-by: ryan.kuba <ryan.kuba@kasmweb.com> Co-authored-by: matt <matt@kasmweb.com>
This commit is contained in:
parent
9a95983382
commit
9f240fc7b1
43
app/ui.js
43
app/ui.js
|
@ -245,6 +245,7 @@ const UI = {
|
||||||
UI.initSetting('enable_perf_stats', false);
|
UI.initSetting('enable_perf_stats', false);
|
||||||
UI.initSetting('virtual_keyboard_visible', false);
|
UI.initSetting('virtual_keyboard_visible', false);
|
||||||
UI.initSetting('enable_ime', false);
|
UI.initSetting('enable_ime', false);
|
||||||
|
UI.initSetting('enable_qoi', false);
|
||||||
UI.initSetting('enable_webrtc', false);
|
UI.initSetting('enable_webrtc', false);
|
||||||
UI.toggleKeyboardControls();
|
UI.toggleKeyboardControls();
|
||||||
|
|
||||||
|
@ -558,6 +559,8 @@ const UI = {
|
||||||
UI.addSettingChangeHandler('virtual_keyboard_visible', UI.toggleKeyboardControls);
|
UI.addSettingChangeHandler('virtual_keyboard_visible', UI.toggleKeyboardControls);
|
||||||
UI.addSettingChangeHandler('enable_ime');
|
UI.addSettingChangeHandler('enable_ime');
|
||||||
UI.addSettingChangeHandler('enable_ime', UI.toggleIMEMode);
|
UI.addSettingChangeHandler('enable_ime', UI.toggleIMEMode);
|
||||||
|
UI.addSettingChangeHandler('enable_qoi');
|
||||||
|
UI.addSettingChangeHandler('enable_qoi', UI.toggleQOI);
|
||||||
UI.addSettingChangeHandler('enable_webrtc');
|
UI.addSettingChangeHandler('enable_webrtc');
|
||||||
UI.addSettingChangeHandler('enable_webrtc', UI.toggleWebRTC);
|
UI.addSettingChangeHandler('enable_webrtc', UI.toggleWebRTC);
|
||||||
},
|
},
|
||||||
|
@ -1399,6 +1402,7 @@ const UI = {
|
||||||
UI.rfb.clipboardSeamless = UI.getSetting('clipboard_seamless');
|
UI.rfb.clipboardSeamless = UI.getSetting('clipboard_seamless');
|
||||||
UI.rfb.keyboard.enableIME = UI.getSetting('enable_ime');
|
UI.rfb.keyboard.enableIME = UI.getSetting('enable_ime');
|
||||||
UI.rfb.clipboardBinary = supportsBinaryClipboard() && UI.rfb.clipboardSeamless;
|
UI.rfb.clipboardBinary = supportsBinaryClipboard() && UI.rfb.clipboardSeamless;
|
||||||
|
UI.rfb.enableQOI = UI.getSetting('enable_qoi');
|
||||||
UI.rfb.enableWebRTC = UI.getSetting('enable_webrtc');
|
UI.rfb.enableWebRTC = UI.getSetting('enable_webrtc');
|
||||||
UI.rfb.mouseButtonMapper = UI.initMouseButtonMapper();
|
UI.rfb.mouseButtonMapper = UI.initMouseButtonMapper();
|
||||||
|
|
||||||
|
@ -1651,6 +1655,18 @@ const UI = {
|
||||||
UI.toggleIMEMode();
|
UI.toggleIMEMode();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'disable_qoi':
|
||||||
|
if(UI.getSetting('enable_qoi')) {
|
||||||
|
UI.forceSetting('enable_qoi', false, false);
|
||||||
|
}
|
||||||
|
UI.toggleQOI();
|
||||||
|
break;
|
||||||
|
case 'enable_qoi':
|
||||||
|
if(!UI.getSetting('enable_qoi')) {
|
||||||
|
UI.forceSetting('enable_qoi', true, false);
|
||||||
|
}
|
||||||
|
UI.toggleQOI();
|
||||||
|
break;
|
||||||
case 'enable_webrtc':
|
case 'enable_webrtc':
|
||||||
if (!UI.getSetting('enable_webrtc')) {
|
if (!UI.getSetting('enable_webrtc')) {
|
||||||
UI.forceSetting('enable_webrtc', true, false);
|
UI.forceSetting('enable_webrtc', true, false);
|
||||||
|
@ -1961,6 +1977,8 @@ const UI = {
|
||||||
UI.enableSetting('video_out_time');
|
UI.enableSetting('video_out_time');
|
||||||
UI.showStatus("Refresh or reconnect to apply changes.");
|
UI.showStatus("Refresh or reconnect to apply changes.");
|
||||||
return;
|
return;
|
||||||
|
case 5: //extreme+lossless
|
||||||
|
UI.forceSetting('enable_qoi', true, false);
|
||||||
case 4: //extreme
|
case 4: //extreme
|
||||||
UI.forceSetting('dynamic_quality_min', 9);
|
UI.forceSetting('dynamic_quality_min', 9);
|
||||||
UI.forceSetting('dynamic_quality_max', 9);
|
UI.forceSetting('dynamic_quality_max', 9);
|
||||||
|
@ -2024,6 +2042,12 @@ const UI = {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//force QOI off if mode is below extreme
|
||||||
|
if (present_mode !== 4 && UI.getSetting('enable_qoi')) {
|
||||||
|
UI.showStatus("Lossless QOI disabled when not in extreme quality mode.");
|
||||||
|
UI.forceSetting('enable_qoi', false, false);
|
||||||
|
}
|
||||||
|
|
||||||
if (UI.rfb) {
|
if (UI.rfb) {
|
||||||
UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
|
UI.rfb.qualityLevel = parseInt(UI.getSetting('quality'));
|
||||||
UI.rfb.antiAliasing = parseInt(UI.getSetting('anti_aliasing'));
|
UI.rfb.antiAliasing = parseInt(UI.getSetting('anti_aliasing'));
|
||||||
|
@ -2041,6 +2065,7 @@ const UI = {
|
||||||
UI.rfb.frameRate = parseInt(UI.getSetting('framerate'));
|
UI.rfb.frameRate = parseInt(UI.getSetting('framerate'));
|
||||||
UI.rfb.enableWebP = UI.getSetting('enable_webp');
|
UI.rfb.enableWebP = UI.getSetting('enable_webp');
|
||||||
UI.rfb.videoQuality = parseInt(UI.getSetting('video_quality'));
|
UI.rfb.videoQuality = parseInt(UI.getSetting('video_quality'));
|
||||||
|
UI.rfb.enableQOI = UI.getSetting('enable_qoi');
|
||||||
|
|
||||||
// Gracefully update settings server side
|
// Gracefully update settings server side
|
||||||
UI.rfb.updateConnectionSettings();
|
UI.rfb.updateConnectionSettings();
|
||||||
|
@ -2096,9 +2121,27 @@ const UI = {
|
||||||
} else {
|
} else {
|
||||||
UI.rfb.enableWebRTC = false;
|
UI.rfb.enableWebRTC = false;
|
||||||
}
|
}
|
||||||
|
UI.updateQuality();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleQOI() {
|
||||||
|
if(UI.rfb) {
|
||||||
|
if(UI.getSetting('enable_qoi')) {
|
||||||
|
UI.rfb.enableQOI = true;
|
||||||
|
if (!UI.rfb.enableQOI) {
|
||||||
|
UI.showStatus("Enabling QOI failed, browser may not be compatible with WASM and/or Workers.");
|
||||||
|
UI.forceSetting('enable_qoi', false, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UI.forceSetting('video_quality', 4, false); //force into extreme quality mode
|
||||||
|
} else {
|
||||||
|
UI.rfb.enableQOI = false;
|
||||||
|
}
|
||||||
|
UI.updateQuality();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
showKeyboardControls() {
|
showKeyboardControls() {
|
||||||
document.getElementById('noVNC_keyboard_control').classList.add("is-visible");
|
document.getElementById('noVNC_keyboard_control').classList.add("is-visible");
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,7 +8,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default class CopyRectDecoder {
|
export default class CopyRectDecoder {
|
||||||
decodeRect(x, y, width, height, sock, display, depth) {
|
|
||||||
|
decodeRect(x, y, width, height, sock, display, depth, frame_id) {
|
||||||
if (sock.rQwait("COPYRECT", 4)) {
|
if (sock.rQwait("COPYRECT", 4)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -20,7 +21,7 @@ export default class CopyRectDecoder {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
display.copyImage(deltaX, deltaY, x, y, width, height);
|
display.copyImage(deltaX, deltaY, x, y, width, height, frame_id);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default class HextileDecoder {
|
||||||
this._tileBuffer = new Uint8Array(16 * 16 * 4);
|
this._tileBuffer = new Uint8Array(16 * 16 * 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeRect(x, y, width, height, sock, display, depth) {
|
decodeRect(x, y, width, height, sock, display, depth, frame_id) {
|
||||||
if (this._tiles === 0) {
|
if (this._tiles === 0) {
|
||||||
this._tilesX = Math.ceil(width / 16);
|
this._tilesX = Math.ceil(width / 16);
|
||||||
this._tilesY = Math.ceil(height / 16);
|
this._tilesY = Math.ceil(height / 16);
|
||||||
|
@ -85,7 +85,7 @@ export default class HextileDecoder {
|
||||||
// Weird: ignore blanks are RAW
|
// Weird: ignore blanks are RAW
|
||||||
Log.Debug(" Ignoring blank after RAW");
|
Log.Debug(" Ignoring blank after RAW");
|
||||||
} else {
|
} else {
|
||||||
display.fillRect(tx, ty, tw, th, this._background);
|
display.fillRect(tx, ty, tw, th, this._background, frame_id);
|
||||||
}
|
}
|
||||||
} else if (subencoding & 0x01) { // Raw
|
} else if (subencoding & 0x01) { // Raw
|
||||||
let pixels = tw * th;
|
let pixels = tw * th;
|
||||||
|
@ -93,7 +93,7 @@ export default class HextileDecoder {
|
||||||
for (let i = 0;i < pixels;i++) {
|
for (let i = 0;i < pixels;i++) {
|
||||||
rQ[rQi + i * 4 + 3] = 255;
|
rQ[rQi + i * 4 + 3] = 255;
|
||||||
}
|
}
|
||||||
display.blitImage(tx, ty, tw, th, rQ, rQi);
|
display.blitImage(tx, ty, tw, th, rQ, rQi, frame_id);
|
||||||
rQi += bytes - 1;
|
rQi += bytes - 1;
|
||||||
} else {
|
} else {
|
||||||
if (subencoding & 0x02) { // Background
|
if (subencoding & 0x02) { // Background
|
||||||
|
@ -131,7 +131,7 @@ export default class HextileDecoder {
|
||||||
this._subTile(sx, sy, sw, sh, color);
|
this._subTile(sx, sy, sw, sh, color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this._finishTile(display);
|
this._finishTile(display, frame_id);
|
||||||
}
|
}
|
||||||
sock.rQi = rQi;
|
sock.rQi = rQi;
|
||||||
this._lastsubencoding = subencoding;
|
this._lastsubencoding = subencoding;
|
||||||
|
@ -183,9 +183,9 @@ export default class HextileDecoder {
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw the current tile to the screen
|
// draw the current tile to the screen
|
||||||
_finishTile(display) {
|
_finishTile(display, frame_id) {
|
||||||
display.blitImage(this._tileX, this._tileY,
|
display.blitImage(this._tileX, this._tileY,
|
||||||
this._tileW, this._tileH,
|
this._tileW, this._tileH,
|
||||||
this._tileBuffer, 0);
|
this._tileBuffer, 0, frame_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,342 @@
|
||||||
|
let wasm;
|
||||||
|
const heap = new Array(32).fill(undefined);
|
||||||
|
heap.push(undefined, null, true, false);
|
||||||
|
|
||||||
|
function getObject(idx) {
|
||||||
|
return heap[idx];
|
||||||
|
}
|
||||||
|
let heap_next = heap.length;
|
||||||
|
|
||||||
|
function dropObject(idx) {
|
||||||
|
if (idx < 36) return;
|
||||||
|
heap[idx] = heap_next;
|
||||||
|
heap_next = idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function takeObject(idx) {
|
||||||
|
const ret = getObject(idx);
|
||||||
|
dropObject(idx);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
const cachedTextDecoder = new TextDecoder('utf-8', {
|
||||||
|
ignoreBOM: true,
|
||||||
|
fatal: true
|
||||||
|
});
|
||||||
|
cachedTextDecoder.decode();
|
||||||
|
let cachegetUint8Memory0 = null;
|
||||||
|
|
||||||
|
function getUint8Memory0() {
|
||||||
|
if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) {
|
||||||
|
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
|
||||||
|
}
|
||||||
|
return cachegetUint8Memory0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStringFromWasm0(ptr, len) {
|
||||||
|
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachegetInt32Memory0 = null;
|
||||||
|
|
||||||
|
function getInt32Memory0() {
|
||||||
|
if (cachegetInt32Memory0 === null || cachegetInt32Memory0.buffer !== wasm.memory.buffer) {
|
||||||
|
cachegetInt32Memory0 = new Int32Array(wasm.memory.buffer);
|
||||||
|
}
|
||||||
|
return cachegetInt32Memory0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArrayU8FromWasm0(ptr, len) {
|
||||||
|
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
|
||||||
|
}
|
||||||
|
|
||||||
|
let WASM_VECTOR_LEN = 0;
|
||||||
|
|
||||||
|
function passArray8ToWasm0(arg, malloc) {
|
||||||
|
const ptr = malloc(arg.length * 1);
|
||||||
|
getUint8Memory0().set(arg, ptr / 1);
|
||||||
|
WASM_VECTOR_LEN = arg.length;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {ImageData}
|
||||||
|
*/
|
||||||
|
function decode_qoi(bytes) {
|
||||||
|
try {
|
||||||
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||||
|
const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_malloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
wasm.decode_qoi(retptr, ptr0, len0);
|
||||||
|
var r0 = getInt32Memory0()[retptr / 4 + 0];
|
||||||
|
var r1 = getInt32Memory0()[retptr / 4 + 1];
|
||||||
|
var r2 = getInt32Memory0()[retptr / 4 + 2];
|
||||||
|
if (r2) {
|
||||||
|
throw takeObject(r1);
|
||||||
|
}
|
||||||
|
return takeObject(r0);
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addHeapObject(obj) {
|
||||||
|
if (heap_next === heap.length) heap.push(heap.length + 1);
|
||||||
|
const idx = heap_next;
|
||||||
|
heap_next = heap[idx];
|
||||||
|
|
||||||
|
heap[idx] = obj;
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedTextEncoder = new TextEncoder('utf-8');
|
||||||
|
|
||||||
|
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' ?
|
||||||
|
function(arg, view) {
|
||||||
|
return cachedTextEncoder.encodeInto(arg, view);
|
||||||
|
} :
|
||||||
|
function(arg, view) {
|
||||||
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
|
view.set(buf);
|
||||||
|
return {
|
||||||
|
read: arg.length,
|
||||||
|
written: buf.length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function passStringToWasm0(arg, malloc, realloc) {
|
||||||
|
|
||||||
|
if (realloc === undefined) {
|
||||||
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
|
const ptr = malloc(buf.length);
|
||||||
|
getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||||
|
WASM_VECTOR_LEN = buf.length;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = arg.length;
|
||||||
|
let ptr = malloc(len);
|
||||||
|
|
||||||
|
const mem = getUint8Memory0();
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (; offset < len; offset++) {
|
||||||
|
const code = arg.charCodeAt(offset);
|
||||||
|
if (code > 0x7F) break;
|
||||||
|
mem[ptr + offset] = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset !== len) {
|
||||||
|
if (offset !== 0) {
|
||||||
|
arg = arg.slice(offset);
|
||||||
|
}
|
||||||
|
ptr = realloc(ptr, len, len = offset + arg.length * 3);
|
||||||
|
const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
|
||||||
|
const ret = encodeString(arg, view);
|
||||||
|
|
||||||
|
offset += ret.written;
|
||||||
|
}
|
||||||
|
|
||||||
|
WASM_VECTOR_LEN = offset;
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachegetUint8ClampedMemory0 = null;
|
||||||
|
|
||||||
|
function getUint8ClampedMemory0() {
|
||||||
|
if (cachegetUint8ClampedMemory0 === null || cachegetUint8ClampedMemory0.buffer !== wasm.memory.buffer) {
|
||||||
|
cachegetUint8ClampedMemory0 = new Uint8ClampedArray(wasm.memory.buffer);
|
||||||
|
}
|
||||||
|
return cachegetUint8ClampedMemory0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClampedArrayU8FromWasm0(ptr, len) {
|
||||||
|
return getUint8ClampedMemory0().subarray(ptr / 1, ptr / 1 + len);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleError(f, args) {
|
||||||
|
try {
|
||||||
|
return f.apply(this, args);
|
||||||
|
} catch (e) {
|
||||||
|
wasm.__wbindgen_exn_store(addHeapObject(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
class QoiImage {
|
||||||
|
|
||||||
|
static __wrap(ptr) {
|
||||||
|
const obj = Object.create(QoiImage.prototype);
|
||||||
|
obj.ptr = ptr;
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
__destroy_into_raw() {
|
||||||
|
const ptr = this.ptr;
|
||||||
|
this.ptr = 0;
|
||||||
|
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
free() {
|
||||||
|
const ptr = this.__destroy_into_raw();
|
||||||
|
wasm.__wbg_qoiimage_free(ptr);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
const ret = wasm.qoiimage_new();
|
||||||
|
return QoiImage.__wrap(ret);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get_width() {
|
||||||
|
const ret = wasm.qoiimage_get_width(this.ptr);
|
||||||
|
return ret >>> 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get_height() {
|
||||||
|
const ret = wasm.qoiimage_get_height(this.ptr);
|
||||||
|
return ret >>> 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get_channels() {
|
||||||
|
const ret = wasm.qoiimage_get_channels(this.ptr);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
get_bytes() {
|
||||||
|
try {
|
||||||
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
||||||
|
wasm.qoiimage_get_bytes(retptr, this.ptr);
|
||||||
|
var r0 = getInt32Memory0()[retptr / 4 + 0];
|
||||||
|
var r1 = getInt32Memory0()[retptr / 4 + 1];
|
||||||
|
var v0 = getArrayU8FromWasm0(r0, r1).slice();
|
||||||
|
wasm.__wbindgen_free(r0, r1 * 1);
|
||||||
|
return v0;
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_add_to_stack_pointer(16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load(module, imports) {
|
||||||
|
if (typeof Response === 'function' && module instanceof Response) {
|
||||||
|
const bytes = await module.arrayBuffer();
|
||||||
|
return await WebAssembly.instantiate(bytes, imports);
|
||||||
|
} else {
|
||||||
|
const instance = await WebAssembly.instantiate(module, imports);
|
||||||
|
|
||||||
|
if (instance instanceof WebAssembly.Instance) {
|
||||||
|
return {
|
||||||
|
instance,
|
||||||
|
module
|
||||||
|
};
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init(input) {
|
||||||
|
if (typeof input === 'undefined') {
|
||||||
|
input = '/core/decoders/qoi/qoi_viewer_bg.wasm';
|
||||||
|
}
|
||||||
|
const imports = {};
|
||||||
|
imports.wbg = {};
|
||||||
|
imports.wbg.__wbg_new_693216e109162396 = function() {
|
||||||
|
const ret = new Error();
|
||||||
|
return addHeapObject(ret);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_stack_0ddaca5d1abfb52f = function(arg0, arg1) {
|
||||||
|
const ret = getObject(arg1).stack;
|
||||||
|
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
getInt32Memory0()[arg0 / 4 + 1] = len0;
|
||||||
|
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_error_09919627ac0992f5 = function(arg0, arg1) {
|
||||||
|
try {
|
||||||
|
console.error(getStringFromWasm0(arg0, arg1));
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_free(arg0, arg1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
|
||||||
|
takeObject(arg0);
|
||||||
|
};
|
||||||
|
imports.wbg.__wbg_newwithu8clampedarrayandsh_87d2f0a48030f922 = function() {
|
||||||
|
return handleError(function(arg0, arg1, arg2, arg3) {
|
||||||
|
const ret = new ImageData(getClampedArrayU8FromWasm0(arg0, arg1), arg2 >>> 0, arg3 >>> 0);
|
||||||
|
return addHeapObject(ret);
|
||||||
|
}, arguments)
|
||||||
|
};
|
||||||
|
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
|
||||||
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||||
|
};
|
||||||
|
if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
|
||||||
|
input = fetch(input);
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
instance,
|
||||||
|
module
|
||||||
|
} = await load(await input, imports);
|
||||||
|
wasm = instance.exports;
|
||||||
|
init.__wbindgen_wasm_module = module;
|
||||||
|
return wasm;
|
||||||
|
}
|
||||||
|
|
||||||
|
var arr;
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
self.addEventListener('message', function(evt) {
|
||||||
|
try {
|
||||||
|
let length = evt.data.length;
|
||||||
|
let data = new Uint8Array(evt.data.sab.slice(0, length));
|
||||||
|
let resultData = decode_qoi(data);
|
||||||
|
if (!arr) {
|
||||||
|
arr = new Uint8Array(evt.data.sabR);
|
||||||
|
}
|
||||||
|
let lengthR = resultData.data.length;
|
||||||
|
arr.set(resultData.data);
|
||||||
|
let img = {
|
||||||
|
colorSpace: resultData.colorSpace,
|
||||||
|
width: resultData.width,
|
||||||
|
height: resultData.height
|
||||||
|
};
|
||||||
|
self.postMessage({
|
||||||
|
result: 0,
|
||||||
|
img: img,
|
||||||
|
length: lengthR,
|
||||||
|
width: evt.data.width,
|
||||||
|
height: evt.data.height,
|
||||||
|
x: evt.data.x,
|
||||||
|
y: evt.data.y,
|
||||||
|
frame_id: evt.data.frame_id
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
self.postMessage({
|
||||||
|
result: 2,
|
||||||
|
error: err
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
await init();
|
||||||
|
|
||||||
|
//Send message that worker is ready
|
||||||
|
self.postMessage({
|
||||||
|
result: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
Binary file not shown.
|
@ -12,7 +12,7 @@ export default class RawDecoder {
|
||||||
this._lines = 0;
|
this._lines = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeRect(x, y, width, height, sock, display, depth) {
|
decodeRect(x, y, width, height, sock, display, depth, frame_id) {
|
||||||
if ((width === 0) || (height === 0)) {
|
if ((width === 0) || (height === 0)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ export default class RawDecoder {
|
||||||
data[i * 4 + 3] = 255;
|
data[i * 4 + 3] = 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
display.blitImage(x, curY, width, currHeight, data, index);
|
display.blitImage(x, curY, width, currHeight, data, index, frame_id);
|
||||||
sock.rQskipBytes(currHeight * bytesPerLine);
|
sock.rQskipBytes(currHeight * bytesPerLine);
|
||||||
this._lines -= currHeight;
|
this._lines -= currHeight;
|
||||||
if (this._lines > 0) {
|
if (this._lines > 0) {
|
||||||
|
|
|
@ -12,7 +12,7 @@ export default class RREDecoder {
|
||||||
this._subrects = 0;
|
this._subrects = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeRect(x, y, width, height, sock, display, depth) {
|
decodeRect(x, y, width, height, sock, display, depth, frame_id) {
|
||||||
if (this._subrects === 0) {
|
if (this._subrects === 0) {
|
||||||
if (sock.rQwait("RRE", 4 + 4)) {
|
if (sock.rQwait("RRE", 4 + 4)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -34,7 +34,7 @@ export default class RREDecoder {
|
||||||
let sy = sock.rQshift16();
|
let sy = sock.rQshift16();
|
||||||
let swidth = sock.rQshift16();
|
let swidth = sock.rQshift16();
|
||||||
let sheight = sock.rQshift16();
|
let sheight = sock.rQshift16();
|
||||||
display.fillRect(x + sx, y + sy, swidth, sheight, color);
|
display.fillRect(x + sx, y + sy, swidth, sheight, color, frame_id);
|
||||||
|
|
||||||
this._subrects--;
|
this._subrects--;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,14 @@ import * as Log from '../util/logging.js';
|
||||||
import Inflator from "../inflator.js";
|
import Inflator from "../inflator.js";
|
||||||
|
|
||||||
export default class TightDecoder {
|
export default class TightDecoder {
|
||||||
constructor() {
|
constructor(display) {
|
||||||
this._ctl = null;
|
this._ctl = null;
|
||||||
this._filter = null;
|
this._filter = null;
|
||||||
this._numColors = 0;
|
this._numColors = 0;
|
||||||
this._palette = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel)
|
this._palette = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel)
|
||||||
this._len = 0;
|
this._len = 0;
|
||||||
|
this._enableQOI = false;
|
||||||
|
this._displayGlobal = display;
|
||||||
|
|
||||||
this._zlibs = [];
|
this._zlibs = [];
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
|
@ -25,7 +27,14 @@ export default class TightDecoder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeRect(x, y, width, height, sock, display, depth) {
|
enableQOI() {
|
||||||
|
if (!this._enableQOI) {
|
||||||
|
this._enableQOIWorkers();
|
||||||
|
}
|
||||||
|
return this._enableQOI; //did it succeed
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeRect(x, y, width, height, sock, display, depth, frame_id) {
|
||||||
if (this._ctl === null) {
|
if (this._ctl === null) {
|
||||||
if (sock.rQwait("TIGHT compression-control", 1)) {
|
if (sock.rQwait("TIGHT compression-control", 1)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -37,7 +46,7 @@ export default class TightDecoder {
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
if ((this._ctl >> i) & 1) {
|
if ((this._ctl >> i) & 1) {
|
||||||
this._zlibs[i].reset();
|
this._zlibs[i].reset();
|
||||||
Log.Debug("Reset zlib stream " + i);
|
Log.Info("Reset zlib stream " + i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,19 +58,22 @@ export default class TightDecoder {
|
||||||
|
|
||||||
if (this._ctl === 0x08) {
|
if (this._ctl === 0x08) {
|
||||||
ret = this._fillRect(x, y, width, height,
|
ret = this._fillRect(x, y, width, height,
|
||||||
sock, display, depth);
|
sock, display, depth, frame_id);
|
||||||
} else if (this._ctl === 0x09) {
|
} else if (this._ctl === 0x09) {
|
||||||
ret = this._jpegRect(x, y, width, height,
|
ret = this._jpegRect(x, y, width, height,
|
||||||
sock, display, depth);
|
sock, display, depth, frame_id);
|
||||||
} else if (this._ctl === 0x0A) {
|
} else if (this._ctl === 0x0A) {
|
||||||
ret = this._pngRect(x, y, width, height,
|
ret = this._pngRect(x, y, width, height,
|
||||||
sock, display, depth);
|
sock, display, depth, frame_id);
|
||||||
} else if ((this._ctl & 0x08) == 0) {
|
} else if ((this._ctl & 0x08) == 0) {
|
||||||
ret = this._basicRect(this._ctl, x, y, width, height,
|
ret = this._basicRect(this._ctl, x, y, width, height,
|
||||||
sock, display, depth);
|
sock, display, depth, frame_id);
|
||||||
} else if (this._ctl === 0x0B) {
|
} else if (this._ctl === 0x0B) {
|
||||||
ret = this._webpRect(x, y, width, height,
|
ret = this._webpRect(x, y, width, height,
|
||||||
sock, display, depth);
|
sock, display, depth, frame_id);
|
||||||
|
} else if (this._ctl === 0x0C) {
|
||||||
|
ret = this._qoiRect(x, y, width, height,
|
||||||
|
sock, display, depth, frame_id);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Illegal tight compression received (ctl: " +
|
throw new Error("Illegal tight compression received (ctl: " +
|
||||||
this._ctl + ")");
|
this._ctl + ")");
|
||||||
|
@ -74,7 +86,7 @@ export default class TightDecoder {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
_fillRect(x, y, width, height, sock, display, depth) {
|
_fillRect(x, y, width, height, sock, display, depth, frame_id) {
|
||||||
if (sock.rQwait("TIGHT", 3)) {
|
if (sock.rQwait("TIGHT", 3)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -83,39 +95,81 @@ export default class TightDecoder {
|
||||||
const rQ = sock.rQ;
|
const rQ = sock.rQ;
|
||||||
|
|
||||||
display.fillRect(x, y, width, height,
|
display.fillRect(x, y, width, height,
|
||||||
[rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], false);
|
[rQ[rQi], rQ[rQi + 1], rQ[rQi + 2]], frame_id, false);
|
||||||
sock.rQskipBytes(3);
|
sock.rQskipBytes(3);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_jpegRect(x, y, width, height, sock, display, depth) {
|
_jpegRect(x, y, width, height, sock, display, depth, frame_id) {
|
||||||
let data = this._readData(sock);
|
let data = this._readData(sock);
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
display.imageRect(x, y, width, height, "image/jpeg", data);
|
display.imageRect(x, y, width, height, "image/jpeg", data, frame_id);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_webpRect(x, y, width, height, sock, display, depth) {
|
_webpRect(x, y, width, height, sock, display, depth, frame_id) {
|
||||||
let data = this._readData(sock);
|
let data = this._readData(sock);
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
display.imageRect(x, y, width, height, "image/webp", data);
|
display.imageRect(x, y, width, height, "image/webp", data, frame_id);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_pngRect(x, y, width, height, sock, display, depth) {
|
_processRectQ() {
|
||||||
|
while (this._availableWorkers.length > 0 && this._qoiRects.length > 0) {
|
||||||
|
let i = this._availableWorkers.pop();
|
||||||
|
let worker = this._workers[i];
|
||||||
|
let rect = this._qoiRects.shift();
|
||||||
|
this._arrs[i].set(rect.data);
|
||||||
|
worker.postMessage({
|
||||||
|
length: rect.data.length,
|
||||||
|
x: rect.x,
|
||||||
|
y: rect.y,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
depth: rect.depth,
|
||||||
|
sab: this._sabs[i],
|
||||||
|
sabR: this._sabsR[i],
|
||||||
|
frame_id: rect.frame_id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_qoiRect(x, y, width, height, sock, display, depth, frame_id) {
|
||||||
|
let data = this._readData(sock);
|
||||||
|
if (data === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._enableQOI) {
|
||||||
|
let dataClone = new Uint8Array(data);
|
||||||
|
let item = {x: x,y: y,width: width,height: height,data: dataClone,depth: depth, frame_id: frame_id};
|
||||||
|
if (this._qoiRects.length < 1000) {
|
||||||
|
this._qoiRects.push(item);
|
||||||
|
this._processRectQ();
|
||||||
|
} else {
|
||||||
|
Log.Warn("QOI queue exceeded limit.");
|
||||||
|
this._qoiRects.splice(0, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pngRect(x, y, width, height, sock, display, depth, frame_id) {
|
||||||
throw new Error("PNG received in standard Tight rect");
|
throw new Error("PNG received in standard Tight rect");
|
||||||
}
|
}
|
||||||
|
|
||||||
_basicRect(ctl, x, y, width, height, sock, display, depth) {
|
_basicRect(ctl, x, y, width, height, sock, display, depth, frame_id) {
|
||||||
if (this._filter === null) {
|
if (this._filter === null) {
|
||||||
if (ctl & 0x4) {
|
if (ctl & 0x4) {
|
||||||
if (sock.rQwait("TIGHT", 1)) {
|
if (sock.rQwait("TIGHT", 1)) {
|
||||||
|
@ -136,15 +190,15 @@ export default class TightDecoder {
|
||||||
switch (this._filter) {
|
switch (this._filter) {
|
||||||
case 0: // CopyFilter
|
case 0: // CopyFilter
|
||||||
ret = this._copyFilter(streamId, x, y, width, height,
|
ret = this._copyFilter(streamId, x, y, width, height,
|
||||||
sock, display, depth);
|
sock, display, depth, frame_id);
|
||||||
break;
|
break;
|
||||||
case 1: // PaletteFilter
|
case 1: // PaletteFilter
|
||||||
ret = this._paletteFilter(streamId, x, y, width, height,
|
ret = this._paletteFilter(streamId, x, y, width, height,
|
||||||
sock, display, depth);
|
sock, display, depth, frame_id);
|
||||||
break;
|
break;
|
||||||
case 2: // GradientFilter
|
case 2: // GradientFilter
|
||||||
ret = this._gradientFilter(streamId, x, y, width, height,
|
ret = this._gradientFilter(streamId, x, y, width, height,
|
||||||
sock, display, depth);
|
sock, display, depth, frame_id);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error("Illegal tight filter received (ctl: " +
|
throw new Error("Illegal tight filter received (ctl: " +
|
||||||
|
@ -158,7 +212,7 @@ export default class TightDecoder {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
_copyFilter(streamId, x, y, width, height, sock, display, depth) {
|
_copyFilter(streamId, x, y, width, height, sock, display, depth, frame_id) {
|
||||||
const uncompressedSize = width * height * 3;
|
const uncompressedSize = width * height * 3;
|
||||||
let data;
|
let data;
|
||||||
|
|
||||||
|
@ -191,12 +245,12 @@ export default class TightDecoder {
|
||||||
rgbx[i + 3] = 255; // Alpha
|
rgbx[i + 3] = 255; // Alpha
|
||||||
}
|
}
|
||||||
|
|
||||||
display.blitImage(x, y, width, height, rgbx, 0, false);
|
display.blitImage(x, y, width, height, rgbx, 0, frame_id, false);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_paletteFilter(streamId, x, y, width, height, sock, display, depth) {
|
_paletteFilter(streamId, x, y, width, height, sock, display, depth, frame_id) {
|
||||||
if (this._numColors === 0) {
|
if (this._numColors === 0) {
|
||||||
if (sock.rQwait("TIGHT palette", 1)) {
|
if (sock.rQwait("TIGHT palette", 1)) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -244,9 +298,9 @@ export default class TightDecoder {
|
||||||
|
|
||||||
// Convert indexed (palette based) image data to RGB
|
// Convert indexed (palette based) image data to RGB
|
||||||
if (this._numColors == 2) {
|
if (this._numColors == 2) {
|
||||||
this._monoRect(x, y, width, height, data, this._palette, display);
|
this._monoRect(x, y, width, height, data, this._palette, display, frame_id);
|
||||||
} else {
|
} else {
|
||||||
this._paletteRect(x, y, width, height, data, this._palette, display);
|
this._paletteRect(x, y, width, height, data, this._palette, display, frame_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._numColors = 0;
|
this._numColors = 0;
|
||||||
|
@ -254,7 +308,7 @@ export default class TightDecoder {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_monoRect(x, y, width, height, data, palette, display) {
|
_monoRect(x, y, width, height, data, palette, display, frame_id) {
|
||||||
// Convert indexed (palette based) image data to RGB
|
// Convert indexed (palette based) image data to RGB
|
||||||
// TODO: reduce number of calculations inside loop
|
// TODO: reduce number of calculations inside loop
|
||||||
const dest = this._getScratchBuffer(width * height * 4);
|
const dest = this._getScratchBuffer(width * height * 4);
|
||||||
|
@ -284,10 +338,10 @@ export default class TightDecoder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
display.blitImage(x, y, width, height, dest, 0, false);
|
display.blitImage(x, y, width, height, dest, 0, frame_id, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_paletteRect(x, y, width, height, data, palette, display) {
|
_paletteRect(x, y, width, height, data, palette, display, frame_id) {
|
||||||
// Convert indexed (palette based) image data to RGB
|
// Convert indexed (palette based) image data to RGB
|
||||||
const dest = this._getScratchBuffer(width * height * 4);
|
const dest = this._getScratchBuffer(width * height * 4);
|
||||||
const total = width * height * 4;
|
const total = width * height * 4;
|
||||||
|
@ -299,10 +353,10 @@ export default class TightDecoder {
|
||||||
dest[i + 3] = 255;
|
dest[i + 3] = 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
display.blitImage(x, y, width, height, dest, 0, false);
|
display.blitImage(x, y, width, height, dest, 0, frame_id, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_gradientFilter(streamId, x, y, width, height, sock, display, depth) {
|
_gradientFilter(streamId, x, y, width, height, sock, display, depth, frame_id) {
|
||||||
throw new Error("Gradient filter not implemented");
|
throw new Error("Gradient filter not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -342,4 +396,63 @@ export default class TightDecoder {
|
||||||
}
|
}
|
||||||
return this._scratchBuffer;
|
return this._scratchBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_enableQOIWorkers() {
|
||||||
|
let sabTest = typeof SharedArrayBuffer;
|
||||||
|
if (sabTest !== 'undefined') {
|
||||||
|
this._enableQOI = true;
|
||||||
|
if ((window.navigator.hardwareConcurrency) && (window.navigator.hardwareConcurrency > 8)) {
|
||||||
|
this._threads = window.navigator.hardwareConcurrency;
|
||||||
|
} else {
|
||||||
|
this._threads = 8;
|
||||||
|
}
|
||||||
|
this._workerEnabled = false;
|
||||||
|
this._workers = [];
|
||||||
|
this._availableWorkers = [];
|
||||||
|
this._sabs = [];
|
||||||
|
this._sabsR = [];
|
||||||
|
this._arrs = [];
|
||||||
|
this._arrsR = [];
|
||||||
|
this._qoiRects = [];
|
||||||
|
this._rectQlooping = false;
|
||||||
|
for (let i = 0; i < this._threads; i++) {
|
||||||
|
this._workers.push(new Worker("/core/decoders/qoi/decoder.js"));
|
||||||
|
this._sabs.push(new SharedArrayBuffer(300000));
|
||||||
|
this._sabsR.push(new SharedArrayBuffer(400000));
|
||||||
|
this._arrs.push(new Uint8Array(this._sabs[i]));
|
||||||
|
this._arrsR.push(new Uint8ClampedArray(this._sabsR[i]));
|
||||||
|
this._workers[i].onmessage = (evt) => {
|
||||||
|
this._availableWorkers.push(i);
|
||||||
|
switch(evt.data.result) {
|
||||||
|
case 0:
|
||||||
|
let data = new Uint8ClampedArray(evt.data.length);
|
||||||
|
data.set(this._arrsR[i].slice(0, evt.data.length));
|
||||||
|
let img = new ImageData(data, evt.data.img.width, evt.data.img.height, {colorSpace: evt.data.img.colorSpace});
|
||||||
|
|
||||||
|
this._displayGlobal.blitQoi(
|
||||||
|
evt.data.x,
|
||||||
|
evt.data.y,
|
||||||
|
evt.data.width,
|
||||||
|
evt.data.height,
|
||||||
|
img,
|
||||||
|
0,
|
||||||
|
evt.data.frame_id,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
this._processRectQ();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
Log.Info("QOI Worker is now available.");
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
Log.Info("Error on worker: " + evt.error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._enableQOI = false;
|
||||||
|
Log.Warn("Enabling QOI Failed, client not compatible.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,13 +10,13 @@
|
||||||
import TightDecoder from './tight.js';
|
import TightDecoder from './tight.js';
|
||||||
|
|
||||||
export default class TightPNGDecoder extends TightDecoder {
|
export default class TightPNGDecoder extends TightDecoder {
|
||||||
_pngRect(x, y, width, height, sock, display, depth) {
|
_pngRect(x, y, width, height, sock, display, depth, frame_id) {
|
||||||
let data = this._readData(sock);
|
let data = this._readData(sock);
|
||||||
if (data === null) {
|
if (data === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
display.imageRect(x, y, width, height, "image/png", data);
|
display.imageRect(x, y, width, height, "image/png", data, frame_id);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ export default class UDPDecoder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeRect(x, y, width, height, data, display, depth) {
|
decodeRect(x, y, width, height, data, display, depth, frame_id) {
|
||||||
let ctl = data[12];
|
let ctl = data[12];
|
||||||
ctl = ctl >> 4;
|
ctl = ctl >> 4;
|
||||||
|
|
||||||
|
|
324
core/display.js
324
core/display.js
|
@ -9,12 +9,31 @@
|
||||||
import * as Log from './util/logging.js';
|
import * as Log from './util/logging.js';
|
||||||
import Base64 from "./base64.js";
|
import Base64 from "./base64.js";
|
||||||
import { toSigned32bit } from './util/int.js';
|
import { toSigned32bit } from './util/int.js';
|
||||||
|
import { isWindows } from './util/browser.js';
|
||||||
|
|
||||||
export default class Display {
|
export default class Display {
|
||||||
constructor(target) {
|
constructor(target) {
|
||||||
this._renderQ = []; // queue drawing actions for in-oder rendering
|
Log.Debug(">> Display.constructor");
|
||||||
this._currentFrame = [];
|
|
||||||
this._nextFrame = [];
|
/*
|
||||||
|
For performance reasons we use a multi dimensional array
|
||||||
|
1st Dimension of Array Represents Frames, each element is a Frame
|
||||||
|
2nd Dimension 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 = [];
|
||||||
|
/*
|
||||||
|
Buffer for incoming frames. The larger the buffer the more time there is to collect, process, and order rects
|
||||||
|
but the more delay there is. May need to adjust this higher for lower power devices when UDP is complete.
|
||||||
|
Decoders that use WASM in parallel can also cause out of order rects
|
||||||
|
*/
|
||||||
|
this._maxAsyncFrameQueue = 3;
|
||||||
|
this._clearAsyncQueue();
|
||||||
|
|
||||||
this._flushing = false;
|
this._flushing = false;
|
||||||
|
|
||||||
// the full frame buffer (logical canvas) size
|
// the full frame buffer (logical canvas) size
|
||||||
|
@ -22,12 +41,7 @@ export default class Display {
|
||||||
this._fbHeight = 0;
|
this._fbHeight = 0;
|
||||||
|
|
||||||
this._renderMs = 0;
|
this._renderMs = 0;
|
||||||
|
|
||||||
this._prevDrawStyle = "";
|
this._prevDrawStyle = "";
|
||||||
|
|
||||||
Log.Debug(">> Display.constructor");
|
|
||||||
|
|
||||||
// The visible canvas
|
|
||||||
this._target = target;
|
this._target = target;
|
||||||
|
|
||||||
if (!this._target) {
|
if (!this._target) {
|
||||||
|
@ -49,20 +63,22 @@ export default class Display {
|
||||||
|
|
||||||
Log.Debug("User Agent: " + navigator.userAgent);
|
Log.Debug("User Agent: " + navigator.userAgent);
|
||||||
|
|
||||||
// performance metrics, try to calc a fps equivelant
|
// performance metrics
|
||||||
this._flipCnt = 0;
|
this._flipCnt = 0;
|
||||||
this._lastFlip = Date.now();
|
this._lastFlip = Date.now();
|
||||||
|
this._droppedFrames = 0;
|
||||||
|
this._droppedRects = 0;
|
||||||
|
this._missingRectCnt = 0;
|
||||||
setInterval(function() {
|
setInterval(function() {
|
||||||
let delta = Date.now() - this._lastFlip;
|
let delta = Date.now() - this._lastFlip;
|
||||||
if (delta > 0) {
|
if (delta > 0) {
|
||||||
this._fps = (this._flipCnt / (delta / 1000)).toFixed(2);
|
this._fps = (this._flipCnt / (delta / 1000)).toFixed(2);
|
||||||
}
|
}
|
||||||
|
Log.Info('Dropped Frames: ' + this._droppedFrames + ' Dropped Rects: ' + this._droppedRects + ' Missing Rect Cnt: ' + this._missingRectCnt);
|
||||||
this._flipCnt = 0;
|
this._flipCnt = 0;
|
||||||
this._lastFlip = Date.now();
|
this._lastFlip = Date.now();
|
||||||
}.bind(this), 5000);
|
}.bind(this), 5000);
|
||||||
|
|
||||||
Log.Debug("<< Display.constructor");
|
|
||||||
|
|
||||||
// ===== PROPERTIES =====
|
// ===== PROPERTIES =====
|
||||||
|
|
||||||
this._scale = 1.0;
|
this._scale = 1.0;
|
||||||
|
@ -73,6 +89,11 @@ export default class Display {
|
||||||
// ===== EVENT HANDLERS =====
|
// ===== EVENT HANDLERS =====
|
||||||
|
|
||||||
this.onflush = () => { }; // A flush request has finished
|
this.onflush = () => { }; // A flush request has finished
|
||||||
|
|
||||||
|
// Use requestAnimationFrame to write to canvas, to match display refresh rate
|
||||||
|
window.requestAnimationFrame( () => { this._pushAsyncFrame(); });
|
||||||
|
|
||||||
|
Log.Debug("<< Display.constructor");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== PROPERTIES =====
|
// ===== PROPERTIES =====
|
||||||
|
@ -149,8 +170,6 @@ export default class Display {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
|
Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
|
||||||
|
|
||||||
this.flip();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
viewportChangeSize(width, height) {
|
viewportChangeSize(width, height) {
|
||||||
|
@ -186,8 +205,6 @@ export default class Display {
|
||||||
// The position might need to be updated if we've grown
|
// The position might need to be updated if we've grown
|
||||||
this.viewportChangePos(0, 0);
|
this.viewportChangePos(0, 0);
|
||||||
|
|
||||||
this.flip();
|
|
||||||
|
|
||||||
// Update the visible size of the target canvas
|
// Update the visible size of the target canvas
|
||||||
this._rescale(this._scale);
|
this._rescale(this._scale);
|
||||||
}
|
}
|
||||||
|
@ -243,54 +260,37 @@ export default class Display {
|
||||||
}
|
}
|
||||||
|
|
||||||
// rendering canvas
|
// rendering canvas
|
||||||
flip(fromQueue) {
|
flip(frame_id, rect_cnt) {
|
||||||
if (!fromQueue) {
|
this._asyncRenderQPush({
|
||||||
this._renderQPush({
|
'type': 'flip',
|
||||||
'type': 'flip'
|
'frame_id': frame_id,
|
||||||
});
|
'rect_cnt': rect_cnt
|
||||||
} else {
|
});
|
||||||
for (let i = 0; i < this._currentFrame.length; i++) {
|
|
||||||
const a = this._currentFrame[i];
|
|
||||||
switch (a.type) {
|
|
||||||
case 'copy':
|
|
||||||
this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true);
|
|
||||||
break;
|
|
||||||
case 'fill':
|
|
||||||
this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
|
|
||||||
break;
|
|
||||||
case 'blit':
|
|
||||||
this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
|
|
||||||
break;
|
|
||||||
case 'img':
|
|
||||||
this.drawImage(a.img, a.x, a.y, a.width, a.height);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._flipCnt += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pending() {
|
pending() {
|
||||||
return this._renderQ.length > 0;
|
//is the slot in the queue for the newest frame in use
|
||||||
|
return this._asyncFrameQueue[this._maxAsyncFrameQueue - 1][0] > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
flush() {
|
flush() {
|
||||||
if (this._renderQ.length === 0) {
|
//force oldest frame to render
|
||||||
this.onflush();
|
this._asyncFrameComplete(0, true);
|
||||||
} else {
|
|
||||||
this._flushing = true;
|
//this in effect blocks more incoming frames until the oldest frame has been rendered to canvas (tcp only)
|
||||||
}
|
this._flushing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fillRect(x, y, width, height, color, fromQueue) {
|
fillRect(x, y, width, height, color, frame_id, fromQueue) {
|
||||||
if (!fromQueue) {
|
if (!fromQueue) {
|
||||||
this._renderQPush({
|
this._asyncRenderQPush({
|
||||||
'type': 'fill',
|
'type': 'fill',
|
||||||
'x': x,
|
'x': x,
|
||||||
'y': y,
|
'y': y,
|
||||||
'width': width,
|
'width': width,
|
||||||
'height': height,
|
'height': height,
|
||||||
'color': color
|
'color': color,
|
||||||
|
'frame_id': frame_id
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this._setFillColor(color);
|
this._setFillColor(color);
|
||||||
|
@ -298,9 +298,9 @@ export default class Display {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
copyImage(oldX, oldY, newX, newY, w, h, fromQueue) {
|
copyImage(oldX, oldY, newX, newY, w, h, frame_id, fromQueue) {
|
||||||
if (!fromQueue) {
|
if (!fromQueue) {
|
||||||
this._renderQPush({
|
this._asyncRenderQPush({
|
||||||
'type': 'copy',
|
'type': 'copy',
|
||||||
'oldX': oldX,
|
'oldX': oldX,
|
||||||
'oldY': oldY,
|
'oldY': oldY,
|
||||||
|
@ -308,6 +308,7 @@ export default class Display {
|
||||||
'y': newY,
|
'y': newY,
|
||||||
'width': w,
|
'width': w,
|
||||||
'height': h,
|
'height': h,
|
||||||
|
'frame_id': frame_id
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Due to this bug among others [1] we need to disable the image-smoothing to
|
// Due to this bug among others [1] we need to disable the image-smoothing to
|
||||||
|
@ -328,39 +329,40 @@ export default class Display {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
imageRect(x, y, width, height, mime, arr) {
|
imageRect(x, y, width, height, mime, arr, frame_id) {
|
||||||
/* The internal logic cannot handle empty images, so bail early */
|
/* The internal logic cannot handle empty images, so bail early */
|
||||||
if ((width === 0) || (height === 0)) {
|
if ((width === 0) || (height === 0)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.src = "data: " + mime + ";base64," + Base64.encode(arr);
|
img.src = "data: " + mime + ";base64," + Base64.encode(arr);
|
||||||
|
|
||||||
this._renderQPush({
|
this._asyncRenderQPush({
|
||||||
'type': 'img',
|
'type': 'img',
|
||||||
'img': img,
|
'img': img,
|
||||||
'x': x,
|
'x': x,
|
||||||
'y': y,
|
'y': y,
|
||||||
'width': width,
|
'width': width,
|
||||||
'height': height
|
'height': height,
|
||||||
|
'frame_id': frame_id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
blitImage(x, y, width, height, arr, offset, fromQueue) {
|
blitImage(x, y, width, height, arr, offset, frame_id, fromQueue) {
|
||||||
if (!fromQueue) {
|
if (!fromQueue) {
|
||||||
// NB(directxman12): it's technically more performant here to use preallocated arrays,
|
// 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,
|
// 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
|
// this probably isn't getting called *nearly* as much
|
||||||
const newArr = new Uint8Array(width * height * 4);
|
const newArr = new Uint8Array(width * height * 4);
|
||||||
newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
|
newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
|
||||||
this._renderQPush({
|
this._asyncRenderQPush({
|
||||||
'type': 'blit',
|
'type': 'blit',
|
||||||
'data': newArr,
|
'data': newArr,
|
||||||
'x': x,
|
'x': x,
|
||||||
'y': y,
|
'y': y,
|
||||||
'width': width,
|
'width': width,
|
||||||
'height': height,
|
'height': height,
|
||||||
|
'frame_id': frame_id
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// NB(directxman12): arr must be an Type Array view
|
// NB(directxman12): arr must be an Type Array view
|
||||||
|
@ -372,9 +374,25 @@ export default class Display {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
drawImage(img, x, y, w, h) {
|
drawImage(img, x, y, w, h) {
|
||||||
try {
|
try {
|
||||||
if (img.width != w || img.height != h) {
|
if (img.width != w || img.height != h) {
|
||||||
this._targetCtx.drawImage(img, x, y, w, h);
|
this._targetCtx.drawImage(img, x, y, w, h);
|
||||||
} else {
|
} else {
|
||||||
this._targetCtx.drawImage(img, x, y);
|
this._targetCtx.drawImage(img, x, y);
|
||||||
|
@ -407,6 +425,144 @@ export default class Display {
|
||||||
|
|
||||||
// ===== PRIVATE METHODS =====
|
// ===== PRIVATE METHODS =====
|
||||||
|
|
||||||
|
/*
|
||||||
|
Process incoming rects into a frame buffer, assume rects are out of order due to either UDP or parallel processing of decoding
|
||||||
|
*/
|
||||||
|
_asyncRenderQPush(rect) {
|
||||||
|
let frameIx = -1;
|
||||||
|
let oldestFrameID = Number.MAX_SAFE_INTEGER;
|
||||||
|
let newestFrameID = 0;
|
||||||
|
for (let i=0; i<this._maxAsyncFrameQueue; 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) {
|
||||||
|
let rect_cnt = ((rect.type == "flip") ? rect.rect_cnt : 0);
|
||||||
|
this._asyncFrameQueue[i][0] = rect.frame_id;
|
||||||
|
this._asyncFrameQueue[i][1] = rect_cnt;
|
||||||
|
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
|
||||||
|
this._asyncFrameQueue[frameIx][1] = rect.rect_cnt;
|
||||||
|
}
|
||||||
|
|
||||||
|
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++;
|
||||||
|
return;
|
||||||
|
} else if (rect.frame_id > newestFrameID) {
|
||||||
|
//frame is newer than any frame in the queue, drop old frames
|
||||||
|
this._asyncFrameQueue.shift();
|
||||||
|
let 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
|
||||||
|
*/
|
||||||
|
_clearAsyncQueue() {
|
||||||
|
this._droppedFrames += this._asyncFrameQueue.length;
|
||||||
|
|
||||||
|
this._asyncFrameQueue = [];
|
||||||
|
for (let 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
|
||||||
|
*/
|
||||||
|
_asyncFrameComplete(frameIx, force=false) {
|
||||||
|
let currentFrameRectIx = this._asyncFrameQueue[frameIx][4];
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
if (this._asyncFrameQueue[frameIx][1] == 0) {
|
||||||
|
this._missingRectCnt++;
|
||||||
|
} else if (this._asyncFrameQueue[frameIx][1] !== this._asyncFrameQueue[frameIx][2].length) {
|
||||||
|
this._droppedRects += (this._asyncFrameQueue[frameIx][1] - this._asyncFrameQueue[frameIx][2].length);
|
||||||
|
}
|
||||||
|
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', () => { this._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
|
||||||
|
*/
|
||||||
|
_pushAsyncFrame() {
|
||||||
|
if (this._asyncFrameQueue[0][3]) {
|
||||||
|
let frame = this._asyncFrameQueue.shift()[2];
|
||||||
|
this._asyncFrameQueue.push([ 0, 0, [], false, 0 ]);
|
||||||
|
|
||||||
|
//render the selected frame
|
||||||
|
for (let i = 0; i < frame.length; i++) {
|
||||||
|
|
||||||
|
const 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.requestAnimationFrame( () => { this._pushAsyncFrame(); });
|
||||||
|
}
|
||||||
|
|
||||||
_rescale(factor) {
|
_rescale(factor) {
|
||||||
this._scale = factor;
|
this._scale = factor;
|
||||||
const vp = this._viewportLoc;
|
const vp = this._viewportLoc;
|
||||||
|
@ -445,56 +601,4 @@ export default class Display {
|
||||||
this._prevDrawStyle = newStyle;
|
this._prevDrawStyle = newStyle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderQPush(action) {
|
|
||||||
this._renderQ.push(action);
|
|
||||||
if (this._renderQ.length === 1) {
|
|
||||||
this._scanRenderQ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_resumeRenderQ() {
|
|
||||||
this.removeEventListener('load', this._noVNCDisplay._resumeRenderQ);
|
|
||||||
this._noVNCDisplay._scanRenderQ();
|
|
||||||
}
|
|
||||||
|
|
||||||
_scanRenderQ() {
|
|
||||||
let ready = true;
|
|
||||||
let before = Date.now();
|
|
||||||
while (ready && this._renderQ.length > 0) {
|
|
||||||
const a = this._renderQ[0];
|
|
||||||
switch (a.type) {
|
|
||||||
case 'flip':
|
|
||||||
this._currentFrame = this._nextFrame;
|
|
||||||
this._nextFrame = [];
|
|
||||||
this.flip(true);
|
|
||||||
break;
|
|
||||||
case 'img':
|
|
||||||
if (a.img.complete) {
|
|
||||||
this._nextFrame.push(a);
|
|
||||||
} else {
|
|
||||||
a.img._noVNCDisplay = this;
|
|
||||||
a.img.addEventListener('load', this._resumeRenderQ);
|
|
||||||
// We need to wait for this image to 'load'
|
|
||||||
ready = false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this._nextFrame.push(a);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ready) {
|
|
||||||
this._renderQ.shift();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._renderQ.length === 0 && this._flushing) {
|
|
||||||
this._flushing = false;
|
|
||||||
this.onflush();
|
|
||||||
}
|
|
||||||
|
|
||||||
let elapsed = Date.now() - before;
|
|
||||||
this._renderMs += elapsed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,7 @@ export const encodings = {
|
||||||
pseudoEncodingVideoScalingLevel9: -1987,
|
pseudoEncodingVideoScalingLevel9: -1987,
|
||||||
pseudoEncodingVideoOutTimeLevel1: -1986,
|
pseudoEncodingVideoOutTimeLevel1: -1986,
|
||||||
pseudoEncodingVideoOutTimeLevel100: -1887,
|
pseudoEncodingVideoOutTimeLevel100: -1887,
|
||||||
|
pseudoEncodingQOI: -1886,
|
||||||
|
|
||||||
pseudoEncodingVMwareCursor: 0x574d5664,
|
pseudoEncodingVMwareCursor: 0x574d5664,
|
||||||
pseudoEncodingVMwareCursorPosition: 0x574d5666,
|
pseudoEncodingVMwareCursorPosition: 0x574d5666,
|
||||||
|
|
67
core/rfb.js
67
core/rfb.js
|
@ -43,6 +43,7 @@ const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';
|
||||||
|
|
||||||
var _videoQuality = 2;
|
var _videoQuality = 2;
|
||||||
var _enableWebP = false;
|
var _enableWebP = false;
|
||||||
|
var _enableQOI = false;
|
||||||
|
|
||||||
// Minimum wait (ms) between two mouse moves
|
// Minimum wait (ms) between two mouse moves
|
||||||
const MOUSE_MOVE_DELAY = 17;
|
const MOUSE_MOVE_DELAY = 17;
|
||||||
|
@ -139,6 +140,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._maxVideoResolutionY = 540;
|
this._maxVideoResolutionY = 540;
|
||||||
this._clipboardBinary = true;
|
this._clipboardBinary = true;
|
||||||
this._useUdp = true;
|
this._useUdp = true;
|
||||||
|
this._enableQOI = false;
|
||||||
this.TransitConnectionStates = {
|
this.TransitConnectionStates = {
|
||||||
Tcp: Symbol("tcp"),
|
Tcp: Symbol("tcp"),
|
||||||
Udp: Symbol("udp"),
|
Udp: Symbol("udp"),
|
||||||
|
@ -173,12 +175,14 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._decoders = {};
|
this._decoders = {};
|
||||||
|
|
||||||
this._FBU = {
|
this._FBU = {
|
||||||
rects: 0,
|
rects: 0, // current rect number
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
encoding: null,
|
encoding: null,
|
||||||
|
frame_id: 0,
|
||||||
|
rect_total: 0, //Total rects in frame
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mouse state
|
// Mouse state
|
||||||
|
@ -248,15 +252,6 @@ export default class RFB extends EventTargetMixin {
|
||||||
// initial cursor instead.
|
// initial cursor instead.
|
||||||
this._cursorImage = RFB.cursors.none;
|
this._cursorImage = RFB.cursors.none;
|
||||||
|
|
||||||
// populate decoder array with objects
|
|
||||||
this._decoders[encodings.encodingRaw] = new RawDecoder();
|
|
||||||
this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder();
|
|
||||||
this._decoders[encodings.encodingRRE] = new RREDecoder();
|
|
||||||
this._decoders[encodings.encodingHextile] = new HextileDecoder();
|
|
||||||
this._decoders[encodings.encodingTight] = new TightDecoder();
|
|
||||||
this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
|
|
||||||
this._decoders[encodings.encodingUDP] = new UDPDecoder();
|
|
||||||
|
|
||||||
// NB: nothing that needs explicit teardown should be done
|
// NB: nothing that needs explicit teardown should be done
|
||||||
// before this point, since this can throw an exception
|
// before this point, since this can throw an exception
|
||||||
try {
|
try {
|
||||||
|
@ -267,6 +262,15 @@ export default class RFB extends EventTargetMixin {
|
||||||
}
|
}
|
||||||
this._display.onflush = this._onFlush.bind(this);
|
this._display.onflush = this._onFlush.bind(this);
|
||||||
|
|
||||||
|
// populate decoder array with objects
|
||||||
|
this._decoders[encodings.encodingRaw] = new RawDecoder();
|
||||||
|
this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder();
|
||||||
|
this._decoders[encodings.encodingRRE] = new RREDecoder();
|
||||||
|
this._decoders[encodings.encodingHextile] = new HextileDecoder();
|
||||||
|
this._decoders[encodings.encodingTight] = new TightDecoder(this._display);
|
||||||
|
this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
|
||||||
|
this._decoders[encodings.encodingUDP] = new UDPDecoder();
|
||||||
|
|
||||||
this._keyboard = new Keyboard(this._canvas, touchInput);
|
this._keyboard = new Keyboard(this._canvas, touchInput);
|
||||||
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
|
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
|
||||||
|
|
||||||
|
@ -481,6 +485,21 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._pendingApplyEncodingChanges = true;
|
this._pendingApplyEncodingChanges = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get enableQOI() { return this._enableQOI; }
|
||||||
|
set enableQOI(enabled) {
|
||||||
|
if(this._enableQOI === enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (enabled) {
|
||||||
|
if (!this._decoders[encodings.encodingTight].enableQOI()) {
|
||||||
|
//enabling qoi failed
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._enableQOI = enabled;
|
||||||
|
this._pendingApplyEncodingChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
get antiAliasing() { return this._display.antiAliasing; }
|
get antiAliasing() { return this._display.antiAliasing; }
|
||||||
set antiAliasing(value) {
|
set antiAliasing(value) {
|
||||||
this._display.antiAliasing = value;
|
this._display.antiAliasing = value;
|
||||||
|
@ -2520,12 +2539,8 @@ export default class RFB extends EventTargetMixin {
|
||||||
encs.push(encodings.encodingRaw);
|
encs.push(encodings.encodingRaw);
|
||||||
|
|
||||||
// Psuedo-encoding settings
|
// Psuedo-encoding settings
|
||||||
var quality = 6;
|
|
||||||
var compression = 2;
|
|
||||||
var screensize = this._screenSize(false);
|
|
||||||
encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);
|
encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);
|
||||||
encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);
|
encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);
|
||||||
|
|
||||||
encs.push(encodings.pseudoEncodingDesktopSize);
|
encs.push(encodings.pseudoEncodingDesktopSize);
|
||||||
encs.push(encodings.pseudoEncodingLastRect);
|
encs.push(encodings.pseudoEncodingLastRect);
|
||||||
encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
|
encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
|
||||||
|
@ -2537,6 +2552,9 @@ export default class RFB extends EventTargetMixin {
|
||||||
encs.push(encodings.pseudoEncodingExtendedClipboard);
|
encs.push(encodings.pseudoEncodingExtendedClipboard);
|
||||||
if (this._hasWebp())
|
if (this._hasWebp())
|
||||||
encs.push(encodings.pseudoEncodingWEBP);
|
encs.push(encodings.pseudoEncodingWEBP);
|
||||||
|
if (this._enableQOI)
|
||||||
|
encs.push(encodings.pseudoEncodingQOI);
|
||||||
|
|
||||||
|
|
||||||
// kasm settings; the server may be configured to ignore these
|
// kasm settings; the server may be configured to ignore these
|
||||||
encs.push(encodings.pseudoEncodingJpegVideoQualityLevel0 + this.jpegVideoQuality);
|
encs.push(encodings.pseudoEncodingJpegVideoQualityLevel0 + this.jpegVideoQuality);
|
||||||
|
@ -3057,7 +3075,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
switch (frame.encoding) {
|
switch (frame.encoding) {
|
||||||
case encodings.pseudoEncodingLastRect:
|
case encodings.pseudoEncodingLastRect:
|
||||||
if (document.visibilityState !== "hidden") {
|
if (document.visibilityState !== "hidden") {
|
||||||
this._display.flip();
|
this._display.flip(false); //TODO: UDP is now broken, flip needs rect count and frame number
|
||||||
this._udpBuffer.clear();
|
this._udpBuffer.clear();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -3158,6 +3176,9 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._sock.rQskipBytes(1); // Padding
|
this._sock.rQskipBytes(1); // Padding
|
||||||
this._FBU.rects = this._sock.rQshift16();
|
this._FBU.rects = this._sock.rQshift16();
|
||||||
|
|
||||||
|
this._FBU.frame_id++;
|
||||||
|
this._FBU.rect_total = 0;
|
||||||
|
|
||||||
// Make sure the previous frame is fully rendered first
|
// Make sure the previous frame is fully rendered first
|
||||||
// to avoid building up an excessive queue
|
// to avoid building up an excessive queue
|
||||||
if (this._display.pending()) {
|
if (this._display.pending()) {
|
||||||
|
@ -3181,6 +3202,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
(hdr[10] << 8) + hdr[11], 10);
|
(hdr[10] << 8) + hdr[11], 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!this._handleRect()) {
|
if (!this._handleRect()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -3189,7 +3211,9 @@ export default class RFB extends EventTargetMixin {
|
||||||
this._FBU.encoding = null;
|
this._FBU.encoding = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._display.flip();
|
if (this._FBU.rect_total > 0) {
|
||||||
|
this._display.flip(this._FBU.frame_id, this._FBU.rect_total);
|
||||||
|
}
|
||||||
|
|
||||||
return true; // We finished this FBU
|
return true; // We finished this FBU
|
||||||
}
|
}
|
||||||
|
@ -3197,6 +3221,7 @@ export default class RFB extends EventTargetMixin {
|
||||||
_handleRect() {
|
_handleRect() {
|
||||||
switch (this._FBU.encoding) {
|
switch (this._FBU.encoding) {
|
||||||
case encodings.pseudoEncodingLastRect:
|
case encodings.pseudoEncodingLastRect:
|
||||||
|
this._FBU.rect_total++; //only track rendered rects and last rect
|
||||||
this._FBU.rects = 1; // Will be decreased when we return
|
this._FBU.rects = 1; // Will be decreased when we return
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
@ -3224,7 +3249,11 @@ export default class RFB extends EventTargetMixin {
|
||||||
return this._handleExtendedDesktopSize();
|
return this._handleExtendedDesktopSize();
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return this._handleDataRect();
|
if (this._handleDataRect()) {
|
||||||
|
this._FBU.rect_total++; //only track rendered rects and last rect
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3524,9 +3553,9 @@ export default class RFB extends EventTargetMixin {
|
||||||
return decoder.decodeRect(this._FBU.x, this._FBU.y,
|
return decoder.decodeRect(this._FBU.x, this._FBU.y,
|
||||||
this._FBU.width, this._FBU.height,
|
this._FBU.width, this._FBU.height,
|
||||||
this._sock, this._display,
|
this._sock, this._display,
|
||||||
this._fbDepth);
|
this._fbDepth, this._FBU.frame_id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._fail("Error decoding rect: " + err);
|
this._fail("Error decoding rect: " + err);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6
vnc.html
6
vnc.html
|
@ -277,6 +277,12 @@
|
||||||
<span class="slider-label">IME Input Mode</span>
|
<span class="slider-label">IME Input Mode</span>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<label class="switch"><input id="noVNC_setting_enable_qoi" type="checkbox" />
|
||||||
|
<span class="slider round"></span>
|
||||||
|
<span class="slider-label">QOI Lossless</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label class="switch"><input id="noVNC_setting_virtual_keyboard_visible" type="checkbox" />
|
<label class="switch"><input id="noVNC_setting_virtual_keyboard_visible" type="checkbox" />
|
||||||
<span class="slider round"></span>
|
<span class="slider round"></span>
|
||||||
|
|
Loading…
Reference in New Issue