split web js to v1 and v2
This commit is contained in:
@@ -0,0 +1 @@
|
||||
v1 is not compatible with current Flutter source code.
|
||||
@@ -0,0 +1,183 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
||||
The path provided below has to start and end with a slash "/" in order for
|
||||
it to work correctly.
|
||||
|
||||
For more details:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
|
||||
|
||||
This is a placeholder for base href that will be replaced by the value of
|
||||
the `--base-href` argument provided to `flutter build`.
|
||||
-->
|
||||
<base href="$FLUTTER_BASE_HREF">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="Remote Desktop.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="RustDesk">
|
||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
|
||||
<title>RustDesk</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<script src="ogvjs-1.8.6/ogv.js"></script>
|
||||
<script type="module" crossorigin src="js/dist/index.js"></script>
|
||||
<link rel="modulepreload" href="js/dist/vendor.js">
|
||||
<script src="yuv-canvas-1.2.6.js"></script>
|
||||
<style>
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: 16px solid #f3f3f3;
|
||||
border-radius: 50%;
|
||||
border: 15px solid;
|
||||
border-top: 16px solid #024eff;
|
||||
border-right: 16px solid white;
|
||||
border-bottom: 16px solid #024eff;
|
||||
border-left: 16px solid white;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
-webkit-animation: spin 2s linear infinite;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="loading">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
<!-- This script installs service_worker.js to provide PWA functionality to
|
||||
application. For more information, see:
|
||||
https://developers.google.com/web/fundamentals/primers/service-workers -->
|
||||
<script>
|
||||
var serviceWorkerVersion = null;
|
||||
var scriptLoaded = false;
|
||||
function loadMainDartJs() {
|
||||
if (scriptLoaded) {
|
||||
return;
|
||||
}
|
||||
scriptLoaded = true;
|
||||
var scriptTag = document.createElement('script');
|
||||
scriptTag.src = 'main.dart.js';
|
||||
scriptTag.type = 'application/javascript';
|
||||
document.body.append(scriptTag);
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Service workers are supported. Use them.
|
||||
window.addEventListener('load', function () {
|
||||
// Wait for registration to finish before dropping the <script> tag.
|
||||
// Otherwise, the browser will load the script multiple times,
|
||||
// potentially different versions.
|
||||
var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
|
||||
navigator.serviceWorker.register(serviceWorkerUrl)
|
||||
.then((reg) => {
|
||||
function waitForActivation(serviceWorker) {
|
||||
serviceWorker.addEventListener('statechange', () => {
|
||||
if (serviceWorker.state == 'activated') {
|
||||
console.log('Installed new service worker.');
|
||||
loadMainDartJs();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!reg.active && (reg.installing || reg.waiting)) {
|
||||
// No active web worker and we have installed or are installing
|
||||
// one for the first time. Simply wait for it to activate.
|
||||
waitForActivation(reg.installing || reg.waiting);
|
||||
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
|
||||
// When the app updates the serviceWorkerVersion changes, so we
|
||||
// need to ask the service worker to update.
|
||||
console.log('New service worker available.');
|
||||
reg.update();
|
||||
waitForActivation(reg.installing);
|
||||
} else {
|
||||
// Existing service worker is still good.
|
||||
console.log('Loading app from service worker.');
|
||||
loadMainDartJs();
|
||||
}
|
||||
});
|
||||
|
||||
// If service worker doesn't succeed in a reasonable amount of time,
|
||||
// fallback to plaint <script> tag.
|
||||
setTimeout(() => {
|
||||
if (!scriptLoaded) {
|
||||
console.warn(
|
||||
'Failed to load app from service worker. Falling back to plain <script> tag.',
|
||||
);
|
||||
loadMainDartJs();
|
||||
}
|
||||
}, 4000);
|
||||
});
|
||||
} else {
|
||||
// Service workers not supported. Just drop the <script> tag.
|
||||
loadMainDartJs();
|
||||
}
|
||||
</script>
|
||||
<script src="libs/firebase-app.js?8.10.1"></script>
|
||||
<script src="libs/firebase-analytics.js?8.10.1"></script>
|
||||
|
||||
<script>
|
||||
// Your web app's Firebase configuration
|
||||
// For Firebase JS SDK v7.20.0 and later, measurementId is optional
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyCgehIZk1aFP0E7wZtYRRqrfvNiNAF39-A",
|
||||
authDomain: "rustdesk.firebaseapp.com",
|
||||
databaseURL: "https://rustdesk.firebaseio.com",
|
||||
projectId: "rustdesk",
|
||||
storageBucket: "rustdesk.appspot.com",
|
||||
messagingSenderId: "768133699366",
|
||||
appId: "1:768133699366:web:d50faf0792cb208d7993e7",
|
||||
measurementId: "G-9PEH85N6ZQ"
|
||||
};
|
||||
|
||||
// Initialize Firebase
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
firebase.analytics();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
* text=auto
|
||||
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
*log
|
||||
ogvjs
|
||||
.vscode
|
||||
.yarn
|
||||
@@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import re
|
||||
import os
|
||||
import glob
|
||||
from tabnanny import check
|
||||
|
||||
def pad_start(s, n, c = ' '):
|
||||
if len(s) >= n:
|
||||
return s
|
||||
return c * (n - len(s)) + s
|
||||
|
||||
def safe_unicode(s):
|
||||
res = ""
|
||||
for c in s:
|
||||
res += r"\u{}".format(pad_start(hex(ord(c))[2:], 4, '0'))
|
||||
return res
|
||||
|
||||
def main():
|
||||
print('export const LANGS = {')
|
||||
for fn in glob.glob('../../../src/lang/*'):
|
||||
lang = os.path.basename(fn)[:-3]
|
||||
if lang == 'template': continue
|
||||
print(' %s: {'%lang)
|
||||
for ln in open(fn, encoding='utf-8'):
|
||||
ln = ln.strip()
|
||||
if ln.startswith('("'):
|
||||
toks = ln.split('", "')
|
||||
assert(len(toks) == 2)
|
||||
a = toks[0][2:]
|
||||
b = toks[1][:-3]
|
||||
print(' "%s": "%s",'%(safe_unicode(a), safe_unicode(b)))
|
||||
print(' },')
|
||||
print('}')
|
||||
check_if_retry = ['', False]
|
||||
KEY_MAP = ['', False]
|
||||
for ln in open('../../../src/client.rs', encoding='utf-8'):
|
||||
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, retry_for_relay: boolean) {')
|
||||
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('../../../Cargo.toml', encoding='utf-8'):
|
||||
if ln.startswith('version ='):
|
||||
print('export const ' + ln)
|
||||
|
||||
|
||||
def removeComment(ln):
|
||||
return re.sub('\s+\/\/.*$', '', ln)
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg?v2" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script src="ogvjs-1.8.6/ogv.js"></script>
|
||||
<script src="./yuv-canvas-1.2.6.js"></script>
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
@@ -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() + ": Connecting to rendezvous server: " + uri + ", for " + id
|
||||
);
|
||||
await ws.open();
|
||||
console.log(new Date() + ": Connected to rendezvous 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();
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
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";
|
||||
|
||||
window.curConn = undefined;
|
||||
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 (!type || (type == 'error' && !text)) return;
|
||||
const text2 = text.toLowerCase();
|
||||
var hasRetry = checkIfRetry(type, title, text) ? 'true' : '';
|
||||
onGlobalEvent(JSON.stringify({ 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) {
|
||||
payload = jsonfyForDart(payload);
|
||||
payload.name = name;
|
||||
onGlobalEvent(JSON.stringify(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);
|
||||
}
|
||||
onRgba(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) {
|
||||
setByName('remote_id', id);
|
||||
await curConn.start(id);
|
||||
}
|
||||
|
||||
export function close() {
|
||||
getConn()?.close();
|
||||
setConn(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 '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) => {
|
||||
onRgba(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import "./globals";
|
||||
import "./ui";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import "./style.css";
|
||||
import "./connection";
|
||||
import * as globals from "./globals";
|
||||
|
||||
const app = document.querySelector('#app');
|
||||
|
||||
if (app) {
|
||||
app.innerHTML = `
|
||||
<div id="connect" style="text-align: center"><table style="display: inline-block">
|
||||
<tr><td><span>Host: </span></td><td><input id="host" /></td></tr>
|
||||
<tr><td><span>Key: </span></td><td><input id="key" /></td></tr>
|
||||
<tr><td><span>Id: </span></td><td><input id="id" /></td></tr>
|
||||
<tr><td></td><td><button onclick="connect();">Connect</button></td></tr>
|
||||
</table></div>
|
||||
<div id="password" style="display: none;">
|
||||
<input type="password" id="password" />
|
||||
<button id="confirm" onclick="confirm()">Confirm</button>
|
||||
<button id="cancel" onclick="cancel();">Cancel</button>
|
||||
</div>
|
||||
<div id="status" style="display: none;">
|
||||
<div id="text" style="line-height: 2em"></div>
|
||||
<button id="cancel" onclick="cancel();">Cancel</button>
|
||||
</div>
|
||||
<div id="canvas" style="display: none;">
|
||||
<button id="cancel" onclick="cancel();">Cancel</button>
|
||||
<canvas id="player"></canvas>
|
||||
<canvas id="test-yuv-decoder-canvas"></canvas>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = '<div style="color: red; font-weight: bold;">' + text + '</div>';
|
||||
} else {
|
||||
document.querySelector('div#password').style.display = 'none';
|
||||
document.querySelector('div#status').style.display = 'block';
|
||||
document.querySelector('div#text').innerHTML = '<div style="font-weight: bold;">' + text + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -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<message.Message>) {
|
||||
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<rendezvous.RendezvousMessage>) {
|
||||
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<Websock> {
|
||||
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<rendezvous.RendezvousMessage | message.Message> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
|
||||
path = os.path.abspath(os.path.join(os.getcwd(), '..', '..', '..', '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)
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
manifest: false,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: `[name].js`,
|
||||
chunkFileNames: `[name].js`,
|
||||
assetFileNames: `[name].[ext]`,
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "rustdesk",
|
||||
"short_name": "rustdesk",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "Remote Desktop.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
var wasmExports;
|
||||
|
||||
fetch('yuv.wasm').then(function (res) { return res.arrayBuffer(); })
|
||||
.then(function (file) { return WebAssembly.instantiate(file); })
|
||||
.then(function (wasm) {
|
||||
wasmExports = wasm.instance.exports;
|
||||
console.log('yuv ready');
|
||||
});
|
||||
|
||||
var yPtr, yPtrLen, uPtr, uPtrLen, vPtr, vPtrLen, outPtr, outPtrLen;
|
||||
let testSpeed = [0, 0];
|
||||
function I420ToARGB(yb) {
|
||||
if (!wasmExports) return;
|
||||
var tm0 = new Date().getTime();
|
||||
var { malloc, free, memory } = wasmExports;
|
||||
var HEAPU8 = new Uint8Array(memory.buffer);
|
||||
let n = yb.y.bytes.length;
|
||||
if (yPtrLen != n) {
|
||||
if (yPtr) free(yPtr);
|
||||
yPtrLen = n;
|
||||
yPtr = malloc(n);
|
||||
}
|
||||
HEAPU8.set(yb.y.bytes, yPtr);
|
||||
n = yb.u.bytes.length;
|
||||
if (uPtrLen != n) {
|
||||
if (uPtr) free(uPtr);
|
||||
uPtrLen = n;
|
||||
uPtr = malloc(n);
|
||||
}
|
||||
HEAPU8.set(yb.u.bytes, uPtr);
|
||||
n = yb.v.bytes.length;
|
||||
if (vPtrLen != n) {
|
||||
if (vPtr) free(vPtr);
|
||||
vPtrLen = n;
|
||||
vPtr = malloc(n);
|
||||
}
|
||||
HEAPU8.set(yb.v.bytes, vPtr);
|
||||
var w = yb.format.displayWidth;
|
||||
var h = yb.format.displayHeight;
|
||||
n = w * h * 4;
|
||||
if (outPtrLen != n) {
|
||||
if (outPtr) free(outPtr);
|
||||
outPtrLen = n;
|
||||
outPtr = malloc(n);
|
||||
HEAPU8.fill(255, outPtr, outPtr + n);
|
||||
}
|
||||
// var res = wasmExports.I420ToARGB(yPtr, yb.y.stride, uPtr, yb.u.stride, vPtr, yb.v.stride, outPtr, w * 4, w, h);
|
||||
// var res = wasmExports.AVX_YUV_to_ARGB(outPtr, yPtr, yb.y.stride, uPtr, yb.u.stride, vPtr, yb.v.stride, w, h);
|
||||
var res = wasmExports.yuv420_rgb24_std(w, h, yPtr, uPtr, vPtr, yb.y.stride, yb.v.stride, outPtr, w * 4, 1);
|
||||
var out = HEAPU8.slice(outPtr, outPtr + n);
|
||||
testSpeed[1] += new Date().getTime() - tm0;
|
||||
testSpeed[0] += 1;
|
||||
if (testSpeed[0] > 30) {
|
||||
console.log('yuv: ' + parseInt('' + testSpeed[1] / testSpeed[0]));
|
||||
testSpeed = [0, 0];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
var currentFrame;
|
||||
self.addEventListener('message', (e) => {
|
||||
currentFrame = e.data;
|
||||
});
|
||||
|
||||
function run() {
|
||||
if (currentFrame) {
|
||||
self.postMessage(I420ToARGB(currentFrame));
|
||||
currentFrame = undefined;
|
||||
}
|
||||
setTimeout(run, 1);
|
||||
}
|
||||
|
||||
run();
|
||||
Binary file not shown.
Reference in New Issue
Block a user