diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 8067ca146..113c41676 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -10,6 +10,7 @@ import 'remote_page.dart'; import 'settings_page.dart'; import 'scan_page.dart'; +/// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget implements PageShape { ConnectionPage({Key? key}) : super(key: key); @@ -26,8 +27,12 @@ class ConnectionPage extends StatefulWidget implements PageShape { _ConnectionPageState createState() => _ConnectionPageState(); } +/// State for the connection page. class _ConnectionPageState extends State { + /// Controller for the id input bar. final _idController = TextEditingController(); + + /// Update url. If it's not null, means an update is available. var _updateUrl = ''; var _menuPos; @@ -60,11 +65,15 @@ class _ConnectionPageState extends State { ); } + /// Callback for the connect button. + /// Connects to the selected peer. void onConnect() { var id = _idController.text.trim(); connect(id); } + /// Connect to a peer with [id]. + /// If [isFileTransfer], starts a session only for file transfer. void connect(String id, {bool isFileTransfer = false}) async { if (id == '') return; id = id.replaceAll(' ', ''); @@ -94,6 +103,8 @@ class _ConnectionPageState extends State { } } + /// UI for software update. + /// If [_updateUrl] is not empty, shows a button to update the software. Widget getUpdateUI() { return _updateUrl.isEmpty ? SizedBox(height: 0) @@ -114,6 +125,8 @@ class _ConnectionPageState extends State { color: Colors.white, fontWeight: FontWeight.bold)))); } + /// UI for the search bar. + /// Search for a peer and connect to it if the id exists. Widget getSearchBarUI() { var w = Padding( padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0), @@ -187,6 +200,7 @@ class _ConnectionPageState extends State { super.dispose(); } + /// Get the image for the current [platform]. Widget getPlatformImage(String platform) { platform = platform.toLowerCase(); if (platform == 'mac os') @@ -195,6 +209,7 @@ class _ConnectionPageState extends State { return Image.asset('assets/$platform.png', width: 24, height: 24); } + /// Get all the saved peers. Widget getPeers() { final size = MediaQuery.of(context).size; final space = 8.0; @@ -244,6 +259,8 @@ class _ConnectionPageState extends State { return Wrap(children: cards, spacing: space, runSpacing: space); } + /// Show the peer menu and handle user's choice. + /// User might remove the peer or send a file to the peer. void showPeerMenu(BuildContext context, String id) async { var value = await showMenu( context: context, diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index aef7a535d..464e171aa 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -119,6 +119,7 @@ class FfiModel with ChangeNotifier { _permissions.clear(); } + /// Bind the event listener to receive events from the Rust core. void updateEventListener(String peerId) { final void Function(Map) cb = (evt) { var name = evt['name']; @@ -179,6 +180,7 @@ class FfiModel with ChangeNotifier { notifyListeners(); } + /// Handle the message box event based on [evt] and [id]. void handleMsgBox(Map evt, String id) { var type = evt['type']; var title = evt['title']; @@ -193,6 +195,7 @@ class FfiModel with ChangeNotifier { } } + /// Show a message box with [type], [title] and [text]. void showMsgBox(String type, String title, String text, bool hasRetry) { msgBox(type, title, text); _timer?.cancel(); @@ -207,6 +210,7 @@ class FfiModel with ChangeNotifier { } } + /// Handle the peer info event based on [evt]. void handlePeerInfo(Map evt) { SmartDialog.dismiss(); _pi.version = evt['version']; @@ -649,6 +653,7 @@ class CursorModel with ChangeNotifier { } } +/// Mouse button enum. enum MouseButtons { left, right, wheel } extension ToString on MouseButtons { @@ -664,6 +669,7 @@ extension ToString on MouseButtons { } } +/// FFI class for communicating with the Rust core. class FFI { static var id = ""; static var shift = false; @@ -679,29 +685,35 @@ class FFI { static final chatModel = ChatModel(); static final fileModel = FileModel(); + /// Get the remote id for current client. static String getId() { return getByName('remote_id'); } + /// Send a mouse tap event(down and up). static void tap(MouseButtons button) { sendMouse('down', button); sendMouse('up', button); } + /// Send scroll event with scroll distance [y]. static void scroll(int y) { setByName('send_mouse', json.encode(modify({'type': 'wheel', 'y': y.toString()}))); } + /// Reconnect to the remote peer. static void reconnect() { setByName('reconnect'); FFI.ffiModel.clearPermissions(); } + /// Reset key modifiers to false, including [shift], [ctrl], [alt] and [command]. static void resetModifiers() { shift = ctrl = alt = command = false; } + /// Modify the given modifier map [evt] based on current modifier key status. static Map modify(Map evt) { if (ctrl) evt['ctrl'] = 'true'; if (shift) evt['shift'] = 'true'; @@ -710,12 +722,16 @@ class FFI { return evt; } + /// Send mouse press event. static void sendMouse(String type, MouseButtons button) { if (!ffiModel.keyboard()) return; setByName('send_mouse', json.encode(modify({'type': type, 'buttons': button.value}))); } + /// Send key stroke event. + /// [down] indicates the key's state(down or up). + /// [press] indicates a click event(down and up). static void inputKey(String name, {bool? down, bool? press}) { if (!ffiModel.keyboard()) return; setByName( @@ -727,6 +743,7 @@ class FFI { }))); } + /// Send mouse movement event with distance in [x] and [y]. static void moveMouse(double x, double y) { if (!ffiModel.keyboard()) return; var x2 = x.toInt(); @@ -734,6 +751,7 @@ class FFI { setByName('send_mouse', json.encode(modify({'x': '$x2', 'y': '$y2'}))); } + /// List the saved peers. static List peers() { try { var str = getByName('peers'); @@ -750,6 +768,7 @@ class FFI { return []; } + /// Connect with the given [id]. Only transfer file if [isFileTransfer]. static void connect(String id, {bool isFileTransfer = false}) { if (isFileTransfer) { setByName('connect_file_transfer', id); @@ -772,6 +791,7 @@ class FFI { return null; } + /// Login with [password], choose if the client should [remember] it. static void login(String password, bool remember) { setByName( 'login', @@ -781,6 +801,7 @@ class FFI { })); } + /// Close the remote session. static void close() { chatModel.close(); if (FFI.imageModel.image != null && !isWebDesktop) { @@ -796,10 +817,13 @@ class FFI { resetModifiers(); } + /// Send **get** command to the Rust core based on [name] and [arg]. + /// Return the result as a string. static String getByName(String name, [String arg = '']) { return PlatformFFI.getByName(name, arg); } + /// Send **set** command to the Rust core based on [name] and [value]. static void setByName(String name, [String value = '']) { PlatformFFI.setByName(name, value); } @@ -953,6 +977,7 @@ void initializeCursorAndCanvas() async { FFI.canvasModel.update(xCanvas, yCanvas, scale); } +/// Translate text based on the pre-defined dictionary. String translate(String name) { if (name.startsWith('Failed to') && name.contains(': ')) { return name.split(': ').map((x) => translate(x)).join(': '); diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 21ecd37e3..f9135a06c 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -22,6 +22,8 @@ class RgbaFrame extends Struct { typedef F2 = Pointer Function(Pointer, Pointer); typedef F3 = void Function(Pointer, Pointer); +/// FFI wrapper around the native Rust core. +/// Hides the platform differences. class PlatformFFI { static Pointer? _lastRgbaFrame; static String _dir = ''; @@ -36,6 +38,8 @@ class PlatformFFI { return packageInfo.version; } + /// Send **get** command to the Rust core based on [name] and [arg]. + /// Return the result as a string. static String getByName(String name, [String arg = '']) { if (_getByName == null) return ''; var a = name.toNativeUtf8(); @@ -49,6 +53,7 @@ class PlatformFFI { return res; } + /// Send **set** command to the Rust core based on [name] and [value]. static void setByName(String name, [String value = '']) { if (_setByName == null) return; var a = name.toNativeUtf8(); @@ -58,6 +63,7 @@ class PlatformFFI { calloc.free(b); } + /// Init the FFI class, loads the native Rust core library. static Future init() async { isIOS = Platform.isIOS; isAndroid = Platform.isAndroid; @@ -112,6 +118,7 @@ class PlatformFFI { version = await getVersion(); } + /// Start listening to the Rust core's events and frames. static void _startListenEvent(RustdeskImpl rustdeskImpl) { () async { await for (final message in rustdeskImpl.startEventStream()) { diff --git a/src/client.rs b/src/client.rs index be2b788ab..236eb331d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -39,6 +39,7 @@ pub mod helper; pub use helper::LatencyController; pub const SEC30: Duration = Duration::from_secs(30); +/// Client of the remote desktop. pub struct Client; #[cfg(not(any(target_os = "android", target_os = "linux")))] @@ -106,6 +107,7 @@ impl Drop for OboePlayer { } impl Client { + /// Start a new connection. pub async fn start( peer: &str, key: &str, @@ -125,6 +127,7 @@ impl Client { } } + /// Start a new connection. async fn _start( peer: &str, key: &str, @@ -259,6 +262,7 @@ impl Client { .await } + /// Connect to the peer. async fn connect( local_addr: SocketAddr, peer: SocketAddr, @@ -345,6 +349,7 @@ impl Client { Ok((conn, direct)) } + /// Establish secure connection with the server. async fn secure_connection( peer_id: &str, signed_id_pk: Vec, @@ -422,6 +427,7 @@ impl Client { Ok(()) } + /// Request a relay connection to the server. async fn request_relay( peer: &str, relay_server: String, @@ -478,6 +484,7 @@ impl Client { Self::create_relay(peer, uuid, relay_server, key, conn_type).await } + /// Create a relay connection to the server. async fn create_relay( peer: &str, uuid: String, @@ -505,6 +512,7 @@ impl Client { } } +/// Audio handler for the [`Client`]. #[derive(Default)] pub struct AudioHandler { audio_decoder: Option<(AudioDecoder, Vec)>, @@ -522,6 +530,7 @@ pub struct AudioHandler { } impl AudioHandler { + /// Create a new audio handler. pub fn new(latency_controller: Arc>) -> Self { AudioHandler { latency_controller, @@ -529,6 +538,7 @@ impl AudioHandler { } } + /// Start the audio playback. #[cfg(target_os = "linux")] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { use psimple::Simple; @@ -558,6 +568,7 @@ impl AudioHandler { Ok(()) } + /// Start the audio playback. #[cfg(target_os = "android")] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { self.oboe = Some(OboePlayer::new( @@ -568,6 +579,7 @@ impl AudioHandler { Ok(()) } + /// Start the audio playback. #[cfg(not(any(target_os = "android", target_os = "linux")))] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { let device = AUDIO_HOST @@ -592,6 +604,7 @@ impl AudioHandler { Ok(()) } + /// Handle audio format and create an audio decoder. pub fn handle_format(&mut self, f: AudioFormat) { match AudioDecoder::new(f.sample_rate, if f.channels > 1 { Stereo } else { Mono }) { Ok(d) => { @@ -606,6 +619,7 @@ impl AudioHandler { } } + /// Handle audio frame and play it. pub fn handle_frame(&mut self, frame: AudioFrame) { if frame.timestamp != 0 { if self @@ -673,6 +687,7 @@ impl AudioHandler { }); } + /// Build audio output stream for current device. #[cfg(not(any(target_os = "android", target_os = "linux")))] fn build_output_stream( &mut self, @@ -708,6 +723,7 @@ impl AudioHandler { } } +/// Video handler for the [`Client`]. pub struct VideoHandler { decoder: Decoder, latency_controller: Arc>, @@ -715,6 +731,7 @@ pub struct VideoHandler { } impl VideoHandler { + /// Create a new video handler. pub fn new(latency_controller: Arc>) -> Self { VideoHandler { decoder: Decoder::new(VideoCodecId::VP9, (num_cpus::get() / 2) as _).unwrap(), @@ -723,8 +740,10 @@ impl VideoHandler { } } + /// Handle a new video frame. pub fn handle_frame(&mut self, vf: VideoFrame) -> ResultType { if vf.timestamp != 0 { + // Update the lantency controller with the latest timestamp. self.latency_controller .lock() .unwrap() @@ -736,6 +755,7 @@ impl VideoHandler { } } + /// Handle a VP9S frame. pub fn handle_vp9s(&mut self, vp9s: &VP9s) -> ResultType { let mut last_frame = Image::new(); for vp9 in vp9s.frames.iter() { @@ -756,11 +776,13 @@ impl VideoHandler { } } + /// Reset the decoder. pub fn reset(&mut self) { self.decoder = Decoder::new(VideoCodecId::VP9, 1).unwrap(); } } +/// Login config handler for [`Client`]. #[derive(Default)] pub struct LoginConfigHandler { id: String, @@ -783,12 +805,24 @@ impl Deref for LoginConfigHandler { } } +/// Load [`PeerConfig`] from id. +/// +/// # Arguments +/// +/// * `id` - id of peer #[inline] pub fn load_config(id: &str) -> PeerConfig { PeerConfig::load(id) } impl LoginConfigHandler { + /// Initialize the login config handler. + /// + /// # Arguments + /// + /// * `id` - id of peer + /// * `is_file_transfer` - Whether the connection is file transfer. + /// * `is_port_forward` - Whether the connection is port forward. pub fn initialize(&mut self, id: String, is_file_transfer: bool, is_port_forward: bool) { self.id = id; self.is_file_transfer = is_file_transfer; @@ -798,6 +832,8 @@ impl LoginConfigHandler { self.config = config; } + /// Check if the client should auto login. + /// Return password if the client should auto login, otherwise return empty string. pub fn should_auto_login(&self) -> String { let l = self.lock_after_session_end; let a = !self.get_option("auto-login").is_empty(); @@ -809,27 +845,49 @@ impl LoginConfigHandler { } } + /// Load [`PeerConfig`]. fn load_config(&self) -> PeerConfig { load_config(&self.id) } + /// Save a [`PeerConfig`] into the handler. + /// + /// # Arguments + /// + /// * `config` - [`PeerConfig`] to save. pub fn save_config(&mut self, config: PeerConfig) { config.store(&self.id); self.config = config; } + /// Set an option for handler's [`PeerConfig`]. + /// + /// # Arguments + /// + /// * `k` - key of option + /// * `v` - value of option pub fn set_option(&mut self, k: String, v: String) { let mut config = self.load_config(); config.options.insert(k, v); self.save_config(config); } + /// Save view style to the current config. + /// + /// # Arguments + /// + /// * `value` - The view style to be saved. pub fn save_view_style(&mut self, value: String) { let mut config = self.load_config(); config.view_style = value; self.save_config(config); } + /// Toggle an option in the handler. + /// + /// # Arguments + /// + /// * `name` - The name of the option to toggle. pub fn toggle_option(&mut self, name: String) -> Option { let mut option = OptionMessage::default(); let mut config = self.load_config(); @@ -905,6 +963,12 @@ impl LoginConfigHandler { Some(msg_out) } + /// Get [`OptionMessage`] of the current [`LoginConfigHandler`]. + /// Return `None` if there's no option, for example, when the session is only for file transfer. + /// + /// # Arguments + /// + /// * `ignore_default` - If `true`, ignore the default value of the option. fn get_option_message(&self, ignore_default: bool) -> Option { if self.is_port_forward || self.is_file_transfer { return None; @@ -958,6 +1022,13 @@ impl LoginConfigHandler { } } + /// Parse the image quality option. + /// Return [`ImageQuality`] if the option is valid, otherwise return `None`. + /// + /// # Arguments + /// + /// * `q` - The image quality option. + /// * `ignore_default` - Ignore the default value. fn get_image_quality_enum(&self, q: &str, ignore_default: bool) -> Option { if q == "low" { Some(ImageQuality::Low) @@ -974,6 +1045,11 @@ impl LoginConfigHandler { } } + /// Get the status of a toggle option. + /// + /// # Arguments + /// + /// * `name` - The name of the toggle option. pub fn get_toggle_option(&self, name: &str) -> bool { if name == "show-remote-cursor" { self.config.show_remote_cursor @@ -992,6 +1068,7 @@ impl LoginConfigHandler { } } + /// Create a [`Message`] for refreshing video. pub fn refresh() -> Message { let mut misc = Misc::new(); misc.set_refresh_video(true); @@ -1000,6 +1077,12 @@ impl LoginConfigHandler { msg_out } + /// Create a [`Message`] for saving custom image quality. + /// + /// # Arguments + /// + /// * `bitrate` - The given bitrate. + /// * `quantizer` - The given quantizer. pub fn save_custom_image_quality(&mut self, bitrate: i32, quantizer: i32) -> Message { let mut misc = Misc::new(); misc.set_option(OptionMessage { @@ -1015,6 +1098,11 @@ impl LoginConfigHandler { msg_out } + /// Save the given image quality to the config. + /// Return a [`Message`] that contains image quality, or `None` if the image quality is not valid. + /// # Arguments + /// + /// * `value` - The image quality. pub fn save_image_quality(&mut self, value: String) -> Option { let mut res = None; if let Some(q) = self.get_image_quality_enum(&value, false) { @@ -1041,6 +1129,8 @@ impl LoginConfigHandler { } } + /// Handle login error. + /// Return true if the password is wrong, return false if there's an actual error. pub fn handle_login_error(&mut self, err: &str, interface: &impl Interface) -> bool { if err == "Wrong Password" { self.password = Default::default(); @@ -1052,6 +1142,12 @@ impl LoginConfigHandler { } } + /// Get user name. + /// Return the name of the given peer. If the peer has no name, return the name in the config. + /// + /// # Arguments + /// + /// * `pi` - peer info. pub fn get_username(&self, pi: &PeerInfo) -> String { return if pi.username.is_empty() { self.info.username.clone() @@ -1060,6 +1156,12 @@ impl LoginConfigHandler { }; } + /// Handle peer info. + /// + /// # Arguments + /// + /// * `username` - The name of the peer. + /// * `pi` - The peer info. pub fn handle_peer_info(&mut self, username: String, pi: PeerInfo) { if !pi.version.is_empty() { self.version = hbb_common::get_version_number(&pi.version); @@ -1109,6 +1211,7 @@ impl LoginConfigHandler { serde_json::to_string::>(&x).unwrap_or_default() } + /// Create a [`Message`] for login. fn create_login_msg(&self, password: Vec) -> Message { #[cfg(any(target_os = "android", target_os = "ios"))] let my_id = Config::get_id_or(crate::common::MOBILE_INFO1.lock().unwrap().clone()); @@ -1141,6 +1244,7 @@ impl LoginConfigHandler { } } +/// Media data. pub enum MediaData { VideoFrame(VideoFrame), AudioFrame(AudioFrame), @@ -1150,6 +1254,12 @@ pub enum MediaData { pub type MediaSender = mpsc::Sender; +/// Start video and audio thread. +/// Return two [`MediaSender`], they should be given to the media producer. +/// +/// # Arguments +/// +/// * `video_callback` - The callback for video frame. Being called when a video frame is ready. pub fn start_video_audio_threads(video_callback: F) -> (MediaSender, MediaSender) where F: 'static + FnMut(&[u8]) + Send, @@ -1204,6 +1314,12 @@ where return (video_sender, audio_sender); } +/// Handle latency test. +/// +/// # Arguments +/// +/// * `t` - The latency test message. +/// * `peer` - The peer. pub async fn handle_test_delay(t: TestDelay, peer: &mut Stream) { if !t.from_client { let mut msg_out = Message::new(); @@ -1212,9 +1328,21 @@ pub async fn handle_test_delay(t: TestDelay, peer: &mut Stream) { } } -// mask = buttons << 3 | type -// type, 1: down, 2: up, 3: wheel -// buttons, 1: left, 2: right, 4: middle +/// Send mouse data. +/// +/// # Arguments +/// +/// * `mask` - Mouse event. +/// * mask = buttons << 3 | type +/// * type, 1: down, 2: up, 3: wheel +/// * buttons, 1: left, 2: right, 4: middle +/// * `x` - X coordinate. +/// * `y` - Y coordinate. +/// * `alt` - Whether the alt key is pressed. +/// * `ctrl` - Whether the ctrl key is pressed. +/// * `shift` - Whether the shift key is pressed. +/// * `command` - Whether the command key is pressed. +/// * `interface` - The interface for sending data. #[inline] pub fn send_mouse( mask: i32, @@ -1249,6 +1377,11 @@ pub fn send_mouse( interface.send(Data::Message(msg_out)); } +/// Avtivate OS by sending mouse movement. +/// +/// # Arguments +/// +/// * `interface` - The interface for sending data. fn activate_os(interface: &impl Interface) { send_mouse(0, 0, 0, false, false, false, false, interface); std::thread::sleep(Duration::from_millis(50)); @@ -1267,12 +1400,26 @@ fn activate_os(interface: &impl Interface) { */ } +/// Input the OS's password. +/// +/// # Arguments +/// +/// * `p` - The password. +/// * `avtivate` - Whether to activate OS. +/// * `interface` - The interface for sending data. pub fn input_os_password(p: String, activate: bool, interface: impl Interface) { std::thread::spawn(move || { _input_os_password(p, activate, interface); }); } +/// Input the OS's password. +/// +/// # Arguments +/// +/// * `p` - The password. +/// * `avtivate` - Whether to activate OS. +/// * `interface` - The interface for sending data. fn _input_os_password(p: String, activate: bool, interface: impl Interface) { if activate { activate_os(&interface); @@ -1289,6 +1436,15 @@ fn _input_os_password(p: String, activate: bool, interface: impl Interface) { interface.send(Data::Message(msg_out)); } +/// Handle hash message sent by peer. +/// Hash will be used for login. +/// +/// # Arguments +/// +/// * `lc` - Login config. +/// * `hash` - Hash sent by peer. +/// * `interface` - [`Interface`] for sending data. +/// * `peer` - [`Stream`] for communicating with peer. pub async fn handle_hash( lc: Arc>, hash: Hash, @@ -1312,11 +1468,26 @@ pub async fn handle_hash( lc.write().unwrap().hash = hash; } +/// Send login message to peer. +/// +/// # Arguments +/// +/// * `lc` - Login config. +/// * `password` - Password. +/// * `peer` - [`Stream`] for communicating with peer. async fn send_login(lc: Arc>, password: Vec, peer: &mut Stream) { let msg_out = lc.read().unwrap().create_login_msg(password); allow_err!(peer.send(&msg_out).await); } +/// Handle login request made from ui. +/// +/// # Arguments +/// +/// * `lc` - Login config. +/// * `password` - Password. +/// * `remember` - Whether to remember password. +/// * `peer` - [`Stream`] for communicating with peer. pub async fn handle_login_from_ui( lc: Arc>, password: String, @@ -1335,6 +1506,7 @@ pub async fn handle_login_from_ui( send_login(lc.clone(), hasher2.finalize()[..].into(), peer).await; } +/// Interface for client to send data and commands. #[async_trait] pub trait Interface: Send + Clone + 'static + Sized { fn send(&self, data: Data); @@ -1346,6 +1518,7 @@ pub trait Interface: Send + Clone + 'static + Sized { async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream); } +/// Data used by the client interface. #[derive(Clone)] pub enum Data { Close, @@ -1368,6 +1541,7 @@ pub enum Data { ResumeJob((i32, bool)), } +/// Keycode for key events. #[derive(Clone)] pub enum Key { ControlKey(ControlKey), @@ -1498,6 +1672,13 @@ lazy_static::lazy_static! { ].iter().cloned().collect(); } +/// Check if the given message is an error and can be retried. +/// +/// # Arguments +/// +/// * `msgtype` - The message type. +/// * `title` - The title of the message. +/// * `text` - The text of the message. #[inline] pub fn check_if_retry(msgtype: &str, title: &str, text: &str) -> bool { msgtype == "error" diff --git a/src/client/helper.rs b/src/client/helper.rs index abd20d312..b29930e1c 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -8,8 +8,8 @@ use hbb_common::log; const MAX_LATENCY: i64 = 500; const MIN_LATENCY: i64 = 100; -// based on video frame time, fix audio latency relatively. -// only works on audio, can't fix video latency. +/// Latency controller for syncing audio with the video stream. +/// Only sync the audio to video, not the other way around. #[derive(Debug)] pub struct LatencyController { last_video_remote_ts: i64, // generated on remote deivce @@ -28,21 +28,23 @@ impl Default for LatencyController { } impl LatencyController { + /// Create a new latency controller. pub fn new() -> Arc> { Arc::new(Mutex::new(LatencyController::default())) } - // first, receive new video frame and update time + /// Update the latency controller with the latest video timestamp. pub fn update_video(&mut self, timestamp: i64) { self.last_video_remote_ts = timestamp; self.update_time = Instant::now(); } - // second, compute audio latency - // set MAX and MIN, avoid fixing too frequently. + /// Check if the audio should be played based on the current latency. pub fn check_audio(&mut self, timestamp: i64) -> bool { + // Compute audio latency. let expected = self.update_time.elapsed().as_millis() as i64 + self.last_video_remote_ts; let latency = expected - timestamp; + // Set MAX and MIN, avoid fixing too frequently. if self.allow_audio { if latency.abs() > MAX_LATENCY { log::debug!("LATENCY > {}ms cut off, latency:{}", MAX_LATENCY, latency); diff --git a/src/flutter.rs b/src/flutter.rs index 80dd1f807..e40084450 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -4,8 +4,12 @@ use hbb_common::{ allow_err, compress::decompress, config::{Config, LocalConfig}, - fs, log, - fs::{can_enable_overwrite_detection, new_send_confirm, DigestCheckResult, get_string, transform_windows_path}, + fs, + fs::{ + can_enable_overwrite_detection, get_string, new_send_confirm, transform_windows_path, + DigestCheckResult, + }, + log, message_proto::*, protobuf::Message as _, rendezvous_proto::ConnType, @@ -36,6 +40,12 @@ pub struct Session { } impl Session { + /// Create a new remote session with the given id. + /// + /// # Arguments + /// + /// * `id` - The id of the remote session. + /// * `is_file_transfer` - If the session is used for file transfer. pub fn start(id: &str, is_file_transfer: bool) { LocalConfig::set_remote_id(id); Self::close(); @@ -52,10 +62,16 @@ impl Session { }); } + /// Get the current session instance. pub fn get() -> Arc>> { SESSION.clone() } + /// Get the option of the current session. + /// + /// # Arguments + /// + /// * `name` - The name of the option to get. Currently only `remote_dir` is supported. pub fn get_option(name: &str) -> String { if let Some(session) = SESSION.read().unwrap().as_ref() { if name == "remote_dir" { @@ -66,6 +82,12 @@ impl Session { "".to_owned() } + /// Set the option of the current session. + /// + /// # Arguments + /// + /// * `name` - The name of the option to set. Currently only `remote_dir` is supported. + /// * `value` - The value of the option to set. pub fn set_option(name: String, value: String) { if let Some(session) = SESSION.read().unwrap().as_ref() { let mut value = value; @@ -76,18 +98,25 @@ impl Session { } } + /// Input the OS password. pub fn input_os_password(pass: String, activate: bool) { if let Some(session) = SESSION.read().unwrap().as_ref() { input_os_password(pass, activate, session.clone()); } } + /// Send message to the remote session. + /// + /// # Arguments + /// + /// * `data` - The data to send. See [`Data`] for more details. fn send(data: Data) { if let Some(session) = SESSION.read().unwrap().as_ref() { session.send(data); } } + /// Pop a event from the event queue. pub fn pop_event() -> Option { if let Some(session) = SESSION.read().unwrap().as_ref() { session.events2ui.write().unwrap().pop_front() @@ -96,6 +125,7 @@ impl Session { } } + /// Toggle an option. pub fn toggle_option(name: &str) { if let Some(session) = SESSION.read().unwrap().as_ref() { let msg = session.lc.write().unwrap().toggle_option(name.to_owned()); @@ -105,10 +135,12 @@ impl Session { } } + /// Send a refresh command. pub fn refresh() { Self::send(Data::Message(LoginConfigHandler::refresh())); } + /// Get image quality. pub fn get_image_quality() -> String { if let Some(session) = SESSION.read().unwrap().as_ref() { session.lc.read().unwrap().image_quality.clone() @@ -117,6 +149,7 @@ impl Session { } } + /// Set image quality. pub fn set_image_quality(value: &str) { if let Some(session) = SESSION.read().unwrap().as_ref() { let msg = session @@ -130,6 +163,12 @@ impl Session { } } + /// Get the status of a toggle option. + /// Return `None` if the option is not found. + /// + /// # Arguments + /// + /// * `name` - The name of the option to get. pub fn get_toggle_option(name: &str) -> Option { if let Some(session) = SESSION.read().unwrap().as_ref() { Some(session.lc.write().unwrap().get_toggle_option(name)) @@ -138,15 +177,23 @@ impl Session { } } + /// Login. + /// + /// # Arguments + /// + /// * `password` - The password to login. + /// * `remember` - If the password should be remembered. pub fn login(password: &str, remember: bool) { Session::send(Data::Login((password.to_owned(), remember))); } + /// Close the session. pub fn close() { Session::send(Data::Close); SESSION.write().unwrap().take(); } + /// Reconnect to the current session. pub fn reconnect() { if let Some(session) = SESSION.read().unwrap().as_ref() { if let Some(sender) = session.sender.read().unwrap().as_ref() { @@ -159,6 +206,7 @@ impl Session { } } + /// Get `remember` flag in [`LoginConfigHandler`]. pub fn get_remember() -> bool { if let Some(session) = SESSION.read().unwrap().as_ref() { session.lc.read().unwrap().remember @@ -167,6 +215,11 @@ impl Session { } } + /// Send message over the current session. + /// + /// # Arguments + /// + /// * `msg` - The message to send. #[inline] pub fn send_msg(&self, msg: Message) { if let Some(sender) = self.sender.read().unwrap().as_ref() { @@ -174,6 +227,11 @@ impl Session { } } + /// Send chat message over the current session. + /// + /// # Arguments + /// + /// * `text` - The message to send. pub fn send_chat(text: String) { let mut misc = Misc::new(); misc.set_chat_message(ChatMessage { @@ -185,6 +243,7 @@ impl Session { Self::send_msg_static(msg_out); } + /// Send file over the current session. pub fn send_files( id: i32, path: String, @@ -198,6 +257,7 @@ impl Session { } } + /// Confirm file override. pub fn set_confirm_override_file( id: i32, file_num: i32, @@ -225,6 +285,11 @@ impl Session { } } + /// Static method to send message over the current session. + /// + /// # Arguments + /// + /// * `msg` - The message to send. #[inline] pub fn send_msg_static(msg: Message) { if let Some(session) = SESSION.read().unwrap().as_ref() { @@ -232,6 +297,13 @@ impl Session { } } + /// Push an event to the event queue. + /// An event is stored as json in the event queue. + /// + /// # Arguments + /// + /// * `name` - The name of the event. + /// * `event` - Fields of the event content. fn push_event(&self, name: &str, event: Vec<(&str, &str)>) { let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); assert!(h.get("name").is_none()); @@ -242,11 +314,13 @@ impl Session { }; } + /// Get platform of peer. #[inline] fn peer_platform(&self) -> String { self.lc.read().unwrap().info.platform.clone() } + /// Quick method for sending a ctrl_alt_del command. pub fn ctrl_alt_del() { if let Some(session) = SESSION.read().unwrap().as_ref() { if session.peer_platform() == "Windows" { @@ -259,6 +333,11 @@ impl Session { } } + /// Switch the display. + /// + /// # Arguments + /// + /// * `display` - The display to switch to. pub fn switch_display(display: i32) { let mut misc = Misc::new(); misc.set_switch_display(SwitchDisplay { @@ -270,6 +349,7 @@ impl Session { Self::send_msg_static(msg_out); } + /// Send lock screen command. pub fn lock_screen() { if let Some(session) = SESSION.read().unwrap().as_ref() { let k = Key::ControlKey(ControlKey::LockScreen); @@ -277,6 +357,17 @@ impl Session { } } + /// Send key input command. + /// + /// # Arguments + /// + /// * `name` - The name of the key. + /// * `down` - Whether the key is down or up. + /// * `press` - If the key is simply being pressed(Down+Up). + /// * `alt` - If the alt key is also pressed. + /// * `ctrl` - If the ctrl key is also pressed. + /// * `shift` - If the shift key is also pressed. + /// * `command` - If the command key is also pressed. pub fn input_key( name: &str, down: bool, @@ -299,6 +390,12 @@ impl Session { } } + /// Input a string of text. + /// String is parsed into individual key presses. + /// + /// # Arguments + /// + /// * `value` - The text to input. pub fn input_string(value: &str) { let mut key_event = KeyEvent::new(); key_event.set_seq(value.to_owned()); @@ -499,6 +596,12 @@ struct Connection { } impl Connection { + /// Create a new connection. + /// + /// # Arguments + /// + /// * `session` - The session to create a new connection for. + /// * `is_file_transfer` - Whether the connection is for file transfer. #[tokio::main(flavor = "current_thread")] async fn start(session: Session, is_file_transfer: bool) { let mut last_recv_time = Instant::now(); @@ -591,6 +694,10 @@ impl Connection { } } + /// Handle message from peer. + /// Return false if the connection should be closed. + /// + /// The message is handled by [`Message`], see [`message::Union`] for possible types. async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { if let Ok(msg_in) = Message::parse_from_bytes(&data) { match msg_in.union { @@ -1144,6 +1251,7 @@ impl Connection { } } +/// Parse [`FileDirectory`] to json. pub fn make_fd_to_json(fd: FileDirectory) -> String { use serde_json::json; let mut fd_json = serde_json::Map::new(); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 2e62bdf69..98fac8242 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -47,6 +47,13 @@ pub fn start_rgba_stream(s: StreamSink>>) -> ResultType<( Ok(()) } +/// FFI for **get** commands which are idempotent. +/// Return result in c string. +/// +/// # Arguments +/// +/// * `name` - name of the command +/// * `arg` - argument of the command #[no_mangle] unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *const c_char { let mut res = "".to_owned(); @@ -174,6 +181,12 @@ unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *co CString::from_vec_unchecked(res.into_bytes()).into_raw() } +/// FFI for **set** commands which are not idempotent. +/// +/// # Arguments +/// +/// * `name` - name of the command +/// * `arg` - argument of the command #[no_mangle] unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { let value: &CStr = CStr::from_ptr(value);