From 8dff263a0c9f611a6943b937b5433e77f84ac0ac Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 25 Mar 2024 10:47:53 +0800 Subject: [PATCH] Refact. Flutter web, mid commit (#7502) * Refact. Flutter web, mid commit Signed-off-by: fufesou * Refact. Flutter web, mid commit Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/models/model.dart | 17 ++- flutter/lib/models/native_model.dart | 2 +- flutter/lib/models/web_model.dart | 6 +- flutter/lib/web/bridge.dart | 159 +++++++++++++++++++-------- flutter/web/js/src/connection.ts | 50 +++++---- flutter/web/js/src/globals.js | 34 +++--- 6 files changed, 181 insertions(+), 87 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 7540a5b41..a58a46db5 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -690,7 +690,7 @@ class FfiModel with ChangeNotifier { // Because this function is asynchronous, there's an "await" in this function. cachedPeerData.peerInfo = {...evt}; - // recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs) + // Recent peer is updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs) bind.mainLoadRecentPeers(); parent.target?.dialogManager.dismissAll(); @@ -868,7 +868,16 @@ class FfiModel with ChangeNotifier { handleResolutions(String id, dynamic resolutions) { try { - final List dynamicArray = jsonDecode(resolutions as String); + final resolutionsObj = json.decode(resolutions as String); + late List dynamicArray; + if (resolutionsObj is Map) { + // The web version + dynamicArray = (resolutionsObj as Map)['resolutions'] + as List; + } else { + // The rust version + dynamicArray = resolutionsObj as List; + } List arr = List.empty(growable: true); for (int i = 0; i < dynamicArray.length; i++) { var width = dynamicArray[i]["width"]; @@ -2236,6 +2245,10 @@ class FFI { } final stream = bind.sessionStart(sessionId: sessionId, id: id); if (isWeb) { + platformFFI.setRgbaCallback((int display, Uint8List data) { + onEvent2UIRgba(); + imageModel.onRgba(display, data); + }); return; } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 9460f12bf..fe8fba732 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -247,7 +247,7 @@ class PlatformFFI { _eventCallback = fun; } - void setRgbaCallback(void Function(Uint8List) fun) async {} + void setRgbaCallback(void Function(int, Uint8List) fun) async {} void startDesktopWebListener() {} diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart index ef8bfff21..5c44859ce 100644 --- a/flutter/lib/models/web_model.dart +++ b/flutter/lib/models/web_model.dart @@ -123,10 +123,10 @@ class PlatformFFI { }; } - void setRgbaCallback(void Function(Uint8List) fun) { - context["onRgba"] = (Uint8List? rgba) { + void setRgbaCallback(void Function(int, Uint8List) fun) { + context["onRgba"] = (int display, Uint8List? rgba) { if (rgba != null) { - fun(rgba); + fun(display, rgba); } }; } diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 03fb4d158..2440ee25b 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -52,20 +52,18 @@ class RustdeskImpl { } int peerGetDefaultSessionsCount({required String id, dynamic hint}) { - throw UnimplementedError(); + return 0; } String sessionAddExistedSync( {required String id, required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + return ''; } void sessionTryAddDisplay( {required UuidValue sessionId, required Int32List displays, - dynamic hint}) { - throw UnimplementedError(); - } + dynamic hint}) {} String sessionAddSync( {required UuidValue sessionId, @@ -95,7 +93,8 @@ class RustdeskImpl { Future sessionGetRemember( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('getByName', ['remember']) == 'true'); } Future sessionGetToggleOption( @@ -122,7 +121,15 @@ class RustdeskImpl { required String password, required bool remember, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'login', + jsonEncode({ + 'os_username': osUsername, + 'os_password': osPassword, + 'password': password, + 'remember': remember + }) + ])); } Future sessionSend2Fa( @@ -156,7 +163,7 @@ class RustdeskImpl { Future sessionReconnect( {required UuidValue sessionId, required bool forceRelay, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', ['reconnect'])); } Future sessionToggleOption( @@ -225,77 +232,110 @@ class RustdeskImpl { Future sessionGetViewStyle( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + // TODO: default values + return Future(() => + js.context.callMethod('getByName', ['option:peer', 'view_style'])); } Future sessionSetViewStyle( {required UuidValue sessionId, required String value, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'option:peer', + jsonEncode({'name': 'view_style', 'value': value}) + ])); } Future sessionGetScrollStyle( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + // TODO: default values + return Future(() => + js.context.callMethod('getByName', ['option:peer', 'scroll_style'])); } Future sessionSetScrollStyle( {required UuidValue sessionId, required String value, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'option:peer', + jsonEncode({'name': 'scroll_style', 'value': value}) + ])); } Future sessionGetImageQuality( - {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + // TODO: default values + {required UuidValue sessionId, + dynamic hint}) { + return Future(() => + js.context.callMethod('getByName', ['option:peer', 'image_quality'])); } Future sessionSetImageQuality( {required UuidValue sessionId, required String value, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'option:peer', + jsonEncode({'name': 'image_quality', 'value': value}) + ])); } Future sessionGetKeyboardMode( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + // TODO: default values + return Future(() => + js.context.callMethod('getByName', ['option:peer', 'keyboard_mode'])); } Future sessionSetKeyboardMode( {required UuidValue sessionId, required String value, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'option:peer', + jsonEncode({'name': 'keyboard_mode', 'value': value}) + ])); } String? sessionGetReverseMouseWheelSync( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + return js.context + .callMethod('getByName', ['option:peer', 'reverse_mouse_wheel']); } Future sessionSetReverseMouseWheel( {required UuidValue sessionId, required String value, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'option:peer', + jsonEncode({'name': 'reverse_mouse_wheel', 'value': value}) + ])); } String? sessionGetDisplaysAsIndividualWindows( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + return js.context.callMethod( + 'getByName', ['option:peer', 'displays_as_individual_windows']); } Future sessionSetDisplaysAsIndividualWindows( {required UuidValue sessionId, required String value, dynamic hint}) { - throw UnimplementedError(); + return Future.value(); } String? sessionGetUseAllMyDisplaysForTheRemoteSession( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + return ''; } Future sessionSetUseAllMyDisplaysForTheRemoteSession( {required UuidValue sessionId, required String value, dynamic hint}) { - throw UnimplementedError(); + return Future.value(); } Future sessionGetCustomImageQuality( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + try { + return Future(() => Int32List.fromList([ + int.parse(js.context.callMethod( + 'getByName', ['option:peer', 'custom_image_quality'])) + ])); + } catch (e) { + return Future.value(null); + } } bool sessionIsKeyboardModeSupported( @@ -305,12 +345,18 @@ class RustdeskImpl { Future sessionSetCustomImageQuality( {required UuidValue sessionId, required int value, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'option:peer', + jsonEncode({'name': 'custom_image_quality', 'value': value}) + ])); } Future sessionSetCustomFps( {required UuidValue sessionId, required int fps, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('setByName', [ + 'option:peer', + jsonEncode({'name': 'custom_fps', 'value': fps}) + ])); } Future sessionLockScreen({required UuidValue sessionId, dynamic hint}) { @@ -373,17 +419,22 @@ class RustdeskImpl { required String name, required String value, dynamic hint}) { - throw UnimplementedError(); + return Future(() => js.context.callMethod('SetByName', [ + 'option:peer', + jsonEncode({'name': name, 'value': value}) + ])); } Future sessionGetPeerOption( {required UuidValue sessionId, required String name, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('getByName', ['option:peer', name])); } Future sessionInputOsPassword( {required UuidValue sessionId, required String value, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['input_os_password', value])); } Future sessionReadRemoteDir( @@ -531,7 +582,7 @@ class RustdeskImpl { required int width, required int height, dynamic hint}) { - throw UnimplementedError(); + return Future.value(); } Future sessionSendSelectedSessionId( @@ -991,7 +1042,8 @@ class RustdeskImpl { Future sessionSendMouse( {required UuidValue sessionId, required String msg, dynamic hint}) { - throw UnimplementedError(); + return Future( + () => js.context.callMethod('setByName', ['send_mouse', msg])); } Future sessionRestartRemoteDevice( @@ -1021,7 +1073,7 @@ class RustdeskImpl { Future sessionOnWaitingForImageDialogShow( {required UuidValue sessionId, dynamic hint}) { - throw UnimplementedError(); + return Future.value(); } Future sessionToggleVirtualDisplay( @@ -1142,36 +1194,55 @@ class RustdeskImpl { int sessionGetRgbaSize( {required UuidValue sessionId, required int display, dynamic hint}) { - throw UnimplementedError(); + return 0; } void sessionNextRgba( - {required UuidValue sessionId, required int display, dynamic hint}) { - throw UnimplementedError(); - } + {required UuidValue sessionId, required int display, dynamic hint}) {} void sessionRegisterPixelbufferTexture( {required UuidValue sessionId, required int display, required int ptr, - dynamic hint}) { - throw UnimplementedError(); - } + dynamic hint}) {} void sessionRegisterGpuTexture( {required UuidValue sessionId, required int display, required int ptr, - dynamic hint}) { - throw UnimplementedError(); - } + dynamic hint}) {} Future queryOnlines({required List ids, dynamic hint}) { throw UnimplementedError(); } + // Dup to the function in hbb_common, lib.rs + // Maybe we need to move this function to js part. int versionToNumber({required String v, dynamic hint}) { - throw UnimplementedError(); + List versions = v.split('-'); + + int n = 0; + + // The first part is the version number. + // 1.1.10 -> 1001100, 1.2.3 -> 1001030, multiple the last number by 10 + // to leave space for patch version. + if (versions.isNotEmpty) { + int last = 0; + for (var x in versions[0].split('.')) { + last = int.tryParse(x) ?? 0; + n = n * 1000 + last; + } + n -= last; + n += last * 10; + } + + if (versions.length > 1) { + n += int.tryParse(versions[1]) ?? 0; + } + + // Ignore the rest + + return n; } Future optionSynced({dynamic hint}) { @@ -1419,7 +1490,7 @@ class RustdeskImpl { } bool isSupportMultiUiSession({required String version, dynamic hint}) { - throw UnimplementedError(); + return versionToNumber(v: version) > versionToNumber(v: '1.2.4'); } bool isSelinuxEnforcing({dynamic hint}) { diff --git a/flutter/web/js/src/connection.ts b/flutter/web/js/src/connection.ts index 9d89fd231..7ae40d627 100644 --- a/flutter/web/js/src/connection.ts +++ b/flutter/web/js/src/connection.ts @@ -16,7 +16,7 @@ let HOST = localStorage.getItem("rendezvous-server") || HOSTS[0]; const SCHEMA = "ws://"; type MsgboxCallback = (type: string, title: string, text: string, link: string) => void; -type DrawCallback = (data: Uint8Array) => void; +type DrawCallback = (display: number, data: Uint8Array) => void; //const cursorCanvas = document.createElement("canvas"); export default class Connection { @@ -41,7 +41,6 @@ export default class Connection { this._msgs = []; this._id = ""; this._videoTestSpeed = [0, 0]; - this._options = {}; //this._cursors = {}; } @@ -67,7 +66,7 @@ export default class Connection { try { this._password = Uint8Array.from(JSON.parse("[" + p + "]")); } catch (e) { - console.error(e); + console.error('Failed to get password, ' + e); } } } @@ -171,7 +170,7 @@ export default class Connection { pk = undefined; } } catch (e) { - console.error(e); + console.error('Failed to verify id pk, ', e); pk = undefined; } if (!pk) @@ -196,7 +195,7 @@ export default class Connection { try { signedId = await globals.verify(signedId.id, Uint8Array.from(pk!)); } catch (e) { - console.error(e); + console.error('Failed to verify signed id pk, ', 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({}); @@ -243,7 +242,7 @@ export default class Connection { this.login(); } else if (msg?.test_delay) { const test_delay = msg?.test_delay; - console.log(test_delay); + console.log('test delay: ', test_delay); if (!test_delay.from_client) { this._ws?.sendMessage({ test_delay }); } @@ -275,7 +274,7 @@ export default class Connection { try { globals.copyToClipboard(new TextDecoder().decode(cb.content)); } catch (e) { - console.error(e); + console.error('Failed to copy to clipboard, ', e); } // globals.pushEvent("clipboard", cb); } else if (msg?.cursor_data) { @@ -323,9 +322,9 @@ export default class Connection { this._msgbox?.(type_, title, text, link); } - draw(frame: any) { - this._draw?.(frame); - globals.draw(frame); + draw(display: number, frame: any) { + this._draw?.(display, frame); + globals.draw(display, frame); } close() { @@ -348,22 +347,25 @@ export default class Connection { this._draw = callback; } - login(password: string | undefined = undefined) { - if (password) { + login(info?: { + os_login?: message.OSLogin, + password?: Uint8Array + }) { + if (info?.password) { const salt = this._hash?.salt; - let p = hash([password, salt!]); + let p = hash([info.password, salt!]); this._password = p; const challenge = this._hash?.challenge; p = hash([p, challenge!]); this.msgbox("connecting", "Connecting...", "Logging in..."); - this._sendLoginMessage(p); + this._sendLoginMessage({ os_login: info.os_login, password: p }); } else { let p = this._password; if (p) { const challenge = this._hash?.challenge; p = hash([p, challenge!]); } - this._sendLoginMessage(p); + this._sendLoginMessage({ os_login: info?.os_login, password: p }); } } @@ -372,14 +374,18 @@ export default class Connection { await this.start(this._id); } - _sendLoginMessage(password: Uint8Array | undefined = undefined) { + _sendLoginMessage(login: { + os_login?: message.OSLogin, + password?: Uint8Array, + }) { const login_request = message.LoginRequest.fromPartial({ username: this._id!, my_id: "web", // to-do my_name: "web", // to-do - password, + password: login.password, option: this.getOptionMessage(), video_ack_required: true, + os_login: login.os_login, }); this._ws?.sendMessage({ login_request }); } @@ -436,7 +442,7 @@ export default class Connection { i++; if (i == n) this.sendVideoReceived(); if (ok && dec.frameBuffer && n == i) { - this.draw(dec.frameBuffer); + this.draw(vf.display, dec.frameBuffer); const now = new Date().getTime(); var elapsed = now - tm; this._videoTestSpeed[1] += elapsed; @@ -570,10 +576,8 @@ export default class Connection { } this._options["tm"] = new Date().getTime(); const peers = globals.getPeers(); - if (peers) { - peers[this._id] = this._options; - localStorage.setItem("peers", JSON.stringify(peers)); - } + peers[this._id] = this._options; + localStorage.setItem("peers", JSON.stringify(peers)); } inputKey( @@ -744,7 +748,7 @@ export default class Connection { loadVp9((decoder: any) => { this._videoDecoder = decoder; console.log("vp9 loaded"); - console.log(decoder); + console.log('The decoder: ', decoder); }); } } diff --git a/flutter/web/js/src/globals.js b/flutter/web/js/src/globals.js index 8ae2ac115..d6f4080aa 100644 --- a/flutter/web/js/src/globals.js +++ b/flutter/web/js/src/globals.js @@ -28,7 +28,13 @@ 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); + if (value instanceof String || typeof value == 'string') { + tmp[key] = value; + } else if (value instanceof Uint8Array) { + tmp[key] = '[' + value.toString() + ']'; + } else { + tmp[key] = JSON.stringify(value); + } } return tmp; } @@ -54,10 +60,10 @@ if (YUVCanvas.WebGLFrameSink.isAvailable()) { } let testSpeed = [0, 0]; -export function draw(frame) { +export function draw(display, frame) { if (yuvWorker) { // frame's (y/u/v).bytes already detached, can not transferrable any more. - yuvWorker.postMessage(frame); + yuvWorker.postMessage({display, frame}); } else { var tm0 = new Date().getTime(); yuvCanvas.drawFrame(frame); @@ -75,7 +81,7 @@ export function draw(frame) { for (let i = 0; i < size; i += row) { flipPixels.set(pixels.subarray(i, i + row), end - i); } - onRgba(flipPixels); + onRgba(display, flipPixels); testSpeed[1] += new Date().getTime() - tm0; testSpeed[0] += 1; if (testSpeed[0] > 30) { @@ -189,8 +195,14 @@ window.setByName = (name, value) => { break; case 'login': value = JSON.parse(value); - curConn.setRemember(value.remember == 'true'); - curConn.login(value.password); + curConn.setRemember(value.remember); + curConn.login({ + os_login: { + username: value.os_username, + password: value.os_password, + }, + password: value.password, + }); break; case 'close': close(); @@ -270,7 +282,7 @@ window.setByName = (name, value) => { value = JSON.parse(value); localStorage.setItem(name + ':' + value.name, value.value); break; - case 'peer_option': + case 'option:peer': value = JSON.parse(value); curConn.setOption(value.name, value.value); break; @@ -409,7 +421,7 @@ export function playAudio(packet) { window.init = async () => { if (yuvWorker) { yuvWorker.onmessage = (e) => { - onRgba(e.data); + onRgba(e.data.display, e.data.frame); } } opusWorker.onmessage = (e) => { @@ -493,12 +505,6 @@ function sessionAdd(value) { const data = JSON.parse(value); window.curConn?.close(); const conn = new Connection(); - if (data['password']) { - // TODO: encrypt password - conn.setOption('password', data['password']) - } else { - conn.setOption('password', undefined); - } setConn(conn); return ''; } catch (e) {