diff --git a/web/js/.gitattributes b/web/js/.gitattributes
new file mode 100644
index 000000000..176a458f9
--- /dev/null
+++ b/web/js/.gitattributes
@@ -0,0 +1 @@
+* text=auto
diff --git a/web/js/.gitignore b/web/js/.gitignore
new file mode 100644
index 000000000..620c5689f
--- /dev/null
+++ b/web/js/.gitignore
@@ -0,0 +1,8 @@
+node_modules
+.DS_Store
+dist
+dist-ssr
+*.local
+*log
+ogvjs
+.vscode
diff --git a/web/js/gen_js_from_hbb.py b/web/js/gen_js_from_hbb.py
new file mode 100755
index 000000000..7e9d78cbe
--- /dev/null
+++ b/web/js/gen_js_from_hbb.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+
+import re
+import os
+import glob
+from tabnanny import check
+
+def main():
+ print('export const LANGS = {')
+ for fn in glob.glob('../hbb/src/lang/*'):
+ lang = os.path.basename(fn)[:-3]
+ if lang == 'template': continue
+ print(' %s: {'%lang)
+ for ln in open(fn):
+ ln = ln.strip()
+ if ln.startswith('("'):
+ toks = ln.split('", "')
+ assert(len(toks) == 2)
+ a = toks[0][2:]
+ b = toks[1][:-3]
+ print(' "%s": "%s",'%(a, b))
+ print(' },')
+ print('}')
+ check_if_retry = ['', False]
+ KEY_MAP = ['', False]
+ for ln in open('../hbb/src/client.rs'):
+ ln = ln.strip()
+ if 'check_if_retry' in ln:
+ check_if_retry[1] = True
+ continue
+ if ln.startswith('}') and check_if_retry[1]:
+ check_if_retry[1] = False
+ continue
+ if check_if_retry[1]:
+ ln = removeComment(ln)
+ check_if_retry[0] += ln + '\n'
+ if 'KEY_MAP' in ln:
+ KEY_MAP[1] = True
+ continue
+ if '.collect' in ln and KEY_MAP[1]:
+ KEY_MAP[1] = False
+ continue
+ if KEY_MAP[1] and ln.startswith('('):
+ ln = removeComment(ln)
+ toks = ln.split('", Key::')
+ assert(len(toks) == 2)
+ a = toks[0][2:]
+ b = toks[1].replace('ControlKey(ControlKey::', '').replace("Chr('", '').replace("' as _)),", '').replace(')),', '')
+ KEY_MAP[0] += ' "%s": "%s",\n'%(a, b)
+ print()
+ print('export function checkIfRetry(msgtype: string, title: string, text: string) {')
+ print(' return %s'%check_if_retry[0].replace('to_lowercase', 'toLowerCase').replace('contains', 'indexOf').replace('!', '').replace('")', '") < 0'))
+ print(';}')
+ print()
+ print('export const KEY_MAP: any = {')
+ print(KEY_MAP[0])
+ print('}')
+ for ln in open('../hbb/Cargo.toml'):
+ if ln.startswith('version ='):
+ print('export const ' + ln)
+
+
+def removeComment(ln):
+ return re.sub('\s+\/\/.*$', '', ln)
+
+main()
diff --git a/web/js/index.html b/web/js/index.html
new file mode 100644
index 000000000..0ae0a2410
--- /dev/null
+++ b/web/js/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+ Vite App
+
+
+
+
+
+
diff --git a/web/js/package.json b/web/js/package.json
new file mode 100644
index 000000000..15e0e75b8
--- /dev/null
+++ b/web/js/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "web_hbb",
+ "version": "1.0.0",
+ "scripts": {
+ "dev": "vite",
+ "build": "./gen_js_from_hbb.py > src/gen_js_from_hbb.ts && ./ts_proto.py && tsc && vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "typescript": "^4.4.4",
+ "vite": "^2.7.2"
+ },
+ "dependencies": {
+ "fast-sha256": "^1.3.0",
+ "libsodium": "^0.7.9",
+ "libsodium-wrappers": "^0.7.9",
+ "pcm-player": "^0.0.11",
+ "ts-proto": "^1.101.0",
+ "wasm-feature-detect": "^1.2.11",
+ "zstddec": "^0.0.2"
+ }
+}
diff --git a/web/js/src/codec.js b/web/js/src/codec.js
new file mode 100644
index 000000000..dc579b5f3
--- /dev/null
+++ b/web/js/src/codec.js
@@ -0,0 +1,43 @@
+// example: https://github.com/rgov/js-theora-decoder/blob/main/index.html
+// https://github.com/brion/ogv.js/releases, yarn add has no simd
+// dev: copy decoder files from node/ogv/dist/* to project dir
+// dist: .... to dist
+/*
+ OGVDemuxerOggW: 'ogv-demuxer-ogg-wasm.js',
+ OGVDemuxerWebMW: 'ogv-demuxer-webm-wasm.js',
+ OGVDecoderAudioOpusW: 'ogv-decoder-audio-opus-wasm.js',
+ OGVDecoderAudioVorbisW: 'ogv-decoder-audio-vorbis-wasm.js',
+ OGVDecoderVideoTheoraW: 'ogv-decoder-video-theora-wasm.js',
+ OGVDecoderVideoVP8W: 'ogv-decoder-video-vp8-wasm.js',
+ OGVDecoderVideoVP8MTW: 'ogv-decoder-video-vp8-mt-wasm.js',
+ OGVDecoderVideoVP9W: 'ogv-decoder-video-vp9-wasm.js',
+ OGVDecoderVideoVP9SIMDW: 'ogv-decoder-video-vp9-simd-wasm.js',
+ OGVDecoderVideoVP9MTW: 'ogv-decoder-video-vp9-mt-wasm.js',
+ OGVDecoderVideoVP9SIMDMTW: 'ogv-decoder-video-vp9-simd-mt-wasm.js',
+ OGVDecoderVideoAV1W: 'ogv-decoder-video-av1-wasm.js',
+ OGVDecoderVideoAV1SIMDW: 'ogv-decoder-video-av1-simd-wasm.js',
+ OGVDecoderVideoAV1MTW: 'ogv-decoder-video-av1-mt-wasm.js',
+ OGVDecoderVideoAV1SIMDMTW: 'ogv-decoder-video-av1-simd-mt-wasm.js',
+*/
+import { simd } from "wasm-feature-detect";
+
+export async function loadVp9(callback) {
+ // Multithreading is used only if `options.threading` is true.
+ // This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs,
+ // currently available in Firefox and Chrome with experimental flags enabled.
+ // 所有主流浏览器均默认于2018年1月5日禁用SharedArrayBuffer
+ const isSIMD = await simd();
+ console.log('isSIMD: ' + isSIMD);
+ window.OGVLoader.loadClass(
+ isSIMD ? "OGVDecoderVideoVP9SIMDW" : "OGVDecoderVideoVP9W",
+ (videoCodecClass) => {
+ window.videoCodecClass = videoCodecClass;
+ videoCodecClass({ videoFormat: {} }).then((decoder) => {
+ decoder.init(() => {
+ callback(decoder);
+ })
+ })
+ },
+ { worker: true, threading: true }
+ );
+}
\ No newline at end of file
diff --git a/web/js/src/common.ts b/web/js/src/common.ts
new file mode 100644
index 000000000..8da049a4d
--- /dev/null
+++ b/web/js/src/common.ts
@@ -0,0 +1,77 @@
+import * as zstd from "zstddec";
+import { KeyEvent, controlKeyFromJSON, ControlKey } from "./message";
+import { KEY_MAP, LANGS } from "./gen_js_from_hbb";
+
+let decompressor: zstd.ZSTDDecoder;
+
+export async function initZstd() {
+ const tmp = new zstd.ZSTDDecoder();
+ await tmp.init();
+ console.log("zstd ready");
+ decompressor = tmp;
+}
+
+export async function decompress(compressedArray: Uint8Array) {
+ const MAX = 1024 * 1024 * 64;
+ const MIN = 1024 * 1024;
+ let n = 30 * compressedArray.length;
+ if (n > MAX) {
+ n = MAX;
+ }
+ if (n < MIN) {
+ n = MIN;
+ }
+ try {
+ if (!decompressor) {
+ await initZstd();
+ }
+ return decompressor.decode(compressedArray, n);
+ } catch (e) {
+ console.error("decompress failed: " + e);
+ return undefined;
+ }
+}
+
+const LANG = getLang();
+
+export function translate(locale: string, text: string): string {
+ const lang = LANG || locale.substring(locale.length - 2).toLowerCase();
+ let en = LANGS.en as any;
+ let dict = (LANGS as any)[lang];
+ if (!dict) dict = en;
+ let res = dict[text];
+ if (!res && lang != "en") res = en[text];
+ return res || text;
+}
+
+const zCode = "z".charCodeAt(0);
+const aCode = "a".charCodeAt(0);
+
+export function mapKey(name: string, isDesktop: Boolean) {
+ const tmp = KEY_MAP[name] || name;
+ if (tmp.length == 1) {
+ const chr = tmp.charCodeAt(0);
+ if (!isDesktop && (chr > zCode || chr < aCode))
+ return KeyEvent.fromPartial({ unicode: chr });
+ else return KeyEvent.fromPartial({ chr });
+ }
+ const control_key = controlKeyFromJSON(tmp);
+ if (control_key == ControlKey.UNRECOGNIZED) {
+ console.error("Unknown control key " + tmp);
+ }
+ return KeyEvent.fromPartial({ control_key });
+}
+
+export async function sleep(ms: number) {
+ await new Promise((r) => setTimeout(r, ms));
+}
+
+function getLang(): string {
+ try {
+ const queryString = window.location.search;
+ const urlParams = new URLSearchParams(queryString);
+ return urlParams.get("lang") || "";
+ } catch (e) {
+ return "";
+ }
+}
diff --git a/web/js/src/connection.ts b/web/js/src/connection.ts
new file mode 100644
index 000000000..2846d9078
--- /dev/null
+++ b/web/js/src/connection.ts
@@ -0,0 +1,773 @@
+import Websock from "./websock";
+import * as message from "./message.js";
+import * as rendezvous from "./rendezvous.js";
+import { loadVp9 } from "./codec";
+import * as sha256 from "fast-sha256";
+import * as globals from "./globals";
+import { decompress, mapKey, sleep } from "./common";
+
+const PORT = 21116;
+const HOSTS = [
+ "rs-sg.rustdesk.com",
+ "rs-cn.rustdesk.com",
+ "rs-us.rustdesk.com",
+];
+let HOST = localStorage.getItem("rendezvous-server") || HOSTS[0];
+const SCHEMA = "ws://";
+
+type MsgboxCallback = (type: string, title: string, text: string) => void;
+type DrawCallback = (data: Uint8Array) => void;
+//const cursorCanvas = document.createElement("canvas");
+
+export default class Connection {
+ _msgs: any[];
+ _ws: Websock | undefined;
+ _interval: any;
+ _id: string;
+ _hash: message.Hash | undefined;
+ _msgbox: MsgboxCallback;
+ _draw: DrawCallback;
+ _peerInfo: message.PeerInfo | undefined;
+ _firstFrame: Boolean | undefined;
+ _videoDecoder: any;
+ _password: Uint8Array | undefined;
+ _options: any;
+ _videoTestSpeed: number[];
+ //_cursors: { [name: number]: any };
+
+ constructor() {
+ this._msgbox = globals.msgbox;
+ this._draw = globals.draw;
+ this._msgs = [];
+ this._id = "";
+ this._videoTestSpeed = [0, 0];
+ //this._cursors = {};
+ }
+
+ async start(id: string) {
+ try {
+ await this._start(id);
+ } catch (e: any) {
+ this.msgbox(
+ "error",
+ "Connection Error",
+ e.type == "close" ? "Reset by the peer" : String(e)
+ );
+ }
+ }
+
+ async _start(id: string) {
+ if (!this._options) {
+ this._options = globals.getPeers()[id] || {};
+ }
+ if (!this._password) {
+ const p = this.getOption("password");
+ if (p) {
+ try {
+ this._password = Uint8Array.from(JSON.parse("[" + p + "]"));
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+ this._interval = setInterval(() => {
+ while (this._msgs.length) {
+ this._ws?.sendMessage(this._msgs[0]);
+ this._msgs.splice(0, 1);
+ }
+ }, 1);
+ this.loadVideoDecoder();
+ const uri = getDefaultUri();
+ const ws = new Websock(uri, true);
+ this._ws = ws;
+ this._id = id;
+ console.log(
+ new Date() + ": Conntecting to rendezvoous server: " + uri + ", for " + id
+ );
+ await ws.open();
+ console.log(new Date() + ": Connected to rendezvoous server");
+ const conn_type = rendezvous.ConnType.DEFAULT_CONN;
+ const nat_type = rendezvous.NatType.SYMMETRIC;
+ const punch_hole_request = rendezvous.PunchHoleRequest.fromPartial({
+ id,
+ licence_key: localStorage.getItem("key") || undefined,
+ conn_type,
+ nat_type,
+ token: localStorage.getItem("access_token") || undefined,
+ });
+ ws.sendRendezvous({ punch_hole_request });
+ const msg = (await ws.next()) as rendezvous.RendezvousMessage;
+ ws.close();
+ console.log(new Date() + ": Got relay response");
+ const phr = msg.punch_hole_response;
+ const rr = msg.relay_response;
+ if (phr) {
+ if (phr?.other_failure) {
+ this.msgbox("error", "Error", phr?.other_failure);
+ return;
+ }
+ if (phr.failure != rendezvous.PunchHoleResponse_Failure.UNRECOGNIZED) {
+ switch (phr?.failure) {
+ case rendezvous.PunchHoleResponse_Failure.ID_NOT_EXIST:
+ this.msgbox("error", "Error", "ID does not exist");
+ break;
+ case rendezvous.PunchHoleResponse_Failure.OFFLINE:
+ this.msgbox("error", "Error", "Remote desktop is offline");
+ break;
+ case rendezvous.PunchHoleResponse_Failure.LICENSE_MISMATCH:
+ this.msgbox("error", "Error", "Key mismatch");
+ break;
+ case rendezvous.PunchHoleResponse_Failure.LICENSE_OVERUSE:
+ this.msgbox("error", "Error", "Key overuse");
+ break;
+ }
+ }
+ } else if (rr) {
+ if (!rr.version) {
+ this.msgbox("error", "Error", "Remote version is low, not support web");
+ return;
+ }
+ await this.connectRelay(rr);
+ }
+ }
+
+ async connectRelay(rr: rendezvous.RelayResponse) {
+ const pk = rr.pk;
+ let uri = rr.relay_server;
+ if (uri) {
+ uri = getrUriFromRs(uri, true, 2);
+ } else {
+ uri = getDefaultUri(true);
+ }
+ const uuid = rr.uuid;
+ console.log(new Date() + ": Connecting to relay server: " + uri);
+ const ws = new Websock(uri, false);
+ await ws.open();
+ console.log(new Date() + ": Connected to relay server");
+ this._ws = ws;
+ const request_relay = rendezvous.RequestRelay.fromPartial({
+ licence_key: localStorage.getItem("key") || undefined,
+ uuid,
+ });
+ ws.sendRendezvous({ request_relay });
+ const secure = (await this.secure(pk)) || false;
+ globals.pushEvent("connection_ready", { secure, direct: false });
+ await this.msgLoop();
+ }
+
+ async secure(pk: Uint8Array | undefined) {
+ if (pk) {
+ const RS_PK = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=";
+ try {
+ pk = await globals.verify(pk, localStorage.getItem("key") || RS_PK);
+ if (pk) {
+ const idpk = message.IdPk.decode(pk);
+ if (idpk.id == this._id) {
+ pk = idpk.pk;
+ }
+ }
+ if (pk?.length != 32) {
+ pk = undefined;
+ }
+ } catch (e) {
+ console.error(e);
+ pk = undefined;
+ }
+ if (!pk)
+ console.error(
+ "Handshake failed: invalid public key from rendezvous server"
+ );
+ }
+ if (!pk) {
+ // send an empty message out in case server is setting up secure and waiting for first message
+ const public_key = message.PublicKey.fromPartial({});
+ this._ws?.sendMessage({ public_key });
+ return;
+ }
+ const msg = (await this._ws?.next()) as message.Message;
+ let signedId: any = msg?.signed_id;
+ if (!signedId) {
+ console.error("Handshake failed: invalid message type");
+ const public_key = message.PublicKey.fromPartial({});
+ this._ws?.sendMessage({ public_key });
+ return;
+ }
+ try {
+ signedId = await globals.verify(signedId.id, Uint8Array.from(pk!));
+ } catch (e) {
+ console.error(e);
+ // fall back to non-secure connection in case pk mismatch
+ console.error("pk mismatch, fall back to non-secure");
+ const public_key = message.PublicKey.fromPartial({});
+ this._ws?.sendMessage({ public_key });
+ return;
+ }
+ const idpk = message.IdPk.decode(signedId);
+ const id = idpk.id;
+ const theirPk = idpk.pk;
+ if (id != this._id!) {
+ console.error("Handshake failed: sign failure");
+ const public_key = message.PublicKey.fromPartial({});
+ this._ws?.sendMessage({ public_key });
+ return;
+ }
+ if (theirPk.length != 32) {
+ console.error(
+ "Handshake failed: invalid public box key length from peer"
+ );
+ const public_key = message.PublicKey.fromPartial({});
+ this._ws?.sendMessage({ public_key });
+ return;
+ }
+ const [mySk, asymmetric_value] = globals.genBoxKeyPair();
+ const secret_key = globals.genSecretKey();
+ const symmetric_value = globals.seal(secret_key, theirPk, mySk);
+ const public_key = message.PublicKey.fromPartial({
+ asymmetric_value,
+ symmetric_value,
+ });
+ this._ws?.sendMessage({ public_key });
+ this._ws?.setSecretKey(secret_key);
+ console.log("secured");
+ return true;
+ }
+
+ async msgLoop() {
+ while (true) {
+ const msg = (await this._ws?.next()) as message.Message;
+ if (msg?.hash) {
+ this._hash = msg?.hash;
+ if (!this._password)
+ this.msgbox("input-password", "Password Required", "");
+ this.login();
+ } else if (msg?.test_delay) {
+ const test_delay = msg?.test_delay;
+ console.log(test_delay);
+ if (!test_delay.from_client) {
+ this._ws?.sendMessage({ test_delay });
+ }
+ } else if (msg?.login_response) {
+ const r = msg?.login_response;
+ if (r.error) {
+ if (r.error == "Wrong Password") {
+ this._password = undefined;
+ this.msgbox(
+ "re-input-password",
+ r.error,
+ "Do you want to enter again?"
+ );
+ } else {
+ this.msgbox("error", "Login Error", r.error);
+ }
+ } else if (r.peer_info) {
+ this.handlePeerInfo(r.peer_info);
+ }
+ } else if (msg?.video_frame) {
+ this.handleVideoFrame(msg?.video_frame!);
+ } else if (msg?.clipboard) {
+ const cb = msg?.clipboard;
+ if (cb.compress) {
+ const c = await decompress(cb.content);
+ if (!c) continue;
+ cb.content = c;
+ }
+ try {
+ globals.copyToClipboard(new TextDecoder().decode(cb.content));
+ } catch (e) {
+ console.error(e);
+ }
+ // globals.pushEvent("clipboard", cb);
+ } else if (msg?.cursor_data) {
+ const cd = msg?.cursor_data;
+ const c = await decompress(cd.colors);
+ if (!c) continue;
+ cd.colors = c;
+ globals.pushEvent("cursor_data", cd);
+ /*
+ let ctx = cursorCanvas.getContext("2d");
+ cursorCanvas.width = cd.width;
+ cursorCanvas.height = cd.height;
+ let imgData = new ImageData(
+ new Uint8ClampedArray(c),
+ cd.width,
+ cd.height
+ );
+ ctx?.clearRect(0, 0, cd.width, cd.height);
+ ctx?.putImageData(imgData, 0, 0);
+ let url = cursorCanvas.toDataURL();
+ const img = document.createElement("img");
+ img.src = url;
+ this._cursors[cd.id] = img;
+ //cursorCanvas.width /= 2.;
+ //cursorCanvas.height /= 2.;
+ //ctx?.drawImage(img, cursorCanvas.width, cursorCanvas.height);
+ url = cursorCanvas.toDataURL();
+ document.body.style.cursor =
+ "url(" + url + ")" + cd.hotx + " " + cd.hoty + ", default";
+ console.log(document.body.style.cursor);
+ */
+ } else if (msg?.cursor_id) {
+ globals.pushEvent("cursor_id", { id: msg?.cursor_id });
+ } else if (msg?.cursor_position) {
+ globals.pushEvent("cursor_position", msg?.cursor_position);
+ } else if (msg?.misc) {
+ if (!this.handleMisc(msg?.misc)) break;
+ } else if (msg?.audio_frame) {
+ globals.playAudio(msg?.audio_frame.data);
+ }
+ }
+ }
+
+ msgbox(type_: string, title: string, text: string) {
+ this._msgbox?.(type_, title, text);
+ }
+
+ draw(frame: any) {
+ this._draw?.(frame);
+ globals.draw(frame);
+ }
+
+ close() {
+ this._msgs = [];
+ clearInterval(this._interval);
+ this._ws?.close();
+ this._videoDecoder?.close();
+ }
+
+ refresh() {
+ const misc = message.Misc.fromPartial({ refresh_video: true });
+ this._ws?.sendMessage({ misc });
+ }
+
+ setMsgbox(callback: MsgboxCallback) {
+ this._msgbox = callback;
+ }
+
+ setDraw(callback: DrawCallback) {
+ this._draw = callback;
+ }
+
+ login(password: string | undefined = undefined) {
+ if (password) {
+ const salt = this._hash?.salt;
+ let p = hash([password, salt!]);
+ this._password = p;
+ const challenge = this._hash?.challenge;
+ p = hash([p, challenge!]);
+ this.msgbox("connecting", "Connecting...", "Logging in...");
+ this._sendLoginMessage(p);
+ } else {
+ let p = this._password;
+ if (p) {
+ const challenge = this._hash?.challenge;
+ p = hash([p, challenge!]);
+ }
+ this._sendLoginMessage(p);
+ }
+ }
+
+ async reconnect() {
+ this.close();
+ await this.start(this._id);
+ }
+
+ _sendLoginMessage(password: Uint8Array | undefined = undefined) {
+ const login_request = message.LoginRequest.fromPartial({
+ username: this._id!,
+ my_id: "web", // to-do
+ my_name: "web", // to-do
+ password,
+ option: this.getOptionMessage(),
+ video_ack_required: true,
+ });
+ this._ws?.sendMessage({ login_request });
+ }
+
+ getOptionMessage(): message.OptionMessage | undefined {
+ let n = 0;
+ const msg = message.OptionMessage.fromPartial({});
+ const q = this.getImageQualityEnum(this.getImageQuality(), true);
+ const yes = message.OptionMessage_BoolOption.Yes;
+ if (q != undefined) {
+ msg.image_quality = q;
+ n += 1;
+ }
+ if (this._options["show-remote-cursor"]) {
+ msg.show_remote_cursor = yes;
+ n += 1;
+ }
+ if (this._options["lock-after-session-end"]) {
+ msg.lock_after_session_end = yes;
+ n += 1;
+ }
+ if (this._options["privacy-mode"]) {
+ msg.privacy_mode = yes;
+ n += 1;
+ }
+ if (this._options["disable-audio"]) {
+ msg.disable_audio = yes;
+ n += 1;
+ }
+ if (this._options["disable-clipboard"]) {
+ msg.disable_clipboard = yes;
+ n += 1;
+ }
+ return n > 0 ? msg : undefined;
+ }
+
+ sendVideoReceived() {
+ const misc = message.Misc.fromPartial({ video_received: true });
+ this._ws?.sendMessage({ misc });
+ }
+
+ handleVideoFrame(vf: message.VideoFrame) {
+ if (!this._firstFrame) {
+ this.msgbox("", "", "");
+ this._firstFrame = true;
+ }
+ if (vf.vp9s) {
+ const dec = this._videoDecoder;
+ var tm = new Date().getTime();
+ var i = 0;
+ const n = vf.vp9s?.frames.length;
+ vf.vp9s.frames.forEach((f) => {
+ dec.processFrame(f.data.slice(0).buffer, (ok: any) => {
+ i++;
+ if (i == n) this.sendVideoReceived();
+ if (ok && dec.frameBuffer && n == i) {
+ this.draw(dec.frameBuffer);
+ const now = new Date().getTime();
+ var elapsed = now - tm;
+ this._videoTestSpeed[1] += elapsed;
+ this._videoTestSpeed[0] += 1;
+ if (this._videoTestSpeed[0] >= 30) {
+ console.log(
+ "video decoder: " +
+ parseInt(
+ "" + this._videoTestSpeed[1] / this._videoTestSpeed[0]
+ )
+ );
+ this._videoTestSpeed = [0, 0];
+ }
+ }
+ });
+ });
+ }
+ }
+
+ handlePeerInfo(pi: message.PeerInfo) {
+ this._peerInfo = pi;
+ if (pi.displays.length == 0) {
+ this.msgbox("error", "Remote Error", "No Display");
+ return;
+ }
+ this.msgbox("success", "Successful", "Connected, waiting for image...");
+ globals.pushEvent("peer_info", pi);
+ const p = this.shouldAutoLogin();
+ if (p) this.inputOsPassword(p);
+ const username = this.getOption("info")?.username;
+ if (username && !pi.username) pi.username = username;
+ this.setOption("info", pi);
+ if (this.getRemember()) {
+ if (this._password?.length) {
+ const p = this._password.toString();
+ if (p != this.getOption("password")) {
+ this.setOption("password", p);
+ console.log("remember password of " + this._id);
+ }
+ }
+ } else {
+ this.setOption("password", undefined);
+ }
+ }
+
+ shouldAutoLogin(): string {
+ const l = this.getOption("lock-after-session-end");
+ const a = !!this.getOption("auto-login");
+ const p = this.getOption("os-password");
+ if (p && l && a) {
+ return p;
+ }
+ return "";
+ }
+
+ handleMisc(misc: message.Misc) {
+ if (misc.audio_format) {
+ globals.initAudio(
+ misc.audio_format.channels,
+ misc.audio_format.sample_rate
+ );
+ } else if (misc.chat_message) {
+ globals.pushEvent("chat", { text: misc.chat_message.text });
+ } else if (misc.permission_info) {
+ const p = misc.permission_info;
+ console.info("Change permission " + p.permission + " -> " + p.enabled);
+ let name;
+ switch (p.permission) {
+ case message.PermissionInfo_Permission.Keyboard:
+ name = "keyboard";
+ break;
+ case message.PermissionInfo_Permission.Clipboard:
+ name = "clipboard";
+ break;
+ case message.PermissionInfo_Permission.Audio:
+ name = "audio";
+ break;
+ default:
+ return;
+ }
+ globals.pushEvent("permission", { [name]: p.enabled });
+ } else if (misc.switch_display) {
+ this.loadVideoDecoder();
+ globals.pushEvent("switch_display", misc.switch_display);
+ } else if (misc.close_reason) {
+ this.msgbox("error", "Connection Error", misc.close_reason);
+ this.close();
+ return false;
+ }
+ return true;
+ }
+
+ getRemember(): Boolean {
+ return this._options["remember"] || false;
+ }
+
+ setRemember(v: Boolean) {
+ this.setOption("remember", v);
+ }
+
+ getOption(name: string): any {
+ return this._options[name];
+ }
+
+ setOption(name: string, value: any) {
+ if (value == undefined) {
+ delete this._options[name];
+ } else {
+ this._options[name] = value;
+ }
+ this._options["tm"] = new Date().getTime();
+ const peers = globals.getPeers();
+ peers[this._id] = this._options;
+ localStorage.setItem("peers", JSON.stringify(peers));
+ }
+
+ inputKey(
+ name: string,
+ down: boolean,
+ press: boolean,
+ alt: Boolean,
+ ctrl: Boolean,
+ shift: Boolean,
+ command: Boolean
+ ) {
+ const key_event = mapKey(name, globals.isDesktop());
+ if (!key_event) return;
+ if (alt && (name == "VK_MENU" || name == "RAlt")) {
+ alt = false;
+ }
+ if (ctrl && (name == "VK_CONTROL" || name == "RControl")) {
+ ctrl = false;
+ }
+ if (shift && (name == "VK_SHIFT" || name == "RShift")) {
+ shift = false;
+ }
+ if (command && (name == "Meta" || name == "RWin")) {
+ command = false;
+ }
+ key_event.down = down;
+ key_event.press = press;
+ key_event.modifiers = this.getMod(alt, ctrl, shift, command);
+ this._ws?.sendMessage({ key_event });
+ }
+
+ ctrlAltDel() {
+ const key_event = message.KeyEvent.fromPartial({ down: true });
+ if (this._peerInfo?.platform == "Windows") {
+ key_event.control_key = message.ControlKey.CtrlAltDel;
+ } else {
+ key_event.control_key = message.ControlKey.Delete;
+ key_event.modifiers = this.getMod(true, true, false, false);
+ }
+ this._ws?.sendMessage({ key_event });
+ }
+
+ inputString(seq: string) {
+ const key_event = message.KeyEvent.fromPartial({ seq });
+ this._ws?.sendMessage({ key_event });
+ }
+
+ switchDisplay(display: number) {
+ const switch_display = message.SwitchDisplay.fromPartial({ display });
+ const misc = message.Misc.fromPartial({ switch_display });
+ this._ws?.sendMessage({ misc });
+ }
+
+ async inputOsPassword(seq: string) {
+ this.inputMouse();
+ await sleep(50);
+ this.inputMouse(0, 3, 3);
+ await sleep(50);
+ this.inputMouse(1 | (1 << 3));
+ this.inputMouse(2 | (1 << 3));
+ await sleep(1200);
+ const key_event = message.KeyEvent.fromPartial({ press: true, seq });
+ this._ws?.sendMessage({ key_event });
+ }
+
+ lockScreen() {
+ const key_event = message.KeyEvent.fromPartial({
+ down: true,
+ control_key: message.ControlKey.LockScreen,
+ });
+ this._ws?.sendMessage({ key_event });
+ }
+
+ getMod(alt: Boolean, ctrl: Boolean, shift: Boolean, command: Boolean) {
+ const mod: message.ControlKey[] = [];
+ if (alt) mod.push(message.ControlKey.Alt);
+ if (ctrl) mod.push(message.ControlKey.Control);
+ if (shift) mod.push(message.ControlKey.Shift);
+ if (command) mod.push(message.ControlKey.Meta);
+ return mod;
+ }
+
+ inputMouse(
+ mask: number = 0,
+ x: number = 0,
+ y: number = 0,
+ alt: Boolean = false,
+ ctrl: Boolean = false,
+ shift: Boolean = false,
+ command: Boolean = false
+ ) {
+ const mouse_event = message.MouseEvent.fromPartial({
+ mask,
+ x,
+ y,
+ modifiers: this.getMod(alt, ctrl, shift, command),
+ });
+ this._ws?.sendMessage({ mouse_event });
+ }
+
+ toggleOption(name: string) {
+ const v = !this._options[name];
+ const option = message.OptionMessage.fromPartial({});
+ const v2 = v
+ ? message.OptionMessage_BoolOption.Yes
+ : message.OptionMessage_BoolOption.No;
+ switch (name) {
+ case "show-remote-cursor":
+ option.show_remote_cursor = v2;
+ break;
+ case "disable-audio":
+ option.disable_audio = v2;
+ break;
+ case "disable-clipboard":
+ option.disable_clipboard = v2;
+ break;
+ case "lock-after-session-end":
+ option.lock_after_session_end = v2;
+ break;
+ case "privacy-mode":
+ option.privacy_mode = v2;
+ break;
+ case "block-input":
+ option.block_input = message.OptionMessage_BoolOption.Yes;
+ break;
+ case "unblock-input":
+ option.block_input = message.OptionMessage_BoolOption.No;
+ break;
+ default:
+ return;
+ }
+ if (name.indexOf("block-input") < 0) this.setOption(name, v);
+ const misc = message.Misc.fromPartial({ option });
+ this._ws?.sendMessage({ misc });
+ }
+
+ getImageQuality() {
+ return this.getOption("image-quality");
+ }
+
+ getImageQualityEnum(
+ value: string,
+ ignoreDefault: Boolean
+ ): message.ImageQuality | undefined {
+ switch (value) {
+ case "low":
+ return message.ImageQuality.Low;
+ case "best":
+ return message.ImageQuality.Best;
+ case "balanced":
+ return ignoreDefault ? undefined : message.ImageQuality.Balanced;
+ default:
+ return undefined;
+ }
+ }
+
+ setImageQuality(value: string) {
+ this.setOption("image-quality", value);
+ const image_quality = this.getImageQualityEnum(value, false);
+ if (image_quality == undefined) return;
+ const option = message.OptionMessage.fromPartial({ image_quality });
+ const misc = message.Misc.fromPartial({ option });
+ this._ws?.sendMessage({ misc });
+ }
+
+ loadVideoDecoder() {
+ this._videoDecoder?.close();
+ loadVp9((decoder: any) => {
+ this._videoDecoder = decoder;
+ console.log("vp9 loaded");
+ console.log(decoder);
+ });
+ }
+}
+
+function testDelay() {
+ var nearest = "";
+ HOSTS.forEach((host) => {
+ const now = new Date().getTime();
+ new Websock(getrUriFromRs(host), true).open().then(() => {
+ console.log("latency of " + host + ": " + (new Date().getTime() - now));
+ if (!nearest) {
+ HOST = host;
+ localStorage.setItem("rendezvous-server", host);
+ }
+ });
+ });
+}
+
+testDelay();
+
+function getDefaultUri(isRelay: Boolean = false): string {
+ const host = localStorage.getItem("custom-rendezvous-server");
+ return getrUriFromRs(host || HOST, isRelay);
+}
+
+function getrUriFromRs(
+ uri: string,
+ isRelay: Boolean = false,
+ roffset: number = 0
+): string {
+ if (uri.indexOf(":") > 0) {
+ const tmp = uri.split(":");
+ const port = parseInt(tmp[1]);
+ uri = tmp[0] + ":" + (port + (isRelay ? roffset || 3 : 2));
+ } else {
+ uri += ":" + (PORT + (isRelay ? 3 : 2));
+ }
+ return SCHEMA + uri;
+}
+
+function hash(datas: (string | Uint8Array)[]): Uint8Array {
+ const hasher = new sha256.Hash();
+ datas.forEach((data) => {
+ if (typeof data == "string") {
+ data = new TextEncoder().encode(data);
+ }
+ return hasher.update(data);
+ });
+ return hasher.digest();
+}
diff --git a/web/js/src/globals.js b/web/js/src/globals.js
new file mode 100644
index 000000000..a9eb941a5
--- /dev/null
+++ b/web/js/src/globals.js
@@ -0,0 +1,404 @@
+import Connection from "./connection";
+import _sodium from "libsodium-wrappers";
+import { CursorData } from "./message";
+import { loadVp9 } from "./codec";
+import { checkIfRetry, version } from "./gen_js_from_hbb";
+import { initZstd, translate } from "./common";
+import PCMPlayer from "pcm-player";
+
+var currentFrame = undefined;
+var events = [];
+
+window.curConn = undefined;
+window.getRgba = () => {
+ const tmp = currentFrame;
+ currentFrame = undefined;
+ return tmp || null;
+}
+window.isMobile = () => {
+ return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent)
+ || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4));
+}
+
+export function isDesktop() {
+ return !isMobile();
+}
+
+export function msgbox(type, title, text) {
+ if (!events) return;
+ if (!type || (type == 'error' && !text)) return;
+ const text2 = text.toLowerCase();
+ var hasRetry = checkIfRetry(type, title, text) ? 'true' : '';
+ events.push({ name: 'msgbox', type, title, text, hasRetry });
+}
+
+function jsonfyForDart(payload) {
+ var tmp = {};
+ for (const [key, value] of Object.entries(payload)) {
+ if (!key) continue;
+ tmp[key] = value instanceof Uint8Array ? '[' + value.toString() + ']' : JSON.stringify(value);
+ }
+ return tmp;
+}
+
+export function pushEvent(name, payload) {
+ if (!events) return;
+ payload = jsonfyForDart(payload);
+ payload.name = name;
+ events.push(payload);
+}
+
+let yuvWorker;
+let yuvCanvas;
+let gl;
+let pixels;
+let flipPixels;
+let oldSize;
+if (YUVCanvas.WebGLFrameSink.isAvailable()) {
+ var canvas = document.createElement('canvas');
+ yuvCanvas = YUVCanvas.attach(canvas, { webGL: true });
+ gl = canvas.getContext("webgl");
+} else {
+ yuvWorker = new Worker("./yuv.js");
+}
+let testSpeed = [0, 0];
+
+export function draw(frame) {
+ if (yuvWorker) {
+ // frame's (y/u/v).bytes already detached, can not transferrable any more.
+ yuvWorker.postMessage(frame);
+ } else {
+ var tm0 = new Date().getTime();
+ yuvCanvas.drawFrame(frame);
+ var width = canvas.width;
+ var height = canvas.height;
+ var size = width * height * 4;
+ if (size != oldSize) {
+ pixels = new Uint8Array(size);
+ flipPixels = new Uint8Array(size);
+ oldSize = size;
+ }
+ gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
+ const row = width * 4;
+ const end = (height - 1) * row;
+ for (let i = 0; i < size; i += row) {
+ flipPixels.set(pixels.subarray(i, i + row), end - i);
+ }
+ currentFrame = flipPixels;
+ testSpeed[1] += new Date().getTime() - tm0;
+ testSpeed[0] += 1;
+ if (testSpeed[0] > 30) {
+ console.log('gl: ' + parseInt('' + testSpeed[1] / testSpeed[0]));
+ testSpeed = [0, 0];
+ }
+ }
+ /*
+ var testCanvas = document.getElementById("test-yuv-decoder-canvas");
+ if (testCanvas && currentFrame) {
+ var ctx = testCanvas.getContext("2d");
+ testCanvas.width = frame.format.displayWidth;
+ testCanvas.height = frame.format.displayHeight;
+ var img = ctx.createImageData(testCanvas.width, testCanvas.height);
+ img.data.set(currentFrame);
+ ctx.putImageData(img, 0, 0);
+ }
+ */
+}
+
+export function sendOffCanvas(c) {
+ let canvas = c.transferControlToOffscreen();
+ yuvWorker.postMessage({ canvas }, [canvas]);
+}
+
+export function setConn(conn) {
+ window.curConn = conn;
+}
+
+export function getConn() {
+ return window.curConn;
+}
+
+export async function startConn(id) {
+ currentFrame = undefined;
+ events = [];
+ setByName('remote_id', id);
+ await curConn.start(id);
+}
+
+export function close() {
+ getConn()?.close();
+ setConn(undefined);
+ currentFrame = undefined;
+ events = undefined;
+}
+
+export function newConn() {
+ window.curConn?.close();
+ const conn = new Connection();
+ setConn(conn);
+ return conn;
+}
+
+let sodium;
+export async function verify(signed, pk) {
+ if (!sodium) {
+ await _sodium.ready;
+ sodium = _sodium;
+ }
+ if (typeof pk == 'string') {
+ pk = decodeBase64(pk);
+ }
+ return sodium.crypto_sign_open(signed, pk);
+}
+
+export function decodeBase64(pk) {
+ return sodium.from_base64(pk, sodium.base64_variants.ORIGINAL);
+}
+
+export function genBoxKeyPair() {
+ const pair = sodium.crypto_box_keypair();
+ const sk = pair.privateKey;
+ const pk = pair.publicKey;
+ return [sk, pk];
+}
+
+export function genSecretKey() {
+ return sodium.crypto_secretbox_keygen();
+}
+
+export function seal(unsigned, theirPk, ourSk) {
+ const nonce = Uint8Array.from(Array(24).fill(0));
+ return sodium.crypto_box_easy(unsigned, nonce, theirPk, ourSk);
+}
+
+function makeOnce(value) {
+ var byteArray = Array(24).fill(0);
+
+ for (var index = 0; index < byteArray.length && value > 0; index++) {
+ var byte = value & 0xff;
+ byteArray[index] = byte;
+ value = (value - byte) / 256;
+ }
+
+ return Uint8Array.from(byteArray);
+};
+
+export function encrypt(unsigned, nonce, key) {
+ return sodium.crypto_secretbox_easy(unsigned, makeOnce(nonce), key);
+}
+
+export function decrypt(signed, nonce, key) {
+ return sodium.crypto_secretbox_open_easy(signed, makeOnce(nonce), key);
+}
+
+window.setByName = (name, value) => {
+ switch (name) {
+ case 'remote_id':
+ localStorage.setItem('remote-id', value);
+ break;
+ case 'connect':
+ newConn();
+ startConn(value);
+ break;
+ case 'login':
+ value = JSON.parse(value);
+ curConn.setRemember(value.remember == 'true');
+ curConn.login(value.password);
+ break;
+ case 'close':
+ close();
+ break;
+ case 'refresh':
+ curConn.refresh();
+ break;
+ case 'reconnect':
+ curConn.reconnect();
+ break;
+ case 'toggle_option':
+ curConn.toggleOption(value);
+ break;
+ case 'image_quality':
+ curConn.setImageQuality(value);
+ break;
+ case 'lock_screen':
+ curConn.lockScreen();
+ break;
+ case 'ctrl_alt_del':
+ curConn.ctrlAltDel();
+ break;
+ case 'switch_display':
+ curConn.switchDisplay(value);
+ break;
+ case 'remove':
+ const peers = getPeers();
+ delete peers[value];
+ localStorage.setItem('peers', JSON.stringify(peers));
+ break;
+ case 'input_key':
+ value = JSON.parse(value);
+ curConn.inputKey(value.name, value.down == 'true', value.press == 'true', value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
+ break;
+ case 'input_string':
+ curConn.inputString(value);
+ break;
+ case 'send_mouse':
+ let mask = 0;
+ value = JSON.parse(value);
+ switch (value.type) {
+ case 'down':
+ mask = 1;
+ break;
+ case 'up':
+ mask = 2;
+ break;
+ case 'wheel':
+ mask = 3;
+ break;
+ }
+ switch (value.buttons) {
+ case 'left':
+ mask |= 1 << 3;
+ break;
+ case 'right':
+ mask |= 2 << 3;
+ break;
+ case 'wheel':
+ mask |= 4 << 3;
+ }
+ curConn.inputMouse(mask, parseInt(value.x || '0'), parseInt(value.y || '0'), value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
+ break;
+ case 'option':
+ value = JSON.parse(value);
+ localStorage.setItem(value.name, value.value);
+ break;
+ case 'peer_option':
+ value = JSON.parse(value);
+ curConn.setOption(value.name, value.value);
+ break;
+ case 'input_os_password':
+ curConn.inputOsPassword(value);
+ break;
+ default:
+ break;
+ }
+}
+
+window.getByName = (name, arg) => {
+ let v = _getByName(name, arg);
+ if (typeof v == 'string' || v instanceof String) return v;
+ if (v == undefined || v == null) return '';
+ return JSON.stringify(v);
+}
+
+function getPeersForDart() {
+ const peers = [];
+ for (const [id, value] of Object.entries(getPeers())) {
+ if (!id) continue;
+ const tm = value['tm'];
+ const info = value['info'];
+ if (!tm || !info) continue;
+ peers.push([tm, id, info]);
+ }
+ return peers.sort().reverse().map(x => x.slice(1));
+}
+
+function _getByName(name, arg) {
+ switch (name) {
+ case 'peers':
+ return getPeersForDart();
+ case 'remote_id':
+ return localStorage.getItem('remote-id');
+ case 'remember':
+ return curConn.getRemember();
+ case 'event':
+ if (events && events.length) {
+ const e = events[0];
+ events.splice(0, 1);
+ return JSON.stringify(e);
+ }
+ break;
+ case 'toggle_option':
+ return curConn.getOption(arg) || false;
+ case 'option':
+ return localStorage.getItem(arg);
+ case 'image_quality':
+ return curConn.getImageQuality();
+ case 'translate':
+ arg = JSON.parse(arg);
+ return translate(arg.locale, arg.text);
+ case 'peer_option':
+ return curConn.getOption(arg);
+ case 'test_if_valid_server':
+ break;
+ case 'version':
+ return version;
+ }
+ return '';
+}
+
+let opusWorker = new Worker("./libopus.js");
+let pcmPlayer;
+
+export function initAudio(channels, sampleRate) {
+ pcmPlayer = newAudioPlayer(channels, sampleRate);
+ opusWorker.postMessage({ channels, sampleRate });
+}
+
+export function playAudio(packet) {
+ opusWorker.postMessage(packet, [packet.buffer]);
+}
+
+window.init = async () => {
+ if (yuvWorker) {
+ yuvWorker.onmessage = (e) => {
+ currentFrame = e.data;
+ }
+ }
+ opusWorker.onmessage = (e) => {
+ pcmPlayer.feed(e.data);
+ }
+ loadVp9(() => { });
+ await initZstd();
+ console.log('init done');
+}
+
+export function getPeers() {
+ try {
+ return JSON.parse(localStorage.getItem('peers')) || {};
+ } catch (e) {
+ return {};
+ }
+}
+
+function newAudioPlayer(channels, sampleRate) {
+ return new PCMPlayer({
+ channels,
+ sampleRate,
+ flushingTime: 2000
+ });
+}
+
+export function copyToClipboard(text) {
+ if (window.clipboardData && window.clipboardData.setData) {
+ // Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
+ return window.clipboardData.setData("Text", text);
+
+ }
+ else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
+ var textarea = document.createElement("textarea");
+ textarea.textContent = text;
+ textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge.
+ document.body.appendChild(textarea);
+ textarea.select();
+ try {
+ return document.execCommand("copy"); // Security exception may be thrown by some browsers.
+ }
+ catch (ex) {
+ console.warn("Copy to clipboard failed.", ex);
+ // return prompt("Copy to clipboard: Ctrl+C, Enter", text);
+ }
+ finally {
+ document.body.removeChild(textarea);
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/js/src/main.ts b/web/js/src/main.ts
new file mode 100644
index 000000000..2be877f58
--- /dev/null
+++ b/web/js/src/main.ts
@@ -0,0 +1,2 @@
+import "./globals";
+import "./ui";
\ No newline at end of file
diff --git a/web/js/src/style.css b/web/js/src/style.css
new file mode 100644
index 000000000..852de7aa2
--- /dev/null
+++ b/web/js/src/style.css
@@ -0,0 +1,8 @@
+#app {
+ font-family: Avenir, Helvetica, Arial, sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-align: center;
+ color: #2c3e50;
+ margin-top: 60px;
+}
diff --git a/web/js/src/ui.js b/web/js/src/ui.js
new file mode 100644
index 000000000..446334022
--- /dev/null
+++ b/web/js/src/ui.js
@@ -0,0 +1,108 @@
+import "./style.css";
+import "./connection";
+import * as globals from "./globals";
+
+const app = document.querySelector('#app');
+
+if (app) {
+ app.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+`;
+
+ let player;
+ window.init();
+
+ document.body.onload = () => {
+ const host = document.querySelector('#host');
+ host.value = localStorage.getItem('custom-rendezvous-server');
+ const id = document.querySelector('#id');
+ id.value = localStorage.getItem('id');
+ const key = document.querySelector('#key');
+ key.value = localStorage.getItem('key');
+ player = YUVCanvas.attach(document.getElementById('player'));
+ // globals.sendOffCanvas(document.getElementById('player'));
+ };
+
+ window.connect = () => {
+ const host = document.querySelector('#host');
+ localStorage.setItem('custom-rendezvous-server', host.value);
+ const id = document.querySelector('#id');
+ localStorage.setItem('id', id.value);
+ const key = document.querySelector('#key');
+ localStorage.setItem('key', key.value);
+ const func = async () => {
+ const conn = globals.newConn();
+ conn.setMsgbox(msgbox);
+ conn.setDraw((f) => {
+ /*
+ if (!(document.getElementById('player').width > 0)) {
+ document.getElementById('player').width = f.format.displayWidth;
+ document.getElementById('player').height = f.format.displayHeight;
+ }
+ */
+ globals.draw(f);
+ player.drawFrame(f);
+ });
+ document.querySelector('div#status').style.display = 'block';
+ document.querySelector('div#connect').style.display = 'none';
+ document.querySelector('div#text').innerHTML = 'Connecting ...';
+ await conn.start(id.value);
+ };
+ func();
+ }
+
+ function msgbox(type, title, text) {
+ if (!globals.getConn()) return;
+ if (type == 'input-password') {
+ document.querySelector('div#status').style.display = 'none';
+ document.querySelector('div#password').style.display = 'block';
+ } else if (!type) {
+ document.querySelector('div#canvas').style.display = 'block';
+ document.querySelector('div#password').style.display = 'none';
+ document.querySelector('div#status').style.display = 'none';
+ } else if (type == 'error') {
+ document.querySelector('div#status').style.display = 'block';
+ document.querySelector('div#canvas').style.display = 'none';
+ document.querySelector('div#text').innerHTML = '' + text + '
';
+ } else {
+ document.querySelector('div#password').style.display = 'none';
+ document.querySelector('div#status').style.display = 'block';
+ document.querySelector('div#text').innerHTML = '' + text + '
';
+ }
+ }
+
+ window.cancel = () => {
+ globals.close();
+ document.querySelector('div#connect').style.display = 'block';
+ document.querySelector('div#password').style.display = 'none';
+ document.querySelector('div#status').style.display = 'none';
+ document.querySelector('div#canvas').style.display = 'none';
+ }
+
+ window.confirm = () => {
+ const password = document.querySelector('input#password').value;
+ if (password) {
+ document.querySelector('div#password').style.display = 'none';
+ globals.getConn().login(password);
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/js/src/vite-env.d.ts b/web/js/src/vite-env.d.ts
new file mode 100644
index 000000000..151aa6856
--- /dev/null
+++ b/web/js/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
\ No newline at end of file
diff --git a/web/js/src/websock.ts b/web/js/src/websock.ts
new file mode 100644
index 000000000..6f05e6f6b
--- /dev/null
+++ b/web/js/src/websock.ts
@@ -0,0 +1,183 @@
+import * as message from "./message.js";
+import * as rendezvous from "./rendezvous.js";
+import * as globals from "./globals";
+
+type Keys = "message" | "open" | "close" | "error";
+
+export default class Websock {
+ _websocket: WebSocket;
+ _eventHandlers: { [key in Keys]: Function };
+ _buf: (rendezvous.RendezvousMessage | message.Message)[];
+ _status: any;
+ _latency: number;
+ _secretKey: [Uint8Array, number, number] | undefined;
+ _uri: string;
+ _isRendezvous: boolean;
+
+ constructor(uri: string, isRendezvous: boolean = true) {
+ this._eventHandlers = {
+ message: (_: any) => {},
+ open: () => {},
+ close: () => {},
+ error: () => {},
+ };
+ this._uri = uri;
+ this._status = "";
+ this._buf = [];
+ this._websocket = new WebSocket(uri);
+ this._websocket.onmessage = this._recv_message.bind(this);
+ this._websocket.binaryType = "arraybuffer";
+ this._latency = new Date().getTime();
+ this._isRendezvous = isRendezvous;
+ }
+
+ latency(): number {
+ return this._latency;
+ }
+
+ setSecretKey(key: Uint8Array) {
+ this._secretKey = [key, 0, 0];
+ }
+
+ sendMessage(json: message.DeepPartial) {
+ let data = message.Message.encode(
+ message.Message.fromPartial(json)
+ ).finish();
+ let k = this._secretKey;
+ if (k) {
+ k[1] += 1;
+ data = globals.encrypt(data, k[1], k[0]);
+ }
+ this._websocket.send(data);
+ }
+
+ sendRendezvous(data: rendezvous.DeepPartial) {
+ this._websocket.send(
+ rendezvous.RendezvousMessage.encode(
+ rendezvous.RendezvousMessage.fromPartial(data)
+ ).finish()
+ );
+ }
+
+ parseMessage(data: Uint8Array) {
+ return message.Message.decode(data);
+ }
+
+ parseRendezvous(data: Uint8Array) {
+ return rendezvous.RendezvousMessage.decode(data);
+ }
+
+ // Event Handlers
+ off(evt: Keys) {
+ this._eventHandlers[evt] = () => {};
+ }
+
+ on(evt: Keys, handler: Function) {
+ this._eventHandlers[evt] = handler;
+ }
+
+ async open(timeout: number = 12000): Promise {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ if (this._status != "open") {
+ reject(this._status || "Timeout");
+ }
+ }, timeout);
+ this._websocket.onopen = () => {
+ this._latency = new Date().getTime() - this._latency;
+ this._status = "open";
+ console.debug(">> WebSock.onopen");
+ if (this._websocket?.protocol) {
+ console.info(
+ "Server choose sub-protocol: " + this._websocket.protocol
+ );
+ }
+
+ this._eventHandlers.open();
+ console.info("WebSock.onopen");
+ resolve(this);
+ };
+ this._websocket.onclose = (e) => {
+ if (this._status == "open") {
+ // e.code 1000 means that the connection was closed normally.
+ //
+ }
+ this._status = e;
+ console.error("WebSock.onclose: ");
+ console.error(e);
+ this._eventHandlers.close(e);
+ reject("Reset by the peer");
+ };
+ this._websocket.onerror = (e: any) => {
+ if (!this._status) {
+ reject("Failed to connect to " + (this._isRendezvous ? "rendezvous" : "relay") + " server");
+ return;
+ }
+ this._status = e;
+ console.error("WebSock.onerror: ")
+ console.error(e);
+ this._eventHandlers.error(e);
+ };
+ });
+ }
+
+ async next(
+ timeout = 12000
+ ): Promise {
+ const func = (
+ resolve: (value: rendezvous.RendezvousMessage | message.Message) => void,
+ reject: (reason: any) => void,
+ tm0: number
+ ) => {
+ if (this._buf.length) {
+ resolve(this._buf[0]);
+ this._buf.splice(0, 1);
+ } else {
+ if (this._status != "open") {
+ reject(this._status);
+ return;
+ }
+ if (new Date().getTime() > tm0 + timeout) {
+ reject("Timeout");
+ } else {
+ setTimeout(() => func(resolve, reject, tm0), 1);
+ }
+ }
+ };
+ return new Promise((resolve, reject) => {
+ func(resolve, reject, new Date().getTime());
+ });
+ }
+
+ close() {
+ this._status = "";
+ if (this._websocket) {
+ if (
+ this._websocket.readyState === WebSocket.OPEN ||
+ this._websocket.readyState === WebSocket.CONNECTING
+ ) {
+ console.info("Closing WebSocket connection");
+ this._websocket.close();
+ }
+
+ this._websocket.onmessage = () => {};
+ }
+ }
+
+ _recv_message(e: any) {
+ if (e.data instanceof window.ArrayBuffer) {
+ let bytes = new Uint8Array(e.data);
+ const k = this._secretKey;
+ if (k) {
+ k[2] += 1;
+ bytes = globals.decrypt(bytes, k[2], k[0]);
+ }
+ this._buf.push(
+ this._isRendezvous
+ ? this.parseRendezvous(bytes)
+ : this.parseMessage(bytes)
+ );
+ }
+ this._eventHandlers.message(e.data);
+ }
+}
diff --git a/web/js/ts_proto.py b/web/js/ts_proto.py
new file mode 100755
index 000000000..f917c6b37
--- /dev/null
+++ b/web/js/ts_proto.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+
+import os
+
+path = os.path.abspath(os.path.join(os.getcwd(), '..', 'hbb', 'libs', 'hbb_common', 'protos'))
+
+if os.name == 'nt':
+ cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=protoc-gen-ts_proto=.\node_modules\.bin\protoc-gen-ts_proto.cmd -I "%s" --ts_proto_out=./src/ rendezvous.proto'%path
+ print(cmd)
+ os.system(cmd)
+ cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=protoc-gen-ts_proto=.\node_modules\.bin\protoc-gen-ts_proto.cmd -I "%s" --ts_proto_out=./src/ message.proto'%path
+ print(cmd)
+ os.system(cmd)
+else:
+ cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=./node_modules/.bin/protoc-gen-ts_proto -I "%s" --ts_proto_out=./src/ rendezvous.proto'%path
+ print(cmd)
+ os.system(cmd)
+ cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=./node_modules/.bin/protoc-gen-ts_proto -I "%s" --ts_proto_out=./src/ message.proto'%path
+ print(cmd)
+ os.system(cmd)
diff --git a/web/js/tsconfig.json b/web/js/tsconfig.json
new file mode 100644
index 000000000..ca949de6a
--- /dev/null
+++ b/web/js/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "allowJs": true,
+ "lib": [
+ "ESNext",
+ "DOM"
+ ],
+ "moduleResolution": "Node",
+ "strict": true,
+ "sourceMap": true,
+ "resolveJsonModule": true,
+ "esModuleInterop": true,
+ "noEmit": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true
+ },
+ "include": [
+ "./src"
+ ]
+}
\ No newline at end of file
diff --git a/web/js/yarn.lock b/web/js/yarn.lock
new file mode 100644
index 000000000..cf4caafe5
--- /dev/null
+++ b/web/js/yarn.lock
@@ -0,0 +1,374 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
+ integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78=
+
+"@protobufjs/base64@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
+ integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
+
+"@protobufjs/codegen@^2.0.4":
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
+ integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
+
+"@protobufjs/eventemitter@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
+ integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A=
+
+"@protobufjs/fetch@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
+ integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=
+ dependencies:
+ "@protobufjs/aspromise" "^1.1.1"
+ "@protobufjs/inquire" "^1.1.0"
+
+"@protobufjs/float@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
+ integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=
+
+"@protobufjs/inquire@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
+ integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=
+
+"@protobufjs/path@^1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
+ integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=
+
+"@protobufjs/pool@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
+ integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=
+
+"@protobufjs/utf8@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
+ integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
+
+"@types/long@^4.0.1":
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
+ integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
+
+"@types/node@>=13.7.0":
+ version "17.0.8"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.8.tgz#50d680c8a8a78fe30abe6906453b21ad8ab0ad7b"
+ integrity sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg==
+
+"@types/object-hash@^1.3.0":
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-1.3.4.tgz#079ba142be65833293673254831b5e3e847fe58b"
+ integrity sha512-xFdpkAkikBgqBdG9vIlsqffDV8GpvnPEzs0IUtr1v3BEB97ijsFQ4RXVbUZwjFThhB4MDSTUfvmxUD5PGx0wXA==
+
+dataloader@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8"
+ integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==
+
+esbuild-android-arm64@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.13.15.tgz#3fc3ff0bab76fe35dd237476b5d2b32bb20a3d44"
+ integrity sha512-m602nft/XXeO8YQPUDVoHfjyRVPdPgjyyXOxZ44MK/agewFFkPa8tUo6lAzSWh5Ui5PB4KR9UIFTSBKh/RrCmg==
+
+esbuild-darwin-64@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.13.15.tgz#8e9169c16baf444eacec60d09b24d11b255a8e72"
+ integrity sha512-ihOQRGs2yyp7t5bArCwnvn2Atr6X4axqPpEdCFPVp7iUj4cVSdisgvEKdNR7yH3JDjW6aQDw40iQFoTqejqxvQ==
+
+esbuild-darwin-arm64@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.15.tgz#1b07f893b632114f805e188ddfca41b2b778229a"
+ integrity sha512-i1FZssTVxUqNlJ6cBTj5YQj4imWy3m49RZRnHhLpefFIh0To05ow9DTrXROTE1urGTQCloFUXTX8QfGJy1P8dQ==
+
+esbuild-freebsd-64@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.15.tgz#0b8b7eca1690c8ec94c75680c38c07269c1f4a85"
+ integrity sha512-G3dLBXUI6lC6Z09/x+WtXBXbOYQZ0E8TDBqvn7aMaOCzryJs8LyVXKY4CPnHFXZAbSwkCbqiPuSQ1+HhrNk7EA==
+
+esbuild-freebsd-arm64@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.15.tgz#2e1a6c696bfdcd20a99578b76350b41db1934e52"
+ integrity sha512-KJx0fzEDf1uhNOZQStV4ujg30WlnwqUASaGSFPhznLM/bbheu9HhqZ6mJJZM32lkyfGJikw0jg7v3S0oAvtvQQ==
+
+esbuild-linux-32@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.13.15.tgz#6fd39f36fc66dd45b6b5f515728c7bbebc342a69"
+ integrity sha512-ZvTBPk0YWCLMCXiFmD5EUtB30zIPvC5Itxz0mdTu/xZBbbHJftQgLWY49wEPSn2T/TxahYCRDWun5smRa0Tu+g==
+
+esbuild-linux-64@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.13.15.tgz#9cb8e4bcd7574e67946e4ee5f1f1e12386bb6dd3"
+ integrity sha512-eCKzkNSLywNeQTRBxJRQ0jxRCl2YWdMB3+PkWFo2BBQYC5mISLIVIjThNtn6HUNqua1pnvgP5xX0nHbZbPj5oA==
+
+esbuild-linux-arm64@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.15.tgz#3891aa3704ec579a1b92d2a586122e5b6a2bfba1"
+ integrity sha512-bYpuUlN6qYU9slzr/ltyLTR9YTBS7qUDymO8SV7kjeNext61OdmqFAzuVZom+OLW1HPHseBfJ/JfdSlx8oTUoA==
+
+esbuild-linux-arm@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.13.15.tgz#8a00e99e6a0c6c9a6b7f334841364d8a2b4aecfe"
+ integrity sha512-wUHttDi/ol0tD8ZgUMDH8Ef7IbDX+/UsWJOXaAyTdkT7Yy9ZBqPg8bgB/Dn3CZ9SBpNieozrPRHm0BGww7W/jA==
+
+esbuild-linux-mips64le@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.15.tgz#36b07cc47c3d21e48db3bb1f4d9ef8f46aead4f7"
+ integrity sha512-KlVjIG828uFPyJkO/8gKwy9RbXhCEUeFsCGOJBepUlpa7G8/SeZgncUEz/tOOUJTcWMTmFMtdd3GElGyAtbSWg==
+
+esbuild-linux-ppc64le@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.15.tgz#f7e6bba40b9a11eb9dcae5b01550ea04670edad2"
+ integrity sha512-h6gYF+OsaqEuBjeesTBtUPw0bmiDu7eAeuc2OEH9S6mV9/jPhPdhOWzdeshb0BskRZxPhxPOjqZ+/OqLcxQwEQ==
+
+esbuild-netbsd-64@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.15.tgz#a2fedc549c2b629d580a732d840712b08d440038"
+ integrity sha512-3+yE9emwoevLMyvu+iR3rsa+Xwhie7ZEHMGDQ6dkqP/ndFzRHkobHUKTe+NCApSqG5ce2z4rFu+NX/UHnxlh3w==
+
+esbuild-openbsd-64@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.15.tgz#b22c0e5806d3a1fbf0325872037f885306b05cd7"
+ integrity sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g==
+
+esbuild-sunos-64@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.13.15.tgz#d0b6454a88375ee8d3964daeff55c85c91c7cef4"
+ integrity sha512-lbivT9Bx3t1iWWrSnGyBP9ODriEvWDRiweAs69vI+miJoeKwHWOComSRukttbuzjZ8r1q0mQJ8Z7yUsDJ3hKdw==
+
+esbuild-windows-32@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.13.15.tgz#c96d0b9bbb52f3303322582ef8e4847c5ad375a7"
+ integrity sha512-fDMEf2g3SsJ599MBr50cY5ve5lP1wyVwTe6aLJsM01KtxyKkB4UT+fc5MXQFn3RLrAIAZOG+tHC+yXObpSn7Nw==
+
+esbuild-windows-64@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.13.15.tgz#1f79cb9b1e1bb02fb25cd414cb90d4ea2892c294"
+ integrity sha512-9aMsPRGDWCd3bGjUIKG/ZOJPKsiztlxl/Q3C1XDswO6eNX/Jtwu4M+jb6YDH9hRSUflQWX0XKAfWzgy5Wk54JQ==
+
+esbuild-windows-arm64@0.13.15:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.15.tgz#482173070810df22a752c686509c370c3be3b3c3"
+ integrity sha512-zzvyCVVpbwQQATaf3IG8mu1IwGEiDxKkYUdA4FpoCHi1KtPa13jeScYDjlW0Qh+ebWzpKfR2ZwvqAQkSWNcKjA==
+
+esbuild@^0.13.12:
+ version "0.13.15"
+ resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.13.15.tgz#db56a88166ee373f87dbb2d8798ff449e0450cdf"
+ integrity sha512-raCxt02HBKv8RJxE8vkTSCXGIyKHdEdGfUmiYb8wnabnaEmHzyW7DCHb5tEN0xU8ryqg5xw54mcwnYkC4x3AIw==
+ optionalDependencies:
+ esbuild-android-arm64 "0.13.15"
+ esbuild-darwin-64 "0.13.15"
+ esbuild-darwin-arm64 "0.13.15"
+ esbuild-freebsd-64 "0.13.15"
+ esbuild-freebsd-arm64 "0.13.15"
+ esbuild-linux-32 "0.13.15"
+ esbuild-linux-64 "0.13.15"
+ esbuild-linux-arm "0.13.15"
+ esbuild-linux-arm64 "0.13.15"
+ esbuild-linux-mips64le "0.13.15"
+ esbuild-linux-ppc64le "0.13.15"
+ esbuild-netbsd-64 "0.13.15"
+ esbuild-openbsd-64 "0.13.15"
+ esbuild-sunos-64 "0.13.15"
+ esbuild-windows-32 "0.13.15"
+ esbuild-windows-64 "0.13.15"
+ esbuild-windows-arm64 "0.13.15"
+
+fast-sha256@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/fast-sha256/-/fast-sha256-1.3.0.tgz#7916ba2054eeb255982608cccd0f6660c79b7ae6"
+ integrity sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==
+
+fsevents@~2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+ integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+ integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+has@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+ integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+ dependencies:
+ function-bind "^1.1.1"
+
+is-core-module@^2.8.0:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211"
+ integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==
+ dependencies:
+ has "^1.0.3"
+
+libsodium-wrappers@^0.7.9:
+ version "0.7.9"
+ resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz#4ffc2b69b8f7c7c7c5594a93a4803f80f6d0f346"
+ integrity sha512-9HaAeBGk1nKTRFRHkt7nzxqCvnkWTjn1pdjKgcUnZxj0FyOP4CnhgFhMdrFfgNsukijBGyBLpP2m2uKT1vuWhQ==
+ dependencies:
+ libsodium "^0.7.0"
+
+libsodium@^0.7.0, libsodium@^0.7.9:
+ version "0.7.9"
+ resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.7.9.tgz#4bb7bcbf662ddd920d8795c227ae25bbbfa3821b"
+ integrity sha512-gfeADtR4D/CM0oRUviKBViMGXZDgnFdMKMzHsvBdqLBHd9ySi6EtYnmuhHVDDYgYpAO8eU8hEY+F8vIUAPh08A==
+
+lodash@^4.17.15:
+ version "4.17.21"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+
+long@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
+ integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
+
+nanoid@^3.1.30:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c"
+ integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==
+
+object-hash@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df"
+ integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA==
+
+path-parse@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+pcm-player@^0.0.11:
+ version "0.0.11"
+ resolved "https://registry.yarnpkg.com/pcm-player/-/pcm-player-0.0.11.tgz#1dd0e37d8e1238c10b153079939b1fc88958a9ff"
+ integrity sha512-+FmX62jiqZa7wDCqSRQ1g3DuU6JNgpymgOLCWhmiE/Lj/M+rOUNqgNwVQX509LdA9dtBtVD3EQQUSp9JqU6upw==
+
+picocolors@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
+ integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
+
+postcss@^8.4.5:
+ version "8.4.5"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95"
+ integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==
+ dependencies:
+ nanoid "^3.1.30"
+ picocolors "^1.0.0"
+ source-map-js "^1.0.1"
+
+prettier@^2.5.1:
+ version "2.5.1"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
+ integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
+
+protobufjs@^6.8.8:
+ version "6.11.2"
+ resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b"
+ integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==
+ dependencies:
+ "@protobufjs/aspromise" "^1.1.2"
+ "@protobufjs/base64" "^1.1.2"
+ "@protobufjs/codegen" "^2.0.4"
+ "@protobufjs/eventemitter" "^1.1.0"
+ "@protobufjs/fetch" "^1.1.0"
+ "@protobufjs/float" "^1.0.2"
+ "@protobufjs/inquire" "^1.1.0"
+ "@protobufjs/path" "^1.1.2"
+ "@protobufjs/pool" "^1.1.0"
+ "@protobufjs/utf8" "^1.1.0"
+ "@types/long" "^4.0.1"
+ "@types/node" ">=13.7.0"
+ long "^4.0.0"
+
+resolve@^1.20.0:
+ version "1.21.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.21.0.tgz#b51adc97f3472e6a5cf4444d34bc9d6b9037591f"
+ integrity sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==
+ dependencies:
+ is-core-module "^2.8.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
+rollup@^2.59.0:
+ version "2.64.0"
+ resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.64.0.tgz#f0f59774e21fbb56de438a37d06a2189632b207a"
+ integrity sha512-+c+lbw1lexBKSMb1yxGDVfJ+vchJH3qLbmavR+awDinTDA2C5Ug9u7lkOzj62SCu0PKUExsW36tpgW7Fmpn3yQ==
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+source-map-js@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf"
+ integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==
+
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+ts-poet@^4.5.0:
+ version "4.10.0"
+ resolved "https://registry.yarnpkg.com/ts-poet/-/ts-poet-4.10.0.tgz#8732374655e87f8f833e5d110938e346713e8c66"
+ integrity sha512-V5xzt+LDMVtxWvK12WVwHhGHTA//CeoPdWOqka0mMjlRqq7RPKYSfWEnzJdMmhNbd34BwZuZpip4mm+nqEcbQA==
+ dependencies:
+ lodash "^4.17.15"
+ prettier "^2.5.1"
+
+ts-proto-descriptors@^1.2.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/ts-proto-descriptors/-/ts-proto-descriptors-1.3.1.tgz#760ebaaa19475b03662f7b358ffea45b9c5348f5"
+ integrity sha512-Cybb3fqceMwA6JzHdC32dIo8eVGVmXrM6TWhdk1XQVVHT/6OQqk0ioyX1dIdu3rCIBhRmWUhUE4HsyK+olmgMw==
+ dependencies:
+ long "^4.0.0"
+ protobufjs "^6.8.8"
+
+ts-proto@^1.101.0:
+ version "1.101.0"
+ resolved "https://registry.yarnpkg.com/ts-proto/-/ts-proto-1.101.0.tgz#f8ce4523a0cb32ff224ff8a5759c2c046bf96244"
+ integrity sha512-XUV0WKQ3icHMToOOpUjf0RCKF9md+Lu9TV00LQlZ6ailJhbBqLh0BXQJ1PFUGxEW8YV65Wc/N8vAir152OE2Sg==
+ dependencies:
+ "@types/object-hash" "^1.3.0"
+ dataloader "^1.4.0"
+ object-hash "^1.3.1"
+ protobufjs "^6.8.8"
+ ts-poet "^4.5.0"
+ ts-proto-descriptors "^1.2.1"
+
+typescript@^4.4.4:
+ version "4.5.4"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"
+ integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==
+
+vite@^2.7.2:
+ version "2.7.12"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-2.7.12.tgz#7784ab19e7ff98f6a192d2d7d877480a8c2b7e7d"
+ integrity sha512-KvPYToRQWhRfBeVkyhkZ5hASuHQkqZUUdUcE3xyYtq5oYEPIJ0h9LWiWTO6v990glmSac2cEPeYeXzpX5Z6qKQ==
+ dependencies:
+ esbuild "^0.13.12"
+ postcss "^8.4.5"
+ resolve "^1.20.0"
+ rollup "^2.59.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+wasm-feature-detect@^1.2.11:
+ version "1.2.11"
+ resolved "https://registry.yarnpkg.com/wasm-feature-detect/-/wasm-feature-detect-1.2.11.tgz#e21992fd1f1d41a47490e392a5893cb39d81e29e"
+ integrity sha512-HUqwaodrQGaZgz1lZaNioIkog9tkeEJjrM3eq4aUL04whXOVDRc/o2EGb/8kV0QX411iAYWEqq7fMBmJ6dKS6w==
+
+zstddec@^0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/zstddec/-/zstddec-0.0.2.tgz#57e2f28dd1ff56b750e07d158a43f0611ad9eeb4"
+ integrity sha512-DCo0oxvcvOTGP/f5FA6tz2Z6wF+FIcEApSTu0zV5sQgn9hoT5lZ9YRAKUraxt9oP7l4e8TnNdi8IZTCX6WCkwA==