Merge pull request #635 from SoLongAndThanksForAllThePizza/flutter_desktop

Added comments and connection page
This commit is contained in:
RustDesk 2022-05-28 09:34:00 +08:00 committed by GitHub
commit f56dcc9e6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 365 additions and 11 deletions

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/mobile/pages/connection_page.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:provider/provider.dart';
@ -42,7 +43,7 @@ class _DesktopHomePageState extends State<DesktopHomePage> {
buildServerBoard(BuildContext context) {
return Center(
child: Text("waiting implementation"),
child: ConnectionPage(key: null),
);
}

View File

@ -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<ConnectionPage> {
/// 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<ConnectionPage> {
);
}
/// 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<ConnectionPage> {
}
}
/// 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<ConnectionPage> {
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<ConnectionPage> {
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<ConnectionPage> {
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<ConnectionPage> {
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,

View File

@ -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<String, dynamic>) 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<String, dynamic> 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<String, dynamic> 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<String, String> modify(Map<String, String> 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<Peer> 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(': ');

View File

@ -22,6 +22,8 @@ class RgbaFrame extends Struct {
typedef F2 = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>);
typedef F3 = void Function(Pointer<Utf8>, Pointer<Utf8>);
/// FFI wrapper around the native Rust core.
/// Hides the platform differences.
class PlatformFFI {
static Pointer<RgbaFrame>? _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<Null> 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()) {

View File

@ -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<u8>,
@ -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<f32>)>,
@ -522,6 +530,7 @@ pub struct AudioHandler {
}
impl AudioHandler {
/// Create a new audio handler.
pub fn new(latency_controller: Arc<Mutex<LatencyController>>) -> 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<T: cpal::Sample>(
&mut self,
@ -708,6 +723,7 @@ impl AudioHandler {
}
}
/// Video handler for the [`Client`].
pub struct VideoHandler {
decoder: Decoder,
latency_controller: Arc<Mutex<LatencyController>>,
@ -715,6 +731,7 @@ pub struct VideoHandler {
}
impl VideoHandler {
/// Create a new video handler.
pub fn new(latency_controller: Arc<Mutex<LatencyController>>) -> 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<bool> {
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<bool> {
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<Message> {
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<OptionMessage> {
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<ImageQuality> {
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<Message> {
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::<HashMap<String, String>>(&x).unwrap_or_default()
}
/// Create a [`Message`] for login.
fn create_login_msg(&self, password: Vec<u8>) -> 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<MediaData>;
/// 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<F>(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<RwLock<LoginConfigHandler>>,
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<RwLock<LoginConfigHandler>>, password: Vec<u8>, 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<RwLock<LoginConfigHandler>>,
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"

View File

@ -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<Mutex<LatencyController>> {
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);

View File

@ -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<RwLock<Option<Session>>> {
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<String> {
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<bool> {
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();

View File

@ -47,6 +47,13 @@ pub fn start_rgba_stream(s: StreamSink<ZeroCopyBuffer<Vec<u8>>>) -> 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);