globals.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. import Connection from "./connection";
  2. import _sodium from "libsodium-wrappers";
  3. import {loadVp9} from "./codec";
  4. import {checkIfRetry, version} from "./gen_js_from_hbb";
  5. import {initZstd, translate} from "./common";
  6. import PCMPlayer from "pcm-player";
  7. import {getServerConf} from "./ljw";
  8. window.myconsole = (...args) => {
  9. console.log(args);
  10. }
  11. window.curConn = undefined;
  12. window.isMobile = () => {
  13. 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)
  14. || /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));
  15. }
  16. export function isDesktop() {
  17. return !isMobile();
  18. }
  19. export function msgbox(type, title, text) {
  20. if (!type || (type == 'error' && !text)) return;
  21. const text2 = text.toLowerCase();
  22. var hasRetry = checkIfRetry(type, title, text) ? 'true' : '';
  23. onGlobalEvent(JSON.stringify({name: 'msgbox', type, title, text, hasRetry}));
  24. }
  25. function jsonfyForDart(payload) {
  26. var tmp = {};
  27. for (const [key, value] of Object.entries(payload)) {
  28. if (!key) continue;
  29. tmp[key] = value instanceof Uint8Array ? '[' + value.toString() + ']' : JSON.stringify(value);
  30. }
  31. return tmp;
  32. }
  33. export function pushEvent(name, payload) {
  34. payload = jsonfyForDart(payload);
  35. payload.name = name;
  36. onGlobalEvent(JSON.stringify(payload));
  37. }
  38. let yuvWorker;
  39. let yuvCanvas;
  40. let gl;
  41. let pixels;
  42. let flipPixels;
  43. let oldSize;
  44. if (YUVCanvas.WebGLFrameSink.isAvailable()) {
  45. var canvas = document.createElement('canvas');
  46. yuvCanvas = YUVCanvas.attach(canvas, {webGL: true});
  47. gl = canvas.getContext("webgl");
  48. } else {
  49. yuvWorker = new Worker("./yuv.js");
  50. }
  51. let testSpeed = [0, 0];
  52. export function draw(frame) {
  53. if (yuvWorker) {
  54. // frame's (y/u/v).bytes already detached, can not transferrable any more.
  55. yuvWorker.postMessage(frame);
  56. } else {
  57. var tm0 = new Date().getTime();
  58. yuvCanvas.drawFrame(frame);
  59. var width = canvas.width;
  60. var height = canvas.height;
  61. var size = width * height * 4;
  62. if (size != oldSize) {
  63. pixels = new Uint8Array(size);
  64. flipPixels = new Uint8Array(size);
  65. oldSize = size;
  66. }
  67. gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
  68. const row = width * 4;
  69. const end = (height - 1) * row;
  70. for (let i = 0; i < size; i += row) {
  71. flipPixels.set(pixels.subarray(i, i + row), end - i);
  72. }
  73. onRgba(flipPixels);
  74. testSpeed[1] += new Date().getTime() - tm0;
  75. testSpeed[0] += 1;
  76. if (testSpeed[0] > 30) {
  77. console.log('gl: ' + parseInt('' + testSpeed[1] / testSpeed[0]));
  78. testSpeed = [0, 0];
  79. }
  80. }
  81. /*
  82. var testCanvas = document.getElementById("test-yuv-decoder-canvas");
  83. if (testCanvas && currentFrame) {
  84. var ctx = testCanvas.getContext("2d");
  85. testCanvas.width = frame.format.displayWidth;
  86. testCanvas.height = frame.format.displayHeight;
  87. var img = ctx.createImageData(testCanvas.width, testCanvas.height);
  88. img.data.set(currentFrame);
  89. ctx.putImageData(img, 0, 0);
  90. }
  91. */
  92. }
  93. export function sendOffCanvas(c) {
  94. let canvas = c.transferControlToOffscreen();
  95. yuvWorker.postMessage({canvas}, [canvas]);
  96. }
  97. export function setConn(conn) {
  98. window.curConn = conn;
  99. }
  100. export function getConn() {
  101. return window.curConn;
  102. }
  103. export async function startConn(id) {
  104. setByName('remote_id', id);
  105. await curConn.start(id);
  106. }
  107. export function close() {
  108. getConn()?.close();
  109. setConn(undefined);
  110. }
  111. export function newConn() {
  112. window.curConn?.close();
  113. const conn = new Connection();
  114. setConn(conn);
  115. return conn;
  116. }
  117. let sodium;
  118. export async function verify(signed, pk) {
  119. if (!sodium) {
  120. await _sodium.ready;
  121. sodium = _sodium;
  122. }
  123. if (typeof pk == 'string') {
  124. pk = decodeBase64(pk);
  125. }
  126. return sodium.crypto_sign_open(signed, pk);
  127. }
  128. export function decodeBase64(pk) {
  129. return sodium.from_base64(pk, sodium.base64_variants.ORIGINAL);
  130. }
  131. export function genBoxKeyPair() {
  132. const pair = sodium.crypto_box_keypair();
  133. const sk = pair.privateKey;
  134. const pk = pair.publicKey;
  135. return [sk, pk];
  136. }
  137. export function genSecretKey() {
  138. return sodium.crypto_secretbox_keygen();
  139. }
  140. export function seal(unsigned, theirPk, ourSk) {
  141. const nonce = Uint8Array.from(Array(24).fill(0));
  142. return sodium.crypto_box_easy(unsigned, nonce, theirPk, ourSk);
  143. }
  144. function makeOnce(value) {
  145. var byteArray = Array(24).fill(0);
  146. for (var index = 0; index < byteArray.length && value > 0; index++) {
  147. var byte = value & 0xff;
  148. byteArray[index] = byte;
  149. value = (value - byte) / 256;
  150. }
  151. return Uint8Array.from(byteArray);
  152. };
  153. export function encrypt(unsigned, nonce, key) {
  154. return sodium.crypto_secretbox_easy(unsigned, makeOnce(nonce), key);
  155. }
  156. export function decrypt(signed, nonce, key) {
  157. return sodium.crypto_secretbox_open_easy(signed, makeOnce(nonce), key);
  158. }
  159. window.setByName = (name, value) => {
  160. myconsole('setByName', name, value);
  161. switch (name) {
  162. case 'remote_id':
  163. localStorage.setItem('remote-id', value);
  164. break;
  165. case 'connect':
  166. newConn();
  167. startConn(value);
  168. break;
  169. case 'login':
  170. value = JSON.parse(value);
  171. curConn.setRemember(value.remember == 'true');
  172. curConn.login(value.password);
  173. break;
  174. case 'close':
  175. close();
  176. break;
  177. case 'refresh':
  178. curConn.refresh();
  179. break;
  180. case 'reconnect':
  181. curConn.reconnect();
  182. break;
  183. case 'toggle_option':
  184. curConn.toggleOption(value);
  185. break;
  186. case 'image_quality':
  187. curConn.setImageQuality(value);
  188. break;
  189. case 'lock_screen':
  190. curConn.lockScreen();
  191. break;
  192. case 'ctrl_alt_del':
  193. curConn.ctrlAltDel();
  194. break;
  195. case 'switch_display':
  196. curConn.switchDisplay(value);
  197. break;
  198. case 'remove':
  199. const peers = getPeers();
  200. delete peers[value];
  201. localStorage.setItem('peers', JSON.stringify(peers));
  202. break;
  203. case 'input_key':
  204. value = JSON.parse(value);
  205. curConn.inputKey(value.name, value.down == 'true', value.press == 'true', value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
  206. break;
  207. case 'input_string':
  208. curConn.inputString(value);
  209. break;
  210. case 'send_mouse':
  211. let mask = 0;
  212. value = JSON.parse(value);
  213. switch (value.type) {
  214. case 'down':
  215. mask = 1;
  216. break;
  217. case 'up':
  218. mask = 2;
  219. break;
  220. case 'wheel':
  221. mask = 3;
  222. break;
  223. }
  224. switch (value.buttons) {
  225. case 'left':
  226. mask |= 1 << 3;
  227. break;
  228. case 'right':
  229. mask |= 2 << 3;
  230. break;
  231. case 'wheel':
  232. mask |= 4 << 3;
  233. }
  234. curConn.inputMouse(mask, parseInt(value.x || '0'), parseInt(value.y || '0'), value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true');
  235. break;
  236. case 'option':
  237. value = JSON.parse(value);
  238. localStorage.setItem(value.name, value.value);
  239. if (value.name === 'access_token' && value.value) {
  240. getServerConf(value.value);
  241. }
  242. break;
  243. case 'peer_option':
  244. value = JSON.parse(value);
  245. curConn.setOption(value.name, value.value);
  246. break;
  247. case 'input_os_password':
  248. curConn.inputOsPassword(value);
  249. break;
  250. default:
  251. break;
  252. }
  253. }
  254. window.getByName = (name, arg) => {
  255. let v = _getByName(name, arg);
  256. myconsole('getByName', name, arg, v);
  257. if (typeof v == 'string' || v instanceof String) return v;
  258. if (v == undefined || v == null) return '';
  259. return JSON.stringify(v);
  260. }
  261. function getPeersForDart() {
  262. const peers = [];
  263. for (const [id, value] of Object.entries(getPeers())) {
  264. if (!id) continue;
  265. const tm = value['tm'];
  266. const info = value['info'];
  267. if (!tm || !info) continue;
  268. peers.push([tm, id, info]);
  269. }
  270. return peers.sort().reverse().map(x => x.slice(1));
  271. }
  272. function _getByName(name, arg) {
  273. switch (name) {
  274. case 'peers':
  275. return getPeersForDart();
  276. case 'remote_id':
  277. return localStorage.getItem('remote-id');
  278. case 'remember':
  279. return curConn.getRemember();
  280. case 'toggle_option':
  281. return curConn.getOption(arg) || false;
  282. case 'option':
  283. const v = localStorage.getItem(arg);
  284. if (arg === 'access_token' && v) {
  285. getServerConf(v);
  286. }
  287. return v;
  288. case 'image_quality':
  289. return curConn.getImageQuality();
  290. case 'translate':
  291. arg = JSON.parse(arg);
  292. return translate(arg.locale, arg.text);
  293. case 'peer_option':
  294. return curConn.getOption(arg);
  295. case 'test_if_valid_server':
  296. break;
  297. case 'version':
  298. return version;
  299. }
  300. return '';
  301. }
  302. let opusWorker = new Worker("./libopus.js");
  303. let pcmPlayer;
  304. export function initAudio(channels, sampleRate) {
  305. pcmPlayer = newAudioPlayer(channels, sampleRate);
  306. opusWorker.postMessage({channels, sampleRate});
  307. }
  308. export function playAudio(packet) {
  309. opusWorker.postMessage(packet, [packet.buffer]);
  310. }
  311. window.init = async () => {
  312. if (yuvWorker) {
  313. yuvWorker.onmessage = (e) => {
  314. onRgba(e.data);
  315. }
  316. }
  317. opusWorker.onmessage = (e) => {
  318. pcmPlayer.feed(e.data);
  319. }
  320. loadVp9(() => {
  321. });
  322. await initZstd();
  323. console.log('init done');
  324. }
  325. export function getPeers() {
  326. try {
  327. return JSON.parse(localStorage.getItem('peers')) || {};
  328. } catch (e) {
  329. return {};
  330. }
  331. }
  332. function newAudioPlayer(channels, sampleRate) {
  333. return new PCMPlayer({
  334. channels,
  335. sampleRate,
  336. flushingTime: 2000
  337. });
  338. }
  339. export function copyToClipboard(text) {
  340. if (window.clipboardData && window.clipboardData.setData) {
  341. // Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
  342. return window.clipboardData.setData("Text", text);
  343. } else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
  344. var textarea = document.createElement("textarea");
  345. textarea.textContent = text;
  346. textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge.
  347. document.body.appendChild(textarea);
  348. textarea.select();
  349. try {
  350. return document.execCommand("copy"); // Security exception may be thrown by some browsers.
  351. } catch (ex) {
  352. console.warn("Copy to clipboard failed.", ex);
  353. // return prompt("Copy to clipboard: Ctrl+C, Enter", text);
  354. } finally {
  355. document.body.removeChild(textarea);
  356. }
  357. }
  358. }