Compare commits

..

No commits in common. "88a5a59629bdf948e5416a6f4e0c702b9bd78195" and "c6c8e5e51329001f778a72607bc68432b9d359f8" have entirely different histories.

15 changed files with 90 additions and 826 deletions

View File

@ -21,10 +21,10 @@
"Extra keys": "Дополнительные Кнопки",
"Show Extra Keys": "Показать Дополнительные Кнопки",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Зажать Ctrl",
"Toggle Ctrl": "Переключение нажатия Ctrl",
"Alt": "Alt",
"Toggle Alt": "Зажать Alt",
"Toggle Windows": "Зажать Windows",
"Toggle Alt": "Переключение нажатия Alt",
"Toggle Windows": "Переключение вкладок",
"Windows": "Вкладка",
"Send Tab": "Передать нажатие Tab",
"Tab": "Tab",
@ -47,7 +47,7 @@
"Clip to Window": "В окно",
"Scaling Mode:": "Масштаб:",
"None": "Нет",
"Local Scaling": "Локальный масштаб",
"Local Scaling": "Локльный масштаб",
"Remote Resizing": "Удаленная перенастройка размера",
"Advanced": "Дополнительно",
"Quality:": "Качество",

125
app/ui.js
View File

@ -20,12 +20,8 @@ import * as WebUtil from "./webutil.js";
const PAGE_TITLE = "noVNC";
const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
const UI = {
customSettings: {},
connected: false,
desktopName: "",
@ -46,31 +42,20 @@ const UI = {
reconnectCallback: null,
reconnectPassword: null,
async start(options={}) {
UI.customSettings = options.settings || {};
if (UI.customSettings.defaults === undefined) {
UI.customSettings.defaults = {};
}
if (UI.customSettings.mandatory === undefined) {
UI.customSettings.mandatory = {};
}
prime() {
return WebUtil.initSettings().then(() => {
if (document.readyState === "interactive" || document.readyState === "complete") {
return UI.start();
}
// Set up translations
try {
await l10n.setup(LINGUAS, "app/locale/");
} catch (err) {
Log.Error("Failed to load translations: " + err);
}
// Initialize setting storage
await WebUtil.initSettings();
// Wait for the page to load
if (document.readyState !== "interactive" && document.readyState !== "complete") {
await new Promise((resolve, reject) => {
document.addEventListener('DOMContentLoaded', resolve);
return new Promise((resolve, reject) => {
document.addEventListener('DOMContentLoaded', () => UI.start().then(resolve).catch(reject));
});
}
});
},
// Render default UI and initialize settings menu
start() {
UI.initSettings();
@ -85,20 +70,22 @@ const UI = {
}
// Try to fetch version number
try {
let response = await fetch('./package.json');
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
let packageInfo = await response.json();
Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version);
} catch (err) {
Log.Error("Couldn't fetch package.json: " + err);
Array.from(document.getElementsByClassName('noVNC_version_wrapper'))
.concat(Array.from(document.getElementsByClassName('noVNC_version_separator')))
.forEach(el => el.style.display = 'none');
}
fetch('./package.json')
.then((response) => {
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
return response.json();
})
.then((packageInfo) => {
Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version);
})
.catch((err) => {
Log.Error("Couldn't fetch package.json: " + err);
Array.from(document.getElementsByClassName('noVNC_version_wrapper'))
.concat(Array.from(document.getElementsByClassName('noVNC_version_separator')))
.forEach(el => el.style.display = 'none');
});
// Adapt the interface for touch screen devices
if (isTouchDevice) {
@ -133,7 +120,7 @@ const UI = {
document.documentElement.classList.remove("noVNC_loading");
let autoconnect = UI.getSetting('autoconnect');
let autoconnect = WebUtil.getConfigVar('autoconnect', false);
if (autoconnect === 'true' || autoconnect == '1') {
autoconnect = true;
UI.connect();
@ -142,6 +129,8 @@ const UI = {
// Show the connect panel on first load unless autoconnecting
UI.openConnectPanel();
}
return Promise.resolve(UI.rfb);
},
initFullscreen() {
@ -169,26 +158,21 @@ const UI = {
UI.initSetting('logging', 'warn');
UI.updateLogging();
UI.setupSettingLabels();
/* Populate the controls if defaults are provided in the URL */
UI.initSetting('host', '');
UI.initSetting('port', 0);
UI.initSetting('encrypt', (window.location.protocol === "https:"));
UI.initSetting('password');
UI.initSetting('autoconnect', false);
UI.initSetting('view_clip', false);
UI.initSetting('resize', 'off');
UI.initSetting('quality', 6);
UI.initSetting('compression', 2);
UI.initSetting('shared', true);
UI.initSetting('bell', 'on');
UI.initSetting('view_only', false);
UI.initSetting('show_dot', false);
UI.initSetting('path', 'websockify');
UI.initSetting('repeaterID', '');
UI.initSetting('reconnect', false);
UI.initSetting('reconnect_delay', 5000);
UI.setupSettingLabels();
},
// Adds a link to the label elements on the corresponding input elements
setupSettingLabels() {
@ -750,10 +734,6 @@ const UI = {
// Initial page load read/initialization of settings
initSetting(name, defVal) {
// Has the user overridden the default value?
if (name in UI.customSettings.defaults) {
defVal = UI.customSettings.defaults[name];
}
// Check Query string followed by cookie
let val = WebUtil.getConfigVar(name);
if (val === null) {
@ -761,11 +741,6 @@ const UI = {
}
WebUtil.setSetting(name, val);
UI.updateSetting(name);
// Has the user forced a value?
if (name in UI.customSettings.mandatory) {
val = UI.customSettings.mandatory[name];
UI.forceSetting(name, val);
}
return val;
},
@ -784,12 +759,9 @@ const UI = {
let value = UI.getSetting(name);
const ctrl = document.getElementById('noVNC_setting_' + name);
if (ctrl === null) {
return;
}
if (ctrl.type === 'checkbox') {
ctrl.checked = value;
} else if (typeof ctrl.options !== 'undefined') {
for (let i = 0; i < ctrl.options.length; i += 1) {
if (ctrl.options[i].value === value) {
@ -822,8 +794,7 @@ const UI = {
getSetting(name) {
const ctrl = document.getElementById('noVNC_setting_' + name);
let val = WebUtil.readSetting(name);
if (typeof val !== 'undefined' && val !== null &&
ctrl !== null && ctrl.type === 'checkbox') {
if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') {
if (val.toString().toLowerCase() in {'0': 1, 'no': 1, 'false': 1}) {
val = false;
} else {
@ -838,22 +809,14 @@ const UI = {
// disable the labels that belong to disabled input elements.
disableSetting(name) {
const ctrl = document.getElementById('noVNC_setting_' + name);
if (ctrl !== null) {
ctrl.disabled = true;
if (ctrl.label !== undefined) {
ctrl.label.classList.add('noVNC_disabled');
}
}
ctrl.disabled = true;
ctrl.label.classList.add('noVNC_disabled');
},
enableSetting(name) {
const ctrl = document.getElementById('noVNC_setting_' + name);
if (ctrl !== null) {
ctrl.disabled = false;
if (ctrl.label !== undefined) {
ctrl.label.classList.remove('noVNC_disabled');
}
}
ctrl.disabled = false;
ctrl.label.classList.remove('noVNC_disabled');
},
/* ------^-------
@ -1035,7 +998,7 @@ const UI = {
const path = UI.getSetting('path');
if (typeof password === 'undefined') {
password = UI.getSetting('password');
password = WebUtil.getConfigVar('password');
UI.reconnectPassword = password;
}
@ -1765,7 +1728,7 @@ const UI = {
},
bell(e) {
if (UI.getSetting('bell') === 'on') {
if (WebUtil.getConfigVar('bell', 'on') === 'on') {
const promise = document.getElementById('noVNC_bell').play();
// The standards disagree on the return value here
if (promise) {
@ -1796,4 +1759,10 @@ const UI = {
*/
};
// Set up translations
const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
l10n.setup(LINGUAS, "app/locale/")
.catch(err => Log.Error("Failed to load translations: " + err))
.then(UI.prime);
export default UI;

View File

@ -1,321 +0,0 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2024 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*
*/
import * as Log from '../util/logging.js';
export class H264Parser {
constructor(data) {
this._data = data;
this._index = 0;
this.profileIdc = null;
this.constraintSet = null;
this.levelIdc = null;
}
_getStartSequenceLen(index) {
let data = this._data;
if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 0 && data[index + 3] == 1) {
return 4;
}
if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 1) {
return 3;
}
return 0;
}
_indexOfNextNalUnit(index) {
let data = this._data;
for (let i = index; i < data.length; ++i) {
if (this._getStartSequenceLen(i) != 0) {
return i;
}
}
return -1;
}
_parseSps(index) {
this.profileIdc = this._data[index];
this.constraintSet = this._data[index + 1];
this.levelIdc = this._data[index + 2];
}
_parseNalUnit(index) {
const firstByte = this._data[index];
if (firstByte & 0x80) {
throw new Error('H264 parsing sanity check failed, forbidden zero bit is set');
}
const unitType = firstByte & 0x1f;
switch (unitType) {
case 1: // coded slice, non-idr
return { slice: true };
case 5: // coded slice, idr
return { slice: true, key: true };
case 6: // sei
return {};
case 7: // sps
this._parseSps(index + 1);
return {};
case 8: // pps
return {};
default:
Log.Warn("Unhandled unit type: ", unitType);
break;
}
return {};
}
parse() {
const startIndex = this._index;
let isKey = false;
while (this._index < this._data.length) {
const startSequenceLen = this._getStartSequenceLen(this._index);
if (startSequenceLen == 0) {
throw new Error('Invalid start sequence in bit stream');
}
const { slice, key } = this._parseNalUnit(this._index + startSequenceLen);
let nextIndex = this._indexOfNextNalUnit(this._index + startSequenceLen);
if (nextIndex == -1) {
this._index = this._data.length;
} else {
this._index = nextIndex;
}
if (key) {
isKey = true;
}
if (slice) {
break;
}
}
if (startIndex === this._index) {
return null;
}
return {
frame: this._data.subarray(startIndex, this._index),
key: isKey,
};
}
}
export class H264Context {
constructor(width, height) {
this.lastUsed = 0;
this._width = width;
this._height = height;
this._profileIdc = null;
this._constraintSet = null;
this._levelIdc = null;
this._decoder = null;
this._pendingFrames = [];
}
_handleFrame(frame) {
let pending = this._pendingFrames.shift();
if (pending === undefined) {
throw new Error("Pending frame queue empty when receiving frame from decoder");
}
if (pending.timestamp != frame.timestamp) {
throw new Error("Video frame timestamp mismatch. Expected " +
frame.timestamp + " but but got " + pending.timestamp);
}
pending.frame = frame;
pending.ready = true;
pending.resolve();
if (!pending.keep) {
frame.close();
}
}
_handleError(e) {
throw new Error("Failed to decode frame: " + e.message);
}
_configureDecoder(profileIdc, constraintSet, levelIdc) {
if (this._decoder === null || this._decoder.state === 'closed') {
this._decoder = new VideoDecoder({
output: frame => this._handleFrame(frame),
error: e => this._handleError(e),
});
}
const codec = 'avc1.' +
profileIdc.toString(16).padStart(2, '0') +
constraintSet.toString(16).padStart(2, '0') +
levelIdc.toString(16).padStart(2, '0');
this._decoder.configure({
codec: codec,
codedWidth: this._width,
codedHeight: this._height,
optimizeForLatency: true,
});
}
_preparePendingFrame(timestamp) {
let pending = {
timestamp: timestamp,
promise: null,
resolve: null,
frame: null,
ready: false,
keep: false,
};
pending.promise = new Promise((resolve) => {
pending.resolve = resolve;
});
this._pendingFrames.push(pending);
return pending;
}
decode(payload) {
let parser = new H264Parser(payload);
let result = null;
// Ideally, this timestamp should come from the server, but we'll just
// approximate it instead.
let timestamp = Math.round(window.performance.now() * 1e3);
while (true) {
let encodedFrame = parser.parse();
if (encodedFrame === null) {
break;
}
if (parser.profileIdc !== null) {
self._profileIdc = parser.profileIdc;
self._constraintSet = parser.constraintSet;
self._levelIdc = parser.levelIdc;
}
if (this._decoder === null || this._decoder.state !== 'configured') {
if (!encodedFrame.key) {
Log.Warn("Missing key frame. Can't decode until one arrives");
continue;
}
if (self._profileIdc === null) {
Log.Warn('Cannot config decoder. Have not received SPS and PPS yet.');
continue;
}
this._configureDecoder(self._profileIdc, self._constraintSet,
self._levelIdc);
}
result = this._preparePendingFrame(timestamp);
const chunk = new EncodedVideoChunk({
timestamp: timestamp,
type: encodedFrame.key ? 'key' : 'delta',
data: encodedFrame.frame,
});
try {
this._decoder.decode(chunk);
} catch (e) {
Log.Warn("Failed to decode:", e);
}
}
// We only keep last frame of each payload
if (result !== null) {
result.keep = true;
}
return result;
}
}
export default class H264Decoder {
constructor() {
this._tick = 0;
this._contexts = {};
}
_contextId(x, y, width, height) {
return [x, y, width, height].join(',');
}
_findOldestContextId() {
let oldestTick = Number.MAX_VALUE;
let oldestKey = undefined;
for (const [key, value] of Object.entries(this._contexts)) {
if (value.lastUsed < oldestTick) {
oldestTick = value.lastUsed;
oldestKey = key;
}
}
return oldestKey;
}
_createContext(x, y, width, height) {
const maxContexts = 64;
if (Object.keys(this._contexts).length >= maxContexts) {
let oldestContextId = this._findOldestContextId();
delete this._contexts[oldestContextId];
}
let context = new H264Context(width, height);
this._contexts[this._contextId(x, y, width, height)] = context;
return context;
}
_getContext(x, y, width, height) {
let context = this._contexts[this._contextId(x, y, width, height)];
return context !== undefined ? context : this._createContext(x, y, width, height);
}
_resetContext(x, y, width, height) {
delete this._contexts[this._contextId(x, y, width, height)];
}
_resetAllContexts() {
this._contexts = {};
}
decodeRect(x, y, width, height, sock, display, depth) {
const resetContextFlag = 1;
const resetAllContextsFlag = 2;
if (sock.rQwait("h264 header", 8)) {
return false;
}
const length = sock.rQshift32();
const flags = sock.rQshift32();
if (sock.rQwait("h264 payload", length, 8)) {
return false;
}
if (flags & resetAllContextsFlag) {
this._resetAllContexts();
} else if (flags & resetContextFlag) {
this._resetContext(x, y, width, height);
}
let context = this._getContext(x, y, width, height);
context.lastUsed = this._tick++;
if (length !== 0) {
let payload = sock.rQshiftBytes(length, false);
let frame = context.decode(payload);
if (frame !== null) {
display.videoFrame(x, y, width, height, frame);
}
}
return true;
}
}

View File

@ -380,17 +380,6 @@ export default class Display {
});
}
videoFrame(x, y, width, height, frame) {
this._renderQPush({
'type': 'frame',
'frame': frame,
'x': x,
'y': y,
'width': width,
'height': height
});
}
blitImage(x, y, width, height, arr, offset, fromQueue) {
if (this._renderQ.length !== 0 && !fromQueue) {
// NB(directxman12): it's technically more performant here to use preallocated arrays,
@ -417,16 +406,9 @@ export default class Display {
}
}
drawImage(img, ...args) {
this._drawCtx.drawImage(img, ...args);
if (args.length <= 4) {
const [x, y] = args;
this._damage(x, y, img.width, img.height);
} else {
const [,, sw, sh, dx, dy] = args;
this._damage(dx, dy, sw, sh);
}
drawImage(img, x, y) {
this._drawCtx.drawImage(img, x, y);
this._damage(x, y, img.width, img.height);
}
autoscale(containerWidth, containerHeight) {
@ -529,35 +511,6 @@ export default class Display {
ready = false;
}
break;
case 'frame':
if (a.frame.ready) {
// The encoded frame may be larger than the rect due to
// limitations of the encoder, so we need to crop the
// frame.
let frame = a.frame.frame;
if (frame.codedWidth < a.width || frame.codedHeight < a.height) {
Log.Warn("Decoded video frame does not cover its full rectangle area. Expecting at least " +
a.width + "x" + a.height + " but got " +
frame.codedWidth + "x" + frame.codedHeight);
}
const sx = 0;
const sy = 0;
const sw = a.width;
const sh = a.height;
const dx = a.x;
const dy = a.y;
const dw = sw;
const dh = sh;
this.drawImage(frame, sx, sy, sw, sh, dx, dy, dw, dh);
frame.close();
} else {
let display = this;
a.frame.promise.then(() => {
display._scanRenderQ();
});
ready = false;
}
break;
}
if (ready) {

View File

@ -16,7 +16,6 @@ export const encodings = {
encodingZRLE: 16,
encodingTightPNG: -260,
encodingJPEG: 21,
encodingH264: 50,
pseudoEncodingQualityLevel9: -23,
pseudoEncodingQualityLevel0: -32,
@ -47,7 +46,6 @@ export function encodingName(num) {
case encodings.encodingZRLE: return "ZRLE";
case encodings.encodingTightPNG: return "TightPNG";
case encodings.encodingJPEG: return "JPEG";
case encodings.encodingH264: return "H.264";
default: return "[unknown encoding " + num + "]";
}
}

View File

@ -10,7 +10,7 @@
import { toUnsigned32bit, toSigned32bit } from './util/int.js';
import * as Log from './util/logging.js';
import { encodeUTF8, decodeUTF8 } from './util/strings.js';
import { dragThreshold, supportsWebCodecsH264Decode } from './util/browser.js';
import { dragThreshold } from './util/browser.js';
import { clientToElement } from './util/element.js';
import { setCapture } from './util/events.js';
import EventTargetMixin from './util/eventtarget.js';
@ -36,7 +36,6 @@ import TightDecoder from "./decoders/tight.js";
import TightPNGDecoder from "./decoders/tightpng.js";
import ZRLEDecoder from "./decoders/zrle.js";
import JPEGDecoder from "./decoders/jpeg.js";
import H264Decoder from "./decoders/h264.js";
// How many seconds to wait for a disconnect to finish
const DISCONNECT_TIMEOUT = 3;
@ -251,7 +250,6 @@ export default class RFB extends EventTargetMixin {
this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
this._decoders[encodings.encodingZRLE] = new ZRLEDecoder();
this._decoders[encodings.encodingJPEG] = new JPEGDecoder();
this._decoders[encodings.encodingH264] = new H264Decoder();
// NB: nothing that needs explicit teardown should be done
// before this point, since this can throw an exception
@ -2119,9 +2117,6 @@ export default class RFB extends EventTargetMixin {
encs.push(encodings.encodingCopyRect);
// Only supported with full depth support
if (this._fbDepth == 24) {
if (supportsWebCodecsH264Decode) {
encs.push(encodings.encodingH264);
}
encs.push(encodings.encodingTight);
encs.push(encodings.encodingTightPNG);
encs.push(encodings.encodingZRLE);

View File

@ -70,26 +70,6 @@ try {
}
export const hasScrollbarGutter = _hasScrollbarGutter;
export let supportsWebCodecsH264Decode = false;
async function _checkWebCodecsH264DecodeSupport() {
if (!('VideoDecoder' in window)) {
return;
}
// We'll need to make do with some placeholders here
const config = {
codec: 'avc1.42401f',
codedWidth: 1920,
codedHeight: 1080,
optimizeForLatency: true,
};
const result = await VideoDecoder.isConfigSupported(config);
supportsWebCodecsH264Decode = result.supported;
}
_checkWebCodecsH264DecodeSupport();
/*
* The functions for detection of platforms and browsers below are exported
* but the use of these should be minimized as much as possible.

View File

@ -208,7 +208,7 @@ export default class Websock {
chunkSize = bytes.length - offset;
}
this._sQ.set(bytes.subarray(offset, offset + chunkSize), this._sQlen);
this._sQ.set(bytes.subarray(offset, chunkSize), this._sQlen);
this._sQlen += chunkSize;
offset += chunkSize;
}

View File

@ -1 +0,0 @@
{}

View File

@ -1 +0,0 @@
{}

View File

@ -6,11 +6,11 @@
#
msgid ""
msgstr ""
"Project-Id-Version: noVNC 1.5.0\n"
"Project-Id-Version: noVNC 1.3.0\n"
"Report-Msgid-Bugs-To: novnc@googlegroups.com\n"
"POT-Creation-Date: 2021-08-27 16:03+0200\n"
"PO-Revision-Date: 2024-02-11 03:58+0300\n"
"Last-Translator: Dim5x <dim5x@yahoo.com>\n"
"PO-Revision-Date: 2021-09-09 10:29+0400\n"
"Last-Translator: Nia Remez <nia.remez@cendio.com>\n"
"Language-Team: Russian\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
@ -111,7 +111,7 @@ msgstr "Ctrl"
#: ../vnc.html:94
msgid "Toggle Ctrl"
msgstr "Зажать Ctrl"
msgstr "Переключение нажатия Ctrl"
#: ../vnc.html:97
msgid "Alt"
@ -119,11 +119,11 @@ msgstr "Alt"
#: ../vnc.html:97
msgid "Toggle Alt"
msgstr "Зажать Alt"
msgstr "Переключение нажатия Alt"
#: ../vnc.html:100
msgid "Toggle Windows"
msgstr "Зажать Windows"
msgstr "Переключение вкладок"
#: ../vnc.html:100
msgid "Windows"
@ -215,7 +215,7 @@ msgstr "Нет"
#: ../vnc.html:175
msgid "Local Scaling"
msgstr "Локальный масштаб"
msgstr "Локльный масштаб"
#: ../vnc.html:176
msgid "Remote Resizing"

View File

@ -37,15 +37,6 @@ export default class FakeWebSocket {
} else {
data = new Uint8Array(data);
}
if (this.bufferedAmount + data.length > this._sendQueue.length) {
let newlen = this._sendQueue.length;
while (this.bufferedAmount + data.length > newlen) {
newlen *= 2;
}
let newbuf = new Uint8Array(newlen);
newbuf.set(this._sendQueue);
this._sendQueue = newbuf;
}
this._sendQueue.set(data, this.bufferedAmount);
this.bufferedAmount += data.length;
}

View File

@ -1,264 +0,0 @@
import Websock from '../core/websock.js';
import Display from '../core/display.js';
import { H264Parser } from '../core/decoders/h264.js';
import H264Decoder from '../core/decoders/h264.js';
import Base64 from '../core/base64.js';
import FakeWebSocket from './fake.websocket.js';
/* This is a 3 frame 16x16 video where the first frame is solid red, the second
* is solid green and the third is solid blue.
*
* The colour space is BT.709. It is encoded into the stream.
*/
const redGreenBlue16x16Video = new Uint8Array(Base64.decode(
'AAAAAWdCwBTZnpuAgICgAAADACAAAAZB4oVNAAAAAWjJYyyAAAABBgX//4HcRem95tlIt5Ys' +
'2CDZI+7veDI2NCAtIGNvcmUgMTY0IHIzMTA4IDMxZTE5ZjkgLSBILjI2NC9NUEVHLTQgQVZD' +
'IGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDIzIC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcv' +
'eDI2NC5odG1sIC0gb3B0aW9uczogY2FiYWM9MCByZWY9NSBkZWJsb2NrPTE6MDowIGFuYWx5' +
'c2U9MHgxOjB4MTExIG1lPWhleCBzdWJtZT04IHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4' +
'ZWRfcmVmPTEgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0yIDh4OGRjdD0wIGNx' +
'bT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJvbWFfcXBfb2Zmc2V0PS0yIHRo' +
'cmVhZHM9MSBsb29rYWhlYWRfdGhyZWFkcz0xIHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNp' +
'bWF0ZT0xIGludGVybGFjZWQ9MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9' +
'MCBiZnJhbWVzPTAgd2VpZ2h0cD0wIGtleWludD1pbmZpbml0ZSBrZXlpbnRfbWluPTI1IHNj' +
'ZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NTAgcmM9YWJyIG1idHJl' +
'ZT0xIGJpdHJhdGU9NDAwIHJhdGV0b2w9MS4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02' +
'OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAABZYiEBrxmKAAPVccAAS04' +
'4AA5DRJMnkycJk4TPwAAAAFBiIga8RigADVVHAAGaGOAANtuAAAAAUGIkBr///wRRQABVf8c' +
'AAcho4AAiD4='));
let _haveH264Decode = null;
async function haveH264Decode() {
if (_haveH264Decode !== null) {
return _haveH264Decode;
}
if (!('VideoDecoder' in window)) {
_haveH264Decode = false;
return false;
}
// We'll need to make do with some placeholders here
const config = {
codec: 'avc1.42401f',
codedWidth: 1920,
codedHeight: 1080,
optimizeForLatency: true,
};
_haveH264Decode = await VideoDecoder.isConfigSupported(config);
return _haveH264Decode;
}
function createSolidColorFrameBuffer(color, width, height) {
const r = (color >> 24) & 0xff;
const g = (color >> 16) & 0xff;
const b = (color >> 8) & 0xff;
const a = (color >> 0) & 0xff;
const size = width * height * 4;
let array = new Uint8ClampedArray(size);
for (let i = 0; i < size / 4; ++i) {
array[i * 4 + 0] = r;
array[i * 4 + 1] = g;
array[i * 4 + 2] = b;
array[i * 4 + 3] = a;
}
return array;
}
function makeMessageHeader(length, resetContext, resetAllContexts) {
let flags = 0;
if (resetContext) {
flags |= 1;
}
if (resetAllContexts) {
flags |= 2;
}
let header = new Uint8Array(8);
let i = 0;
let appendU32 = (v) => {
header[i++] = (v >> 24) & 0xff;
header[i++] = (v >> 16) & 0xff;
header[i++] = (v >> 8) & 0xff;
header[i++] = v & 0xff;
};
appendU32(length);
appendU32(flags);
return header;
}
function wrapRectData(data, resetContext, resetAllContexts) {
let header = makeMessageHeader(data.length, resetContext, resetAllContexts);
return Array.from(header).concat(Array.from(data));
}
function testDecodeRect(decoder, x, y, width, height, data, display, depth) {
let sock;
let done = false;
sock = new Websock;
sock.open("ws://example.com");
sock.on('message', () => {
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
});
// Empty messages are filtered at multiple layers, so we need to
// do a direct call
if (data.length === 0) {
done = decoder.decodeRect(x, y, width, height, sock, display, depth);
} else {
sock._websocket._receiveData(new Uint8Array(data));
}
display.flip();
return done;
}
function almost(a, b) {
let diff = Math.abs(a - b);
return diff < 5;
}
describe('H.264 Parser', function () {
it('should parse constrained baseline video', function () {
let parser = new H264Parser(redGreenBlue16x16Video);
let frame = parser.parse();
expect(frame).to.have.property('key', true);
expect(parser).to.have.property('profileIdc', 66);
expect(parser).to.have.property('constraintSet', 192);
expect(parser).to.have.property('levelIdc', 20);
frame = parser.parse();
expect(frame).to.have.property('key', false);
frame = parser.parse();
expect(frame).to.have.property('key', false);
frame = parser.parse();
expect(frame).to.be.null;
});
});
describe('H.264 Decoder Unit Test', function () {
let decoder;
beforeEach(async function () {
if (!await haveH264Decode()) {
this.skip();
return;
}
decoder = new H264Decoder();
});
it('creates and resets context', function () {
let context = decoder._getContext(1, 2, 3, 4);
expect(context._width).to.equal(3);
expect(context._height).to.equal(4);
expect(decoder._contexts).to.not.be.empty;
decoder._resetContext(1, 2, 3, 4);
expect(decoder._contexts).to.be.empty;
});
it('resets all contexts', function () {
decoder._getContext(0, 0, 1, 1);
decoder._getContext(2, 2, 1, 1);
expect(decoder._contexts).to.not.be.empty;
decoder._resetAllContexts();
expect(decoder._contexts).to.be.empty;
});
it('caches contexts', function () {
let c1 = decoder._getContext(1, 2, 3, 4);
c1.lastUsed = 1;
let c2 = decoder._getContext(1, 2, 3, 4);
c2.lastUsed = 2;
expect(Object.keys(decoder._contexts).length).to.equal(1);
expect(c1.lastUsed).to.equal(c2.lastUsed);
});
it('deletes oldest context', function () {
for (let i = 0; i < 65; ++i) {
let context = decoder._getContext(i, 0, 1, 1);
context.lastUsed = i;
}
expect(decoder._findOldestContextId()).to.equal('1,0,1,1');
expect(decoder._contexts[decoder._contextId(0, 0, 1, 1)]).to.be.undefined;
expect(decoder._contexts[decoder._contextId(1, 0, 1, 1)]).to.not.be.null;
expect(decoder._contexts[decoder._contextId(63, 0, 1, 1)]).to.not.be.null;
expect(decoder._contexts[decoder._contextId(64, 0, 1, 1)]).to.not.be.null;
});
});
describe('H.264 Decoder Functional Test', function () {
let decoder;
let display;
before(FakeWebSocket.replace);
after(FakeWebSocket.restore);
beforeEach(async function () {
if (!await haveH264Decode()) {
this.skip();
return;
}
decoder = new H264Decoder();
display = new Display(document.createElement('canvas'));
display.resize(16, 16);
});
it('should handle H.264 rect', async function () {
let data = wrapRectData(redGreenBlue16x16Video, false, false);
let done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24);
expect(done).to.be.true;
await display.flush();
let targetData = createSolidColorFrameBuffer(0x0000ffff, 16, 16);
expect(display).to.have.displayed(targetData, almost);
});
it('should handle specific context reset', async function () {
let data = wrapRectData(redGreenBlue16x16Video, false, false);
let done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24);
expect(done).to.be.true;
await display.flush();
let targetData = createSolidColorFrameBuffer(0x0000ffff, 16, 16);
expect(display).to.have.displayed(targetData, almost);
data = wrapRectData([], true, false);
done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24);
expect(done).to.be.true;
await display.flush();
expect(decoder._contexts[decoder._contextId(0, 0, 16, 16)]._decoder).to.be.null;
});
it('should handle global context reset', async function () {
let data = wrapRectData(redGreenBlue16x16Video, false, false);
let done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24);
expect(done).to.be.true;
await display.flush();
let targetData = createSolidColorFrameBuffer(0x0000ffff, 16, 16);
expect(display).to.have.displayed(targetData, almost);
data = wrapRectData([], false, true);
done = testDecodeRect(decoder, 0, 0, 16, 16, data, display, 24);
expect(done).to.be.true;
await display.flush();
expect(decoder._contexts[decoder._contextId(0, 0, 16, 16)]._decoder).to.be.null;
});
});

View File

@ -261,15 +261,20 @@ describe('Websock', function () {
});
it('should implicitly split a large buffer', function () {
let str = '';
let expected = [];
for (let i = 0;i < bufferSize * 3;i++) {
let byte = Math.random() * 0xff;
str += String.fromCharCode(byte);
expected.push(byte);
for (let i = 0;i <= bufferSize/5;i++) {
str += '\x12\x34\x56\x78\x90';
}
sock.sQpushString(str);
sock.flush();
let expected = [];
for (let i = 0;i < bufferSize/5;i++) {
expected.push(0x12);
expected.push(0x34);
expected.push(0x56);
expected.push(0x78);
expected.push(0x90);
}
expect(sock).to.have.sent(new Uint8Array(expected));
});
@ -303,15 +308,24 @@ describe('Websock', function () {
});
it('should implicitly split a large buffer', function () {
let buffer = [];
let expected = [];
for (let i = 0;i < bufferSize * 3;i++) {
let byte = Math.random() * 0xff;
buffer.push(byte);
expected.push(byte);
for (let i = 0;i <= bufferSize/5;i++) {
buffer.push(0x12);
buffer.push(0x34);
buffer.push(0x56);
buffer.push(0x78);
buffer.push(0x90);
}
sock.sQpushBytes(new Uint8Array(buffer));
sock.flush();
let expected = [];
for (let i = 0;i < bufferSize/5;i++) {
expected.push(0x12);
expected.push(0x34);
expected.push(0x56);
expected.push(0x78);
expected.push(0x90);
}
expect(sock).to.have.sent(new Uint8Array(expected));
});

View File

@ -46,56 +46,7 @@
<link rel="preload" as="image" href="app/images/warning.svg">
<script type="module" crossorigin="anonymous" src="app/error-handler.js"></script>
<script type="module">
import UI from "./app/ui.js";
import * as Log from './core/util/logging.js';
let response;
let defaults = {};
let mandatory = {};
// Default settings will be loaded from defaults.json. Mandatory
// settings will be loaded from mandatory.json, which the user
// cannot change.
try {
response = await fetch('./defaults.json');
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
defaults = await response.json();
} catch (err) {
Log.Error("Couldn't fetch defaults.json: " + err);
}
try {
response = await fetch('./mandatory.json');
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
mandatory = await response.json();
} catch (err) {
Log.Error("Couldn't fetch mandatory.json: " + err);
}
// You can also override any defaults you need here:
//
// defaults['host'] = 'vnc.example.com';
// Or force a specific setting, preventing the user from
// changing it:
//
// mandatory['view_only'] = true;
// See docs/EMBEDDING.md for a list of possible settings.
UI.start({ settings: { defaults: defaults,
mandatory: mandatory } });
</script>
<script type="module" crossorigin="anonymous" src="app/ui.js"></script>
</head>
<body>